diff --git a/task 1.ipynb b/task 1.ipynb index c889e1e..85557a5 100644 --- a/task 1.ipynb +++ b/task 1.ipynb @@ -38,33 +38,46 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T17:17:26.193607Z", + "start_time": "2025-10-09T17:17:26.178663Z" + } + }, "source": [ "def toy_hash(x):\n", " \"\"\"Hash function with unique preimages: (3*x + 5) mod 8.\"\"\"\n", " return (3 * x + 5) % 8\n", "\n" - ] + ], + "outputs": [], + "execution_count": 29 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T17:17:26.208993Z", + "start_time": "2025-10-09T17:17:26.194629Z" + } + }, "source": [ "# Setup\n", "n_qubits = 3 # 3-bit password (values 0-7)\n", "target_hash = 5 # The hash value we want to find a pre-image for\n", "\n" - ] + ], + "outputs": [], + "execution_count": 30 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T17:17:26.234388Z", + "start_time": "2025-10-09T17:17:26.227422Z" + } + }, "source": [ "def classical_preimage_search():\n", " \"\"\"Classically search for a pre-image of the target hash.\"\"\"\n", @@ -77,25 +90,43 @@ "\n", "actual_password = classical_preimage_search()\n", "\n" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h(0) = 5\n" + ] + } + ], + "execution_count": 31 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T17:17:26.272100Z", + "start_time": "2025-10-09T17:17:26.266483Z" + } + }, "source": [ "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile\n", "from qiskit.circuit.library import MCMTGate, ZGate\n", "from qiskit_aer import AerSimulator\n", "import numpy as np\n" - ] + ], + "outputs": [], + "execution_count": 32 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:06:48.610911Z", + "start_time": "2025-10-09T19:06:48.601493Z" + } + }, "source": [ "def create_oracle(n_qubits, target_hash):\n", " \"\"\"\n", @@ -115,38 +146,53 @@ "\n", " # Step 1: Compute hash(x) = (3*x + 5) mod 8\n", " # TODO: Multiply by 3. HINT: 3*x = 2*x + x, so use CNOTs for left shift plus addition\n", - "\n", - "\n", + " # We only care about the lower 3 qubits of hash\n", + " # We can throw the MSB away because of the mod 8\n", + " for i in range(n_qubits - 1):\n", + " oracle.cx(input_qubits[i],hash_qubits[i + 1])\n", + "\n", + " for i in range(n_qubits):\n", + " oracle.cx(input_qubits[i], hash_qubits[i])\n", + " \n", " # TODO: Add 5. HINT: Use X gates to add constants in binary (5 = 101 in binary)\n", - "\n", + " oracle.x(n_qubits)\n", + " oracle.x(n_qubits+2)\n", " # Step 2: Check if hash(x) == target_hash\n", " target_binary = format(target_hash, f\"0{n_qubits}b\")\n", "\n", " # Flip qubits where target bit is 0 (so we match on |111...>)\n", " for i, bit in enumerate(target_binary):\n", " # TODO: Update here\n", - " pass \n", + " if bit == '0':\n", + " oracle.x(n_qubits+i)\n", "\n", " # TODO: Mark the state with multi-controlled NOT\n", " oracle.mcx(hash_qubits, phase_qubit)\n", "\n", " # Unflip the checking bits\n", " for i, bit in enumerate(target_binary):\n", - " # TODO: Update here\n", - " pass\n", + " if bit == '0':\n", + " oracle.x(n_qubits+i)\n", "\n", " # Step 3: Uncompute hash to clean up ancilla\n", " # TODO: Reverse the hash computation steps\n", - "\n", + " \n", + " # The ancilla is unmodified in the hash calculation\n", + " \n", " return oracle\n", "\n" - ] + ], + "outputs": [], + "execution_count": 65 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:06:49.955583Z", + "start_time": "2025-10-09T19:06:49.943819Z" + } + }, "source": [ "def create_diffusion(n_qubits):\n", " \"\"\"Create diffusion operator (inversion about average)\"\"\"\n", @@ -155,43 +201,52 @@ " # TODO: Implement the diffusion operator\n", "\n", " # Step 1: Apply H to all qubits\n", - "\n", + " diffusion.h(range(n_qubits))\n", "\n", " # Step 2: Apply X to all qubits\n", - "\n", + " diffusion.x(range(n_qubits))\n", "\n", " # Step 3: Multi-controlled Z (using ancilla if needed)\n", - "\n", - "\n", - "\n", + " mcz = MCMTGate(ZGate(),n_qubits-1,1)\n", + " diffusion.append(mcz, range(n_qubits))\n", "\n", "\n", " # Step 4: Apply X to all qubits\n", - "\n", + " diffusion.x(range(n_qubits))\n", "\n", " # Step 5: Apply H to all qubits\n", - "\n", + " diffusion.h(range(n_qubits))\n", "\n", " return diffusion\n", "\n" - ] + ], + "outputs": [], + "execution_count": 66 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:06:50.296651Z", + "start_time": "2025-10-09T19:06:50.291068Z" + } + }, "source": [ "# Calculate number of Grover iterations\n", "N = 2**n_qubits\n", "optimal_iterations = int(np.pi / 4 * np.sqrt(N)) # Since there's 1 solution, otherwise use N/M, where M is the number of solutions\n" - ] + ], + "outputs": [], + "execution_count": 67 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:30:00.362049Z", + "start_time": "2025-10-09T19:30:00.342055Z" + } + }, "source": [ "# Build Grover circuit\n", "\n", @@ -202,10 +257,11 @@ "qc = QuantumCircuit(qr, cr)\n", "\n", "# TODO: Initialize input qubits to superposition\n", - "\n", + "qc.h(range(n_qubits))\n", "\n", "# TODO: Initialize phase qubit to |-⟩ for phase kickback\n", - "\n", + "qc.x(total_qubits-1)\n", + "qc.h(total_qubits-1)\n", "qc.barrier()\n", "\n", "# Grover iterations\n", @@ -213,19 +269,38 @@ "diffusion = create_diffusion(n_qubits)\n", "\n", "for _ in range(optimal_iterations):\n", - " # TODO: Apply oracle and diffusion\n", - " pass\n", + " qc.append(oracle, range(total_qubits))\n", + " qc.barrier()\n", + " qc.append(diffusion, range(n_qubits))\n", + " qc.barrier()\n", "\n", - "# TODO: Measure input qubits\n", "\n", + "# TODO: Measure input qubits\n", + "qc.measure(range(n_qubits), range(n_qubits))\n", "\n" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 79 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:30:00.963626Z", + "start_time": "2025-10-09T19:30:00.787853Z" + } + }, "source": [ "# Simulate the circuit\n", "simulator = AerSimulator()\n", @@ -250,7 +325,33 @@ "print(\n", " f\"\\nMost likely password guess: {found_password} (h({found_password})={toy_hash(found_password)})\"\n", ")\n" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 000 (password=0): 1850 shots ( 45.2%) - h(0)=5 ✓✓✓\n", + " 111 (password=7): 339 shots ( 8.3%) - h(7)=2 \n", + " 010 (password=2): 339 shots ( 8.3%) - h(2)=3 \n", + " 011 (password=3): 336 shots ( 8.2%) - h(3)=6 \n", + " 100 (password=4): 333 shots ( 8.1%) - h(4)=1 \n", + " 110 (password=6): 307 shots ( 7.5%) - h(6)=7 \n", + " 001 (password=1): 301 shots ( 7.3%) - h(1)=0 \n", + " 101 (password=5): 291 shots ( 7.1%) - h(5)=4 \n", + "\n", + "Most likely password guess: 0 (h(0)=5)\n" + ] + } + ], + "execution_count": 80 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "" } ], "metadata": { diff --git a/task 2.ipynb b/task 2.ipynb index fdf1421..e8db96d 100644 --- a/task 2.ipynb +++ b/task 2.ipynb @@ -44,31 +44,44 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T19:07:51.594405Z", + "start_time": "2025-10-09T19:07:51.588995Z" + } + }, "source": [ "def toy_hash(x):\n", - " \"\"\"Hash function with unique preimages: (3*x + 5) mod 8.\"\"\"\n", + " \"\"\"Hash function with unique preimages: (2*x + 3) mod 8.\"\"\"\n", " return (2 * x + 3) % 8" - ] + ], + "outputs": [], + "execution_count": 6 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:41.607258Z", + "start_time": "2025-10-09T21:05:41.602204Z" + } + }, "source": [ "# Setup\n", "n_qubits = 3 # 3-bit password (values 0-7)\n", "target_hash = 5 # The hash value we want to find a pre-image for" - ] + ], + "outputs": [], + "execution_count": 198 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:42.033573Z", + "start_time": "2025-10-09T21:05:42.026089Z" + } + }, "source": [ "def classical_preimage_search():\n", " \"\"\"Classically search for a pre-image of the target hash.\"\"\"\n", @@ -79,21 +92,272 @@ " return None\n", "\n", "\n", - "actual_password = classical_preimage_search()\n", - "\n" - ] + "actual_password = classical_preimage_search()\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h(0) = 3\n", + "h(1) = 5\n" + ] + } + ], + "execution_count": 199 }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:42.422084Z", + "start_time": "2025-10-09T21:05:42.412205Z" + } + }, "source": [ "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile\n", "from qiskit.circuit.library import MCMTGate, ZGate\n", "from qiskit_aer import AerSimulator\n", "import numpy as np\n" - ] + ], + "outputs": [], + "execution_count": 200 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:42.821637Z", + "start_time": "2025-10-09T21:05:42.812593Z" + } + }, + "cell_type": "code", + "source": [ + "def create_oracle(n_qubits, target_hash):\n", + " \"\"\"\n", + " Create an oracle that marks states x where hash(x) == target_hash.\n", + " This implements: hash(x) = (2*x + 3) mod 8\n", + "\n", + " Hint: You need ancilla qubits to compute the hash and check equality.\n", + " \"\"\"\n", + "\n", + " # n_qubits for input, n_qubits for hash output, 1 ancilla for phase kickback\n", + " total_qubits = n_qubits + n_qubits + 1\n", + " oracle = QuantumCircuit(total_qubits, name=\"Oracle\")\n", + "\n", + " input_qubits = list(range(n_qubits))\n", + " hash_qubits = list(range(n_qubits, n_qubits + n_qubits))\n", + " phase_qubit = 2 * n_qubits\n", + "\n", + " # Step 1: Compute hash(x) = (2*x + 3) mod 8\n", + " \n", + " # We only care about the lower 3 qubits of hash\n", + " # We can throw the MSB away because of the mod 8\n", + " for i in range(n_qubits - 1):\n", + " oracle.cx(input_qubits[i],hash_qubits[i + 1])\n", + " \n", + " # TODO: Add 3. HINT: Use X gates to add constants in binary (3 = 011 in binary)\n", + " oracle.ccx(0,n_qubits,total_qubits-1)\n", + " oracle.x(n_qubits)\n", + " oracle.cx(total_qubits-1,n_qubits)\n", + " oracle.ccx(0,n_qubits,total_qubits-1)\n", + "\n", + " oracle.ccx(0,n_qubits+1,total_qubits-1)\n", + " oracle.x(n_qubits+1)\n", + " oracle.cx(total_qubits-1,n_qubits+1)\n", + " oracle.ccx(0,n_qubits+1,total_qubits-1)\n", + " # Step 2: Check if hash(x) == target_hash\n", + " target_binary = format(target_hash, f\"0{n_qubits}b\")\n", + "\n", + " # Flip qubits where target bit is 0 (so we match on |111...>)\n", + " for i, bit in enumerate(target_binary):\n", + " # TODO: Update here\n", + " if bit == '0':\n", + " oracle.x(n_qubits+i)\n", + "\n", + " # TODO: Mark the state with multi-controlled NOT\n", + " oracle.mcx(hash_qubits, phase_qubit)\n", + "\n", + " # Unflip the checking bits\n", + " for i, bit in enumerate(target_binary):\n", + " if bit == '0':\n", + " oracle.x(n_qubits+i)\n", + "\n", + " # Step 3: Uncompute hash to clean up ancilla\n", + " # TODO: Reverse the hash computation steps\n", + " \n", + " # The ancilla is unmodified in the hash calculation\n", + " oracle.cx(total_qubits-1,n_qubits+1)\n", + " oracle.x(n_qubits+1)\n", + " \n", + " oracle.cx(total_qubits-1,n_qubits)\n", + " oracle.x(n_qubits)\n", + " oracle.cx(input_qubits[:n_qubits-1],hash_qubits[1:n_qubits])\n", + " return oracle\n", + "\n" + ], + "outputs": [], + "execution_count": 201 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:43.234991Z", + "start_time": "2025-10-09T21:05:43.227240Z" + } + }, + "cell_type": "code", + "source": [ + "def create_diffusion(n_qubits):\n", + " \"\"\"Create diffusion operator (inversion about average)\"\"\"\n", + " diffusion = QuantumCircuit(n_qubits, name=\"Diffusion\")\n", + "\n", + " # TODO: Implement the diffusion operator\n", + "\n", + " # Step 1: Apply H to all qubits\n", + " diffusion.h(range(n_qubits))\n", + "\n", + " # Step 2: Apply X to all qubits\n", + " diffusion.x(range(n_qubits))\n", + "\n", + " # Step 3: Multi-controlled Z (using ancilla if needed)\n", + " mcz = MCMTGate(ZGate(),n_qubits-1,1)\n", + " diffusion.append(mcz, range(n_qubits))\n", + "\n", + "\n", + " # Step 4: Apply X to all qubits\n", + " diffusion.x(range(n_qubits))\n", + "\n", + " # Step 5: Apply H to all qubits\n", + " diffusion.h(range(n_qubits))\n", + "\n", + " return diffusion\n", + "\n" + ], + "outputs": [], + "execution_count": 202 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:43.557947Z", + "start_time": "2025-10-09T21:05:43.546096Z" + } + }, + "cell_type": "code", + "source": [ + "# Calculate number of Grover iterations\n", + "N = 2**n_qubits\n", + "optimal_iterations = int(np.pi / 4 * np.sqrt(N)) # Since there's 1 solution, otherwise use N/M, where M is the number of solutions\n" + ], + "outputs": [], + "execution_count": 203 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:44.033873Z", + "start_time": "2025-10-09T21:05:44.017401Z" + } + }, + "cell_type": "code", + "source": [ + "# Build Grover circuit\n", + "\n", + "total_qubits = n_qubits + n_qubits + 1 # Input qubits + hash qubits + ancilla\n", + "qr = QuantumRegister(total_qubits, \"q\")\n", + "cr = ClassicalRegister(n_qubits, \"c\") # Only measure the input qubits\n", + "\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "# TODO: Initialize input qubits to superposition\n", + "qc.h(range(n_qubits))\n", + "\n", + "# TODO: Initialize phase qubit to |-⟩ for phase kickback\n", + "qc.x(total_qubits-1)\n", + "qc.h(total_qubits-1)\n", + "qc.barrier()\n", + "\n", + "# Grover iterations\n", + "oracle = create_oracle(n_qubits, target_hash)\n", + "diffusion = create_diffusion(n_qubits)\n", + "\n", + "for _ in range(optimal_iterations):\n", + " qc.append(oracle, range(total_qubits))\n", + " qc.barrier()\n", + " qc.append(diffusion, range(n_qubits))\n", + " qc.barrier()\n", + "\n", + "\n", + "# TODO: Measure input qubits\n", + "qc.measure(range(n_qubits), range(n_qubits))\n", + "\n" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 204, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 204 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-09T21:05:44.572962Z", + "start_time": "2025-10-09T21:05:44.423605Z" + } + }, + "cell_type": "code", + "source": [ + "# Simulate the circuit\n", + "simulator = AerSimulator()\n", + "job = simulator.run(transpile(qc, simulator), shots=4096)\n", + "result = job.result()\n", + "counts = result.get_counts()\n", + "\n", + "sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)\n", + "for state, count in sorted_counts:\n", + " percentage = count / 4096 * 100\n", + " password_guess = int(state, 2)\n", + " hash_value = toy_hash(password_guess)\n", + " marker = \"✓✓✓\" if password_guess == actual_password else \"\"\n", + " print(\n", + " f\" {state} (password={password_guess}): {count:4d} shots ({percentage:5.1f}%) - h({password_guess})={hash_value} {marker}\"\n", + " )\n", + "\n", + "# Find the most likely password guess\n", + "most_common = max(counts, key=counts.get)\n", + "found_password = int(most_common, 2)\n", + "\n", + "print(\n", + " f\"\\nMost likely password guess: {found_password} (h({found_password})={toy_hash(found_password)})\"\n", + ")\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 101 (password=5): 750 shots ( 18.3%) - h(5)=5 \n", + " 001 (password=1): 731 shots ( 17.8%) - h(1)=5 ✓✓✓\n", + " 111 (password=7): 499 shots ( 12.2%) - h(7)=1 \n", + " 011 (password=3): 477 shots ( 11.6%) - h(3)=1 \n", + " 010 (password=2): 422 shots ( 10.3%) - h(2)=7 \n", + " 110 (password=6): 409 shots ( 10.0%) - h(6)=7 \n", + " 100 (password=4): 406 shots ( 9.9%) - h(4)=3 \n", + " 000 (password=0): 402 shots ( 9.8%) - h(0)=3 \n", + "\n", + "Most likely password guess: 5 (h(5)=5)\n" + ] + } + ], + "execution_count": 205 } ], "metadata": {