From dbd4547763cfd11bb1cef2cd9c6b0abf2a3e45f8 Mon Sep 17 00:00:00 2001 From: "Roy F. Cruz Candelaria" <55238184+roy-cruz@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:49:36 -0500 Subject: [PATCH] Optimized implementation (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5) * Re-implementation of Qubit classes. Now represented as wavevector of system. * Added some docstrings * Bit of refactoring and doc; began showcase nb * Qubit tests made compatible with new implementation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Began making Agent tets compatible with new implem * Added numpy dependency to toml, finished basic tests for neq implem * Testing fixed, testing in more Python versions * Additional type hinting, documentation, fixed to gates.py, cleaning code * Grammar fixes, added small type hinting * Used List from typing to avoid pytest complaint for python3.8 * Higher verbosity for coverage CI --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Guillermo A. Fidalgo-Rodríguez --- .github/workflows/ci.yml | 27 +- notebooks/BB84.ipynb | 1096 +++---------------------------- notebooks/qCryptoShowcase.ipynb | 132 ++++ pyproject.toml | 11 +- src/qcrypto/gates.py | 42 ++ src/qcrypto/simbasics.py | 524 ++++++++++++--- tests/test_simbasics.py | 218 +++--- 7 files changed, 866 insertions(+), 1184 deletions(-) create mode 100644 notebooks/qCryptoShowcase.ipynb create mode 100644 src/qcrypto/gates.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb31419..795b9d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,28 +3,35 @@ name: Auto Testing on: pull_request: push: - branches: - - master jobs: tests: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - - name: Set up Python 3.11 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} - name: Install dependencies run: - python -m pip install --upgrade pip - pip install numpy coverage pytest dataclasses iniconfig packaging pluggy - pip install ./ + pip install .[tests] - name: Run Unit Tests with Coverage - run: - coverage run -m unittest discover - coverage report + run: | + coverage run -m pytest -vv + coverage report -m + coverage html + + - name: Upload Coverage HTML report to Artifacts + uses: actions/upload-artifact@v2 + if: always() + with: + name: coverage-report + path: htmlcov/ diff --git a/notebooks/BB84.ipynb b/notebooks/BB84.ipynb index 3bb8085..cce3ddb 100644 --- a/notebooks/BB84.ipynb +++ b/notebooks/BB84.ipynb @@ -1,15 +1,5 @@ { "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] - }, { "cell_type": "markdown", "metadata": { @@ -22,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -34,148 +24,148 @@ "import os\n", "import dataclasses\n", "import sys\n", - "from concurrent.futures import ThreadPoolExecutor\n", - "from qcrypto.simbasics import Qubit, Agent" + "from copy import copy\n", + "from qcrypto.simbasics import *\n", + "import numba" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mDocstring:\u001b[0m\n", + "getattr(object, name[, default]) -> value\n", + "\n", + "Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.\n", + "When a default argument is given, it is returned when the attribute doesn't\n", + "exist; without it, an exception is raised in that case.\n", + "\u001b[0;31mType:\u001b[0m builtin_function_or_method" + ] + } + ], + "source": [ + "getattr?" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "def BB84(alice: Agent, bob: Agent, numcheckbits=10, eve=None):\n", - " \"\"\"\n", - " Simulation of BB84 protocol using the qcrypto library\n", - " \"\"\"\n", - " # Initialization.\n", - " numqubits = len(alice.qubits)\n", + "def BB84(numqubits, numcheckbits, eve=False):\n", + " # Alice initializations\n", + " Alice = Agent(priv_qbittype=\"unentangled\", num_priv_qubits=numqubits)\n", + " Alice_base_choice = np.random.choice([1, 0], size=(numqubits))\n", + " Alice_key = []\n", "\n", - " # Alice sends qubits to Bob\n", - " if eve is None:\n", - " alice.send_quantum(bob, np.random.choice([0, np.pi/4], numqubits))\n", - " else:\n", - " alice.send_quantum(eve, np.random.choice([0, np.pi/4], numqubits))\n", - " eve.send_quantum(bob, np.random.choice([0, np.pi/4], numqubits))\n", - "\n", - " # Getting bases for Bob and Alice\n", - " alice_bases = np.array([qubit.base for qubit in alice.qubits])\n", - " bob_bases = np.array([qubit.base for qubit in bob.qubits])\n", - "\n", - " bases_mask = (alice_bases == bob_bases)\n", - " alice.qubits = np.array(alice.qubits)[bases_mask]\n", - " bob.qubits = np.array(bob.qubits)[bases_mask]\n", - "\n", - " alice_bases = alice_bases[bases_mask]\n", - " bob_bases = bob_bases[bases_mask]\n", + " for qubit_idx in range(numqubits):\n", + " if Alice_base_choice[qubit_idx] == 1: # If 1, apply H gate and measure\n", + " Alice.priv_qstates.apply_gate(H_gate, qubit_idx=qubit_idx)\n", + " Alice_key.append(Alice.measure(\"private\", qubit_idx=qubit_idx))\n", + " Alice.priv_qstates.apply_gate(H_gate, qubit_idx=qubit_idx) # Re-apply gate to return to comp basis\n", + " else: # If 0 just measure\n", + " Alice_key.append(Alice.measure(\"private\", qubit_idx=qubit_idx)) \n", + " Alice_key = np.array(Alice_key)\n", + " \n", + " Bob = Agent(priv_qstates=copy(Alice.priv_qstates))\n", + " Bob_base_choice = np.random.choice([1, 0], size=(numqubits))\n", + " Bob_key = []\n", + "\n", + " for qubit_idx in range(numqubits):\n", + " if Bob_base_choice[qubit_idx] == 1:\n", + " Bob.priv_qstates.apply_gate(H_gate, qubit_idx=qubit_idx)\n", + " Bob_key.append(Bob.measure(\"private\", qubit_idx=qubit_idx))\n", + " Bob.priv_qstates.apply_gate(H_gate, qubit_idx=qubit_idx)\n", + " else:\n", + " Bob_key.append(Bob.measure(\"private\", qubit_idx=qubit_idx))\n", + " Bob_key = np.array(Bob_key)\n", "\n", - " alice_key = alice.get_key(alice_bases)\n", - " bob_key = bob.get_key(bob_bases)\n", + " Alice_Bob_base_check = Alice_base_choice == Bob_base_choice\n", + " Alice_key = Alice_key[Alice_Bob_base_check]\n", + " Bob_key = Bob_key[Alice_Bob_base_check]\n", "\n", - " alice_check_bits = np.array(alice_key[:numcheckbits])\n", - " bob_check_bits = np.array(bob_key[:numcheckbits])\n", + " Alice_check = Alice_key[:numcheckbits]\n", + " Bob_check = Bob_key[:numcheckbits]\n", "\n", - " alice_key = alice_key[numcheckbits:]\n", - " bob_key = bob_key[numcheckbits:]\n", + " discovered = not (Alice_check == Bob_check).all()\n", "\n", - " comparison = alice_check_bits != bob_check_bits\n", - " comp_result = comparison.sum()\n", - " intruder_detected = comp_result != 0\n", + " Alice_key = Alice_key[numcheckbits:]\n", + " Bob_key = Bob_key[numcheckbits:]\n", "\n", - " return intruder_detected, (alice_key, bob_key)" + " return discovered, (Alice_key, Bob_key)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n", + "[0 1 1 ... 0 1 0]\n", + "[0 1 1 ... 0 1 0]\n" + ] + } + ], "source": [ - "numqubits = 100\n", - "numcheckbits = 0\n", - "intruder = True\n", - "alice = Agent(numqubits=numqubits)\n", - "bob = Agent(numqubits=0)\n", - "if intruder:\n", - " eve = Agent(numqubits=0)\n", - "else:\n", - " eve = None\n", - "\n", - "detected, keys = BB84(alice, bob, numcheckbits=3, eve=eve)\n", - "print(\n", - " \"\"\"\n", - " Alice's key: \n", - " {}\n", - " Bob's key: \n", - " {}\n", - " Intruder detected?: {}\n", - " \"\"\".format(keys[0], keys[1], detected)\n", - ")" + "discovered, (Alice_key, Bob_key) = BB84(100_000, 200, eve=False)\n", + "print(discovered)\n", + "print(Alice_key)\n", + "print(Bob_key)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "numqubits = 10\n", + "numqubits = 50\n", "nchecks_lst = np.arange(numqubits//2)\n", "probs = [] \n", - "numtrials = 10\n", + "numtrials = 1000\n", "\n", "def run_trial(nchecks, eve_present=True):\n", " if eve_present:\n", - " detected, _ = BB84(Agent(numqubits=numqubits), Agent(numqubits=0), numcheckbits=nchecks, eve=Agent(numqubits=0))\n", + " detected, _ = BB84(numqubits, nchecks, eve=True)\n", " else:\n", - " detected, _ = BB84(Agent(numqubits=numqubits), Agent(numqubits=0), numcheckbits=nchecks)\n", + " detected, _ = BB84(numqubits, nchecks, eve=False)\n", " return detected\n", "\n", - "with ThreadPoolExecutor() as executor:\n", - " for nchecks in nchecks_lst:\n", - " futures = [executor.submit(run_trial, nchecks) for _ in range(numtrials)]\n", - " results = [f.result() for f in futures]\n", - " prob = sum(results) / numtrials\n", - " probs.append(prob)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.scatter(nchecks_lst, probs, s=0.01)\n", - "plt.xlabel(r\"Number of checkbits ($n_{\\mathrm{check}}$)\")\n", - "plt.ylabel(r\"$P_{\\mathrm{detection}}(n_{\\mathrm{check}}\\ |\\ \\mathrm{Eve})$\")\n", - "plt.title(r\"Probability of Eve being detected\")\n", - "plt.errorbar(y=probs, x=nchecks_lst, yerr=[1/np.sqrt(numtrials)]*len(probs), fmt=\".\", capsize=3)\n", - "plt.grid()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# As a sanity check, we run the same simulation, but without Eve\n", - "probs = []\n", - "\n", - "with ThreadPoolExecutor() as executor:\n", - " for nchecks in nchecks_lst:\n", - " futures = [executor.submit(run_trial, nchecks, eve_present=False) for _ in range(numtrials)]\n", - " results = [f.result() for f in futures]\n", - " prob = sum(results) / numtrials\n", - " probs.append(prob)" + "for nchecks in nchecks_lst:\n", + " temp = 0\n", + " for _ in range(numtrials):\n", + " detected = run_trial(nchecks, numqubits)\n", + " if detected: temp += 1\n", + " prob = temp / numtrials\n", + " probs.append(prob) " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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f39cvnwZzs7OAIDExEQMHDiwvOWJiIiIZCfbSdB//fUX7O3tpcd2dnb466+/5CpPREREJBvZToLu27cv2rVrh/feew8qlQq7du1Cv3795CpPREREJJsyBaCbN2/iwoULcHJykr4Ade7cuXjnnXdw7NgxAMDXX38Nb29v+TolIiIikkmpA9CPP/6IIUOGICcnByqVCl5eXvj111/h4OAAHx8f+Pj4VESfRERERLIp9TlA8+bNQ//+/XHlyhUcOHAAADBjxgzZGyMiIiKqKKXeA3Tjxg3s27cPbm5uaNCgAX744Qd4e3tj7dq1FdEfERERkexKvQcoNzcX5ubm0uNGjRpBq9UiMTFR1saIiIiIKkqZLoP/7rvvcPz4cTx+/BgAYGxsjIyMDFkbIyIiIqoopT4E9tprr2HhwoVIT09HtWrV4O7ujszMTKxduxb+/v7w8fGBhYVFRfRKREREJItSB6BDhw4BAK5du4YzZ87g7NmzOHv2LFauXImwsDBUq1YN9evXx+XLl2VvloiIiEgOZb4RYv369VG/fn307dtXGouPj8fp06dx7tw5WZojIiIiqgiy3Qka+Oeb4N3d3dG7d285yxIRERHJSrbvAiMiIiJ6UTAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiVMkAtGLFCri5ucHU1BStW7fGyZMnC112zZo1eO2112BjYwMbGxv4+/sXuTwRERFRlQtA4eHhCAkJQWhoKM6ePQtPT08EBAQgOTm5wOWjo6PRr18/REVFISYmBrVr10aXLl1w586dSu6ciIiIXhTGhm7geUuXLsWIESMwdOhQAMCqVauwZ88erFu3DjNmzNBbftOmTTqPv/32W+zYsQORkZEYPHiw3vJZWVnIysqSHqelpQEAcnJykJOTI+dbkerJXZeKxnk3DM67YXDeDYPzbhjPznt5514lhBByNCWH7OxsmJubY/v27ejZs6c0HhQUhJSUFOzevbvYGunp6XB0dMS2bdvwzjvv6D0/d+5czJs3T2988+bNMDc3L1f/REREVDkyMjLQv39/pKamwtLSstSvr1J7gO7fv4+8vDw4OTnpjDs5OeHKlSslqjF9+nTUrFkT/v7+BT4/c+ZMhISESI/T0tKkw2ZlmcCi5OTkICIiAp07d4ZarZa1NhWO824YnHfD4LwbBufdMJ6d96dPn5arVpUKQOW1ePFibNmyBdHR0TA1NS1wGY1GA41GozeuVqsrbCOuyNpUOM67YXDeDYPzbhicd8NQq9XIzc0tV40qFYDs7e1hZGSEpKQknfGkpCQ4OzsX+drPP/8cixcvxsGDB9GiRYuKbJOIiIhecFXqKjATExN4e3sjMjJSGtNqtYiMjISfn1+hr1uyZAkWLFiAffv2wcfHpzJaJSIiohdYldoDBAAhISEICgqCj48PfH19sWzZMjx58kS6Kmzw4MGoVasWwsLCAACffvop5syZg82bN8PNzQ2JiYkAgBo1aqBGjRoGex9ERERUdVW5ABQYGIh79+5hzpw5SExMRMuWLbFv3z7pxOhbt26hWrX/7bhauXIlsrOz0atXL506oaGhmDt3bmW2TkRERC+IKheAACA4OBjBwcEFPhcdHa3zOCEhoeIbIiIiopdKlToHiIiIiKgyMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIwABEREZHiMAARERGR4jAAERERkeIYG7qBl01yWiaS07MAALm5ubj9GPjj7zQYG/8z1Y4WGjhampa6VkFKWkuuOi9KLc67YWqVZ95fhPdXVWtx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IiJK/J5Kso58mzdvRr9+/bBp0yYMGDCgxPP6rOXLl8PPzw8jRozA3bt3cffuXajVavTr1w8ffvghLl++jOjoaLz//vs6Iep5R44cgbe3t85YbGwsVCoVli5ditmzZyM2NhZ16tTBjBkzCq1TWXx9fXHy5ElkZWUZuhUiXYKIKlRQUJDo0aOHEEKINm3aiA8//FAIIcTOnTvFs7+Crq6u4ssvv9R5raenpwgNDZUed+jQQbRv3156nJubK6pXry4GDRokjd29e1cAEDExMdJrGjduLLRarbTM9OnTRePGjYUQQmRmZgpzc3Nx/PhxnXUPGzZM9OvXT2fdXl5eRb7Xx48fC7VaLTZt2iSNZWdni5o1a4olS5YIIYT48ssvhaura6E10tLShEajEWvWrCl0mefnQQghWrVqJaZPn16i91TSdUycOFF8/fXXwsrKSkRHR+s9X9S8PlujsMdnzpwRAERCQkKhfTyvR48e0jaUb86cOcLGxkYkJydLY//+979F06ZNC60THx8vvL29S7zestaJjY0t9Xskqgwv78F3oiro008/xZtvvokpU6aUuUaLFi2k/29kZAQ7Ozs0b95cGsvfi5GcnCyNtWnTRueEaz8/P3zxxRfIy8vD9evXkZGRgc6dO+usJzs7G15eXjpjz+95eF5cXBxycnLQrl07aUytVsPX1xeXL18u0fu7fPkysrKy0KlTpyKXe3YeAMDFxUV6z8W9p5KuY/v27UhOTsaxY8fQqlUrveeLmlcjI6MiawOAp6cnOnXqhObNmyMgIABdunRBr169YGNjU+hrnj59ClNTU52x2NhY9OjRAw4ODtJYfHw86tWrV2wPFc3MzAwAdPZIElUFPARGVIlef/11BAQEYObMmXrPVatWTe/QR05Ojt5yarVa57FKpdIZy/8HWavVlqinx48fA/jnHJfz589LP5cuXdI7D6h69eolqlke+f9gFqegech/z8W9p5Kuw8vLCw4ODli3bl2Rh6XKysjICBEREfj111/RpEkTfPXVV2jYsCHi4+MLfY29vb3eyfSxsbHw8/PTGTt//jxatmwpPV63bh1atGgBT09PKYDn5OQgKCgIjRs3RmBgoPQeN27ciFatWsHT0xMhISFF1njWxYsX4ePjg7i4OGns4cOHAKATzoiqAgYgokq2ePFi/Pzzz4iJidEZd3BwwN27d6XHaWlpRf5DWBrPnwz7+++/o379+jAyMkKTJk2g0Whw69Yt1KtXT+endu3apVqPh4eHdI5QvpycHJw6dQpNmjQpUY369evDzMwMkZGRpVr3s4p7TyVdh4eHB6KiorB7926MHz9e7/mi5rUgJiYmyMvL0xlTqVRo164d5s2bh3PnzsHExAQ7d+4stCcvLy9cunRJepyamoqEhAS9vXXPBqD//ve/+PLLL3H48GHExsbio48+AvDP3rbp06fj0qVLSEpKwtGjR3H58mXs3r0bMTExiI2Nxf3797Fnz55Ca+S7cOECgoKCEB4eDg8PD2n84sWLeOWVV2Bvb1/oeyIyBB4CI6pkzZs3x4ABA/Dvf/9bZ/zNN9/Ehg0b0L17d1hbW2POnDklOoxSErdu3UJISAhGjRqFs2fP4quvvpJuUGdhYYEpU6Zg8uTJ0Gq1aN++PVJTU3Hs2DFYWloiKCioxOupXr06xowZg6lTp8LW1hZ16tTBkiVLkJGRUeLLtk1NTTF9+nRMmzYNJiYmaNeuHe7du4c//vijxDVK8p5Kuo4GDRogKioKHTt2hLGxsc6VekXNa0Hc3Nxw4sQJJCQkoEaNGoiLi0NkZCS6dOkCR0dHnDhxAvfu3UPjxo0LrZG/B/HRo0ewsbHBhQsXYGxsrHMY9ObNm3j06JEUgKKiohAYGAhra2sA/1yen5aWhoYNG0rB1MvLCwkJCYiNjcXvv/8OHx8fAP8cuvL29kZcXJxejXx///03+vbti19++QV169bV6ffIkSPo0qVLoe+HyFAYgIgMYP78+QgPD9cZmzlzJuLj4/HOO+/AysoKCxYskG0P0ODBg/H06VP4+vrCyMgIEydOxMiRI6XnFyxYAAcHB4SFheHGjRuwtrbGq6++qvdXfkksXrwYWq0WgwYNQnp6Onx8fLB///4iz2t53uzZs2FsbIw5c+bg77//houLC0aPHl2qPop7T6VZR8OGDfHbb7+hY8eOMDIykkJOcfP6vClTpiAoKAhNmjTB06dPcenSJRw+fBjLli1DWloaXF1d8cUXX+Ctt94qtEbz5s3x6quvYuvWrRg1ahRiY2PRsGFDnfOCzp07B2tr62JvlKjRaKT/b2RkhLy8PGi1WowYMQKhoaE6yz4f2J9lY2MjXZn4bADKzMzErl27sG/fviL7IDIElaiIA9tERFRh9uzZg6lTp+LixYuoVq34MxkuXryIfv364ejRo7CyssLDhw+RlpaGXr164fTp0wD+CWfNmjVDq1at0Lt3bxw5cgR2dnZITk5GXl4eHjx4oFfD1tYWCQkJ6NWrFyIjI9G5c2fMnz8fXbt2BQCsXLkSO3fuxIEDByp0PojKgnuAiIheMG+//TauXbuGO3fulOg8rWbNmmHixIlo164djI2N0aVLF4wdO7bAZZs2bYqPP/4YnTp1glarhUajwYYNGwqssWTJEul1VlZW+Pnnn9GlSxdYWFigXbt2UKvV+Oqrr2R730Ry4h4gIiIiUhxeBUZERESKwwBEREREisMARERERIrDAERERESKwwBEREREisMARERERIrDAERERESKwwBEREREisMARERERIrDAERERESKwwBEREREivP/ALc683zbzTdGAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.scatter(nchecks_lst, probs, s=0.01)\n", "plt.xlabel(r\"Number of checkbits ($n_{\\mathrm{check}}$)\")\n", @@ -186,884 +176,6 @@ "plt.grid()\n", "plt.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "aaYg6KE37ZhM" - }, - "outputs": [], - "source": [ - "# Function for assigning Standard (s) or Hadamard (h) measurement basis\n", - "def choose_bases(stringLength=2):\n", - " \"\"\"\n", - " Generate a random string of fixed length\n", - " \"\"\"\n", - " basis = \"sh\"\n", - " return \"\".join(random.choice(basis) for i in range(stringLength))\n", - "\n", - "\n", - "# Function for executing the BB84 protocol using n qubits and N check-bits\n", - "def BB84(numqbits, numcheckbits, eve=True, strings=False):\n", - " \"\"\"\n", - " BB84(n,N)\n", - "\n", - " n: Length of the initial bit-string\n", - "\n", - " N: Number of bits to be used to verify the security of the key\n", - "\n", - " Eve: Default True. If True, Eve will be present in the protocol. If False, Eve will not be present.\n", - "\n", - " Stings: Default False. If True, return Alice's , Bob's and Eve's:\n", - " 1- initial bit strings\n", - " 2- keys\n", - " 3- initial basis used\n", - " 4- check bit sequence\n", - "\n", - " --------\n", - "\n", - " Returns\n", - "\n", - " R: List of strings of \"OK\" and \"ABORT\" that indicate when Eve has been detected\n", - "\n", - " a: List of Alice's bits\n", - " b: List of Bob's bits\n", - " e: List of Eve's bits\n", - " x: List of Alice's key\n", - " y: List of Bob's key\n", - " z: List of Eve's key\n", - "\n", - " aa: List of Alice's bases assignments\n", - " bb: List of Bob's bases assignments\n", - " ee: List of Eve's bases assignments\n", - "\n", - " xx: List of Alice's check-bits\n", - " yy: List of Bob's check-bits\n", - "\n", - " \"\"\"\n", - "\n", - " a_bits = []\n", - " b_bits = []\n", - " e_bits = []\n", - " a_key = []\n", - " b_key = []\n", - " e_key = []\n", - "\n", - " a_bases = choose_bases(numqbits) # Alice's bases assignment\n", - " b_bases = choose_bases(numqbits) # Bob's bases assignment\n", - " \n", - " if eve == True:\n", - " e_bases = choose_bases(numqbits) # Eve's bases assignment (when present)\n", - " else:\n", - " e_bases = a_bases # When Eve's not present, she can be thought of as being present,\n", - " # but having exactly the same bit-string and same basis as Alice\n", - "\n", - " # Generate a,b,e and x,y,z\n", - " for i in range(numqbits):\n", - " a_bits.append(random.randint(0, 1))\n", - "\n", - " if e_bases[i] == a_bases[i]: # If Eve is not present\n", - " e_bits.append(a_bits[i])\n", - " else:\n", - " e_bits.append(random.randint(0, 1)) # If Eve is present\n", - "\n", - " if b_bases[i] == e_bases[i]:\n", - " b_bits.append(e_bits[i]) # If Eve is present, and bases are the same between Bob and Eve, just pass qbit state to Bob\n", - " else:\n", - " b_bits.append(random.randint(0, 1)) # If Eve is present, and bases are different between Bob and Eve, Bob's qbit is in a random state\n", - " \n", - " if a_bases[i] == b_bases[i]: # \n", - " a_key.append(a_bits[i])\n", - " b_key.append(b_bits[i])\n", - " e_key.append(e_bits[i])\n", - "\n", - " R = []\n", - "\n", - " for j in numcheckbits:\n", - " if j <= len(a_key):\n", - " s = random.sample(range(len(a_key)), j) # Choice of check-bits\n", - " a_checkbits = []\n", - " b_checkbits = []\n", - " for i in range(j): # Generate xx,yy\n", - " a_checkbits.append(a_key[s[i]])\n", - " b_checkbits.append(b_key[s[i]])\n", - " if a_checkbits != b_checkbits: # Check for Eve's presence\n", - " R.append(\"ABORT\") # Eve detected\n", - " else:\n", - " R.append(\"OK\") # Eve not detected\n", - " else:\n", - " break\n", - "\n", - " count = 0\n", - " for k in range(len(a_key)):\n", - " if e_key[k] == a_key[k] and e_key[k] == b_key[k]:\n", - " count += 1\n", - " # print(count)\n", - " guessed_bits_perc = (count / len(e_key)) * 100\n", - " discarded_perc = 100 * (numqbits - len(a_key)) / numqbits\n", - "\n", - " if strings == False:\n", - " return R, discarded_perc, guessed_bits_perc\n", - " if strings == True:\n", - " return (\n", - " R,\n", - " a_bits,\n", - " b_bits,\n", - " e_bits,\n", - " a_bases,\n", - " b_bases,\n", - " e_bases,\n", - " a_key,\n", - " b_key,\n", - " e_key,\n", - " a_checkbits,\n", - " b_checkbits,\n", - " s, # ????\n", - " )\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "U_CrPCAapEl0" - }, - "source": [ - "# One instance of BB84 checking 2 bits for security (example)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "HZJvQuZguExY" - }, - "outputs": [], - "source": [ - "%%timeit\n", - "R,a_bits,b_bits,e_bits,a_bases,b_bases,e_bases,a_key,b_key,e_key,xx,yy,s=BB84(128,range(3),strings=True)\n", - "print(\"Alice's Check sequence: \",xx)\n", - "print(\"Bob's Check sequence: \",yy)\n", - "print('qubits checked are # : ',s)\n", - "print(\"Result of Simulation: \",R[-1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "UKyUQ9FmpEmA" - }, - "outputs": [], - "source": [ - "print(\"Alice's keys: \",a_key)\n", - "print()\n", - "print(\"Bobs's keys: \",b_key)\n", - "print()\n", - "print(\"Eve's keys: \",e_key)\n", - "print()\n", - "print(\"Alice's Basis: \",a_bases)\n", - "print()\n", - "print(\"Bobs's Basis: \",b_bases)\n", - "print()\n", - "print(\"Eve's Basis: \",e_bases)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "vTY8tJQ2TyVe" - }, - "source": [ - "# Simulation of the BB84 protocol" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "068zU_X2Q3kR" - }, - "outputs": [], - "source": [ - "k1=100 #Number of iterations of BB84\n", - "k2=100 # Sample points \n", - "numqbits=128 #Number of qubits\n", - "\n", - "# a=np.arange(5)# dummy variable \n", - "# N=2**a #Number of check-bits\n", - "\n", - "# In order to see the simulation with data for all values of possible checkbits uncomment the next line\n", - "numcheckbits=np.arange(1,16)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "ZuIdhWeQGOOF" - }, - "outputs": [], - "source": [ - "dist=np.empty([k2,len(numcheckbits)]) #Probability distribution\n", - "avrg_discarded_perc=0\n", - "avrg_guessed_bits_perc=0\n", - "#Generate dist,avrg\n", - "counter=0\n", - "for j in range(k2): #Loop for generating dist\n", - " abort=np.zeros(len(numcheckbits),int) #Number of ABORT\n", - " \n", - " for i in range(k1): #Loop for executing BB84\n", - " R,discarded_perc,guessed_bits_perc=BB84(numqbits,numcheckbits)\n", - "# if counter % 1000 == 0:\n", - "# print(avrg_guessed_bits_perc)\n", - " avrg_discarded_perc+=discarded_perc\n", - " avrg_guessed_bits_perc+=guessed_bits_perc\n", - " counter+=1\n", - " for m in range(len(R)): #Loop for each N\n", - " if R[m]=='ABORT': #Check for ABORT results\n", - " abort[m]+=1\n", - " pabort=abort/k1 #Experimental probability of ABORT\n", - " dist[j]=pabort\n", - "avrg_discarded_perc=(avrg_discarded_perc)/(k1*k2)\n", - "avrg_guessed_bits_perc=avrg_guessed_bits_perc/(k1*k2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Calculating Percentage of Bits discarded" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\\% \\text{ of discarded bits}= \\frac{1}{k_1 k_2} \\sum_{i=1}^{k_1 k_2} \\frac{n-\\text{len}(x_i)}{n}\\times 100 $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where $n$ is the length of the initial bit-string\n", - "\n", - "$k_1 k_2$ is the total number of iterations\n", - "\n", - "$\\text{len}(x_i)$ is the length of key prior to using the check bits on the $i$-th iteration" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Average percentage of bits that Eve guessed correctly" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\\% \\text{ of bits guessed by Eve}= \\frac{1}{k_1 k_2} \\sum_{i=1}^{k_1 k_2} \\frac{c_i}{\\text{len}(z_i)}\\times 100 $$\n", - "\n", - "where $c_i$ is the number of correctly guessed bits \n", - "\n", - "$\\text{len}(z_i)$ is the length of Eve's key " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print('average percentage of bits discarded is %.2f'%avrg_discarded_perc,'%')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print('average percentage of bits that Eve guessed is %.2f'%avrg_guessed_bits_perc,'%')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "ZuIdhWeQGOOF" - }, - "outputs": [], - "source": [ - "avrg1=np.mean(dist,axis=0) #Average of each column of dist\n", - "\n", - "print(avrg1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The average percentage of discovering Eve is the same as the probability of the key being compromised" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " The list above is normalized to 1. It shows the average probability of discovering Eve when varying the amount of bits to be used to verify the security of the key.\n", - "If you just want the probability for a specific number of \"check-bits\" call `avrg1[#]` where `#` is any number from 0-14 (remember that python is 0-indexed) and therfore \n", - "\n", - "`avrg1[0]` = the probability of discovering Eve when **checking only 1 bit**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "oJXty6UKzw1W" - }, - "source": [ - "# Now we use Matplotlib's hist function to draw a distribution \n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "_NaTnIpiGOOU" - }, - "source": [ - "## Configuration of the plots" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "02ceqNtfyHBd" - }, - "outputs": [], - "source": [ - "#Where to store the plots\n", - "outpath='plots_BB84'\n", - "\n", - "#Check if folder exists\n", - "if outpath not in os.listdir(): \n", - " os.mkdir(outpath)\n", - "else: \n", - " print(outpath,'already exists!')\n", - "\n", - "\n", - "#Configuration for the plots\n", - "start = 0\n", - "stop = 1\n", - "step = .05\n", - "bins=np.linspace(start, stop, num=150)\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "DEwW5uZqGOOn" - }, - "source": [ - "## For one plot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "UCiJnbN_GOOx" - }, - "outputs": [], - "source": [ - " \n", - "#Making 1 single plot\n", - "#Specifing qb changes the plot\n", - "\n", - "qb=1\n", - "# qb is the amount of qubits to inspect UNLESS you changed the parameter lines at the simulation stage\n", - "# Keep in mind that python is 0-indexed 1 does NOT mean 1 qubit\n", - "\n", - "\n", - "\n", - "plt.figure(num=qb,dpi=200)\n", - "count,val,_=plt.hist(dist[:,qb],bins=bins,align='left',histtype='step')\n", - "plt.vlines(x=avrg1[qb],ymin=0,ymax=max(count),label=\"Average Value\",alpha=.63,linestyles='dashed')\n", - "\n", - "plt.xticks(np.arange(start, stop+step, 2*step))\n", - "plt.legend()\n", - "plt.xlabel('Probability of discovering Eve when using %i check-bits' %numcheckbits[qb],fontsize=12)\n", - "plt.ylabel('Frequency',fontsize=12)\n", - "plt.xlim(0.01,1.0)\n", - "plt.title('BB84 protocol using %i qubits'%numqbits)\n", - "\n", - "plt.savefig(outpath+'/'+'BB84-dist with %i check-bits.png'%numcheckbits[qb],dpi=200)\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "7Dsb4Q5MGOO-" - }, - "source": [ - "## For all plots individually" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "HJK9OpgjGOPA", - "scrolled": true - }, - "outputs": [], - "source": [ - "for qb in range(len(numcheckbits)):\n", - " count,val=[],[]\n", - " plt.figure(num=qb,dpi=200)\n", - " count,val,_=plt.hist(dist[:,qb],bins=bins,align='left',histtype='step' )\n", - " plt.vlines(x=avrg1[qb],ymin=0,ymax=max(count),label=\"Average Value\",linestyles=\"dashed\",alpha=0.63)\n", - " plt.xticks(np.arange(start, stop+step, 2*step))\n", - " plt.xlabel('Probability of discovering Eve when using %i check-bits' %numcheckbits[qb])\n", - " plt.ylabel('Frequency')\n", - " plt.xlim(0.01,1.0)\n", - " plt.title('BB84 protocol using %i qubits'%numqbits)\n", - " plt.savefig(outpath+'/'+'BB84-dist with %i check-bits'%numcheckbits[qb],dpi=200)\n", - " plt.show()\n", - " plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "mwXx17HLGOPM" - }, - "source": [ - "## This part is completely optional if you want a closer look at the individual distributions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "kNiJ0v5Gz9xr", - "jupyter": { - "outputs_hidden": true - } - }, - "outputs": [], - "source": [ - "for qb in range(len(numcheckbits)):\n", - " count,val=[],[]\n", - " plt.figure(num=qb,dpi=200)\n", - " count,val,_=plt.hist(dist[:,qb],bins=50,align='left',histtype='step' )\n", - " ll=['%.3f' %a for a in val]\n", - " plt.xticks(ticks=val[::5],labels=ll[::5],fontsize=8)\n", - " plt.yticks(fontsize=8)\n", - " plt.xlabel('Probability of discovering Eve when using %i check-bits' %numcheckbits[qb])\n", - " plt.ylabel('Frequency')\n", - " plt.title('BB84 protocol using %i qubits'%numqbits)\n", - " plt.savefig(outpath+'/'+'CloserLook_BB84-dist with %i check-bits'%numcheckbits[qb],dpi=200)\n", - " plt.show()\n", - " plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "GOiePP4uGOPc" - }, - "source": [ - "## A few of them together" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "eiYWP0vPyl8N" - }, - "outputs": [], - "source": [ - "plt.figure(dpi=200)\n", - "start = 0\n", - "stop = 1\n", - "step = .05\n", - "bins=np.linspace(start, stop, num=150)\n", - "\n", - "numcheckbits=np.arange(1,16)\n", - "\n", - "for qb in range(0,len(numcheckbits),3):\n", - " count,val,_=plt.hist(dist[:,qb],align='left',histtype='stepfilled',label='Probability using %i check-bits' %numcheckbits[qb],bins=bins )\n", - " plt.vlines(x=avrg1[qb],ymin=0,ymax=max(count), colors='k', linestyles='dashed',alpha=.63)\n", - "\n", - "plt.vlines(x=avrg1[0],ymin=0,ymax=.001, colors='k', linestyles='dashed', label='Average Values',alpha=.63)\n", - "plt.xticks(np.arange(start, stop+step, 2*step),fontsize=10)\n", - "plt.yticks(fontsize=10)\n", - "plt.xlabel('Probability of discovering Eve',fontsize=10)\n", - "plt.ylabel('Frequency',fontsize=12)\n", - "plt.xlim(0.05,1.0)\n", - "# plt.grid(axis='x')\n", - "plt.legend(shadow=True,fontsize=7,bbox_to_anchor=(1.015,.5), loc=\"center left\",borderaxespad=0)\n", - "plt.title('BB84 protocol using %i qubits'%numqbits , fontsize=14)\n", - "plt.savefig(outpath+'/'+'BB84-dist-superimposed',dpi=200,bbox_inches=\"tight\")\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Theoretical values of the probability of finding Eve" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "eKB5yZeQpEm3", - "scrolled": true - }, - "outputs": [], - "source": [ - "N1=np.arange(1,16)\n", - "P=1-(.75)**N1\n", - "print(P)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "WPynFhP5GOP3" - }, - "source": [ - "## Errors between our theoretical values and our simulation values for the probabilities of detecting Eve" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "zxbNjYlMpEm7" - }, - "outputs": [], - "source": [ - "# Absolute Error\n", - "abserr=np.abs(avrg1-P)\n", - "\n", - "# Percentage Error\n", - "percenterr=(abserr/P)*100\n", - "print('\\n Percentage of Error Absolute Error')\n", - "count=1\n", - "for i,a_key in zip(percenterr,abserr):\n", - " print(str(count)+') \\t'+'%.4f' %i+' %'+'\\t\\t %.4f' %a_key)\n", - " count+=1\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "tiurWrLX-61y" - }, - "outputs": [], - "source": [ - "plt.figure(dpi=200)\n", - "plt.bar(numcheckbits,avrg1,alpha=.5,align='edge')\n", - "\n", - "plt.plot(numcheckbits,P,'--g',label='$P=1-(3/4)^N$')\n", - "plt.xticks(ticks=numcheckbits,fontsize=8)\n", - "plt.yticks(ticks=np.arange(start,stop+step,2*step),fontsize=8)\n", - "plt.xlabel('Number of Check-bits',fontsize=10)\n", - "plt.ylabel('Average Prob of Discovering Eve',fontsize=10)\n", - "plt.title('BB84 \\n Probability of Discovering Eve when varying amount of check-bits', fontsize=10)\n", - "plt.legend(fontsize=10,loc='upper left',shadow=True)\n", - "plt.grid(axis='y',color='k',linestyle='--',alpha=.7)\n", - "plt.savefig(outpath+'/'+'BB84-prob-per-Check-bits.png',dpi=200)\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "SJCf6mbkGOQU" - }, - "source": [ - "# We also have a CSV file with data from a more precise simulation" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "3NCblmeaGOQW" - }, - "source": [ - "We won't need to run a sim each time you need to look a the plots or generate other plots\n", - "\n", - "We can use pandas to read the CSV file provided in the Github Repo" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "KAXdwHhCGOQY" - }, - "outputs": [], - "source": [ - "import pandas as pd" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "4tCxNibcGOQl" - }, - "source": [ - "## Configuration of the plots" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "_PJkhsXjGOQn" - }, - "outputs": [], - "source": [ - "df=pd.read_csv('Distribution-Data-for-BB84.csv')\n", - "df" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Here we see some statistics of the simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "7r0R1PQlGOQ1", - "scrolled": false - }, - "outputs": [], - "source": [ - "print(df.info())\n", - "df.describe()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "zkc7Ha1wGORA" - }, - "outputs": [], - "source": [ - "#Where to store the plots\n", - "outpath='pandas_plots_BB84'\n", - "\n", - "#Check if folder exists\n", - "if outpath not in os.listdir():\n", - " os.mkdir(outpath)\n", - "else: \n", - " print(outpath,'already exists!')\n", - "\n", - "\n", - "#Configuration for the plots\n", - "start = 0\n", - "stop = 1\n", - "step = .05\n", - "bins=np.linspace(start, stop, num=250)\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "dvJTKC5GGORK" - }, - "source": [ - "## Plotting all check-bit distributions in one" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wHwf8ZDwGORM" - }, - "outputs": [], - "source": [ - "plt.figure(dpi=200)\n", - "df.plot(kind='hist',fontsize=10, align='left',histtype='stepfilled' ,bins=bins,ax = plt.gca())\n", - "plt.legend(loc='upper center',ncol=5,fontsize=8,shadow=True)\n", - "plt.ylabel('Frequency',fontsize=12)\n", - "plt.xlabel('Probability of Detecting Eve', fontsize=12)\n", - "plt.xticks(np.arange(start, stop+step, 2*step))\n", - "plt.xlim(0.05,1.0)\n", - "plt.title('BB84 \\n Probability of Detecting Eve varying the amount of checkbits',fontsize=12)\n", - "plt.savefig(outpath+'/'+'Pandas-Dist-Supermposed-All.png',dpi=200,format='png')\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "YY_aq3m-GOR4" - }, - "source": [ - "## Probabilities of finding Eve (average values)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "-3Nhf9AXGORZ" - }, - "outputs": [], - "source": [ - "avrg2=df.mean()\n", - "avrg2" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "WPynFhP5GOP3" - }, - "source": [ - "## Errors between our theoretical values and our simulation values for the probabilities of detecting Eve" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "zxbNjYlMpEm7" - }, - "outputs": [], - "source": [ - "# Absolute Error\n", - "abserr=np.abs(avrg2-P)\n", - "\n", - "# Percentage Error\n", - "percenterr=(abserr/P)*100\n", - "print('\\n Percentage of Error Absolute Error')\n", - "count=1\n", - "for i,a_key in zip(percenterr,abserr):\n", - " print(str(count)+') \\t'+'%.4f' %i+' %'+'\\t\\t %.4f' %a_key)\n", - " count+=1\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Final plot with the results of the simulation " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "C1o9IRJQGORt" - }, - "outputs": [], - "source": [ - "N2=[0,1,3,7,14] #choosing the checkbit distributions\n", - "plt.figure(dpi=200)\n", - "for qb in N2:\n", - " count,val,_=plt.hist(df[df.columns[qb]],align='left',histtype='bar',label='Probability using %s check-bits' %df.columns[qb],bins=bins )\n", - " plt.vlines(x=avrg2[qb],ymin=0,ymax=max(count), colors='k', linestyles='dashed',alpha=.63)\n", - "\n", - "\n", - "plt.vlines(x=avrg2,ymin=0,ymax=.01, linestyles='dashed', label='Average Values')\n", - "plt.xticks(np.arange(start, stop+step, 2*step),fontsize=10)\n", - "plt.yticks(fontsize=10)\n", - "plt.xlabel('Probability of Detecting Eve',fontsize=12)\n", - "plt.ylabel('Frequency',fontsize=12)\n", - "plt.xlim(0.05,1.0)\n", - "\n", - "plt.legend(loc='center left',ncol=1,shadow=True,fontsize=8,bbox_to_anchor=(1.015,.5))\n", - "plt.title('BB84\\n Probability of Detecting Eve' , fontsize=12)\n", - "plt.savefig(outpath+'/'+'Pandas-BB84-dist-superimposed.png',dpi=200,bbox_inches=\"tight\")\n", - "plt.show()\n", - "plt.close()" - ] } ], "metadata": { diff --git a/notebooks/qCryptoShowcase.ipynb b/notebooks/qCryptoShowcase.ipynb new file mode 100644 index 0000000..781a95d --- /dev/null +++ b/notebooks/qCryptoShowcase.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# `qcrypto` Showcase\n", + "\n", + "This notebook presents the key features of `qcrypto`, a Python library of the simulation of simple quantum cryptography simulations. The primary classes of this library are `QstateEnt` and `QstateUnEnt`. These classes are used to represent the quantum state of a set of qubits. The former is the most general, being capable of simulating the state of a possiibility entangled set of qubits, whereas the latter can only be used to simulate a system of qubits which are unentangled." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from qcrypto.simbasics import QstateEnt, QstateUnEnt" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.05056588+0.45371015j 0.24549441+0.12567301j 0.7470044 +0.03376755j\n", + " 0.17609874+0.35406553j]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Entangled set of n qubits\n", + "n = 2\n", + "qstateent = QstateEnt(init_method=\"random\", init_nbqubits=n)\n", + "qstateent" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0, 0])" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qstateent.measure_all(\"simult\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Unentangled set of n qubits\n", + "n = 2\n", + "# qstateunent = QstateUnEnt(init_method=\"random\", )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`qcrypto` also provides methods for applying quantum gates to the qubits, giving it the potential to be used to simulate simple quantum circuits. For instance, we can apply the Hadamard gate:\n", + "$$\n", + " H = \\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", + " 1 & 1 \\\\ 1 & -1\n", + " \\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index e0d275b..2fc038f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "qcrypto" -version = "0.0.1" +version = "1.0.0" authors = [ {name = "Roy Cruz", email = "roy.cruz@upr.edu"}, {name = "Guillermo Fidalgo", email = "guillermo.fidalgo@upr.edu"}, @@ -13,6 +13,15 @@ authors = [ description = "A package for simple quantum cryptography simulations." readme = "README.md" requires-python = ">=3.8" +dependencies = [ + "numpy" +] + +[project.optional-dependencies] +tests = [ + "pytest", + "coverage" +] [project.urls] Homepage = "https://github.com/GuillermoFidalgo/QKDP" diff --git a/src/qcrypto/gates.py b/src/qcrypto/gates.py new file mode 100644 index 0000000..9d562a6 --- /dev/null +++ b/src/qcrypto/gates.py @@ -0,0 +1,42 @@ +import numpy as np +import numpy.typing as npt +from typing import Dict + +Pauli: Dict[str, npt.NDArray[np.complex_]] = { + "x": np.array([[0, 1], [1, 0]], dtype=np.complex_), + "y": np.array([[0, -1j], [1j, 0]], dtype=np.complex_), + "z": np.array([[1, 0], [0, -1]], dtype=np.complex_), +} + +H_gate: npt.NDArray[np.complex_] = (1 / np.sqrt(2)) * np.array( + [[1, 1], [1, -1]], dtype=np.complex_ +) + + +def Phase_shift(phase: float) -> npt.NDArray[np.complex_]: + """Generates a phase shift gate for a given phase. + + Args: + phase (float): The phase angle in radians. + + Returns: + NDArray: A 2x2 numpy array representing the phase shift gate. + """ + phase_shift_gate = np.array([1, 0], [0, np.e ** (1j * phase)]) + return phase_shift_gate + + +def tensor_power(gate: npt.NDArray[np.complex_], N: int) -> npt.NDArray[np.complex_]: + """Computes the tensor power of a 2x2 gate matrix. + + Args: + gate (NDArray): A 2x2 numpy array representing a quantum gate. + N (int): The power to which the gate matrix is to be raised, tensor-wise. + + Returns: + NDArray: A numpy array representing the N-th tensor power of the gate. + """ + result = gate + for _ in range(N - 1): + result = np.kron(result, gate) + return result diff --git a/src/qcrypto/simbasics.py b/src/qcrypto/simbasics.py index ff90469..0a3aaa3 100644 --- a/src/qcrypto/simbasics.py +++ b/src/qcrypto/simbasics.py @@ -1,132 +1,482 @@ import numpy as np +import numpy.typing as npt import dataclasses +from qcrypto.gates import tensor_power +from typing import Union, Dict, Tuple, Optional, List +from abc import ABC, abstractmethod + + +class QState(ABC): + @abstractmethod + def measure(self, qubit_idx: int) -> int: ... + + @abstractmethod + def measure_all(self) -> npt.NDArray[np.int_]: ... + + @abstractmethod + def apply_gate( + self, + gate: npt.NDArray[np.complex_], + qubit_idx: Union[int, npt.NDArray[np.int_], List[int], None] = None, + ) -> None: ... + + @abstractmethod + def _calculate_measurement_probs(self, qubit_idx: int) -> Tuple[float, float]: ... + + @abstractmethod + def _update_state_post_measurement(self, qubit_idx: int, outcome: int) -> None: ... + + @abstractmethod + def _normalize_state(self) -> None: ... @dataclasses.dataclass -class Qubit: - """ - Data class which represents the qubit and its quantum state. - Quantum state is given by: - |\psi> = \cos(\frac12 \theta) |0> + e^{i\phi} \sin(\frac12 \theta) |1> - """ +class QstateUnEnt(QState): + _state: Optional[npt.NDArray[np.complex_]] = None + num_qubits: int = 10 + init_method: str = "zeros" - def __init__(self, theta, base, phi=0): - self.base = base - self.theta = theta # Meaningless once projected. FIX - self.phi = phi # Same with this. FIX - self.state = np.array([np.cos(theta / 2), np.exp(1j * phi) * np.sin(theta / 2)]) + def __post_init__(self) -> None: + if self._state is None: + if self.init_method == "zeros": + self._state = np.zeros((self.num_qubits, 2), dtype=np.complex_) + self._state[:, 0] = 1 + elif self.init_method == "random": + self._state = np.random.random( + (self.num_qubits, 2) + ) + 1j * np.random.random((self.num_qubits, 2)) + self._normalize_state() + else: + raise ValueError("Invalid initialization method.") + else: + if self._state.shape != (self.num_qubits, 2): + raise ValueError( + "State vector shape not appropriate for the number of qubits." + ) + self._normalize_state() + self.num_qubits = len(self._state) + self._state = np.asarray(self._state, dtype=np.complex_) - def __str__(self): - return "{} |0> + {} |1>".format(self.state[0], self.state[1]) + def measure(self, qubit_idx: int) -> int: + """ + Simulates the measurement of a single qubit. As a result, the state of said qubit is collapsed depending on the result. - def __repr__(self): - return "{} |0> + {} |1>".format(self.state[0], self.state[1]) + Args: + qubit_idx (int): Index of the qubit to be measured + + Returns: + Outcome of the measurement. Also collapses the state of the qubit. - def normalize(self): """ - Nomalizes the qubit's quantum state + + probs_0, probs_1 = self._calculate_measurement_probs(qubit_idx) + outcome = np.random.choice([0, 1], p=[probs_0, probs_1]) + self._update_state_post_measurement(qubit_idx, outcome) + return outcome + + def measure_all(self, *args) -> npt.NDArray[np.int_]: """ - norm = np.sqrt(np.dot(np.conjugate(self.state), self.state)) - self.state /= norm + Measures all of the qubits in sequential order. + + Args: + n/a + + Returns: + Numpy array containing the outcome of all of the measurments. - def get_probs(self): """ - Gives the probabilities of getting 0 or 1 when measuring the qubit + outcome = [] + for qubit_idx in range(self.num_qubits): + outcome.append(self.measure(qubit_idx=qubit_idx)) + return np.array(outcome) + + def apply_gate( + self, + gate: npt.NDArray[np.complex_], + qubit_idx: Union[int, npt.NDArray[np.int_], List[int], None] = None, + ) -> None: """ + Applies a given gate to a subset of qubits, modifying the quantum state. - prob0 = (np.conjugate(self.state[0]) * self.state[0]).real - prob1 = (np.conjugate(self.state[1]) * self.state[1]).real - return prob0, prob1 + Args: + gate (np.array): Gate to be applied. Represented as a numpy matrix + qubit_idx (int, list): Index/Indices of qubit/qubits which will be transformed by gate - def copy(self): - return Qubit(theta=self.theta, base=self.base, phi=self.phi) + Returns: + None + """ + if self._state is None: + raise ValueError("Error applying gate. State has not been initialized.") + if qubit_idx is not None: -class Agent: + self._state[qubit_idx] = np.dot(gate, self._state[qubit_idx]) + else: + reshaped_states = self._state.reshape(self._state.shape[0], 2, 1) + new_states = np.dot(gate, reshaped_states) + self._state = new_states.reshape(self._state.shape) + self._normalize_state() + + def _calculate_measurement_probs(self, qubit_idx: int) -> Tuple[float, float]: + """ + Computes the probability of measuring qubit_idx to be in state 0 or 1 in whatever base its in. + + Args: + qubit_idx (int): Index of qubit + + Returns: + Probabilities of obtaining 0 and 1 if qubit were to be measured + """ + + if self._state is None: + raise ValueError( + "Unable to compute measurement probabilities. State has not been initialized." + ) + + prob_0 = np.abs(self._state[qubit_idx, 0]) ** 2 + prob_1 = np.abs(self._state[qubit_idx, 1]) ** 2 + return prob_0, prob_1 + + def _update_state_post_measurement(self, qubit_idx: int, outcome: int) -> None: + """ + Updates the quantum state of a qubit by projecting it unto a given outcome state. + + Args: + qubit_idx (int): Index of qubit + outcome (int): Outcome unto which the qubit will be projected + + Returns: + None + """ + + if self._state is None: + raise ValueError("Unable to update state. State has not been initialized.") + + if outcome == 0: + self._state[qubit_idx] = np.array([1, 0], dtype=np.complex_) + else: + self._state[qubit_idx] = np.array([0, 1], dtype=np.complex_) + + def _normalize_state(self) -> None: + """ + Normalizes the quantum state. + + Args: + n/a + + Returns: + None + """ + + if self._state is None: + raise ValueError("Error normalizing state. State has not been initialized.") + + norms = np.linalg.norm(self._state, axis=1) + norms = norms.reshape(-1, 1) + self._state = self._state / norms + + +@dataclasses.dataclass +class QstateEnt(QState): """ - Class representing the players in a quantum cryptography simulation (e.g. Alice, Eve or Bob) + Represents the state of a set of N qubits which might be entangled. """ - def __init__(self, numqubits=None, basis_selection=None, key=None, message=None): + _state: Optional[npt.NDArray[np.complex_]] = None + num_qubits: int = 10 + init_method: str = "zeros" + + def __post_init__(self) -> None: + + if self._state is None: + self._auto_init() + else: + if len(self._state) != 2**self.num_qubits: + raise ValueError( + "State vector size not appropriate for the number of qubits specified." + ) + self._normalize_state() + self._state = np.asarray(self._state, dtype=np.complex_) + + def _auto_init(self) -> None: """ + Initializes the quantum state of the system depending on the initialziation method chosen by the user. + Args: - numqubits: Number of qubits to be used - basis_selection: Set of bases to choose from when generating the set of numqubits qubits. - If not given, will default to 0 and pi/2. + init_method (str): Initialziation method + + Returns: + None + """ - self.qubits = [] - self.measurements = [] - self.key = [] + if self.num_qubits is None: + raise ValueError("Number of qubits not specified.") + + if self.init_method not in ["zeros", "random"]: + raise ValueError( + f""" + Invalid intialization method. Got {self.init_method} + expected one of either ['zeros', 'random'] + """ + ) + if self.init_method == "zeros": + self._state = np.zeros(2**self.num_qubits, dtype=np.complex_) + self._state[0] = 1 + elif self.init_method == "random": + self._state = np.random.rand(2**self.num_qubits) + 1j * np.random.rand( + 2**self.num_qubits + ) + self._normalize_state() - if numqubits is not None or numqubits != 0: - if basis_selection is None: - self.basis_selection = [0, np.pi / 4] + def measure(self, qubit_idx: int) -> int: + """ + Measure the qubit_idx'th qubit, calculating the probability of each outcome and returning said outcome. + + Args: + qubit_idx (int): Index identifying the qubit to be measured + + Returns: + Outcome of the measurement, either 0 or 1 + """ + + probs_0, probs_1 = self._calculate_measurement_probs(qubit_idx) + outcome = np.random.choice([0, 1], p=[probs_0, probs_1]) + self._update_state_post_measurement(qubit_idx, outcome) + return outcome + + def measure_all(self, order: str) -> npt.NDArray[np.int_]: + """ + Measures all of the qubits + + Args: + order (str): Specifies the order in which the qubits will be measured. + "simult" = all qubits measured simultaneously + "sequential" = qubits measured in sequential order (first 0, second 1, etc.) + + Returns: + Outcome of the measurements done. Array of 0's and 1's equal in length to the number of qubits in the system. + """ + + if self._state is None: + raise ValueError("Error performing measurements. State is not initialized.") + + if order == "simult": + outcome = np.random.choice( + np.arange(len(self._state)), p=self._calculate_measurement_probs() + ) + self._state.fill(0 + 0j) + self._state[outcome] = 1 + 0j + outcome_arr = np.array( + list( + "0" * (self.num_qubits - len(bin(outcome)[2:])) + bin(outcome)[2:] + ), + dtype=np.int_, + ) + return outcome_arr + elif order == "sequential": + outcome = [] + for i in range(self.num_qubits): + outcome.append(self.measure(qubit_idx=i)) + self._update_state_post_measurement + outcome_arr = np.array(outcome) + return outcome_arr + else: + raise ValueError("Order specified not valid.") + + def _calculate_measurement_probs(self, qubit_idx: Optional[int] = None): + """ + From the probability amplitude, computes the probability that a measurement of a given qubit will give 0 or 1. + + Args: + qubits_idx (int): Index identifying the qubit to be measured + + Returns: + Probability of measuring qubit in position qubit_idx to be measured to be 0 or to be 1 + """ + + if self._state is None: + raise ValueError( + "Error calculating measurement probabilities. State has not been initialized." + ) + + if qubit_idx is None: + outcome_probs = np.abs(self._state) ** 2 + return outcome_probs + else: + if qubit_idx >= self.num_qubits or qubit_idx < 0: + raise ValueError( + "Invalid qubit index. Make sure it is between 1 and num_qubits - 1" + ) + + prob_0 = 0 + prob_1 = 0 + for idx, prob_amp in enumerate(self._state): + if (idx >> qubit_idx) & 1 == 0: + prob_0 += np.abs(prob_amp) ** 2 + else: + prob_1 += np.abs(prob_amp) ** 2 + return prob_0, prob_1 + + def _update_state_post_measurement(self, qubit_idx: int, outcome: int) -> None: + """ + Updates the quantum state post-measurement, effectively collapsing the wave function. based on the result obtained. + + Args: + qubit_idx (int): Index identifying qubit which was measured + outcome (int): Result of the measurement. Either 0 or 1 + + Returns: + None + """ + + if self._state is None: + raise ValueError("Error updating state. State has not been initialized.") + + new_state = [] + for idx, amplitude in enumerate(self._state): + if ((idx >> qubit_idx) & 1) == outcome: + new_state.append(amplitude) else: - self.basis_selection = basis_selection + new_state.append(0) + + self._state = np.array(new_state) + self._normalize_state() + + def _normalize_state(self): + """ + Updates the state of the system by normalizing its states. - qubitbases = np.random.choice(self.basis_selection, numqubits) + Args: + n/a - for basis in qubitbases: - # theta = 0 -> qubit = 0 state - # theta = pi -> qubit = 1 state - qubit = Qubit(np.random.choice([0, np.pi]), basis, phi=0) - self.qubits.append(qubit) + Retuns: + n/a + """ + self._state /= np.linalg.norm(self._state) - def project(self, qubit, base): + def apply_gate(self, gate: npt.NDArray[np.complex_]): """ - Projects the given qubit unto the agent's measuring basis + Applies quantum gate to the system of qubits. + + Args: + gate (np.NDarray): Gate to be applied to the to the system + + Returns: + n/a + """ - newqubit = qubit - base_delta = base - newqubit.base - # Projection oprator - proj_mtrx = np.array( - [ - [np.cos(base_delta), -1 * np.sin(base_delta)], - [np.sin(base_delta), np.cos(base_delta)], - ] - ) + if self._state is None: + raise ValueError("Error applying gate. State has not been initialized.") + + N = int(np.log2(len(self._state))) + gate = tensor_power(gate, N) + self._state = np.dot(gate, self._state) + self._normalize_state() - newqubit.state = np.dot(proj_mtrx, newqubit.state) - newqubit.normalize() + def __str__(self): + return str(self._state) + + def __repr__(self): + return str(self._state) + + +@dataclasses.dataclass +class Agent: + num_priv_qubits: Optional[int] = None + qstates: Dict[str, Optional[QState]] = dataclasses.field( + default_factory=lambda: {"private": None, "public": None} + ) + keys: Dict[str, Optional[npt.NDArray[np.int_]]] = dataclasses.field( + default_factory=lambda: {"private": None, "public": None} + ) + priv_qstates: Union[QstateEnt, QstateUnEnt, None] = None + init_method: str = "random" + priv_qbittype: Optional[str] = None - newqubit.base = base + def __post_init__(self) -> None: + """ + Initializes the priavate `qstate`. - return newqubit + Args: + n/a - def measure(self, qubit, base, rtrn_result=True): + Returns: + None """ - Gives the result of a sample measurement of the qubit. + + if self.priv_qstates is None: + if self.priv_qbittype == "entangled": + self.set_qstate( + QstateEnt( + num_qubits=self.num_priv_qubits, init_method=self.init_method + ), + "private", + ) + elif self.priv_qbittype == "unentangled": + self.set_qstate( + QstateUnEnt( + num_qubits=self.num_priv_qubits, init_method=self.init_method + ), + "private", + ) + elif isinstance(self.priv_qstates, (QState)): + self.set_qstate(qstate=self.priv_qstates, qstate_type="private") + + def set_qstate( + self, qstate: Union[QstateEnt, QstateUnEnt], qstate_type: str + ) -> None: """ - self.project(qubit, base) - measurement_result = np.random.choice((0, 1), p=qubit.get_probs()) - self.measurements.append(measurement_result) - if rtrn_result: - return measurement_result + Sets a given qstate as either a private or public qubit of the Agent - def send_quantum(self, recipient, recipient_bases): - # Recipient obtains the qubits from self and measures using given basis - for qubit, base in zip(self.qubits, recipient_bases): - recipient.measure(qubit.copy(), base) + Args: + qstate (QstateEnt or QstateUnEnt): Quantum state of a private or public system of qubits + qstate_type (str): Whether the given qstate is to be private or public - # Recipient construct qubits based on these measurements and on their own basis selection - recipient.qubits = [] - for base, measurement in zip(recipient_bases, recipient.measurements): - received_qubit = Qubit(measurement * np.pi, base) # measurement = 0 or 1 - recipient.qubits.append(received_qubit) - recipient.qubits = np.array(recipient.qubits) + Returns: + None + """ - def get_key(self, bases): - for qubit, base in zip(self.qubits, bases): - measurement = self.measure(qubit, base) - self.key.append(measurement) - return self.key + if not isinstance(qstate, QstateUnEnt) and not isinstance(qstate, QstateEnt): + raise ValueError("Wrong type given for system state.") - def send_classic(self, receiverAgent, bits): - pass + self.qstates[qstate_type] = qstate - def genkey(self, numqubits, numcheckbits): + def measure(self, qstate_type: str, qubit_idx=None) -> int: """ - Generates an array of qubits + Measures the """ - pass + if qstate_type not in self.qstates.keys(): + raise ValueError("Not valid qstate type.") + + if self.qstates.get(qstate_type) is None: + raise ValueError("") + + outcome = self.qstates[qstate_type].measure(qubit_idx=qubit_idx) + return outcome + + def measure_all(self, qstate_type: str, order=None) -> npt.NDArray[np.int_]: + + if self.qstates[qstate_type] is None: + raise ValueError( + "Error measuring {} qstate. It has not been initialized.".format( + qstate_type + ) + ) + + if qstate_type not in self.qstates.keys(): + raise ValueError("Invalid qstate type") + + outcome = self.qstates[qstate_type].measure_all(order) + return outcome + + def apply_gate(self, gate: npt.NDArray[np.complex_], qstate_type: str) -> None: + if qstate_type not in self.qstates.keys(): + raise ValueError("Invalid qstate type") + + self.qstates[qstate_type].apply_gate(gate) + + def get_key(self, qstate_type: str, order=None) -> npt.NDArray[np.int_]: + outcome = self.measure_all(qstate_type=qstate_type, order=order) + self.keys[qstate_type] = outcome + return outcome diff --git a/tests/test_simbasics.py b/tests/test_simbasics.py index fe925e2..08e0083 100644 --- a/tests/test_simbasics.py +++ b/tests/test_simbasics.py @@ -1,119 +1,149 @@ import unittest -from qcrypto.simbasics import Qubit, Agent +from qcrypto.simbasics import QstateUnEnt, QstateEnt, Agent +from qcrypto.gates import H_gate import numpy as np class TestQubit(unittest.TestCase): - def test_initialization(self): + def test_initialization_unent_zeros(self): """ - Test case to verify the correct initialization of a Qubit object with specified theta and phi parameters. + Test case to verify the correct initialization of a QstateUnEnt object with specified state. """ - qubit = Qubit(np.pi / 2, 0) - expected_state = np.array([np.cos(np.pi / 4), np.sin(np.pi / 4)]) - np.testing.assert_array_almost_equal(qubit.state, expected_state) + qubit = QstateUnEnt(num_qubits=2, init_method="zeros") + expected_state = np.complex_(np.array([[1, 0], [1, 0]])) + np.testing.assert_array_almost_equal(qubit._state, expected_state) - def test_normalization(self): + def test_initialization_ent_zeros(self): """ - Test case to verify the normalization of a Qubit object. + Test case to verify the correct initialization of a QstateEnt object with specified state. """ - qubit = Qubit(0, 0) - qubit.state = np.array([1, 2]) - qubit.normalize() - expected_state = qubit.state / np.linalg.norm(qubit.state) - np.testing.assert_array_almost_equal(qubit.state, expected_state) + qubit = QstateEnt(num_qubits=2, init_method="zeros") + expected_state = np.complex_(np.array([1, 0, 0, 0])) + np.testing.assert_array_almost_equal(qubit._state, expected_state) - def test_probability_calculation(self): + def test_normalization_unent(self): """ - Test case to verify the probability calculation of a Qubit object for |0> and |1> states. + Test case to verify the normalization of a QstateUnEnt object. """ - qubit = Qubit(0, 0) # Should be |0> state - probs = qubit.get_probs() + state = np.array([[1, 2], [3, 4]]) + qubit = QstateUnEnt(num_qubits=2, _state=state.copy()) + norms = np.linalg.norm(state, axis=1) + norms = norms.reshape(-1, 1) + expected_states = state / norms + np.testing.assert_array_almost_equal(qubit._state, expected_states) + + def test_normalization_ent(self): + """ + Test case to verify the normalization of a QstateEnt object. + """ + state = np.complex_(np.array([1, 2, 3, 4])) + qubit = QstateEnt(num_qubits=2, _state=state.copy()) + expected_state = state / np.linalg.norm(state) + np.testing.assert_array_almost_equal(qubit._state, expected_state) + + def test_probability_calculation_unent(self): + """ + Test case to verify the probability calculation of a QstateEnt object. + """ + state0 = np.complex_(np.array([[1, 0]])) + qubit = QstateUnEnt(num_qubits=1, _state=state0) + probs = qubit._calculate_measurement_probs(qubit_idx=0) self.assertAlmostEqual(probs[0], 1, places=7) self.assertAlmostEqual(probs[1], 0, places=7) - qubit = Qubit(np.pi, 0) # Should be |1> state - probs = qubit.get_probs() + state1 = np.complex_(np.array([[0, 1]])) + qubit = QstateUnEnt(num_qubits=1, _state=state1) # Should be |1> state + probs = qubit._calculate_measurement_probs(qubit_idx=0) self.assertAlmostEqual(probs[0], 0, places=7) self.assertAlmostEqual(probs[1], 1, places=7) + def test_probability_calculation_ent(self): + """ + Test case to verify the probability calculation of a QstateUnEnt object. + """ + bin_outcome = "1010" + outcome = int(bin_outcome, 2) # 10 + num_qubits = 4 + state = np.complex_(np.zeros(2**num_qubits)) + state[outcome] = 1 + 0j + qubit = QstateEnt(num_qubits=num_qubits, _state=state) + probs = qubit._calculate_measurement_probs() + + np.testing.assert_equal(np.where(np.isclose(probs, 1))[0][0], outcome) + class TestAgent(unittest.TestCase): - def test_measurement(self): - """ - Test case to verify the measurement process of an Agent object with specified bases. - """ - agent = Agent(numqubits=1, basis_selection=[0]) - qubit = agent.qubits[0] - result_zero_basis = agent.measure(qubit, 0) - self.assertIn(result_zero_basis, [0, 1]) - - agent_pi_4 = Agent(numqubits=1, basis_selection=[np.pi / 4]) - qubit_pi_4 = agent_pi_4.qubits[0] - result_pi_4_basis = agent_pi_4.measure(qubit_pi_4, np.pi / 4) - self.assertIn(result_pi_4_basis, [0, 1]) - - def test_send_quantum_same_basis(self): - """ - Test case to verify the sending and measurement of qubits between Alice and Bob - when they use the same basis for sending and measuring. - """ - basis_options = [0, np.pi / 4] - chosen_basis = np.random.choice(basis_options) - alice_state = np.random.choice([0, np.pi]) - alice_qubit_state = [alice_state] - alice = Agent(numqubits=1, basis_selection=[chosen_basis]) - alice.qubits = [ - Qubit(theta=state, base=chosen_basis) for state in alice_qubit_state - ] - bob = Agent(numqubits=1, basis_selection=[chosen_basis]) - alice.send_quantum(bob, [chosen_basis]) - measured_value = bob.measurements[0] - # This should always be True since they are using the same basis - expected_value = 0 if alice_state == 0 else 1 - self.assertEqual(measured_value, expected_value) - - def test_send_quantum_cross_basis(self): - """ - Test case to verify the sending and measurement of qubits between Alice and Bob - when they use different bases for sending and measuring. - """ - # Alice sends in 0 basis, Bob measures in π/4 basis. - alice = Agent(numqubits=1, basis_selection=[0]) - bob_pi_4 = Agent(numqubits=1, basis_selection=[np.pi / 4]) - alice.send_quantum(bob_pi_4, [np.pi / 4]) - self.assertEqual(len(bob_pi_4.measurements), 1) - # Assert that Bob's measurement is either 0 or 1. - self.assertIn(bob_pi_4.measurements[0], [0, 1]) - - # Now, we test the opposite: Alice sends in π/4 basis, Bob measures in 0 basis. - alice_pi_4 = Agent(numqubits=1, basis_selection=[np.pi / 4]) - bob = Agent(numqubits=1, basis_selection=[0]) - alice_pi_4.send_quantum(bob, [0]) - self.assertEqual(len(bob.measurements), 1) - # Assert that Bob's measurement is either 0 or 1. - self.assertIn(bob.measurements[0], [0, 1]) - - def test_get_keys(self): - """ - Test case to verify the key generation process between Alice and Bob. - """ - numqubits = 10 - alice_bases = np.random.choice([0, np.pi / 4], size=numqubits) - alice_states = np.random.choice([0, np.pi], size=numqubits) - alice = Agent(numqubits=numqubits) - alice.qubits = np.array( - [ - Qubit(theta=state, base=base) - for state, base in zip(alice_states, alice_bases) - ] + def test_measurement_unent(self): + """ + Test case to verify the measurement process of an Agent object with QstateUnEnt. + """ + num_qubits = 2 + qstate = QstateUnEnt(num_qubits=num_qubits, init_method="zeros") + agent = Agent( + priv_qbittype="unentangled", num_priv_qubits=2, init_method="zeros" ) - bob = Agent(numqubits=numqubits) - alice.send_quantum(bob, alice_bases) - alice_key = alice.get_key(alice_bases) - # Bob generates his key after receiving and measuring qubits from Alice - bob_key = bob.get_key(alice_bases) - # Check if Alice's and Bob's keys match - self.assertTrue(np.array_equal(alice_key, bob_key)) + agent_outcome = agent.measure_all(qstate_type="private") + qstate_outcome = qstate.measure_all() + np.testing.assert_array_almost_equal(agent_outcome, qstate_outcome) + + agent_outcome0 = agent.measure(qubit_idx=0, qstate_type="private") + qstate_outcome0 = qstate.measure(qubit_idx=0) + self.assertAlmostEqual(agent_outcome0, qstate_outcome0) + + def test_measurement_ent(self): + """ + Test case to verify the measurement process of an Agent object with QstateEnt. + """ + num_qubits = 2 + qstate = QstateEnt(num_qubits=num_qubits, init_method="zeros") + agent = Agent(priv_qbittype="entangled", num_priv_qubits=2, init_method="zeros") + agent_outcome = agent.measure_all(qstate_type="private", order="simult") + qstate_outcome = qstate.measure_all(order="simult") + np.testing.assert_array_almost_equal(agent_outcome, qstate_outcome) + + agent_outcome0 = agent.measure(qubit_idx=0, qstate_type="private") + qstate_outcome0 = qstate.measure(qubit_idx=0) + self.assertAlmostEqual(agent_outcome0, qstate_outcome0) + + def test_public_qstates_unent(self): + """ + Test case to verify the sharing of QstateUnEnt object works for Agents. + """ + Alice = Agent() + Bob = Agent() + + public_qstate = QstateUnEnt(num_qubits=2, init_method="zeros") + + Alice.set_qstate(qstate=public_qstate, qstate_type="public") + Bob.set_qstate(qstate=public_qstate, qstate_type="public") + + self.assertIs(Alice.qstates["public"], Bob.qstates["public"]) + + Alice.get_key(qstate_type="public") + Bob.get_key(qstate_type="public") + + np.testing.assert_array_almost_equal(Alice.keys["public"], Bob.keys["public"]) + + def test_public_qstate_ent(self): + """ + Test case to verify the sharing of QstateEnt object works for Agents. + """ + Alice = Agent() + Bob = Agent() + + public_qstate = QstateEnt(num_qubits=2, init_method="zeros") + + Alice.set_qstate(qstate=public_qstate, qstate_type="public") + Bob.set_qstate(qstate=public_qstate, qstate_type="public") + + self.assertIs(Alice.qstates["public"], Bob.qstates["public"]) + + Alice.apply_gate(gate=H_gate, qstate_type="public") + + Alice.get_key(qstate_type="public", order="sequential") + Bob.get_key(qstate_type="public", order="sequential") + + np.testing.assert_array_almost_equal(Alice.keys["public"], Bob.keys["public"]) if __name__ == "__main__":