diff --git a/learning/modules/computer-science/grovers.ipynb b/learning/modules/computer-science/grovers.ipynb
index 06b58d88aec..747f1b6340b 100644
--- a/learning/modules/computer-science/grovers.ipynb
+++ b/learning/modules/computer-science/grovers.ipynb
@@ -95,7 +95,7 @@
"id": "4f900ae6",
"metadata": {},
"source": [
- "## Math\n",
+ "## Theory\n",
"Suppose there exists a function $f$ that maps binary strings to a single binary variable, meaning\n",
"$$\n",
"f: \\Sigma^n \\rightarrow \\Sigma\n",
@@ -130,7 +130,7 @@
"id": "a1a51eb6",
"metadata": {},
"source": [
- "## Sketch of circuits in Grover's algorithm\n",
+ "### Sketch of circuits in Grover's algorithm\n",
"\n",
"A full mathematical walkthrough of Grover's algorithm can be found, for example, in [Fundamentals of quantum algorithms](/learning/courses/fundamentals-of-quantum-algorithms), a course by John Watrous on IBM Quantum Learning. A condensed treatment is provided in an appendix at the end of this module. But for now, we will only review the overall structure of the quantum circuit that implements Grover's algorithm.\n",
"\n",
@@ -265,22 +265,84 @@
"In the next section, we will put this algorithm into practice using real IBM® quantum computers."
]
},
+ {
+ "cell_type": "markdown",
+ "id": "geo_picture_01",
+ "metadata": {},
+ "source": [
+ "### The geometric picture\n",
+ "\n",
+ "The two-qubit example above showed how the algebra works out for a small case, but there is a much more intuitive way to understand Grover's algorithm: as a sequence of geometric reflections in a two-dimensional plane. Below we describe this picture. You can also see John Watrous's course [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/analysis) for more details.\n",
+ "\n",
+ "**Setting up the plane.** We can decompose the initial superposition state $|\\psi\\rangle$ into two components. The correct state — the one we're searching for — we call $|A_1\\rangle$. Every other state, lumped together, we call $|A_0\\rangle$. By definition, $|A_1\\rangle$ and $|A_0\\rangle$ are orthogonal to one another, so we can plot them as perpendicular axes in an abstract, two-dimensional space. Since $|\\psi\\rangle$ is a linear combination of these two components, it sits at some small angle $\\theta$ to the $|A_0\\rangle$ axis — close to $|A_0\\rangle$, because at the start, only a tiny fraction of the state is in the correct component $|A_1\\rangle$.\n",
+ "\n",
+ "\n",
+ "**Reflections.** The key mathematical fact we need is that an operator of the form\n",
+ "$$\n",
+ "2|v\\rangle\\langle v| - I\n",
+ "$$\n",
+ "reflects any state about the axis defined by $|v\\rangle.$ To see why, consider two cases: a state along $|v\\rangle$ is left unchanged, and a state perpendicular to $|v\\rangle$ gets its sign flipped. Any other state can be decomposed into these two components, and the operator acts on each accordingly — which is exactly a reflection about $|v\\rangle$.\n",
+ "\n",
+ "It turns out that both the oracle and the diffusion steps in Grover's algorithm can be expressed as reflections in this geometric picture.\n",
+ "\n",
+ "**The oracle as a reflection.** The oracle flips the sign of the $|A_1\\rangle$ state and leaves everything else alone. That is the same as a reflection about the $|A_0\\rangle$ axis.\n",
+ "\n",
+ "\n",
+ "\n",
+ "**Diffusion as a reflection.** It is a little trickier to see how the diffusion operator is also a reflection. The diffusion operator is\n",
+ "$$\n",
+ "H^{\\otimes n}\\, Z_{\\text{OR}}\\, H^{\\otimes n}\n",
+ "$$\n",
+ "$Z_{\\text{OR}}$ by itself is a reflection about the all-zero state, since it flips the sign of every state that is not $|0\\rangle^{\\otimes n}$. This can be written as $2|0\\rangle\\langle 0| - I$. The surrounding Hadamard layers effectively perform a change of basis, transforming the axis of reflection. Recall that $H^{\\otimes n}$ maps $|0\\rangle^{\\otimes n}$ to the uniform superposition $|u\\rangle = \\frac{1}{\\sqrt{N}}\\sum_{x}|x\\rangle$. Since the Hadamard is its own inverse, the full expression becomes\n",
+ "$$\n",
+ "H^{\\otimes n}\\left(2|0\\rangle\\langle 0| - I\\right)H^{\\otimes n} = 2|u\\rangle\\langle u| - I\n",
+ "$$\n",
+ "which is a reflection about $|u\\rangle$. Since $|u\\rangle$ is very close to $|\\psi\\rangle$ (both are nearly along $|A_0\\rangle$), this second reflection sends the state to an angle $2\\theta$ from where it started.\n",
+ "\n",
+ "\n",
+ "\n",
+ "**Rotation by $2\\theta$.** The combined effect of these two reflections is a rotation by $2\\theta$ toward $|A_1\\rangle$. Each successive iteration of the Grover operator rotates the state by another $2\\theta.$\n",
+ "\n",
+ "**Optimal number of iterations.** Our goal is to rotate the state as close to $|A_1\\rangle$ as possible, which means rotating by a total of approximately $\\pi/2$ radians (a quarter turn). If each iteration contributes $2\\theta$, the optimal number of iterations $t$ satisfies\n",
+ "$$\n",
+ "(2t + 1)\\theta \\approx \\frac{\\pi}{2}\n",
+ "$$\n",
+ "For a single solution among $N$ states, the initial angle is $\\theta \\approx \\sin^{-1}(1/\\sqrt{N}) \\approx 1/\\sqrt{N}$ (for large $N$). Substituting,\n",
+ "$$\n",
+ "t \\approx \\frac{\\pi}{4}\\sqrt{N} - \\frac{1}{2}\n",
+ "$$\n",
+ "This is where the famous $\\sqrt{N}$ speedup comes from: we only need $O(\\sqrt{N})$ iterations to reach the target, rather than the $O(N)$ checks a classical search would require.\n",
+ "\n",
+ "More generally, if there are $|A_1|$ solution states among $N$ total states, the optimal number of iterations is\n",
+ "$$\n",
+ "t \\approx \\frac{\\pi}{4}\\sqrt{\\frac{N}{|A_1|}} - \\frac{1}{2}\n",
+ "$$\n",
+ "\n",
+ "Note that if you apply too many iterations, you rotate past $|A_1\\rangle$ and the probability of finding your target state will start to decrease again. Finding the right number of iterations is important, though on noisy quantum hardware the experimentally optimal number may differ from this ideal formula."
+ ]
+ },
{
"cell_type": "markdown",
"id": "3fd8fbb3",
"metadata": {},
"source": [
- "## Obvious caveat\n",
+ "### Why is Grover's algorithm useful?\n",
"\n",
- "In order to apply Grover's algorithm, we had to build the Grover operator, which means we must be able to flip the phase on states that satisfy our solution criteria. This begs the question: if we know our solution set so well that we can design a quantum circuit to label each one, what are we searching for?! The answer is three-fold, and we will revisit this throughout the tutorial:\n",
+ "At this point you may be wondering: we just built an oracle that marks a target state — but to build it, we had to know the target state. So what are we actually searching for?\n",
"\n",
- "**(1) These kinds of query algorithms often involve two parties**: one who has the oracle that establishes the solution criteria, and another who is trying to guess/find a solution state. The fact that one person can build the oracle does not negate the need for search.\n",
+ "This is a fair question, and there are several good answers.\n",
"\n",
- "**(2) There are problems for which it is easier to specify a solution criterion than it is to find the solution.** The best-known example of this is probably identifying prime factors of large numbers. Grover's algorithm is not particularly effective at solving that specific problem; we would use Shor's algorithm for prime factoring. This is just an example to point out that knowing the criterion on the behavior of a state is not always the same as knowing the state.\n",
+ "- **The query model is a theoretical tool.** The query model of computation was never designed to be directly practical. Its purpose is to give us a clean way to analyze algorithmic complexity by separating a problem into two parts: the oracle, and everything else. How hard is the search, given that verification is free? How does the number of queries scale with the size of the input? These are useful questions even if no real system works exactly this way.\n",
"\n",
- "**(3) Grover's algorithm does not only return classical data.** True, if we make a measurement of the final state after $t$ repetitions of the Grover operator, we are likely to obtain classical information identifying the solution state. But what if we don't want classical information; what if we want a solution state prepared on a quantum computer for further use in another algorithm? Grover's algorithm actually produces a quantum state with the solutions heavily weighted. So you may expect to find Grover's algorithm as a subroutine in more complicated quantum algorithms.\n",
+ "- You can also think of it as a **two-party activity**: one person knows the target state and builds the oracle; the other person's job is to find the answer using the oracle as a black box, with no peeking inside. In Activity 2 below, you will do exactly this with a partner.\n",
"\n",
- "With these in mind, let us work through several examples. We'll begin with an example in which the solution state is clearly specified so we can follow the logic of the algorithm, and we will move on to examples in which the usefulness of Grover's algorithm becomes more clear."
+ "- **Amplitude amplification is a broadly useful subroutine.** Even if this first demonstration seems circular, the underlying mechanism — called *amplitude amplification* — shows up again and again in quantum computing. What we are really building here is an intuition for a tool that appears as a subroutine in many more complex quantum algorithms.\n",
+ "\n",
+ "- **There are problems where you can build an oracle without knowing the answer.** The key insight is that there exists a whole class of problems for which it is very hard to *find* a solution, but very easy to *check* that a given solution is correct. Factoring is one example: given a product of two large primes, it is extremely difficult to determine what those primes are, but once you have them, you can easily multiply them together to verify. (We have a better algorithm than Grover's for factoring specifically — see Shor's algorithm — but this is far from the only problem with this feature.) Sudoku, constraint satisfaction, and even the classic game of Minesweeper are all problems that are difficult to solve but easy to check.\n",
+ "\n",
+ "Why is that relevant? It means we can know all of the *conditions* and *requirements* that a solution must satisfy, and we can encode those requirements into a quantum circuit that serves as the oracle — even though we do not know the solution itself. Grover's algorithm will find it for us.\n",
+ "\n",
+ "With these ideas in mind, let us work through several examples. We will begin with an example in which the solution state is clearly specified so we can follow the logic of the algorithm. We will then move on to a two-party activity, and finally to an example in which the oracle is built from problem constraints rather than from knowledge of the answer."
]
},
{
@@ -288,7 +350,7 @@
"id": "90cfe463",
"metadata": {},
"source": [
- "## General imports and approach\n",
+ "### General imports and approach\n",
"\n",
"We start by importing several necessary packages."
]
@@ -304,7 +366,7 @@
"import math\n",
"\n",
"# Imports from Qiskit\n",
- "from qiskit import QuantumCircuit\n",
+ "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n",
"from qiskit.circuit.library import grover_operator, MCMTGate, ZGate\n",
"from qiskit.visualization import plot_distribution\n",
"from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager"
@@ -338,7 +400,7 @@
"id": "23b5217f",
"metadata": {},
"source": [
- "## Step 1: Map classical inputs to a quantum problem\n",
+ "### Step 1: Map classical inputs to a quantum problem\n",
"\n",
"We need the phase query gate to put an overall phase (-1) on solution states, and leave the non-solution states unaffected. Another way of saying this is that Grover's algorithm requires an oracle that specifies one or more marked computational basis states, where \"marked\" means a state with a phase of -1. This is done using a controlled-Z gate, or its multi-controlled generalization over $N$ qubits. To see how this works, consider a specific example of a bitstring `{110}`. We would like a circuit that acts on a state $|\\psi\\rangle = |q_2,q_1,q_0\\rangle$ and applies a phase if $|\\psi\\rangle = |011\\rangle$ (where we have flipped the order of the binary string, because of the notation in Qiskit, which puts the least significant (often 0) qubit on the right).\n",
"\n",
@@ -497,12 +559,11 @@
"id": "fb293e00",
"metadata": {},
"source": [
- "As we argued above, we may need to apply the Grover operator multiple times. The optimal number of iterations, $t,$ to maximize the amplitude of the target state in the absence of noise can be obtained from this expression:\n",
+ "As we discussed in the geometric picture above, we might need to apply the Grover operator multiple times. The optimal number of iterations $t$ to maximize the amplitude of the target state in the absence of noise is\n",
"$$\n",
- "(2t+1)\\theta = (2t+1)\\sin^{-1}\\left( \\sqrt{\\frac{|A_1|}{N}}\\right) \\approx (2t+1)\\sqrt{\\frac{|A_1|}{N}} \\approx \\frac{\\pi}{2}\\\\\n",
"t\\approx \\frac{\\pi}{4} \\sqrt{\\frac{N}{|A_1|}}-\\frac{1}{2}\n",
"$$\n",
- "Here $A_1$ is the number of solutions or target states. On modern noisy quantum computers, the experimentally optimal number of iterations might be different - but here we calculate and use this theoretical, optimal number using $A_1=1$."
+ "where $|A_1|$ is the number of solution states and $N=2^n$ is the total number of states. On modern noisy quantum computers, the experimentally optimal number of iterations might be different — but here we calculate and use this theoretical, optimal number using $|A_1|=1$."
]
},
{
@@ -569,7 +630,7 @@
"source": [
"We have constructed our Grover circuit!\n",
"\n",
- "## Step 2: Optimize problem for quantum hardware execution\n",
+ "### Step 2: Optimize problem for quantum hardware execution\n",
"\n",
"We have defined our abstract quantum circuit, but we need to rewrite it in terms of gates that are native to the quantum computer we actually want to use. We also need to specify which qubits on the quantum computer should be used. For these reasons and others, we now must transpile our circuit. First, let us specify the quantum computer we wish to use.\n",
"\n",
@@ -676,7 +737,7 @@
"source": [
"These are actually quite large numbers, even for this simple case. Since all quantum gates (and especially two-qubit gates) experience errors and are subject to noise, a series of over 100 two-qubit gates would result in nothing but noise if the qubits were not extremely high-performing. Let's see how these perform.\n",
"\n",
- "## Execute using Qiskit primitives\n",
+ "### Step 3: Execute using Qiskit primitives\n",
"\n",
"We want to make many measurements and see which state is the most likely. Such an amplitude amplification is a sampling problem that is suitable for execution with the `Sampler` Qiskit Runtime primitive.\n",
"\n",
@@ -727,7 +788,7 @@
"id": "f19b1adb",
"metadata": {},
"source": [
- "## Step 4: Post-process and return result in desired classical format\n",
+ "### Step 4: Post-process and return result in desired classical format\n",
"\n",
"Now we can plot the results of our sampling in a histogram."
]
@@ -794,11 +855,11 @@
"\n",
"\n",
"\n",
- "# Activity 2: An accurate query algorithm workflow\n",
+ "## Activity 2: An accurate query algorithm workflow\n",
"\n",
"We will start this activity exactly as the first one, except that now you will pair up with another Qiskit enthusiast. You will pick a secret bitstring, and your partner will pick a (generally) different bitstring. You will each generate a quantum circuit that functions as an oracle, and you will exchange them. You will then use Grover's algorithm with that oracle to determine your partner's secret bitstring.\n",
"\n",
- "## Step 1: Map classical inputs to a quantum problem\n",
+ "### Step 1: Map classical inputs to a quantum problem\n",
"\n",
"Using the `grover_oracle` function defined above, construct an oracle circuit for one or more marked states. Make sure you tell your partner how many states you have marked, so they can apply the Grover operator the optimal number of times. **Don't make your bitstring too long. 3-5 bits should work without much difficulty.** Longer bitstrings would result in deep circuits that require more advanced techniques like error mitigation."
]
@@ -915,7 +976,7 @@
"id": "37eb709b",
"metadata": {},
"source": [
- "## Step 2: Optimize problem for quantum hardware execution\n",
+ "### Step 2: Optimize problem for quantum hardware execution\n",
"\n",
"This proceeds exactly as before."
]
@@ -942,7 +1003,7 @@
"id": "de2af3e1",
"metadata": {},
"source": [
- "## Step 3: Execute using Qiskit primitives\n",
+ "### Step 3: Execute using Qiskit primitives\n",
"\n",
"This is also identical to the process in the first activity."
]
@@ -969,7 +1030,7 @@
"id": "14bbde32",
"metadata": {},
"source": [
- "## Step 4: Post-process and return result in desired classical format\n",
+ "### Step 4: Post-process and return result in desired classical format\n",
"\n",
"Now display a histogram of your sampling results. One or more states should have much higher measurement probability than the others. Report these to your partner and check if you correctly determined the target states. By default, the histogram displayed is of the same circuit from the first activity. You should obtain different results from your partner's circuit."
]
@@ -1051,184 +1112,232 @@
},
{
"cell_type": "markdown",
- "id": "8c85ebd9",
+ "id": "mine_intro_01",
"metadata": {},
"source": [
- "## Activity 3: Criterion other than a specific bitstring\n",
+ "## Activity 3: Solve a Minesweeper grid with Grover's algorithm\n",
+ "\n",
+ "In the previous section, we noted that Grover's algorithm becomes genuinely useful when we can build an oracle from the *constraints* of a problem, rather than from knowledge of the answer. Minesweeper is a perfect example: the numbered cells tell us how many mines are adjacent, and those constraints fully determine where the mines must be — but finding the configuration requires search.\n",
+ "\n",
+ "Minesweeper has been proven to be NP-complete: it is hard to solve but easy to check. That makes it a natural candidate for Grover's algorithm. Of course, we cannot yet solve a full 9$\\times$9 grid on a noisy quantum computer — the circuits would be far too deep. Instead, we will use a tiny grid as a toy demonstration of how one would approach a larger board on a future fault-tolerant machine.\n",
+ "\n",
+ "A few important caveats. Grover's algorithm provides only a quadratic speedup over *unstructured* classical search. Minesweeper almost certainly has exploitable structure that a clever classical algorithm could use. And for an exponentially growing search space, even the $\\sqrt{N}$ improvement only goes so far. But let us set those concerns aside and use this toy problem to illustrate how problem constraints get encoded into a quantum oracle.\n",
"\n",
- "As a final illustration of how Grover's algorithm might be useful in the context of a subroutine, let us imagine that we need quantum states with a certain number of `1` characters in the bitstring representation. This is common in situations where conservation laws are involved. For example, in the context of quantum chemistry, one often maps a `1` state of a qubit to an occupation of an electronic orbital. In order for charge to be conserved, the total number of `1` characters in the bitstring must also stay constant. Constraints like this are so common they have a special name: **Hamming weight constraints**, and the number of `1` characters in the bitstring representation of the state is called the **Hamming weight**.\n",
+ "### The grid\n",
"\n",
- "## Step 1:\n",
+ "Here is our baby minesweeper grid:\n",
"\n",
- "Let us first write a function that marks states with the desired Hamming weight. We will write this in general for an unspecified number of qubits `num_qubits` and target Hamming weight `target_weight`."
+ "\n",
+ "\n",
+ "Each blank cell can be represented by a binary variable indicating whether it contains a mine. We label these $x_0$, $x_1$, and $x_2$, where $x_i = 1$ means there is a mine on that cell and $x_i = 0$ means there is not:\n",
+ "\n",
+ "\n",
+ "\n",
+ "We could solve this in our heads in about half a second, but we are using this toy problem to illustrate how a much harder board could be approached with a quantum computer."
]
},
{
- "cell_type": "code",
- "execution_count": null,
- "id": "a3bb0650",
+ "cell_type": "markdown",
+ "id": "mine_bool_01",
"metadata": {},
- "outputs": [],
"source": [
- "from qiskit import QuantumCircuit\n",
+ "### Encode the constraints\n",
"\n",
+ "Each numbered cell places a condition on the adjacent blank cells. We need to express these conditions as Boolean expressions that can be encoded into a quantum circuit.\n",
"\n",
- "def grover_oracle_hamming_weight(num_qubits, target_weight):\n",
- " \"\"\"\n",
- " Build a Grover oracle that marks all states with Hamming weight == target_weight.\n",
+ "The \"1\" cell adjacent to $x_0$ and $x_1$ says that exactly one of them contains a mine. This is precisely the exclusive-OR (XOR) operation, $\\oplus$, which returns true when exactly one of its inputs is true:\n",
+ "$$\n",
+ "(x_0 \\oplus x_1)\n",
+ "$$\n",
"\n",
- " Parameters:\n",
- " num_qubits (int): Number of qubits in the circuit.\n",
- " target_weight (int): The Hamming weight to mark.\n",
+ "Similarly, the other \"1\" cell (adjacent to $x_1$ and $x_2$) gives us:\n",
+ "$$\n",
+ "(x_1 \\oplus x_2)\n",
+ "$$\n",
"\n",
- " Returns:\n",
- " QuantumCircuit: Quantum circuit representing Grover oracle.\n",
- " \"\"\"\n",
- " qc = QuantumCircuit(num_qubits)\n",
- " marked_count = 0\n",
- " marked_list = []\n",
- " for x in range(2**num_qubits):\n",
- " bitstr = format(x, f\"0{num_qubits}b\")\n",
- " if bitstr.count(\"1\") == target_weight:\n",
- " # Count the number of target states\n",
- " marked_count = marked_count + 1\n",
- " marked_list.append(bitstr)\n",
- " # Reverse for Qiskit bit order (qubit 0 is rightmost)\n",
- " rev_target = bitstr[::-1]\n",
- " zero_inds = [i for i, b in enumerate(rev_target) if b == \"0\"]\n",
- " # Apply X gates to 'open' controls (where bit is 0)\n",
- " qc.x(zero_inds)\n",
- " # Multi-controlled Z (as MCX conjugated by H)\n",
- " if num_qubits == 1:\n",
- " qc.z(0)\n",
- " else:\n",
- " qc.h(num_qubits - 1)\n",
- " qc.mcx(list(range(num_qubits - 1)), num_qubits - 1)\n",
- " qc.h(num_qubits - 1)\n",
- " # Undo X gates\n",
- " qc.x(zero_inds)\n",
- " return qc, marked_count, marked_list"
+ "The \"2\" cell says that two of the three blank cells must contain mines. Since XOR is a parity operation, $x_0 \\oplus x_1 \\oplus x_2$ returns true when an *odd* number of the variables are true. We want an *even* number (specifically two) to be true, so we negate with $\\lnot$:\n",
+ "$$\n",
+ "\\lnot(x_0 \\oplus x_1 \\oplus x_2)\n",
+ "$$\n",
+ "On its own, this expression would be satisfied by either zero or two qubits in the $|1\\rangle$ state, since it is a statement about parity. But combined with the other two clauses, which each require at least one mine, the only satisfying assignment has exactly two mines.\n",
+ "\n",
+ "All three conditions must be simultaneously satisfied, so we join them with and symbols $\\land$:\n",
+ "$$\n",
+ "(x_0 \\oplus x_1) \\;\\land\\; (x_1 \\oplus x_2) \\;\\land\\; \\lnot(x_0 \\oplus x_1 \\oplus x_2)\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_oracle_01",
+ "metadata": {},
+ "source": [
+ "### Step 1: Map classical inputs to a quantum problem\n",
+ "\n",
+ "Now we need to encode this Boolean expression into a quantum circuit that serves as the oracle. The quantum version of XOR can be accomplished with CX (CNOT) gates: applying two CX gates from the data qubits to a workspace (ancilla) qubit effectively computes their XOR and stores the result in the ancilla.\n",
+ "\n",
+ "We introduce three workspace qubits — one for each clause. We store the result of each Boolean expression in its corresponding workspace qubit, then use a multi-controlled Z gate to flip the phase of the three-qubit state that makes all three workspace qubits $|1\\rangle$ (meaning all clauses are satisfied simultaneously).\n",
+ "\n",
+ "In the first code cell below, we build the \"compute\" half of the oracle — the part that evaluates each clause and writes the result into the workspace qubits."
]
},
{
"cell_type": "code",
- "execution_count": 20,
- "id": "5f5e4675",
+ "execution_count": null,
+ "id": "mine_code_oracle1",
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "['011', '101', '110']\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 20,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
- "# Completing step 1\n",
- "oracle, num_marked_states, marked_states = grover_oracle_hamming_weight(3, 2)\n",
- "print(marked_states)\n",
- "oracle.draw(output=\"mpl\", style=\"iqp\")"
+ "x = QuantumRegister(3, \"x\")\n",
+ "a = QuantumRegister(3, \"a\")\n",
+ "qc = QuantumCircuit(x, a)\n",
+ "\n",
+ "# Clause 1: x0 XOR x1 -> stored in a[0]\n",
+ "qc.cx(x[0], a[0])\n",
+ "qc.cx(x[1], a[0])\n",
+ "\n",
+ "# Clause 2: x1 XOR x2 -> stored in a[1]\n",
+ "qc.cx(x[1], a[1])\n",
+ "qc.cx(x[2], a[1])\n",
+ "\n",
+ "# Clause 3: NOT(x0 XOR x1 XOR x2) -> stored in a[2]\n",
+ "qc.cx(x[0], a[2])\n",
+ "qc.cx(x[1], a[2])\n",
+ "qc.cx(x[2], a[2])\n",
+ "qc.x(a[2]) # The NOT\n",
+ "\n",
+ "qc.draw(\"mpl\", style=\"iqp\")"
]
},
{
"cell_type": "markdown",
- "id": "f26a8955",
+ "id": "mine_mcz_01",
"metadata": {},
"source": [
- "From here the entire process is identical to that from the previous two activities. For brevity, the Qiskit patterns steps are shown only in code comments here."
+ "At this point, the result of each clause is stored in its corresponding workspace qubit. Now we need the three-qubit data state that makes all three workspace qubits $|1\\rangle$ to pick up a minus sign. We do this with a multi-controlled Z gate (implemented as an MCX gate sandwiched by Hadamard gates on the target).\n",
+ "\n",
+ "After applying the phase flip, we must **uncompute** — undo all the clause-evaluation steps in reverse order — to reset the workspace qubits back to $|0\\rangle.$ This is essential so that the workspace qubits are clean for subsequent iterations of the Grover operator."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "63034fc7",
+ "id": "mine_code_oracle2",
"metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "qiskit_runtime_service._resolve_cloud_instances:WARNING:2025-08-08 14:14:51,686: Default instance not set. Searching all available instances.\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "ibm_brisbane\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
- "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
+ "# Multi-controlled Z: flip phase if all workspace qubits are |1>\n",
+ "qc.h(a[2])\n",
+ "qc.mcx([a[0], a[1]], a[2])\n",
+ "qc.h(a[2])\n",
"\n",
- "# Completing step 1\n",
- "oracle, num_marked_states, marked_states = grover_oracle_hamming_weight(4, 2)\n",
- "oracle.draw(output=\"mpl\", style=\"iqp\")\n",
+ "# Uncompute clause 3: NOT(x0 XOR x1 XOR x2)\n",
+ "qc.x(a[2])\n",
+ "qc.cx(x[2], a[2])\n",
+ "qc.cx(x[1], a[2])\n",
+ "qc.cx(x[0], a[2])\n",
"\n",
- "grover_op = grover_operator(oracle)\n",
- "grover_op.decompose().draw(output=\"mpl\", style=\"iqp\")\n",
- "optimal_num_iterations = math.floor(\n",
- " math.pi / (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))\n",
- ")\n",
+ "# Uncompute clause 2: x1 XOR x2\n",
+ "qc.cx(x[2], a[1])\n",
+ "qc.cx(x[1], a[1])\n",
"\n",
- "qc = QuantumCircuit(grover_op.num_qubits)\n",
- "qc.h(range(grover_op.num_qubits))\n",
- "qc.compose(grover_op.power(optimal_num_iterations), inplace=True)\n",
- "qc.measure_all()\n",
- "qc.draw(output=\"mpl\", style=\"iqp\")\n",
+ "# Uncompute clause 1: x0 XOR x1\n",
+ "qc.cx(x[1], a[0])\n",
+ "qc.cx(x[0], a[0])\n",
"\n",
- "# Step 2: Optimize for running on real quantum computers\n",
+ "qc.draw(\"mpl\", style=\"iqp\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_groverop_01",
+ "metadata": {},
+ "source": [
+ "This circuit is our oracle: it flips the phase of the data-qubit state that satisfies all three Minesweeper constraints, and leaves the workspace qubits back in $|0\\rangle.$\n",
"\n",
+ "Now we construct the full Grover operator from this oracle. Note the `reflection_qubits` argument: we pass only the data qubits `x`, because the workspace qubits are not part of the search space. Their job is done once the oracle has been applied."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "mine_code_groverop",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "grover_op = grover_operator(qc, reflection_qubits=x)\n",
+ "grover_op.decompose(reps=0).draw(output=\"mpl\", style=\"iqp\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_circuit_01",
+ "metadata": {},
+ "source": [
+ "With three data qubits and one solution state, the optimal number of Grover iterations is $t \\approx \\frac{\\pi}{4}\\sqrt{8} - \\frac{1}{2} \\approx 1.7$, so we use two iterations. We apply Hadamard gates to the data qubits to create the initial superposition, compose the Grover operator twice, and measure only the data qubits."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "mine_code_circuit",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x = QuantumRegister(3, \"x\")\n",
+ "a = QuantumRegister(4, \"a\")\n",
+ "meas = ClassicalRegister(3, \"meas\")\n",
+ "\n",
+ "qc = QuantumCircuit(x, a, meas)\n",
+ "# Create superposition over the data qubits only\n",
+ "qc.h(x)\n",
+ "# Apply 2 iterations of the Grover operator\n",
+ "qc.compose(grover_op.power(2), inplace=True)\n",
+ "# Measure only the data qubits\n",
+ "qc.measure(x, meas)\n",
+ "qc.decompose().draw(output=\"mpl\", style=\"iqp\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_step2_01",
+ "metadata": {},
+ "source": [
+ "### Step 2: Optimize problem for quantum hardware execution\n",
+ "\n",
+ "As before, we transpile the circuit for the target backend."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "mine_code_transpile",
+ "metadata": {},
+ "outputs": [],
+ "source": [
"service = QiskitRuntimeService()\n",
"backend = service.least_busy(operational=True, simulator=False)\n",
"print(backend.name)\n",
"\n",
"target = backend.target\n",
"pm = generate_preset_pass_manager(target=target, optimization_level=3)\n",
- "circuit_isa = pm.run(qc)\n",
- "\n",
- "# Step 3: Execute using Qiskit primitives\n",
- "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 seconds of QPU time)\n",
- "\n",
- "sampler = Sampler(mode=backend)\n",
- "sampler.options.default_shots = 10_000\n",
- "result = sampler.run([circuit_isa]).result()\n",
- "dist = result[0].data.meas.get_counts()\n",
- "\n",
- "# To run on local simulator:\n",
- "# from qiskit.primitives import StatevectorSampler as Sampler\n",
- "# sampler = Sampler()\n",
- "# result = sampler.run([qc]).result()\n",
- "# dist = result[0].data.meas.get_counts()"
+ "circuit_isa = pm.run(qc)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_depth_01",
+ "metadata": {},
+ "source": [
+ "Now we can check the depth of the transpiled circuit. Because the Minesweeper oracle uses workspace qubits and multiple CX gates, the transpiled circuit will be deeper than those in the previous activities."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "0f7daf19",
+ "id": "mine_code_depth",
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "The total depth is 502\n",
- "The depth of two-qubit gates is 130\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"print(\"The total depth is \", circuit_isa.depth())\n",
"print(\n",
@@ -1237,54 +1346,71 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "mine_step3_01",
+ "metadata": {},
+ "source": [
+ "### Step 3: Execute using Qiskit primitives"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
- "id": "7c987ff1",
+ "id": "mine_code_run",
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "6"
- ]
- },
- "execution_count": 150,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
- "num_marked_states"
+ "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU time)\n",
+ "\n",
+ "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
+ "\n",
+ "sampler = Sampler(mode=backend)\n",
+ "sampler.options.default_shots = 10_000\n",
+ "result = sampler.run([circuit_isa]).result()\n",
+ "dist = result[0].data.meas.get_counts()"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "cf8b6be8",
+ "id": "mine_code_sim",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# To run on local simulator:\n",
+ "# from qiskit.primitives import StatevectorSampler as Sampler\n",
+ "# sampler = Sampler()\n",
+ "# result = sampler.run([qc]).result()\n",
+ "# dist = result[0].data.meas.get_counts()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mine_step4_01",
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 151,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "source": [
+ "### Step 4: Post-process and return result in desired classical format"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "mine_code_plot",
+ "metadata": {},
+ "outputs": [],
"source": [
"plot_distribution(dist)"
]
},
{
"cell_type": "markdown",
- "id": "268508ce",
+ "id": "mine_conclusion_01",
"metadata": {},
"source": [
- "Here Grover's algorithm did indeed prepare the states with Hamming weight 2 to be much more likely to be measured than any other states, roughly one order of magnitude more likely."
+ "The `101` state should appear with far higher probability than any other, indicating that mines are located at $x_0$ and $x_2$. We have used a quantum computer to solve a tiny game of Minesweeper!\n",
+ "\n",
+ "Of course, the best classical algorithms for minesweeper are better than a brute-force search through all possible mine configurations — they exploit the structure of the grid. Grover's algorithm would only offer an advantage on extremely difficult boards designed to be maximally ambiguous, and even then, the quadratic speedup means it cannot keep pace with exponential growth indefinitely. But the real takeaway is the technique: encoding problem constraints into a quantum oracle is a powerful pattern that extends to constraint satisfaction, combinatorial optimization, and many other domains."
]
},
{
@@ -1292,6 +1418,9 @@
"id": "494c1799",
"metadata": {},
"source": [
+ "## Questions and critical concepts:\n",
+ "\n",
+ "\n",
"### Critical concepts:\n",
"\n",
"In this module, we learned some key features of Grover's algorithm:\n",
@@ -1299,8 +1428,8 @@
"- Grover's algorithm involves repeating a series of operations (commonly called the \"Grover operator\") a number of times $t,$ chosen to make the target states optimally likely to be measured.\n",
"- Grover's algorithm can be run with fewer than $t$ iterations and still amplify the target states.\n",
"- Grover's algorithm fits into the query model of computation and makes the most sense when one person controls the search and another controls/constructs the oracle. It may also be useful as a subroutine in other quantum computations.\n",
+ "- An oracle can be built from *problem constraints* rather than from knowledge of the solution, as demonstrated with the Minesweeper example.\n",
"\n",
- "## Questions\n",
"\n",
"### T/F questions:\n",
"\n",
@@ -1342,7 +1471,7 @@
"\n",
"### Discussion questions:\n",
"\n",
- "1. What other conditions might you use in a quantum oracle? Consider conditions that are easy to state or impose on a bitstring but are not merely writing out the marked bitstrings.\n",
+ "1. What other problems could you formulate as a Grover search? Think of problems where it is difficult to find a solution but easy to verify one.\n",
"\n",
"\n",
"2. Can you see any problems with scaling Grover's algorithm on modern quantum computers?"
diff --git a/public/learning/images/modules/computer-science/grovers/extracted-outputs/5f5e4675-1.avif b/public/learning/images/modules/computer-science/grovers/extracted-outputs/5f5e4675-1.avif
deleted file mode 100644
index c9358623b68..00000000000
Binary files a/public/learning/images/modules/computer-science/grovers/extracted-outputs/5f5e4675-1.avif and /dev/null differ
diff --git a/public/learning/images/modules/computer-science/grovers/extracted-outputs/cf8b6be8-0.avif b/public/learning/images/modules/computer-science/grovers/extracted-outputs/cf8b6be8-0.avif
deleted file mode 100644
index c732bf555f4..00000000000
Binary files a/public/learning/images/modules/computer-science/grovers/extracted-outputs/cf8b6be8-0.avif and /dev/null differ
diff --git a/public/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif b/public/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif
new file mode 100644
index 00000000000..e4bea19dea0
Binary files /dev/null and b/public/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif differ
diff --git a/public/learning/images/modules/computer-science/grovers/grover-geometric-setup.avif b/public/learning/images/modules/computer-science/grovers/grover-geometric-setup.avif
new file mode 100644
index 00000000000..48d0fcb606a
Binary files /dev/null and b/public/learning/images/modules/computer-science/grovers/grover-geometric-setup.avif differ
diff --git a/public/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif b/public/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif
new file mode 100644
index 00000000000..93a8904d9da
Binary files /dev/null and b/public/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif differ
diff --git a/public/learning/images/modules/computer-science/grovers/minesweeper-grid.avif b/public/learning/images/modules/computer-science/grovers/minesweeper-grid.avif
new file mode 100644
index 00000000000..c28293b82ef
Binary files /dev/null and b/public/learning/images/modules/computer-science/grovers/minesweeper-grid.avif differ