diff --git a/team-submissions/SV.ipynb b/team-submissions/SV.ipynb new file mode 100644 index 00000000..378bae17 --- /dev/null +++ b/team-submissions/SV.ipynb @@ -0,0 +1,180 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "884e5b8d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N= 10 best energy= 13 count= 40\n", + "[1, 1, 1, 1, 1, -1, -1, 1, -1, 1]\n", + "[1, 1, 1, 1, -1, 1, -1, -1, 1, 1]\n", + "[1, 1, 1, 1, -1, -1, 1, 1, -1, 1]\n", + "[1, 1, 1, 1, -1, -1, 1, -1, 1, -1]\n", + "[1, 1, 1, -1, 1, -1, 1, 1, -1, -1]\n", + "[1, 1, 1, -1, -1, 1, 1, -1, 1, -1]\n", + "[1, 1, 1, -1, -1, -1, 1, -1, -1, 1]\n", + "[1, 1, -1, -1, 1, -1, 1, 1, 1, 1]\n", + "[1, 1, -1, -1, 1, -1, 1, -1, -1, -1]\n", + "[1, 1, -1, -1, -1, 1, -1, -1, 1, -1]\n", + "[1, -1, 1, 1, 1, 1, 1, -1, -1, 1]\n", + "[1, -1, 1, 1, -1, 1, 1, 1, -1, -1]\n", + "[1, -1, 1, 1, -1, -1, 1, 1, 1, 1]\n", + "[1, -1, 1, -1, 1, 1, -1, -1, -1, -1]\n", + "[1, -1, 1, -1, -1, 1, 1, 1, 1, 1]\n", + "[1, -1, 1, -1, -1, 1, 1, -1, -1, -1]\n", + "[1, -1, 1, -1, -1, -1, -1, 1, 1, -1]\n", + "[1, -1, -1, 1, 1, 1, 1, 1, -1, 1]\n", + "[1, -1, -1, 1, 1, 1, 1, -1, 1, -1]\n", + "[1, -1, -1, 1, -1, -1, -1, 1, 1, 1]\n", + "[-1, 1, 1, -1, 1, 1, 1, -1, -1, -1]\n", + "[-1, 1, 1, -1, -1, -1, -1, 1, -1, 1]\n", + "[-1, 1, 1, -1, -1, -1, -1, -1, 1, -1]\n", + "[-1, 1, -1, 1, 1, 1, 1, -1, -1, 1]\n", + "[-1, 1, -1, 1, 1, -1, -1, 1, 1, 1]\n", + "[-1, 1, -1, 1, 1, -1, -1, -1, -1, -1]\n", + "[-1, 1, -1, 1, -1, -1, 1, 1, 1, 1]\n", + "[-1, 1, -1, -1, 1, 1, -1, -1, -1, -1]\n", + "[-1, 1, -1, -1, 1, -1, -1, -1, 1, 1]\n", + "[-1, 1, -1, -1, -1, -1, -1, 1, 1, -1]\n", + "[-1, -1, 1, 1, 1, -1, 1, 1, -1, 1]\n", + "[-1, -1, 1, 1, -1, 1, -1, 1, 1, 1]\n", + "[-1, -1, 1, 1, -1, 1, -1, -1, -1, -1]\n", + "[-1, -1, -1, 1, 1, 1, -1, 1, 1, -1]\n", + "[-1, -1, -1, 1, 1, -1, -1, 1, -1, 1]\n", + "[-1, -1, -1, 1, -1, 1, -1, -1, 1, 1]\n", + "[-1, -1, -1, -1, 1, 1, -1, 1, -1, 1]\n", + "[-1, -1, -1, -1, 1, 1, -1, -1, 1, -1]\n", + "[-1, -1, -1, -1, 1, -1, 1, 1, -1, -1]\n", + "[-1, -1, -1, -1, -1, 1, 1, -1, 1, -1]\n" + ] + } + ], + "source": [ + "from itertools import product\n", + "\n", + "def C_k(s, k):\n", + " N = len(s)\n", + " total = 0\n", + " for i in range(N - k):\n", + " total += s[i] * s[i + k]\n", + " return total\n", + "\n", + "def E(s):\n", + " N = len(s)\n", + " total = 0\n", + " for k in range(1, N):\n", + " ck = C_k(s, k)\n", + " total += ck * ck\n", + " return total\n", + "\n", + "def brute_force_sequences(N):\n", + " # generator that yields (seq_list, energy)\n", + " for seq in product([1, -1], repeat=N):\n", + " seq_list = list(seq)\n", + " yield seq_list, E(seq_list)\n", + "\n", + "def calculate(N):\n", + " best_energy = None\n", + " best_seqs = []\n", + " for seq, energy in brute_force_sequences(N):\n", + " if best_energy is None or energy < best_energy:\n", + " best_energy = energy\n", + " best_seqs = [seq]\n", + " elif energy == best_energy:\n", + " best_seqs.append(seq)\n", + " return best_energy, best_seqs\n", + "\n", + "# Example usage:\n", + "if __name__ == \"__main__\":\n", + " N = 10\n", + " best_energy, best_seqs = calculate(N)\n", + " print(\"N=\", N, \"best energy=\", best_energy, \"count=\", len(best_seqs))\n", + " for seq in best_seqs:\n", + " print(seq)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1917fa7f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Energy: 13\n", + "Number of Optimal Sequences: 40\n", + "Optimal Sequence: [1, 1, 1, 1, 1, -1, -1, 1, -1, 1]\n", + "Optimal Sequence: [1, 1, 1, 1, -1, 1, -1, -1, 1, 1]\n", + "Optimal Sequence: [1, 1, 1, 1, -1, -1, 1, 1, -1, 1]\n", + "Optimal Sequence: [1, 1, 1, 1, -1, -1, 1, -1, 1, -1]\n", + "Optimal Sequence: [1, 1, 1, -1, 1, -1, 1, 1, -1, -1]\n", + "Optimal Sequence: [1, 1, 1, -1, -1, 1, 1, -1, 1, -1]\n", + "Optimal Sequence: [1, 1, 1, -1, -1, -1, 1, -1, -1, 1]\n", + "Optimal Sequence: [1, 1, -1, -1, 1, -1, 1, 1, 1, 1]\n", + "Optimal Sequence: [1, 1, -1, -1, 1, -1, 1, -1, -1, -1]\n", + "Optimal Sequence: [1, 1, -1, -1, -1, 1, -1, -1, 1, -1]\n", + "Optimal Sequence: [1, -1, 1, 1, 1, 1, 1, -1, -1, 1]\n", + "Optimal Sequence: [1, -1, 1, 1, -1, 1, 1, 1, -1, -1]\n", + "Optimal Sequence: [1, -1, 1, 1, -1, -1, 1, 1, 1, 1]\n", + "Optimal Sequence: [1, -1, 1, -1, 1, 1, -1, -1, -1, -1]\n", + "Optimal Sequence: [1, -1, 1, -1, -1, 1, 1, 1, 1, 1]\n", + "Optimal Sequence: [1, -1, 1, -1, -1, 1, 1, -1, -1, -1]\n", + "Optimal Sequence: [1, -1, 1, -1, -1, -1, -1, 1, 1, -1]\n", + "Optimal Sequence: [1, -1, -1, 1, 1, 1, 1, 1, -1, 1]\n", + "Optimal Sequence: [1, -1, -1, 1, 1, 1, 1, -1, 1, -1]\n", + "Optimal Sequence: [1, -1, -1, 1, -1, -1, -1, 1, 1, 1]\n", + "Optimal Sequence: [-1, 1, 1, -1, 1, 1, 1, -1, -1, -1]\n", + "Optimal Sequence: [-1, 1, 1, -1, -1, -1, -1, 1, -1, 1]\n", + "Optimal Sequence: [-1, 1, 1, -1, -1, -1, -1, -1, 1, -1]\n", + "Optimal Sequence: [-1, 1, -1, 1, 1, 1, 1, -1, -1, 1]\n", + "Optimal Sequence: [-1, 1, -1, 1, 1, -1, -1, 1, 1, 1]\n", + "Optimal Sequence: [-1, 1, -1, 1, 1, -1, -1, -1, -1, -1]\n", + "Optimal Sequence: [-1, 1, -1, 1, -1, -1, 1, 1, 1, 1]\n", + "Optimal Sequence: [-1, 1, -1, -1, 1, 1, -1, -1, -1, -1]\n", + "Optimal Sequence: [-1, 1, -1, -1, 1, -1, -1, -1, 1, 1]\n", + "Optimal Sequence: [-1, 1, -1, -1, -1, -1, -1, 1, 1, -1]\n", + "Optimal Sequence: [-1, -1, 1, 1, 1, -1, 1, 1, -1, 1]\n", + "Optimal Sequence: [-1, -1, 1, 1, -1, 1, -1, 1, 1, 1]\n", + "Optimal Sequence: [-1, -1, 1, 1, -1, 1, -1, -1, -1, -1]\n", + "Optimal Sequence: [-1, -1, -1, 1, 1, 1, -1, 1, 1, -1]\n", + "Optimal Sequence: [-1, -1, -1, 1, 1, -1, -1, 1, -1, 1]\n", + "Optimal Sequence: [-1, -1, -1, 1, -1, 1, -1, -1, 1, 1]\n", + "Optimal Sequence: [-1, -1, -1, -1, 1, 1, -1, 1, -1, 1]\n", + "Optimal Sequence: [-1, -1, -1, -1, 1, 1, -1, -1, 1, -1]\n", + "Optimal Sequence: [-1, -1, -1, -1, 1, -1, 1, 1, -1, -1]\n", + "Optimal Sequence: [-1, -1, -1, -1, -1, 1, 1, -1, 1, -1]\n" + ] + } + ], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/team-submissions/symValidator.py b/team-submissions/symValidator.py new file mode 100644 index 00000000..f4b08979 --- /dev/null +++ b/team-submissions/symValidator.py @@ -0,0 +1,136 @@ +'''“We verify correctness by (a) exact enumeration for small N, +(b) symmetry tests mandated by the LABS Hamiltonian (global spin-flip, +reversal, and their combination), +and (c) cross-checks between classical MTS and the +quantum algorithm on small instances. +This mirrors standard practice in quantum-classical algorithm validation where +invariants and small-instance exact checks are used as the ground-truth proxy +when a solution key is not available.” ''' +# symValidator.py +#LABS energy function (canonical) +import numpy as np + +def labs_energy(s): + """Compute LABS energy for a sequence s of +/-1 (list or 1D numpy array).""" + s = np.asarray(s, dtype=int) + N = s.size + E = 0 + for k in range(1, N): + Ck = int(np.sum(s[:N-k] * s[k:])) # autocorrelation at shift k + E += Ck * Ck + return int(E) + + +# brute force enumeration for small N +import itertools + +def brute_force_labs(N): + energies = {} + for bits in itertools.product([-1, 1], repeat=N): + energies[bits] = labs_energy(bits) + # return sorted list of (sequence, energy) + return sorted(energies.items(), key=lambda t: t[1]) + + +# symmetry positive tests (must pass) +def test_global_flip(s): + assert labs_energy(s) == labs_energy([-x for x in s]), "Global flip symmetry failed" + +def test_reversal(s): + assert labs_energy(s) == labs_energy(list(reversed(s))), "Reversal symmetry failed" + +def test_flip_and_reverse(s): + assert labs_energy(s) == labs_energy([-x for x in reversed(s)]), "Flip+reverse symmetry failed" + +# test on random and canonical sequences +import random +#negative tests to catch incorrect assumptions (eg, accidental cyclic autocor) +def cyclic_rotate(s, r): + """Cyclic rotate right by r""" + r = r % len(s) + return s[-r:] + s[:-r] + +# cross-check classical vs quantum (small N) +# integrate after phase 1 is completed +'''def compare_solvers(N): + bf = brute_force_labs(N) + best_exact_e = bf[0][1] + # run MTS (classical baseline) - placeholder: use your notebook's MTS call + mts_seq, mts_e = run_MTS(N) # must return (sequence, energy) + # run quantum procedure (placeholder) + q_seq, q_e = run_quantum(N) # must return (sequence, energy) + print(f"N={N}: exact={best_exact_e}, MTS={mts_e}, Quantum={q_e}") + return best_exact_e, mts_e, q_e + +# Example call (only for N small where brute force is available) +N = 6 +compare_solvers(N) +''' + +# build orbit under dihedral-like ops +def dihedral_orbit(s): + #the 'shape' is a 1D sequence, so we can't use rotations + s = list(s) + ops = [ + lambda x: x, + lambda x: [-a for a in x], # flip + lambda x: list(reversed(x)), # reverse + lambda x: [-a for a in reversed(x)] # flip+reverse + ] + return {tuple(op(s)) for op in ops} + + +# CELL: small test suite runner +def run_all_tests(): + # smoke tests for small Ns using brute force sequences + for N in range(3,7): + print(f"Testing all symmetries on all {2**N} sequences for N={N}") + bf = brute_force_labs(N) + for seq, e in bf: + test_global_flip(seq) + test_reversal(seq) + test_flip_and_reverse(seq) + # negative test sample + s = [1, -1, 1, 1, -1, 1] + assert labs_energy(s) != labs_energy(cyclic_rotate(s, 1)) + print("All tests passed.") + +# Run the tests +if __name__ == "__main__": + # run for N=5 + N = 5 + bf = brute_force_labs(N) + print(f"Found {len(bf)} sequences; best energy = {bf[0][1]}") + # show top few + for seq, e in bf[:6]: + print(seq, e) + + # symmetry smoke tests on random sequences + for N in [3, 4, 5, 6]: + for _ in range(10): + s = [random.choice([-1, 1]) for _ in range(N)] + test_global_flip(s) + test_reversal(s) + test_flip_and_reverse(s) + print("Positive symmetry tests passed on random samples.") + + # pick a random sequence and check rotation changes energy (for linear LABS) + s = [1, -1, 1, 1, -1] # example + E_orig = labs_energy(s) + E_rot = labs_energy(cyclic_rotate(s, 1)) + print("orig:", s, "E=", E_orig, "rotated E=", E_rot) + assert E_orig != E_rot, ( + "Rotation did not change energy check whether you accidentally implemented circular autocor!" + ) + + # dihedral orbit demo + s = [1, -1, 1, 1] + # get all transformations + orbit = dihedral_orbit(s) + print("orbit size:", len(orbit)) + # print energy, should be the same + for seq in orbit: + print(seq, labs_energy(seq)) + + # Run the tests + run_all_tests() \ No newline at end of file diff --git a/tutorial_notebook/.ipynb_checkpoints/01_quantum_enhanced_optimization_LABS-checkpoint.ipynb b/tutorial_notebook/.ipynb_checkpoints/01_quantum_enhanced_optimization_LABS-checkpoint.ipynb new file mode 100644 index 00000000..c3ce8f37 --- /dev/null +++ b/tutorial_notebook/.ipynb_checkpoints/01_quantum_enhanced_optimization_LABS-checkpoint.ipynb @@ -0,0 +1,1385 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "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": 22, + "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": {}, + "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": 49, + "id": "341c9a3f-565c-49ab-9903-0aa9a508722c", + "metadata": {}, + "outputs": [], + "source": [ + "#TODO - Write code to perform MTS\n", + "import numpy as np\n", + "import time\n", + "import matplotlib.pyplot as plt\n", + "import itertools\n", + "\n", + "# 1) LABS objective for ±1 sequences\n", + "\n", + "def pm1_to_bits01(s_pm1: np.ndarray) -> np.ndarray:\n", + " return ((s_pm1 + 1) // 2).astype(np.int8)\n", + "\n", + "def bits01_to_pm1(bits01) -> np.ndarray:\n", + " x = np.array(bits01, dtype=np.int8)\n", + " return (2*x - 1).astype(np.int8) # 0->-1, 1->+1\n", + "\n", + "def labs_correlations_pm1(s: np.ndarray) -> np.ndarray:\n", + " \"\"\"C[k-1] = C_k for k=1..N-1, C_k = sum_i s[i]*s[i+k].\"\"\"\n", + " N = s.size\n", + " C = np.empty(N-1, dtype=np.int32)\n", + " for k in range(1, N):\n", + " C[k-1] = int(np.dot(s[:-k], s[k:]))\n", + " return C\n", + "\n", + "def labs_energy_from_C(C: np.ndarray) -> int:\n", + " C64 = C.astype(np.int64)\n", + " return int(np.sum(C64*C64))\n", + "\n", + "def labs_energy_pm1(s: np.ndarray) -> int:\n", + " return labs_energy_from_C(labs_correlations_pm1(s))\n", + "\n", + "def aperiodic_autocorr_full(s_pm1: np.ndarray) -> np.ndarray:\n", + " \"\"\"lags = -(N-1)..+(N-1)\"\"\"\n", + " N = len(s_pm1)\n", + " out = []\n", + " for lag in range(-(N-1), N):\n", + " if lag < 0:\n", + " out.append(int(np.dot(s_pm1[-lag:], s_pm1[:N+lag])))\n", + " elif lag == 0:\n", + " out.append(int(np.dot(s_pm1, s_pm1)))\n", + " else:\n", + " out.append(int(np.dot(s_pm1[:-lag], s_pm1[lag:])))\n", + " return np.array(out, dtype=int)\n", + "\n", + "\n", + "# 2) Algorithm 3: Combine & Mutate \n", + "\n", + "\n", + "def combine_alg3(p1: np.ndarray, p2: np.ndarray, rng: np.random.Generator) -> np.ndarray:\n", + " N = p1.size\n", + " k = int(rng.integers(1, N)) # k in {1,...,N-1}\n", + " child = np.empty_like(p1)\n", + " child[:k] = p1[:k]\n", + " child[k:] = p2[k:]\n", + " return child\n", + "\n", + "def mutate_alg3(s: np.ndarray, p_mut: float, rng: np.random.Generator) -> np.ndarray:\n", + " out = s.copy()\n", + " if p_mut <= 0.0:\n", + " return out\n", + " mask = rng.random(out.size) < p_mut\n", + " out[mask] *= -1\n", + " return out\n", + "\n", + "# 3) Tabu Search (single-bit flip neighborhood)\n", + "# - aspiration: allow tabu move if it improves best found in this tabu run\n", + "# - candidate_size: evaluate subset of flips each step (CPU-friendly)\n", + "\n", + "\n", + "def delta_energy_single_flip_pm1(s: np.ndarray, C: np.ndarray, E: int, j: int):\n", + " \"\"\"\n", + " After flipping s[j], update correlations deltaC and energy E_new.\n", + " Flip affects C_k terms that involve index j:\n", + " (j, j+k) and (j-k, j) when in bounds.\n", + " Each affected product changes sign => delta contribution = -2*old_term.\n", + " \"\"\"\n", + " N = s.size\n", + " sj = int(s[j])\n", + " deltaC = np.zeros_like(C, dtype=np.int32)\n", + "\n", + " for k in range(1, N):\n", + " d = 0\n", + " jp = j + k\n", + " jm = j - k\n", + " if jp < N:\n", + " d += sj * int(s[jp])\n", + " if jm >= 0:\n", + " d += int(s[jm]) * sj\n", + " if d != 0:\n", + " deltaC[k-1] = -2 * d\n", + "\n", + " C64 = C.astype(np.int64)\n", + " d64 = deltaC.astype(np.int64)\n", + " dE = int(np.sum(2*C64*d64 + d64*d64))\n", + " return E + dE, deltaC\n", + "\n", + "def tabu_search_pm1(\n", + " s0: np.ndarray,\n", + " max_iters: int = 1000,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " rng: np.random.Generator | None = None,\n", + "):\n", + " if rng is None:\n", + " rng = np.random.default_rng()\n", + "\n", + " s = s0.copy()\n", + " C = labs_correlations_pm1(s)\n", + " E = labs_energy_from_C(C)\n", + "\n", + " best_s = s.copy()\n", + " best_E = int(E)\n", + " print(best_s)\n", + "\n", + " N = s.size\n", + " tabu_until = np.zeros(N, dtype=np.int32)\n", + "\n", + " for it in range(1, max_iters + 1):\n", + " # choose candidate flip indices\n", + " if candidate_size >= N:\n", + " candidates = np.arange(N)\n", + " else:\n", + " candidates = rng.choice(N, size=candidate_size, replace=False)\n", + "\n", + " chosen_j = None\n", + " chosen_E = None\n", + " chosen_dC = None\n", + "\n", + " # pick best admissible (tabu allowed only if aspiration)\n", + " for j in candidates:\n", + " E_new, dC = delta_energy_single_flip_pm1(s, C, E, int(j))\n", + " is_tabu = tabu_until[j] > it\n", + " if is_tabu and (E_new >= best_E):\n", + " continue\n", + " if (chosen_E is None) or (E_new < chosen_E):\n", + " chosen_j, chosen_E, chosen_dC = int(j), int(E_new), dC\n", + "\n", + " # if all were blocked, ignore tabu\n", + " if chosen_j is None:\n", + " for j in candidates:\n", + " E_new, dC = delta_energy_single_flip_pm1(s, C, E, int(j))\n", + " if (chosen_E is None) or (E_new < chosen_E):\n", + " chosen_j, chosen_E, chosen_dC = int(j), int(E_new), dC\n", + "\n", + " # apply flip\n", + " s[chosen_j] *= -1\n", + " C += chosen_dC\n", + " E = chosen_E\n", + "\n", + " # update tabu tenure (with slight randomness)\n", + " tenure = tabu_tenure + int(rng.integers(0, max(1, tabu_tenure // 3)))\n", + " tabu_until[chosen_j] = it + tenure\n", + "\n", + " if E < best_E:\n", + " best_E = int(E)\n", + " best_s = s.copy()\n", + " print(best_s)\n", + "\n", + " return best_s, best_E\n", + "\n", + "\n", + "# 4) MTS main loop (matches your diagram)\n", + "\n", + "def mts_labs_pm1(\n", + " N: int,\n", + " pop_size: int = 32,\n", + " p_combine: float = 0.7,\n", + " p_mut: float = 1.0/50.0,\n", + " mts_iters: int = 1000,\n", + " tabu_iters: int = 800,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " target_E: int | None = None,\n", + " seed: int = 0,\n", + " verbose_every: int = 100,\n", + "):\n", + " rng = np.random.default_rng(seed)\n", + "\n", + " # init population: k random bitstrings\n", + " pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + " pop_E = np.array([labs_energy_pm1(pop[i]) for i in range(pop_size)], dtype=np.int64)\n", + "\n", + " best_idx = int(np.argmin(pop_E))\n", + " best_s = pop[best_idx].copy()\n", + " print(best_s)\n", + " best_E = int(pop_E[best_idx])\n", + "\n", + " trace = [best_E]\n", + " t0 = time.time()\n", + "\n", + " for it in range(1, mts_iters + 1):\n", + " if target_E is not None and best_E <= target_E:\n", + " break\n", + "\n", + " # ---- Make Child ----\n", + " if rng.random() < p_combine:\n", + " i1, i2 = rng.integers(0, pop_size, size=2)\n", + " child = combine_alg3(pop[i1], pop[i2], rng)\n", + " else:\n", + " i = int(rng.integers(0, pop_size))\n", + " child = pop[i].copy()\n", + "\n", + " # ---- Mutate Child ----\n", + " child = mutate_alg3(child, p_mut, rng)\n", + "\n", + " # ---- Tabu Search with Child ----\n", + " result_s, result_E = tabu_search_pm1(\n", + " child,\n", + " max_iters=tabu_iters,\n", + " tabu_tenure=tabu_tenure,\n", + " candidate_size=candidate_size,\n", + " rng=rng,\n", + " )\n", + "\n", + " # ---- Update best solution ----\n", + " if result_E < best_E:\n", + " best_E = int(result_E)\n", + " best_s = result_s.copy()\n", + "\n", + " # ---- Add result to Population ----\n", + " # randomly replace a member if result is better\n", + " r = int(rng.integers(0, pop_size))\n", + " if result_E < pop_E[r]:\n", + " pop[r] = result_s\n", + " pop_E[r] = result_E\n", + "\n", + " # (optional, helpful) elitism: keep global best in population\n", + " worst = int(np.argmax(pop_E))\n", + " if best_E < pop_E[worst]:\n", + " pop[worst] = best_s\n", + " pop_E[worst] = best_E\n", + "\n", + " trace.append(best_E)\n", + "\n", + " if verbose_every and (it % verbose_every == 0):\n", + " print(f\"[MTS {it:5d}] best_E={best_E} elapsed={time.time()-t0:.2f}s\")\n", + "\n", + " return {\n", + " \"best_s_pm1\": best_s,\n", + " \"best_s_01\": pm1_to_bits01(best_s),\n", + " \"best_E\": best_E,\n", + " \"best_trace\": np.array(trace, dtype=np.int64),\n", + " \"population_pm1\": pop,\n", + " \"population_E\": pop_E.copy(),\n", + " \"elapsed_sec\": time.time() - t0,\n", + " }\n", + "\n", + "\n", + "# 5) Visualization: best curve + final population energy distribution\n", + "\n", + "def visualize_mts(res: dict):\n", + " print(\"Best E Via Classification:\", res[\"best_E\"])\n", + " print(\"Best bitstring (0/1):\", \"\".join(map(str, res[\"best_s_01\"].tolist())))\n", + " print(\"Elapsed (s):\", f\"{res['elapsed_sec']:.3f}\")\n", + "\n", + " trace = res[\"best_trace\"]\n", + " popE = res[\"population_E\"]\n", + "\n", + " plt.figure()\n", + " plt.plot(trace)\n", + " plt.xlabel(\"MTS iteration\")\n", + " plt.ylabel(\"Best-so-far Energy E\")\n", + " plt.title(\"MTS Best-so-far Curve\")\n", + " plt.show()\n", + "\n", + " plt.figure()\n", + " plt.hist(popE, bins=20)\n", + " plt.xlabel(\"Energy E\")\n", + " plt.ylabel(\"Count\")\n", + " plt.title(\"Final Population Energy Distribution\")\n", + " plt.show()\n", + "\n", + "\n", + "# N=7: show a very good and very poor sequence difference\n", + "\n", + "\n", + "def brute_best_worst_N7():\n", + " N = 7\n", + " rows = []\n", + " for bits in itertools.product([0,1], repeat=N):\n", + " s = bits01_to_pm1(bits)\n", + " E = labs_energy_pm1(s)\n", + " rows.append((bits, E))\n", + " minE = min(E for _,E in rows)\n", + " maxE = max(E for _,E in rows)\n", + " good = [b for b,E in rows if E==minE][0]\n", + " bad = [b for b,E in rows if E==maxE][0]\n", + "\n", + " def show(bits, title):\n", + " s = bits01_to_pm1(bits)\n", + " C = labs_correlations_pm1(s)\n", + " E = labs_energy_pm1(s)\n", + " ac = aperiodic_autocorr_full(s)\n", + " print(f\"--- {title} ---\")\n", + " print(\"bits:\", \"\".join(map(str,bits)))\n", + " print(\"E:\", E)\n", + " print(\"Ck k=1..6:\", C.tolist())\n", + " print(\"aperiodic autocorr lags -6..+6:\", ac.tolist())\n", + " print()\n", + "\n", + "\n", + " print(\"N=7 global minE and maxE (by brute force):\", minE, maxE)\n", + " show(good, \"Very good (global optimal)\")\n", + " show(bad, \"Very poor (max energy)\")\n", + " \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da991c1b-0182-41d5-bcfe-1b24d197383d", + "metadata": {}, + "outputs": [], + "source": [ + "# Example runs\n", + "\n", + "\n", + "# 1) N=7 demo (good vs poor)\n", + "brute_best_worst_N7()\n", + "\n", + "# 2) Run MTS (CPU baseline) - start small\n", + "N = 64\n", + "res = mts_labs_pm1(\n", + " N=N,\n", + " pop_size=32,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "visualize_mts(res)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "bb31d339-f315-4fcb-9fe9-99b2f523e081", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best E Found: 10\n", + "First reached at iteration: 0\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHqCAYAAAAZLi26AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfxRJREFUeJzt3XeYE/XaxvE7m2Q7W+i9g4CAFEUUe++993rsXY/Hjr13Ofb6euy9Yu9iAVRUFCwgSK+7bE2b94+42Q3swiYkmzzZ7+e69nIzmUyeyZ2sPPnN/MblOI4jAAAAAACQcFmpLgAAAAAAgExF0w0AAAAAQJLQdAMAAAAAkCQ03QAAAAAAJAlNNwAAAAAASULTDQAAAABAktB0AwAAAACQJDTdAAAAAAAkCU03AAAAAABJQtMNAGmmh2fjyM9zj7+e6nKaNOnjyVG1zp09v0Wff7N+e0ae+7Yr72/R50bzzZ09P+p9MunjyUl7rlS/J5uD923srPxNBICmeFJdAABkuiWLlul/D76szz/4Rn/MmK2yFeXyeD3q0burRm46VLvvt7222WVzuVyuVJea8Z57/HWdd/yVUcuOPf1gXXXHBWus++QDL+qiU6+PWnbOZSfq3CtO0jnHjdcLT7wR03PXPVaSViwv04O3/08fvv25Zv/+t3y1PhWVtFFpu2L126C3NtxooA4+di917dE5pucoW7lKTz/0sj6a+KVmTv9TZSvKlZefqy49OmnTLUfpiBP30+DhA2La5trMnT1fm/ffK3L7uffv02bbbJyw7VuzWb899fdfCyRF521ND0/jGebkZKt9p7YaNXaYjjr5QI3dalRCn+vWh6/QQUfvud7bBIB0Q9MNAEn0+L3P6+oL7lBtTW3Ucr8/oJnT/9TM6X/q2Udf05e/v6YevbumqMr49OrXXZfceFbkdknbohRWE7/nH39D/776VBW2KYha/ug9zybl+f7+a4H22/oELfh7UdTyZUtWaNmSFfr919l659WPNXijgTE13R9N/FJnHX2ZViwri1ruL6tQeVmFZvz0h56493mdcNZhuuTGM+XxtMw/AUraFkW9T3r1656057LwnjzjouNUXlYhSdp4s+EpriY2tbU+zZuzUPPmLNTrz72nC646RWdefHyqywKAtEfTDQBJcu/Nj+u6i+6O3Ha73dput3EaPmqw5HLprz/m6pN3v9KSRctSWGX8uvborJPPOzLVZay3ilWVeu6x13XcGYdEln32/teaOf3PJh+z10E7aYMN+0Utu+eGR1W2olyS1LNvNx150gFR99c1WNdddFek4fZ43Nr9gB00YHAfOY40Z9Y8TZn0g/6cOSemffj6s+90/L7nyu8PSAq/13bbfzsNGtpfy5eu1BsvvK9F85dIkh668yn5/X5dc9eFMT1HvNoUFbbY+8TCe/KwE/ZNdQkxGb7xEO154I5yQiHN+n2uXnryLdXW+iRJt1xxn7bbdZyGjhyU4ioBIL3RdANAEsyc/qduvPS/kdvtO7bV/7151xr/OPX7A3r+8deVl5+7zm2uWF6m/974mH6c+qv++vNvrVhWJl+tT8WlRRo8rL/2P3J37Xf4bmscpv7u65/oiXtf0M/fz9DK5WXKzctV2w4lGjS0v0aOGapT/320srLCU3wsX7pSE258TB+/86Xmzp6vgD+gkrbF6tqjk0aM2VD7HbabRo0dJil8/uxBO5wceZ7VR+sdx9FbL32gF/7vTf049VetWLpSeQV56t6rszbbemNddP0Zys72SpKeefRVffLOJP360+9atmSFVpVVKDcvVz37dtPWO47VyecfpbbtS2ILoRmysrIUCoX02H+f07GnHxx57R65+xlJ4eY1GAyu8bhtd9lc2+6yedSyx//7XKTp7tq9U5PN36fvfR35/cyLj9c5l/9rjXV++2WWcvNymrUPoVBI/znl2qiG++l3JkQd5n3uFSfpwO3+pek/zPyn1ue1/+G7a+SmQyWtedj9bxVfaMINj+mlp97Swr8Xq3O3jjrwqD106oXHRDJreCh1nYbvh7FbjdLzHz6w1kPQb7vyft1+9YOSpO69uuiVzx/VDRffrQ/e+lx+X0Bjtxqly24+W30H9tKPU3/VjZdO0OQvf5DH49YW24/R5becE3U0QFPvydVraErd+rF+1ho73eD2qx+M7JskzQ1MXuN1a+wQ9GlTftEjdz+tbz7/XosXLJXb41b33l209Y6b6cSzD1OX7p2i1j9wu3/pq0+nSpIOOGoPnXnRcbpl/H367P2vVVVRrQFD+ujsy07Uzntts879b8zAIX2j3ssjxwzVv0+6RlL4Mz7xlY81dOQgzZk1T4/c/YymTflFf/+1QCuXlykYCKq0fYmGjRykw07YVzvuuVWjddc57/grI+/D7r26aNIfjZ+//dWnU3XH1Q/q+29/liRtMm6ELr3prDW+CAOAdEHTDQBJ8OiEZ6Oatesm/KfR0SCv19Pska9F85fovlv/b43lSxcv12cffKPPPvhGX348Wbc+dEXkvsbOYa5YVamKVZWa8+c8vfvaJzrh7MOUm5ujmppa7bf18fpjxl9R6y9ZtExLFi3TD5Onq6AgP9J0r01NTa1OPuhCffDW51HLfT6/ylaU6+fvZ+rsy06MNHBP3PeCfpzyyxp1Tv9hpqb/MFMvPz1Rr096XJ27dljnc8dixz230juvfqxZv83RR29/oe1220Kz//hbH779ReT+ia98lNDnDAbq3xe/z5it2lqfcnKyo9YZMLhPs7f39adT9fuvsyO39z5kpzXOqy4qLtTF15+pI3Y7PbLs/x54MdJ0r+6YPc/WFx99G7k9Z9Y83Xrl/Zo29Rc9/NKtSZl/oGJVlfbd8rioyc/ef/MzfffNT7rhvkt0+mEXR0ZYJenNFz/Q9Gm/6d3vnlZubvO+oGiueD5rifDQnU/p6gvuUCgUql9YK838+U/N/PlPPfvoq3roxVuaPG/+5+9naLcxR6piVWVk2U/fzdCJ+1+gpyZO0Bbbj1nvGlf//NcdqTPz5z/18F1Pr7H+ovlLtGj+Er3/5mc674qTdPZlJ67X87/y9ER98eG3Ua/Rx+98qR8m/6yPfnpB7TqUrtf2ASAZaLoBIAm++LC+YSkuLdLOe2+z3tvMysrSgMF9tNEmG6pjp3YqKmmj2ppa/fT9DL3/xmdyHEfPPfa6jvjX/ho5JtxM/d99L0Qev9HGQ7TD7lsqEAhq/t+L9P03P+m3X2ZF7p/00eRIw52Tm6NDjttLnbt21JJFyzT797lrjEqtzdUX3BHVcHft0Um77LOt2hQVaub0P/TBm9HNePsOpdphjy3Vq293lbQtltudpYXzl+j1597VimVlWjhvse667mFdd89/4nrtmnLkSQfow7c+l98f0CP3PKvtdttCj014NvIP+uNOPzjhTffQkRtEXsvXnn1XH739hUaNHa6hIwdp5KYbaty2m6xxfvnafP3591G3dz9gh0bX23qnsSouaaOylaskSd98/l2T2/zy48na/4jd1LVHZ7398oeRpv691z/Vi0++qQOO3ENnXBRukO+54dHI4444aX/16hs+Z7trj06NbbpJK5eXqaa6RsefeaiqKqv19MOvSAqf637i/ueroDBfR596kObNWaA3X/xAkjTrtzl659WPtffBO69126ufVy5JNVU1uvv6R+Tz+SVJHbu0j5wDHutnre50g4anGGy5w6baasexzd7/rz6dqqvOv12O40iSuvXsrL0P3lmVlVV67rHXVV1Vo/KyCv3roAv12YyXVVK65vnqv0z7TcWlRTrhrMNUU1Orpx96RcFgUI7j6L5b/y8hTffUr36Mut2hUztJktvj1oYjBmr46CFq275UbYoKVFVZrclf/qAv/5mx/s5rH9LBx+2tLt066siTD9D2u2+pay+8M7KtPQ/aUcNHD5EU/qKoMZ+9/7X6D+qtXfbZVtN/mBn5gmzFsjI988irOu3CY9Z7HwEg0Wi6ASAJFs5bHPm974CekcO318fAIX314Y/Pa96chfph8s9avHCZPF6PxmwxUj9O/TXynJ+8+1Wk6W44MnjVHResMUo1d/b8yGhzTYN1x241ao1zfmtrfVq+dOU661y5olxPPfhS5PbQkRvohY8eVEFhfmTZ/LkLlV+QF7n9xBt3qbqqRlMmTdOcWfNUWVGlHr27apNxI/Tua5/8s1+T1vncserUtb12P2AHvfL0RH363leaNuUXPffYa5KkwcMHJGUm7otvOFMHbHNipNlbVV6pT96dFNm/nNwcHX7ivvrPtac367SDxQuXRt3u3rNLk+t269Ul0nQvXrC0yfUuuOoUnXHRcZKkUy44SlsM3CeS/VMPvqwDjtxDh52w7xpN914H7rher9lN91+qfQ/bVVJ45HTKV9Mi99368OXaff8d5DiONum1W+Qc9R++nb7Opnv188oDgYCO2+fcSAZFxYV68s271aYo3OjF+lmrO92g4SkGG282PKbzyx+843+RhruwTYHe+OoJte/YVpK0/a5b6Kg9w18arFxepheeeEMnnHXYGttwuVx65t3/Ro6qycnJjow+/zB5erNraWjm9D91363/F3VOd8Pn22WfbSTVn3Lx58y/9NP3M7R8yQp5vB5tu+s4fffNT6quqlEgENSXH32r/Y/YXXsdtJMkRTXd2+y8+TpnL+/ao5Nen/R45IupXTc5XD99N+Offfw5rn0EgGSj6QYAI1YsW6lzjh2/xiHbq1s4r35W7DFbjNAv036TJB22y2kaNXaY+gzoqQGD+2jTLUdp8LD+kXVHbDxEOTnZqq316ZN3J2n74Qdp0LD+6juglzYcuYG22G4TdenWcZ11fvfVjwo0OIT61H8fE9VwS1pjVu4Hbn9St135gCorqprer78XN3nf+jjujEP0ytMT5TiOjt/vPK0qDx+ae+xpByfl+UaOGarXvnxMt131gD56+4vIudh1amtq9cjdz6i8rEK3PzI+KTWsy/5H7Bb5vU1RoXbYY0s991j4/Nofv/s1Kc/p8bi150E7Rm53790l0nR7vR7tss+2ksKNXs/eXSNNd9nK8piex3EcnX/CVfpo4peSwl9yPPzybVGXUovns7a+Go4gb7PzZpGGW5K23XWc2nUo1bIlKyRJU776USectcYmNHrssKjTWPpt0Cvye92XAbGaNnm6pjXRsJ97+b8izzd39nydeeSlmjxpWqPr1lmwnp/j/Q7fLepIkD4Dekaa7rIVq9Zr2wCQLDTdAJAEnbt11KzfwjNQ//nbHDmOs97nwZ5/4tXrbAIkqbbWH/n9wmtO05w/5+mjiV+qsqJKn73/tT57v34ir7FbjdLjr9+p/II8deneSbc+coUuP+tmLV+6MnJJszoFhfm68f5L1jmquHK1f9yv61JoE1/9WFdfcMc696tuVDLRRo4ZqpFjhuq7b36KjGCWtivWPoftkpTnk6QNR2ygh1+6VdVVNfr+m5809euf9Mm7kzTpkymRdV544g1dfss5Km1bvNZtdezcPur233MWaMhGAxtdd16Dic86dmnf6DqS1K5BwydJHTq2i/xeU13b6Hno66tdx7ZRlzHz/nMERt19brc7ctvtqf89FHJiep6rL7hDL/4zWut2u3X3k9escb3peD5r62vl8vpLvbXv1HaN+9t3ahtpuptqoLuv9lnLbpBR3Sj6+sjO9oav073pMB118gFRRzWcsP/5kYn61sbX4IiaeKz+96Th+zDqXHgASCPrf7wjAGAN47bbJPJ72YpyvfPPIdLxqqqsjjoPeovtxujzma9odu3XmhuYrI02HtLo49oUFeqJN+7SN7Pf1H3P3qB/X32q9j1s18hhy199OlX33vxEZP29D95Zk+dO1EufPKTrJvxHJ559uIaO3ECSVFlRpQtOvHqto9GS1jjXtOHEWI15/bl3I78XFObrf2/fo98qvtDcwGRdc3fLXNbquDMPibp92PH7Ki9v3Yd2r6+8/Fxtts3GOu3CY/TcB/frvPEnR90/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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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Y5bdMrt6uSS16dhMVWtWvOj7rlG3ilped7X1/umnh76gRQv+UJ361WUEBWnvrv1aufRfbV6/XcOevsvnx3xlpn6T2qpWu5J1MbVXx72j3Tv2qmTp4vpmzk8XfH97/pgo3di3kz56+/Rndaf87xTndl2v8Toa6q8zn3gQfzJBa1dv1OLvl8jlclvr+997s1q1b3LB+zkZd0pta9+sKjUrqF6jmipWoojCwkP11x//6OSJeGu76PzpYbZYbFHr65+/+13/9/hkFSgco4KFY2z7eEG7f4bP/pmTpPtve1INm9aRERSkm2673vpEBV9q1K2iFm0b6/eFyyVJb7z0oXZt36sqNSro1x/Tr14uSQMfuCVLZx8AQHYidANADqnToLpad2imxd+fPvK5ad02TRz9lqTTwe3sozU5JX9MlN6e/aIG3ThcJ+JOKTkpxfrs4zOCgoL0xPNDM716cZFihVS8VFF99NbnGdb1G3Kz13vIb7mzu96e9JH1x/mmtdu0ae02BQUFqVX7pl5XyM6KISP7W2HI4/Ho42lfSpLyRUao0w1tNP/LRT5v16F7a706bro8Ho88Ho/enTxL0unTny8UurPjMbNDZlcvl04fmb6U0C1Jr304Vrddf7/Wrt4kj8ejn775TT99k/nZBP54adoz1ud0m6apz//3olRoqFPN2zSyrlydWZi6477eVug+41LDaWafeBAc7NBDT9+lBx6/06/7O7Ov+1KvUU01aVXfWu50Qxvr90NSYrKmvviBJKlKzQq2hO6ChWNUuFhBW3+G6zeto6IlCltvufnh61+ssxiatmpw3tAtSa9+OEZ92t+rTetOP4bffv6zvv38Z69trr+xre5/fEAWpwaA7MNLfQCQg96e/YIGDu2joiUKy+kMUblKpfXoc/fppWlP53RrlibX1NeP/3yqu4fdpio1Kyg8IkxOZ4hiyxTXDbd20le/vat7Hs78Ik+hYU599tObGvTgrdbR4opVy2r0pBF67rVHvLYtXLSgZi98W206NlO+yAhF5AtX8zaN9NnPb6lb78w/1iozjVvU00ffva6GTesoNNSp6PyRatupub787V1Vq5X5R27VrFdVr388TrXrV/Pr84zPuNTHLDcqXLSgvl7ygcZPeUzN2zRSwcIxcjgcisgXrkrVyunGvp302oznNHhEP7/vu27DGvryt3d17fUtlC8y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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "def visualize_classical_mts_results(mts_res):\n", + " \"\"\"\n", + " Visualizes the optimization path and population distribution \n", + " with a 'Quantum Vibe' aesthetic.\n", + " \"\"\"\n", + " # 1. Extraction and Search Logic\n", + " trace = mts_res[\"best_trace\"]\n", + " pop_e = mts_res[\"population_E\"]\n", + " best_e = mts_res[\"best_E\"]\n", + "\n", + "#Dynamically find the first iteration index where the best energy was hit\n", + "#Using np.where is faster and cleaner for finding the first occurrence\n", + " iteration_indices = np.where(trace == best_e)[0]\n", + " best_iteration = iteration_indices[0] if iteration_indices.size > 0 else 0\n", + "\n", + " print(f\"Best E Found: {best_e}\")\n", + " print(f\"First reached at iteration: {best_iteration}\")\n", + "\n", + " bg_color = '#FFFFFF' # White background\n", + " accent_color = '#1c0333' # Deep dark purple for text/lines\n", + " bar_color = '#601f9e' # Vibrant purple for fills/bars\n", + " grid_color = '#F0F0F0' # Light grey for subtle grid\n", + "\n", + " # --- Plot 1: Best-so-far Curve ---\n", + " fig1, ax1 = plt.subplots(figsize=(10, 5))\n", + " fig1.patch.set_facecolor(bg_color)\n", + " ax1.set_facecolor(bg_color)\n", + "\n", + " ax1.plot(trace, color=accent_color, linewidth=2.5, label='Energy Path')\n", + "\n", + "#Highlight the minimum energy point\n", + " ax1.scatter(best_iteration, best_e, color='blue', s=100, zorder=5, \n", + " label=f'Min Energy ({best_iteration}, {best_e})')\n", + "\n", + "#Annotate the minimum point\n", + " ax1.text(best_iteration + (len(trace)*0.05), best_e + (max(trace)*0.05), \n", + " f'Minimum energy at MTS({best_iteration}, {best_e})', \n", + " fontsize=11, color='blue', fontweight='bold')\n", + "\n", + " # Formatting\n", + " ax1.set_xlabel(\"MTS Iteration\", color=accent_color, fontweight='bold')\n", + " ax1.set_ylabel(\"Best-so-far Energy E\", color=accent_color, fontweight='bold')\n", + " ax1.set_title(\"Classical MTS Optimization Path\", color=accent_color, fontsize=14, fontweight='bold')\n", + " ax1.grid(True, linestyle='--', alpha=0.7, color=grid_color)\n", + " ax1.tick_params(colors=accent_color)\n", + " ax1.set_xlim(left=0) # Ensure X-axis starts at 0\n", + "#Modern area fill\n", + " ax1.fill_between(range(len(trace)), trace, color=bar_color, alpha=0.15)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " # --- Plot 2: Final Population Energy Distribution ---\n", + " fig2, ax2 = plt.subplots(figsize=(10, 5))\n", + " fig2.patch.set_facecolor(bg_color)\n", + " ax2.set_facecolor(bg_color)\n", + "\n", + " # Histogram\n", + " ax2.hist(pop_e, bins=20, color=bar_color, edgecolor=bg_color, linewidth=1.2)\n", + "\n", + " # Formatting\n", + " ax2.set_xlabel(\"Energy E\", color=accent_color, fontweight='bold')\n", + " ax2.set_ylabel(\"Candidate Count\", color=accent_color, fontweight='bold')\n", + " ax2.set_title(\"Final Population Energy Distribution\", color=accent_color, fontsize=14, fontweight='bold')\n", + " ax2.grid(axis='y', linestyle='--', alpha=0.7, color=grid_color)\n", + " ax2.tick_params(colors=accent_color)\n", + "\n", + " # Clean up the spines\n", + " for spine in ax2.spines.values():\n", + " spine.set_edgecolor(accent_color)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "#Usage:\n", + "visualize_classical_mts_results(res)" + ] + }, + { + "cell_type": "markdown", + "id": "5da2352b-7836-4af5-a7d3-5050a83775f7", + "metadata": {}, + "source": [ + "## Building a Quantum Enhanced Workflow\n", + "\n", + "Despite the effectiveness of MTS, it still exhibits exponential scaling $O(1.34^N)$ behavior and becomes intractable for large $N$. Quantum computing provides a potential alternative method for solving the LABS problem because the properties of entanglement, interference, and superpositon may allow for a better global search. Recent demonstrations have even produced evidence that the quantum approximate optimization algorithm (QAOA) can be used to reduce the scaling of the LABS problem to $O(1.21^N)$ for $N$ between 28 and 40 with quantum minimum finding.\n", + "\n", + "However, current quantum hardware limitations restrict solution to problems of greater than about $N=20$, meaning that it will be some time before quantum approaches can outperform the classical state of the art. It should also be noted that standard QAOA can struggle with LABS and require many layers to converge the parameters if other tricks are not employed.\n", + "\n", + "The authors of [Scaling advantage with quantum-enhanced memetic tabu search for LABS](https://arxiv.org/html/2511.04553v1) cleverly explored an alternate path that combines quantum and classical approaches and might be able to provide a more near-term benefit. Instead of expecting the quantum computer to solve the problem entirely, they asked how a quantum approach might enhance MTS.\n", + "\n", + "The basic idea is that a quantum optimization routine could run first and the resulting state be sampled to produce a better population for MTS. Many such heuristics for defining the initial population are possible, but the rest of this notebook will explore their methodology, help you to build the workflow yourself, and allow you to analyze the benefits of their approach.\n", + "\n", + "The first step of quantum enhanced MTS (QE-MTS) is to prepare a circuit with CUDA-Q that approximates the ground state of the Hamiltonian corresponding to the LABS problem. You could do this with any optimization algorithm such as QAOA or using an adiabatic approach. (See the [Quantum Portfolio Optimization](https://github.com/NVIDIA/cuda-q-academic/blob/main/quantum-applications-to-finance/03_qchop.ipynb) CUDA-Q Academic lab for a detailed comparison of these two common approaches.)\n", + "\n", + "The authors of this work opted for an adiabatic approach (More on why later). Recall that the goal of an adiabatic optimization is to begin with a Hamiltonian that has an easily prepared ground state ($H_i$). Then, the adiabatic Hamiltonian $H_{ad}$ can be constructed as $H_{ad}(\\lambda) = (1-\\lambda)H_i +\\lambda H_f $, where $\\lambda$ is a function of time and $H_f$ is the Hamiltonian representing a qubit encoding of the LABS problem. \n", + "\n", + "$$H_f = 2 \\sum_{i=1}^{N-2} \\sigma_i^z \\sum_{k=1}^{\\lfloor \\frac{N-i}{2} \\rfloor} \\sigma_{i+k}^z \n", + "+ 4 \\sum_{i=1}^{N-3} \\sigma_i^z \\sum_{t=1}^{\\lfloor \\frac{N-i-1}{2} \\rfloor} \\sum_{k=t+1}^{N-i-t} \\sigma_{i+t}^z \\sigma_{i+k}^z \\sigma_{i+k+t}^z$$\n", + "\n", + "The authors also selected $H_i = \\sum_i h^x_i \\sigma^x_i $ which has an easily prepared ground state of $\\ket{+}^{\\otimes N}$.\n", + "\n", + "The challenge for implementing the optimization procedure becomes selection of an operator that will quickly and accurately evolve to the ground state of $H_f$. One approach is to use a so-called auxiliary countradiabatic (CD) term $H_{CD}$, which corrects diabatic transitions that jump out of the ground state during the evolution. The figure below demonstrates the benefit of using a CD correction.\n", + "\n", + "\n", + "\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": 25, + "id": "c91bbaae-62a1-41e7-a285-af885627a942", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO Write CUDA-Q kernels to apply the 2 and 4 qubit operators.\n", + "# define rzz\n", + "@cudaq.kernel\n", + "def rzz(q0: cudaq.qubit, q1: cudaq.qubit, theta: float):# for pi/2\n", + " x.ctrl(q0, q1) \n", + " rz(theta, q1) \n", + " x.ctrl(q0, q1)\n", + "\n", + "@cudaq.kernel\n", + "def two_qubit_rotation_block(q0: cudaq.qubit, q1: cudaq.qubit, theta: float):\n", + " pi = np.pi\n", + " rx(pi/2.0, q1)\n", + " rzz(q0, q1, theta)\n", + " rx(pi/2.0, q0)\n", + " rx(-pi/2.0, q1)\n", + " rzz(q0, q1, theta)\n", + " rx(-pi/2.0, q0)\n", + "\n", + "@cudaq.kernel\n", + "def four_qubit_rotation_block(q0: cudaq.qubit, q1: cudaq.qubit, q2: cudaq.qubit, q3: cudaq.qubit, theta: float):\n", + " pi = np.pi\n", + " rx(-pi/2.0, q0)\n", + " ry(pi/2.0, q1)\n", + " ry(-pi/2.0, q2)\n", + " \n", + " rzz(q0, q1, -pi/2.0)\n", + " rzz(q2, q3, -pi/2.0)\n", + "\n", + " rx(pi/2.0, q0)\n", + " ry(-pi/2.0, q1) \n", + " ry(pi/2.0, q2)\n", + " rx(-pi/2.0, q3) \n", + "\n", + " rx(-pi/2.0, q1) \n", + " rx(-pi/2.0, q2) \n", + " \n", + " rzz(q1, q2, theta)\n", + "\n", + " rx(pi/2.0, q1) \n", + " rx(pi, q2) \n", + "\n", + " ry(pi/2.0, q1)\n", + " \n", + " rzz(q0, q1, pi/2.0)\n", + " \n", + " rx(pi/2.0, q0)\n", + " ry(-pi/2.0, q2) \n", + " \n", + " rzz(q1, q2, -theta)\n", + " \n", + " rx(pi/2.0, q1)\n", + " rx(-pi, q2) \n", + " \n", + " rzz(q1, q2, -theta)\n", + " \n", + " rx(-pi, q1)\n", + " ry(pi/2.0, q2)\n", + " \n", + " rzz(q2, q3, -pi/2.0)\n", + " \n", + " ry(-pi/2.0, q2) # Ry_dagger\n", + " rx(-pi/2.0, q3)\n", + "\n", + " rx(-pi/2.0, q2)\n", + " \n", + " rzz(q1, q2, theta)\n", + "\n", + " rx(pi/2.0, q1)\n", + " rx(pi/2.0, q2)\n", + " \n", + " ry(-pi/2.0, q1)\n", + " ry(pi/2.0, q2)\n", + " \n", + " rzz(q0, q1, pi/2.0)\n", + " rzz(q2, q3, pi/2.0)\n", + "\n", + " \n", + " ry(pi/2.0, q1)\n", + " ry(-pi/2.0, q2)\n", + " rx(pi/2.0, q3)" + ] + }, + { + "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": 26, + "id": "abf34168-42f9-4dbf-86e1-99976232ad7e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N=6: |G2|=6, |G4|=7\n", + " G2 sample: [[0, 1], [0, 2], [1, 2], [1, 3], [2, 3], [3, 4]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 2, 3, 5], [1, 2, 3, 4], [1, 2, 4, 5]]\n", + "----------------------------------------\n", + "N=7: |G2|=9, |G4|=13\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 2, 3, 5], [0, 2, 4, 6]]\n", + "----------------------------------------\n", + "N=8: |G2|=12, |G4|=22\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [1, 4]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 1, 6, 7], [0, 2, 3, 5]]\n", + "----------------------------------------\n", + "N=10: |G2|=20, |G4|=50\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [1, 3]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 1, 6, 7], [0, 1, 7, 8]]\n", + "----------------------------------------\n" + ] + } + ], + "source": [ + "def get_interactions(N: int):\n", + " \"\"\"\n", + " Generates the interaction sets G2 and G4 based on the loop limits in Eq.15 (noted in the notebook).\n", + " Returns standard 0-based indices as lists of lists of ints.\n", + "\n", + " Args:\n", + " N (int): Sequence length.\n", + "\n", + " Returns:\n", + " G2: List of [i, i+k] two-body index pairs (0-based).\n", + " G4: List of [i, i+t, i+k, i+k+t] four-body index quadruples (0-based).\n", + " \"\"\"\n", + " if not isinstance(N, int) or N < 4:\n", + " # N must be at least 4 for non-empty G4; G2 exists for N>=3, but we choose N>=4 here as safe min.\n", + " raise ValueError(\"N must be an integer >= 4\")\n", + "\n", + " G2 = []\n", + " G4 = []\n", + "\n", + " # Build G2\n", + " # 1-based loops in the guide: i = 1..N-2, k = 1..floor((N-i)/2)\n", + " # Convert to 0-based: i0 = i-1 in range(0, N-2), k runs 1..floor((N-(i))/2)\n", + " for i0 in range(0, N - 2): # i0 corresponds to i=1..N-2\n", + " max_k = (N - (i0 + 1)) // 2 # floor((N-i)/2) where i = i0+1\n", + " for k in range(1, max_k + 1):\n", + " j0 = i0 + k\n", + " # sanity: ensure indices inside [0, N-1]\n", + " if 0 <= j0 < N:\n", + " G2.append([i0, j0])\n", + "\n", + " # Build G4\n", + " # 1-based loops in the guide: i = 1..N-3, t = 1..floor((N-i-1)/2), k = t+1..N-i-t\n", + " # Convert to 0-based: i0 in 0..N-4, t,k as below.\n", + " for i0 in range(0, N - 3): # i0 corresponds to i=1..N-3\n", + " max_t = (N - (i0 + 1) - 1) // 2 # floor((N-i-1)/2) with i=i0+1\n", + " for t in range(1, max_t + 1):\n", + " # k loop (1-based): k = t+1 .. N - i - t\n", + " # convert bounds to 0-based arithmetic: iterate over k in that integer range\n", + " k_min = t + 1\n", + " k_max = (N - (i0 + 1) - t) # inclusive in 1-based formula\n", + " # k_min..k_max (both inclusive) in 1-based; iterate accordingly\n", + " for k in range(k_min, k_max + 1):\n", + " # map to 0-based indices for the four-body term:\n", + " # (i, i+t, i+k, i+k+t) in 1-based -> subtract 1 for 0-based:\n", + " a = i0\n", + " b = i0 + t\n", + " c = i0 + k\n", + " d = i0 + k + t\n", + " # ensure within bounds\n", + " if 0 <= a < N and 0 <= b < N and 0 <= c < N and 0 <= d < N:\n", + " G4.append([a, b, c, d])\n", + "\n", + " # Sanity checks (counts)\n", + " # expected_G2_len = sum_{i=1}^{N-2} floor((N-i)/2)\n", + " expected_G2 = sum(((N - i) // 2) for i in range(1, N - 1))\n", + " # expected_G4_len = sum_{i=1}^{N-3} sum_{t=1}^{floor((N-i-1)/2)} (N - i - 2*t)\n", + " expected_G4 = 0\n", + " for i in range(1, N - 2):\n", + " max_t = (N - i - 1) // 2\n", + " for t in range(1, max_t + 1):\n", + " expected_G4 += max(0, N - i - 2 * t)\n", + "\n", + " assert len(G2) == expected_G2, f\"G2 length mismatch: got {len(G2)}, expected {expected_G2}\"\n", + " assert len(G4) == expected_G4, f\"G4 length mismatch: got {len(G4)}, expected {expected_G4}\"\n", + "\n", + " return G2, G4\n", + "\n", + "\n", + "# Quick usage example/smoke test:\n", + "\n", + "if __name__ == \"__main__\":\n", + " for Ntest in [6, 7, 8, 10]:\n", + " G2, G4 = get_interactions(Ntest)\n", + " print(f\"N={Ntest}: |G2|={len(G2)}, |G4|={len(G4)}\")\n", + " # print a few examples\n", + " print(\" G2 sample:\", G2[:6])\n", + " print(\" G4 sample:\", G4[:6])\n", + " print(\"-\" * 40)\n" + ] + }, + { + "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": 27, + "id": "8c2e6d7d-03b2-482f-bc36-d572e6d4855a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First 10 thetas: [1.5394042220828748e-18]\n", + "the size of population: 813\n", + "first 5 MTS instance: ['111111111111', '111111110111', '111111110010', '111111101110', '111111101010']\n" + ] + } + ], + "source": [ + "@cudaq.kernel\n", + "def trotterized_circuit(N: int, G2: list[list[int]], G4: list[list[int]], steps: int, dt: float, T: float, thetas: list[float]):\n", + " \n", + " reg = cudaq.qvector(N)\n", + " h(reg)\n", + "\n", + " nums_g2 = len(G2)\n", + " nums_g4 = len(G4)\n", + " \n", + " for n in range(steps): # fomula:[1, n]\n", + " theta = thetas[n]\n", + " for i in range(nums_g2):\n", + " pair = G2[i] # fomula:[1,N-2] for i and # [1,(N - i) // 2 ] for k\n", + " two_qubit_rotation_block(reg[pair[0]], reg[pair[1]], theta)\n", + " \n", + " for j in range(nums_g4):#[1,N-3] for j;#[1,(N-i-1)/2] for t and #[t+1,N-i-t]for k\n", + " quad = G4[j]\n", + " four_qubit_rotation_block(reg[quad[0]], reg[quad[1]], reg[quad[2]], reg[quad[3]], theta) \n", + "\n", + "# test it\n", + "T=1 # total time\n", + "n_steps = 1 # number of trotter steps\n", + "dt = T / n_steps\n", + "N = 12 # change from 20\n", + "G2, G4 = get_interactions(N)\n", + "\n", + "thetas =[]\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", + "print(f\"First 10 thetas: {thetas[:10]}\")\n", + "\n", + "# TODO - Sample your kernel to make sure it works\n", + "counts = cudaq.sample(trotterized_circuit, N, G2, G4, n_steps, dt, T, thetas)\n", + "population = [bits for bits, count in counts.items()]\n", + "\n", + "print(f\"the size of population: {len(population)}\")\n", + "print(f\"first 5 MTS instance: {population[:5]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f626db2c-6a8c-40a0-acdf-6c36404dfa49", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "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": 35, + "id": "8e5f02e6-41bf-4634-9cb1-f0f543ef2e3f", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'best_S' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[35], line 105\u001b[0m\n\u001b[1;32m 102\u001b[0m \u001b[38;5;66;03m# Run Just MTS\u001b[39;00m\n\u001b[1;32m 104\u001b[0m N \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m12\u001b[39m\n\u001b[0;32m--> 105\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[43mmts_labs_pm1\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 106\u001b[0m \u001b[43m \u001b[49m\u001b[43mN\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mN\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 107\u001b[0m \u001b[43m \u001b[49m\u001b[43mpop_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m32\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 108\u001b[0m \u001b[43m \u001b[49m\u001b[43mp_combine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.7\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 109\u001b[0m \u001b[43m \u001b[49m\u001b[43mp_mut\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[38;5;241;43m50.0\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 110\u001b[0m \u001b[43m \u001b[49m\u001b[43mmts_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m400\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 111\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m800\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 112\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_tenure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m30\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 113\u001b[0m \u001b[43m \u001b[49m\u001b[43mcandidate_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m64\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 114\u001b[0m \u001b[43m \u001b[49m\u001b[43mseed\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m42\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 115\u001b[0m \u001b[43m \u001b[49m\u001b[43mverbose_every\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m50\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 116\u001b[0m \u001b[43m)\u001b[49m\n\u001b[1;32m 117\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mClassical MTS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 118\u001b[0m visualize_mts(res)\n", + "Cell \u001b[0;32mIn[34], line 204\u001b[0m, in \u001b[0;36mmts_labs_pm1\u001b[0;34m(N, pop_size, p_combine, p_mut, mts_iters, tabu_iters, tabu_tenure, candidate_size, target_E, seed, verbose_every)\u001b[0m\n\u001b[1;32m 201\u001b[0m child \u001b[38;5;241m=\u001b[39m mutate_alg3(child, p_mut, rng)\n\u001b[1;32m 203\u001b[0m \u001b[38;5;66;03m# ---- Tabu Search with Child ----\u001b[39;00m\n\u001b[0;32m--> 204\u001b[0m result_s, result_E \u001b[38;5;241m=\u001b[39m \u001b[43mtabu_search_pm1\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 205\u001b[0m \u001b[43m \u001b[49m\u001b[43mchild\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 206\u001b[0m \u001b[43m \u001b[49m\u001b[43mmax_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtabu_iters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 207\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_tenure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtabu_tenure\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 208\u001b[0m \u001b[43m \u001b[49m\u001b[43mcandidate_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcandidate_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 209\u001b[0m \u001b[43m \u001b[49m\u001b[43mrng\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrng\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 210\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 212\u001b[0m \u001b[38;5;66;03m# ---- Update best solution ----\u001b[39;00m\n\u001b[1;32m 213\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m result_E \u001b[38;5;241m<\u001b[39m best_E_Class:\n", + "Cell \u001b[0;32mIn[34], line 112\u001b[0m, in \u001b[0;36mtabu_search_pm1\u001b[0;34m(s0, max_iters, tabu_tenure, candidate_size, rng)\u001b[0m\n\u001b[1;32m 110\u001b[0m best_s \u001b[38;5;241m=\u001b[39m s\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[1;32m 111\u001b[0m best_E_Class \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(E)\n\u001b[0;32m--> 112\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mbest_S\u001b[49m)\n\u001b[1;32m 114\u001b[0m N \u001b[38;5;241m=\u001b[39m s\u001b[38;5;241m.\u001b[39msize\n\u001b[1;32m 115\u001b[0m tabu_until \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mzeros(N, dtype\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39mint32)\n", + "\u001b[0;31mNameError\u001b[0m: name 'best_S' is not defined" + ] + } + ], + "source": [ + "# TODO - write code here to sample from your CUDA-Q kernel and used the results to seed your MTS population\n", + "\n", + "# updated the MTS main loop \n", + "\n", + "def mts_quant1(\n", + " N: int,\n", + " pop_size: int = 32,\n", + " initial_pop: np.ndarray = None, # for quantum algo output \n", + " p_combine: float = 0.7,\n", + " p_mut: float = 1.0/50.0,\n", + " mts_iters: int = 1000,\n", + " tabu_iters: int = 800,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " target_E: int | None = None,\n", + " seed: int = 0,\n", + " verbose_every: int = 100,\n", + "):\n", + " ## change\n", + " rng = np.random.default_rng(seed)\n", + " ## check if have pop or not\n", + " if initial_pop is not None:\n", + " print(f\"Using Quantum-enhanced population (size: {len(initial_pop)})\")\n", + " pop = initial_pop.copy()\n", + " else:\n", + " print(\"Using Randomly generated population.\")\n", + " pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + "\n", + " if pop.shape[0] < pop_size:\n", + " extra_count = pop_size - pop.shape[0]\n", + " extra = rng.choice(np.array([-1, 1], dtype=np.int8), size=(extra_count, N))\n", + " pop = np.vstack([pop, extra])\n", + " \n", + " # init population: k random bitstrings\n", + " #pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + " pop_E = np.array([labs_energy_pm1(pop[i]) for i in range(pop_size)], dtype=np.int64)\n", + "\n", + " best_idx = int(np.argmin(pop_E))\n", + " best_s = pop[best_idx].copy()\n", + " best_E = int(pop_E[best_idx])\n", + "\n", + " trace = [best_E]\n", + " t0 = time.time()\n", + "\n", + " for it in range(1, mts_iters + 1):\n", + " if target_E is not None and best_E <= target_E:\n", + " break\n", + "\n", + " # ---- Make Child ----\n", + " if rng.random() < p_combine:\n", + " i1, i2 = rng.integers(0, pop_size, size=2)\n", + " child = combine_alg3(pop[i1], pop[i2], rng)\n", + " else:\n", + " i = int(rng.integers(0, pop_size))\n", + " child = pop[i].copy()\n", + "\n", + " # ---- Mutate Child ----\n", + " child = mutate_alg3(child, p_mut, rng)\n", + "\n", + " # ---- Tabu Search with Child ----\n", + " result_s, result_E = tabu_search_pm1(\n", + " child,\n", + " max_iters=tabu_iters,\n", + " tabu_tenure=tabu_tenure,\n", + " candidate_size=candidate_size,\n", + " rng=rng,\n", + " )\n", + "\n", + " # ---- Update best solution ----\n", + " if result_E < best_E:\n", + " best_E = int(result_E)\n", + " best_s = result_s.copy()\n", + "\n", + " # ---- Add result to Population ----\n", + " # randomly replace a member if result is better\n", + " r = int(rng.integers(0, pop_size))\n", + " if result_E < pop_E[r]:\n", + " pop[r] = result_s\n", + " pop_E[r] = result_E\n", + "\n", + " # (optional, helpful) elitism: keep global best in population\n", + " worst = int(np.argmax(pop_E))\n", + " if best_E < pop_E[worst]:\n", + " pop[worst] = best_s\n", + " pop_E[worst] = best_E\n", + "\n", + " trace.append(best_E)\n", + "\n", + " if verbose_every and (it % verbose_every == 0):\n", + " print(f\"[MTS {it:5d}] best_E={best_E} elapsed={time.time()-t0:.2f}s\")\n", + "\n", + " return {\n", + " \"best_s_pm1\": best_s,\n", + " \"best_s_01\": pm1_to_bits01(best_s),\n", + " \"best_E\": best_E,\n", + " \"best_trace\": np.array(trace, dtype=np.int64),\n", + " \"population_pm1\": pop,\n", + " \"population_E\": pop_E.copy(),\n", + " \"elapsed_sec\": time.time() - t0,\n", + " }\n", + "\n", + "# Run Just MTS\n", + "\n", + "N = 12\n", + "res = mts_labs_pm1(\n", + " N=N,\n", + " pop_size=32,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "print(\"Classical MTS\")\n", + "visualize_mts(res)\n", + "\n", + "# Run cp + MTS \n", + "# test it\n", + "T=1 # total time\n", + "n_steps = 1 # number of trotter steps\n", + "dt = T / n_steps\n", + "# N\n", + "G2, G4 = get_interactions(N)\n", + "\n", + "thetas =[]\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", + "\n", + "# TODO - Sample your kernel to make sure it works\n", + "counts_6 = cudaq.sample(trotterized_circuit, N, G2, G4, n_steps, dt, T, thetas)\n", + "quantum_pop_list = []\n", + "for bitstring, count in counts_6.items():\n", + " bits_01 = np.array([int(b) for b in bitstring], dtype=np.int8)\n", + " s_pm1 = bits01_to_pm1(bits_01)\n", + " quantum_pop_list.append(s_pm1)\n", + "initial_quantum_pop = np.array(quantum_pop_list)\n", + "#N = \n", + "res = mts_quant1(\n", + " N=N,\n", + " pop_size=32,\n", + " initial_pop=initial_quantum_pop,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "print(\"CP + MTS\")\n", + "visualize_mts(res)\n" + ] + }, + { + "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": {}, + "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?" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d34db9f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Starting Validation Suite for N = 7 ---\n", + "Symmetry Check:\n", + " Original Energy: 19\n", + " Bit-flip Energy: 19 (Match: True)\n", + " Reversal Energy: 19 (Match: True)\n", + " Both Match: 19 (Match: True)\n", + "\n", + "Quantum-Classical Cross-Reference:\n", + " Sampled Bitstring: 1001010 \n", + " Computed Energy: 23\n", + " Status: Success (Kernel produced valid measurable state)\n", + "\n", + "Known Solution Verification (N=4 Barker):\n", + " Expected: 2, Got: 2 (Match: True)\n" + ] + } + ], + "source": [ + "import unittest\n", + "import numpy as np\n", + "\n", + "def validation_suite(N_val = 7):\n", + " print(f\"--- Starting Validation Suite for N = {N_val} ---\")\n", + " \n", + " # 1. Symmetry Checks\n", + " # LABS energy should be invariant under:\n", + " # - Bit-flip: s -> -s\n", + " # - Reversal: s_i -> s_{N-i+1}\n", + " test_seq = np.random.choice( [-1, 1], size = N_val)\n", + " energy_orig = labs_energy_pm1(test_seq )\n", + " \n", + " # Bit-flip symmetry\n", + " energy_flip = labs_energy_pm1(-test_seq )\n", + " # Reversal symmetry\n", + " energy_rev = labs_energy_pm1(test_seq[::-1])\n", + " # Combined symmetry: Bit-flip + Reversal\n", + " energy_both = labs_energy_pm1(-test_seq[::-1])\n", + " \n", + " print(f\"Symmetry Check:\")\n", + " print(f\" Original Energy: {energy_orig}\")\n", + " print(f\" Bit-flip Energy: {energy_flip} (Match: {energy_orig == energy_flip})\")\n", + " print(f\" Reversal Energy: {energy_rev} (Match: {energy_orig == energy_rev})\")\n", + " print(f\" Both Match: {energy_both } (Match: {energy_orig == energy_both})\")\n", + "\n", + " # 2. CUDA-Q vs Classical Cross-Reference\n", + " # We sample the quantum kernel and ensure the bitstrings produced \n", + " # are evaluated correctly by the classical energy function.\n", + " print(f\"\\nQuantum-Classical Cross-Reference:\")\n", + " # Using small parameters for quick validation\n", + " G2_v, G4_v = get_interactions(N_val)\n", + " # Ensure thetas matches the step count used in the test\n", + " test_steps = 1\n", + " test_thetas = [0.5] # Dummy theta for structural test\n", + " \n", + " # Use cudaq.sample to get the distribution\n", + " counts = cudaq.sample( trotterized_circuit, N_val, G2_v, G4_v, test_steps, 1.0, 1.0, test_thetas)\n", + " \n", + " # Updated: Robust way to get the most frequent bitstring\n", + " # counts.items() returns a list of (bitstring, count) tuples\n", + " sample_bitstring = max(counts.items() , key=lambda item: item[1])[0]\n", + " \n", + " # Convert bitstring '0101...' to pm1 array\n", + " s_sample = bits01_to_pm1( np.array([ int(b) for b in sample_bitstring] ))\n", + " energy_sample = labs_energy_pm1(s_sample )\n", + " print(f\" Sampled Bitstring: {sample_bitstring} \")\n", + " print(f\" Computed Energy: {energy_sample}\")\n", + " print(f\" Status: Success (Kernel produced valid measurable state)\")\n", + "\n", + " # 3. Small n Hand-Verification (N=4)\n", + " # For N=4, the Barker sequence [1, 1, 1, -1] has energy 2.\n", + " # C1=1, C2=0 , C3=-1 -> 1^2 + 0^2 + (-1)^2 =2\n", + " s_barker = np.array([1, 1, 1, -1])\n", + " e_barker = labs_energy_pm1( s_barker)\n", + " print(f\"\\nKnown Solution Verification (N=4 Barker):\")\n", + " print(f\" Expected: 2, Got: {e_barker} (Match: {e_barker == 2})\")\n", + "\n", + "# Run the validation\n", + "validation_suite()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "164b5fa0-fa04-417f-a008-dd5232226a5c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Starting Validation Suite (N=7) ---\n", + "[TEST 1] Barker Sequence (N=4): Expected Energy 2, Got 2.\n", + "[TEST 2] Symmetry Check - Bit-flip: PASSED\n", + "[TEST 3] Symmetry Check - Reversal: PASSED\n", + "[TEST 4] Quantum Cross-Ref: Bitstring [1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1] has Classical Energy 10\n" + ] + } + ], + "source": [ + "\n", + "from validations import run_validation\n", + "\n", + "# Example: pass the best bitstring from quantum sampler to cross-reference it\n", + "# best_bitstring = \"1110\" \n", + "run_validation(N=7, quantum_sample_bitstring=res[\"best_s_01\"].tolist())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15bc79e9-120f-4b5a-b05a-7021aedf787a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 [cuda-q-v0.13.0]", + "language": "python", + "name": "python3_0pt5" + }, + "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/.ipynb_checkpoints/validations-checkpoint.py b/tutorial_notebook/.ipynb_checkpoints/validations-checkpoint.py new file mode 100644 index 00000000..68329f67 --- /dev/null +++ b/tutorial_notebook/.ipynb_checkpoints/validations-checkpoint.py @@ -0,0 +1,53 @@ +import numpy as np + +def labs_energy_pm1(s): + """ + Calculates the energy of a binary sequence s in {+1, -1}. + E(s) = sum_{k=1}^{N-1} (sum_{i=1}^{N-k} s_i * s_{i+k})^2 + """ + N = len(s) + total_energy = 0 + for k in range(1, N): + ck = 0 + for i in range(N - k): + ck += s[i] * s[i+k] + total_energy += ck**2 + return total_energy + +def bits01_to_pm1(bits): + """Converts a bitstring of 0s and 1s to +1s and -1s.""" + return 1 - 2 * np.array(bits) + +def run_validation(N=7, quantum_sample_bitstring=None): + print(f"--- Starting Validation Suite (N={N}) ---") + + # 1. Small N Hand-Calculation Verification + # For N=4, the sequence [1, 1, 1, -1] is a known Barker sequence with Energy = 2. + s_barker = np.array([1, 1, 1, -1]) + e_barker = labs_energy_pm1(s_barker) + print(f"[TEST 1] Barker Sequence (N=4): Expected Energy 2, Got {e_barker}.") + assert e_barker == 2, "Energy calculation failed on known N=4 case." + + # 2. Check Symmetries + # The LABS problem is invariant under bit-flip (s -> -s) and reversal. + test_seq = np.random.choice([-1, 1], size=N) + e_orig = labs_energy_pm1(test_seq) + + e_flip = labs_energy_pm1(-test_seq) + e_rev = labs_energy_pm1(test_seq[::-1]) + + print(f"[TEST 2] Symmetry Check - Bit-flip: {'PASSED' if e_orig == e_flip else 'FAILED'}") + print(f"[TEST 3] Symmetry Check - Reversal: {'PASSED' if e_orig == e_rev else 'FAILED'}") + + # 3. Quantum-Classical Cross-Reference + if quantum_sample_bitstring: + # Convert bitstring (e.g., '101') to numerical array + bits = [int(b) for b in quantum_sample_bitstring] + s_quantum = bits01_to_pm1(bits) + e_quantum = labs_energy_pm1(s_quantum) + print(f"[TEST 4] Quantum Cross-Ref: Bitstring {quantum_sample_bitstring} has Classical Energy {e_quantum}") + else: + print("[TEST 4] Quantum Cross-Ref: Skipped (No sample provided)") + +if __name__ == "__main__": + run_validation() \ No newline at end of file diff --git a/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb b/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb index f96cb45c..c3ce8f37 100644 --- a/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb +++ b/tutorial_notebook/01_quantum_enhanced_optimization_LABS.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b1f70ff2-492a-41a4-97db-5da6d5775cb7", "metadata": {}, "outputs": [], @@ -61,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 22, "id": "bfc407dd-113d-485c-88db-7ddbad344ead", "metadata": {}, "outputs": [], @@ -140,12 +140,463 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "id": "341c9a3f-565c-49ab-9903-0aa9a508722c", "metadata": {}, "outputs": [], "source": [ - "#TODO - Write code to perform MTS" + "#TODO - Write code to perform MTS\n", + "import numpy as np\n", + "import time\n", + "import matplotlib.pyplot as plt\n", + "import itertools\n", + "\n", + "# 1) LABS objective for ±1 sequences\n", + "\n", + "def pm1_to_bits01(s_pm1: np.ndarray) -> np.ndarray:\n", + " return ((s_pm1 + 1) // 2).astype(np.int8)\n", + "\n", + "def bits01_to_pm1(bits01) -> np.ndarray:\n", + " x = np.array(bits01, dtype=np.int8)\n", + " return (2*x - 1).astype(np.int8) # 0->-1, 1->+1\n", + "\n", + "def labs_correlations_pm1(s: np.ndarray) -> np.ndarray:\n", + " \"\"\"C[k-1] = C_k for k=1..N-1, C_k = sum_i s[i]*s[i+k].\"\"\"\n", + " N = s.size\n", + " C = np.empty(N-1, dtype=np.int32)\n", + " for k in range(1, N):\n", + " C[k-1] = int(np.dot(s[:-k], s[k:]))\n", + " return C\n", + "\n", + "def labs_energy_from_C(C: np.ndarray) -> int:\n", + " C64 = C.astype(np.int64)\n", + " return int(np.sum(C64*C64))\n", + "\n", + "def labs_energy_pm1(s: np.ndarray) -> int:\n", + " return labs_energy_from_C(labs_correlations_pm1(s))\n", + "\n", + "def aperiodic_autocorr_full(s_pm1: np.ndarray) -> np.ndarray:\n", + " \"\"\"lags = -(N-1)..+(N-1)\"\"\"\n", + " N = len(s_pm1)\n", + " out = []\n", + " for lag in range(-(N-1), N):\n", + " if lag < 0:\n", + " out.append(int(np.dot(s_pm1[-lag:], s_pm1[:N+lag])))\n", + " elif lag == 0:\n", + " out.append(int(np.dot(s_pm1, s_pm1)))\n", + " else:\n", + " out.append(int(np.dot(s_pm1[:-lag], s_pm1[lag:])))\n", + " return np.array(out, dtype=int)\n", + "\n", + "\n", + "# 2) Algorithm 3: Combine & Mutate \n", + "\n", + "\n", + "def combine_alg3(p1: np.ndarray, p2: np.ndarray, rng: np.random.Generator) -> np.ndarray:\n", + " N = p1.size\n", + " k = int(rng.integers(1, N)) # k in {1,...,N-1}\n", + " child = np.empty_like(p1)\n", + " child[:k] = p1[:k]\n", + " child[k:] = p2[k:]\n", + " return child\n", + "\n", + "def mutate_alg3(s: np.ndarray, p_mut: float, rng: np.random.Generator) -> np.ndarray:\n", + " out = s.copy()\n", + " if p_mut <= 0.0:\n", + " return out\n", + " mask = rng.random(out.size) < p_mut\n", + " out[mask] *= -1\n", + " return out\n", + "\n", + "# 3) Tabu Search (single-bit flip neighborhood)\n", + "# - aspiration: allow tabu move if it improves best found in this tabu run\n", + "# - candidate_size: evaluate subset of flips each step (CPU-friendly)\n", + "\n", + "\n", + "def delta_energy_single_flip_pm1(s: np.ndarray, C: np.ndarray, E: int, j: int):\n", + " \"\"\"\n", + " After flipping s[j], update correlations deltaC and energy E_new.\n", + " Flip affects C_k terms that involve index j:\n", + " (j, j+k) and (j-k, j) when in bounds.\n", + " Each affected product changes sign => delta contribution = -2*old_term.\n", + " \"\"\"\n", + " N = s.size\n", + " sj = int(s[j])\n", + " deltaC = np.zeros_like(C, dtype=np.int32)\n", + "\n", + " for k in range(1, N):\n", + " d = 0\n", + " jp = j + k\n", + " jm = j - k\n", + " if jp < N:\n", + " d += sj * int(s[jp])\n", + " if jm >= 0:\n", + " d += int(s[jm]) * sj\n", + " if d != 0:\n", + " deltaC[k-1] = -2 * d\n", + "\n", + " C64 = C.astype(np.int64)\n", + " d64 = deltaC.astype(np.int64)\n", + " dE = int(np.sum(2*C64*d64 + d64*d64))\n", + " return E + dE, deltaC\n", + "\n", + "def tabu_search_pm1(\n", + " s0: np.ndarray,\n", + " max_iters: int = 1000,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " rng: np.random.Generator | None = None,\n", + "):\n", + " if rng is None:\n", + " rng = np.random.default_rng()\n", + "\n", + " s = s0.copy()\n", + " C = labs_correlations_pm1(s)\n", + " E = labs_energy_from_C(C)\n", + "\n", + " best_s = s.copy()\n", + " best_E = int(E)\n", + " print(best_s)\n", + "\n", + " N = s.size\n", + " tabu_until = np.zeros(N, dtype=np.int32)\n", + "\n", + " for it in range(1, max_iters + 1):\n", + " # choose candidate flip indices\n", + " if candidate_size >= N:\n", + " candidates = np.arange(N)\n", + " else:\n", + " candidates = rng.choice(N, size=candidate_size, replace=False)\n", + "\n", + " chosen_j = None\n", + " chosen_E = None\n", + " chosen_dC = None\n", + "\n", + " # pick best admissible (tabu allowed only if aspiration)\n", + " for j in candidates:\n", + " E_new, dC = delta_energy_single_flip_pm1(s, C, E, int(j))\n", + " is_tabu = tabu_until[j] > it\n", + " if is_tabu and (E_new >= best_E):\n", + " continue\n", + " if (chosen_E is None) or (E_new < chosen_E):\n", + " chosen_j, chosen_E, chosen_dC = int(j), int(E_new), dC\n", + "\n", + " # if all were blocked, ignore tabu\n", + " if chosen_j is None:\n", + " for j in candidates:\n", + " E_new, dC = delta_energy_single_flip_pm1(s, C, E, int(j))\n", + " if (chosen_E is None) or (E_new < chosen_E):\n", + " chosen_j, chosen_E, chosen_dC = int(j), int(E_new), dC\n", + "\n", + " # apply flip\n", + " s[chosen_j] *= -1\n", + " C += chosen_dC\n", + " E = chosen_E\n", + "\n", + " # update tabu tenure (with slight randomness)\n", + " tenure = tabu_tenure + int(rng.integers(0, max(1, tabu_tenure // 3)))\n", + " tabu_until[chosen_j] = it + tenure\n", + "\n", + " if E < best_E:\n", + " best_E = int(E)\n", + " best_s = s.copy()\n", + " print(best_s)\n", + "\n", + " return best_s, best_E\n", + "\n", + "\n", + "# 4) MTS main loop (matches your diagram)\n", + "\n", + "def mts_labs_pm1(\n", + " N: int,\n", + " pop_size: int = 32,\n", + " p_combine: float = 0.7,\n", + " p_mut: float = 1.0/50.0,\n", + " mts_iters: int = 1000,\n", + " tabu_iters: int = 800,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " target_E: int | None = None,\n", + " seed: int = 0,\n", + " verbose_every: int = 100,\n", + "):\n", + " rng = np.random.default_rng(seed)\n", + "\n", + " # init population: k random bitstrings\n", + " pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + " pop_E = np.array([labs_energy_pm1(pop[i]) for i in range(pop_size)], dtype=np.int64)\n", + "\n", + " best_idx = int(np.argmin(pop_E))\n", + " best_s = pop[best_idx].copy()\n", + " print(best_s)\n", + " best_E = int(pop_E[best_idx])\n", + "\n", + " trace = [best_E]\n", + " t0 = time.time()\n", + "\n", + " for it in range(1, mts_iters + 1):\n", + " if target_E is not None and best_E <= target_E:\n", + " break\n", + "\n", + " # ---- Make Child ----\n", + " if rng.random() < p_combine:\n", + " i1, i2 = rng.integers(0, pop_size, size=2)\n", + " child = combine_alg3(pop[i1], pop[i2], rng)\n", + " else:\n", + " i = int(rng.integers(0, pop_size))\n", + " child = pop[i].copy()\n", + "\n", + " # ---- Mutate Child ----\n", + " child = mutate_alg3(child, p_mut, rng)\n", + "\n", + " # ---- Tabu Search with Child ----\n", + " result_s, result_E = tabu_search_pm1(\n", + " child,\n", + " max_iters=tabu_iters,\n", + " tabu_tenure=tabu_tenure,\n", + " candidate_size=candidate_size,\n", + " rng=rng,\n", + " )\n", + "\n", + " # ---- Update best solution ----\n", + " if result_E < best_E:\n", + " best_E = int(result_E)\n", + " best_s = result_s.copy()\n", + "\n", + " # ---- Add result to Population ----\n", + " # randomly replace a member if result is better\n", + " r = int(rng.integers(0, pop_size))\n", + " if result_E < pop_E[r]:\n", + " pop[r] = result_s\n", + " pop_E[r] = result_E\n", + "\n", + " # (optional, helpful) elitism: keep global best in population\n", + " worst = int(np.argmax(pop_E))\n", + " if best_E < pop_E[worst]:\n", + " pop[worst] = best_s\n", + " pop_E[worst] = best_E\n", + "\n", + " trace.append(best_E)\n", + "\n", + " if verbose_every and (it % verbose_every == 0):\n", + " print(f\"[MTS {it:5d}] best_E={best_E} elapsed={time.time()-t0:.2f}s\")\n", + "\n", + " return {\n", + " \"best_s_pm1\": best_s,\n", + " \"best_s_01\": pm1_to_bits01(best_s),\n", + " \"best_E\": best_E,\n", + " \"best_trace\": np.array(trace, dtype=np.int64),\n", + " \"population_pm1\": pop,\n", + " \"population_E\": pop_E.copy(),\n", + " \"elapsed_sec\": time.time() - t0,\n", + " }\n", + "\n", + "\n", + "# 5) Visualization: best curve + final population energy distribution\n", + "\n", + "def visualize_mts(res: dict):\n", + " print(\"Best E Via Classification:\", res[\"best_E\"])\n", + " print(\"Best bitstring (0/1):\", \"\".join(map(str, res[\"best_s_01\"].tolist())))\n", + " print(\"Elapsed (s):\", f\"{res['elapsed_sec']:.3f}\")\n", + "\n", + " trace = res[\"best_trace\"]\n", + " popE = res[\"population_E\"]\n", + "\n", + " plt.figure()\n", + " plt.plot(trace)\n", + " plt.xlabel(\"MTS iteration\")\n", + " plt.ylabel(\"Best-so-far Energy E\")\n", + " plt.title(\"MTS Best-so-far Curve\")\n", + " plt.show()\n", + "\n", + " plt.figure()\n", + " plt.hist(popE, bins=20)\n", + " plt.xlabel(\"Energy E\")\n", + " plt.ylabel(\"Count\")\n", + " plt.title(\"Final Population Energy Distribution\")\n", + " plt.show()\n", + "\n", + "\n", + "# N=7: show a very good and very poor sequence difference\n", + "\n", + "\n", + "def brute_best_worst_N7():\n", + " N = 7\n", + " rows = []\n", + " for bits in itertools.product([0,1], repeat=N):\n", + " s = bits01_to_pm1(bits)\n", + " E = labs_energy_pm1(s)\n", + " rows.append((bits, E))\n", + " minE = min(E for _,E in rows)\n", + " maxE = max(E for _,E in rows)\n", + " good = [b for b,E in rows if E==minE][0]\n", + " bad = [b for b,E in rows if E==maxE][0]\n", + "\n", + " def show(bits, title):\n", + " s = bits01_to_pm1(bits)\n", + " C = labs_correlations_pm1(s)\n", + " E = labs_energy_pm1(s)\n", + " ac = aperiodic_autocorr_full(s)\n", + " print(f\"--- {title} ---\")\n", + " print(\"bits:\", \"\".join(map(str,bits)))\n", + " print(\"E:\", E)\n", + " print(\"Ck k=1..6:\", C.tolist())\n", + " print(\"aperiodic autocorr lags -6..+6:\", ac.tolist())\n", + " print()\n", + "\n", + "\n", + " print(\"N=7 global minE and maxE (by brute force):\", minE, maxE)\n", + " show(good, \"Very good (global optimal)\")\n", + " show(bad, \"Very poor (max energy)\")\n", + " \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da991c1b-0182-41d5-bcfe-1b24d197383d", + "metadata": {}, + "outputs": [], + "source": [ + "# Example runs\n", + "\n", + "\n", + "# 1) N=7 demo (good vs poor)\n", + "brute_best_worst_N7()\n", + "\n", + "# 2) Run MTS (CPU baseline) - start small\n", + "N = 64\n", + "res = mts_labs_pm1(\n", + " N=N,\n", + " pop_size=32,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "visualize_mts(res)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "bb31d339-f315-4fcb-9fe9-99b2f523e081", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best E Found: 10\n", + "First reached at iteration: 0\n" + ] + }, + { + "data": { + "image/png": "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Kqv19MOvSAqf637i/ueroDBfR596kObNWaA3X/xAkjTrtzl659WPtffBO69126ufVy5JNVU1uvv6R+Tz+SVJHbu0j5wDHutnre50g4anGGy5w6baasexzd7/rz6dqqvOv12O40iSuvXsrL0P3lmVlVV67rHXVV1Vo/KyCv3roAv12YyXVVK65vnqv0z7TcWlRTrhrMNUU1Orpx96RcFgUI7j6L5b/y8hTffUr36Mut2hUztJktvj1oYjBmr46CFq275UbYoKVFVZrclf/qAv/5mx/s5rH9LBx+2tLt066siTD9D2u2+pay+8M7KtPQ/aUcNHD5EU/qKoMZ+9/7X6D+qtXfbZVtN/mBn5gmzFsjI988irOu3CY9Z7HwEg0Wi6ASAJFs5bHPm974CekcO318fAIX314Y/Pa96chfph8s9avHCZPF6PxmwxUj9O/TXynJ+8+1Wk6W44MnjVHResMUo1d/b8yGhzTYN1x241ao1zfmtrfVq+dOU661y5olxPPfhS5PbQkRvohY8eVEFhfmTZ/LkLlV+QF7n9xBt3qbqqRlMmTdOcWfNUWVGlHr27apNxI/Tua5/8s1+T1vncserUtb12P2AHvfL0RH363leaNuUXPffYa5KkwcMHJGUm7otvOFMHbHNipNlbVV6pT96dFNm/nNwcHX7ivvrPtac367SDxQuXRt3u3rNLk+t269Ul0nQvXrC0yfUuuOoUnXHRcZKkUy44SlsM3CeS/VMPvqwDjtxDh52w7xpN914H7rher9lN91+qfQ/bVVJ45HTKV9Mi99368OXaff8d5DiONum1W+Qc9R++nb7Opnv188oDgYCO2+fcSAZFxYV68s271aYo3OjF+lmrO92g4SkGG282PKbzyx+843+RhruwTYHe+OoJte/YVpK0/a5b6Kg9w18arFxepheeeEMnnHXYGttwuVx65t3/Ro6qycnJjow+/zB5erNraWjm9D91363/F3VOd8Pn22WfbSTVn3Lx58y/9NP3M7R8yQp5vB5tu+s4fffNT6quqlEgENSXH32r/Y/YXXsdtJMkRTXd2+y8+TpnL+/ao5Nen/R45IupXTc5XD99N+Offfw5rn0EgGSj6QYAI1YsW6lzjh2/xiHbq1s4r35W7DFbjNAv036TJB22y2kaNXaY+gzoqQGD+2jTLUdp8LD+kXVHbDxEOTnZqq316ZN3J2n74Qdp0LD+6juglzYcuYG22G4TdenWcZ11fvfVjwo0OIT61H8fE9VwS1pjVu4Hbn9St135gCorqprer78XN3nf+jjujEP0ytMT5TiOjt/vPK0qDx+ae+xpByfl+UaOGarXvnxMt131gD56+4vIudh1amtq9cjdz6i8rEK3PzI+KTWsy/5H7Bb5vU1RoXbYY0s991j4/Nofv/s1Kc/p8bi150E7Rm53790l0nR7vR7tss+2ksKNXs/eXSNNd9nK8piex3EcnX/CVfpo4peSwl9yPPzybVGXUovns7a+Go4gb7PzZpGGW5K23XWc2nUo1bIlKyRJU776USectcYmNHrssKjTWPpt0Cvye92XAbGaNnm6pjXRsJ97+b8izzd39nydeeSlmjxpWqPr1lmwnp/j/Q7fLepIkD4Dekaa7rIVq9Zr2wCQLDTdAJAEnbt11KzfwjNQ//nbHDmOs97nwZ5/4tXrbAIkqbbWH/n9wmtO05w/5+mjiV+qsqJKn73/tT57v34ir7FbjdLjr9+p/II8deneSbc+coUuP+tmLV+6MnJJszoFhfm68f5L1jmquHK1f9yv61JoE1/9WFdfcMc696tuVDLRRo4ZqpFjhuq7b36KjGCWtivWPoftkpTnk6QNR2ygh1+6VdVVNfr+m5809euf9Mm7kzTpkymRdV544g1dfss5Km1bvNZtdezcPur233MWaMhGAxtdd16Dic86dmnf6DqS1K5BwydJHTq2i/xeU13b6Hno66tdx7ZRlzHz/nMERt19brc7ctvtqf89FHJiep6rL7hDL/4zWut2u3X3k9escb3peD5r62vl8vpLvbXv1HaN+9t3ahtpuptqoLuv9lnLbpBR3Sj6+sjO9oav073pMB118gFRRzWcsP/5kYn61sbX4IiaeKz+96Th+zDqXHgASCPrf7wjAGAN47bbJPJ72YpyvfPPIdLxqqqsjjoPeovtxujzma9odu3XmhuYrI02HtLo49oUFeqJN+7SN7Pf1H3P3qB/X32q9j1s18hhy199OlX33vxEZP29D95Zk+dO1EufPKTrJvxHJ559uIaO3ECSVFlRpQtOvHqto9GS1jjXtOHEWI15/bl3I78XFObrf2/fo98qvtDcwGRdc3fLXNbquDMPibp92PH7Ki9v3Yd2r6+8/Fxtts3GOu3CY/TcB/frvPEnR90/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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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Y5bdMrt6uSS16dhMVWtWvOj7rlG3ilped7X1/umnh76gRQv+UJ361WUEBWnvrv1aufRfbV6/XcOevsvnx3xlpn6T2qpWu5J1MbVXx72j3Tv2qmTp4vpmzk8XfH97/pgo3di3kz56+/Rndaf87xTndl2v8Toa6q8zn3gQfzJBa1dv1OLvl8jlclvr+997s1q1b3LB+zkZd0pta9+sKjUrqF6jmipWoojCwkP11x//6OSJeGu76PzpYbZYbFHr65+/+13/9/hkFSgco4KFY2z7eEG7f4bP/pmTpPtve1INm9aRERSkm2673vpEBV9q1K2iFm0b6/eFyyVJb7z0oXZt36sqNSro1x/Tr14uSQMfuCVLZx8AQHYidANADqnToLpad2imxd+fPvK5ad02TRz9lqTTwe3sozU5JX9MlN6e/aIG3ThcJ+JOKTkpxfrs4zOCgoL0xPNDM716cZFihVS8VFF99NbnGdb1G3Kz13vIb7mzu96e9JH1x/mmtdu0ae02BQUFqVX7pl5XyM6KISP7W2HI4/Ho42lfSpLyRUao0w1tNP/LRT5v16F7a706bro8Ho88Ho/enTxL0unTny8UurPjMbNDZlcvl04fmb6U0C1Jr304Vrddf7/Wrt4kj8ejn775TT99k/nZBP54adoz1ud0m6apz//3olRoqFPN2zSyrlydWZi6477eVug+41LDaWafeBAc7NBDT9+lBx6/06/7O7Ov+1KvUU01aVXfWu50Qxvr90NSYrKmvviBJKlKzQq2hO6ChWNUuFhBW3+G6zeto6IlCltvufnh61+ssxiatmpw3tAtSa9+OEZ92t+rTetOP4bffv6zvv38Z69trr+xre5/fEAWpwaA7MNLfQCQg96e/YIGDu2joiUKy+kMUblKpfXoc/fppWlP53RrlibX1NeP/3yqu4fdpio1Kyg8IkxOZ4hiyxTXDbd20le/vat7Hs78Ik+hYU599tObGvTgrdbR4opVy2r0pBF67rVHvLYtXLSgZi98W206NlO+yAhF5AtX8zaN9NnPb6lb78w/1iozjVvU00ffva6GTesoNNSp6PyRatupub787V1Vq5X5R27VrFdVr388TrXrV/Pr84zPuNTHLDcqXLSgvl7ygcZPeUzN2zRSwcIxcjgcisgXrkrVyunGvp302oznNHhEP7/vu27DGvryt3d17fUtlC8y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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "def visualize_classical_mts_results(mts_res):\n", + " \"\"\"\n", + " Visualizes the optimization path and population distribution \n", + " with a 'Quantum Vibe' aesthetic.\n", + " \"\"\"\n", + " # 1. Extraction and Search Logic\n", + " trace = mts_res[\"best_trace\"]\n", + " pop_e = mts_res[\"population_E\"]\n", + " best_e = mts_res[\"best_E\"]\n", + "\n", + "#Dynamically find the first iteration index where the best energy was hit\n", + "#Using np.where is faster and cleaner for finding the first occurrence\n", + " iteration_indices = np.where(trace == best_e)[0]\n", + " best_iteration = iteration_indices[0] if iteration_indices.size > 0 else 0\n", + "\n", + " print(f\"Best E Found: {best_e}\")\n", + " print(f\"First reached at iteration: {best_iteration}\")\n", + "\n", + " bg_color = '#FFFFFF' # White background\n", + " accent_color = '#1c0333' # Deep dark purple for text/lines\n", + " bar_color = '#601f9e' # Vibrant purple for fills/bars\n", + " grid_color = '#F0F0F0' # Light grey for subtle grid\n", + "\n", + " # --- Plot 1: Best-so-far Curve ---\n", + " fig1, ax1 = plt.subplots(figsize=(10, 5))\n", + " fig1.patch.set_facecolor(bg_color)\n", + " ax1.set_facecolor(bg_color)\n", + "\n", + " ax1.plot(trace, color=accent_color, linewidth=2.5, label='Energy Path')\n", + "\n", + "#Highlight the minimum energy point\n", + " ax1.scatter(best_iteration, best_e, color='blue', s=100, zorder=5, \n", + " label=f'Min Energy ({best_iteration}, {best_e})')\n", + "\n", + "#Annotate the minimum point\n", + " ax1.text(best_iteration + (len(trace)*0.05), best_e + (max(trace)*0.05), \n", + " f'Minimum energy at MTS({best_iteration}, {best_e})', \n", + " fontsize=11, color='blue', fontweight='bold')\n", + "\n", + " # Formatting\n", + " ax1.set_xlabel(\"MTS Iteration\", color=accent_color, fontweight='bold')\n", + " ax1.set_ylabel(\"Best-so-far Energy E\", color=accent_color, fontweight='bold')\n", + " ax1.set_title(\"Classical MTS Optimization Path\", color=accent_color, fontsize=14, fontweight='bold')\n", + " ax1.grid(True, linestyle='--', alpha=0.7, color=grid_color)\n", + " ax1.tick_params(colors=accent_color)\n", + " ax1.set_xlim(left=0) # Ensure X-axis starts at 0\n", + "#Modern area fill\n", + " ax1.fill_between(range(len(trace)), trace, color=bar_color, alpha=0.15)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " # --- Plot 2: Final Population Energy Distribution ---\n", + " fig2, ax2 = plt.subplots(figsize=(10, 5))\n", + " fig2.patch.set_facecolor(bg_color)\n", + " ax2.set_facecolor(bg_color)\n", + "\n", + " # Histogram\n", + " ax2.hist(pop_e, bins=20, color=bar_color, edgecolor=bg_color, linewidth=1.2)\n", + "\n", + " # Formatting\n", + " ax2.set_xlabel(\"Energy E\", color=accent_color, fontweight='bold')\n", + " ax2.set_ylabel(\"Candidate Count\", color=accent_color, fontweight='bold')\n", + " ax2.set_title(\"Final Population Energy Distribution\", color=accent_color, fontsize=14, fontweight='bold')\n", + " ax2.grid(axis='y', linestyle='--', alpha=0.7, color=grid_color)\n", + " ax2.tick_params(colors=accent_color)\n", + "\n", + " # Clean up the spines\n", + " for spine in ax2.spines.values():\n", + " spine.set_edgecolor(accent_color)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "#Usage:\n", + "visualize_classical_mts_results(res)" ] }, { @@ -230,12 +681,91 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "c91bbaae-62a1-41e7-a285-af885627a942", "metadata": {}, "outputs": [], "source": [ - "# TODO Write CUDA-Q kernels to apply the 2 and 4 qubit operators. " + "# TODO Write CUDA-Q kernels to apply the 2 and 4 qubit operators.\n", + "# define rzz\n", + "@cudaq.kernel\n", + "def rzz(q0: cudaq.qubit, q1: cudaq.qubit, theta: float):# for pi/2\n", + " x.ctrl(q0, q1) \n", + " rz(theta, q1) \n", + " x.ctrl(q0, q1)\n", + "\n", + "@cudaq.kernel\n", + "def two_qubit_rotation_block(q0: cudaq.qubit, q1: cudaq.qubit, theta: float):\n", + " pi = np.pi\n", + " rx(pi/2.0, q1)\n", + " rzz(q0, q1, theta)\n", + " rx(pi/2.0, q0)\n", + " rx(-pi/2.0, q1)\n", + " rzz(q0, q1, theta)\n", + " rx(-pi/2.0, q0)\n", + "\n", + "@cudaq.kernel\n", + "def four_qubit_rotation_block(q0: cudaq.qubit, q1: cudaq.qubit, q2: cudaq.qubit, q3: cudaq.qubit, theta: float):\n", + " pi = np.pi\n", + " rx(-pi/2.0, q0)\n", + " ry(pi/2.0, q1)\n", + " ry(-pi/2.0, q2)\n", + " \n", + " rzz(q0, q1, -pi/2.0)\n", + " rzz(q2, q3, -pi/2.0)\n", + "\n", + " rx(pi/2.0, q0)\n", + " ry(-pi/2.0, q1) \n", + " ry(pi/2.0, q2)\n", + " rx(-pi/2.0, q3) \n", + "\n", + " rx(-pi/2.0, q1) \n", + " rx(-pi/2.0, q2) \n", + " \n", + " rzz(q1, q2, theta)\n", + "\n", + " rx(pi/2.0, q1) \n", + " rx(pi, q2) \n", + "\n", + " ry(pi/2.0, q1)\n", + " \n", + " rzz(q0, q1, pi/2.0)\n", + " \n", + " rx(pi/2.0, q0)\n", + " ry(-pi/2.0, q2) \n", + " \n", + " rzz(q1, q2, -theta)\n", + " \n", + " rx(pi/2.0, q1)\n", + " rx(-pi, q2) \n", + " \n", + " rzz(q1, q2, -theta)\n", + " \n", + " rx(-pi, q1)\n", + " ry(pi/2.0, q2)\n", + " \n", + " rzz(q2, q3, -pi/2.0)\n", + " \n", + " ry(-pi/2.0, q2) # Ry_dagger\n", + " rx(-pi/2.0, q3)\n", + "\n", + " rx(-pi/2.0, q2)\n", + " \n", + " rzz(q1, q2, theta)\n", + "\n", + " rx(pi/2.0, q1)\n", + " rx(pi/2.0, q2)\n", + " \n", + " ry(-pi/2.0, q1)\n", + " ry(pi/2.0, q2)\n", + " \n", + " rzz(q0, q1, pi/2.0)\n", + " rzz(q2, q3, pi/2.0)\n", + "\n", + " \n", + " ry(pi/2.0, q1)\n", + " ry(-pi/2.0, q2)\n", + " rx(pi/2.0, q3)" ] }, { @@ -279,28 +809,112 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "abf34168-42f9-4dbf-86e1-99976232ad7e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N=6: |G2|=6, |G4|=7\n", + " G2 sample: [[0, 1], [0, 2], [1, 2], [1, 3], [2, 3], [3, 4]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 2, 3, 5], [1, 2, 3, 4], [1, 2, 4, 5]]\n", + "----------------------------------------\n", + "N=7: |G2|=9, |G4|=13\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 2, 3, 5], [0, 2, 4, 6]]\n", + "----------------------------------------\n", + "N=8: |G2|=12, |G4|=22\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [1, 4]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 1, 6, 7], [0, 2, 3, 5]]\n", + "----------------------------------------\n", + "N=10: |G2|=20, |G4|=50\n", + " G2 sample: [[0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [1, 3]]\n", + " G4 sample: [[0, 1, 2, 3], [0, 1, 3, 4], [0, 1, 4, 5], [0, 1, 5, 6], [0, 1, 6, 7], [0, 1, 7, 8]]\n", + "----------------------------------------\n" + ] + } + ], "source": [ - "def get_interactions(N):\n", + "def get_interactions(N: int):\n", " \"\"\"\n", - " Generates the interaction sets G2 and G4 based on the loop limits in Eq. 15.\n", + " Generates the interaction sets G2 and G4 based on the loop limits in Eq.15 (noted in the notebook).\n", " Returns standard 0-based indices as lists of lists of ints.\n", - " \n", + "\n", " Args:\n", " N (int): Sequence length.\n", - " \n", + "\n", " Returns:\n", - " G2: List of lists containing two body term indices\n", - " G4: List of lists containing four body term indices\n", + " G2: List of [i, i+k] two-body index pairs (0-based).\n", + " G4: List of [i, i+t, i+k, i+k+t] four-body index quadruples (0-based).\n", " \"\"\"\n", - " \n", - " #TODO - complete the loops below to compute G2 and G4 indicies\n", - "\n", - " \n", - " return G2, G4" + " if not isinstance(N, int) or N < 4:\n", + " # N must be at least 4 for non-empty G4; G2 exists for N>=3, but we choose N>=4 here as safe min.\n", + " raise ValueError(\"N must be an integer >= 4\")\n", + "\n", + " G2 = []\n", + " G4 = []\n", + "\n", + " # Build G2\n", + " # 1-based loops in the guide: i = 1..N-2, k = 1..floor((N-i)/2)\n", + " # Convert to 0-based: i0 = i-1 in range(0, N-2), k runs 1..floor((N-(i))/2)\n", + " for i0 in range(0, N - 2): # i0 corresponds to i=1..N-2\n", + " max_k = (N - (i0 + 1)) // 2 # floor((N-i)/2) where i = i0+1\n", + " for k in range(1, max_k + 1):\n", + " j0 = i0 + k\n", + " # sanity: ensure indices inside [0, N-1]\n", + " if 0 <= j0 < N:\n", + " G2.append([i0, j0])\n", + "\n", + " # Build G4\n", + " # 1-based loops in the guide: i = 1..N-3, t = 1..floor((N-i-1)/2), k = t+1..N-i-t\n", + " # Convert to 0-based: i0 in 0..N-4, t,k as below.\n", + " for i0 in range(0, N - 3): # i0 corresponds to i=1..N-3\n", + " max_t = (N - (i0 + 1) - 1) // 2 # floor((N-i-1)/2) with i=i0+1\n", + " for t in range(1, max_t + 1):\n", + " # k loop (1-based): k = t+1 .. N - i - t\n", + " # convert bounds to 0-based arithmetic: iterate over k in that integer range\n", + " k_min = t + 1\n", + " k_max = (N - (i0 + 1) - t) # inclusive in 1-based formula\n", + " # k_min..k_max (both inclusive) in 1-based; iterate accordingly\n", + " for k in range(k_min, k_max + 1):\n", + " # map to 0-based indices for the four-body term:\n", + " # (i, i+t, i+k, i+k+t) in 1-based -> subtract 1 for 0-based:\n", + " a = i0\n", + " b = i0 + t\n", + " c = i0 + k\n", + " d = i0 + k + t\n", + " # ensure within bounds\n", + " if 0 <= a < N and 0 <= b < N and 0 <= c < N and 0 <= d < N:\n", + " G4.append([a, b, c, d])\n", + "\n", + " # Sanity checks (counts)\n", + " # expected_G2_len = sum_{i=1}^{N-2} floor((N-i)/2)\n", + " expected_G2 = sum(((N - i) // 2) for i in range(1, N - 1))\n", + " # expected_G4_len = sum_{i=1}^{N-3} sum_{t=1}^{floor((N-i-1)/2)} (N - i - 2*t)\n", + " expected_G4 = 0\n", + " for i in range(1, N - 2):\n", + " max_t = (N - i - 1) // 2\n", + " for t in range(1, max_t + 1):\n", + " expected_G4 += max(0, N - i - 2 * t)\n", + "\n", + " assert len(G2) == expected_G2, f\"G2 length mismatch: got {len(G2)}, expected {expected_G2}\"\n", + " assert len(G4) == expected_G4, f\"G4 length mismatch: got {len(G4)}, expected {expected_G4}\"\n", + "\n", + " return G2, G4\n", + "\n", + "\n", + "# Quick usage example/smoke test:\n", + "\n", + "if __name__ == \"__main__\":\n", + " for Ntest in [6, 7, 8, 10]:\n", + " G2, G4 = get_interactions(Ntest)\n", + " print(f\"N={Ntest}: |G2|={len(G2)}, |G4|={len(G4)}\")\n", + " # print a few examples\n", + " print(\" G2 sample:\", G2[:6])\n", + " print(\" G4 sample:\", G4[:6])\n", + " print(\"-\" * 40)\n" ] }, { @@ -321,26 +935,45 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "8c2e6d7d-03b2-482f-bc36-d572e6d4855a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First 10 thetas: [1.5394042220828748e-18]\n", + "the size of population: 813\n", + "first 5 MTS instance: ['111111111111', '111111110111', '111111110010', '111111101110', '111111101010']\n" + ] + } + ], "source": [ "@cudaq.kernel\n", "def trotterized_circuit(N: int, G2: list[list[int]], G4: list[list[int]], steps: int, dt: float, T: float, thetas: list[float]):\n", " \n", " reg = cudaq.qvector(N)\n", " h(reg)\n", - " \n", - " # TODO - write the full kernel to apply the trotterized circuit\n", - "\n", - "\n", - " \n", "\n", + " nums_g2 = len(G2)\n", + " nums_g4 = len(G4)\n", + " \n", + " for n in range(steps): # fomula:[1, n]\n", + " theta = thetas[n]\n", + " for i in range(nums_g2):\n", + " pair = G2[i] # fomula:[1,N-2] for i and # [1,(N - i) // 2 ] for k\n", + " two_qubit_rotation_block(reg[pair[0]], reg[pair[1]], theta)\n", + " \n", + " for j in range(nums_g4):#[1,N-3] for j;#[1,(N-i-1)/2] for t and #[t+1,N-i-t]for k\n", + " quad = G4[j]\n", + " four_qubit_rotation_block(reg[quad[0]], reg[quad[1]], reg[quad[2]], reg[quad[3]], theta) \n", + "\n", + "# test it\n", "T=1 # total time\n", "n_steps = 1 # number of trotter steps\n", "dt = T / n_steps\n", - "N = 20\n", + "N = 12 # change from 20\n", "G2, G4 = get_interactions(N)\n", "\n", "thetas =[]\n", @@ -349,10 +982,24 @@ " t = step * dt\n", " theta_val = utils.compute_theta(t, dt, T, N, G2, G4)\n", " thetas.append(theta_val)\n", + "print(f\"First 10 thetas: {thetas[:10]}\")\n", "\n", - "# TODO - Sample your kernel to make sure it works" + "# TODO - Sample your kernel to make sure it works\n", + "counts = cudaq.sample(trotterized_circuit, N, G2, G4, n_steps, dt, T, thetas)\n", + "population = [bits for bits, count in counts.items()]\n", + "\n", + "print(f\"the size of population: {len(population)}\")\n", + "print(f\"first 5 MTS instance: {population[:5]}\")\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "f626db2c-6a8c-40a0-acdf-6c36404dfa49", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "fb89d90e-66e2-4700-85b9-40df9fca22c1", @@ -372,12 +1019,183 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "8e5f02e6-41bf-4634-9cb1-f0f543ef2e3f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'best_S' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[35], line 105\u001b[0m\n\u001b[1;32m 102\u001b[0m \u001b[38;5;66;03m# Run Just MTS\u001b[39;00m\n\u001b[1;32m 104\u001b[0m N \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m12\u001b[39m\n\u001b[0;32m--> 105\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[43mmts_labs_pm1\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 106\u001b[0m \u001b[43m \u001b[49m\u001b[43mN\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mN\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 107\u001b[0m \u001b[43m \u001b[49m\u001b[43mpop_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m32\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 108\u001b[0m \u001b[43m \u001b[49m\u001b[43mp_combine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.7\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 109\u001b[0m \u001b[43m \u001b[49m\u001b[43mp_mut\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[38;5;241;43m50.0\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 110\u001b[0m \u001b[43m \u001b[49m\u001b[43mmts_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m400\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 111\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m800\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 112\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_tenure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m30\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 113\u001b[0m \u001b[43m \u001b[49m\u001b[43mcandidate_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m64\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 114\u001b[0m \u001b[43m \u001b[49m\u001b[43mseed\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m42\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 115\u001b[0m \u001b[43m \u001b[49m\u001b[43mverbose_every\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m50\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 116\u001b[0m \u001b[43m)\u001b[49m\n\u001b[1;32m 117\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mClassical MTS\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 118\u001b[0m visualize_mts(res)\n", + "Cell \u001b[0;32mIn[34], line 204\u001b[0m, in \u001b[0;36mmts_labs_pm1\u001b[0;34m(N, pop_size, p_combine, p_mut, mts_iters, tabu_iters, tabu_tenure, candidate_size, target_E, seed, verbose_every)\u001b[0m\n\u001b[1;32m 201\u001b[0m child \u001b[38;5;241m=\u001b[39m mutate_alg3(child, p_mut, rng)\n\u001b[1;32m 203\u001b[0m \u001b[38;5;66;03m# ---- Tabu Search with Child ----\u001b[39;00m\n\u001b[0;32m--> 204\u001b[0m result_s, result_E \u001b[38;5;241m=\u001b[39m \u001b[43mtabu_search_pm1\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 205\u001b[0m \u001b[43m \u001b[49m\u001b[43mchild\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 206\u001b[0m \u001b[43m \u001b[49m\u001b[43mmax_iters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtabu_iters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 207\u001b[0m \u001b[43m \u001b[49m\u001b[43mtabu_tenure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtabu_tenure\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 208\u001b[0m \u001b[43m \u001b[49m\u001b[43mcandidate_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcandidate_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 209\u001b[0m \u001b[43m \u001b[49m\u001b[43mrng\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrng\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 210\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 212\u001b[0m \u001b[38;5;66;03m# ---- Update best solution ----\u001b[39;00m\n\u001b[1;32m 213\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m result_E \u001b[38;5;241m<\u001b[39m best_E_Class:\n", + "Cell \u001b[0;32mIn[34], line 112\u001b[0m, in \u001b[0;36mtabu_search_pm1\u001b[0;34m(s0, max_iters, tabu_tenure, candidate_size, rng)\u001b[0m\n\u001b[1;32m 110\u001b[0m best_s \u001b[38;5;241m=\u001b[39m s\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[1;32m 111\u001b[0m best_E_Class \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(E)\n\u001b[0;32m--> 112\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mbest_S\u001b[49m)\n\u001b[1;32m 114\u001b[0m N \u001b[38;5;241m=\u001b[39m s\u001b[38;5;241m.\u001b[39msize\n\u001b[1;32m 115\u001b[0m tabu_until \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mzeros(N, dtype\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39mint32)\n", + "\u001b[0;31mNameError\u001b[0m: name 'best_S' is not defined" + ] + } + ], "source": [ - "# TODO - write code here to sample from your CUDA-Q kernel and used the results to seed your MTS population" + "# TODO - write code here to sample from your CUDA-Q kernel and used the results to seed your MTS population\n", + "\n", + "# updated the MTS main loop \n", + "\n", + "def mts_quant1(\n", + " N: int,\n", + " pop_size: int = 32,\n", + " initial_pop: np.ndarray = None, # for quantum algo output \n", + " p_combine: float = 0.7,\n", + " p_mut: float = 1.0/50.0,\n", + " mts_iters: int = 1000,\n", + " tabu_iters: int = 800,\n", + " tabu_tenure: int = 30,\n", + " candidate_size: int = 64,\n", + " target_E: int | None = None,\n", + " seed: int = 0,\n", + " verbose_every: int = 100,\n", + "):\n", + " ## change\n", + " rng = np.random.default_rng(seed)\n", + " ## check if have pop or not\n", + " if initial_pop is not None:\n", + " print(f\"Using Quantum-enhanced population (size: {len(initial_pop)})\")\n", + " pop = initial_pop.copy()\n", + " else:\n", + " print(\"Using Randomly generated population.\")\n", + " pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + "\n", + " if pop.shape[0] < pop_size:\n", + " extra_count = pop_size - pop.shape[0]\n", + " extra = rng.choice(np.array([-1, 1], dtype=np.int8), size=(extra_count, N))\n", + " pop = np.vstack([pop, extra])\n", + " \n", + " # init population: k random bitstrings\n", + " #pop = rng.choice(np.array([-1, 1], dtype=np.int8), size=(pop_size, N))\n", + " pop_E = np.array([labs_energy_pm1(pop[i]) for i in range(pop_size)], dtype=np.int64)\n", + "\n", + " best_idx = int(np.argmin(pop_E))\n", + " best_s = pop[best_idx].copy()\n", + " best_E = int(pop_E[best_idx])\n", + "\n", + " trace = [best_E]\n", + " t0 = time.time()\n", + "\n", + " for it in range(1, mts_iters + 1):\n", + " if target_E is not None and best_E <= target_E:\n", + " break\n", + "\n", + " # ---- Make Child ----\n", + " if rng.random() < p_combine:\n", + " i1, i2 = rng.integers(0, pop_size, size=2)\n", + " child = combine_alg3(pop[i1], pop[i2], rng)\n", + " else:\n", + " i = int(rng.integers(0, pop_size))\n", + " child = pop[i].copy()\n", + "\n", + " # ---- Mutate Child ----\n", + " child = mutate_alg3(child, p_mut, rng)\n", + "\n", + " # ---- Tabu Search with Child ----\n", + " result_s, result_E = tabu_search_pm1(\n", + " child,\n", + " max_iters=tabu_iters,\n", + " tabu_tenure=tabu_tenure,\n", + " candidate_size=candidate_size,\n", + " rng=rng,\n", + " )\n", + "\n", + " # ---- Update best solution ----\n", + " if result_E < best_E:\n", + " best_E = int(result_E)\n", + " best_s = result_s.copy()\n", + "\n", + " # ---- Add result to Population ----\n", + " # randomly replace a member if result is better\n", + " r = int(rng.integers(0, pop_size))\n", + " if result_E < pop_E[r]:\n", + " pop[r] = result_s\n", + " pop_E[r] = result_E\n", + "\n", + " # (optional, helpful) elitism: keep global best in population\n", + " worst = int(np.argmax(pop_E))\n", + " if best_E < pop_E[worst]:\n", + " pop[worst] = best_s\n", + " pop_E[worst] = best_E\n", + "\n", + " trace.append(best_E)\n", + "\n", + " if verbose_every and (it % verbose_every == 0):\n", + " print(f\"[MTS {it:5d}] best_E={best_E} elapsed={time.time()-t0:.2f}s\")\n", + "\n", + " return {\n", + " \"best_s_pm1\": best_s,\n", + " \"best_s_01\": pm1_to_bits01(best_s),\n", + " \"best_E\": best_E,\n", + " \"best_trace\": np.array(trace, dtype=np.int64),\n", + " \"population_pm1\": pop,\n", + " \"population_E\": pop_E.copy(),\n", + " \"elapsed_sec\": time.time() - t0,\n", + " }\n", + "\n", + "# Run Just MTS\n", + "\n", + "N = 12\n", + "res = mts_labs_pm1(\n", + " N=N,\n", + " pop_size=32,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "print(\"Classical MTS\")\n", + "visualize_mts(res)\n", + "\n", + "# Run cp + MTS \n", + "# test it\n", + "T=1 # total time\n", + "n_steps = 1 # number of trotter steps\n", + "dt = T / n_steps\n", + "# N\n", + "G2, G4 = get_interactions(N)\n", + "\n", + "thetas =[]\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", + "\n", + "# TODO - Sample your kernel to make sure it works\n", + "counts_6 = cudaq.sample(trotterized_circuit, N, G2, G4, n_steps, dt, T, thetas)\n", + "quantum_pop_list = []\n", + "for bitstring, count in counts_6.items():\n", + " bits_01 = np.array([int(b) for b in bitstring], dtype=np.int8)\n", + " s_pm1 = bits01_to_pm1(bits_01)\n", + " quantum_pop_list.append(s_pm1)\n", + "initial_quantum_pop = np.array(quantum_pop_list)\n", + "#N = \n", + "res = mts_quant1(\n", + " N=N,\n", + " pop_size=32,\n", + " initial_pop=initial_quantum_pop,\n", + " p_combine=0.7,\n", + " p_mut=1.0/50.0,\n", + " mts_iters=400,\n", + " tabu_iters=800,\n", + " tabu_tenure=30,\n", + " candidate_size=64,\n", + " seed=42,\n", + " verbose_every=50,\n", + ")\n", + "print(\"CP + MTS\")\n", + "visualize_mts(res)\n" ] }, { @@ -416,13 +1234,138 @@ "\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?" ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d34db9f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Starting Validation Suite for N = 7 ---\n", + "Symmetry Check:\n", + " Original Energy: 19\n", + " Bit-flip Energy: 19 (Match: True)\n", + " Reversal Energy: 19 (Match: True)\n", + " Both Match: 19 (Match: True)\n", + "\n", + "Quantum-Classical Cross-Reference:\n", + " Sampled Bitstring: 1001010 \n", + " Computed Energy: 23\n", + " Status: Success (Kernel produced valid measurable state)\n", + "\n", + "Known Solution Verification (N=4 Barker):\n", + " Expected: 2, Got: 2 (Match: True)\n" + ] + } + ], + "source": [ + "import unittest\n", + "import numpy as np\n", + "\n", + "def validation_suite(N_val = 7):\n", + " print(f\"--- Starting Validation Suite for N = {N_val} ---\")\n", + " \n", + " # 1. Symmetry Checks\n", + " # LABS energy should be invariant under:\n", + " # - Bit-flip: s -> -s\n", + " # - Reversal: s_i -> s_{N-i+1}\n", + " test_seq = np.random.choice( [-1, 1], size = N_val)\n", + " energy_orig = labs_energy_pm1(test_seq )\n", + " \n", + " # Bit-flip symmetry\n", + " energy_flip = labs_energy_pm1(-test_seq )\n", + " # Reversal symmetry\n", + " energy_rev = labs_energy_pm1(test_seq[::-1])\n", + " # Combined symmetry: Bit-flip + Reversal\n", + " energy_both = labs_energy_pm1(-test_seq[::-1])\n", + " \n", + " print(f\"Symmetry Check:\")\n", + " print(f\" Original Energy: {energy_orig}\")\n", + " print(f\" Bit-flip Energy: {energy_flip} (Match: {energy_orig == energy_flip})\")\n", + " print(f\" Reversal Energy: {energy_rev} (Match: {energy_orig == energy_rev})\")\n", + " print(f\" Both Match: {energy_both } (Match: {energy_orig == energy_both})\")\n", + "\n", + " # 2. CUDA-Q vs Classical Cross-Reference\n", + " # We sample the quantum kernel and ensure the bitstrings produced \n", + " # are evaluated correctly by the classical energy function.\n", + " print(f\"\\nQuantum-Classical Cross-Reference:\")\n", + " # Using small parameters for quick validation\n", + " G2_v, G4_v = get_interactions(N_val)\n", + " # Ensure thetas matches the step count used in the test\n", + " test_steps = 1\n", + " test_thetas = [0.5] # Dummy theta for structural test\n", + " \n", + " # Use cudaq.sample to get the distribution\n", + " counts = cudaq.sample( trotterized_circuit, N_val, G2_v, G4_v, test_steps, 1.0, 1.0, test_thetas)\n", + " \n", + " # Updated: Robust way to get the most frequent bitstring\n", + " # counts.items() returns a list of (bitstring, count) tuples\n", + " sample_bitstring = max(counts.items() , key=lambda item: item[1])[0]\n", + " \n", + " # Convert bitstring '0101...' to pm1 array\n", + " s_sample = bits01_to_pm1( np.array([ int(b) for b in sample_bitstring] ))\n", + " energy_sample = labs_energy_pm1(s_sample )\n", + " print(f\" Sampled Bitstring: {sample_bitstring} \")\n", + " print(f\" Computed Energy: {energy_sample}\")\n", + " print(f\" Status: Success (Kernel produced valid measurable state)\")\n", + "\n", + " # 3. Small n Hand-Verification (N=4)\n", + " # For N=4, the Barker sequence [1, 1, 1, -1] has energy 2.\n", + " # C1=1, C2=0 , C3=-1 -> 1^2 + 0^2 + (-1)^2 =2\n", + " s_barker = np.array([1, 1, 1, -1])\n", + " e_barker = labs_energy_pm1( s_barker)\n", + " print(f\"\\nKnown Solution Verification (N=4 Barker):\")\n", + " print(f\" Expected: 2, Got: {e_barker} (Match: {e_barker == 2})\")\n", + "\n", + "# Run the validation\n", + "validation_suite()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "164b5fa0-fa04-417f-a008-dd5232226a5c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Starting Validation Suite (N=7) ---\n", + "[TEST 1] Barker Sequence (N=4): Expected Energy 2, Got 2.\n", + "[TEST 2] Symmetry Check - Bit-flip: PASSED\n", + "[TEST 3] Symmetry Check - Reversal: PASSED\n", + "[TEST 4] Quantum Cross-Ref: Bitstring [1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1] has Classical Energy 10\n" + ] + } + ], + "source": [ + "\n", + "from validations import run_validation\n", + "\n", + "# Example: pass the best bitstring from quantum sampler to cross-reference it\n", + "# best_bitstring = \"1110\" \n", + "run_validation(N=7, quantum_sample_bitstring=res[\"best_s_01\"].tolist())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15bc79e9-120f-4b5a-b05a-7021aedf787a", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3 [cuda-q-v0.13.0]", "language": "python", - "name": "python3" + "name": "python3_0pt5" }, "language_info": { "codemirror_mode": { @@ -434,7 +1377,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/tutorial_notebook/__pycache__/validation_tests.cpython-311.pyc b/tutorial_notebook/__pycache__/validation_tests.cpython-311.pyc new file mode 100644 index 00000000..d9abaa1d Binary files /dev/null and b/tutorial_notebook/__pycache__/validation_tests.cpython-311.pyc differ diff --git a/tutorial_notebook/__pycache__/validations.cpython-311.pyc b/tutorial_notebook/__pycache__/validations.cpython-311.pyc new file mode 100644 index 00000000..97a15968 Binary files /dev/null and b/tutorial_notebook/__pycache__/validations.cpython-311.pyc differ diff --git a/tutorial_notebook/auxiliary_files/__pycache__/labs_utils.cpython-311.pyc b/tutorial_notebook/auxiliary_files/__pycache__/labs_utils.cpython-311.pyc new file mode 100644 index 00000000..3370a755 Binary files /dev/null and b/tutorial_notebook/auxiliary_files/__pycache__/labs_utils.cpython-311.pyc differ diff --git a/tutorial_notebook/validations.py b/tutorial_notebook/validations.py new file mode 100644 index 00000000..68329f67 --- /dev/null +++ b/tutorial_notebook/validations.py @@ -0,0 +1,53 @@ +import numpy as np + +def labs_energy_pm1(s): + """ + Calculates the energy of a binary sequence s in {+1, -1}. + E(s) = sum_{k=1}^{N-1} (sum_{i=1}^{N-k} s_i * s_{i+k})^2 + """ + N = len(s) + total_energy = 0 + for k in range(1, N): + ck = 0 + for i in range(N - k): + ck += s[i] * s[i+k] + total_energy += ck**2 + return total_energy + +def bits01_to_pm1(bits): + """Converts a bitstring of 0s and 1s to +1s and -1s.""" + return 1 - 2 * np.array(bits) + +def run_validation(N=7, quantum_sample_bitstring=None): + print(f"--- Starting Validation Suite (N={N}) ---") + + # 1. Small N Hand-Calculation Verification + # For N=4, the sequence [1, 1, 1, -1] is a known Barker sequence with Energy = 2. + s_barker = np.array([1, 1, 1, -1]) + e_barker = labs_energy_pm1(s_barker) + print(f"[TEST 1] Barker Sequence (N=4): Expected Energy 2, Got {e_barker}.") + assert e_barker == 2, "Energy calculation failed on known N=4 case." + + # 2. Check Symmetries + # The LABS problem is invariant under bit-flip (s -> -s) and reversal. + test_seq = np.random.choice([-1, 1], size=N) + e_orig = labs_energy_pm1(test_seq) + + e_flip = labs_energy_pm1(-test_seq) + e_rev = labs_energy_pm1(test_seq[::-1]) + + print(f"[TEST 2] Symmetry Check - Bit-flip: {'PASSED' if e_orig == e_flip else 'FAILED'}") + print(f"[TEST 3] Symmetry Check - Reversal: {'PASSED' if e_orig == e_rev else 'FAILED'}") + + # 3. Quantum-Classical Cross-Reference + if quantum_sample_bitstring: + # Convert bitstring (e.g., '101') to numerical array + bits = [int(b) for b in quantum_sample_bitstring] + s_quantum = bits01_to_pm1(bits) + e_quantum = labs_energy_pm1(s_quantum) + print(f"[TEST 4] Quantum Cross-Ref: Bitstring {quantum_sample_bitstring} has Classical Energy {e_quantum}") + else: + print("[TEST 4] Quantum Cross-Ref: Skipped (No sample provided)") + +if __name__ == "__main__": + run_validation() \ No newline at end of file