diff --git a/binder-index.md b/binder-index.md index 06b87df6042c..714d48d789ac 100644 --- a/binder-index.md +++ b/binder-index.md @@ -251,6 +251,14 @@ These are noted in the README.md files for each sample, along with complete inst Q# standalone + + + Solving Sudoku with Grover's Search + + + Q# standalone + + Characterization: Bayesian Phase Estimation diff --git a/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.csproj b/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.csproj new file mode 100644 index 000000000000..cc1cf5882c6d --- /dev/null +++ b/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.csproj @@ -0,0 +1,9 @@ + + + + Exe + net6.0 + quantinuum.hqs-lt-s1-sim + + + diff --git a/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.ipynb b/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.ipynb new file mode 100644 index 000000000000..c6f2064272d8 --- /dev/null +++ b/samples/azure-quantum/grover-sudoku/GroversSudokuQuantinuum.ipynb @@ -0,0 +1,1572 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solving Sudoku Puzzles Using Grover's Search\n", + "\n", + "In this sample, we will be solving Sudoku puzzles using Grover's search.\n", + "\n", + "Given that we will run our algorithm on current quantum hardware, we need to minimize qubit count and circuit depth (number of gates) required by the algorithm.\n", + "\n", + "Since Grover's search is fundamentally a quantum algorithm requiring classical preprocessing, we will use the feature of Python notebooks integrating with Q#.\n", + "This will further enable us to have some convenience in the data structures we build, such as classical validation of Sudoku puzzles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "import qsharp # Enable Q#-Python integration" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's connect to Azure Quantum and set our target!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import qsharp.azure\n", + "import datetime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "targets = qsharp.azure.connect(\n", + " resourceId=\"/subscriptions/2cc419b3-3ace-4156-b256-44663dd90190/resourceGroups/AzureQuantum/providers/Microsoft.Quantum/Workspaces/AdriansProjectWorkspace\",\n", + " location=\"westus\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"This workspace's {len(targets)} targets:\")\n", + "for target in targets:\n", + " print(f\"- {target.id} (average queue time {datetime.timedelta(seconds=target.average_queue_time)})\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qsharp.azure.target('quantinuum.hqs-lt-s1-sim')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining a Classical Data Structure for Sudoku Puzzles\n", + "Let us first write the code to define, validate, and print Sudoku puzzles. This code will be entirely classical and written in Python, serving as an example of integration of classical and quantum code. \n", + "Later, we will expand the functionality of the `Sudoku` class to include quantum computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "from copy import deepcopy\n", + "from typing import List, Dict, Tuple\n", + "\n", + "class Sudoku:\n", + " data: List[List[int]] \n", + "\n", + " def get_size(self) -> int:\n", + " \"\"\"The width/height of the puzzle\"\"\"\n", + " return len(self.data)\n", + "\n", + " size = property(get_size)\n", + "\n", + " def __getitem__(self, pos : Tuple[int, int]) -> int:\n", + " \"\"\"Return the value of a cell at a given location (0 if the cell is empty)\"\"\"\n", + " (i, j) = pos\n", + " return self.data[i][j]\n", + "\n", + " def __setitem__(self, pos : Tuple[int, int], val: int) -> None:\n", + " \"\"\"Sets the value of a cell at a given location (val=0 will empty the cell)\"\"\"\n", + " (i, j) = pos\n", + " self.data[i][j] = val\n", + "\n", + " def __init__(self, data: List[List[int]]) -> None:\n", + " \"\"\"Initializes the puzzle. \n", + " data has to be a 2D array of size 4x4 or 9x9 with row-column indexing. \n", + " Cells marked 0 will be considered empty\"\"\"\n", + " size = len(data)\n", + " if size not in {4, 9}:\n", + " raise ValueError(\"Must be 4x4 or 9x9 array\")\n", + " # We currently only support Sudoku puzzles up to size 9\n", + " # Larger Sudoku puzzles would require an unreasonable amount of RAM for quantum simulation\n", + " for row in data:\n", + " if len(row) != size:\n", + " raise ValueError(\"Must be 4x4 or 9x9 array\")\n", + " self.data = deepcopy(data)\n", + "\n", + " def get_bit_length(self) -> int:\n", + " \"\"\"The number of bits required to represent a number in the puzzle\"\"\"\n", + " if self.size == 4:\n", + " return 2\n", + " return 4\n", + "\n", + " bit_length = property(get_bit_length)\n", + "\n", + " def __str__(self) -> str:\n", + " \"\"\"Creates a human-readable representation of the puzzle\"\"\"\n", + " str = ''\n", + " for row in self.data:\n", + " str += ('-' * (4 * self.size + 1)) + '\\n'\n", + " for el in row:\n", + " if el == 0:\n", + " str += \"| \"\n", + " else:\n", + " str += f\"| {el} \"\n", + " str += '|\\n'\n", + " str += ('-' * (4 * self.size + 1)) + '\\n'\n", + " return str\n", + "\n", + " def is_valid(self) -> bool:\n", + " \"\"\"Checks whether the puzzle is complete and meets all Sudoku constraints\"\"\"\n", + " # Check for empty cells\n", + " for i in range(self.size):\n", + " for j in range(self.size):\n", + " if not self[i, j]:\n", + " return False\n", + "\n", + " # Check rows\n", + " for row in range(self.size):\n", + " values_in_row = set()\n", + " for i in range(self.size):\n", + " curr = self[row, i]\n", + " if curr in values_in_row:\n", + " return False\n", + " values_in_row.add(curr)\n", + " # Check cols\n", + " for col in range(self.size):\n", + " values_in_col = set()\n", + " for j in range(self.size):\n", + " curr = self[j, col]\n", + " if curr in values_in_col:\n", + " return False\n", + " values_in_col.add(curr)\n", + " # Check subgrids\n", + " sub_size = math.floor(math.sqrt(self.size))\n", + "\n", + " for sub_grid_i in range(sub_size):\n", + " for sub_grid_j in range(sub_size):\n", + " sub_start_i = sub_grid_i * sub_size\n", + " sub_start_j = sub_grid_j * sub_size\n", + " values_in_sub_grid = set()\n", + " for i in range(sub_start_i, sub_start_i + sub_size):\n", + " for j in range(sub_start_j, sub_start_j + sub_size):\n", + " curr = self[i, j]\n", + " if curr in values_in_sub_grid:\n", + " return False\n", + " values_in_sub_grid.add(curr)\n", + " # No violations found\n", + " return True\n", + "\n", + " def count_empty_squares(self) -> int:\n", + " \"\"\"Returns the number of empty squares in the puzzle\"\"\"\n", + " empty = 0\n", + " for i in range(self.size):\n", + " for j in range(self.size):\n", + " if not self[i, j]:\n", + " empty += 1\n", + " return empty\n", + " \n", + " def prepare_constraints(self):\n", + " return _prepare_constraints(self)\n", + "\n", + " def solve_quantum(self, solve_fn):\n", + " return _solve_quantum(self, solve_fn)\n", + " \n", + " def get_constraints(self):\n", + " return _get_constraints(self)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test our code so far." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sudoku = Sudoku([\n", + " [1, 3, 4, 2],\n", + " [2, 4, 3, 1],\n", + " [3, 2, 1, 4],\n", + " [4, 1, 2, 3]\n", + " ])\n", + "print(sudoku)\n", + "print(\"Valid!\" if sudoku.is_valid() else \"Invalid\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sudoku Puzzles as Vertex Coloring Problems\n", + "\n", + "We will use Grover's search to solve Sudoku puzzles by viewing them as a vertex coloring problem: each empty cell of the puzzle is a vertex that needs to have a color assigned based on the constraints imposed by the other cells (we will describe this in more detail and provide an example in the section on Classical precomputation).\n", + "\n", + "Python pre-processing code converts the input puzzle into a set of constraints and passes it to the quantum part of the program.\n", + "Q# code solves the problem defined by the constraints and returns a bitstring that represents the numbers assigned to the empty cells. \n", + "Finally, Python post-processing code parses the solution and validates its correctness.\n", + "\n", + "We represent the integer that will be placed in each empty cell as a bitstring of either two (if $n=2$) or four (if $n=3$) bits, where bitstrings are interpreted as binary integers. \n", + "We then concatenate each cell’s bitstrings, remembering the indices of each empty cell. This means that for a $4 \\times 4$ puzzle with $k$ empty cells, we need $2k$ bits for the representation of the problem.\n", + "\n", + "For example for the board with 3 empty squares:\n", + "```\n", + "-----------------\n", + "| 1 | | 4 | 2 |\n", + "-----------------\n", + "| 2 | 4 | 3 | |\n", + "-----------------\n", + "| 3 | 2 | 1 | 4 |\n", + "-----------------\n", + "| 4 | 1 | | 3 |\n", + "-----------------\n", + "```\n", + "we'd use 6 qubits for our representation, where for example $\\ket{100001}$ would result in the solutions being $3,1,2$ resulting in the board\n", + "```\n", + "-----------------\n", + "| 1 | 3 | 4 | 2 |\n", + "-----------------\n", + "| 2 | 4 | 3 | 1 |\n", + "-----------------\n", + "| 3 | 2 | 1 | 4 |\n", + "-----------------\n", + "| 4 | 1 | 2 | 3 |\n", + "-----------------\n", + "```\n", + "\n", + "\n", + "The algorithm in the quantum component is Grover's search algorithm, an algorithm that prepares a search space and then uses an oracle to perform \"Grover iterations\".\n", + "A \"Grover iteration\" involves applying an oracle and then diffusion the state.\n", + "The amazing thing is that we find the correct results in $\\mathcal{O}(\\sqrt{k})$.\n", + "If you'd like to learn more about Grover's search, please see [this article](https://docs.microsoft.com/en-us/azure/quantum/concepts-grovers)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Classical Precomputation\n", + "\n", + "Let's get building! First, we classically create the constraints and then translate them into quantum states.\n", + "\n", + "We classically convert the puzzle into two types of constraints:\n", + "\n", + "- Starting number constraints specify the numbers that cannot be assigned to a cell based on the current nonempty cells.\n", + "- Edge constraints specify that any two empty cells that are in the same row, column, or subgrid cannot be assigned the same number.\n", + "\n", + "The figure below shows the constraints of our running example Sudoku puzzle. \n", + "The red borders represent sub-grid borders, the orange numbers are the predefined numbers. \n", + "The black sets are the sets of possible numbers under the starting number constraints - the numbers 1 through 4, except the numbers found in the same row, column, or subgrid as the cell. \n", + "The arrows show the edge constraints." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will encode the constraints as lists. Each empty square of the grid will be assigned an index.\n", + "For the pair of empty squares with indices $i, j$ the edge constraint will be expressed by the tuple $(i,j)$ or $(j,i)$.\n", + "For an empty square with index $i$ that cannot have value $x$, the starting number constraint will be expressed as $(i,x)$.\n", + "\n", + "The constraint definition code returns a tuple of arrays representing the constraints and the indexed list for mapping indices to empty squares, where the $i^{\\text{th}}$ element is the coordinate of the $i^{\\text{th}}$ empty square." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _get_constraints(sudoku : Sudoku) -> Tuple[List[Tuple[int, int]], # Empty square edges\n", + " List[Tuple[int, int]], # Starting constraints\n", + " List[Tuple[int, int]] # Empty squares\n", + " ]:\n", + " sub_size = math.floor(math.sqrt(sudoku.size))\n", + " empty_indices = dict()\n", + " empty_squares = list()\n", + " empty_square_edges = list()\n", + " starting_number_constraints = set() # We want to avoid duplicates\n", + " empty_index = 0\n", + "\n", + " for i in range(sudoku.size):\n", + " for j in range(sudoku.size):\n", + "\n", + " # Only consider empty_squares for constraint calculation\n", + " \n", + " if sudoku[i, j]:\n", + " continue\n", + " empty_indices[i, j] = empty_index\n", + " empty_squares.append((i, j))\n", + "\n", + " # Introspect subgrid for constraints \n", + " # (i.e. the 2x2/3x3 square of the current element)\n", + "\n", + " i_sub_grid = (i // sub_size) * sub_size\n", + " j_sub_grid = (j // sub_size) * sub_size\n", + "\n", + " for i_sub in range(i_sub_grid, i_sub_grid + sub_size):\n", + " for j_sub in range(j_sub_grid, j_sub_grid + sub_size):\n", + " if i_sub == i and j_sub == j:\n", + " continue\n", + " if sudoku[i_sub, j_sub]:\n", + " starting_number_constraints.add(\n", + " (empty_index, sudoku[i_sub, j_sub] - 1))\n", + " elif j_sub < i_sub and i_sub < i and j_sub < j:\n", + " empty_square_edges.append(\n", + " (empty_index, empty_indices[(i_sub, j_sub)]))\n", + "\n", + " # Check for column constraints\n", + "\n", + " for row_index in range(sudoku.size):\n", + " if sudoku[row_index, j]:\n", + " starting_number_constraints.add(\n", + " (empty_index, sudoku[row_index, j] - 1))\n", + " elif row_index < i:\n", + " empty_square_edges.append(\n", + " (empty_index, empty_indices[row_index, j]))\n", + "\n", + " # Check for row constraints\n", + "\n", + " for col_index in range(sudoku.size):\n", + " if sudoku[i, col_index]:\n", + " starting_number_constraints.add(\n", + " (empty_index, sudoku[i, col_index] - 1))\n", + " elif col_index < j:\n", + " empty_square_edges.append(\n", + " (empty_index, empty_indices[i, col_index]))\n", + "\n", + " # Exclude illegal values on a 9x9 puzzle\n", + " # Not needed for 4x4 since 4x4 has bit width 2, which only represents legal values\n", + "\n", + " if sudoku.size == 9:\n", + " for invalid in range(9, 16):\n", + " starting_number_constraints.add((empty_index, invalid))\n", + "\n", + " empty_index += 1\n", + " return (empty_square_edges, list(starting_number_constraints), empty_squares)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quantum Code\n", + "We have now created our constraints classically. \n", + "Let's convert them into the elements of quantum program.\n", + "\n", + "In the most straightforward approach, we can search through all assignments of integers $[1, n^2]$ to each of the empty cells. In this case, the oracle has to check that the assignment of numbers satisfies both the starting number and the edge constraints.\n", + "In the optimized approach, we handle edge constraints and starting number constraints separately. During state preparation, for each cell, we use the starting number constraints to calculate the allowed values and set its qubit representation into uniform superposition of only these values.\n", + "The oracle will then only include the checks of the edge constraints.\n", + "\n", + "This drastically reduces search space size. \n", + "In the example above, the search space size shrinks from $4^4=256$ to $4$.\n", + "The reduction allows us to use fewer search iterations, resulting in fewer oracle calls, less noise-prone computation, increased performance, and therefore more difficult puzzles we can solve!\n", + "\n", + "Specifically, the optimal number of iterations is given by the formula $n_\\textrm{iter}(s) = \\lfloor \\frac{\\pi}{4\\arcsin{\\sqrt{s^{-1}}}} - \\frac{1}{2} \\rceil$, where $s$ is the search space size. \n", + "In the example, the optimization reduces the number of iterations from $n_\\textrm{iter}(256) = 12$ to $n_\\textrm{iter}(4) = 1$.\n", + "Further, we do not need to encode starting constraints the oracle, significantly lowering the number of qubits required.\n", + "\n", + "But since this is a large project, we'll need to break it down into components:\n", + "- Prepare data for the algorithm classically and encode constraints\n", + "- Prepare a search state\n", + "- An oracle\n", + "- Loop over individual iterations\n", + "- Measure and extract the information we need\n", + "\n", + "\n", + "Q# gives us a lot of library features to help us in the process of building our algorithm. You can learn more about the libraries in the [documentation](https://docs.microsoft.com/en-us/qsharp/api/qsharp/) or the [Quantum Katas](https://docs.microsoft.com/en-us/azure/quantum/tutorial-qdk-intro-to-katas)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "microsoft": { + "language": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "open Microsoft.Quantum.Arithmetic;\n", + "open Microsoft.Quantum.Arrays;\n", + "open Microsoft.Quantum.Convert;\n", + "open Microsoft.Quantum.Intrinsic;\n", + "open Microsoft.Quantum.Logical;\n", + "open Microsoft.Quantum.Math;\n", + "open Microsoft.Quantum.Measurement;\n", + "open Microsoft.Quantum.Preparation;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Not required, but can be helpful to provide type information to some IDEs.\n", + "AllowedAmplitudes: qsharp.QSharpCallable = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "microsoft": { + "language": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "/// # Summary\n", + "/// Encodes stating number constraints into amplitudes.\n", + "///\n", + "/// # Inputs\n", + "/// ## nVertices\n", + "/// The number of vertices in the graph.\n", + "/// ## bitsPerColor\n", + "/// The bit width for number of colors.\n", + "/// ## startingNumberConstraints\n", + "/// The array of (Vertex#, Color) specifying the disallowed colors for vertices.\n", + "///\n", + "/// # Examples\n", + "/// Consider the case where we have 2 vertices, 2 bits per color, and the constraints (0,1),(0,2),(0,3),(1,2).\n", + "/// Then we would get the result where all non-disallowed values have a 1.0 amplitude:\n", + "/// [[1.0, 0.0, 0.0, 0.0], \n", + "/// [1.0, 1.0, 0.0, 1.0]]\n", + "///\n", + "///\n", + "/// # Output\n", + "/// A 2D array of amplitudes where the first index is the cell and the second index is the value of a basis state (i.e., value) for the cell. =\n", + "/// Allowed amplitudes will have a value 1.0, disallowed amplitudes 0.0\n", + "function AllowedAmplitudes(\n", + " nVertices : Int,\n", + " bitsPerColor : Int,\n", + " startingNumberConstraints : (Int, Int)[]\n", + ") : Double[][] {\n", + " mutable amplitudes = [[1.0, size=1 <<< bitsPerColor], size=nVertices];\n", + " for (cell, value) in startingNumberConstraints {\n", + " set amplitudes w/= cell <- (amplitudes[cell] w/ value <- 0.0);\n", + " }\n", + " return amplitudes;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us take a look at how this function works: From the starting number constraints we build an array that for each cell represents the amplitudes of each value.\n", + "So given a diagram, where we only have two empty square, one of which can have the values $2$ and $4$ and the other can be $1,2,3$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "allowed_amplitudes = AllowedAmplitudes.simulate(nVertices=2, bitsPerColor=2, startingNumberConstraints=[(0,0), (0,2), (1,3)])\n", + "for i in range(len(allowed_amplitudes)):\n", + " plt.bar(range(1,5), allowed_amplitudes[i])\n", + " plt.title(f\"Allowed amplitudes for empty cell with index {i}\")\n", + " plt.xticks(range(1,5))\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%qsharp\n", + "/// # Summary\n", + "/// Prepare an equal superposition of all basis states that satisfy the constraints\n", + "/// imposed by the digits already placed in the grid.\n", + "///\n", + "/// # Inputs\n", + "/// ## nVertices\n", + "/// The number of vertices in the graph.\n", + "/// ## bitsPerColor\n", + "/// The bit width for number of colors.\n", + "/// ## startingNumberConstraints\n", + "/// The array of (Vertex#, Color) specifying the disallowed colors for vertices.\n", + "///\n", + "/// # Remarks\n", + "/// Prepares the search space. Using the allowed amplitudes prepares uniform superposition of all allowed values for each cell\n", + "operation PrepareSearchStatesSuperposition(\n", + " nVertices : Int,\n", + " bitsPerColor : Int,\n", + " startingNumberConstraints : (Int, Int)[],\n", + " register : Qubit[]\n", + ") : Unit is Adj + Ctl {\n", + " // Split the given register into nVertices chunks of size bitsPerColor.\n", + " let colorRegisters = Chunks(bitsPerColor, register);\n", + " // For each vertex, create an array of possible states we're looking at.\n", + " let amplitudes = AllowedAmplitudes(nVertices, bitsPerColor, startingNumberConstraints);\n", + " // For each vertex, prepare a superposition of its possible states on the chunk storing its color.\n", + " for (amps, chunk) in Zipped(amplitudes, colorRegisters) {\n", + " PrepareArbitraryStateD(amps, LittleEndian(chunk));\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We further use our starting number constraints to calculate the size of the search space, which is just the total number of possible combinations.\n", + "With that information we can calculate the number of Grover's iterations we need." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "/// # Summary\n", + "/// Show the size of the search space, i.e. the number of possible combinations\n", + "///\n", + "/// # Inputs\n", + "/// ## nVertices\n", + "/// The number of vertices in the graph.\n", + "/// ## bitsPerColor\n", + "/// The bit width for number of colors.\n", + "/// ## startingNumberConstraints\n", + "/// The array of (Vertex#, Color) specifying the disallowed colors for vertices.\n", + "///\n", + "/// # Output\n", + "/// The size of the search space (i.e., number of possible combinations)\n", + "function SearchSpaceSize(\n", + " nVertices : Int,\n", + " bitsPerColor : Int,\n", + " startingNumberConstraints : (Int, Int)[]\n", + ") : Int {\n", + " mutable colorOptions = [1 <<< bitsPerColor, size=nVertices];\n", + " for (cell, _) in startingNumberConstraints {\n", + " set colorOptions w/= cell <- colorOptions[cell] - 1;\n", + " }\n", + " return Fold(TimesI, 1, colorOptions);\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us try calculating the search space size for the example above, where we had two empty cells." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f\"Search space size with two cells, one where three values are allowed and another where two values are allowed: \\\n", + "{SearchSpaceSize.simulate(nVertices=2, bitsPerColor=2, startingNumberConstraints=[(0,0), (0,2), (1,3)])}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Not required, but can be helpful to provide type information to some IDEs.\n", + "NIterations: qsharp.QSharpCallable = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%qsharp\n", + "/// # Summary\n", + "/// Estimate the number of iterations required for solution.\n", + "///\n", + "/// # Input\n", + "/// ## searchSpaceSize\n", + "/// The size of the search space.\n", + "///\n", + "/// # Remarks\n", + "/// This is correct for an amplitude amplification problem with a single \n", + "/// correct solution, but would need to be adapted when there are multiple\n", + "/// solutions\n", + "function NIterations(searchSpaceSize : Int) : Int {\n", + " let angle = ArcSin(1. / Sqrt(IntAsDouble(searchSpaceSize)));\n", + " let nIterations = Round(0.25 * PI() / angle - 0.5);\n", + " return nIterations;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now validate our ealier findings of search space size by running the NInterations. As a reminder, we predicted that for a puzzle with search space size $4$ we need $1$ iterations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f\"Number of iterations for search space size 4: {NIterations.simulate(searchSpaceSize=4)}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us now build our oracle. We will first build a marking oracle based on our edge constraints and then transform it to a phase oracle.\n", + "\n", + "Our marking oracle operates by marking the states for which each pair of colors connected by an edge constraint is different.\n", + "Since the colors are represented by bit strings, we need a separate operation `ApplyColorEqualityOracle` for comparing the colors and marking their equality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "microsoft": { + "language": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "/// # Summary\n", + "/// N-bit color equality oracle\n", + "///\n", + "/// # Input\n", + "/// ## color0\n", + "/// First color.\n", + "/// ## color1\n", + "/// Second color.\n", + "/// ## target\n", + "/// Will be flipped if colors are the same.\n", + "operation ApplyColorEqualityOracle(\n", + " color0 : Qubit[], color1 : Qubit[],\n", + " target : Qubit\n", + ")\n", + ": Unit is Adj + Ctl {\n", + " within {\n", + " // compute XOR of q0 and q1 in place (storing it in q1).\n", + " ApplyToEachCA(CNOT, Zipped(color0, color1));\n", + " } apply {\n", + " // if all XORs are 0, the bit strings are equal.\n", + " ControlledOnInt(0, X)(color1, target);\n", + " }\n", + "}\n", + "\n", + "\n", + "/// # Summary\n", + "/// Oracle for verifying vertex coloring. Checks that vertices that are related by an edge constraint do not share a value.\n", + "/// \n", + "/// \n", + "/// # Input\n", + "/// ## nVertices\n", + "/// The number of vertices in the graph.\n", + "/// ## bitsPerColor\n", + "/// The bits per color e.g. 2 bits per color allows for 4 colors.\n", + "/// ## edges\n", + "/// The array of (Vertex#,Vertex#) specifying the Vertices that can not be\n", + "/// the same color.\n", + "///\n", + "/// # Output\n", + "/// An marking oracle that marks as allowed those states in which the colors of qubits related by an edge constraint are not equal.\n", + "operation ApplyVertexColoringOracle (\n", + " nVertices : Int, \n", + " bitsPerColor : Int, \n", + " edges : (Int, Int)[],\n", + " colorsRegister : Qubit[],\n", + " target : Qubit\n", + ")\n", + ": Unit is Adj + Ctl {\n", + " let nEdges = Length(edges);\n", + " // we are looking for a solution that has no edge with same color at both ends\n", + " use edgeConflictQubits = Qubit[nEdges];\n", + " within {\n", + " for ((start, end), conflictQubit) in Zipped(edges, edgeConflictQubits) {\n", + " // Check that endpoints of the edge have different colors:\n", + " // apply ApplyColorEqualityOracle oracle;\n", + " // if the colors are the same the result will be 1, indicating a conflict\n", + " ApplyColorEqualityOracle(\n", + " colorsRegister[start * bitsPerColor .. (start + 1) * bitsPerColor - 1],\n", + " colorsRegister[end * bitsPerColor .. (end + 1) * bitsPerColor - 1],\n", + " conflictQubit\n", + " );\n", + " }\n", + " } apply {\n", + " // If there are no conflicts (all qubits are in 0 state), the vertex coloring is valid.\n", + " ControlledOnInt(0, X)(edgeConflictQubits, target);\n", + " }\n", + "}\n", + "\n", + "/// # Summary\n", + "/// Converts a marking oracle into a phase oracle.\n", + "///\n", + "/// # Input\n", + "/// ## oracle\n", + "/// The oracle which will mark the valid solutions.\n", + "///\n", + "/// # Output\n", + "/// A phase oracle that flips the phase of a state, iff the marking oracle marks a state.\n", + "operation ApplyPhaseOracle (oracle : ((Qubit[], Qubit) => Unit is Adj),\n", + " register : Qubit[]\n", + ")\n", + ": Unit is Adj {\n", + " use target = Qubit();\n", + " within {\n", + " // Put the target into the |-⟩ state.\n", + " X(target);\n", + " H(target);\n", + " } apply {\n", + " // Apply the marking oracle; since the target is in the |-⟩ state,\n", + " // flipping the target if the register satisfies the oracle condition\n", + " // will apply a -1 factor to the state.\n", + " oracle(register, target);\n", + " }\n", + " // We put the target back into |0⟩ so we can return it.\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With what we have now, we can build the main Grover's search algorithm loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "microsoft": { + "language": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "/// # Summary\n", + "/// Grover's Algorithm loop.\n", + "///\n", + "/// # Input\n", + "/// ## register\n", + "/// The register of qubits.\n", + "/// ## oracle\n", + "/// The oracle defining the solution we want.\n", + "/// ## iterations\n", + "/// The number of iterations to try.\n", + "///\n", + "/// # Remarks\n", + "/// Unitary implementing Grover's search algorithm.\n", + "operation ApplyGroversAlgorithmLoop(\n", + " register : Qubit[],\n", + " oracle : ((Qubit[], Qubit) => Unit is Adj),\n", + " statePrep : (Qubit[] => Unit is Adj),\n", + " iterations : Int\n", + ")\n", + ": Unit {\n", + " let applyPhaseOracle = ApplyPhaseOracle(oracle, _);\n", + " statePrep(register);\n", + "\n", + " for _ in 1 .. iterations {\n", + " applyPhaseOracle(register);\n", + " within {\n", + " Adjoint statePrep(register);\n", + " ApplyToEachA(X, register);\n", + " } apply {\n", + " Controlled Z(Most(register), Tail(register));\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And the combination of all these components will be the operation `SolvePuzzle` - the entry point to the quantum part of the program.\n", + "\n", + "> To be able to run our code on Azure Quantum, we have to return raw measurement results and convert them to the grid numbers later with classical post-processing code.\n", + ">\n", + "> We also cannot pass arrays of tuples to our operation using Azure Quantum job arguments. Therefore, we split our tuples arrays into pairs of arrays in the classical code, pass them as input parameters of type `Int[]`, and then zip them again in the Q# code.\n", + ">\n", + "> If we were just targeting simulation these compromises would not be necessary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Not required, but can be helpful to provide type information to some IDEs.\n", + "SolvePuzzle: qsharp.QSharpCallable = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "microsoft": { + "language": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "/// # Summary\n", + "/// Using Grover's search to find vertex coloring.\n", + "///\n", + "/// # Input\n", + "/// ## nVertices\n", + "/// The number of Vertices in the graph.\n", + "/// ## bitsPerColor\n", + "/// The number of bits per color.\n", + "/// ## nIterations\n", + "/// An estimate of the maximum iterations needed.\n", + "/// ## oracle\n", + "/// The Oracle used to find solution.\n", + "/// ## statePrep\n", + "/// An operation that prepares an equal superposition of all basis states in the search space.\n", + "///\n", + "/// # Output\n", + "/// An array giving the color of each vertex.\n", + "operation FindColorsWithGrover(\n", + " nVertices : Int, bitsPerColor : Int, nIterations : Int,\n", + " oracle : ((Qubit[], Qubit) => Unit is Adj),\n", + " statePrep : (Qubit[] => Unit is Adj)) : Result[] {\n", + "\n", + " // Coloring register has bitsPerColor qubits for each vertex\n", + " use register = Qubit[bitsPerColor * nVertices];\n", + "\n", + " Message($\"Trying search with {nIterations} iterations...\");\n", + " if (nIterations > 75) {\n", + " Message($\"Warning: This might take a while\");\n", + " }\n", + " ApplyGroversAlgorithmLoop(register, oracle, statePrep, nIterations);\n", + " return MultiM(register);\n", + "}\n", + "\n", + "/// # Summary\n", + "/// Solve a Sudoku puzzle using Grover's algorithm.\n", + "///\n", + "///\n", + "/// # Input\n", + "/// ## nVertices\n", + "/// number of blank squares.\n", + "/// ## bitsPerColor\n", + "/// The bits per color e.g. 2 bits per color allows for 4 colors.\n", + "/// ## emptySquareEdges{1,2}\n", + "/// The traditional edges passed to the graph coloring algorithm which, \n", + "/// in our case, are empty puzzle squares.\n", + "/// These edges define any \"same row\", \"same column\", \"same sub-grid\" \n", + "/// relationships between empty cells. Due to limitations the tuple list is split.\n", + "///\n", + "/// ## startingNumberConstraints{1,2}\n", + "/// The constraints on the empty squares due to numbers already in the \n", + "/// puzzle when we start. Due to limitations the tuple list is split.\n", + "///\n", + "/// # Output\n", + "/// An array of numbers for each empty square.\n", + "///\n", + "/// # Remarks\n", + "/// The inputs and outputs for the following 4x4 puzzle are:\n", + "/// -----------------\n", + "/// | | 1 | 2 | 3 | <--- empty square #0\n", + "/// -----------------\n", + "/// | 2 | | 0 | 1 | <--- empty square #1\n", + "/// -----------------\n", + "/// | 1 | 2 | 3 | 0 |\n", + "/// -----------------\n", + "/// | 3 | | 1 | 2 | <--- empty square #2\n", + "/// -----------------\n", + "/// emptySquareEdges = [(1, 0),(2, 1)] \n", + "/// empty square #0 can not have the same color/number as empty call #1.\n", + "/// empty square #1 and #2 can not have the same color/number (same column).\n", + "/// startingNumberConstraints = [(0, 2),(0, 1),(0, 3),(1, 1),(1, 2),(1, 0),(2, 1),(2, 2),(2, 3)]\n", + "/// empty square #0 can not have values 2,1,3 because same row/column/2x2grid.\n", + "/// empty square #1 can not have values 1,2,0 because same row/column/2x2grid.\n", + "/// Results = [0,3,0] i.e. Empty Square #0 = 0, Empty Square #1 = 3, Empty Square #2 = 0.\n", + "@EntryPoint()\n", + "operation SolvePuzzle(\n", + " nVertices : Int, bitsPerColor : Int,\n", + " emptySquareEdges1 : Int[],\n", + " emptySquareEdges2 : Int[],\n", + " startingNumberConstraints1: Int[],\n", + " startingNumberConstraints2: Int[]\n", + ") : Result[] {\n", + " let emptySquareEdges = Zipped(emptySquareEdges1, emptySquareEdges2);\n", + " let startingNumberConstraints = Zipped(startingNumberConstraints1, startingNumberConstraints2);\n", + " let oracle = ApplyVertexColoringOracle(nVertices, bitsPerColor, emptySquareEdges, _, _);\n", + " let statePrep = PrepareSearchStatesSuperposition(nVertices, bitsPerColor, startingNumberConstraints, _);\n", + " let searchSpaceSize = SearchSpaceSize(nVertices, bitsPerColor, startingNumberConstraints);\n", + " let numIterations = NIterations(searchSpaceSize);\n", + " Message($\"Solving Sudoku puzzle with #Vertex = {nVertices}\");\n", + " Message($\" Bits Per Color = {bitsPerColor}\");\n", + " Message($\" emptySquareEdges = {emptySquareEdges}\");\n", + " Message($\" startingNumberConstraints = {startingNumberConstraints}\");\n", + " Message($\" Estimated #iterations needed = {numIterations}\");\n", + " Message($\" Search Space Size = {searchSpaceSize}\");\n", + " return FindColorsWithGrover(nVertices, bitsPerColor, numIterations, oracle, statePrep);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Classical Postprocessing\n", + "Since we can't get the integer results directly from Q#, we will write classical post-processing code to convert the bitstrings we get into integers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def chunks(lst: list, n: int) -> list:\n", + " \"\"\"\n", + " Yield successive n-sized chunks from lst\n", + " \"\"\"\n", + " for i in range(0, len(lst), n):\n", + " yield lst[i:i + n]\n", + "\n", + "\n", + "def parse_measured_integer(arr: List[int]) -> int:\n", + " \"\"\"\n", + " Little endian bit list to integer\n", + " [1,0,1] -> 7\n", + " [0,1,1] -> 6\n", + " [] -> 0\n", + " \"\"\"\n", + " res = 0\n", + " for i in range(len(arr)):\n", + " if arr[i] != 0:\n", + " res += (2 ** i)\n", + " return res\n", + "\n", + "\n", + "def parse_measured_integers(arr: List[int], bit_length: int) -> list:\n", + " \"\"\"\n", + " Takes a resulting state vector from `SolvePuzzle`, \n", + " chunks it up into `bit_length` sized chunks and converts each chunk to an integer.\n", + " Returns list of resulting integers\n", + " \"\"\"\n", + " return list(map(parse_measured_integer, chunks(arr, bit_length)))\n", + "\n", + "\n", + "def _prepare_constraints(sudoku : Sudoku) -> tuple:\n", + " \"\"\"\n", + " The Azure Quantum service only allows lists of basic types so we will split up our list of tuples into tuples of lists\n", + " \"\"\"\n", + " (empty_square_edges, starting_number_constraints,\n", + " empty_squares) = sudoku.get_constraints()\n", + " empty_square_edges_1 = list(map(lambda x: x[0], empty_square_edges))\n", + " empty_square_edges_2 = list(map(lambda x: x[1], empty_square_edges))\n", + " starting_number_constraints_1 = list(\n", + " map(lambda x: x[0], starting_number_constraints))\n", + " starting_number_constraints_2 = list(\n", + " map(lambda x: x[1], starting_number_constraints))\n", + " return (empty_square_edges_1, empty_square_edges_2, starting_number_constraints_1, starting_number_constraints_2, empty_squares)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly we'll create a function that puts our classical preprocessing, our quantum algorithm, and classical postprocessing together to solve Sudoku puzzles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _solve_quantum(sudoku : Sudoku, solve_fn):\n", + " \"\"\"\n", + " Solve a Sudoku puzzle based on a quantum execution function that \n", + " takes the parameters for SolveSudoku from the Q# code above \n", + " and returns an array of the resulting bits\n", + " \"\"\"\n", + " (empty_square_edges_1, empty_square_edges_2,\n", + " starting_number_constraints_1, starting_number_constraints_2,\n", + " empty_squares) = sudoku.prepare_constraints()\n", + " measurements = solve_fn(nVertices=len(empty_squares),\n", + " bitsPerColor=sudoku.bit_length,\n", + " emptySquareEdges1=empty_square_edges_1,\n", + " emptySquareEdges2=empty_square_edges_2,\n", + " startingNumberConstraints1=starting_number_constraints_1,\n", + " startingNumberConstraints2=starting_number_constraints_2)\n", + " found_solution = isinstance(measurements, list)\n", + " if not found_solution:\n", + " # This will be hit when resource estimation is the case\n", + " print(\"No solution computed. Did you use resource estimation?\")\n", + " return measurements\n", + " print(\"Solved puzzle!\")\n", + " solution = parse_measured_integers(measurements, sudoku.bit_length)\n", + " print(f\"Raw solution: {solution}\")\n", + " for empty_idx in range(len(empty_squares)):\n", + " (i, j) = empty_squares[empty_idx]\n", + " # Solution range is [0,3] or [0,8], whereas human-readable puzzles work with [1,4] or [1,9], respectively.\n", + " sudoku[i, j] = solution[empty_idx] + 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Full State Simulator vs Sparse Simulator\n", + "\n", + "The QDK provides two main options for simulating the programs locally: regular (\"full state\") simulation and sparse simulation.\n", + "\n", + "Since the quantum states used in Grover's search inherently have many zero amplitudes and a lot of entanglement, sparse simulation should perform a lot better. \n", + "Let's try both options and see whether this is the case!\n", + "\n", + "To simulate a Q# operation from Python, you can call `QSharpFunctionName.simulate(params)` or `QSharpFunctionName.simulate_sparse(params)}`, where `params` are named parameters for your Q# operation. \n", + "We handle parameter passing in the `solve_quantum` function above, so all that's left to do is to specify which of the simulation functions we want to use as the `solve_fn` parameter.\n", + "\n", + "Let's use full-state and sparse simulation on 4x4 puzzles with few empty spaces to compare the performance of both simulators on this type of problems." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "from IPython.display import clear_output\n", + "\n", + "\n", + "def run_puzzles(simulator):\n", + " puzzles = [\n", + " [\n", + " [0, 2, 0, 4],\n", + " [3, 0, 0, 2],\n", + " [0, 0, 4, 1],\n", + " [4, 0, 2, 0]\n", + " ],\n", + " [\n", + " [0, 2, 3, 4],\n", + " [3, 4, 1, 2],\n", + " [2, 3, 4, 1],\n", + " [4, 1, 2, 3]\n", + " ],\n", + " [\n", + " [0, 2, 3, 4],\n", + " [3, 0, 1, 2],\n", + " [2, 3, 4, 1],\n", + " [4, 0, 2, 3]\n", + " ],\n", + " [\n", + " [0, 0, 3, 4],\n", + " [0, 0, 1, 2],\n", + " [2, 3, 4, 1],\n", + " [4, 1, 2, 3]\n", + " ]\n", + " ]\n", + " for puzzle in puzzles:\n", + " sudoku = Sudoku(puzzle)\n", + " sudoku.solve_quantum(simulator)\n", + " print(sudoku)\n", + " print(\"Valid!\" if sudoku.is_valid() else \"Invalid!\")\n", + " if not sudoku.is_valid():\n", + " raise Exception(\"Invalid solution!\")\n", + "\n", + "\n", + "time_reg_sim = time.time()\n", + "run_puzzles(SolvePuzzle.simulate)\n", + "time_reg_sim = time.time() - time_reg_sim\n", + "\n", + "time_sparse_sim = time.time()\n", + "run_puzzles(SolvePuzzle.simulate_sparse)\n", + "time_sparse_sim = time.time() - time_sparse_sim\n", + "\n", + "clear_output(wait=True) # Remove all the execution printing\n", + "\n", + "print(f\"Regular simulator time: {time_reg_sim}s\", flush=True)\n", + "print(f\"Sparse simulator time: {time_sparse_sim}s\", flush=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can clearly see that the sparse simulator is two orders of magnitude faster, and this difference will only grow as the program size increases. \n", + "So going forward we will be using it to simulate our code for the larger puzzles. \n", + "Generally speaking, the sparse simulator is often faster than the full state simulator, so make sure to try it when working on your own programs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Solving Puzzles With the Sparse Simulator\n", + "Below we have a few puzzles of varying difficulty, including a 9x9 puzzle with over 20 empty squares. \n", + "Most puzzles take very little time thanks to the sparse simulator, so you can run them without waiting too much.\n", + "The `hard` variable turns on running the \"difficult\" $4x4$ and $9x9$ puzzles (the ones with 13 or more empty cells), so if you have some spare time, set it to true and see that our solution works for these challenging problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hard = False\n", + "\n", + "\n", + "puzzles = [\n", + " [ # A base case problem requiring no iterations (3 empty cells)\n", + " [0, 2, 3, 4],\n", + " [3, 0, 1, 2],\n", + " [2, 3, 4, 1],\n", + " [4, 0, 2, 3]\n", + " ],\n", + " [ # This is the most difficult 4x4 puzzle we simulate reasonable quickly (12 empty cells)\n", + " [0, 0, 3, 0],\n", + " [0, 0, 0, 2],\n", + " [0, 0, 4, 0],\n", + " [0, 1, 0, 0]\n", + " ],\n", + " [ # This is the most difficult 4x4 puzzle we can simulate (13 empty cells)\n", + " [0, 0, 0, 0],\n", + " [0, 0, 0, 2],\n", + " [0, 0, 4, 0],\n", + " [0, 1, 0, 0]\n", + " ],\n", + " [ # This is the most difficult 4x4 puzzle we can run on Quantinuum hardware (4 empty cells)\n", + " [0, 0, 3, 4],\n", + " [3, 4, 1, 2],\n", + " [0, 3, 4, 1],\n", + " [4, 0, 2, 3]\n", + " ],\n", + " [ # This is the most difficult 9x9 puzzle we can run on Quantinuum hardware (3 empty cells)\n", + " [6, 0, 3, 8, 9, 4, 5, 1, 2],\n", + " [9, 0, 2, 7, 3, 5, 4, 8, 6],\n", + " [8, 4, 0, 6, 1, 2, 9, 7, 3],\n", + " [7, 0, 8, 2, 6, 1, 3, 5, 4],\n", + " [5, 2, 6, 4, 7, 3, 8, 9, 1],\n", + " [1, 3, 4, 5, 8, 9, 2, 6, 7],\n", + " [4, 6, 9, 1, 2, 8, 7, 0, 5],\n", + " [2, 8, 7, 3, 5, 6, 1, 4, 9],\n", + " [3, 5, 1, 9, 4, 7, 6, 2, 8]\n", + " ],\n", + " [ # This is the most difficult 9x9 puzzle we can simulate (21 empty cells)\n", + " [0, 7, 3, 8, 0, 0, 5, 1, 2],\n", + " [9, 0, 2, 7, 0, 5, 4, 8, 6],\n", + " [8, 4, 5, 0, 0, 0, 0, 0, 0],\n", + " [7, 0, 8, 2, 0, 1, 3, 5, 0],\n", + " [5, 2, 6, 4, 0, 3, 8, 9, 1],\n", + " [1, 3, 4, 5, 0, 0, 0, 0, 0],\n", + " [4, 6, 9, 1, 2, 8, 7, 3, 5],\n", + " [2, 8, 7, 3, 5, 6, 1, 4, 9],\n", + " [3, 5, 1, 9, 4, 7, 6, 0, 8]\n", + " ]\n", + "]\n", + "\n", + "for puzzle in puzzles:\n", + " sudoku = Sudoku(puzzle)\n", + " print(sudoku)\n", + " if not hard and sudoku.count_empty_squares() > 12:\n", + " print(\"Skipping hard puzzles\")\n", + " continue\n", + " sudoku.solve_quantum(SolvePuzzle.simulate_sparse)\n", + " print(sudoku)\n", + " print(\"Valid!\" if sudoku.is_valid() else \"Invalid!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Resource Estimation\n", + "Seeing that our code solves puzzles correctly, let's get our work ready to run on Azure Quantum targets!\n", + "To do that, we need to see what instances would be feasible to run.\n", + "For that we will use the resource estimator to see how many resources our computation requires. \n", + "\n", + "The resources estimator is one of the local simulators provided by the QDK. \n", + "We can reuse our previous code to estimate resources for each puzzle automatically.\n", + "The resources consumption will depend on the puzzle instance, its size, and the number and the locations of empty squares.\n", + "To see if we can solve a specific puzzle instance on Quantinuum, we check that the number of qubits it requires fits within the number of qubits of the Quantinuum H1-1 system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def runs_on_h11(res) -> bool:\n", + " return res['QubitCount'] <= 20\n", + "\n", + "\n", + "# We'll keep track of which puzzles we can run where!\n", + "h11_puzzles = []\n", + "\n", + "\n", + "for puzzle in puzzles:\n", + " sudoku = Sudoku(puzzle)\n", + " print(sudoku)\n", + " resources = sudoku.solve_quantum(SolvePuzzle.estimate_resources)\n", + " print(resources)\n", + " if runs_on_h11(resources):\n", + " h11_puzzles.append(puzzle)\n", + " print(\"Can run on Quantinuum H1-1!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that our puzzles have vastly differing qubit requirements. \n", + "These are made up of the following:\n", + "* We need $2k$ qubits for our representation of $k$ empty cells\n", + "* We need $m$ auxiliary qubits in the oracle for $m$ edge constraints, and one more to convert the marking oracle into the phase one\n", + "* We could need some auxiliary qubits to decompose multi-controlled gates into sequences of smaller gates\n", + "\n", + "So, if we see a puzzle with $k$ missing numbers that requires exactly $2k$ qubits, such as the first puzzle from our list, we know that the oracle is never called, and the solution hence amounts to state preparation and measurement. \n", + "The larger puzzles will actually use the oracle to find the best states. \n", + "We can confirm these findings based on the search space sizes that we got during simulation.\n", + "\n", + "With that, let's run some puzzles on Quantinuum through Azure Quantum!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Running on Quantinuum targets\n", + "We will now define a function to execute our code on Azure Quantum.\n", + "For this we call we will use the `qsharp.azure.submit` function that accepts as input the Q# function, the number of shots, job name, and parameters that will then return a job which we can later get results from.\n", + "\n", + "We will also define a method, where we can get the job results and use those for our computation. \n", + "This is for when we submit to real hardware/emulators which can have hours of queuing time. \n", + "Here we also need a submission function to have our job ready." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import json\n", + "from qsharp.azure import AzureJob\n", + "\n", + "def get_job(job_id) -> tuple:\n", + " current_job = qsharp.azure.status(jobId=job.id)\n", + " succeeded = current_job.status == 'Succeeded'\n", + " if succeeded:\n", + " print(f'Job completed')\n", + " elif current_job.status == 'Failed':\n", + " print(f'Job for this puzzle failed. Please review reason at {job.uri}')\n", + " else:\n", + " print(f'Job for given puzzle is still running. Please check back later!')\n", + " return (succeeded, current_job)\n", + "\n", + "def get_highest_probability_output(output: dict) -> list:\n", + " \"\"\"\n", + " Goes through outputs from an Azure Quantum job and returns the list with the highest probability\n", + " Returns a list of integers representing the ket notation of the output \n", + " \"\"\"\n", + " max_prob = -1\n", + " max_val = ''\n", + " for (val, prob) in output.items():\n", + " if (prob > max_prob):\n", + " max_prob = prob\n", + " max_val = val\n", + " ret = json.loads(max_val) # convert string rep of list into list type\n", + " return ret\n", + "\n", + "\n", + "def job_name(self: Sudoku) -> str:\n", + " return f\"{self.size}x{self.size} Solve Sudoku Puzzle w/ {self.count_empty_squares()} unknowns:\\n{str(self)}\"\n", + "\n", + "\n", + "def get_results_from_azure(job_id: str):\n", + " \"\"\"\n", + " Wrapper function that generates a function pretends execution\n", + " but instead get results from previous execution of a job with id `job_id`\n", + " \"\"\"\n", + " def get_results_from_azure_with_param(**kwargs) -> list:\n", + " \"\"\"\n", + " Function to hook into `solve_quantum` method of a `Sudoku` \n", + " where it gets the job results and return the highest probability result\n", + " \"\"\"\n", + " output = qsharp.azure.output(jobId=job_id)\n", + " return get_highest_probability_output(output)\n", + " return get_results_from_azure_with_param\n", + "\n", + "\n", + "def job_msg(job) -> str:\n", + " return f\"Submitted job with id {job.id}. Track at {job.uri}\"\n", + "\n", + "\n", + "def submit_job_to_azure(self: Sudoku) -> AzureJob:\n", + " \"\"\"\n", + " Submits a Sudoku puzzle to be run on Azure asynchronously.\n", + " Return the resulting Azure Quantum job\n", + " \"\"\"\n", + " (empty_square_edges_1, empty_square_edges_2,\n", + " starting_number_constraints_1, starting_number_constraints_2,\n", + " empty_squares) = self.prepare_constraints()\n", + " job = qsharp.azure.submit(\n", + " SolvePuzzle,\n", + " shots=250,\n", + " jobName=self.job_name(),\n", + " nVertices=len(empty_squares),\n", + " bitsPerColor=self.bit_length,\n", + " emptySquareEdges1=empty_square_edges_1,\n", + " emptySquareEdges2=empty_square_edges_2,\n", + " startingNumberConstraints1=starting_number_constraints_1,\n", + " startingNumberConstraints2=starting_number_constraints_2\n", + " )\n", + " print(job_msg(job))\n", + " return job\n", + "\n", + "\n", + "Sudoku.job_name = job_name\n", + "Sudoku.submit_job_to_azure = submit_job_to_azure" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now run our code on Quantinuum targets. To check that it is compatible with Quantinuum machines, let us first run it on the API validator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qsharp.azure.target('quantinuum.hqs-lt-s1-apival')\n", + "\n", + "api_validation_jobs = list()\n", + "for puzzle in h11_puzzles: # We'll be using the puzzles we found viable earlier!\n", + " sudoku = Sudoku(puzzle)\n", + " if not sudoku.count_empty_squares() >= 4:\n", + " continue\n", + " print(sudoku)\n", + " job = sudoku.submit_job_to_azure()\n", + " api_validation_jobs.append(job)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "all_completed = True\n", + "for job in api_validation_jobs:\n", + " (completed, _) = get_job(job)\n", + " if not completed:\n", + " all_completed = False\n", + "if all_completed:\n", + " # Previous call would throw exception if invalid\n", + " print(\"API validation succeeded!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After seeing that everything works smoothly, we will run on the H1-1 emulator.\n", + " \n", + "The H1-1 emulator can accurately model the noise of the H1-1 quantum machine, allowing us get good results while staying within our free credits. \n", + "To save on credits we only run the interesting examples of 5 unknowns. \n", + "One of the examples has a search space size of 1, essentially making it state preparation and measurement.\n", + " The other however, has a search space size of 4, causing us to access the Oracle.\n", + "\n", + "**Please note that this sample makes use of paid services on Azure Quantum. \n", + "The cost of running this sample with the provided parameters on Quantinuum in a free trial subscription is approximately 360EHQC.\n", + "This quantity is only an approximate estimate and should not be used as a binding reference. \n", + "The cost of the service might vary depending on your region, demand and other factors.**\n", + "\n", + "Please note that the jobs might take a while. \n", + "If you are running in the browser on Azure Quantum, please keep the tab open.\n", + "If you are running on VSCode, feel free to come back later and load the pickle file.\n", + "Feel free to come back later (you can use the average job time as a point of reference that we saw when connecting to Azure Quantum)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pickle # Used to persist current jobs\n", + "pkl_filename = \"sudoku-quantinuum_jobs.pkl\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qsharp.azure.target('quantinuum.hqs-lt-s1-sim')\n", + "hw_jobs = []\n", + "for puzzle in h11_puzzles:\n", + " sudoku = Sudoku(puzzle)\n", + " if not sudoku.count_empty_squares() >= 4:\n", + " continue\n", + " print(sudoku)\n", + " job = sudoku.submit_job_to_azure()\n", + " hw_jobs.append((puzzle, job))\n", + " print(job_msg(job))\n", + "\n", + "# Save current jobs to file, in case you want to come back later\n", + "pklfile = open(pkl_filename, 'wb')\n", + "pickle.dump(hw_jobs, pklfile)\n", + "pklfile.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pklfile = open(pkl_filename, 'rb')\n", + "hw_jobs = pickle.load(pklfile)\n", + "pklfile.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for (puzzle, job) in hw_jobs:\n", + " sudoku = Sudoku(puzzle)\n", + " print(sudoku)\n", + "\n", + " sudoku.solve_quantum(get_results_from_azure(job.id))\n", + " print(sudoku)\n", + " print(\"Valid!\" if sudoku.is_valid() else \"Invalid\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernel_info": { + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3.7.13 ('qsharp-env-37')", + "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.7.13" + }, + "nteract": { + "version": "nteract-front-end@1.0.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "2ae822f0f83fed12107faf85edb2b742fdce767c971cd51f224ccfc7e38aba9d" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/samples/azure-quantum/grover-sudoku/README.md b/samples/azure-quantum/grover-sudoku/README.md new file mode 100644 index 000000000000..7cd752cd6914 --- /dev/null +++ b/samples/azure-quantum/grover-sudoku/README.md @@ -0,0 +1,37 @@ +--- +page_type: sample +author: adrianleh +description: Solving Sudoku with Grover's search, using the Azure Quantum service +ms.author: t-alehmann@microsoft.com +ms.date: 08/16/2021 +languages: +- qsharp +- python +products: +- qdk +- azure-quantum +--- + +# Solving Sudoku with Grover's search + +In this sample, we will be solving Sudoku puzzles using Grover's search. + +Given that we will run our algorithm on current quantum hardware, we need to minimize qubit count and circuit depth (number of gates) required by the algorithm. + +Since Grover's search is fundamentally a quantum algorithm requiring classical preprocessing, we will use the feature of Python notebooks integrating with Q#. +This will further enable us to have some convenience in the data structures we build, such as classical validation of Sudoku puzzles. + +## Q# with Jupyter Notebook + +Make sure that you have followed the [Q# + Jupyter Notebook quickstart](https://docs.microsoft.com/azure/quantum/install-jupyter-qdk) for the Quantum Development Kit, and then start a new Jupyter Notebook session from the folder containing this sample: + +```shell +cd grover-sudoku +jupyter notebook +``` + +Once Jupyter starts, open the `Grovers-sudoku-quantinuum.ipynb` notebook and follow the instructions there. + +## Manifest + +- [GroversSudokuQuantinuum.ipynb](https://github.com/microsoft/quantum/blob/main/samples/azure-quantum/grover-sudoku/Grovers-sudoku-quantinuum.ipynb): IQ# notebook for this sample targetting Quantinuum.