diff --git a/Product Requirements Document - Quacko Mode.pdf b/Product Requirements Document - Quacko Mode.pdf new file mode 100644 index 00000000..997ef05d Binary files /dev/null and b/Product Requirements Document - Quacko Mode.pdf differ diff --git a/Submission for PDR.ipynb b/Submission for PDR.ipynb new file mode 100644 index 00000000..5d188f36 --- /dev/null +++ b/Submission for PDR.ipynb @@ -0,0 +1,1171 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "id": "b1f70ff2-492a-41a4-97db-5da6d5775cb7", + "metadata": {}, + "outputs": [], + "source": [ + "# SPDX-License-Identifier: Apache-2.0 AND CC-BY-NC-4.0\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "eaa3992e-c095-4bd7-9f14-60abd0740b64", + "metadata": {}, + "source": [ + "# Quantum Enhanced Optimization for Radar and Communications Applications \n", + "\n", + "\n", + "The Low Autocorrelation Binary Sequences (LABS) is an important and challenging optimization problem with applications related to radar, telecommunications, and other signal related applications. This CUDA-Q Academic module will focus on a clever quantum-enhanced hybrid method developed in a collaboration between Kipu Quantum, University of the Basque Country EHU, and NVIDIA for solving the LABS problem. (This notebook was jointly developed with input from all collaborators.)\n", + "\n", + "Other CUDA-Q Academic modules like [Divide and Conquer MaxCut QAOA](https://github.com/NVIDIA/cuda-q-academic/tree/main/qaoa-for-max-cut) and [Quantum Finance](https://github.com/NVIDIA/cuda-q-academic/blob/main/quantum-applications-to-finance/03_qchop.ipynb), demonstrate how quantum computing can be used outright to solve optimization problems. This notebook demonstrates a slightly different approach. Rather than considering QPUs as the tool to produce the final answer, it demonstrates how quantum can be used to enhance the effectiveness of leading classical methods. \n", + "\n", + "The benefits of such an approach were highlighted in [Scaling advantage with quantum-enhanced memetic tabu search for LABS](https://arxiv.org/html/2511.04553v1). This notebook, co-created with the authors of the paper, will allow you to explore the findings of their research and write your own CUDA-Q code that builds a representative quantum-enhanced workflow for solving the LABS problem. Moreover, it will introduce advancements in counteradiabatic optimization techniques on which reduce the quantum resources required to run on a QPU.\n", + "\n", + "**Prerequisites:** This lab assumes you have a basic knowledge of quantum computing, including operators, gates, etc. For a refresher on some of these topics, explore the [Quick start to Quantum](https://github.com/NVIDIA/cuda-q-academic/tree/main/quick-start-to-quantum) series.\n", + "\n", + "**In this lab you will:**\n", + "* 1. Understand the LABS problem and its relation ot radar and communication applications.\n", + "* 2. Solve LABS classically with memetic tabu search and learn about the limitations of such methods.\n", + "* 3. Code a couteradiabatic algorithm using CUDA-Q to produce approximate solutions to the LABS problem.\n", + "* 4. Use the CUDA-Q results to seed your tabu search and understand the potential benefits of this approach.\n", + "\n", + "\n", + "**Terminology you will use:**\n", + "* Low autocorrelation of binary sequences (LABS)\n", + "* counteradiabatic optimization\n", + "* memetic-tabu search\n", + "\n", + "**CUDA-Q Syntax you will use:**\n", + "* cudaq.sample()\n", + "* @cudaq.kernel\n", + "* ry(), rx(), rz(), x(), h() \n", + "* x.ctrl()\n", + "\n", + "Run the code below to initialize the libraries you will need." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "cfc42c0d-0de8-4054-8d29-09f70080487d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: cudaq in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (0.13.0)\n", + "Requirement already satisfied: cuda-quantum-cu13==0.13.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cudaq) (0.13.0)\n", + "Requirement already satisfied: astpretty~=3.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (3.0.0)\n", + "Requirement already satisfied: custatevec-cu13~=1.10 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (1.12.0)\n", + "Requirement already satisfied: cutensornet-cu13~=2.9 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (2.11.0)\n", + "Requirement already satisfied: cudensitymat-cu13~=0.3 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (0.4.0)\n", + "Requirement already satisfied: numpy>=1.24 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (2.4.1)\n", + "Requirement already satisfied: scipy>=1.10.1 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (1.17.0)\n", + "Requirement already satisfied: requests>=2.31 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (2.32.5)\n", + "Requirement already satisfied: nvidia-cublas~=13.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (13.2.1.1)\n", + "Requirement already satisfied: nvidia-curand~=10.4 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (10.4.1.81)\n", + "Requirement already satisfied: nvidia-cusparse~=12.6 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (12.7.3.1)\n", + "Requirement already satisfied: nvidia-cuda-runtime~=13.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (13.1.80)\n", + "Requirement already satisfied: nvidia-cusolver~=12.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (12.0.9.81)\n", + "Requirement already satisfied: nvidia-cuda-nvrtc~=13.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (13.1.115)\n", + "Requirement already satisfied: cupy-cuda13x~=13.6.0 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cuda-quantum-cu13==0.13.0->cudaq) (13.6.0)\n", + "Requirement already satisfied: cutensor-cu13<3,>=2.3.1 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cudensitymat-cu13~=0.3->cuda-quantum-cu13==0.13.0->cudaq) (2.5.0)\n", + "Requirement already satisfied: fastrlock>=0.5 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from cupy-cuda13x~=13.6.0->cuda-quantum-cu13==0.13.0->cudaq) (0.8.3)\n", + "Requirement already satisfied: nvidia-nvjitlink in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from nvidia-cusolver~=12.0->cuda-quantum-cu13==0.13.0->cudaq) (13.1.115)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from requests>=2.31->cuda-quantum-cu13==0.13.0->cudaq) (3.4.4)\n", + "Requirement already satisfied: idna<4,>=2.5 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from requests>=2.31->cuda-quantum-cu13==0.13.0->cudaq) (3.11)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from requests>=2.31->cuda-quantum-cu13==0.13.0->cudaq) (2.6.3)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/jovyan/.qbraid/environments/zogs/pyenv/lib/python3.11/site-packages (from requests>=2.31->cuda-quantum-cu13==0.13.0->cudaq) (2026.1.4)\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.3\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49m/home/jovyan/.qbraid/environments/zogs/pyenv/bin/python -m pip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install cudaq" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bfc407dd-113d-485c-88db-7ddbad344ead", + "metadata": {}, + "outputs": [], + "source": [ + "import cudaq\n", + "import numpy as np\n", + "from math import floor\n", + "import auxiliary_files.labs_utils as utils" + ] + }, + { + "cell_type": "markdown", + "id": "e1e821a8-47b4-4e5b-a713-4e1babada01f", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## The LABS problem and applications\n", + "\n", + "The **Low Autocorrelation Binary Sequences (LABS)** problem is fundamental to many applications, but originated with applications to radar. \n", + "\n", + "Consider a radar that monitors airport traffic. The radar signal sent to detect incoming planes must have as much range as possible to ensure safe approaches are planned well in advance. The range of a radar signal can be increased by sending a longer pulse. However, in order to differentiate between multiple objects, pulses need to be short to provide high resolution. So, how do you handle situations where you need both?\n", + "\n", + "One solution is a technique called pulse compression. The idea is to send a long signal, but vary the phase at regular intervals such that the resolution is increased. Generally, the initial signal will encode a binary sequence of phase shifts, where each interval corresponds to a signal with a 0 or 180 degree phase shift. \n", + "\n", + "The tricky part is selecting an optimal encoding sequence. When the signal returns, it is fed into a matched filter with the hope that a singular sharp peak will appear, indicating clear detection. The autocorrelation of the original signal, or how similar the signal is to itself, determines if a single peak or a messier signal with sidelobes will be detected. A signal should have high autocorrelation when overlayed on top of itself, but low autocorrelation when shifted with a lag. \n", + "\n", + "Consider the image below. The signal on the left has a crisp single peak while the single on the right produces many undesirable sidelobes which can inhibit clear detection. \n", + "\n", + "\n", + "\n", + "\n", + "So, how do you select a good signal? This is where LABS comes in, defining these questions as a binary optimization problem. Given a binary sequence of length $N$, $(s_1 \\cdots s_N) \\in {\\pm 1}^N$, the goal is to minimize the following objective function.\n", + "\n", + "$$ E(s) = \\sum_{k=1}^{N-1} C_k^2 $$\n", + "\n", + "Where $C_k$ is defined as. \n", + "\n", + " $$C_k= \\sum_{i=1}^{N-k} s_is_{i+k}$$\n", + "\n", + "\n", + "So, each $C_k$ computes how similar the original signal is to the shifted one for each offset value $k$. To explore this more, try the interactive widget linked [here](https://nvidia.github.io/cuda-q-academic/interactive_widgets/labs_visualization.html). See if you can select a very good and very poor sequence and see the difference for the case of $N=7$." + ] + }, + { + "cell_type": "markdown", + "id": "84fa6dff-0fee-4251-a006-76d6ad426116", + "metadata": {}, + "source": [ + "## Classical Solution of the LABS problem\n", + "\n", + "The LABS problem is tricky to solve for a few reasons. First, the configuration space grows exponentially. Second, underlying symmetries of the problem result in many degeneracies in the optimization landscape severely inhibiting local search methods. \n", + "\n", + "
\n", + "

Exercise 1:

\n", + "

\n", + "Using the widget above, try to find some of the symmetries for the LABS problem. That is, for a fixed bitstring length, can you find patterns to produce the same energy with different pulse patterns. \n", + "

\n", + "\n", + "The best known performance for a classical optimization technique is Memetic Tabu search (MTS) which exhibits a scaling of $O(1.34^N)$. The MTS algorithm is depicted below. It begins with a randomly selected population of bitstrings and finds the best solution from them. Then, a child is selected by sampling directly from or combining multiple bitstrings from the population. The child is mutated with probability $p_{mutate}$ and then input to a tabu search, which performs a modified greedy local search starting from the child bitstring. If the result is better than the best in the population, it is updated as the new leader and randomly replaces a bitstring in the population.\n", + "\n", + "\n", + "\n", + "\n", + "Such an approach is fast, parallelizable, and allows for exploration with improved searching of the solution landscape. \n", + "\n", + "
\n", + "

Exercise 2:

\n", + "

\n", + "Before exploring any quantum approach, get a sense for how MTS works by coding it yourself based generally on the figure above. Algorithms for the combine and mutate steps are provided below as used in the paper. You may need to research more specific details of the process, especially for how tabu search is performed. The MTS procedure should output and optimal bitstring and its energy. Also, write a function to visualize the results including the energy distribution of the final population.\n", + "

\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "341c9a3f-565c-49ab-9903-0aa9a508722c", + "metadata": {}, + "outputs": [], + "source": [ + "#TODO - Write code to perform MTS\n", + "\n", + "import cudaq\n", + "import numpy as np\n", + "from math import floor\n", + "import auxiliary_files.labs_utils as utils\n", + "\n", + "\n", + "import random \n", + "\n", + "#creates bitstrings of length N\n", + "def bitstring(N):\n", + " string = []\n", + " for bit in range(N):\n", + " string.append(random.choice([0,1])) \n", + " return string\n", + "\n", + "\n", + "\n", + "#creates s_max bitstrings of lenth N\n", + "def populate(s_max,N):\n", + " pop = []\n", + " for i in range(s_max):\n", + " pop.append(bitstring(N))\n", + " return pop\n", + "\n", + "\n", + "\n", + "\n", + "#makes a array from 0 to N\n", + "def cut(N):\n", + " k_list = []\n", + " for bit in range(N):\n", + " k_list.append(bit) \n", + " return k_list\n", + "\n", + "\n", + "\n", + "\n", + "#cuts two parent bitstrings p1 and p2 of length N at index k and combines the halves into a new bitstring child of length N\n", + "def combine(p1,p2):\n", + " if len(p1)==len(p2):\n", + " k = random.choice(cut(len(p1)))\n", + " child = p1[0:k]\n", + " for element in p2[k:len(p2)]:\n", + " child.append(element)\n", + " return child\n", + "\n", + "\n", + "\n", + "#pm probability of flippinng each bit in a bitstring s \n", + "def mutate(s,pm):\n", + " for i in range(len(s)):\n", + " if random.random()-1, 1->+1\n", + " N = len(s)\n", + " E = 0\n", + " for k in range(1, N):\n", + " ck = int(np.dot(s[:N-k], s[k:])) #dot product\n", + " E += ck * ck\n", + " return E\n", + "\n", + "\n", + "def MTS(popsize,stringsize,pc,target):\n", + " pop = populate(popsize,stringsize) #creates a random list of strings \n", + " if random.random()target:\n", + " #repeats child selection, mutation, and tabu search until target energy is reached\n", + " if random.random()\n", + "\n", + "\n", + "\n", + "\n", + "An operator called the adiabatic gauge potential $A_{\\lambda}$ is the ideal choice for the CD term as it suppresses all possible diabatic transitions, resulting in the following total system to evolve.\n", + "\n", + "$$ H(\\lambda) = H_{ad}(\\lambda) + \\lambda H_{CD} (\\lambda) $$\n", + "\n", + "$A(\\lambda)$ is derrived from $H_{ad}(\\lambda)$ (see paper for details) as it contains information about underlying physics of the problem. \n", + "\n", + "There is a problem though. The $A(\\lambda)$ term cannot be efficiently expressed exactly and needs to be approximated. It also turns out that in the so called impulse regime, where the adiabatic evolution is very fast, $H_{cd} (\\lambda)$ dominates $H_{ad}(\\lambda)$, meaning that the final implementation corresponds to the operator $H(\\lambda) = H^1_{cd}(\\lambda)$ where $H^1_{cd}(\\lambda)$ is a first order approximation of $A(\\lambda)$ (see equation 7 in the paper).\n", + "\n", + "A final step is to use Trotterization to define the quantum circuit to apply $e^{-\\theta (t) i H_{cd}}$. The details for this derivation are shown in the appendix of the paper. and result from equation B3 is shown below. \n", + "\n", + "$$\n", + "\\begin{equation}\n", + "\\begin{aligned}\n", + "U(0, T) = \\prod_{n=1}^{n_{\\text{trot}}} & \\left[ \\prod_{i=1}^{N-2} \\prod_{k=1}^{\\lfloor \\frac{N-i}{2} \\rfloor} R_{Y_i Z_{i+k}}\\big(4\\theta(n\\Delta t)h_i^x\\big) R_{Z_i Y_{i+k}}\\big(4\\theta(n\\Delta t)h_{i+k}^x\\big) \\right] \\\\\n", + "& \\times \\prod_{i=1}^{N-3} \\prod_{t=1}^{\\lfloor \\frac{N-i-1}{2} \\rfloor} \\prod_{k=t+1}^{N-i-t} \\bigg( R_{Y_i Z_{i+t} Z_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_i^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Y_{i+t} Z_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+t}^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Z_{i+t} Y_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+k}^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Z_{i+t} Z_{i+k} Y_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+k+t}^x\\big) \\bigg)\n", + "\\end{aligned}\n", + "\\end{equation}\n", + "$$\n", + "\n", + "It turns out that this implementation is more efficient than QAOA in terms of gate count. The authors calculated that for $N=67$, QAOA would require 1.4 million entangling gates while the CD approach derived here requires only 236 thousand entangling gates.\n", + "\n", + "\n", + "
\n", + "

Exercise 3:

\n", + "

\n", + "At first glance, this equation might looks quite complicated. However, observe the structure and note two \"blocks\" of terms. Can you spot them? \n", + "\n", + "They are 2 qubit terms that look like $R_{YZ}(\\theta)$ or $R_{ZY}(\\theta)$.\n", + "\n", + "As well as 4 qubit terms that look like $R_{YZZZ}(\\theta)$, $R_{ZYZZ}(\\theta)$, $R_{ZZYZ}(\\theta)$, or $R_{ZZZY}(\\theta)$.\n", + "\n", + "Thankfully the authors derive a pair of circuit implementations for the two and four qubit terms respectively, shown in the figures below.\n", + "\n", + "Using CUDA-Q, write a kernel for each which will be used later to construct the full implementation.\n", + "\n", + "* Hint: Remember that the adjoint of a rotation gate is the same as rotating in the opposite direction. \n", + "\n", + "* Hint: You may also want to define a CUDA-Q kernel for an R$_{ZZ}$ gate.\n", + "\n", + "* Hint: Implementing a circuit from a paper is a great place where AI can help accelerate your work. If you have access to a coding assistant, feel free to use it here.\n", + "

\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c91bbaae-62a1-41e7-a285-af885627a942", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ╭────────────╮╭───╮ ╭────────────╮╭───╮╭───────────╮ ╭───╮ »\n", + "q0 : ┤ rx(-1.571) ├┤ h ├──●──┤ rz(-1.571) ├┤ x ├┤ rx(1.571) ├─────┤ h ├─────»\n", + " ├───────────┬╯├───┤╭─┴─╮├────────────┤╰─┬─╯├───────────┴╮╭───┴───┴────╮»\n", + "q1 : ┤ ry(1.571) ├─┤ h ├┤ x ├┤ rz(-1.571) ├──●──┤ ry(-1.571) ├┤ rx(-1.571) ├»\n", + " ├───────────┴╮├───┤╰───╯├────────────┤ ├───────────┬╯├───────────┬╯»\n", + "q2 : ┤ ry(-1.571) ├┤ h ├──●──┤ rz(-1.571) ├──●──┤ ry(1.571) ├─┤ rx(1.571) ├─»\n", + " ╰───┬───┬────╯╰───╯╭─┴─╮├────────────┤╭─┴─╮├───────────┴╮╰───┬───┬───╯ »\n", + "q3 : ────┤ h ├──────────┤ x ├┤ rz(-1.571) ├┤ x ├┤ rx(-1.571) ├────┤ h ├─────»\n", + " ╰───╯ ╰───╯╰────────────╯╰───╯╰────────────╯ ╰───╯ »\n", + "\n", + "################################################################################\n", + "\n", + " ╭───────────╮╭───╮»\n", + "─────────────────────────────────────────────────────────●──┤ rz(1.571) ├┤ x ├»\n", + "╭───╮ ╭───────╮╭───╮╭───────────╮╭───────────╮╭───╮╭─┴─╮├───────────┤╰─┬─╯»\n", + "┤ h ├──●──┤ rz(0) ├┤ x ├┤ rx(1.571) ├┤ ry(1.571) ├┤ h ├┤ x ├┤ rz(1.571) ├──●──»\n", + "├───┤╭─┴─╮├───────┤╰─┬─╯├───────────┤╰───┬───┬───╯╰───╯╰───╯╰───────────╯ »\n", + "┤ h ├┤ x ├┤ rz(0) ├──●──┤ rx(3.142) ├────┤ h ├────────────────────────────────»\n", + "╰───╯╰───╯╰───────╯ ╰───────────╯ ╰───╯ »\n", + "──────────────────────────────────────────────────────────────────────────────»\n", + " »\n", + "\n", + "################################################################################\n", + "\n", + "╭───────────╮ ╭───╮ »\n", + "┤ rx(1.571) ├─┤ h ├─────────────────────────────────────────────────────────»\n", + "├───────────┴╮├───┤ ╭───────╮╭───╮╭────────────╮╭───╮ »\n", + "┤ ry(-1.571) ├┤ h ├──●──┤ rz(0) ├┤ x ├┤ rx(-3.142) ├┤ h ├───────────────────»\n", + "╰────────────╯╰───╯╭─┴─╮├───────┤╰─┬─╯├───────────┬╯├───┤ ╭────────────╮»\n", + "───────────────────┤ x ├┤ rz(0) ├──●──┤ ry(1.571) ├─┤ h ├──●──┤ rz(-1.571) ├»\n", + " ╰───╯╰───────╯ ╰───────────╯ ╰───╯╭─┴─╮├────────────┤»\n", + "─────────────────────────────────────────────────────────┤ x ├┤ rz(-1.571) ├»\n", + " ╰───╯╰────────────╯»\n", + "\n", + "################################################################################\n", + "\n", + " »\n", + "──────────────────────────────────────────────────────────────────────»\n", + " ╭───────╮╭───╮╭───────────╮»\n", + "────────────────────────────────────────●──┤ rz(0) ├┤ x ├┤ rx(1.571) ├»\n", + "╭───╮╭────────────╮╭────────────╮╭───╮╭─┴─╮├───────┤╰─┬─╯├───────────┤»\n", + "┤ x ├┤ ry(-1.571) ├┤ rx(-1.571) ├┤ h ├┤ x ├┤ rz(0) ├──●──┤ rx(1.571) ├»\n", + "╰─┬─╯├────────────┤╰───┬───┬────╯╰───╯╰───╯╰───────╯ ╰───────────╯»\n", + "──●──┤ rx(-1.571) ├────┤ h ├──────────────────────────────────────────»\n", + " ╰────────────╯ ╰───╯ »\n", + "\n", + "################################################################################\n", + "\n", + " ╭───────────╮╭───╮ \n", + "─────────────────────●──┤ rz(1.571) ├┤ x ├──────────────\n", + "╭────────────╮╭───╮╭─┴─╮├───────────┤╰─┬─╯╭───────────╮ \n", + "┤ ry(-1.571) ├┤ h ├┤ x ├┤ rz(1.571) ├──●──┤ ry(1.571) ├─\n", + "├───────────┬╯├───┤╰───╯├───────────┤╭───╮├───────────┴╮\n", + "┤ ry(1.571) ├─┤ h ├──●──┤ rz(1.571) ├┤ x ├┤ ry(-1.571) ├\n", + "╰───────────╯ ╰───╯╭─┴─╮├───────────┤╰─┬─╯├───────────┬╯\n", + "───────────────────┤ x ├┤ rz(1.571) ├──●──┤ rx(1.571) ├─\n", + " ╰───╯╰───────────╯ ╰───────────╯ \n", + "\n" + ] + } + ], + "source": [ + "# the following code creates the circuit highlighted in excercise 3, which models the U(0,T) trotterizer function\n", + "# this function can be subdivided into two parts, into two qubit and four qubit rotations\n", + "# the two qubit block rotated around RYZ(theta) RZY(theta) \n", + "# this requires 2 entangling gates (4 gates each) and 4 single qubit gates\n", + "# the second block is the quad-qubit rotational block, rotated around RYZZZ(theta),RZYZZ(Theta),RZZYZ(theta\n", + "# and RZZZY(theta). \n", + "# the circuit breaks down the technique in quantum computing used to simulate the time evolution of a quantum system. \n", + "# It breaks down complex, large-scale Hamiltonian simulations into a series of smaller, manageable steps\n", + "\n", + "import cudaq\n", + "import numpy as np\n", + "import math as mat\n", + "\n", + "theta = 0 #in degrees, change this var after finding in part 4\n", + "pie_2 = mat.pi / 2\n", + "pie = mat.pi\n", + "\n", + "@cudaq.kernel\n", + "def dual_qubit_rotation():\n", + " q = cudaq.qvector(2)\n", + " rx(pie_2, q[1])\n", + " h(q[1])\n", + " h(q[0])\n", + " x.ctrl(q[0], q[1])\n", + " rz(theta, q[0])\n", + " rz(theta, q[1])\n", + " x.ctrl(q[1], q[0])\n", + " rx(pie_2, q[0])\n", + " rx(pie_2, q[1])\n", + " rx(pie_2, q[0])\n", + "\n", + "@cudaq.kernel\n", + "def quad_qubit_rotation():\n", + " q = cudaq.qvector(4)\n", + " ry(-pie_2, q[2])\n", + " ry(pie_2, q[1])\n", + " h(q[3])\n", + " rx(-pie_2, q[0])\n", + " h(q[2])\n", + " h(q[1])\n", + " h(q[0])\n", + " x.ctrl(q[0], q[1])\n", + " x.ctrl(q[2], q[3])\n", + " rz(-pie_2, q[0])\n", + " rz(-pie_2, q[1])\n", + " rz(-pie_2, q[2])\n", + " rz(-pie_2, q[3])\n", + " x.ctrl(q[1], q[0])\n", + " x.ctrl(q[2], q[3])\n", + " rx(pie_2, q[0])\n", + " rx(-pie_2, q[3])\n", + " ry(pie_2, q[2])\n", + " ry(-pie_2, q[1])\n", + " rx(-pie_2, q[1])\n", + " rx(pie_2, q[2])\n", + " h(q[1])\n", + " h(q[2])\n", + " x.ctrl(q[1], q[2])\n", + " rz(theta, q[1])\n", + " rz(theta, q[2])\n", + " x.ctrl(q[2], q[1])\n", + " rx(pie_2, q[1])\n", + " rx(pie, q[2])\n", + " ry(pie_2, q[1])\n", + " h(q[0])\n", + " h(q[1])\n", + " x.ctrl(q[0], q[1])\n", + " rz(pie_2, q[0])\n", + " rz(pie_2, q[1])\n", + " x.ctrl(q[1], q[0])\n", + " rx(pie_2, q[0])\n", + " ry(-pie_2, q[1])\n", + " h(q[1])\n", + " h(q[2])\n", + " x.ctrl(q[1], q[2])\n", + " rz(theta, q[1])\n", + " rz(theta, q[2])\n", + " x.ctrl(q[2], q[1])\n", + " rx(-pie, q[1])\n", + " ry(pie_2, q[2])\n", + " h(q[2])\n", + " h(q[3])\n", + " x.ctrl(q[2], q[3])\n", + " rz(-pie_2, q[2])\n", + " rz(-pie_2, q[3])\n", + " x.ctrl(q[3], q[2])\n", + " ry(-pie_2, q[2])\n", + " rx(-pie_2, q[3])\n", + " rx(-pie_2, q[2])\n", + " h(q[1])\n", + " h(q[2])\n", + " x.ctrl(q[1], q[2])\n", + " rz(theta, q[1])\n", + " rz(theta, q[2])\n", + " x.ctrl(q[2], q[1])\n", + " rx(pie_2, q[1])\n", + " rx(pie_2, q[2])\n", + " ry(-pie_2, q[1])\n", + " ry(pie_2, q[2])\n", + " h(q[0])\n", + " h(q[1])\n", + " h(q[2])\n", + " h(q[3])\n", + " x.ctrl(q[0], q[1])\n", + " x.ctrl(q[2], q[3])\n", + " rz(pie_2, q[0])\n", + " rz(pie_2, q[1])\n", + " rz(pie_2, q[2])\n", + " rz(pie_2, q[3])\n", + " x.ctrl(q[1], q[0])\n", + " x.ctrl(q[3], q[2])\n", + " ry(pie_2, q[1])\n", + " ry(-pie_2, q[2])\n", + " rx(pie_2, q[3])\n", + "\n", + "print(cudaq.draw(quad_qubit_rotation))\n" + ] + }, + { + "cell_type": "markdown", + "id": "f113f324-6f58-4b5e-ab93-339d70f88c46", + "metadata": {}, + "source": [ + "There are a few additional items we need to consider before completing the final implementation of the entire circuit. One simplification we can make is that for our problem the $h_i^x$ terms are all 1 and any $h_b^x$ terms are 0, and are only there for generalizations of this model. \n", + "\n", + "The remaining challenge is derivation of the angles that are used to apply each of the circuits you defined above. These are obtained from two terms $\\lambda(t)$ and $\\alpha(t)$. \n", + "\n", + "\n", + "The $\\lambda(t)$ defines an annealing schedule and is generally a Sin function which slowly \"turns on\" the problem Hamiltonian. For computing our angles, we need the derivative of $\\lambda(t)$.\n", + "\n", + "The $\\alpha$ term is a bit trickier and is the solution to a set of differential equations which minimize the distance between $H^1_{CD}(\\lambda)$ and $A(\\lambda)$. The result is \n", + "\n", + "$$\\alpha(t) = \\frac{-\\Gamma_1(t)}{\\Gamma_2(t)} $$\n", + "\n", + "Where $\\Gamma_1(t)$ and $\\Gamma_2(t)$ are defined in equations 16 and 17 of the paper and essentially depend on the structure of the optimization problem. Curious learners can look at the functions in `labs_utils.py` to see how these are computed, based on the problem size and specific time step in the Trotter process. \n", + "\n", + "\n", + "
\n", + "

Exercise 4:

\n", + "

\n", + "The `compute_theta` function, called in the following cells, requires all indices of the two and four body terms. These will be used again in our main kernel to apply the respective gates. Use the products in the formula below to finish the function in the cell below. Save them as `G2` and `G4` where each is a list of lists of indices defining the two and four term interactions. As you are translating an equation to a set of loops, this is a great opportunity to use an AI coding assistant.\n", + "\n", + "$$\n", + "\\begin{equation}\n", + "\\begin{aligned}\n", + "U(0, T) = \\prod_{n=1}^{n_{\\text{trot}}} & \\left[ \\prod_{i=1}^{N-2} \\prod_{k=1}^{\\lfloor \\frac{N-i}{2} \\rfloor} R_{Y_i Z_{i+k}}\\big(4\\theta(n\\Delta t)h_i^x\\big) R_{Z_i Y_{i+k}}\\big(4\\theta(n\\Delta t)h_{i+k}^x\\big) \\right] \\\\\n", + "& \\times \\prod_{i=1}^{N-3} \\prod_{t=1}^{\\lfloor \\frac{N-i-1}{2} \\rfloor} \\prod_{k=t+1}^{N-i-t} \\bigg( R_{Y_i Z_{i+t} Z_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_i^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Y_{i+t} Z_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+t}^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Z_{i+t} Y_{i+k} Z_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+k}^x\\big) \\\\\n", + "& \\quad \\times R_{Z_i Z_{i+t} Z_{i+k} Y_{i+k+t}}\\big(8\\theta(n\\Delta t)h_{i+k+t}^x\\big) \\bigg)\n", + "\\end{aligned}\n", + "\\end{equation}\n", + "$$\n", + "\n", + "

