{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1f5ee3e6",
   "metadata": {},
   "source": [
    "# QSilver: DFT y QFT\n",
    "\n",
    "Este notebook desarrolla una exposición autoexplicativa sobre la Transformada Discreta de Fourier (DFT) y la Transformada Cuántica de Fourier (QFT). El objetivo no es memorizar una colección de fórmulas, sino entender por qué Fourier aparece de forma natural cuando la información está codificada en fases, periodicidades o frecuencias.\n",
    "\n",
    "La notación de Dirac se escribe sin macros personalizadas para mantener compatibilidad con JupyterLab, Anaconda y Google Colab. Por ejemplo, se usará:\n",
    "\n",
    "$$\n",
    "\\left|x\\right\\rangle,\\qquad\n",
    "\\left|01\\right\\rangle,\\qquad\n",
    "\\left\\langle x\\right|.\n",
    "$$\n",
    "\n",
    "La DFT se tratará como una transformación matricial sobre vectores complejos. La QFT se tratará como la misma matriz unitaria aplicada al vector de amplitudes de un estado cuántico."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1211a00",
   "metadata": {},
   "source": [
    "## 1. Objetivos de aprendizaje\n",
    "\n",
    "Al finalizar el notebook deberías poder formular la DFT unitaria, interpretar sus coeficientes como proyecciones sobre modos de frecuencia, visualizar raíces de la unidad, construir matrices de Fourier, comparar una DFT clásica con una QFT, derivar la acción de la QFT sobre estados base pequeños y construir circuitos QFT manualmente en Qiskit y Cirq.\n",
    "\n",
    "También deberías poder explicar por qué la QFT no es una forma de imprimir todos los coeficientes de Fourier de un estado cuántico. La QFT aplica un cambio de base sobre amplitudes, pero la medición produce muestras; su valor aparece cuando se integra en un algoritmo que convierte fases o periodicidades en resultados observables."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a061b85",
   "metadata": {},
   "source": [
    "## 2. Prerrequisitos operativos\n",
    "\n",
    "Se asume que ya conoces números complejos, módulo, conjugado, forma polar, amplitudes complejas, regla de Born, matrices unitarias, fase global y fase relativa. Aquí esos conceptos se usan directamente.\n",
    "\n",
    "La idea central del módulo es que las raíces de la unidad funcionan como patrones de comparación. Si una señal o un estado se alinea con un patrón de fase, la suma compleja se refuerza; si no se alinea, las contribuciones se cancelan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ba0c7938",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from math import pi, sqrt\n",
    "\n",
    "np.set_printoptions(precision=4, suppress=True)\n",
    "\n",
    "def format_complex(z: complex, tol: float = 1e-10) -> str:\n",
    "    \"\"\"Representación breve de un número complejo para impresión.\"\"\"\n",
    "    a = 0.0 if abs(z.real) < tol else z.real\n",
    "    b = 0.0 if abs(z.imag) < tol else z.imag\n",
    "    return f\"{a:.4g}{b:+.4g}i\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e017b335",
   "metadata": {},
   "source": [
    "## 3. Raíces de la unidad\n",
    "\n",
    "Para una longitud $N$, la raíz fundamental de la unidad es\n",
    "\n",
    "$$\n",
    "\\omega_N=e^{2\\pi i/N}.\n",
    "$$\n",
    "\n",
    "Sus potencias recorren $N$ puntos igualmente espaciados sobre el círculo unitario. La DFT usa esos puntos como fases de comparación. La geometría de estas raíces explica la cancelación de frecuencias incompatibles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "185067d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_roots_of_unity(N: int = 8) -> None:\n",
    "    roots = np.exp(2j * np.pi * np.arange(N) / N)\n",
    "    plt.figure(figsize=(6, 6))\n",
    "    ax = plt.gca()\n",
    "    ax.axhline(0, color=\"0.4\", linewidth=1)\n",
    "    ax.axvline(0, color=\"0.4\", linewidth=1)\n",
    "    circle = plt.Circle((0, 0), 1, fill=False, linestyle=\"--\", color=\"0.5\")\n",
    "    ax.add_artist(circle)\n",
    "    plt.scatter(roots.real, roots.imag, s=80)\n",
    "    for k, z in enumerate(roots):\n",
    "        plt.text(1.18 * z.real, 1.18 * z.imag, f\"k={k}\", ha=\"center\", va=\"center\")\n",
    "    plt.title(f\"Raíces {N}-ésimas de la unidad\")\n",
    "    plt.xlabel(\"Parte real\")\n",
    "    plt.ylabel(\"Parte imaginaria\")\n",
    "    plt.axis(\"equal\")\n",
    "    plt.grid(alpha=0.25)\n",
    "    plt.show()\n",
    "\n",
    "plot_roots_of_unity(8)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "abcc0ddc",
   "metadata": {},
   "source": [
    "## 4. Definición unitaria de la DFT\n",
    "\n",
    "Usaremos la convención unitaria de la DFT:\n",
    "\n",
    "$$\n",
    "X_k=\\frac{1}{\\sqrt{N}}\\sum_{n=0}^{N-1}x_n e^{2\\pi i kn/N}.\n",
    "$$\n",
    "\n",
    "Esta normalización es importante porque preserva la norma del vector. En computación cuántica, preservar norma no es opcional: una operación cerrada sobre estados debe ser unitaria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21a9c7c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def dft_matrix(N: int, sign: int = +1) -> np.ndarray:\n",
    "    \"\"\"Matriz DFT unitaria de tamaño N.\n",
    "\n",
    "    sign=+1 usa exp(+2πikn/N).\n",
    "    sign=-1 usa exp(-2πikn/N).\n",
    "    \"\"\"\n",
    "    k = np.arange(N).reshape((N, 1))\n",
    "    n = np.arange(N).reshape((1, N))\n",
    "    return np.exp(sign * 2j * np.pi * k * n / N) / np.sqrt(N)\n",
    "\n",
    "\n",
    "def dft(x: np.ndarray, sign: int = +1) -> np.ndarray:\n",
    "    \"\"\"Aplica la DFT unitaria a un vector complejo.\"\"\"\n",
    "    x = np.asarray(x, dtype=complex)\n",
    "    return dft_matrix(len(x), sign=sign) @ x\n",
    "\n",
    "# Verificación de unitariedad para N=8\n",
    "N = 8\n",
    "F = dft_matrix(N)\n",
    "print(\"¿F†F ≈ I?\", np.allclose(F.conj().T @ F, np.eye(N)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a475931d",
   "metadata": {},
   "source": [
    "## 5. Visualización de la matriz DFT\n",
    "\n",
    "La matriz DFT tiene entradas de igual magnitud y fases organizadas de manera regular. La magnitud constante indica que la información no se amplifica ni se atenúa por entrada individual; la estructura real está en las fases."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e4d69e4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_dft_matrix(N: int = 8) -> None:\n",
    "    F = dft_matrix(N)\n",
    "    fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))\n",
    "\n",
    "    im0 = axes[0].imshow(np.abs(F), cmap=\"viridis\")\n",
    "    axes[0].set_title(\"Módulo de la matriz DFT\")\n",
    "    axes[0].set_xlabel(\"n\")\n",
    "    axes[0].set_ylabel(\"k\")\n",
    "    plt.colorbar(im0, ax=axes[0], fraction=0.046)\n",
    "\n",
    "    im1 = axes[1].imshow(np.angle(F), cmap=\"twilight\", vmin=-np.pi, vmax=np.pi)\n",
    "    axes[1].set_title(\"Fase de la matriz DFT\")\n",
    "    axes[1].set_xlabel(\"n\")\n",
    "    axes[1].set_ylabel(\"k\")\n",
    "    plt.colorbar(im1, ax=axes[1], fraction=0.046)\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "plot_dft_matrix(8)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff7425ee",
   "metadata": {},
   "source": [
    "## 6. Ejemplo desarrollado: DFT de dos componentes\n",
    "\n",
    "Para $N=2$, la DFT unitaria es\n",
    "\n",
    "$$\n",
    "F_2=\\frac{1}{\\sqrt{2}}\n",
    "\\begin{pmatrix}\n",
    "1&1\\\\\n",
    "1&-1\n",
    "\\end{pmatrix}.\n",
    "$$\n",
    "\n",
    "Aplicada al vector $(3,4)^T$, separa el modo uniforme y el modo alternante:\n",
    "\n",
    "$$\n",
    "F_2\\begin{pmatrix}3\\\\4\\end{pmatrix}\n",
    "=\n",
    "\\begin{pmatrix}\n",
    "7/\\sqrt{2}\\\\\n",
    "-1/\\sqrt{2}\n",
    "\\end{pmatrix}.\n",
    "$$\n",
    "\n",
    "El primer coeficiente mide suma coherente; el segundo mide contraste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d24b9e16",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.array([3, 4], dtype=complex)\n",
    "X = dft(x)\n",
    "print(\"Entrada:\", x)\n",
    "print(\"DFT unitaria:\", X)\n",
    "\n",
    "plt.figure(figsize=(6, 4))\n",
    "plt.bar([\"x0\", \"x1\"], x.real, alpha=0.7, label=\"entrada\")\n",
    "plt.bar([\"X0\", \"X1\"], X.real, alpha=0.7, label=\"salida DFT\")\n",
    "plt.title(\"DFT unitaria de (3, 4)\")\n",
    "plt.ylabel(\"Valor real\")\n",
    "plt.grid(axis=\"y\", alpha=0.3)\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "326f3618",
   "metadata": {},
   "source": [
    "## 7. Componentes armónicas discretas\n",
    "\n",
    "Las filas de la DFT pueden verse como detectores de frecuencia. En la siguiente figura se muestran partes reales de distintos modos armónicos. Cada modo tiene una velocidad de oscilación distinta; la DFT mide cuánto de cada modo aparece en la señal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19bbd15f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_fourier_modes(N: int = 32, modes=(0, 1, 3, 8)) -> None:\n",
    "    n = np.arange(N)\n",
    "    plt.figure(figsize=(10, 4.5))\n",
    "    for k in modes:\n",
    "        mode = np.exp(2j * np.pi * k * n / N)\n",
    "        plt.plot(n, mode.real, marker=\"o\", markersize=3, label=f\"k={k}\")\n",
    "    plt.title(\"Modos de Fourier: parte real\")\n",
    "    plt.xlabel(\"Índice n\")\n",
    "    plt.ylabel(\"Re(exp(2πikn/N))\")\n",
    "    plt.grid(alpha=0.25)\n",
    "    plt.legend(ncol=len(modes))\n",
    "    plt.show()\n",
    "\n",
    "plot_fourier_modes()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1287f0cc",
   "metadata": {},
   "source": [
    "## 8. Señales y espectros\n",
    "\n",
    "Ahora construiremos una señal discreta formada por dos componentes. En el dominio original se observa una secuencia de valores; en el dominio de Fourier aparecen picos que identifican las frecuencias dominantes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23dbd280",
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 64\n",
    "n = np.arange(N)\n",
    "signal = np.sin(2 * np.pi * 5 * n / N) + 0.6 * np.sin(2 * np.pi * 14 * n / N + 0.4)\n",
    "spectrum = dft(signal)\n",
    "\n",
    "plt.figure(figsize=(10, 4))\n",
    "plt.plot(n, signal, marker=\"o\", markersize=3)\n",
    "plt.title(\"Señal discreta\")\n",
    "plt.xlabel(\"n\")\n",
    "plt.ylabel(\"x[n]\")\n",
    "plt.grid(alpha=0.25)\n",
    "plt.show()\n",
    "\n",
    "plt.figure(figsize=(10, 4))\n",
    "plt.stem(np.arange(N // 2), np.abs(spectrum[:N // 2]))\n",
    "plt.title(\"Magnitud de la DFT\")\n",
    "plt.xlabel(\"k\")\n",
    "plt.ylabel(\"|X[k]|\")\n",
    "plt.grid(alpha=0.25)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f36a7eb9",
   "metadata": {},
   "source": [
    "## 9. Suma geométrica de fases\n",
    "\n",
    "La cancelación de Fourier puede visualizarse como una suma de flechas en el plano complejo. Si las flechas rodean el círculo de forma equilibrada, la suma es pequeña o nula. Si se alinean, la suma es grande."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae3c31e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_phase_sum(N: int = 12, frequency: int = 3) -> None:\n",
    "    phases = np.exp(2j * np.pi * frequency * np.arange(N) / N)\n",
    "    plt.figure(figsize=(6, 6))\n",
    "    ax = plt.gca()\n",
    "    ax.axhline(0, color=\"0.4\", linewidth=1)\n",
    "    ax.axvline(0, color=\"0.4\", linewidth=1)\n",
    "    for z in phases:\n",
    "        plt.arrow(0, 0, z.real, z.imag, head_width=0.03, length_includes_head=True, alpha=0.55)\n",
    "    plt.scatter(phases.real, phases.imag, s=40)\n",
    "    plt.title(f\"Suma de fases para frecuencia {frequency}\")\n",
    "    plt.xlabel(\"Re\")\n",
    "    plt.ylabel(\"Im\")\n",
    "    plt.axis(\"equal\")\n",
    "    plt.grid(alpha=0.25)\n",
    "    plt.show()\n",
    "    print(\"Suma:\", format_complex(np.sum(phases)))\n",
    "\n",
    "plot_phase_sum(12, 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce8af04b",
   "metadata": {},
   "source": [
    "## 10. QFT: misma matriz, distinta operación\n",
    "\n",
    "La QFT usa la matriz DFT unitaria, pero la aplica al vector de amplitudes de un estado cuántico. Para $N=2^n$:\n",
    "\n",
    "$$\n",
    "\\mathcal{F}_N\\left|x\\right\\rangle\n",
    "=\n",
    "\\frac{1}{\\sqrt{N}}\\sum_{y=0}^{N-1}e^{2\\pi ixy/N}\\left|y\\right\\rangle.\n",
    "$$\n",
    "\n",
    "La salida es un estado cuántico. Una medición no devuelve todos los coeficientes; devuelve una muestra de la distribución resultante."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "60624554",
   "metadata": {},
   "outputs": [],
   "source": [
    "def qft_matrix(n_qubits: int, sign: int = +1) -> np.ndarray:\n",
    "    \"\"\"Matriz QFT para n_qubits, usando N=2**n_qubits.\"\"\"\n",
    "    return dft_matrix(2 ** n_qubits, sign=sign)\n",
    "\n",
    "\n",
    "def basis_state(index: int, n_qubits: int) -> np.ndarray:\n",
    "    \"\"\"Vector de estado base computacional.\"\"\"\n",
    "    vec = np.zeros(2 ** n_qubits, dtype=complex)\n",
    "    vec[index] = 1.0\n",
    "    return vec\n",
    "\n",
    "nq = 2\n",
    "F4 = qft_matrix(nq)\n",
    "print(\"Matriz QFT para 2 qubits:\")\n",
    "print(F4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "260d3e9a",
   "metadata": {},
   "source": [
    "## 11. Ejemplo desarrollado: QFT de $\\left|01\\right\\rangle$\n",
    "\n",
    "En dos qubits, $\\left|01\\right\\rangle$ corresponde al índice $1$. La QFT produce:\n",
    "\n",
    "$$\n",
    "\\mathcal{F}_4\\left|01\\right\\rangle\n",
    "=\n",
    "\\frac{1}{2}\\left(\n",
    "\\left|00\\right\\rangle+i\\left|01\\right\\rangle\n",
    "-\\left|10\\right\\rangle-i\\left|11\\right\\rangle\n",
    "\\right).\n",
    "$$\n",
    "\n",
    "Todas las probabilidades son iguales, pero la fase cambia de componente en componente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7317e096",
   "metadata": {},
   "outputs": [],
   "source": [
    "psi_01 = basis_state(1, 2)\n",
    "out_01 = F4 @ psi_01\n",
    "labels_2q = [\"00\", \"01\", \"10\", \"11\"]\n",
    "for label, amp in zip(labels_2q, out_01):\n",
    "    print(f\"|{label}>: {format_complex(amp)}\")\n",
    "\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.bar(labels_2q, np.abs(out_01) ** 2)\n",
    "plt.title(\"Probabilidades tras QFT de |01>\")\n",
    "plt.xlabel(\"Estado base\")\n",
    "plt.ylabel(\"Probabilidad\")\n",
    "plt.ylim(0, 0.35)\n",
    "plt.grid(axis=\"y\", alpha=0.25)\n",
    "plt.show()\n",
    "\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.stem(range(4), np.angle(out_01))\n",
    "plt.xticks(range(4), labels_2q)\n",
    "plt.title(\"Fases de las amplitudes tras QFT de |01>\")\n",
    "plt.xlabel(\"Estado base\")\n",
    "plt.ylabel(\"Fase\")\n",
    "plt.grid(alpha=0.25)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34ab7593",
   "metadata": {},
   "source": [
    "## 12. Paisajes de fase de estados base\n",
    "\n",
    "La QFT de un estado base produce amplitudes de igual módulo y fases con pendiente regular. Esa pendiente depende de la etiqueta de entrada. La información queda codificada como un gradiente de fase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f4c0ed2f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_qft_phase_landscapes(n_qubits: int = 3, xs=(1, 2, 3)) -> None:\n",
    "    N = 2 ** n_qubits\n",
    "    y = np.arange(N)\n",
    "    fig, axes = plt.subplots(len(xs), 1, figsize=(9, 2.5 * len(xs)), sharex=True)\n",
    "    if len(xs) == 1:\n",
    "        axes = [axes]\n",
    "    for ax, x in zip(axes, xs):\n",
    "        state = qft_matrix(n_qubits) @ basis_state(x, n_qubits)\n",
    "        ax.stem(y, np.angle(state))\n",
    "        ax.set_ylabel(f\"x={x}\")\n",
    "        ax.grid(alpha=0.25)\n",
    "    axes[-1].set_xlabel(\"Etiqueta de salida y\")\n",
    "    fig.suptitle(\"Fases en QFT de estados base\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "plot_qft_phase_landscapes()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adaf2508",
   "metadata": {},
   "source": [
    "## 13. Interferencia en la QFT\n",
    "\n",
    "La QFT puede concentrar amplitud cuando las fases de entrada contienen periodicidad. La figura siguiente muestra la interferencia de dos componentes armónicas; la DFT permite separarlas porque cada componente se alinea con un detector distinto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "804e0a16",
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 32\n",
    "n = np.arange(N)\n",
    "wave_1 = np.exp(2j * np.pi * 3 * n / N)\n",
    "wave_2 = np.exp(2j * np.pi * 8 * n / N)\n",
    "combined = wave_1 + wave_2\n",
    "\n",
    "plt.figure(figsize=(10, 4))\n",
    "plt.plot(n, combined.real, label=\"suma\")\n",
    "plt.plot(n, wave_1.real, linestyle=\"--\", alpha=0.6, label=\"k=3\")\n",
    "plt.plot(n, wave_2.real, linestyle=\"--\", alpha=0.6, label=\"k=8\")\n",
    "plt.title(\"Interferencia de componentes armónicas\")\n",
    "plt.xlabel(\"n\")\n",
    "plt.ylabel(\"parte real\")\n",
    "plt.grid(alpha=0.25)\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98d7b956",
   "metadata": {},
   "source": [
    "## 14. Compuertas de fase controlada en QFT\n",
    "\n",
    "La QFT no se construye solo con Hadamards. Para tamaños mayores a dos se necesitan fases fraccionarias condicionadas. La familia $CR_k$ aplica una fase de ángulo\n",
    "\n",
    "$$\n",
    "\\theta_k=\\frac{2\\pi}{2^k}.\n",
    "$$\n",
    "\n",
    "Estas fases construyen, bit a bit, el gradiente de Fourier."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3cdf3d1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def crk_angle(k: int) -> float:\n",
    "    \"\"\"Ángulo de fase de CR_k.\"\"\"\n",
    "    return 2 * np.pi / (2 ** k)\n",
    "\n",
    "ks = np.arange(2, 9)\n",
    "angles = np.array([crk_angle(k) for k in ks])\n",
    "plt.figure(figsize=(8, 4))\n",
    "plt.stem(ks, angles / np.pi)\n",
    "plt.xlabel(\"k\")\n",
    "plt.ylabel(\"Ángulo / π\")\n",
    "plt.title(\"Ángulos de fase controlada CR_k\")\n",
    "plt.xticks(ks)\n",
    "plt.grid(alpha=0.25)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e1adfe1",
   "metadata": {},
   "source": [
    "## 15. Acción de $CR_2$ sobre una superposición\n",
    "\n",
    "Si $CR_2$ aplica fase $e^{i\\pi/2}=i$ al componente $\\left|11\\right\\rangle$, entonces\n",
    "\n",
    "$$\n",
    "\\frac{\\left|01\\right\\rangle+\\left|11\\right\\rangle}{\\sqrt{2}}\n",
    "\\mapsto\n",
    "\\frac{\\left|01\\right\\rangle+i\\left|11\\right\\rangle}{\\sqrt{2}}.\n",
    "$$\n",
    "\n",
    "Las probabilidades de medir $01$ o $11$ no cambian, pero la fase relativa sí. Esa diferencia puede ser convertida en probabilidad por interferencia posterior."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39b06a6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "state = np.zeros(4, dtype=complex)\n",
    "state[1] = 1 / np.sqrt(2)  # |01>\n",
    "state[3] = 1 / np.sqrt(2)  # |11>\n",
    "\n",
    "CR2 = np.eye(4, dtype=complex)\n",
    "CR2[3, 3] = np.exp(1j * np.pi / 2)  # fase sobre |11>\n",
    "new_state = CR2 @ state\n",
    "\n",
    "for label, amp in zip(labels_2q, new_state):\n",
    "    if abs(amp) > 1e-12:\n",
    "        print(f\"|{label}>: {format_complex(amp)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58fc7c0f",
   "metadata": {},
   "source": [
    "## 16. Circuito conceptual de la QFT\n",
    "\n",
    "El circuito QFT procesa qubits de forma incremental. En cada etapa se aplica Hadamard y luego compuertas de fase controlada. Al final, con frecuencia se aplican SWAPs para corregir el orden de lectura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "411cc8ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Dibujo esquemático manual con Matplotlib.\n",
    "plt.figure(figsize=(10, 4))\n",
    "plt.axis(\"off\")\n",
    "y_positions = [0.75, 0.5, 0.25]\n",
    "for i, y in enumerate(y_positions):\n",
    "    plt.plot([0.05, 0.95], [y, y], color=\"black\", linewidth=1.5)\n",
    "    plt.text(0.02, y, f\"q{i}\", ha=\"right\", va=\"center\")\n",
    "\n",
    "def box(x, y, label):\n",
    "    plt.text(x, y, label, ha=\"center\", va=\"center\",\n",
    "             bbox=dict(boxstyle=\"square\", fc=\"#EEF2F7\", ec=\"#2F4056\"))\n",
    "\n",
    "box(0.18, y_positions[0], \"H\")\n",
    "box(0.34, y_positions[1], \"R2\")\n",
    "plt.plot([0.34, 0.34], [y_positions[0], y_positions[1]], color=\"black\")\n",
    "plt.scatter([0.34], [y_positions[0]], color=\"black\", s=25)\n",
    "box(0.48, y_positions[2], \"R3\")\n",
    "plt.plot([0.48, 0.48], [y_positions[0], y_positions[2]], color=\"black\")\n",
    "plt.scatter([0.48], [y_positions[0]], color=\"black\", s=25)\n",
    "box(0.62, y_positions[1], \"H\")\n",
    "box(0.76, y_positions[2], \"R2\")\n",
    "plt.plot([0.76, 0.76], [y_positions[1], y_positions[2]], color=\"black\")\n",
    "plt.scatter([0.76], [y_positions[1]], color=\"black\", s=25)\n",
    "box(0.86, y_positions[2], \"H\")\n",
    "plt.text(0.92, 0.5, \"SWAP\", ha=\"center\", va=\"center\",\n",
    "         bbox=dict(boxstyle=\"round\", fc=\"#FCEFC8\", ec=\"#2F4056\"))\n",
    "plt.title(\"Esquema conceptual de QFT de tres qubits\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a33d357",
   "metadata": {},
   "source": [
    "## 17. QFT manual en NumPy\n",
    "\n",
    "Antes de usar frameworks cuánticos, conviene verificar el operador con matrices. La siguiente función aplica QFT a cualquier estado de $n$ qubits representado como vector NumPy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b7dfca1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def apply_qft_statevector(state: np.ndarray, sign: int = +1) -> np.ndarray:\n",
    "    \"\"\"Aplica QFT a un vector de estado cuya longitud es potencia de dos.\"\"\"\n",
    "    state = np.asarray(state, dtype=complex)\n",
    "    N = len(state)\n",
    "    if N & (N - 1) != 0:\n",
    "        raise ValueError(\"La longitud debe ser potencia de dos\")\n",
    "    return dft_matrix(N, sign=sign) @ state\n",
    "\n",
    "# Verificación: QFT de |01>\n",
    "state_01 = basis_state(1, 2)\n",
    "print(apply_qft_statevector(state_01))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3259ff16",
   "metadata": {},
   "source": [
    "## 18. Qiskit: instalación condicional\n",
    "\n",
    "La siguiente celda intenta importar Qiskit. Si no está disponible, intenta instalarlo. En Google Colab esto permite ejecutar el notebook sin editar manualmente el entorno. En instalaciones locales administradas, puedes omitir la instalación si Qiskit ya está disponible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a7bf1d28",
   "metadata": {},
   "outputs": [],
   "source": [
    "import importlib\n",
    "import subprocess\n",
    "import sys\n",
    "\n",
    "\n",
    "def ensure_package(import_name: str, pip_name: str | None = None) -> bool:\n",
    "    \"\"\"Importa un paquete; si no existe, intenta instalarlo con pip.\"\"\"\n",
    "    try:\n",
    "        importlib.import_module(import_name)\n",
    "        return True\n",
    "    except ImportError:\n",
    "        if pip_name is None:\n",
    "            pip_name = import_name\n",
    "        try:\n",
    "            subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pip_name, \"-q\"])\n",
    "            importlib.import_module(import_name)\n",
    "            return True\n",
    "        except Exception as exc:\n",
    "            print(f\"No se pudo instalar {pip_name}: {exc}\")\n",
    "            return False\n",
    "\n",
    "qiskit_available = ensure_package(\"qiskit\", \"qiskit\")\n",
    "qiskit_aer_available = ensure_package(\"qiskit_aer\", \"qiskit-aer\")\n",
    "print(\"Qiskit disponible:\", qiskit_available)\n",
    "print(\"Qiskit Aer disponible:\", qiskit_aer_available)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db359aa1",
   "metadata": {},
   "source": [
    "## 19. QFT manual en Qiskit\n",
    "\n",
    "La implementación manual aplica Hadamards, fases controladas y SWAPs. Construirla explícitamente permite inspeccionar la estructura que una función de biblioteca podría ocultar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "44da7f9c",
   "metadata": {},
   "outputs": [],
   "source": [
    "if qiskit_available:\n",
    "    from qiskit import QuantumCircuit\n",
    "    try:\n",
    "        from qiskit.quantum_info import Operator, Statevector\n",
    "    except Exception as exc:\n",
    "        print(\"Qiskit está instalado, pero no se pudieron importar Operator/Statevector:\", exc)\n",
    "\n",
    "\n",
    "def qft_qiskit(n: int, do_swaps: bool = True):\n",
    "    \"\"\"Circuito QFT manual en Qiskit.\"\"\"\n",
    "    if not qiskit_available:\n",
    "        print(\"Qiskit no está disponible en este entorno.\")\n",
    "        return None\n",
    "    qc = QuantumCircuit(n, name=\"QFT\")\n",
    "    for j in range(n):\n",
    "        qc.h(j)\n",
    "        for m in range(j + 1, n):\n",
    "            angle = np.pi / (2 ** (m - j))\n",
    "            qc.cp(angle, m, j)\n",
    "    if do_swaps:\n",
    "        for j in range(n // 2):\n",
    "            qc.swap(j, n - 1 - j)\n",
    "    return qc\n",
    "\n",
    "qc_qft3 = qft_qiskit(3)\n",
    "if qc_qft3 is not None:\n",
    "    display(qc_qft3.draw(\"text\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9c9d399",
   "metadata": {},
   "source": [
    "## 20. Comparación entre matriz QFT y circuito Qiskit\n",
    "\n",
    "Para pocos qubits podemos comparar la matriz de QFT construida con NumPy y la matriz del circuito. Dependiendo de la convención de orden de qubits, puede ser necesario considerar inversión de bits; por eso el análisis de orden es parte de la verificación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6ed2eed",
   "metadata": {},
   "outputs": [],
   "source": [
    "if qiskit_available and qc_qft3 is not None:\n",
    "    U_qiskit = Operator(qc_qft3).data\n",
    "    print(\"Matriz del circuito Qiskit, forma:\", U_qiskit.shape)\n",
    "    print(\"La matriz es unitaria:\", np.allclose(U_qiskit.conj().T @ U_qiskit, np.eye(8)))\n",
    "else:\n",
    "    print(\"Se omite comparación porque Qiskit no está disponible.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f3f36fb",
   "metadata": {},
   "source": [
    "## 21. Cirq: instalación condicional\n",
    "\n",
    "La construcción en Cirq usa una idea equivalente. Las compuertas controladas de fase pueden expresarse con `CZPowGate`, cuyo exponente controla la fase aplicada al componente activado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "caacb752",
   "metadata": {},
   "outputs": [],
   "source": [
    "cirq_available = ensure_package(\"cirq\", \"cirq\")\n",
    "print(\"Cirq disponible:\", cirq_available)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a334469",
   "metadata": {},
   "source": [
    "## 22. QFT manual en Cirq\n",
    "\n",
    "En Cirq, una fase controlada compatible con $CR_k$ puede construirse mediante `cirq.CZPowGate(exponent=2/2**k)`. El exponente determina la fase sobre el componente controlado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06c8ec8f",
   "metadata": {},
   "outputs": [],
   "source": [
    "if cirq_available:\n",
    "    import cirq\n",
    "\n",
    "\n",
    "def qft_cirq(n: int, do_swaps: bool = True):\n",
    "    \"\"\"Circuito QFT manual en Cirq.\"\"\"\n",
    "    if not cirq_available:\n",
    "        print(\"Cirq no está disponible en este entorno.\")\n",
    "        return None\n",
    "    qubits = cirq.LineQubit.range(n)\n",
    "    circuit = cirq.Circuit()\n",
    "    for j in range(n):\n",
    "        circuit.append(cirq.H(qubits[j]))\n",
    "        for m in range(j + 1, n):\n",
    "            k = m - j + 1\n",
    "            gate = cirq.CZPowGate(exponent=2 / (2 ** k))\n",
    "            circuit.append(gate.on(qubits[m], qubits[j]))\n",
    "    if do_swaps:\n",
    "        for j in range(n // 2):\n",
    "            circuit.append(cirq.SWAP(qubits[j], qubits[n - 1 - j]))\n",
    "    return circuit\n",
    "\n",
    "qc_cirq3 = qft_cirq(3)\n",
    "if qc_cirq3 is not None:\n",
    "    print(qc_cirq3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3db41c43",
   "metadata": {},
   "source": [
    "## 23. Visualización de $CR_k$ como fase fraccionaria\n",
    "\n",
    "La jerarquía $CR_2,CR_3,CR_4,\\ldots$ introduce rotaciones cada vez más pequeñas. Estas fases finas son necesarias para reconstruir la fase total de Fourier en registros con varios qubits."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb5b95b5",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 4))\n",
    "ks = np.arange(2, 10)\n",
    "angles = [crk_angle(k) for k in ks]\n",
    "plt.plot(ks, np.array(angles) / np.pi, marker=\"o\")\n",
    "plt.xlabel(\"k\")\n",
    "plt.ylabel(\"ángulo / π\")\n",
    "plt.title(\"Jerarquía de fases en CR_k\")\n",
    "plt.grid(alpha=0.25)\n",
    "plt.show()\n",
    "\n",
    "for k, a in zip(ks, angles):\n",
    "    print(f\"CR_{k}: ángulo = {a/np.pi:.6f}π\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18417801",
   "metadata": {},
   "source": [
    "## 24. DFT vs QFT: comparación visual\n",
    "\n",
    "La DFT produce un vector clásico de coeficientes. La QFT produce un estado cuántico. La matriz es la misma, pero el acceso a la información es distinto: el estado cuántico debe medirse, y la medición no entrega todos los coeficientes simultáneamente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9ce5d98",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(9, 4))\n",
    "plt.axis(\"off\")\n",
    "\n",
    "def label_box(x, y, text):\n",
    "    plt.text(x, y, text, ha=\"center\", va=\"center\",\n",
    "             bbox=dict(boxstyle=\"round\", fc=\"#EEF2F7\", ec=\"#2F4056\"))\n",
    "\n",
    "label_box(0.15, 0.72, \"Vector clásico x\")\n",
    "label_box(0.50, 0.72, \"Matriz DFT F_N\")\n",
    "label_box(0.85, 0.72, \"Coeficientes X\")\n",
    "plt.annotate(\"\", xy=(0.38, 0.72), xytext=(0.25, 0.72), arrowprops=dict(arrowstyle=\"->\"))\n",
    "plt.annotate(\"\", xy=(0.73, 0.72), xytext=(0.60, 0.72), arrowprops=dict(arrowstyle=\"->\"))\n",
    "\n",
    "label_box(0.15, 0.30, \"Estado cuántico\")\n",
    "label_box(0.50, 0.30, \"Operador QFT\")\n",
    "label_box(0.85, 0.30, \"Estado transformado\")\n",
    "plt.annotate(\"\", xy=(0.38, 0.30), xytext=(0.25, 0.30), arrowprops=dict(arrowstyle=\"->\"))\n",
    "plt.annotate(\"\", xy=(0.73, 0.30), xytext=(0.60, 0.30), arrowprops=dict(arrowstyle=\"->\"))\n",
    "plt.title(\"DFT y QFT: misma matriz, distinta lectura operacional\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4edf5a33",
   "metadata": {},
   "source": [
    "## 25. Ejercicios guiados\n",
    "\n",
    "### Ejercicio 1: DFT de dos componentes\n",
    "\n",
    "Calcular e interpretar la DFT unitaria de $(5,1)^T$.\n",
    "\n",
    "El primer coeficiente representa el modo uniforme:\n",
    "\n",
    "$$\n",
    "(5+1)/\\sqrt{2}=6/\\sqrt{2}.\n",
    "$$\n",
    "\n",
    "El segundo representa el contraste alternante:\n",
    "\n",
    "$$\n",
    "(5-1)/\\sqrt{2}=4/\\sqrt{2}.\n",
    "$$\n",
    "\n",
    "La salida no debe interpretarse como dos valores nuevos sin contexto; representa la descomposición del vector en modos de Fourier."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35da0041",
   "metadata": {},
   "source": [
    "### Ejercicio 2: QFT de $\\left|10\\right\\rangle$\n",
    "\n",
    "Para dos qubits, $\\left|10\\right\\rangle$ corresponde a $x=2$. La fase de salida es\n",
    "\n",
    "$$\n",
    "e^{2\\pi i(2)y/4}=e^{\\pi i y}.\n",
    "$$\n",
    "\n",
    "Por tanto las fases alternan $1,-1,1,-1$ y la salida es\n",
    "\n",
    "$$\n",
    "\\frac{1}{2}\\left(\n",
    "\\left|00\\right\\rangle-\n",
    "\\left|01\\right\\rangle+\n",
    "\\left|10\\right\\rangle-\n",
    "\\left|11\\right\\rangle\n",
    "\\right).\n",
    "$$\n",
    "\n",
    "La información de la etiqueta se codifica en el patrón de fase, no en las magnitudes."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afa27d93",
   "metadata": {},
   "source": [
    "### Ejercicio 3: fase de $CR_4$\n",
    "\n",
    "La familia $CR_k$ usa el ángulo\n",
    "\n",
    "$$\n",
    "\\theta_k=\\frac{2\\pi}{2^k}.\n",
    "$$\n",
    "\n",
    "Para $k=4$:\n",
    "\n",
    "$$\n",
    "\\theta_4=\\frac{2\\pi}{16}=\\frac{\\pi}{8}.\n",
    "$$\n",
    "\n",
    "Esta fase corresponde a una corrección fina. En la QFT, las fases pequeñas codifican contribuciones de bits menos significativos."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c35f16a1",
   "metadata": {},
   "source": [
    "## 26. Ejercicios propuestos\n",
    "\n",
    "1. Calcula la DFT unitaria de $(1,0,0,0)^T$ e interpreta el resultado como distribución uniforme de fases.\n",
    "2. Calcula la QFT de $\\left|11\\right\\rangle$ para dos qubits y explica el patrón de fases.\n",
    "3. Construye la matriz DFT unitaria de tamaño $4$ y verifica numéricamente que es unitaria.\n",
    "4. Modifica el circuito QFT en Qiskit para omitir los SWAPs y compara la matriz resultante.\n",
    "5. En Cirq, cambia el exponente de una compuerta controlada de fase y observa cómo cambia la matriz del circuito."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73206faa",
   "metadata": {},
   "source": [
    "## 27. Resumen final\n",
    "\n",
    "La DFT es un cambio de base que expresa un vector discreto en componentes de frecuencia. Su estructura se basa en raíces de la unidad, sumas de fases y ortogonalidad de modos.\n",
    "\n",
    "La QFT aplica la misma matriz unitaria al vector de amplitudes de un estado cuántico. Su circuito evita construir una matriz exponencialmente grande mediante Hadamards, fases controladas y SWAPs.\n",
    "\n",
    "La QFT no debe entenderse como un método para imprimir todos los coeficientes de Fourier de un estado. Su valor aparece como subrutina dentro de algoritmos que codifican información en fases o periodicidad y luego usan interferencia para hacerla observable."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
