From 08eb073f9a2816152337c54fd80385bce1fd2b38 Mon Sep 17 00:00:00 2001 From: y2sud Date: Tue, 14 Apr 2026 11:23:12 +0530 Subject: [PATCH] Add 3x3 Sudoku Grover's algorithm notebook to games/ Adds a 22-qubit Qiskit/Aer notebook that solves a 3x3 Latin-square Sudoku using Grover's algorithm. Demonstrates oracle construction via neq_pair/neq_const gadgets, phase kickback, and amplitude amplification. Also updates games/README.md with an entry for the new notebook. --- games/3x3_sudoku_grover.ipynb | 772 ++++++++++++++++++++++++++++++++++ games/README.md | 2 + 2 files changed, 774 insertions(+) create mode 100644 games/3x3_sudoku_grover.ipynb diff --git a/games/3x3_sudoku_grover.ipynb b/games/3x3_sudoku_grover.ipynb new file mode 100644 index 00000000..a409cde6 --- /dev/null +++ b/games/3x3_sudoku_grover.ipynb @@ -0,0 +1,772 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e1fa3d7f", + "metadata": {}, + "source": [ + "3×3 Latin-Square Sudoku — Grover's Algorithm (Qiskit)\n", + "======================================================\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8f433d7a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"\\nDemonstrates Grover's algorithm on a constrained combinatorial puzzle.\\nTo keep simulation feasible (≤ 32 GB RAM), we fix the top row\\nas [0, 1, 2] and let Grover search the remaining 6 unknown cells.\\n\\nGrid:\\n +---+---+---+\\n | 0 | 1 | 2 | row 0 ← FIXED (classical, no qubits needed)\\n +---+---+---+\\n | v0| v1| v2| row 1 ← quantum (2 bits each)\\n +---+---+---+\\n | v3| v4| v5| row 2 ← quantum (2 bits each)\\n +---+---+---+\\n\\nEncoding : 2 bits per cell (00=0, 01=1, 10=2, 11=invalid)\\nVariable qubits : 6 × 2 = 12\\nClause ancillae : 9\\nOutput qubit : 1\\nTotal : 22 qubits → 2^22 ≈ 64 MB ✓ feasible\\n\\nFixed row : [0, 1, 2]\\nConstraints involving unknown cells only:\\n Row 1 : v0≠v1, v0≠v2, v1≠v2\\n Row 2 : v3≠v4, v3≠v5, v4≠v5\\n Col 0 : v0≠v3, v0≠0, v3≠0 → v0≠0, v3≠0, v0≠v3\\n Col 1 : v1≠v4, v1≠1, v4≠1 → v1≠1, v4≠1, v1≠v4\\n Col 2 : v2≠v5, v2≠2, v5≠2 → v2≠2, v5≠2, v2≠v5\\n\\nValid solutions (both rows must be permutations of {0,1,2}\\n consistent with fixed row [0,1,2]):\\n row1=[1,2,0], row2=[2,0,1]\\n row1=[2,0,1], row2=[1,2,0]\\n → M = 2 valid solutions\\n\\nSearch space : 4^6 = 4096\\nOptimal iters: ≈ π/4 · √(4096/2) ≈ 35\\n\"" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"\n", + "Demonstrates Grover's algorithm on a constrained combinatorial puzzle.\n", + "To keep simulation feasible (≤ 32 GB RAM), we fix the top row\n", + "as [0, 1, 2] and let Grover search the remaining 6 unknown cells.\n", + "\n", + "Grid:\n", + " +---+---+---+\n", + " | 0 | 1 | 2 | row 0 ← FIXED (classical, no qubits needed)\n", + " +---+---+---+\n", + " | v0| v1| v2| row 1 ← quantum (2 bits each)\n", + " +---+---+---+\n", + " | v3| v4| v5| row 2 ← quantum (2 bits each)\n", + " +---+---+---+\n", + "\n", + "Encoding : 2 bits per cell (00=0, 01=1, 10=2, 11=invalid)\n", + "Variable qubits : 6 × 2 = 12\n", + "Clause ancillae : 9\n", + "Output qubit : 1\n", + "Total : 22 qubits → 2^22 ≈ 64 MB ✓ feasible\n", + "\n", + "Fixed row : [0, 1, 2]\n", + "Constraints involving unknown cells only:\n", + " Row 1 : v0≠v1, v0≠v2, v1≠v2\n", + " Row 2 : v3≠v4, v3≠v5, v4≠v5\n", + " Col 0 : v0≠v3, v0≠0, v3≠0 → v0≠0, v3≠0, v0≠v3\n", + " Col 1 : v1≠v4, v1≠1, v4≠1 → v1≠1, v4≠1, v1≠v4\n", + " Col 2 : v2≠v5, v2≠2, v5≠2 → v2≠2, v5≠2, v2≠v5\n", + "\n", + "Valid solutions (both rows must be permutations of {0,1,2}\n", + " consistent with fixed row [0,1,2]):\n", + " row1=[1,2,0], row2=[2,0,1]\n", + " row1=[2,0,1], row2=[1,2,0]\n", + " → M = 2 valid solutions\n", + "\n", + "Search space : 4^6 = 4096\n", + "Optimal iters: ≈ π/4 · √(4096/2) ≈ 35\n", + "\"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27a37f3a", + "metadata": {}, + "outputs": [], + "source": [ + "# ── Imports ────────────────────────────────────────────────────────────────────\n", + "\n", + "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile\n", + "from qiskit_aer import AerSimulator\n", + "import matplotlib\n", + "matplotlib.use('Agg')\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Patch\n", + "import math\n", + "\n", + "\n", + "# ── Constants ──────────────────────────────────────────────────\n", + "# Each unknown cell is encoded in 2 qubits: 00→0, 01→1, 10→2, 11→(invalid).\n", + "# Six unknown cells × 2 bits = 12 variable qubits total.\n", + "BITS = 2\n", + "N_UNKNOWN = 6 # v0..v5\n", + "N_VAR = N_UNKNOWN * BITS # 12\n", + "\n", + "# The top row is treated as a classical constant — no qubits are allocated for it.\n", + "FIXED_ROW = [0, 1, 2] # top row is classical\n", + "\n", + "# Cell map: v0=row1col0, v1=row1col1, v2=row1col2,\n", + "# v3=row2col0, v4=row2col1, v5=row2col2\n", + "\n", + "# ── Pair-inequality constraints (cell i ≠ cell j) ─────────────────────────────\n", + "# These generate the 9 clause ancilla qubits used in the oracle.\n", + "# Each tuple (i, j) means \"the value encoded in variable i must differ\n", + "# from the value encoded in variable j.\"\n", + "PAIR_CLAUSES = [\n", + " (0,1),(0,2),(1,2), # row 1: all three cells mutually distinct\n", + " (3,4),(3,5),(4,5), # row 2: all three cells mutually distinct\n", + " (0,3),(1,4),(2,5), # columns: col pairs (v0≠v3, v1≠v4, v2≠v5)\n", + "]\n", + "\n", + "# ── Constant-inequality constraints (cell i ≠ fixed value c) ──────────────────\n", + "# Derived from the fixed top row [0, 1, 2].\n", + "# Rather than allocating separate ancilla qubits, these are enforced in the\n", + "# oracle via X-gate pre/post-conditioning on the relevant variable qubits,\n", + "# folding the constant checks into the existing phase-kickback for free.\n", + "CONST_CLAUSES = [\n", + " (0, 0), # v0 ≠ 0 (col 0 has fixed 0)\n", + " (1, 1), # v1 ≠ 1 (col 1 has fixed 1)\n", + " (2, 2), # v2 ≠ 2 (col 2 has fixed 2)\n", + " (3, 0), # v3 ≠ 0 (col 0 has fixed 0)\n", + " (4, 1), # v4 ≠ 1 (col 1 has fixed 1)\n", + " (5, 2), # v5 ≠ 2 (col 2 has fixed 2)\n", + "]\n", + "\n", + "# ── Qubit budget summary ───────────────────────────────────────────────────────\n", + "# Pair-clause ancillae : 9 (one per PAIR_CLAUSES entry)\n", + "# Const-clause ancillae: 6 (folded into oracle via X-gates — 0 extra qubits)\n", + "# Output (phase-kickback) qubit: 1\n", + "# ─────────────────────────────────────────────────────────────\n", + "# Total: 12 + 9 + 1 = 22 qubits → 2^22 states ≈ 64 MB ✓ feasible on laptop\n", + "N_PAIR_CLAUSE = len(PAIR_CLAUSES) # 9\n", + "N_CONST_CLAUSE = len(CONST_CLAUSES) # 6 (but we fold into same ancilla bank)\n", + "N_CLAUSE = N_PAIR_CLAUSE + N_CONST_CLAUSE # 15... \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0e0628b7", + "metadata": {}, + "outputs": [], + "source": [ + "# ──────────────────────────────────────────────────────────────────────────────\n", + "# HELPER: neq_pair\n", + "# Checks whether two 2-bit quantum variables encode different values.\n", + "#\n", + "# Circuit logic (self-inverse — call twice to uncompute):\n", + "# 1. XOR each bit of b with the corresponding bit of a → b[i] ^= a[i]\n", + "# After this, b[i] == 0 iff a[i] == b[i], so b == |00⟩ iff a == b.\n", + "# 2. Flip all bits of b → b == |11⟩ iff a == b (i.e. |00⟩ → |11⟩).\n", + "# 3. Toffoli(b[0], b[1], ancilla) → ancilla flips iff b == |11⟩,\n", + "# i.e. iff a == b.\n", + "# 4. X(ancilla) → ancilla = 1 iff a ≠ b.\n", + "# 5. Unflip b, un-XOR b → restore b to its original value.\n", + "#\n", + "# Args:\n", + "# qc : QuantumCircuit to append gates to\n", + "# a_qubits : list of 2 qubits encoding value a (little-endian)\n", + "# b_qubits : list of 2 qubits encoding value b (little-endian)\n", + "# ancilla : single qubit; set to |1⟩ iff a ≠ b (must start as |0⟩)\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "def neq_pair(qc, a_qubits, b_qubits, ancilla):\n", + " for i in range(BITS):\n", + " qc.cx(a_qubits[i], b_qubits[i]) # XOR\n", + " for i in range(BITS):\n", + " qc.x(b_qubits[i]) # flip\n", + " qc.ccx(b_qubits[0], b_qubits[1], ancilla) # Toffoli\n", + " qc.x(ancilla) # ancilla = (a≠b)\n", + " for i in range(BITS):\n", + " qc.x(b_qubits[i]) # unflip\n", + " for i in range(BITS):\n", + " qc.cx(a_qubits[i], b_qubits[i]) # un-XOR\n", + "\n", + "\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "# HELPER: neq_const\n", + "# Checks whether a 2-bit quantum variable encodes a value different from a\n", + "# classical constant.\n", + "#\n", + "# Circuit logic (self-inverse — call twice to uncompute):\n", + "# 1. Identify which bits of const_val are 0.\n", + "# 2. Apply X to those qubit positions so that const_val maps to |11⟩.\n", + "# (e.g. const_val=1 → binary 01 → flip bit 1 → cell==1 becomes |11⟩)\n", + "# 3. Toffoli → ancilla = 1 iff cell == const_val (i.e. both bits are 1).\n", + "# 4. X(ancilla) → ancilla = 1 iff cell ≠ const_val.\n", + "# 5. Unflip qubits to restore cell to its original encoding.\n", + "#\n", + "# No additional ancilla qubits are needed beyond the one passed in — this is\n", + "# the \"free\" constant-check technique that avoids extra clause ancillae.\n", + "#\n", + "# Args:\n", + "# qc : QuantumCircuit to append gates to\n", + "# cell_qubits : list of 2 qubits encoding the cell value (little-endian)\n", + "# const_val : classical integer in {0, 1, 2} — the forbidden value\n", + "# ancilla : single qubit; set to |1⟩ iff cell ≠ const_val (must start |0⟩)\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "def neq_const(qc, cell_qubits, const_val, ancilla):\n", + " \"\"\"\n", + " ancilla = 1 iff cell_qubits encodes a value ≠ const_val.\n", + "\n", + " Strategy: flip bits of cell so that const_val maps to |11⟩,\n", + " then Toffoli → ancilla=1 means cell==const_val,\n", + " X → ancilla=1 means cell≠const_val, then unflip.\n", + " \"\"\"\n", + " # const_val in 2-bit binary (little-endian: bit0 first)\n", + " bits = [(const_val >> i) & 1 for i in range(BITS)]\n", + "\n", + " # Flip qubits where const bit is 0, so const_val → |11⟩\n", + " for i in range(BITS):\n", + " if bits[i] == 0:\n", + " qc.x(cell_qubits[i])\n", + "\n", + " qc.ccx(cell_qubits[0], cell_qubits[1], ancilla) # ancilla=1 iff cell==const\n", + " qc.x(ancilla) # ancilla=1 iff cell≠const\n", + "\n", + " # Unflip\n", + " for i in range(BITS):\n", + " if bits[i] == 0:\n", + " qc.x(cell_qubits[i])\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2f6ed7c7", + "metadata": {}, + "outputs": [], + "source": [ + "# ──────────────────────────────────────────────────────────────────────────────\n", + "# ORACLE: sudoku_oracle\n", + "#\n", + "# Marks valid Sudoku solutions with a phase flip via the output qubit.\n", + "# Implements the standard \"compute → phase kickback → uncompute\" pattern\n", + "# to keep all ancilla qubits clean (restored to |0⟩) after each call.\n", + "#\n", + "# Steps:\n", + "# 1. COMPUTE — evaluate all 15 constraints into clause ancillae:\n", + "# · 9 pair-clauses (cell i ≠ cell j) via neq_pair\n", + "# · 6 const-clauses (cell i ≠ fixed value) via neq_const\n", + "# Each clause ancilla is set to |1⟩ iff its constraint is met.\n", + "# 2. KICK BACK — multi-controlled X on output_qubit (initialised as |−⟩)\n", + "# flips the phase of the full register iff ALL 15 ancillae\n", + "# are |1⟩, i.e. the candidate is a valid Latin-square solution.\n", + "# 3. UNCOMPUTE — apply every clause gate in reverse order to reset all\n", + "# clause ancillae back to |0⟩ (each helper is self-inverse).\n", + "#\n", + "# Args:\n", + "# qc : QuantumCircuit to append gates to\n", + "# var_qubits : 12-qubit register encoding v0…v5 (2 qubits per cell,\n", + "# little-endian, laid out as [v0[0], v0[1], v1[0], …])\n", + "# clause_qubits: 15-qubit register for constraint ancillae\n", + "# (indices 0–8 → pair clauses, 9–14 → const clauses)\n", + "# output_qubit : single qubit initialised to |−⟩ = H|1⟩ for phase kickback\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "def sudoku_oracle(qc, var_qubits, clause_qubits, output_qubit):\n", + " def cell(i):\n", + " return var_qubits[i * BITS : i * BITS + BITS]\n", + "\n", + " # Forward: set all 15 clause ancillae\n", + " for k, (i, j) in enumerate(PAIR_CLAUSES):\n", + " neq_pair(qc, cell(i), cell(j), clause_qubits[k])\n", + "\n", + " for k, (ci, cv) in enumerate(CONST_CLAUSES):\n", + " neq_const(qc, cell(ci), cv, clause_qubits[N_PAIR_CLAUSE + k])\n", + "\n", + " # Phase kickback when ALL 15 ancillae are |1⟩\n", + " qc.mcx(list(clause_qubits), output_qubit)\n", + "\n", + " # Uncompute (reverse order)\n", + " for k, (ci, cv) in reversed(list(enumerate(CONST_CLAUSES))):\n", + " neq_const(qc, cell(ci), cv, clause_qubits[N_PAIR_CLAUSE + k])\n", + "\n", + " for k, (i, j) in reversed(list(enumerate(PAIR_CLAUSES))):\n", + " neq_pair(qc, cell(i), cell(j), clause_qubits[k])\n", + "\n", + "\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "# DIFFUSER: diffuser\n", + "#\n", + "# Implements the Grover diffuser (inversion-about-the-mean) tailored to the\n", + "# NON-UNIFORM initial superposition used here.\n", + "#\n", + "# Background:\n", + "# A standard Hadamard diffuser assumes the initial state is the uniform\n", + "# superposition |s⟩ = H^⊗n|0⟩. Here, each 2-qubit cell is prepared in a\n", + "# uniform superposition over {0,1,2} only (3 states, not 4), so the initial\n", + "# state is non-uniform in the computational basis. The diffuser must therefore\n", + "# invert about THIS specific state, not the Hadamard basis.\n", + "#\n", + "# ──────────────────────────────────────────────────────────────────────────────\n", + "def diffuser(nqubits):\n", + " \"\"\"Diffuser matching the custom cell superposition state prep.\"\"\"\n", + " qc = QuantumCircuit(nqubits)\n", + "\n", + " # Uncompute state prep\n", + " for i in range(N_UNKNOWN):\n", + " cell_q = list(range(i*BITS, i*BITS+BITS))\n", + " # Inverse of prepare_cell_superposition\n", + " qc.x(cell_q[0])\n", + " qc.ch(cell_q[0], cell_q[1])\n", + " qc.x(cell_q[0])\n", + " #theta = 2 * math.acos(1 / math.sqrt(3))\n", + " theta = 2 * math.acos(math.sqrt(2/3))\n", + " qc.ry(-theta, cell_q[0])\n", + "\n", + " # Flip all, multi-controlled Z, flip back\n", + " qc.x(range(nqubits))\n", + " qc.h(nqubits - 1)\n", + " qc.mcx(list(range(nqubits - 1)), nqubits - 1)\n", + " qc.h(nqubits - 1)\n", + " qc.x(range(nqubits))\n", + "\n", + " # Reapply state prep\n", + " for i in range(N_UNKNOWN):\n", + " cell_q = list(range(i*BITS, i*BITS+BITS))\n", + " #theta = 2 * math.acos(1 / math.sqrt(3))\n", + " theta = 2 * math.acos(math.sqrt(2/3))\n", + " qc.ry(theta, cell_q[0])\n", + " qc.x(cell_q[0])\n", + " qc.ch(cell_q[0], cell_q[1])\n", + " qc.x(cell_q[0])\n", + "\n", + " return qc\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d67838fa", + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare an equal superposition of |00⟩, |01⟩, |10⟩ (amplitide 1/√3 each).\n", + "# The invalid codeword |11⟩ is left with zero amplitude, restricting each\n", + "# cell's search space to the three valid symbols {0, 1, 2}.\n", + "def prepare_cell_superposition(qc, cell_qubits):\n", + "\n", + " # Ry(2*arccos(1/sqrt(3))) on first qubit → amplitude split 1/3 : 2/3\n", + " \n", + " theta = 2 * math.acos(math.sqrt(2/3))\n", + " qc.ry(theta, cell_qubits[0])\n", + " # Hadamard on second qubit controlled on first being |0⟩\n", + " qc.x(cell_qubits[0])\n", + " qc.ch(cell_qubits[0], cell_qubits[1])\n", + " qc.x(cell_qubits[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4a729e6d", + "metadata": {}, + "outputs": [], + "source": [ + "# ──────────────────────────────────────────────────────────────\n", + "# Build circuit\n", + "# Assemble the full Grover circuit for the Sudoku search.\n", + "\n", + "# Structure:\n", + "# 1. State prep — each cell initialised to (1/√3)(|00⟩+|01⟩+|10⟩)\n", + "# 2. Output qubit set to |−⟩ = X·H|0⟩ for phase kickback\n", + "# 3. num_iterations × (Oracle + Diffuser)\n", + "# 4. Measure all 12 variable qubits\n", + "# ────────────────────────────────────────────────────────────── \n", + "def build_circuit(num_iterations):\n", + " var_reg = QuantumRegister(N_VAR, name='v')\n", + " clause_reg = QuantumRegister(N_CLAUSE, name='c')\n", + " out_reg = QuantumRegister(1, name='out')\n", + " meas_reg = ClassicalRegister(N_VAR, name='meas')\n", + "\n", + " qc = QuantumCircuit(var_reg, clause_reg, out_reg, meas_reg)\n", + "\n", + " # Prepare each cell in superposition of {0,1,2} only — no |11⟩\n", + " for i in range(N_UNKNOWN):\n", + " prepare_cell_superposition(qc, var_reg[i*BITS : i*BITS+BITS])\n", + "\n", + " qc.x(out_reg)\n", + " qc.h(out_reg)\n", + " qc.barrier()\n", + " \n", + " for it in range(num_iterations):\n", + " sudoku_oracle(qc, var_reg, clause_reg, out_reg[0])\n", + " qc.barrier()\n", + " qc.append(diffuser(N_VAR).to_gate(label='Diffuser'), var_reg)\n", + " qc.barrier()\n", + " print(f\" Iteration {it+1}/{num_iterations} done\")\n", + "\n", + " qc.measure(var_reg, meas_reg)\n", + " return qc\n", + "\n", + "\n", + "# Convert a measured bitstring to a list of 6 cell values [v0…v5].\n", + "# Qiskit returns bitstrings in big-endian order (rightmost bit = qubit 0),\n", + "# so we reverse the full string first, then reverse each 2-bit chunk to\n", + "# recover the little-endian encoding used during circuit construction.\n", + "def decode(bitstring):\n", + " bits = bitstring[::-1] # reverse whole string\n", + " return [int(bits[i*BITS:(i+1)*BITS][::-1], 2) for i in range(N_UNKNOWN)] # reverse each chunk\n", + "\n", + "\n", + "# Return True if the bitstring encodes a valid Latin-square solution.\n", + "def verify(bitstring):\n", + " v = decode(bitstring)\n", + " if any(x > 2 for x in v): # ← add this line\n", + " return False\n", + " pairs_ok = all(v[i] != v[j] for i, j in PAIR_CLAUSES)\n", + " const_ok = all(v[ci] != cv for ci, cv in CONST_CLAUSES)\n", + " return pairs_ok and const_ok\n", + "\n", + "\n", + "# Print the full 3×3 grid with the fixed top row and decoded rows 1 & 2.\n", + "def print_grid(values, title=\"\"):\n", + " \"\"\"Print full 3×3 grid (fixed top row + decoded rows 1 & 2).\"\"\"\n", + " sep = \" +---+---+---+\"\n", + " if title:\n", + " print(f\"\\n {title}\")\n", + " print(sep)\n", + " # Fixed row 0\n", + " print(\" | \" + \" | \".join(str(x) for x in FIXED_ROW) + \" | ← fixed\")\n", + " print(sep)\n", + " # Row 1: v0, v1, v2\n", + " print(\" | \" + \" | \".join(str(values[i]) for i in range(3)) + \" |\")\n", + " print(sep)\n", + " # Row 2: v3, v4, v5\n", + " print(\" | \" + \" | \".join(str(values[i]) for i in range(3, 6)) + \" |\")\n", + " print(sep)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0282c88a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "============================================================\n", + " 3×3 Latin-Square Sudoku — Grover's Algorithm\n", + " (Top row fixed as [0,1,2] for feasible simulation)\n", + "============================================================\n", + " Encoding : 2 bits/cell, values {0,1,2}\n", + " Unknown cells : 6 (rows 1 & 2)\n", + " Variable qubits : 12\n", + " Clause ancillae : 15\n", + " Total qubits : 28 (2^28 = 268,435,456 amplitudes ✓)\n", + " Search space : 4^6 = 729\n", + " Valid solutions : 2\n", + " Optimal iters : 15\n", + " Shots : 2048\n", + "\n", + " Building circuit...\n", + " Iteration 1/15 done\n", + " Iteration 2/15 done\n", + " Iteration 3/15 done\n", + " Iteration 4/15 done\n", + " Iteration 5/15 done\n", + " Iteration 6/15 done\n", + " Iteration 7/15 done\n", + " Iteration 8/15 done\n", + " Iteration 9/15 done\n", + " Iteration 10/15 done\n", + " Iteration 11/15 done\n", + " Iteration 12/15 done\n", + " Iteration 13/15 done\n", + " Iteration 14/15 done\n", + " Iteration 15/15 done\n", + " Total qubits : 28\n", + " Circuit depth : 42263\n", + " Gate count : 50610\n", + "\n", + " Simulating (2048 shots)...\n" + ] + } + ], + "source": [ + "# ──────────────────────────────────────────────────────────────\n", + "# Main\n", + "# ──────────────────────────────────────────────────────────────\n", + "\n", + "# ── Grover parameters ──────────────────────────────────────────────────────────\n", + "# Optimal iteration count: k ≈ (π/4)·√(N/M)\n", + "# N = 3^6 = 729 (valid codewords per cell × 6 cells)\n", + "# M = 2 solutions: row1=[1,2,0],row2=[2,0,1] and row1=[2,0,1],row2=[1,2,0]\n", + "N_STATES = 3 ** N_UNKNOWN # 729\n", + "M_SOLS = 2 # [1,2,0,2,0,1] and [2,0,1,1,2,0]\n", + "OPT_ITERS = max(1, round((math.pi / 4) * math.sqrt(N_STATES / M_SOLS)))\n", + "NUM_ITERATIONS = OPT_ITERS \n", + "SHOTS = 2048\n", + "\n", + "# ── Summary banner ─────────────────────────────────────────────────────────────\n", + "print(\"=\" * 60)\n", + "print(\" 3×3 Latin-Square Sudoku — Grover's Algorithm\")\n", + "print(\" (Top row fixed as [0,1,2] for feasible simulation)\")\n", + "print(\"=\" * 60)\n", + "print(f\" Encoding : 2 bits/cell, values {{0,1,2}}\")\n", + "print(f\" Unknown cells : {N_UNKNOWN} (rows 1 & 2)\")\n", + "print(f\" Variable qubits : {N_VAR}\")\n", + "print(f\" Clause ancillae : {N_CLAUSE}\")\n", + "print(f\" Total qubits : {N_VAR + N_CLAUSE + 1} \"\n", + " f\"(2^{N_VAR+N_CLAUSE+1} = {2**(N_VAR+N_CLAUSE+1):,} amplitudes ✓)\")\n", + "print(f\" Search space : 4^{N_UNKNOWN} = {N_STATES:,}\")\n", + "print(f\" Valid solutions : {M_SOLS}\")\n", + "print(f\" Optimal iters : {OPT_ITERS}\")\n", + "print(f\" Shots : {SHOTS}\")\n", + "\n", + "# ── Build ──────────────────────────────────────────────────────────────────────\n", + "print(\"\\n Building circuit...\")\n", + "qc = build_circuit(num_iterations=NUM_ITERATIONS)\n", + "\n", + "# Transpile to a small universal gate set for realistic depth/gate estimates.\n", + "# optimization_level=0 avoids gate cancellations that would obscure the raw structure.\n", + "qc_t = transpile(qc,\n", + " basis_gates=['cx', 'u', 'x', 'h', 'ccx'],\n", + " optimization_level=0)\n", + "print(f\" Total qubits : {qc.num_qubits}\")\n", + "print(f\" Circuit depth : {qc_t.depth()}\")\n", + "print(f\" Gate count : {sum(qc_t.count_ops().values())}\")\n", + "\n", + "# ── Simulate ───────────────────────────────────────────────────────────────────\n", + "# Statevector simulation is exact (no shot noise beyond sampling).\n", + "# Results are sorted descending so the two solution bitstrings appear first.\n", + "print(f\"\\n Simulating ({SHOTS} shots)...\")\n", + "sim = AerSimulator(method='statevector')\n", + "result = sim.run(qc_t, shots=SHOTS).result()\n", + "counts = result.get_counts()\n", + "\n", + "sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "07f7e8ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "────────────────────────────────────────────────────────────\n", + " Top 10 Results (✓ = valid solution)\n", + "────────────────────────────────────────────────────────────\n", + "\n", + " Rank 1 | Count: 1073/2048 | ✓ VALID\n", + " +---+---+---+\n", + " | 0 | 1 | 2 | ← fixed\n", + " +---+---+---+\n", + " | 2 | 0 | 1 |\n", + " +---+---+---+\n", + " | 1 | 2 | 0 |\n", + " +---+---+---+\n", + "\n", + " Rank 2 | Count: 975/2048 | ✓ VALID\n", + " +---+---+---+\n", + " | 0 | 1 | 2 | ← fixed\n", + " +---+---+---+\n", + " | 1 | 2 | 0 |\n", + " +---+---+---+\n", + " | 2 | 0 | 1 |\n", + " +---+---+---+\n", + "\n", + "────────────────────────────────────────────────────────────\n", + " Summary\n", + "────────────────────────────────────────────────────────────\n", + " Unique outcomes : 2\n", + " Unique valid grids: 2 (expect 2)\n", + " Shots on valid : 2048/2048 (100.0%)\n", + " Shots on invalid : 0/2048 (0.0%)\n", + "\n", + "────────────────────────────────────────────────────────────\n", + " Valid Sudoku Grids Found\n", + "────────────────────────────────────────────────────────────\n", + "\n", + " shots = 975\n", + " +---+---+---+\n", + " | 0 | 1 | 2 | ← fixed\n", + " +---+---+---+\n", + " | 1 | 2 | 0 |\n", + " +---+---+---+\n", + " | 2 | 0 | 1 |\n", + " +---+---+---+\n", + "\n", + " shots = 1073\n", + " +---+---+---+\n", + " | 0 | 1 | 2 | ← fixed\n", + " +---+---+---+\n", + " | 2 | 0 | 1 |\n", + " +---+---+---+\n", + " | 1 | 2 | 0 |\n", + " +---+---+---+\n" + ] + } + ], + "source": [ + "# ── Top-10 results ─────────────────────────────────────────────────────────────\n", + "# Show the highest-count bitstrings with their decoded grids.\n", + "# A valid solution must pass all pair and constant constraints (see verify()).\n", + "print(f\"\\n{'─'*60}\")\n", + "print(\" Top 10 Results (✓ = valid solution)\")\n", + "print(f\"{'─'*60}\")\n", + "for rank, (bitstr, cnt) in enumerate(sorted_counts[:10], 1):\n", + " v = decode(bitstr)\n", + " valid = verify(bitstr)\n", + " tag = \"✓ VALID\" if valid else \"✗ invalid\"\n", + " print(f\"\\n Rank {rank:>2} | Count: {cnt:>4}/{SHOTS} | {tag}\")\n", + " print_grid(v)\n", + "\n", + "# ── Aggregate statistics ────────────────────────────────────────────────────────\n", + "# valid_shots — total shots that collapsed onto a valid solution bitstring\n", + "# unique_valid — distinct valid bitstrings (expect exactly 2 for this puzzle)\n", + "valid_shots = sum(c for b, c in counts.items() if verify(b))\n", + "unique_valid = [b for b in counts if verify(b)]\n", + "\n", + "print(f\"\\n{'─'*60}\")\n", + "print(\" Summary\")\n", + "print(f\"{'─'*60}\")\n", + "print(f\" Unique outcomes : {len(counts)}\")\n", + "print(f\" Unique valid grids: {len(unique_valid)} (expect 2)\")\n", + "print(f\" Shots on valid : {valid_shots}/{SHOTS} \"\n", + " f\"({100 * valid_shots / SHOTS:.1f}%)\")\n", + "print(f\" Shots on invalid : {SHOTS-valid_shots}/{SHOTS} \"\n", + " f\"({100*(SHOTS-valid_shots)/SHOTS:.1f}%)\")\n", + "\n", + "# ── Valid grids ─────────────────────────────────────────────────────────────────\n", + "# Print each discovered solution as a full 3×3 grid with its shot count.\n", + "# High shot concentration on exactly 2 grids confirms Grover amplification worked.\n", + "print(f\"\\n{'─'*60}\")\n", + "print(\" Valid Sudoku Grids Found\")\n", + "print(f\"{'─'*60}\")\n", + "for b in unique_valid:\n", + " print_grid(decode(b), title=f\"shots = {counts[b]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "7ee8580d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " Histogram saved → grover_3x3_sudoku_final.png\n", + "\n", + " Done.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\Administrator\\AppData\\Local\\Temp\\ipykernel_46188\\64342275.py:38: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown\n", + " plt.show()\n" + ] + } + ], + "source": [ + "# ── Histogram of top-20 outcomes ───────────────────────────────────────────────\n", + "# Each bar represents one measured bitstring, labelled as its full 3×3 grid.\n", + "# Green = valid Latin-square solution; blue = invalid (noise / non-solution states).\n", + "# Grover amplification should concentrate shots onto the 2 green bars.\n", + "top20 = dict(sorted_counts[:20])\n", + "bar_colors = ['#2ecc71' if verify(b) else '#3498db' for b in top20]\n", + "\n", + "def bar_label(b):\n", + " v = decode(b)\n", + " return (f\"{FIXED_ROW[0]} {FIXED_ROW[1]} {FIXED_ROW[2]}\\n\"\n", + " f\"{v[0]} {v[1]} {v[2]}\\n\"\n", + " f\"{v[3]} {v[4]} {v[5]}\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(16, 6))\n", + "ax.bar(range(len(top20)), list(top20.values()), color=bar_colors)\n", + "ax.set_xticks(range(len(top20)))\n", + "ax.set_xticklabels([bar_label(b) for b in top20],\n", + " fontsize=7.5, family='monospace')\n", + "ax.set_ylabel(\"Shot count\")\n", + "ax.set_xlabel(\"Full 3×3 grid (top row fixed)\")\n", + "ax.set_title(\n", + " f\"3×3 Latin-Square Sudoku — Grover's Algorithm\\n\"\n", + " f\"Top row fixed=[0,1,2] | 2 bits/cell | 22 qubits | \"\n", + " f\"{NUM_ITERATIONS} iters | {SHOTS} shots\"\n", + ")\n", + "ax.legend(handles=[\n", + " Patch(color='#2ecc71', label='Valid Latin square ✓'),\n", + " Patch(color='#3498db', label='Invalid'),\n", + "])\n", + "plt.tight_layout()\n", + "\n", + "# Display inline (notebook) and save to disk for the GitHub render.\n", + "from IPython.display import display\n", + "display(fig)\n", + "#plt.close()\n", + "out_img = \"grover_3x3_sudoku_final.png\"\n", + "plt.savefig(out_img, dpi=150)\n", + "plt.show()\n", + "print(f\"\\n Histogram saved → {out_img}\")\n", + "print(\"\\n Done.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9d040c60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15\n", + "9\n", + "6\n", + "15\n" + ] + } + ], + "source": [ + "print(N_CLAUSE) # should print 15\n", + "print(N_PAIR_CLAUSE) # should print 9\n", + "print(N_CONST_CLAUSE) # should print 6\n", + "print(OPT_ITERS)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0740fd9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv-sudoku", + "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.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/games/README.md b/games/README.md index 8740a017..d51b408f 100644 --- a/games/README.md +++ b/games/README.md @@ -17,6 +17,8 @@ This is exactly what we'd like to replicate in this folder. Here you'll find bas * [Quantum Awesomeness](quantum_awesomeness.ipynb) - Puzzles that aim to give hands-on experience of a quantum device's most important features: number of qubits, connectivity and noise. +* [3x3 Sudoku — Grover's Algorithm](3x3_sudoku_grover.ipynb) - Solves a 3×3 Latin-square Sudoku using Grover's algorithm on a 22-qubit Qiskit Aer circuit, demonstrating oracle construction, phase kickback, and amplitude amplification. + You can also find the following game related content in the 'creative' folder. * [Random Terrain Generation](../creative/random_terrain_generation.ipynb) - A simple example of using quantum computers for the kind of procedural generation often used in games.