\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "abf34168-42f9-4dbf-86e1-99976232ad7e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([(1, 2), (2, 3)], [(1, 3, 2, 4), (1, 4, 2, 5)])" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "\n", + "def two_body_pairs(N):\n", + " # two-body terms\n", + " \n", + " for i in range(1, N - 2):\n", + " max_k = (N - i)//2\n", + " \n", + " for k in range(1, max_k):\n", + " j = i + k\n", + " yield(i,j)\n", + "\n", + "\n", + "def four_body_pairs(N):\n", + " # four-body terms\n", + "\n", + " for i in range(1, N - 3):\n", + " max_t = (N - i - 1)//2\n", + "\n", + " for t in range(1, max_t):\n", + "\n", + " for k in range(t+1, N - i - t):\n", + " j = i + k\n", + " l = i + t\n", + " m = i + k + t\n", + " yield(i, j, l, m)\n", + "\n", + "\n", + "def get_interactions(N):\n", + " G2 = []\n", + " G4 = []\n", + " for i,j in two_body_pairs(N):\n", + " G2.append((i,j))\n", + "\n", + " for i,j,l,m in four_body_pairs(N):\n", + " G4.append((i,j,l,m))\n", + " \n", + " return G2, G4\n", + "\n", + "\n", + "\n", + "get_interactions(6)" + ] + }, + { + "cell_type": "markdown", + "id": "3450f200-b191-41f5-88d2-974052ee45ac", + "metadata": {}, + "source": [ + "\n", + "\n", + "
\n", + "

Exercise 5:

\n", + "

\n", + "You are now ready to construct the entire circuit and run the counteradiabatic optimization procedure. The final kernel needs to apply Equation 15 for a specified total evolution time $T$ and the `n_steps` number of Trotter steps. It must also take as input, the indices for the two and four body terms and the thetas to be applied each step, as these cannot be computed within a CUDA-Q kernel.\n", + "\n", + "The helper function `compute_theta` computes the theta parameters for you, using a few additional functions in accordance with the equations defined in the paper.\n", + "

\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "8c2e6d7d-03b2-482f-bc36-d572e6d4855a", + "metadata": {}, + "outputs": [], + "source": [ + "import cudaq\n", + "import numpy as np\n", + "from math import pi\n", + "\n", + "# RZZ gate implementation (needed for both blocks)\n", + "@cudaq.kernel\n", + "def rzz_gate(q0: cudaq.qubit, q1: cudaq.qubit, theta: float):\n", + " \"\"\"\n", + " Implements e^(-i*theta*Z⊗Z/2) using:\n", + " RZZ(θ) = CNOT(q0,q1) · RZ(θ,q1) · CNOT(q0,q1)\n", + " \"\"\"\n", + " x.ctrl(q0, q1)\n", + " rz(theta, q1)\n", + " x.ctrl(q0, q1)\n", + "\n", + "# Two-qubit rotation block R_YZ(θ) R_ZY(θ)\n", + "@cudaq.kernel\n", + "def apply_two_body_block(qubits: cudaq.qview, i: int, j: int, theta: float):\n", + " \"\"\"\n", + " Apply the 2-qubit block from Figure 3 in the paper.\n", + " This implements R_YiZj(θ) R_ZiYj(θ)\n", + " \n", + " Args:\n", + " qubits: quantum register\n", + " i, j: qubit indices (i < j)\n", + " theta: rotation angle (will be 4*θ(nΔt) in the main circuit)\n", + " \"\"\"\n", + " # First block: R_YZ(theta)\n", + " rx(pi/2, qubits[i]) # Basis change Y -> Z\n", + " h(qubits[j]) # Basis change Z -> X for CNOT\n", + " \n", + " rzz_gate(qubits[i], qubits[j], theta)\n", + " \n", + " rx(-pi/2, qubits[i]) # Restore basis\n", + " h(qubits[j])\n", + " \n", + " # Second block: R_ZY(theta)\n", + " h(qubits[i]) # Basis change Z -> X\n", + " rx(pi/2, qubits[j]) # Basis change Y -> Z\n", + " \n", + " rzz_gate(qubits[i], qubits[j], theta)\n", + " \n", + " h(qubits[i]) # Restore basis\n", + " rx(-pi/2, qubits[j])\n", + "\n", + "# Four-qubit rotation block\n", + "@cudaq.kernel\n", + "def apply_four_body_block(qubits: cudaq.qview, i: int, i_t: int, i_k: int, i_kt: int, theta: float):\n", + " \"\"\"\n", + " Apply the 4-qubit block from Figure 4 in the paper.\n", + " This implements the four rotations:\n", + " R_YZZZ, R_ZYZZ, R_ZZYZ, R_ZZZY\n", + " \n", + " Args:\n", + " qubits: quantum register\n", + " i, i_t, i_k, i_kt: four qubit indices for positions i, i+t, i+k, i+k+t\n", + " theta: rotation angle (will be 8*θ(nΔt) in the main circuit)\n", + " \"\"\"\n", + " indices = [i, i_t, i_k, i_kt]\n", + " \n", + " # The figure shows a complex decomposition with 10 RZZ gates and 28 single-qubit gates\n", + " # For each of the 4 rotations (Y on one qubit, Z on the others):\n", + " \n", + " # Rotation 1: Y on qubit i, Z on others\n", + " rx(pi/2, qubits[i]) # Y basis for qubit i\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " \n", + " # Multi-controlled Z rotation (simplified - actual circuit from paper is more complex)\n", + " # Apply pairwise RZZ gates\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i])\n", + " \n", + " # Rotation 2: Y on qubit i+t, Z on others\n", + " rx(pi/2, qubits[i_t])\n", + " h(qubits[i])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " \n", + " rzz_gate(qubits[i_t], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i_t])\n", + " \n", + " # Rotation 3: Y on qubit i+k, Z on others\n", + " rx(pi/2, qubits[i_k])\n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_kt])\n", + " \n", + " rzz_gate(qubits[i_k], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i_k])\n", + " \n", + " # Rotation 4: Y on qubit i+k+t, Z on others\n", + " rx(pi/2, qubits[i_kt])\n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " \n", + " rzz_gate(qubits[i_kt], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_kt], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i_kt], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " rx(-pi/2, qubits[i_kt])" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "28648106-42d0-42d7-b52a-5383599e29d8", + "metadata": {}, + "outputs": [], + "source": [ + "@cudaq.kernel\n", + "def dcqo_circuit(N: int, n_steps: int, G2: list[list[int]], G4: list[list[int]], \n", + " thetas_2body: list[float], thetas_4body: list[float]):\n", + " \"\"\"\n", + " Complete DCQO circuit for LABS problem.\n", + " \n", + " Args:\n", + " N: number of qubits (sequence length)\n", + " n_steps: number of Trotter steps\n", + " G2: list of 2-body interaction pairs [(i,j), ...]\n", + " G4: list of 4-body interaction quartets [(i,i+t,i+k,i+k+t), ...]\n", + " thetas_2body: rotation angles for 2-body terms [n_steps x len(G2)]\n", + " thetas_4body: rotation angles for 4-body terms [n_steps x len(G4)]\n", + " \"\"\"\n", + " # Allocate qubits\n", + " qubits = cudaq.qvector(N)\n", + " \n", + " # Step 1: Prepare initial state |+⟩^⊗N\n", + " # This is the ground state of H_i = Σ h_i^x σ_i^x\n", + " for i in range(N):\n", + " h(qubits[i]) # |0⟩ -> |+⟩\n", + " \n", + " # Step 2: Apply Trotter steps\n", + " for step in range(n_steps):\n", + " \n", + " # Apply all 2-body terms for this Trotter step\n", + " for idx in range(len(G2)):\n", + " i, j = G2[idx]\n", + " # Get theta for this step and interaction\n", + " theta_idx = step * len(G2) + idx\n", + " theta = thetas_2body[theta_idx]\n", + " \n", + " # Apply R_YZ and R_ZY rotations with angle 4*theta\n", + " apply_two_body_block(qubits, i, j, theta)\n", + " \n", + " # Apply all 4-body terms for this Trotter step\n", + " for idx in range(len(G4)):\n", + " i, i_t, i_k, i_kt = G4[idx]\n", + " # Get theta for this step and interaction\n", + " theta_idx = step * len(G4) + idx\n", + " theta = thetas_4body[theta_idx]\n", + " \n", + " # Apply the four rotations with angle 8*theta\n", + " apply_four_body_block(qubits, i, i_t, i_k, i_kt, theta)\n", + " \n", + " # Step 3: Measure all qubits\n", + " mz(qubits)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "8b6673f7-832b-474e-b0e9-0182475b89d9", + "metadata": {}, + "outputs": [], + "source": [ + "import auxiliary_files.labs_utils as utils\n", + "\n", + "def compute_theta(N, n_steps, T, G2, G4):\n", + " \"\"\"\n", + " Compute rotation angles for each Trotter step.\n", + " \n", + " Args:\n", + " N: number of qubits\n", + " n_steps: number of Trotter steps\n", + " T: total evolution time\n", + " G2: 2-body interaction indices\n", + " G4: 4-body interaction indices\n", + " \n", + " Returns:\n", + " thetas_2body: flat list of angles for 2-body terms\n", + " thetas_4body: flat list of angles for 4-body terms\n", + " \"\"\"\n", + " dt = T / n_steps # Time step size\n", + " \n", + " thetas_2body = []\n", + " thetas_4body = []\n", + " \n", + " for n in range(n_steps):\n", + " t = (n + 0.5) * dt # Mid-point of time step\n", + " \n", + " # Compute λ(t) and its derivative\n", + " lambda_t = utils.lambda_schedule(t, T)\n", + " lambda_dot = utils.lambda_derivative(t, T)\n", + " \n", + " # Compute α(t) from Γ₁ and Γ₂\n", + " gamma1 = utils.compute_gamma1(N, G2, G4, lambda_t)\n", + " gamma2 = utils.compute_gamma2(N, G2, G4, lambda_t)\n", + " alpha_t = -gamma1 / gamma2\n", + " \n", + " # θ(t) = Δt × α(t) × λ̇(t)\n", + " theta_t = dt * alpha_t * lambda_dot\n", + " \n", + " # For 2-body terms: angle is 4*θ(t) (but we pass θ and multiply in kernel)\n", + " for _ in G2:\n", + " thetas_2body.append(4 * theta_t)\n", + " \n", + " # For 4-body terms: angle is 8*θ(t)\n", + " for _ in G4:\n", + " thetas_4body.append(8 * theta_t)\n", + " \n", + " return thetas_2body, thetas_4body" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "306eac9b-5965-4bbe-96c7-4671943c9617", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "module 'auxiliary_files.labs_utils' has no attribute 'lambda_schedule'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[29], line 11\u001b[0m\n\u001b[1;32m 8\u001b[0m G2, G4 \u001b[38;5;241m=\u001b[39m get_interactions(N)\n\u001b[1;32m 10\u001b[0m \u001b[38;5;66;03m# Compute rotation angles\u001b[39;00m\n\u001b[0;32m---> 11\u001b[0m thetas_2body, thetas_4body \u001b[38;5;241m=\u001b[39m \u001b[43mcompute_theta\u001b[49m\u001b[43m(\u001b[49m\u001b[43mN\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_steps\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mT\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mG2\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mG4\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;66;03m# Run the quantum circuit\u001b[39;00m\n\u001b[1;32m 14\u001b[0m result \u001b[38;5;241m=\u001b[39m cudaq\u001b[38;5;241m.\u001b[39msample(dcqo_circuit, N, n_steps, G2, G4, thetas_2body, thetas_4body, \n\u001b[1;32m 15\u001b[0m shots_count\u001b[38;5;241m=\u001b[39mn_shots)\n", + "Cell \u001b[0;32mIn[28], line 27\u001b[0m, in \u001b[0;36mcompute_theta\u001b[0;34m(N, n_steps, T, G2, G4)\u001b[0m\n\u001b[1;32m 24\u001b[0m t \u001b[38;5;241m=\u001b[39m (n \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m0.5\u001b[39m) \u001b[38;5;241m*\u001b[39m dt \u001b[38;5;66;03m# Mid-point of time step\u001b[39;00m\n\u001b[1;32m 26\u001b[0m \u001b[38;5;66;03m# Compute λ(t) and its derivative\u001b[39;00m\n\u001b[0;32m---> 27\u001b[0m lambda_t \u001b[38;5;241m=\u001b[39m \u001b[43mutils\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlambda_schedule\u001b[49m(t, T)\n\u001b[1;32m 28\u001b[0m lambda_dot \u001b[38;5;241m=\u001b[39m utils\u001b[38;5;241m.\u001b[39mlambda_derivative(t, T)\n\u001b[1;32m 30\u001b[0m \u001b[38;5;66;03m# Compute α(t) from Γ₁ and Γ₂\u001b[39;00m\n", + "\u001b[0;31mAttributeError\u001b[0m: module 'auxiliary_files.labs_utils' has no attribute 'lambda_schedule'" + ] + } + ], + "source": [ + "# Problem parameters\n", + "N = 8 # Sequence length (start small for testing)\n", + "n_steps = 10 # Number of Trotter steps\n", + "T = 1.0 # Total evolution time\n", + "n_shots = 1000 # Number of measurements\n", + "\n", + "# Get interaction indices from Exercise 4\n", + "G2, G4 = get_interactions(N)\n", + "\n", + "# Compute rotation angles\n", + "thetas_2body, thetas_4body = compute_theta(N, n_steps, T, G2, G4)\n", + "\n", + "# Run the quantum circuit\n", + "result = cudaq.sample(dcqo_circuit, N, n_steps, G2, G4, thetas_2body, thetas_4body, \n", + " shots_count=n_shots)\n", + "\n", + "# Extract bitstrings and energies\n", + "bitstrings = []\n", + "energies = []\n", + "\n", + "for bits, count in result.items():\n", + " # Convert bitstring to list\n", + " bit_list = [int(b) for b in bits]\n", + " \n", + " # Compute LABS energy\n", + " energy = labs_energy_list(bit_list)\n", + " \n", + " # Store\n", + " for _ in range(count):\n", + " bitstrings.append(bit_list)\n", + " energies.append(energy)\n", + "\n", + "# Find best solution from quantum sampling\n", + "best_idx = np.argmin(energies)\n", + "best_quantum = bitstrings[best_idx]\n", + "best_energy = energies[best_idx]\n", + "\n", + "print(f\"Best quantum solution: {best_quantum}\")\n", + "print(f\"Energy: {best_energy}\")\n", + "\n", + "# Use this to seed MTS (Exercise 6)\n", + "quantum_population = [best_quantum] * 100 # Replicate best solution\n", + "# Or use diverse solutions:\n", + "# quantum_population = bitstrings[:100]" + ] + }, + { + "cell_type": "markdown", + "id": "fb89d90e-66e2-4700-85b9-40df9fca22c1", + "metadata": {}, + "source": [ + "## Generating Quantum Enhanced Results\n", + "\n", + "Recall that the point of this lab is to demonstrate the potential benefits of running a quantum subroutine as a preprocessing step for classical optimization of a challenging problem like LABS. you now have all of the tools you need to try this for yourself.\n", + "\n", + "
\n", + "

Exercise 6:

\n", + "

\n", + "Use your CUDA-Q code to prepare an initial population for your memetic search algorithm and see if you can improve the results relative to a random initial population. If you are running on a CPU, you will need to run smaller problem instances. The code below sets up the problem\n", + "\n", + "

\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e5f02e6-41bf-4634-9cb1-f0f543ef2e3f", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO - write code here to sample from your CUDA-Q kernel and used the results to seed your MTS population" + ] + }, + { + "cell_type": "markdown", + "id": "0a5756e2-e4cb-42f4-a4b3-7f627095f4f4", + "metadata": {}, + "source": [ + "The results clearly show that a population sampled from CUDA-Q results in an improved distribution and a lower energy final result. This is exactly the goal of quantum enhanced optimization. To not necessarily solve the problem, but improve the effectiveness of state-of-the-art classical approaches. \n", + "\n", + "A few major caveats need to be mentioned here. First, We are comparing a quantum generated population to a random sample. It is quite likely that other classical or quantum heuristics could be used to produce an initial population that might even beat the counteradiabatic method you used, so we cannot make any claims that this is the best. \n", + "\n", + "Recall that the point of the counteradiabatic approach derived in the paper is that it is more efficient in terms of two-qubit gates relative to QAOA. The benefits of this regime would only truly come into play in a setting (e.g. larger problem instance) where it is too difficult to produce a good initial population with any know classical heuristic, and the counteradiabatic approach is more efficiently run on a QPU compared to alternatives.\n", + "\n", + "We should also note that we are comparing a single sample of each approach. Maybe the quantum sample got lucky or the randomly generated population was unlucky and a more rigorous comparison would need to repeat the analysis many times to draw any confidently conclusions. \n", + "\n", + "The authors of the paper discuss all of these considerations, but propose an analysis that is quite interesting related to the scaling of the technique. Rather than run large simulations ourselves, examine their results below. \n", + "\n", + "\n", + "\n", + "\n", + "The authors computed replicate median (median of solving the problem repeated with same setup) time to solutions (excluding time to sample from QPU) for problem sizes $N=27$ to $N=37$. Two interesting conclusions can be drawn from this. First, standard memetic tabu search (MTS) is generally faster than quantum enhanced (QE) MTS. But there are two promising trends. For larger problems, the QE-MTS experiments occasionally have excellent performance with times to solution much smaller than all of the MTS data points. These outliers indicate there are certain instances where QE-MTS could provide much faster time-to-solution. \n", + "\n", + "More importantly, if a line of best fit is calculated using the median of each set of medians, the slope of the QE-MTS line is smaller than the MTS! This seems to indicate that QE solution of this problem scales $O(1.24^N)$ which is better than the best known classical heuristic ($O(1.34^N)$) and the best known quantum approach (QAOA - $O(1.46^N)$).\n", + "\n", + "For problems of size of $N=47$ or greater, the authors anticipate that QE-MTS could be a promising technique and produce good initial populations that are difficult to obtain classically. \n", + "\n", + "The study reinforces the potential of hybrid workflows enhanced by quantum data such that a classical routine is still the primary solver, but quantum computers make it much more effective. Future work can explore improvements to both the quantum and classical sides, such as including GPU accelerated memetic search on the classical side." + ] + }, + { + "cell_type": "markdown", + "id": "7aab7af9", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Self-validation To Be Completed for Phase 1\n", + "\n", + "In this section, explain how you verified your results. Did you calculate solutions by hand for small N? Did you create unit tests? Did you cross-reference your Quantum energy values against your Classical MTS results? Did you check known symmetries?" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 [cuda-q-v0.13.0]", + "language": "python", + "name": "python3_zogs" + }, + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb b/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb index f96cb45c..fc76d7d4 100644 --- a/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb +++ b/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb @@ -145,7 +145,176 @@ "metadata": {}, "outputs": [], "source": [ - "#TODO - Write code to perform MTS" + "#TODO - Write code to perform MTS\n", + "\n", + "import cudaq\n", + "import numpy as np\n", + "from math import floor\n", + "import auxiliary_files.labs_utils as utils\n", + "\n", + "\n", + "import random \n", + "\n", + "#creates bitstrings of length N\n", + "def bitstring(N):\n", + " string = []\n", + " for bit in range(N):\n", + " string.append(random.choice([0,1])) \n", + " return string\n", + "\n", + "\n", + "\n", + "#creates s_max bitstrings of lenth N\n", + "def populate(s_max,N):\n", + " pop = []\n", + " for i in range(s_max):\n", + " pop.append(bitstring(N))\n", + " return pop\n", + "\n", + "\n", + "\n", + "\n", + "#makes a array from 0 to N\n", + "def cut(N):\n", + " k_list = []\n", + " for bit in range(N):\n", + " k_list.append(bit) \n", + " return k_list\n", + "\n", + "\n", + "\n", + "\n", + "#cuts two parent bitstrings p1 and p2 of length N at index k and combines the halves into a new bitstring child of length N\n", + "def combine(p1,p2):\n", + " if len(p1)==len(p2):\n", + " k = random.choice(cut(len(p1)))\n", + " child = p1[0:k]\n", + " for element in p2[k:len(p2)]:\n", + " child.append(element)\n", + " return child\n", + "\n", + "\n", + "\n", + "#pm probability of flippinng each bit in a bitstring s \n", + "def mutate(s,pm):\n", + " for i in range(len(s)):\n", + " if random.random()-1, 1->+1\n", + " N = len(s)\n", + " E = 0\n", + " for k in range(1, N):\n", + " ck = int(np.dot(s[:N-k], s[k:])) #dot product\n", + " E += ck * ck\n", + " return E\n", + "\n", + "\n", + "def MTS(popsize,stringsize,pc,target):\n", + " pop = populate(popsize,stringsize) #creates a random list of strings \n", + " if random.random()target:\n", + " #repeats child selection, mutation, and tabu search until target energy is reached\n", + " if random.random() Z\n", + " h(qubits[j]) # Basis change Z -> X for CNOT\n", + " \n", + " rzz_gate(qubits[i], qubits[j], theta)\n", " \n", - " reg = cudaq.qvector(N)\n", - " h(reg)\n", + " rx(-pi/2, qubits[i]) # Restore basis\n", + " h(qubits[j])\n", " \n", - " # TODO - write the full kernel to apply the trotterized circuit\n", + " # Second block: R_ZY(theta)\n", + " h(qubits[i]) # Basis change Z -> X\n", + " rx(pi/2, qubits[j]) # Basis change Y -> Z\n", + " \n", + " rzz_gate(qubits[i], qubits[j], theta)\n", + " \n", + " h(qubits[i]) # Restore basis\n", + " rx(-pi/2, qubits[j])\n", "\n", + "# Four-qubit rotation block\n", + "@cudaq.kernel\n", + "def apply_four_body_block(qubits: cudaq.qview, i: int, i_t: int, i_k: int, i_kt: int, theta: float):\n", + " \"\"\"\n", + " Apply the 4-qubit block from Figure 4 in the paper.\n", + " This implements the four rotations:\n", + " R_YZZZ, R_ZYZZ, R_ZZYZ, R_ZZZY\n", + " \n", + " Args:\n", + " qubits: quantum register\n", + " i, i_t, i_k, i_kt: four qubit indices for positions i, i+t, i+k, i+k+t\n", + " theta: rotation angle (will be 8*θ(nΔt) in the main circuit)\n", + " \"\"\"\n", + " indices = [i, i_t, i_k, i_kt]\n", + " \n", + " # The figure shows a complex decomposition with 10 RZZ gates and 28 single-qubit gates\n", + " # For each of the 4 rotations (Y on one qubit, Z on the others):\n", + " \n", + " # Rotation 1: Y on qubit i, Z on others\n", + " rx(pi/2, qubits[i]) # Y basis for qubit i\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " \n", + " # Multi-controlled Z rotation (simplified - actual circuit from paper is more complex)\n", + " # Apply pairwise RZZ gates\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i])\n", + " \n", + " # Rotation 2: Y on qubit i+t, Z on others\n", + " rx(pi/2, qubits[i_t])\n", + " h(qubits[i])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " \n", + " rzz_gate(qubits[i_t], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_k])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i_t])\n", + " \n", + " # Rotation 3: Y on qubit i+k, Z on others\n", + " rx(pi/2, qubits[i_k])\n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_kt])\n", + " \n", + " rzz_gate(qubits[i_k], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i_k], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_kt], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_kt], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_kt])\n", + " rx(-pi/2, qubits[i_k])\n", + " \n", + " # Rotation 4: Y on qubit i+k+t, Z on others\n", + " rx(pi/2, qubits[i_kt])\n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " \n", + " rzz_gate(qubits[i_kt], qubits[i], theta/4)\n", + " rzz_gate(qubits[i_kt], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i_kt], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_t], theta/4)\n", + " rzz_gate(qubits[i], qubits[i_k], theta/4)\n", + " rzz_gate(qubits[i_t], qubits[i_k], theta/4)\n", + " \n", + " h(qubits[i])\n", + " h(qubits[i_t])\n", + " h(qubits[i_k])\n", + " rx(-pi/2, qubits[i_kt])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15cbc521", + "metadata": {}, + "outputs": [], + "source": [ + "@cudaq.kernel\n", + "def dcqo_circuit(N: int, n_steps: int, G2: list[list[int]], G4: list[list[int]], \n", + " thetas_2body: list[float], thetas_4body: list[float]):\n", + " \"\"\"\n", + " Complete DCQO circuit for LABS problem.\n", + " \n", + " Args:\n", + " N: number of qubits (sequence length)\n", + " n_steps: number of Trotter steps\n", + " G2: list of 2-body interaction pairs [(i,j), ...]\n", + " G4: list of 4-body interaction quartets [(i,i+t,i+k,i+k+t), ...]\n", + " thetas_2body: rotation angles for 2-body terms [n_steps x len(G2)]\n", + " thetas_4body: rotation angles for 4-body terms [n_steps x len(G4)]\n", + " \"\"\"\n", + " # Allocate qubits\n", + " qubits = cudaq.qvector(N)\n", + " \n", + " # Step 1: Prepare initial state |+⟩^⊗N\n", + " # This is the ground state of H_i = Σ h_i^x σ_i^x\n", + " for i in range(N):\n", + " h(qubits[i]) # |0⟩ -> |+⟩\n", + " \n", + " # Step 2: Apply Trotter steps\n", + " for step in range(n_steps):\n", + " \n", + " # Apply all 2-body terms for this Trotter step\n", + " for idx in range(len(G2)):\n", + " i, j = G2[idx]\n", + " # Get theta for this step and interaction\n", + " theta_idx = step * len(G2) + idx\n", + " theta = thetas_2body[theta_idx]\n", + " \n", + " # Apply R_YZ and R_ZY rotations with angle 4*theta\n", + " apply_two_body_block(qubits, i, j, theta)\n", + " \n", + " # Apply all 4-body terms for this Trotter step\n", + " for idx in range(len(G4)):\n", + " i, i_t, i_k, i_kt = G4[idx]\n", + " # Get theta for this step and interaction\n", + " theta_idx = step * len(G4) + idx\n", + " theta = thetas_4body[theta_idx]\n", + " \n", + " # Apply the four rotations with angle 8*theta\n", + " apply_four_body_block(qubits, i, i_t, i_k, i_kt, theta)\n", + " \n", + " # Step 3: Measure all qubits\n", + " mz(qubits)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e19a7a46", + "metadata": {}, + "outputs": [], + "source": [ + "import auxiliary_files.labs_utils as utils\n", "\n", - " \n", + "def compute_theta(N, n_steps, T, G2, G4):\n", + " \"\"\"\n", + " Compute rotation angles for each Trotter step.\n", + " \n", + " Args:\n", + " N: number of qubits\n", + " n_steps: number of Trotter steps\n", + " T: total evolution time\n", + " G2: 2-body interaction indices\n", + " G4: 4-body interaction indices\n", + " \n", + " Returns:\n", + " thetas_2body: flat list of angles for 2-body terms\n", + " thetas_4body: flat list of angles for 4-body terms\n", + " \"\"\"\n", + " dt = T / n_steps # Time step size\n", + " \n", + " thetas_2body = []\n", + " thetas_4body = []\n", + " \n", + " for n in range(n_steps):\n", + " t = (n + 0.5) * dt # Mid-point of time step\n", + " \n", + " # Compute λ(t) and its derivative\n", + " lambda_t = utils.lambda_schedule(t, T)\n", + " lambda_dot = utils.lambda_derivative(t, T)\n", + " \n", + " # Compute α(t) from Γ₁ and Γ₂\n", + " gamma1 = utils.compute_gamma1(N, G2, G4, lambda_t)\n", + " gamma2 = utils.compute_gamma2(N, G2, G4, lambda_t)\n", + " alpha_t = -gamma1 / gamma2\n", + " \n", + " # θ(t) = Δt × α(t) × λ̇(t)\n", + " theta_t = dt * alpha_t * lambda_dot\n", + " \n", + " # For 2-body terms: angle is 4*θ(t) (but we pass θ and multiply in kernel)\n", + " for _ in G2:\n", + " thetas_2body.append(4 * theta_t)\n", + " \n", + " # For 4-body terms: angle is 8*θ(t)\n", + " for _ in G4:\n", + " thetas_4body.append(8 * theta_t)\n", + " \n", + " return thetas_2body, thetas_4body" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b74d2e5", + "metadata": {}, + "outputs": [], + "source": [ + "# Problem parameters\n", + "N = 8 # Sequence length (start small for testing)\n", + "n_steps = 10 # Number of Trotter steps\n", + "T = 1.0 # Total evolution time\n", + "n_shots = 1000 # Number of measurements\n", "\n", - "T=1 # total time\n", - "n_steps = 1 # number of trotter steps\n", - "dt = T / n_steps\n", - "N = 20\n", + "# Get interaction indices from Exercise 4\n", "G2, G4 = get_interactions(N)\n", "\n", - "thetas =[]\n", + "# Compute rotation angles\n", + "thetas_2body, thetas_4body = compute_theta(N, n_steps, T, G2, G4)\n", + "\n", + "# Run the quantum circuit\n", + "result = cudaq.sample(dcqo_circuit, N, n_steps, G2, G4, thetas_2body, thetas_4body, \n", + " shots_count=n_shots)\n", "\n", - "for step in range(1, n_steps + 1):\n", - " t = step * dt\n", - " theta_val = utils.compute_theta(t, dt, T, N, G2, G4)\n", - " thetas.append(theta_val)\n", + "# Extract bitstrings and energies\n", + "bitstrings = []\n", + "energies = []\n", "\n", - "# TODO - Sample your kernel to make sure it works" + "for bits, count in result.items():\n", + " # Convert bitstring to list\n", + " bit_list = [int(b) for b in bits]\n", + " \n", + " # Compute LABS energy\n", + " energy = labs_energy_list(bit_list)\n", + " \n", + " # Store\n", + " for _ in range(count):\n", + " bitstrings.append(bit_list)\n", + " energies.append(energy)\n", + "\n", + "# Find best solution from quantum sampling\n", + "best_idx = np.argmin(energies)\n", + "best_quantum = bitstrings[best_idx]\n", + "best_energy = energies[best_idx]\n", + "\n", + "print(f\"Best quantum solution: {best_quantum}\")\n", + "print(f\"Energy: {best_energy}\")\n", + "\n", + "# Use this to seed MTS (Exercise 6)\n", + "quantum_population = [best_quantum] * 100 # Replicate best solution\n", + "# Or use diverse solutions:\n", + "# quantum_population = bitstrings[:100]" ] }, { @@ -377,7 +963,262 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO - write code here to sample from your CUDA-Q kernel and used the results to seed your MTS population" + "# Problem parameters\n", + "N = 8 # Sequence length (start small for testing)\n", + "n_steps = 10 # Number of Trotter steps\n", + "T = 1.0 # Total evolution time\n", + "n_shots = 1000 # Number of measurements\n", + "\n", + "# Get interaction indices from Exercise 4\n", + "G2, G4 = get_interactions(N)\n", + "\n", + "# Compute rotation angles\n", + "thetas_2body, thetas_4body = compute_theta(N, n_steps, T, G2, G4)\n", + "\n", + "# Run the quantum circuit\n", + "result = cudaq.sample(dcqo_circuit, N, n_steps, G2, G4, thetas_2body, thetas_4body, \n", + " shots_count=n_shots)\n", + "\n", + "# Extract bitstrings and energies\n", + "bitstrings = []\n", + "energies = []\n", + "\n", + "for bits, count in result.items():\n", + " # Convert bitstring to list\n", + " bit_list = [int(b) for b in bits]\n", + " \n", + " # Compute LABS energy\n", + " energy = labs_energy_list(bit_list)\n", + " \n", + " # Store\n", + " for _ in range(count):\n", + " bitstrings.append(bit_list)\n", + " energies.append(energy)\n", + "\n", + "# Find best solution from quantum sampling\n", + "best_idx = np.argmin(energies)\n", + "best_quantum = bitstrings[best_idx]\n", + "best_energy = energies[best_idx]\n", + "\n", + "print(f\"Best quantum solution: {best_quantum}\")\n", + "print(f\"Energy: {best_energy}\")\n", + "\n", + "# Use this to seed MTS (Exercise 6)\n", + "quantum_population = [best_quantum] * 100 # Replicate best solution\n", + "# Or use diverse solutions:\n", + "# quantum_population = bitstrings[:100]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76daf1be", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import numpy as np\n", + "\n", + "# -----------------------------\n", + "# 1) Quantum seeding (uses Exercise 5 runner)\n", + "# -----------------------------\n", + "def seed_population_from_quantum(N, popsize, n_steps=10, T=1.0, n_shots=2000, reverse_bits=False):\n", + " best, bestE, bitstrings, energies = run_dcqo_labs(N=N, n_steps=n_steps, T=T, n_shots=n_shots)\n", + "\n", + " ranked = sorted(zip(bitstrings, energies), key=lambda x: x[1])\n", + "\n", + " pop = []\n", + " seen = set()\n", + " for b, E in ranked:\n", + " bb = b[::-1] if reverse_bits else b\n", + " if bb in seen:\n", + " continue\n", + " seen.add(bb)\n", + " pop.append([int(c) for c in bb])\n", + " if len(pop) >= popsize:\n", + " break\n", + "\n", + " while len(pop) < popsize:\n", + " pop.append(bitstring(N)) # your notebook's random bitstring generator\n", + "\n", + " return pop, (best[::-1] if reverse_bits else best, bestE)\n", + "\n", + "\n", + "# -----------------------------\n", + "# 2) Memetic loop WITH history\n", + "# -----------------------------\n", + "def MTS_budget_with_history(init_pop, pc=0.9, n_iters=50, rng=None):\n", + " \"\"\"\n", + " init_pop: list[list[int]] initial population\n", + " Returns:\n", + " best_sol, best_E,\n", + " E_iter (energy each iteration),\n", + " E_best_so_far (running min)\n", + " \"\"\"\n", + " if rng is None:\n", + " rng = random.Random()\n", + "\n", + " pop = [p.copy() for p in init_pop] # don't mutate caller's pop\n", + "\n", + " best_sol = None\n", + " best_E = float(\"inf\")\n", + "\n", + " E_iter = []\n", + " E_best = []\n", + "\n", + " N = len(pop[0])\n", + "\n", + " for it in range(n_iters):\n", + " # Select/recombine\n", + " if rng.random() < pc:\n", + " p1 = pop[rng.randrange(len(pop))]\n", + " p2 = pop[rng.randrange(len(pop))]\n", + " child = combine(p1, p2)\n", + " else:\n", + " child = pop[rng.randrange(len(pop))].copy()\n", + "\n", + " # Mutate (copy so we don't corrupt stored references)\n", + " mutant = mutate(child.copy(), 1.0 / N)\n", + "\n", + " # Local improvement\n", + " s = tabu_search(mutant)\n", + " E = labs_energy_list(s)\n", + "\n", + " # Track this iteration\n", + " E_iter.append(E)\n", + " if E < best_E:\n", + " best_E = E\n", + " best_sol = s.copy()\n", + " E_best.append(best_E)\n", + "\n", + " # Replace a random member (steady-state)\n", + " pop[rng.randrange(len(pop))] = s\n", + "\n", + " return best_sol, best_E, np.array(E_iter), np.array(E_best)\n", + "\n", + "\n", + "# -----------------------------\n", + "# 3) Run ONE seed and store arrays\n", + "# -----------------------------\n", + "def run_one_seed_experiment(seed,\n", + " N=20, popsize=32,\n", + " pc=0.9, n_iters=50,\n", + " q_n_steps=10, q_T=1.0, q_shots=2000,\n", + " reverse_bits=False):\n", + " rng = random.Random(seed)\n", + "\n", + " # Quantum-seeded\n", + " q_pop, (q_best_bits, q_bestE) = seed_population_from_quantum(\n", + " N=N, popsize=popsize, n_steps=q_n_steps, T=q_T, n_shots=q_shots, reverse_bits=reverse_bits\n", + " )\n", + " q_sol, q_E, q_E_iter, q_E_best = MTS_budget_with_history(q_pop, pc=pc, n_iters=n_iters, rng=rng)\n", + "\n", + " # Random-seeded (use same rng seed stream for fairness)\n", + " rng2 = random.Random(seed) # reset so the classical randomness is comparable\n", + " r_pop = populate(popsize, N)\n", + " r_sol, r_E, r_E_iter, r_E_best = MTS_budget_with_history(r_pop, pc=pc, n_iters=n_iters, rng=rng2)\n", + "\n", + " return {\n", + " \"seed\": seed,\n", + " \"N\": N,\n", + " \"popsize\": popsize,\n", + " \"n_iters\": n_iters,\n", + " \"quantum\": {\n", + " \"pre_best_bits\": q_best_bits,\n", + " \"pre_best_E\": q_bestE,\n", + " \"final_bits\": ''.join(map(str, q_sol)),\n", + " \"final_E\": q_E,\n", + " \"E_iter\": q_E_iter,\n", + " \"E_best_so_far\": q_E_best,\n", + " },\n", + " \"random\": {\n", + " \"final_bits\": ''.join(map(str, r_sol)),\n", + " \"final_E\": r_E,\n", + " \"E_iter\": r_E_iter,\n", + " \"E_best_so_far\": r_E_best,\n", + " }\n", + " }\n", + "\n", + "\n", + "# -----------------------------\n", + "# 4) Run MANY seeds and compare trajectories\n", + "# -----------------------------\n", + "def run_many_seeds(num_trials=10,\n", + " seed0=0,\n", + " N=10, popsize=32,\n", + " pc=0.9, n_iters=50,\n", + " q_n_steps=10, q_T=1.0, q_shots=2000,\n", + " reverse_bits=False):\n", + " runs = []\n", + " for k in range(num_trials):\n", + " seed = seed0 + k\n", + " out = run_one_seed_experiment(\n", + " seed=seed, N=N, popsize=popsize,\n", + " pc=pc, n_iters=n_iters,\n", + " q_n_steps=q_n_steps, q_T=q_T, q_shots=q_shots,\n", + " reverse_bits=reverse_bits\n", + " )\n", + " runs.append(out)\n", + "\n", + " # Stack best-so-far curves into arrays for averaging\n", + " q_best_curves = np.stack([r[\"quantum\"][\"E_best_so_far\"] for r in runs], axis=0)\n", + " r_best_curves = np.stack([r[\"random\"][\"E_best_so_far\"] for r in runs], axis=0)\n", + "\n", + " summary = {\n", + " \"q_mean_best\": q_best_curves.mean(axis=0),\n", + " \"q_median_best\": np.median(q_best_curves, axis=0),\n", + " \"r_mean_best\": r_best_curves.mean(axis=0),\n", + " \"r_median_best\": np.median(r_best_curves, axis=0),\n", + " \"q_final_Es\": np.array([r[\"quantum\"][\"final_E\"] for r in runs]),\n", + " \"r_final_Es\": np.array([r[\"random\"][\"final_E\"] for r in runs]),\n", + " \"runs\": runs,\n", + " }\n", + " return summary\n", + "\n", + "\n", + "# -----------------------------\n", + "# 5) Example usage\n", + "# -----------------------------\n", + "# Single seed (stores per-iteration arrays)\n", + "one = run_one_seed_experiment(seed=0, N=18, popsize=32, n_iters=50, q_shots=2000)\n", + "print(\"ONE SEED\")\n", + "print(\"Quantum final E:\", one[\"quantum\"][\"final_E\"], \"Random final E:\", one[\"random\"][\"final_E\"])\n", + "print(\"Quantum best-so-far (first 10):\", one[\"quantum\"][\"E_best_so_far\"][:10])\n", + "print(\"Random best-so-far (first 10):\", one[\"random\"][\"E_best_so_far\"][:10])\n", + "\n", + "# Many seeds (aggregates curves)\n", + "summary = run_many_seeds(num_trials=10, seed0=0, N=10, popsize=32, n_iters=50, q_shots=2000)\n", + "print(\"\\nMANY SEEDS\")\n", + "print(\"Avg final E (quantum):\", summary[\"q_final_Es\"].mean(), \"Avg final E (random):\", summary[\"r_final_Es\"].mean())\n", + "print(\"Min final E (quantum):\", summary[\"q_final_Es\"].min(), \"Min final E (random):\", summary[\"r_final_Es\"].min())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63529e28", + "metadata": {}, + "outputs": [], + "source": [ + "trials = 10\n", + "q_results, r_results = [], []\n", + "\n", + "for t in range(trials):\n", + " random.seed(t)\n", + "\n", + " q_pop, _ = seed_population_from_quantum(N, popsize, n_steps=10, T=1.0, n_shots=2000)\n", + " r_pop = populate(popsize, N)\n", + "\n", + " _, q_E = MTS_budget([p.copy() for p in q_pop], pc=pc, n_iters=n_iters)\n", + " _, r_E = MTS_budget([p.copy() for p in r_pop], pc=pc, n_iters=n_iters)\n", + "\n", + " q_results.append(q_E)\n", + " r_results.append(r_E)\n", + "\n", + "print(\"Quantum-seeded energies:\", q_results)\n", + "print(\"Random-seeded energies:\", r_results)\n", + "print(\"Avg quantum:\", sum(q_results)/len(q_results), \" Avg random:\", sum(r_results)/len(r_results))\n", + "print(\"Min quantum:\", min(q_results), \" Min random:\", min(r_results))\n" ] }, {