{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9979b6b6-97df-48ec-9efb-f7b97bb00e23",
   "metadata": {},
   "source": [
    "# Quantum teleportation and dense coding with GHZ and W states"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8897e34e",
   "metadata": {},
   "source": [
    "*Each task requires you to do use the code proposed below, or, more directly, use Qiskit*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f911d904-7146-4bc0-9d41-0eed75b5cd75",
   "metadata": {},
   "source": [
    "## 1. Can standard teleportation be efficiently done with GHZ state?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8121fa9f-af9a-444c-8e8b-9e585cadda29",
   "metadata": {},
   "source": [
    "In standard quantum teleportation, Alice and Bob share a Bell pair, and Alice can teleport an unknown qubit state to Bob using two classical bits.\n",
    "\n",
    "In this exercise, you will explore how teleportation can (or cannot) be achieved using a **GHZ state** shared among three parties — Alice, Bob, and Charlie.\n",
    "\n",
    "A 3-qubit GHZ state is defined as:\n",
    "\n",
    "$ |\\text{GHZ}\\rangle = \\frac{1}{\\sqrt{2}} (|000\\rangle + |111\\rangle). $\n",
    "\n",
    "### Tasks:\n",
    "1. Prepare a GHZ state.\n",
    "2. Build the state with Qubit layout: $ |\\psi\\rangle |\\text{ghz}_a\\,\\, \\text{ghz}_c\\, \\,\\text{ghz}_b\\rangle $. $|\\psi\\rangle=\\alpha|0\\rangle + \\beta|1\\rangle$ (qubit 0) is the state Alice wants to send. We place GHZ on qubits 1,2,3 where Alice holds qubit 1, Charlie qubit 2, Bob qubit 3. We give Alice access to $|\\psi\\rangle$ and her GHZ qubit ($|\\psi\\rangle$ and $|\\text{ghz}_a\\rangle$), while Bob holds $|\\text{ghz}_b\\rangle$. Charlie holds $|\\text{ghz}_c\\rangle$ but we will consider the case where Charlie does **not** cooperate.\n",
    "3. Let Alice perform a Bell-basis measurement on ($|\\psi\\rangle$, $|\\text{ghz}_a\\rangle$) — same as in standard teleportation.\n",
    "4. Condition on Alice's measurement outcome, allow Bob to apply the usual Pauli corrections **based only on Alice's classical bits**, but *without* any action from Charlie.\n",
    "5. Examine Bob's reduced state and compute fidelity with the original $|\\psi\\rangle$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e5d3f31f-11d5-4c66-957e-fb8642da273d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from numpy import sqrt, exp, pi\n",
    "from qiskit.quantum_info import Statevector, partial_trace, DensityMatrix\n",
    "from itertools import product"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9da80a8a-d643-429f-8541-1d134ee39c6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# single qubit state |psi> parameterized by angles (theta, phi)\n",
    "def single_qubit_state(theta=0.7, phi=0.3):\n",
    "    return np.array([np.cos(theta/2), np.exp(1j*phi)*np.sin(theta/2)], dtype=complex)\n",
    "\n",
    "# prepare initial global state: |psi> (q0) ⊗ |GHZ(1,2,3)>\n",
    "def prepare_global_state(psi_vec):\n",
    "    # GHZ on qubits 1,2,3: (|000> + |111>)/sqrt(2)\n",
    "    ghz = (1/sqrt(2)) * np.array([1,0,0,0,0,0,0,1], dtype=complex)  # 3 qubits\n",
    "    #global_state = kronecker product the two parts to have 1 ⊗ 3 = 4 qubits total\n",
    "    return global_state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e0e2f56-9389-45b6-8157-01f66f36873e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Pauli matrices and Bell basis projectors\n",
    "X = np.array([[0,1],[1,0]], dtype=complex)\n",
    "# define Y, Z, I similarly\n",
    "\n",
    "# Bell states\n",
    "phi_plus = (1/sqrt(2)) * np.array([1,0,0,1], dtype=complex)\n",
    "#define phi_minus, psi_plus and psi_minus similarly\n",
    "bells = [phi_plus, phi_minus, psi_plus, psi_minus]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ecd5af9d-f80c-41d4-950f-9a7102953a31",
   "metadata": {},
   "outputs": [],
   "source": [
    "#define an arbitrary single qubit state and the resultant 4 qubit resource\n",
    "# psi_vec =   single_qubit_state(?)\n",
    "# global_state = prepare_global_state(?)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aacb07b5-418f-4cad-92b9-ec8bd8f25777",
   "metadata": {},
   "outputs": [],
   "source": [
    "# projector onto Bell state for qubits (0,1) in a 4-qubit system: build as kron(P_bell, I_2, I_2)\n",
    "def bell_projector_on_01(bvec):\n",
    "    P = np.outer(bvec.conj(), bvec)\n",
    "    # embed into 4-qubit space: qubits 0 and 1 are the Bell qubits\n",
    "    return np.kron(P, np.kron(I, I))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "736d0e3d-58e2-479e-bd3c-91e70caf3055",
   "metadata": {},
   "outputs": [],
   "source": [
    "# fidelity between pure psi and density matrix rho on a single qubit\n",
    "def fidelity_single_qubit(psi_vec, rho):\n",
    "    # psi_vec is vector, rho is 2x2 density matrix\n",
    "    return np.real(np.vdot(psi_vec, rho @ psi_vec))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eac4cd58-a8a9-4b8b-9aff-10f221dcf783",
   "metadata": {},
   "outputs": [],
   "source": [
    "results = []\n",
    "for idx, bvec in enumerate(bells):\n",
    "    P = bell_projector_on_01(bvec)\n",
    "    # Project global state onto Bell outcome (unnormalized)\n",
    "    proj_state = P @ global_state\n",
    "    prob = np.vdot(proj_state, proj_state).real\n",
    "    if prob < 1e-12:\n",
    "        continue\n",
    "    # Normalize post-measurement global state\n",
    "    proj_state = proj_state / sqrt(prob)\n",
    "    # After Alice's measurement outcome idx, usually Bob would apply a correction depending on idx:\n",
    "    # Mapping: phi_plus -> I, phi_minus -> Z, psi_plus -> X, psi_minus -> XZ (up to phase)\n",
    "    if idx == 0:  # hint: phi_plus -> I (no correction)\n",
    "        corr = np.kron(np.eye(1), np.kron(np.eye(1), np.eye(2)))  # placeholder (unused)\n",
    "        # For 4-qubit vector, we will apply correction on qubit 3 (Bob's qubit)\n",
    "        corr_on_bob = ?\n",
    "    elif idx == 1: \n",
    "        corr_on_bob = ?\n",
    "    elif idx == 2: \n",
    "        corr_on_bob = ?\n",
    "    else:  \n",
    "        corr_on_bob = ?\n",
    "\n",
    "    # Apply correction on qubit 3: form operator I(0) ⊗ I(1) ⊗ I(2) ⊗ corr_on_bob\n",
    "    Ucorr = np.kron(np.eye(8), corr_on_bob)  # 4x4 ⊗ 2x2 = 8x8\n",
    "    state_after_corr = Ucorr @ proj_state\n",
    "\n",
    "    # Now compute Bob's reduced state by tracing out qubits 0,1,2 -> keep qubit 3\n",
    "    # reshape to 4-qubit density and partial trace\n",
    "    rho = np.outer(state_after_corr, state_after_corr.conj())\n",
    "    # trace out qubits 0,1,2 by reshaping\n",
    "    rho_bob = np.zeros((2,2), dtype=complex)\n",
    "    # indices: total 4 qubits -> size 16; iterate basis states and sum over traced subsystems\n",
    "    for i in range(16):\n",
    "        for j in range(16):\n",
    "            # bitstrings for i and j\n",
    "            bi = format(i, '04b')\n",
    "            bj = format(j, '04b')\n",
    "            # keep only elements where first three bits equal (we are tracing them)\n",
    "            if bi[0:3] == bj[0:3]:\n",
    "                # accumulate contributions to bob density matrix element (last bit)\n",
    "                ib = int(bi[3])\n",
    "                jb = int(bj[3])\n",
    "                rho_bob[ib, jb] += rho[i, j]\n",
    "    F = fidelity_single_qubit(psi_vec, rho_bob)\n",
    "    results.append((idx, prob, F))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cdc0f4c0-34bf-4373-a52b-bc63680ac547",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Print results\n",
    "print(\"Bell outcome index, probability, fidelity of Bob's reduced state with original |psi>\")\n",
    "for r in results:\n",
    "    print(r)\n",
    "# compute average fidelity over outcomes (weighted)\n",
    "avgF = sum(prob * F for (_, prob, F) in results) / sum(prob for (_, prob, F) in results)\n",
    "print(\"\\nAverage fidelity (with no Charlie cooperation) = {:.6f}\".format(avgF))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b50ec18-7e2c-4466-82db-3d5ab45ef500",
   "metadata": {},
   "source": [
    "6. why is the fidelity <1 without Charlie's intervention?\n",
    "7. If instead of Charlie, the third qubit of the GHZ state actually modelizes the entanglement of the qubits a and b with the environment (and therefore, this qubit is not accessible to any operator), conclude on the qualitative effect of this entanglement (i.e. a kind of noise) on the teleportation protocol."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af6f34ca-e8c7-458a-9d0d-7c5c6452a869",
   "metadata": {},
   "source": [
    "## 2. Can superdense coding (particularly the case of two classical bits sent by sharing one qubit) be efficiently done with GHZ and W states?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb713949-6169-4386-81b3-7ba5c25dcb56",
   "metadata": {},
   "source": [
    "Examining the original superdense coding architecture, the basic principle is that Alice, who wants to transmit two classical bits of information, performs local unitaries on her qubit and four orthogoonal states (Bell states) are produced. We'll explicitly test whether Alice's local unitaries on her single qubit can produce 4 mutually orthogonal global states when three qubit GHZ and W states are used as resources. \n",
    "\n",
    "The W state is defined as:\n",
    "$ |W\\rangle = \\frac{1}{\\sqrt{3}} (|100\\rangle + |010\\rangle + |001\\rangle) $\n",
    "\n",
    "### Tasks:\n",
    "\n",
    "1. Build the GHZ and W states on 3 qubits (labelled 0, 1, 2).\n",
    "2. On Alice's qubit (qubit 0) apply the four usual encoding unitaries: $I, X, Z, Y=(iXZ)$ on both the GHZ and W states.\n",
    "3. Compute the eight resulting global statevectors and form the Gram matrix of overlaps to test orthogonality (for W state, repeat and observe that the states are not mutually orthogonal).\n",
    "4. Conclude."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4c3e23c3-8041-41ed-8aa0-ff7a65f7f86d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Build GHZ state with ordering |q0 q1 q2> where index = q0*4 + q1*2 + q2\n",
    "ghz = np.zeros(8, dtype=complex)\n",
    "ghz[0] = 1.0\n",
    "ghz[7] = 1.0\n",
    "ghz = ghz / np.linalg.norm(ghz)   # (|000> + |111>)/sqrt(2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d46234d-983a-4ab4-b43b-13d9e15ce4ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Build W state similar to above (|100> + |010> + |001>)/sqrt(3)\n",
    "#w = np.zeros(...)\n",
    "#...\n",
    "#...\n",
    "#...\n",
    "w = w / np.linalg.norm(w)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afb9a1ce-616b-422d-b1f7-fea8c798f2b3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Alice has qubit 0; Bob have qubits 1 and 2\n",
    "# Define encoding unitaries on Alice's qubit: I, X, Z, XZ\n",
    "encs = [I, X, Z, X @ Z]\n",
    "enc_names = ['I','X','Z','XZ']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a54dcbed-5db8-43e3-8b95-471aaaf9c653",
   "metadata": {},
   "outputs": [],
   "source": [
    "def apply_on_alice(U, state):\n",
    "    # apply U on qubit 0 of 3-qubit state: U ⊗ I ⊗ I\n",
    "    return np.kron(U, np.kron(I, I)) @ state\n",
    "\n",
    "def gram_matrix(states):\n",
    "    n = len(states)\n",
    "    G = np.zeros((n,n), dtype=complex)\n",
    "    for i in range(n):\n",
    "        for j in range(n):\n",
    "            G[i,j] = np.vdot(states[i], states[j])\n",
    "    return G"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c382467-de08-4e1e-aa48-9deaa1b51b41",
   "metadata": {},
   "outputs": [],
   "source": [
    "# GHZ case\n",
    "#ghz_states =?... apply single qubit unitaries U on qubit 0 and identities on qubit 1 and 2\n",
    "#G_ghz = ?... calculate the Gram matrix\n",
    "print(\"GHZ Gram matrix (absolute values):\\n\", np.round(np.abs(G_ghz), 6))\n",
    "\n",
    "# Check if GHZ states are mutually orthogonal\n",
    "orth_ghz = np.allclose(G_ghz, np.diag(np.diag(G_ghz)), atol=1e-8)\n",
    "print(\"GHZ: are encoded states orthogonal?\", orth_ghz)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf918757-50b0-4a2c-9417-9b4af1064e7c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# W case\n",
    "# go through the same steps as above, are the encoded states orthogonal?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa73f58f-e008-481a-8719-1fb0d0da86a3",
   "metadata": {},
   "source": [
    "## 3. Can superdense coding of three classical bits be done by sharing two qubits with the GHZ state?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f28e9220-0d48-49b3-995f-d4ca311e35bb",
   "metadata": {},
   "source": [
    "If Alice now has access to two qubits of the GHZ state (the 0th and 1st qubit) and Bob holds the last qubit (2nd), then Alice can send three classical bits by encoding them in an orthogonal set of 8 states.​ Conceptually, this means Alice can performs any two qubit unitaries from $\\{I,X,Y,Z\\}_0\\otimes \\{I,X,Y,Z\\}_1$ (or any 1 and 2 qubits gates, but we restrict ourselves to Pauli in this exercise) on her qubits and the result should contain 8 orthogonal states. \n",
    "\n",
    "1. Apply the all the Pauli unitaries above to the GHZ state."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17fad278-4ee5-4ad2-9dd2-7de7ac1e6d0c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# kronecker for two-qubit operator acting on qubits (0,1) with qubit order q0 q1 q2.\n",
    "def apply_on_alice(U_ab, state):\n",
    "    # U_ab is 4x4 acting on qubits 0 and 1; full operator is U_ab ⊗ I (Bob qubit is last)\n",
    "    #return [?]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16795955-84a7-4e27-99c8-85638e0edf70",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Build two-qubit operator list: all tensor products of single-qubit Paulis\n",
    "single_paulis = [('I',I), ('X',X), ('Y',Y), ('Z',Z)]\n",
    "op_list = []\n",
    "op_names = []\n",
    "for (n1, M1), (n2, M2) in product(single_paulis, repeat=2):\n",
    "    name = f\"{n1}⊗{n2}\"\n",
    "    op = np.kron(M1, M2)\n",
    "    op_list.append(op)\n",
    "    op_names.append(name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5bd76511-b934-462a-8f25-7a61360235d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Compute encoded states\n",
    "encoded_states = [apply_on_alice(U, ghz) for U in op_list]\n",
    "# Ensure normalization (should be unitary applied to normalized state)\n",
    "for v in encoded_states:\n",
    "    assert np.allclose(np.linalg.norm(v), 1.0, atol=1e-9)\n",
    "\n",
    "# Compute Gram matrix\n",
    "G = np.array([[np.vdot(sj, si) for sj in encoded_states] for si in encoded_states], dtype=complex)\n",
    "absG = np.round(np.abs(G), 8)\n",
    "print(\"Absolute Gram matrix (|<psi_i|psi_j>|):\")\n",
    "print(absG)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8d11f30-3f84-4950-b0f8-17bfdf45a7d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Rank (how many orthogonal states)\n",
    "# Use SVD tolerance\n",
    "u,s,vt = np.linalg.svd(G)\n",
    "rank = np.sum(s > 1e-8)\n",
    "print(\"\\n Numeric rank of Gram matrix (max # orthogonal states):\", rank)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c02eb77-1751-45af-a145-57ff3b756b05",
   "metadata": {},
   "source": [
    "Understandably, not all 16 combinations are needed, and 8 are enough. \n",
    "\n",
    "2. Find such a set."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9deee2d-6260-4ed8-90b7-4b970f42c034",
   "metadata": {},
   "outputs": [],
   "source": [
    "# pick an orthogonal subset\n",
    "orth_indices = []\n",
    "tol = 1e-8\n",
    "for i in range(len(encoded_states)):\n",
    "    v = encoded_states[i]\n",
    "    ok = True\n",
    "    for j in orth_indices:\n",
    "        ov = abs(np.vdot(encoded_states[j], v))\n",
    "        if ov > tol:\n",
    "            ok = False\n",
    "            break\n",
    "    if ok:\n",
    "        orth_indices.append(i)\n",
    "    if len(orth_indices) >= rank:\n",
    "        break\n",
    "\n",
    "print(\"\\n Found orthogonal subset of size\", len(orth_indices))\n",
    "for k in orth_indices:\n",
    "    print(\" -\", op_names[k])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f66cbad",
   "metadata": {},
   "source": [
    "3. Conclude"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
