{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "2b1776f4-c259-494e-a33d-1ebb0459426c",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Diagonalización cuántica de Krylov\"\n",
        "description: \"Se describe la diagonalización cuántica de Krylov (KQD), partiendo de los métodos clásicos de Krylov. Se analizan la convergencia y el uso intensivo de recursos.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore Hndt longrightarrow Bigg Jörg Liesen Zdenek Strakos Yousef Saad MINRES vstar nabla */}\n",
        "\n",
        "<span id=\"krylov-quantum-diagonalization\" />\n",
        "\n",
        "# Diagonalización cuántica de Krylov\n",
        "\n",
        "En esta lección sobre la diagonalización cuántica de Krylov (KQD) responderemos a lo siguiente:\n",
        "\n",
        "* ¿Qué es, en general, el método de Krylov?\n",
        "* ¿Por qué funciona el método de Krylov y en qué condiciones?\n",
        "* ¿Qué papel desempeña la informática cuántica?\n",
        "\n",
        "La parte cuántica de los cálculos se basa en gran medida en el trabajo de la Ref [\\[1\\]](#references).\n",
        "\n",
        "El siguiente vídeo ofrece una visión general de los métodos de Krylov en computación clásica, motiva su uso y explica cómo la computación cuántica puede desempeñar un papel en esa corriente de trabajo. El texto siguiente ofrece más detalles e implementa un método de Krylov tanto de forma clásica como utilizando un ordenador cuántico.\n",
        "\n",
        "<IBMVideo id=\"134325510\" title=\"En este vídeo, Chris Porter ofrece una visión general de los métodos clásicos de Krylov y explica por qué son útiles. Explica cómo la computación cuántica puede desempeñar un papel en ese enfoque de la diagonalización. El texto que figura a continuación ofrece más detalles.\" />\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a9ba88fb-734e-44d8-ada4-c588e2654921",
      "metadata": {},
      "source": [
        "<span id=\"1-introduction-to-krylov-methods\" />\n",
        "\n",
        "## 1. Introducción a los métodos de Krylov\n",
        "\n",
        "Un **método de subespacio de Krylov** puede referirse a cualquiera de varios métodos creados alrededor de lo que se denomina el **subespacio de Krylov**. Una revisión completa de estos está más allá del alcance de esta lección, pero Ref [\\[2-4\\]](#references) todos pueden dar sustancialmente más antecedentes. Aquí nos centraremos en qué es un subespacio de Krylov, cómo y por qué es útil para resolver problemas de valores propios y, por último, cómo puede implementarse en un ordenador cuántico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9b47a985-ec1d-41e2-8516-6cfcd7713e5f",
      "metadata": {},
      "source": [
        "**Definición:** Dada una matriz $N\\times N$ simétrica, semidefinida positiva $A$, el espacio de Krylov $\\mathcal{K}^r$ de orden $r$ es el espacio abarcado por los vectores obtenidos multiplicando las potencias superiores de una matriz $A$, hasta $r-1\\leq N$, por un vector de referencia $\\vert v \\rangle$.\n",
        "\n",
        "$$\n",
        "\\mathcal{K}^r = \\text{span}\\left\\{ \\vert v \\rangle, A \\vert v \\rangle, A^2 \\vert v \\rangle, ..., A^{r-1} \\vert v \\rangle \\right\\}\n",
        "$$\n",
        "\n",
        "Aunque los vectores anteriores abarcan lo que llamamos un subespacio de Krylov, no hay razón para pensar que serán ortogonales. A menudo se utiliza un proceso iterativo de ortonormalización similar a la **ortogonalización de Gram-Schmidt**. En este caso, el proceso es ligeramente distinto, ya que cada nuevo vector se hace ortogonal a los demás a medida que se genera. En este contexto se denomina **iteración de Arnoldi**. Partiendo del vector inicial $|v\\rangle$, se genera el siguiente vector $A|v\\rangle$, y luego se asegura que este segundo vector es ortogonal al primero restando su proyección sobre $|v\\rangle$. Es decir\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "|v_0\\rangle &=\\frac{|v\\rangle}{\\left|\\left| |v\\rangle \\right|\\right|}\\\\\n",
        "\n",
        "|v_1\\rangle &=\\frac{A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle}{\\left|\\left|A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle \\right|\\right|}\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "Ahora es fácil ver que $|v_0\\rangle \\perp |v_1\\rangle,$ ya que\n",
        "\n",
        "$$\n",
        "\\langle v_0 | v_1\\rangle=\\frac{\\langle v_0 | A|v\\rangle-\\langle v_0 |A|v\\rangle\\langle v_0|v_0\\rangle}{\\left|\\left| A|v\\rangle-\\langle A|v\\rangle|v_0\\rangle |v_0\\rangle \\right|\\right|}=0\n",
        "$$\n",
        "\n",
        "Hacemos lo mismo con el siguiente vector, asegurándonos de que es ortogonal a los dos anteriores:\n",
        "\n",
        "$$\n",
        "|v_2\\rangle=\\frac{A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle}{\\left|\\left| A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle\\right|\\right|}\n",
        "$$\n",
        "\n",
        "Si repetimos este proceso para todos los vectores de $r$, tendremos una base ortonormal completa para un espacio de Krylov. Nótese que el proceso de ortogonalización aquí dará cero una vez $r>m$, ya que $m$ vector ortogonal necesariamente abarca todo el espacio. El proceso también dará cero si algún vector es un vector propio de $A$, ya que todos los vectores posteriores serán múltiplos de ese vector.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5351576c-bc3c-4e21-8839-9c14eb10747f",
      "metadata": {},
      "source": [
        "<span id=\"11-a-simple-example-krylov-by-hand\" />\n",
        "\n",
        "### 1.1 Un ejemplo sencillo: Krylov a mano\n",
        "\n",
        "Veamos paso a paso la generación de un subespacio de Krylov sobre una matriz trivialmente pequeña, para que podamos ver el proceso. Partimos de una matriz inicial $A$ que nos interesa:\n",
        "\n",
        "$$\n",
        "A=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Para este pequeño ejemplo, podemos determinar los vectores y valores propios fácilmente incluso a mano. Mostramos aquí la solución numérica.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "49017c87-a971-4d2d-b6dc-b8ec9b64184a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The eigenvalues are  [2.58578644 4.         5.41421356]\n",
            "The eigenvectors are  [[ 5.00000000e-01 -7.07106781e-01  5.00000000e-01]\n",
            " [ 7.07106781e-01  1.37464400e-16 -7.07106781e-01]\n",
            " [ 5.00000000e-01  7.07106781e-01  5.00000000e-01]]\n"
          ]
        }
      ],
      "source": [
        "# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use\n",
        "# linalg.eig in this lesson.\n",
        "\n",
        "import numpy as np\n",
        "\n",
        "A = np.array([[4, -1, 0], [-1, 4, -1], [0, -1, 4]])\n",
        "eigenvalues, eigenvectors = np.linalg.eig(A)\n",
        "print(\"The eigenvalues are \", eigenvalues)\n",
        "print(\"The eigenvectors are \", eigenvectors)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a0f96bc7-fef9-48c7-858a-5b44772d710f",
      "metadata": {},
      "source": [
        "Los registramos aquí para su posterior comparación:\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "a_0&=2.59,&|0\\rangle&=&\\begin{pmatrix}1/2\\\\-\\sqrt{2}/2\\\\1/2\\end{pmatrix}\\\\\n",
        "\\\\\n",
        "a_1&=4,&|1\\rangle&=&\\begin{pmatrix}\\sqrt{2}/2\\\\0\\\\-\\sqrt{2}/2\\end{pmatrix}\\\\\n",
        "\\\\\n",
        "a_2&=5.41,&|2\\rangle&=&\\begin{pmatrix}1/2\\\\\\sqrt{2}/2\\\\1/2\\end{pmatrix}\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7f237ba1-d380-4553-b5bc-94422b4f39c1",
      "metadata": {},
      "source": [
        "Nos gustaría estudiar cómo funciona (o falla) este proceso a medida que aumentamos la dimensión de nuestro subespacio de Krylov, $r$. Para ello, aplicaremos este proceso:\n",
        "\n",
        "* Generar un subespacio del espacio vectorial completo a partir de un vector elegido al azar $|v\\rangle$ (llamarlo $|v_0\\rangle$ si ya está normalizado, como arriba).\n",
        "* Proyecte la matriz completa $A$ en ese subespacio y encuentre los valores propios de esa matriz proyectada $\\tilde{A}$.\n",
        "* Aumente el tamaño del subespacio generando más vectores, asegurándose de que son ortonormales, mediante un proceso similar a la ortogonalización Gram-Schmidt.\n",
        "* Proyecte $A$ en el subespacio mayor y encuentre los valores propios de la matriz resultante, $\\tilde{A}$.\n",
        "* Repita esta operación hasta que los valores propios converjan (o, en este caso de juguete, hasta que haya generado vectores que abarquen todo el espacio vectorial de la matriz original $A$ ).\n",
        "\n",
        "Una implementación normal del método de Krylov no necesitaría resolver el problema de valores propios para la matriz proyectada en cada subespacio de Krylov a medida que se construye. Se podría construir el subespacio de la dimensión deseada, proyectar la matriz sobre ese subespacio y diagonalizar la matriz proyectada. La proyección y diagonalización en cada dimensión del subespacio sólo se realiza para comprobar la convergencia.\n",
        "\n",
        "<span id=\"dimension-$r=1$\" />\n",
        "\n",
        "#### $r=1$ de las dimensiones:\n",
        "\n",
        "Elegimos un vector aleatorio, por ejemplo\n",
        "\n",
        "$$\n",
        "|v_0\\rangle=\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Si aún no está normalizado, normalícelo.\n",
        "\n",
        "Ahora proyectamos nuestra matriz $A$ en el subespacio de este único vector:\n",
        "\n",
        "$$\n",
        "\\tilde{A}_0=\\langle v_0| A|v_0\\rangle=\\begin{pmatrix}1&0&0\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=(4)\n",
        "$$\n",
        "\n",
        "Esta es nuestra proyección de la matriz sobre nuestro subespacio de Krylov cuando contiene un único vector, $|v_0\\rangle$. El valor propio de esta matriz es trivialmente 4. Podemos pensar en esto como nuestra estimación de orden cero de los valores propios (en este caso sólo uno) de $A$. Aunque es una estimación pobre, es el orden de magnitud correcto.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a0eeb9b1-86a5-4bc8-8651-66588aea7686",
      "metadata": {},
      "source": [
        "<span id=\"dimension-$r=2$\" />\n",
        "\n",
        "#### $r=2$ de las dimensiones:\n",
        "\n",
        "Ahora generamos el siguiente vector en nuestro subespacio mediante la operación con $A$ sobre el vector anterior:\n",
        "\n",
        "$$\n",
        "A|v_0\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Ahora restamos la proyección de este vector sobre nuestro vector anterior para asegurar la ortogonalidad.\n",
        "\n",
        "$$\n",
        "|v_1\\rangle=A|v_0\\rangle-\\langle v_0 |A|v_0\\rangle|v_0\\rangle\n",
        "$$\n",
        "\n",
        "$$\n",
        "|v_1\\rangle=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}-\\begin{pmatrix}1& 0& 0\\end{pmatrix}\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Si aún no está normalizado, normalícelo. En este caso, el vector ya estaba normalizado, por lo que\n",
        "\n",
        "$$\n",
        "|v_1\\rangle=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Ahora proyectamos nuestra matriz A en el subespacio de estos dos vectores:\n",
        "\n",
        "$$\n",
        "\\tilde{A}_1= \\begin{pmatrix} 1&0&0\\\\0&-1&0 \\end{pmatrix} \\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0\\\\0&-1\\\\0&0\\end{pmatrix}=\\begin{pmatrix}1&0&0\\\\0&-1&0\\end{pmatrix}\\begin{pmatrix}4&1\\\\-1&-4\\\\0&1\\end{pmatrix}=\\begin{pmatrix}4&1\\\\1&4\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Nos queda el problema de determinar los valores propios de esta matriz. Pero esta matriz es ligeramente más pequeña que la matriz completa. En problemas que implican matrices muy grandes, trabajar con este subespacio más pequeño puede resultar muy ventajoso.\n",
        "\n",
        "$$\n",
        "\\det(\\tilde{A_1}-\\lambda I)=0\n",
        "$$\n",
        "\n",
        "$$\n",
        "\\begin{vmatrix} 4-\\lambda&1\\\\1&4-\\lambda\\end{vmatrix} =(4-\\lambda)^2-1=0\n",
        "$$\n",
        "\n",
        "$$\n",
        "4-\\lambda=±1→\\lambda=3,5\n",
        "$$\n",
        "\n",
        "Aunque sigue sin ser una buena estimación, es mejor que la estimación de orden cero. Realizaremos una iteración más para asegurarnos de que el proceso está claro. Sin embargo, esto debilita el objetivo del método, ya que acabaremos diagonalizando una matriz 3x3 en la siguiente iteración, lo que significa que no hemos ahorrado tiempo ni potencia de cálculo.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43ef6c6d-ba18-4e8f-94df-83c404d0015a",
      "metadata": {},
      "source": [
        "<span id=\"dimension-$r=3$\" />\n",
        "\n",
        "#### $r=3$ de las dimensiones:\n",
        "\n",
        "Ahora generamos el siguiente vector en nuestro subespacio mediante la operación con A sobre el vector anterior:\n",
        "\n",
        "$$\n",
        "A|v_1\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Ahora restamos la proyección de este vector sobre nuestros dos vectores anteriores para asegurar la ortogonalidad.\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "|v_2\\rangle&=A|v_1\\rangle-\\langle v_0 |A|v_1\\rangle|v_0\\rangle-\\langle v_1 |A|v_1\\rangle|v_1\\rangle\\\\\n",
        "|v_2\\rangle&=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}-\\begin{pmatrix}1& 0& 0 \\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}-\\begin{pmatrix}0&-1& 0\\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "Si aún no está normalizado, normalícelo. En este caso, el vector ya estaba normalizado, por lo que\n",
        "\n",
        "$$\n",
        "|v_2 \\rangle=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "Ahora proyectamos nuestra matriz $A$ en el subespacio de estos vectores:\n",
        "\n",
        "$$\n",
        "\\tilde{A}_2=\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&-1&0\\\\1&-4&1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&1&0\\\\1&4&1\\\\0&1&4\\end{pmatrix}\n",
        "$$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5f19b90b-93fa-4ccb-b8a2-3bbd55e4dab0",
      "metadata": {},
      "source": [
        "Ahora determinamos los valores propios:\n",
        "\n",
        "$$\n",
        "\\det(\\tilde{A}_2-\\lambda I)=0\n",
        "$$\n",
        "\n",
        "$$\n",
        "\\begin{vmatrix}4-\\lambda&1&0\\\\1&4-\\lambda&1\\\\0&1&4-\\lambda\\end{vmatrix} = (4-\\lambda)((4-\\lambda)^2-1)-(4-\\lambda)=0\\\\\n",
        "$$\n",
        "\n",
        "$$\n",
        "4-\\lambda=0,4-\\lambda=±2^{1/2}→\\lambda=4-2^{1/2},4,4+2^{1/2}≈2.59,4,5.41\n",
        "$$\n",
        "\n",
        "Estos valores propios son exactamente los valores propios de la matriz original $A$. Este debe ser el caso, ya que hemos ampliado nuestro subespacio de Krylov para que abarque todo el espacio vectorial de la matriz original $A$.\n",
        "\n",
        "En este ejemplo, el método de Krylov puede no parecer particularmente más fácil que la diagonalización directa. De hecho, como veremos en secciones posteriores, el método de Krylov sólo es ventajoso a partir de una determinada dimensión de matriz; con ello se pretende ayudarnos a resolver problemas de valores propios/vectores propios de matrices extremadamente grandes.\n",
        "\n",
        "![Imagen que muestra una matriz muy grande proyectada en un subespacio de Krylov, es decir, filas de vectores de Krylov formando una matriz a la izquierda, un Hamiltoniano, y luego columnas de vectores de Krylov a la derecha.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig1.avif)\n",
        "\n",
        "Este es el único ejemplo que mostraremos trabajado \"a mano\", pero en la sección 2 se muestran ejemplos computacionales.\n",
        "\n",
        "<span id=\"clarification-of-terms\" />\n",
        "\n",
        "#### Aclaración de términos\n",
        "\n",
        "Un error común es creer que existe un único subespacio de Krylov para un problema determinado. Pero claro, como hay muchos vectores iniciales a los que se podría aplicar nuestra matriz, hay muchos subespacios de Krylov posibles. Sólo utilizaremos la expresión \"**el** subespacio de Krylov\" para referirnos a un subespacio de Krylov específico ya definido para un ejemplo concreto. Para los planteamientos generales de resolución de problemas nos referiremos a \"**un** subespacio de Krylov\".\n",
        "Una última aclaración es que es válido referirse a un \" **espacio de** Krylov\". A menudo se le denomina \" **subespacio** de Krylov\" por su uso en el contexto de la proyección de matrices de un espacio inicial a un subespacio. En consonancia con ese contexto, aquí nos referiremos principalmente a él como subespacio.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f3117328-61e0-45a2-b8f2-05356d210fae",
      "metadata": {},
      "source": [
        "<span id=\"check-your-understanding\" />\n",
        "\n",
        "#### Comprueba tu comprensión\n",
        "\n",
        "Explique por qué no es (a) útil, y (b) posible extender la dimensión del subespacio de Krylov $r$ más allá de la dimensión $N$ de la matriz de interés.\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Respuesta\">\n",
        "    (a) Dado que estamos ortonormalizando los vectores a medida que los generamos, un conjunto de $N$ tales vectores formará una base completa, lo que significa que una combinación lineal de ellos puede utilizarse para crear cualquier vector del espacio.\n",
        "\n",
        "    (b) El proceso de ortogonalización consiste en restar la proyección de un nuevo vector sobre todos los vectores anteriores. Si todos los vectores anteriores generan el espacio vectorial completo, al restar las proyecciones en el subespacio completo siempre obtendremos un vector nulo.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Imaginemos que un compañero investigador está mostrando cómo se aplica el método de Krylov a una pequeña matriz de ejemplo. ¿Hay algún problema con la elección de la matriz $A$ y el vector inicial $|\\psi\\rangle$?\n",
        "\n",
        "$$\n",
        "A=\\begin{pmatrix}2&1&3\\\\1&2&3\\\\3&3&5\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "y\n",
        "\n",
        "$$\n",
        "|\\psi\\rangle=\\frac{1}{\\sqrt{2}}\\begin{pmatrix}1\\\\-1\\\\0\\end{pmatrix}.\n",
        "$$\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Respuesta\">\n",
        "    Su colega ha elegido accidentalmente un vector propio para su vector inicial. Actuar con la matriz sobre el vector inicial simplemente devolverá el mismo vector, escalado por el valor propio. Esto no generará un subespacio de dimensión creciente. Aconseje a su colega que seleccione un vector inicial diferente, asegurándose de que no sea un vector propio.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Aplica el método de Krylov a la matriz dada, seleccionando un nuevo vector inicial adecuado. Anota las estimaciones del valor propio mínimo en los órdenes 0 y 1 de tu subespacio de Krylov.\n",
        "\n",
        "$$\n",
        "A=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\n",
        "$$\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Respuesta\">\n",
        "    Hay muchas respuestas posibles en función de la elección del vector inicial. Vamos a elegir:\n",
        "\n",
        "    $$\n",
        "    |v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}.\n",
        "    $$\n",
        "\n",
        "    Para obtener $|v_1\\rangle$ aplicamos $A$ una vez a $|v_0\\rangle$, y luego hacemos $|v_1\\rangle$ ortogonal a $|v_0\\rangle.$\n",
        "\n",
        "    $$\n",
        "    A|v_0\\rangle=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\n",
        "    $$\n",
        "\n",
        "    $$\n",
        "    A|v_0\\rangle - \\langle v_0|A|v_0\\rangle |v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix} - \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}-\\frac{7}{3}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}=\\sqrt{\\frac{3}{2}}\\begin{pmatrix}-1/3\\\\2/3\\\\-1/3\\end{pmatrix}\n",
        "    $$\n",
        "\n",
        "    En orden 0, la proyección sobre nuestro subespacio de Krylov es\n",
        "\n",
        "    $$\n",
        "    \\langle v_0|A|v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{7}{3}\n",
        "    $$\n",
        "\n",
        "    En 1er orden, la proyección sobre este subespacio de Krylov es\n",
        "\n",
        "    $$\n",
        "    \\langle V^1|A|V^1\\rangle=\\begin{pmatrix}\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}\\\\-\\sqrt{\\frac{1}{6}}&\\sqrt{\\frac{2}{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\begin{pmatrix}\\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\\\\\frac{1}{\\sqrt{3}}& \\sqrt{\\frac{2}{3}} \\\\  \\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix}\n",
        "    $$\n",
        "\n",
        "    Esto se puede hacer a mano, pero es más fácil con numpy:\n",
        "\n",
        "    ```python\n",
        "    import numpy as np\n",
        "    vstar = np.array([[1/np.sqrt(3),1/np.sqrt(3),1/np.sqrt(3)],[-1/np.sqrt(6),np.sqrt(2/3),-1/np.sqrt(6)]]\n",
        "    )\n",
        "    A = np.array([[1, 1, 0],\n",
        "                  [1, 1, 1],\n",
        "                  [0, 1, 1]])\n",
        "    v = np.array([[1/np.sqrt(3),-1/np.sqrt(6)],[1/np.sqrt(3),np.sqrt(2/3)],[1/np.sqrt(3),-1/np.sqrt(6)]])\n",
        "    proj = vstar@A@v\n",
        "    print(proj)\n",
        "    eigenvalues, eigenvectors = np.linalg.eig(proj)\n",
        "    print(\"The eigenvalues are \", eigenvalues)\n",
        "    print(\"The eigenvectors are \", eigenvectors)\n",
        "    ```\n",
        "\n",
        "    outputs:\n",
        "\n",
        "    ```python\n",
        "    [[ 2.33333333  0.47140452]\n",
        "     [ 0.47140452 -0.33333333]]\n",
        "    The eigenvalues are  [ 2.41421356 -0.41421356]\n",
        "    The eigenvectors are  [[ 0.98559856 -0.16910198]\n",
        "     [ 0.16910198  0.98559856]]\n",
        "    ```\n",
        "\n",
        "    La estimación del valor propio mínimo es -0.414.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5a4f969e-d346-485a-9e4c-a04dbc9492e9",
      "metadata": {},
      "source": [
        "<span id=\"12-types-of-krylov-methods\" />\n",
        "\n",
        "### 1.2 Tipos de métodos de Krylov\n",
        "\n",
        "los \"métodos de subespacios de Krylov\" pueden referirse a cualquiera de las diversas técnicas iterativas utilizadas para resolver grandes sistemas lineales y problemas de valores propios. Todas ellas tienen en común que construyen una solución aproximada a partir de un subespacio de Krylov\n",
        "\n",
        "$\\mathcal{K}^r(A,|v\\rangle ) = \\text{span}\\{|v\\rangle, A|v\\rangle, A^2|v\\rangle, ..., A^{r-1}|v\\rangle\\},$\n",
        "\n",
        "donde $|v\\rangle$ es la conjetura inicial (ver Ref [\\[5\\]](#references) ). Se diferencian en cómo eligen la mejor aproximación de este subespacio, equilibrando factores como la velocidad de convergencia, el uso de memoria y el coste computacional global. El objetivo de esta lección es aprovechar la computación cuántica en el contexto de los métodos de subespacios de Krylov; una discusión exhaustiva de estos métodos queda fuera de su alcance. Las breves definiciones que figuran a continuación sólo sirven de contexto e incluyen algunas referencias para investigar más a fondo estos métodos.\n",
        "\n",
        "**El método del gradiente conjugado (CG** ): Este método se utiliza para resolver sistemas lineales simétricos y definidos positivamente [\\[6\\]](#references). Minimiza la norma A del error en cada iteración, por lo que es particularmente eficaz para sistemas derivados de EDP elípticas discretizadas [\\[7\\]](#references). Utilizaremos este enfoque en la siguiente sección para explicar por qué un subespacio de Krylov sería un subespacio eficaz para buscar soluciones mejoradas a los sistemas lineales.\n",
        "\n",
        "**El método del residuo mínimo generalizado (GMRES** ): Está diseñado para resolver sistemas lineales generales no simétricos. Minimiza la norma residual sobre un espacio de Krylov en cada iteración, lo que lo hace robusto pero potencialmente intensivo en memoria para sistemas grandes [\\[7\\]](#references).\n",
        "\n",
        "**El método del residuo mínimo (MINRES** ): Este método se utiliza para resolver sistemas lineales indefinidos simétricos. Es similar al GMRES pero aprovecha la simetría matricial para reducir el coste computacional [\\[8\\]](#references).\n",
        "\n",
        "Otros enfoques destacables son el **método de ortogonalización completa (FOM)**, estrechamente relacionado con el método de Arnoldi para problemas de valores propios, el **método de gradiente bi-conjugado ( BiCG** ) y el **método de reducción de dimensión inducida (IDR)**.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "774fa41b-9fbb-46cc-9c64-37f7a008e161",
      "metadata": {},
      "source": [
        "<span id=\"13-why-the-krylov-subspace-method-works\" />\n",
        "\n",
        "### 1.3 ¿Por qué funciona el método del subespacio de Krylov?\n",
        "\n",
        "Aquí motivaremos que el método del subespacio de Krylov debería ser una forma eficiente de aproximar los valores propios de la matriz mediante el refinamiento iterativo de las aproximaciones del vector propio, a través de la lente del descenso más pronunciado. Argumentaremos que dada una conjetura inicial de un estado base, el espacio de correcciones sucesivas a esa conjetura inicial que produce la convergencia más rápida es un subespacio de Krylov. No llegamos a una prueba rigurosa del comportamiento de convergencia.\n",
        "\n",
        "Supongamos que nuestra matriz de interés $A$ es simétrica y definida positiva. Esto hace que nuestro argumento sea más relevante para el método CG anterior. Aquí no hacemos suposiciones sobre la dispersión; tampoco afirmamos que $A$ deba ser hermitiano (lo que tiene que ser si es un hamiltoniano).\n",
        "\n",
        "Típicamente deseamos resolver un problema de la forma\n",
        "\n",
        "$$\n",
        "A|x\\rangle=|b\\rangle.\n",
        "$$\n",
        "\n",
        "Uno podría imaginar que $|b\\rangle=c|x\\rangle$ donde $c$ es alguna constante, como en un problema de valor propio. Pero el enunciado de nuestro problema sigue siendo más general por ahora.\n",
        "\n",
        "Partimos de un vector $|x_0\\rangle$ que es una solución aproximada. Aunque existen paralelismos entre esta conjetura $|x_0\\rangle$ y $|v_0\\rangle$ en la sección 1.1, aquí no los aprovechamos.  Nuestra conjetura $|x_0\\rangle$ tiene error, que llamamos $|e_0\\rangle:$\n",
        "\n",
        "$$\n",
        "|e_0\\rangle:=|x\\rangle−|x_0\\rangle.\n",
        "$$\n",
        "\n",
        "También definimos el residuo $R_0:$\n",
        "\n",
        "$$\n",
        "|R_0\\rangle=|b\\rangle−A|x_0\\rangle.\n",
        "$$\n",
        "\n",
        "Aquí utilizamos la mayúscula $R$ para distinguir el residuo de la dimensión de nuestro subespacio de Krylov $r$.\n",
        "\n",
        "![Un vector propio verdadero etiquetado como x, una estimación etiquetada como x₀ y una representación gráfica del error entre ambos.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig2.avif)\n",
        "\n",
        "Ahora queremos hacer un paso de corrección de la forma\n",
        "\n",
        "$$\n",
        "|x_1\\rangle=|x_0\\rangle+|p_0\\rangle,\n",
        "$$\n",
        "\n",
        "lo que esperamos que mejore nuestra aproximación. Aquí $|p_0\\rangle$ es algún vector aún por determinar. Sea $|e_1\\rangle$ el error después de la corrección. Entonces\n",
        "\n",
        "$$\n",
        "|e_1\\rangle=|x\\rangle−|x_1\\rangle=|x\\rangle−(|x_0\\rangle+|p_0\\rangle)=|e_0\\rangle−|p_0\\rangle.\n",
        "$$\n",
        "\n",
        "![Un vector propio verdadero y una actualización de la estimación inicial. La estimación actualizada se acerca más al vector propio real.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig3.avif)\n",
        "\n",
        "Nos interesa saber cómo se comporta nuestro error cuando es transformado por nuestra matriz. Así pues, calculemos la $A$ -norma del error. Es decir\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "∥|e_0\\rangle−|p_0\\rangle∥_A^2&=\\left(\\langle e_0|A−\\langle p_0|A\\right)\\left(|e_0\\rangle−|p_0\\rangle\\right)\\\\\n",
        " & = \\langle e_0|A|e_0 \\rangle − \\langle e_0|A|p_0\\rangle − \\langle p_0|A|e_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n",
        " & = \\langle e_0|A|e_0\\rangle−2\\langle e_0|A|p_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n",
        " & = d−2\\langle R_0|p_0\\rangle +\\langle p_0|A|p_0\\rangle,\n",
        " \\end{aligned}\n",
        "$$\n",
        "\n",
        "donde hemos utilizado la simetría de $A$ y también que $A |e_0\\rangle = |R_0\\rangle.$ Aquí $d$ es alguna constante independiente de $|p_0\\rangle$. Como se mencionó en la Sección 1.2, la $A$ -norma del error no es la única cantidad que podríamos elegir para minimizar, pero es una buena. Queremos ver cómo varía esta cantidad con nuestra elección de vectores de corrección $|p_0\\rangle.$ Así que definimos la función $f$ estableciendo\n",
        "\n",
        "$$\n",
        "f(|p_0\\rangle)=\\langle p_0|A|p_0\\rangle−2\\langle R_0|p_0\\rangle+d.\n",
        "$$\n",
        "\n",
        "$f$ no es más que el error $|e_1\\rangle$ en función de la corrección $|p_0\\rangle$ medida en la $A$ -norma. Por lo tanto, queremos elegir $|p_0\\rangle$ de forma que $f(|p_0\\rangle)$ sea lo más pequeño posible. Para ello, calculamos el gradiente de $f$. Utilizando la simetría de $A$ tenemos\n",
        "\n",
        "$$\n",
        "\\nabla f(|p_0\\rangle) = 2(A|p_0\\rangle−|R_0\\rangle).\n",
        "$$\n",
        "\n",
        "El gradiente apunta en la dirección del ascenso más pronunciado, lo que significa que su opuesto nos da la dirección en la que la función disminuye más: la dirección del **descenso más pronunciado**. En nuestra conjetura inicial $|x_0\\rangle$, donde $|p_0\\rangle=0$, tenemos que $\\nabla f(0) = -2|R_0\\rangle.$ Por lo tanto, la función $f$ es la que más decrece en la dirección del residuo $|R_0\\rangle.$ Así que nuestra elección inicial se beneficiaría más de la adición del vector $|p_0\\rangle=\\alpha_0 |R_0\\rangle$ por algún escalar $\\alpha_0$.\n",
        "\n",
        "En el siguiente paso, elegimos, de nuevo, un vector $|p_1\\rangle$ y añadimos su valor a la aproximación actual. Usando el mismo argumento que antes elegimos $|p_1\\rangle = \\alpha_1 |R_1\\rangle$ para algún escalar $\\alpha_1$. Continuamos de esta manera, de forma que la iteración $k^\\text{th}$ de nuestro vector es\n",
        "\n",
        "$$\n",
        "|x_{k+1}\\rangle=|x_0\\rangle+\\alpha_0 |R_0\\rangle+\\alpha_1 |R_1\\rangle+⋯+\\alpha_k |R_k\\rangle.\n",
        "$$\n",
        "\n",
        "Equivalentemente, queremos construir el espacio del que elegimos nuestras estimaciones mejoradas añadiendo $|R_0\\rangle$, $|R_1\\rangle$, y así sucesivamente, en orden. El vector estimado $k^\\text{th}$ se encuentra en\n",
        "\n",
        "$$\n",
        "|x_{k+1}\\rangle\\in |x_0\\rangle+\\text{span}\\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}.\n",
        "$$\n",
        "\n",
        "Ahora, utilizando la relación que\n",
        "\n",
        "$$\n",
        "|R_{k+1}\\rangle=|b\\rangle−A |x_{k+1}\\rangle=|b\\rangle−A(|x_k\\rangle+\\alpha_k |R_k\\rangle)=|R_k\\rangle−\\alpha_k A |R_k\\rangle,\n",
        "$$\n",
        "\n",
        "vemos que\n",
        "\n",
        "$$\n",
        "\\text{span} \\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}=\\text{span} \\{|R_0\\rangle,A|R_0\\rangle,…,A^{k}|R_0\\rangle \\}.\n",
        "$$\n",
        "\n",
        "Es decir, el espacio que construimos que se aproxima más eficientemente a la solución correcta $|x\\rangle$ es exactamente el espacio construido por la operación sucesiva de la matriz $A$ en $|R_0\\rangle.$. Un subespacio de Krylov *es* el espacio abarcado por los vectores de las direcciones sucesivas del descenso más pronunciado.\n",
        "\n",
        "Por último, reiteramos que no hemos hecho afirmaciones numéricas sobre el escalado de este enfoque, ni hemos discutido el beneficio comparativo para matrices dispersas. Esto sólo pretende motivar el uso de los métodos de subespacios de Krylov, y añadir algo de sentido intuitivo para ellos. A continuación exploraremos numéricamente el comportamiento de estos métodos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2df5e11f-62fe-4237-bcaf-36dcc88f11a7",
      "metadata": {},
      "source": [
        "<span id=\"check-your-understanding\" />\n",
        "\n",
        "#### Comprueba tu comprensión\n",
        "\n",
        "En el flujo de trabajo anterior, propusimos minimizar la $A$ -norma del error. ¿Qué otras cantidades se podrían minimizar al buscar el estado fundamental y su valor propio?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Respuesta\">\n",
        "    Se podría imaginar utilizar el vector residual en lugar de la $A$ -norma del error. Puede haber casos en los que sea útil considerar el propio vector de error.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "56bcf069-b3b5-4c51-b91f-7e9bb62c5c94",
      "metadata": {},
      "source": [
        "<span id=\"2-krylov-methods-in-classical-computation\" />\n",
        "\n",
        "## 2. Métodos de Krylov en la computación clásica\n",
        "\n",
        "En esta sección implementamos computacionalmente las iteraciones de Arnoldi para poder aprovechar un subespacio de Krylov en la resolución de problemas de valores propios. Primero lo aplicaremos a un ejemplo a pequeña escala y, a continuación, examinaremos cómo se escala el tiempo de cálculo a medida que aumenta el tamaño de la matriz de interés. Una idea clave aquí será que la generación de los vectores que abarcan el espacio de Krylov contribuirá en gran medida al tiempo total de cálculo necesario. La memoria necesaria varía según el método de Krylov. Pero las restricciones de memoria pueden limitar el uso de los métodos tradicionales de Krylov.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "952459b6-75dd-4044-8cec-7004fbc48ddf",
      "metadata": {},
      "source": [
        "<span id=\"21-simple-small-scale-example\" />\n",
        "\n",
        "### 2.1 Ejemplo sencillo a pequeña escala\n",
        "\n",
        "En el proceso de creación de un subespacio de Krylov, necesitaremos ortonormalizar los vectores de nuestro subespacio. Definamos una función que tome un vector establecido de nuestro subespacio `vknown` (que no se supone normalizado) y un vector candidato para añadirlo a nuestro subespacio `vnext` y hacer que `vnext` sea ortogonal a `vknown` y normalizado. Definamos además una función que recorra este proceso para todos los vectores establecidos en nuestro subespacio de Krylov para garantizar un conjunto totalmente ortonormal.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "fb4f96ea-906c-4819-a9cb-8e9409cac051",
      "metadata": {},
      "outputs": [],
      "source": [
        "# vknown is some established vector in our subspace. vnext is one we wish to add,\n",
        "# which must be orthogonal to vknown.\n",
        "\n",
        "\n",
        "def orthog_pair(vknown, vnext):\n",
        "    vknown = vknown / np.sqrt(vknown.T @ vknown)\n",
        "    diffvec = vknown.T @ vnext * vknown\n",
        "    vnext = vnext - diffvec\n",
        "    return vnext\n",
        "\n",
        "\n",
        "# v is the candidate vector to be added to our subspace. s is the existing subspace.\n",
        "\n",
        "\n",
        "def orthoset(v, s):\n",
        "    v = v / np.sqrt(v.T @ v)\n",
        "    temp = v\n",
        "    for i in range(len(s)):\n",
        "        temp = orthog_pair(s[i], temp)\n",
        "    v = temp / np.sqrt(temp.T @ temp)\n",
        "    return v"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "61a2704e-3985-4bd4-a9a9-253cc96c708b",
      "metadata": {},
      "source": [
        "Definamos ahora una función que construya iterativamente un subespacio de Krylov cada vez mayor, hasta que el espacio de vectores de Krylov abarque el espacio completo de la matriz original. Esto nos permitirá ver hasta qué punto los valores propios obtenidos mediante nuestro método del subespacio de Krylov coinciden con los valores exactos, en función de la dimensión del subespacio de Krylov. Es importante destacar que nuestra función `krylov_full_build` devuelve los vectores de Krylov, los hamiltonianos proyectados, los valores propios y el tiempo necesario.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "818cceee-bd88-472a-8af9-0a6c0dec5646",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Necessary imports and definitions to track time in microseconds\n",
        "import time\n",
        "\n",
        "\n",
        "def time_mus():\n",
        "    return int(time.time() * 1000000)\n",
        "\n",
        "\n",
        "# This function constructs a Krylov subspace that spans the whole space of the original matrix.\n",
        "#     Input:\n",
        "#       v0          : initial vector\n",
        "#       matrix      : original matrix to be diagonalized\n",
        "#     Output:\n",
        "#       ks          : Krylov vectors\n",
        "#       Hs          : projected Hamiltonians\n",
        "#       eigs        : eigenvalues\n",
        "#       k_tot_times : time required for the operation\n",
        "\n",
        "\n",
        "def krylov_full_build(v0, matrix):\n",
        "    t0 = time_mus()\n",
        "    b = v0 / np.sqrt(v0 @ v0.T)\n",
        "    A = matrix\n",
        "    ks = []\n",
        "    ks.append(b)\n",
        "    Hs = []\n",
        "    eigs = []\n",
        "    Hs.append(b.T @ A @ b)\n",
        "    eigs.append(np.array([b.T @ A @ b]))\n",
        "    k_tot_times = []\n",
        "\n",
        "    for j in range(len(A) - 1):\n",
        "        vec = A @ ks[j].T\n",
        "        ortho = orthoset(vec, ks)\n",
        "        ks.append(ortho)\n",
        "        ksarray = np.array(ks)\n",
        "        Hs.append(ksarray @ A @ ksarray.T)\n",
        "        eigs.append(np.linalg.eig(Hs[j + 1]).eigenvalues)\n",
        "        k_tot_times.append(time_mus() - t0)\n",
        "\n",
        "    # Return the Krylov vectors, the projected Hamiltonians, the eigenvalues,\n",
        "    # and the total time required.\n",
        "    return (ks, Hs, eigs, k_tot_times)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4cd7bdcc-65fd-4199-b9fd-d0a9f62cba17",
      "metadata": {},
      "source": [
        "Probaremos esto en una matriz que sigue siendo bastante pequeña, pero más grande de lo que querríamos hacer a mano.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "3e2aa939-b568-4982-b64a-9f2e4b0da2b5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Define our small test matrix\n",
        "test_matrix = np.array(\n",
        "    [\n",
        "        [4, -1, 0, 1, 0],\n",
        "        [-1, 4, -1, 2, 1],\n",
        "        [0, -1, 4, 3, 3],\n",
        "        [1, 2, 3, 4, 0],\n",
        "        [0, 1, 3, 0, 4],\n",
        "    ]\n",
        ")\n",
        "\n",
        "# Give the test matrix and an initial guess as arguments in the function defined above.\n",
        "# Calculate outputs.\n",
        "test_ks, test_Hs, test_eigs, text_k_tot_times = krylov_full_build(\n",
        "    np.array([0.5, 0.5, 0, 0.5, 0.5]), test_matrix\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2f74fcf0-8959-4862-b33d-c8b1eddda2a8",
      "metadata": {},
      "source": [
        "Podemos comprobar nuestras funciones asegurándonos de que en el último paso (cuando el espacio de Krylov es el espacio vectorial completo de la matriz original) los valores propios del método de Krylov coinciden exactamente con los de la diagonalización numérica exacta:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "704db2f6-5081-404d-9ae9-7264a62ea432",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[-1.36956923  8.43756009  2.9040308   5.34436028  4.68361806]\n",
            "[-1.36956923  8.43756009  2.9040308   4.68361806  5.34436028]\n"
          ]
        }
      ],
      "source": [
        "print(np.linalg.eig(test_matrix).eigenvalues)\n",
        "print(test_eigs[len(test_matrix) - 1])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f0ddf041-d7e8-4131-bb4c-3310d8351c2c",
      "metadata": {},
      "source": [
        "Eso fue un éxito. Por supuesto, lo que realmente importa es lo buena que es nuestra aproximación en función de la dimensión de nuestro subespacio de Krylov. Dado que a menudo nos preocupamos por encontrar estados básicos y otros valores propios mínimos (y por otras razones más algebraicas que se explican más adelante), veamos nuestra estimación del valor propio más bajo en función de la dimensión del subespacio de Krylov. Es decir\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "48b23ca9-a5ba-4a9c-bbc2-7fba75b36124",
      "metadata": {},
      "outputs": [],
      "source": [
        "def errors(matrix, krylov_eigs):\n",
        "    targ_min = min(np.linalg.eig(matrix).eigenvalues)\n",
        "    err = []\n",
        "    for i in range(len(matrix)):\n",
        "        err.append(min(krylov_eigs[i]) - targ_min)\n",
        "    return err"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "0912e12b-63bd-4026-ac8e-56bc5db8736e",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/0912e12b-63bd-4026-ac8e-56bc5db8736e-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "krylov_error = errors(test_matrix, test_eigs)\n",
        "\n",
        "plt.plot(krylov_error)\n",
        "plt.axhline(y=0, color=\"red\", linestyle=\"--\")  # Add dashed red line at y=0\n",
        "plt.xlabel(\"Order of Krylov subspace\")  # Add x-axis label\n",
        "plt.ylabel(\"Error in minimum eigenvalue\")  # Add y-axis label\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7af1b947-205b-46f8-bb23-d439ae6f076a",
      "metadata": {},
      "source": [
        "Vemos que el valor propio mínimo se alcanza con bastante precisión una vez que el subespacio de Krylov ha crecido hasta $\\mathcal{K}^2,$ y es perfecto por $\\mathcal{K}^3.$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7ee80bf2-e3e8-4124-9afc-ef2ef247f831",
      "metadata": {},
      "source": [
        "<span id=\"22-time-scaling-with-matrix-dimension\" />\n",
        "\n",
        "### 2.2 Escalado temporal con dimensión matricial\n",
        "\n",
        "Convenzámonos de que el método de Krylov puede resultar ventajoso frente a los eigensolvers numéricos exactos de la siguiente manera:\n",
        "\n",
        "* Construir matrices aleatorias (no dispersas, no es la aplicación ideal para KQD)\n",
        "* Determine los valores propios utilizando dos métodos: directamente utilizando NumPy y utilizando un subespacio de Krylov.\n",
        "* Elegimos un límite para la precisión de nuestros valores propios antes de aceptar las estimaciones de Krylov.\n",
        "* Compara el tiempo de pared necesario para resolver de estas dos maneras.\n",
        "\n",
        "**Advertencias:** Como discutiremos en detalle más adelante, la diagonalización cuántica de Krylov se aplica mejor a operadores cuyas representaciones matriciales son dispersas y/o pueden escribirse utilizando un pequeño número de grupos de operadores de Pauli conmutantes. Las matrices aleatorias que utilizamos aquí no se ajustan a esa descripción. Sólo son útiles para sondear la escala a la que los métodos clásicos de Krylov podrían ser útiles.\n",
        "En segundo lugar, al utilizar el método de Krylov calcularemos los valores propios utilizando muchos subespacios de Krylov de distintos tamaños. Informaremos del tiempo requerido para el subespacio de Krylov de dimensión mínima que alcanza nuestra precisión requerida para el valor propio del estado fundamental. De nuevo, esto es un poco diferente de resolver un problema que es intratable para eigensolvers exactos, ya que estamos utilizando la solución exacta para evaluar la dimensión necesaria.\n",
        "\n",
        "Comenzamos generando nuestro conjunto de matrices aleatorias.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "a5f8f1f6-b334-43e4-ad40-fd689c07a3e1",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "\n",
        "# Set the random seed\n",
        "np.random.seed(42)\n",
        "\n",
        "# how many random matrices will we make\n",
        "num_matrix = 200\n",
        "\n",
        "matrices = []\n",
        "for m in range(1, num_matrix):\n",
        "    matrices.append(np.random.rand(m, m))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d1c31d97-7e76-4577-ab03-ce8f0aebf134",
      "metadata": {},
      "source": [
        "Ahora diagonalizamos cada matriz directamente, usando numpy. Calculamos el tiempo necesario para la diagonalización para su posterior comparación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "d9cb6e36-9ad3-403f-823f-0072e9cf53fe",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/d9cb6e36-9ad3-403f-823f-0072e9cf53fe-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "matrix_numpy_times = []\n",
        "matrix_numpy_eigs = []\n",
        "for mm in range(num_matrix - 1):\n",
        "    t0 = time_mus()\n",
        "    matrix_numpy_eigs.append(min(np.linalg.eig(matrices[mm]).eigenvalues))\n",
        "    matrix_numpy_times.append(time_mus() - t0)\n",
        "\n",
        "plt.plot(matrix_numpy_times)\n",
        "plt.xlabel(\"Dimension of matrix\")  # Add x-axis label\n",
        "plt.ylabel(\"Time to diagonalize (microsec)\")  # Add y-axis label\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4e5d4af5-78da-4254-a726-4b0a72f98fe6",
      "metadata": {},
      "source": [
        "Obsérvese que, en la imagen anterior, el tiempo anómalamente elevado en torno a una dimensión de 125 puede deberse a la naturaleza aleatoria de las matrices o a la implementación en el procesador clásico utilizado, pero no es reproducible. Si se vuelve a ejecutar el código, se obtendrá un perfil diferente con distintos picos anómalos.\n",
        "\n",
        "Ahora, para cada matriz, construiremos un subespacio de Krylov y calcularemos los valores propios por pasos. En cada paso, comprobaremos si el valor propio más bajo se ha obtenido dentro de nuestro error absoluto especificado. El subespacio que primero nos da valores propios dentro de nuestro error especificado es el subespacio para el que registraremos los tiempos de cálculo. La ejecución de esta célula puede tardar varios minutos, dependiendo de la velocidad del procesador. Puede omitir la evaluación o reducir la dimensión máxima de las matrices diagonalizadas. Basta con mirar los resultados precalculados.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7fbe57ec-507a-412e-b7fe-09aaea28e103",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size\n",
        "# at which that error is achieved.\n",
        "abserr = 0.05\n",
        "accept_subspace_size = []\n",
        "\n",
        "# Lists to store total time spent on the Krylov method, and the subset of that time spent on\n",
        "# diagonalizing the projected matrix.\n",
        "matrix_krylov_tot_times = []\n",
        "matrix_krylov_dim = []\n",
        "\n",
        "# Step through all our random matrices\n",
        "for mm in range(0, num_matrix - 1):\n",
        "    test_ks, test_Hs, test_eigs, test_k_tot_times = krylov_full_build(\n",
        "        np.ones(len(matrices[mm])), matrices[mm]\n",
        "    )\n",
        "    # We have not yet found a Krylov subspace that produces our minimum eigenvalue to\n",
        "    # within the required error.\n",
        "    found = 0\n",
        "    for j in range(0, len(matrices[mm]) - 1):\n",
        "        # If we still haven't found the desired subspace...\n",
        "        if found == 0:\n",
        "            # ...but if this one satisfies the requirement, then record everything\n",
        "            if (\n",
        "                abs((min(test_eigs[j]) - matrix_numpy_eigs[mm]) / matrix_numpy_eigs[mm])\n",
        "                < abserr\n",
        "            ):\n",
        "                accept_subspace_size.append(j)\n",
        "                matrix_krylov_tot_times.append(test_k_tot_times[j])\n",
        "                matrix_krylov_dim.append(mm)\n",
        "                found = 1"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5046bfdb-9ff6-4142-9dc6-260ce26fd3f5",
      "metadata": {},
      "source": [
        "Comparemos los tiempos obtenidos con estos dos métodos:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 94,
      "id": "ed3302d5-a0a8-479b-9731-755fc5a1b684",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/qda-2-krylov/extracted-outputs/ed3302d5-a0a8-479b-9731-755fc5a1b684-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "plt.plot(matrix_numpy_times, color=\"blue\")\n",
        "plt.plot(matrix_krylov_dim, matrix_krylov_tot_times, color=\"green\")\n",
        "plt.xlabel(\"Dimension of matrix\")  # Add x-axis label\n",
        "plt.ylabel(\"Time to diagonalize (microsec)\")  # Add y-axis label\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e3c5ff65-964f-4b7c-8ca7-89958b3d9465",
      "metadata": {},
      "source": [
        "Estos son los tiempos reales requeridos, pero a efectos de discusión, vamos a suavizar estas curvas promediando sobre unos pocos puntos adyacentes / dimensiones de la matriz. Esto se hace a continuación:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "41ac6108-526d-476b-8255-db9bb52a30ca",
      "metadata": {},
      "outputs": [],
      "source": [
        "smooth_numpy_times = []\n",
        "smooth_krylov_times = []\n",
        "\n",
        "# Choose the number of adjacent points over which to average forward;\n",
        "# the same will be used backward.\n",
        "smooth_steps = 10\n",
        "\n",
        "# We will do this smoothing for all points/matrix dimensions\n",
        "for i in range(len(matrix_krylov_tot_times)):\n",
        "    # Ensure we don't exceed the boundaries of our lists\n",
        "    start = max(0, i - smooth_steps)\n",
        "    end = min(len(matrix_krylov_tot_times) - 1, i + smooth_steps)\n",
        "\n",
        "    # Dummy variables for accumulating an average over adjacent points. This is done for both Krylov\n",
        "    # and the NumPy calculations.\n",
        "    smooth_count = 0\n",
        "    smooth_numpy_sum = 0\n",
        "    smooth_krylov_sum = 0\n",
        "\n",
        "    for j in range(start, end):\n",
        "        smooth_numpy_sum = smooth_numpy_sum + matrix_numpy_times[j]\n",
        "        smooth_krylov_sum = smooth_krylov_sum + matrix_krylov_tot_times[j]\n",
        "        smooth_count = smooth_count + 1\n",
        "\n",
        "    # Appending the averaged adjacent values to our new smooth lists\n",
        "    smooth_numpy_times.append(smooth_numpy_sum / smooth_count)\n",
        "    smooth_krylov_times.append(smooth_krylov_sum / smooth_count)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 96,
      "id": "6963ee9b-55a8-42ab-bc4f-d470bb81e543",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/qda-2-krylov/extracted-outputs/6963ee9b-55a8-42ab-bc4f-d470bb81e543-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "plt.plot(smooth_numpy_times, color=\"blue\")\n",
        "plt.plot(smooth_krylov_times, color=\"green\")\n",
        "plt.xlabel(\"Dimension of matrix\")  # Add x-axis label\n",
        "plt.ylabel(\"Time to diagonalize (smoothed, microsec)\")  # Add y-axis label\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7e1bbb91-5da7-43ea-8b99-d04ea1196e76",
      "metadata": {},
      "source": [
        "Nótese que el tiempo requerido para la construcción de un subespacio de Krylov inicialmente excede el tiempo requerido para la diagonalización completa de numpy. Pero a medida que aumenta el tamaño de la matriz, el método de Krylov se vuelve ventajoso. Esto es cierto incluso si reducimos nuestro error aceptable, pero la ventaja se establece en un tamaño de matriz mayor. Merece la pena analizarlo.\n",
        "\n",
        "La complejidad temporal de la diagonalización numérica es $O(n^3)$ (con alguna variación entre algoritmos). La complejidad temporal de generar una base ortonormal de vectores $n$ también es $O(n^3)$. Así que la ventaja del método de Krylov **no** está relacionada con el uso de $\\it{some}$ base ortonormal, sino con el uso de una base ortonormal particular que efectivamente escoge los valores propios de interés. Ya hemos visto esto en el esbozo de una prueba en la primera sección de esta lección, y esto es crítico para las garantías de convergencia en los métodos de Krylov.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "da603cad-2e8e-4494-a0fd-f082953c35cc",
      "metadata": {},
      "source": [
        "Repasemos nuestros progresos hasta ahora:\n",
        "\n",
        "* Para matrices muy grandes, el método del subespacio de Krylov puede producir valores propios aproximados dentro de las tolerancias requeridas más rápidamente que los algoritmos tradicionales de diagonalización.\n",
        "* Para matrices tan grandes, la generación de un subespacio de Krylov es la parte que más tiempo consume del método de subespacios de Krylov.\n",
        "* Por lo tanto, sería muy valiosa una forma eficaz de generar un subespacio de Krylov.\n",
        "  Aquí es donde entra en escena el ordenador cuántico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7782cac1-a86b-4a70-971f-3db5922838d7",
      "metadata": {},
      "source": [
        "<span id=\"check-your-understanding\" />\n",
        "\n",
        "#### Comprueba tu comprensión\n",
        "\n",
        "Consulte el gráfico suavizado de los tiempos de diagonalización en función de la dimensión de la matriz que se muestra más arriba.\n",
        "\n",
        "(a) Según este gráfico, ¿a partir de qué dimensión aproximada de la matriz el método de Krylov empezó a ser más rápido?\n",
        "\n",
        "b) ¿Qué aspectos del cálculo podrían modificar el orden de magnitud a partir del cual el método de Krylov se vuelve más rápido?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Respuesta\">\n",
        "    (a) Los resultados pueden variar si se vuelve a realizar el cálculo, pero el método de Krylov se vuelve más rápido a partir de una dimensión de aproximadamente 80-85.\n",
        "\n",
        "    (b) Hay muchas respuestas posibles. Algunos factores importantes son la precisión que necesitamos y la dispersión de las matrices que se van a diagonalizar.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "03b62a85-4737-41ec-b264-55d22fe814bc",
      "metadata": {},
      "source": [
        "<span id=\"3-krylov-via-time-evolution\" />\n",
        "\n",
        "## 3. Krylov mediante evolución temporal\n",
        "\n",
        "Todo lo que hemos descrito hasta ahora puede hacerse de forma clásica. Entonces, ¿cómo y cuándo utilizaríamos un ordenador cuántico? Para matrices muy grandes, el método de Krylov puede requerir largos tiempos de cálculo y grandes cantidades de memoria. El tiempo necesario para la operación matricial de $H$ en $|v\\rangle$ escala como $O(N^2)$ en el peor de los casos. Incluso la multiplicación de matrices dispersas en un vector (el caso típico de los solucionadores clásicos de tipo Krylov) tiene una complejidad temporal que escala como $O(N)$. Esto se hace para cada vector que queramos en nuestro subespacio. La dimensión del subespacio $r$ no suele ser una fracción significativa de $N$, y a menudo escala como $\\log(N)$. Así que generar todos los vectores se escala como $O(N^2 \\log(N))$ en el peor de los casos. Aunque hay otros pasos, como la ortogonalización, éste es el escalado dominante que hay que tener en cuenta.\n",
        "\n",
        "La computación cuántica nos permite cambiar qué atributos del problema determinan el escalado del tiempo y los recursos necesarios. En lugar de depender del tamaño de la matriz $N$ de forma generalizada, veremos cosas como el número de disparos y el número de términos de Pauli no conmutativos que componen el Hamiltoniano. Veamos cómo funciona.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1636f90b-6bdf-4b11-aee3-9be3f2e82d79",
      "metadata": {},
      "source": [
        "<span id=\"31-time-evolution\" />\n",
        "\n",
        "### 3.1 Evolución temporal\n",
        "\n",
        "Recordemos que el operador que evoluciona en el tiempo un estado cuántico es $e^{-iHt/\\hbar}$ (y es muy común, especialmente en computación cuántica, eliminar el $\\hbar$ de la notación). Una forma de entender e incluso realizar dicha función exponencial de un operador es observar su expansión en serie de Taylor. Nótese que esta operación actuando sobre algún vector inicial $|v\\rangle$ produce una suma de términos con potencias crecientes de $H$ aplicados al estado inicial. Parece que podemos crear nuestro subespacio de Krylov evolucionando en el tiempo nuestro estado inicial\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "e^{-iHt/\\hbar}→e^{-iHt}&≈1-iHt-\\frac{(H^2 t^2)}{2}+⋯\\\\\n",
        "e^{-iHt} |v\\rangle &≈ |v\\rangle-iHt|v\\rangle-\\frac{(H^2 t^2)}{2}|v\\rangle+⋯\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "La salvedad está en realizar la evolución temporal en un ordenador cuántico real. Muchos de los términos del Hamiltoniano no conmutarán entre sí. Así, mientras que algunos operadores exponenciales simples como $e^{-iZ}$ corresponden a circuitos simples, los hamiltonianos generales no. Y como contienen términos no conmutativos, no podemos descomponer simplemente la exponencial en un producto de simples, como podemos hacer con los números.\n",
        "\n",
        "$$\n",
        "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n)t}\\neq e^{-iH_1 t} e^{-iH_2 t}... e^{-iH_n t}\n",
        "$$\n",
        "\n",
        "Así que esto no es trivial, pero es un proceso bien estudiado en la computación cuántica. Llevamos a cabo la evolución temporal en ordenadores cuánticos mediante un proceso llamado trotterización, que en sí mismo es un tema rico [\\[10\\]](#references). Pero a un nivel muy alto, rompiendo la evolución temporal en pasos muy pequeños, digamos $m$ pasos de tamaño $dt$, limitamos los efectos de la no conmutatividad de los términos.\n",
        "\n",
        "$$\n",
        "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n )t} = (e^{-i(H_1+H_2+⋯+H_n )t/m} )^m\n",
        "≈(e^{-iH_1 dt} e^{-iH_2 dt} …e^{-iH_n dt} )^m\n",
        "$$\n",
        "\n",
        "donde $dt = t/m$.\n",
        "\n",
        "Llamemos \"subespacio de Krylov de potencia\" a un subespacio de Krylov de orden r que hayamos generado en el contexto clásico utilizando potencias de H directamente.\n",
        "\n",
        "$$\n",
        "\\mathcal{K}_P^r (H,|v\\rangle)=\\text{span}\\{|v\\rangle,H|v\\rangle,H^2 |v\\rangle… H^{r-1}  |v\\rangle\\}\n",
        "$$\n",
        "\n",
        "Ahora generamos un espacio similar utilizando el operador unitario de evolución temporal $U \\equiv e^{-iHt}$; nos referiremos a éste como el \"espacio unitario de Krylov\" $\\mathcal{K}_U^r$. El subespacio de Krylov de potencia $\\mathcal{K}_P^r$ que utilizamos clásicamente no puede generarse directamente en un ordenador cuántico, ya que $H$ no es un operador unitario. Se puede demostrar que el uso del subespacio unitario de Krylov ofrece garantías de convergencia similares a las del subespacio de Krylov de potencia, es decir, el error del estado fundamental converge eficientemente siempre y cuando el estado inicial $|v\\rangle$ tenga un solapamiento con el estado fundamental verdadero que no sea exponencialmente evanescente, y siempre y cuando exista una brecha suficiente entre los valores propios. Véase la Ref [\\[1\\]](#references) para un análisis más preciso de la convergencia.\n",
        "\n",
        "Aquí, las potencias de $U$ se convierten en diferentes pasos de tiempo (la potencia $k^\\text{th}$ de $U$ se adelanta un paso de tiempo $k \\times dt$ ). Podemos etiquetar el elemento del subespacio que evoluciona en el tiempo para el tiempo total $k dt$ como $|\\psi_k\\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "619c81f7-be3c-4ffa-87f0-2ab6420381ca",
      "metadata": {},
      "source": [
        "$$\n",
        "\\begin{aligned}\n",
        "U&=e^{-iHdt}\\\\\n",
        "U^k&=e^{-iH(kdt)}\\\\\n",
        "\\mathcal{K}_U^r&=\\text{span}\\{|\\psi\\rangle,U|\\psi\\rangle,U^2 |\\psi\\rangle… U^{r-1}  |\\psi\\rangle\\}\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ca6352b4-88c2-4dab-9f82-1750223ec8af",
      "metadata": {},
      "source": [
        "Podemos proyectar nuestro Hamiltoniano H en el subespacio unitario de Krylov, $\\mathcal{K}_U^r$. En otras palabras, calculamos cada elemento matricial de $H$ en la base $\\mathcal{K}_U^r$. Denominaremos a esta matriz proyectada $\\tilde{H}$.\n",
        "\n",
        "<span id=\"32-how-to-implement-on-a-quantum-computer\" />\n",
        "\n",
        "### 3.2 Cómo implementar en un ordenador cuántico\n",
        "\n",
        "Los elementos de la matriz de $\\tilde{H}$ vienen dados por los valores de expectativa $\\langle \\psi_m |H| \\psi_n\\rangle$, que pueden estimarse utilizando el ordenador cuántico. Hay que tener en cuenta que $H$ puede escribirse como una suma de operadores de Pauli en diferentes qubits, y que no todos los operadores de Pauli pueden medirse simultáneamente. Podemos clasificar los términos de Pauli en grupos de términos conmutativos y medirlos todos a la vez. Pero puede que necesitemos muchos grupos de este tipo para abarcar todos los términos. Por lo tanto, el número de grupos de conmutación distintos en los que se pueden dividir los términos, $N_\\text{GCP}$, es importante.\n",
        "\n",
        "$$\n",
        "H=\\sum_{\\alpha=1}^{N_\\text{GCP}} c_\\alpha P_\\alpha\n",
        "$$\n",
        "\n",
        "Aquí, $P_\\alpha$ es una cadena de Pauli de la forma $P_\\alpha \\sim IZIXII...YZXIX$ o un conjunto de tales cadenas de Pauli que conmutan entre sí. Dado que podemos escribir « $H$ » como una suma de operadores medibles, las siguientes expresiones para los elementos de matriz de « $\\tilde{H}$ » pueden obtenerse utilizando el estimador primitivo « IBM Quantum ».\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "\\tilde{H}_{mn}&=\\langle \\psi_m |H| \\psi_n\\rangle\\\\\n",
        "&=\\langle \\psi e^{iHt_m} |H| \\psi e^{-iHt_n}\\rangle\\\\\n",
        "&=\\langle \\psi e^{iHmdt} |H|\\psi e^{-iHndt}\\rangle\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "Donde $\\vert \\psi_n \\rangle = e^{-i H t_n} \\vert \\psi \\rangle$ son los vectores del espacio unitario de Krylov y $t_n = n dt$ son los múltiplos del paso de tiempo $dt$ elegidos. En un ordenador cuántico, el cálculo de los elementos de cada matriz puede realizarse con cualquier algoritmo que permita obtener solapamientos entre estados cuánticos. En esta lección nos centraremos en la prueba de Hadamard. Dado que $\\mathcal{K}_U$ tiene dimensión $r$, el Hamiltoniano proyectado en el subespacio tendrá dimensiones $r \\times r$. Con $r$ suficientemente pequeño (generalmente $r<<100$ es suficiente para obtener la convergencia de las estimaciones de los valores propios) podemos entonces diagonalizar fácilmente el Hamiltoniano proyectado $\\tilde{H},$ clásicamente. Sin embargo, no podemos diagonalizar directamente $\\tilde{H}$ debido a la no ortogonalidad de los vectores del espacio de Krylov. Tendremos que medir sus solapamientos y construir una matriz $\\tilde{S}$\n",
        "\n",
        "$$\n",
        "\\tilde{S}_{mn} = \\langle \\psi_m \\vert \\psi_n \\rangle\n",
        "$$\n",
        "\n",
        "Esto nos permite resolver el problema de valores propios en un espacio no ortogonal (también llamado problema de valores propios generalizado)\n",
        "\n",
        "$$\n",
        "\\tilde{H} \\ \\vec{c} = E \\ \\tilde{S} \\ \\vec{c}\n",
        "$$\n",
        "\n",
        "A continuación, se pueden obtener estimaciones de los valores propios y los estados propios de $H$ observando las soluciones de este problema de valores propios generalizado. Por ejemplo, la estimación de la energía del estado fundamental se obtiene tomando el valor propio más pequeño $E$ y el estado fundamental del vector propio correspondiente $\\vec{c}$. Los coeficientes en $\\vec{c}$ determinan la contribución de los distintos vectores que abarcan $\\mathcal{K}_U$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7d29b607-3f2c-445c-9630-3cde4edf3299",
      "metadata": {},
      "source": [
        "<span id=\"generalized-eigenvalue-problem\" />\n",
        "\n",
        "#### Problema generalizado de valores propios\n",
        "\n",
        "¿Por qué no podemos simplemente diagonalizar $\\tilde{H}$? Dado que $\\tilde{S}$ contiene la información sobre la geometría de la base de Krylov (que es no ortogonal en todos los casos excepto en casos muy especiales), $\\tilde{H}$ por sí solo no describe una proyección del Hamiltoniano completo, por lo que sus valores propios no tienen ninguna relación particular con los del Hamiltoniano completo -- podrían ser cualquier valor aleatorio. La resolución del problema generalizado de valores propios es necesaria para obtener los valores propios y vectores propios aproximados correspondientes a la proyección del Hamiltoniano completo en el espacio de Krylov....\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2f3ba07d-0c16-47fb-aeed-b4ef0a1f25e3",
      "metadata": {},
      "source": [
        "![Un diagrama de circuito con muchas capas que indica que el circuito debe utilizarse muchas veces con diferentes estados para realizar la prueba de Hadamard modificada.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig4.avif)\n",
        "\n",
        "La figura muestra una representación en circuito de la prueba de Hadamard modificada, un método que se utiliza para calcular el solapamiento entre diferentes estados cuánticos. Para cada elemento de la matriz $\\tilde{H}_{i,j}$, se realiza una prueba Hadamard entre el estado $\\vert \\psi_i \\rangle$, $\\vert \\psi_j \\rangle$. Esto se destaca en la figura por el esquema de colores para los elementos de la matriz y las operaciones $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ correspondientes. Así, se requiere un conjunto de pruebas de Hadamard para todas las combinaciones posibles de vectores del espacio de Krylov para calcular todos los elementos de la matriz del Hamiltoniano proyectado $\\tilde{H}$. El hilo superior del circuito de pruebas de Hadamard es un qubit ancilla que se mide en la base X o Y, su valor de expectativa determina el valor del solapamiento entre los estados. El hilo inferior representa todos los qubits del Hamiltoniano del sistema. La operación $\\text{Prep} \\; \\psi_i$ prepara el qubit del sistema en el estado $\\vert \\psi_i \\rangle$ controlado por el estado del qubit ancilla (de forma similar para $\\text{Prep} \\; \\psi_j$ ) y la operación $P$ representa la descomposición de Pauli del Hamiltoniano del sistema $H = \\sum_i P_i$. La implementación de esto en un ordenador cuántico se muestra con más detalle a continuación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c12c2855-f276-48a2-8901-13baf0384778",
      "metadata": {},
      "source": [
        "<span id=\"4-krylov-quantum-diagonalization-on-a-quantum-computer\" />\n",
        "\n",
        "## 4. Diagonalización cuántica de Krylov en un ordenador cuántico\n",
        "\n",
        "Ahora implementaremos la diagonalización cuántica de Krylov en un ordenador cuántico real. Empecemos por importar algunos paquetes útiles.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 64,
      "id": "afa233e1-1e80-4843-a958-0f84cec707ea",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "import scipy as sp\n",
        "import matplotlib.pylab as plt\n",
        "from typing import Union, List\n",
        "import warnings\n",
        "\n",
        "from qiskit.quantum_info import SparsePauliOp, Pauli\n",
        "from qiskit.circuit import Parameter\n",
        "from qiskit import QuantumCircuit, QuantumRegister\n",
        "from qiskit.circuit.library import PauliEvolutionGate\n",
        "from qiskit.synthesis import LieTrotter\n",
        "\n",
        "# from qiskit.providers.fake_provider import Fake20QV1\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator, Batch\n",
        "\n",
        "import itertools as it\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e4a3fba7-051f-4df4-b78a-7bfeb18caf1e",
      "metadata": {},
      "source": [
        "Definimos la siguiente función para resolver el problema generalizado de valores propios que acabamos de explicar.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 65,
      "id": "5446eb74-126f-4db1-b018-d5f4613f79e7",
      "metadata": {},
      "outputs": [],
      "source": [
        "def solve_regularized_gen_eig(\n",
        "    h: np.ndarray,\n",
        "    s: np.ndarray,\n",
        "    threshold: float,\n",
        "    k: int = 1,\n",
        "    return_dimn: bool = False,\n",
        ") -> Union[float, List[float]]:\n",
        "    \"\"\"\n",
        "    Method for solving the generalized eigenvalue problem with regularization\n",
        "\n",
        "    Args:\n",
        "        h (numpy.ndarray):\n",
        "            The effective representation of the matrix in our Krylov subspace\n",
        "        s (numpy.ndarray):\n",
        "            The matrix of overlaps between vectors of our Krylov subspace\n",
        "        threshold (float):\n",
        "            Cut-off value for the eigenvalue of s\n",
        "        k (int):\n",
        "            Number of eigenvalues to return\n",
        "        return_dimn (bool):\n",
        "            Whether to return the size of the regularized subspace\n",
        "\n",
        "    Returns:\n",
        "        lowest k-eigenvalue(s) that are the solution of the regularized generalized eigenvalue problem\n",
        "\n",
        "\n",
        "    \"\"\"\n",
        "    s_vals, s_vecs = sp.linalg.eigh(s)\n",
        "    s_vecs = s_vecs.T\n",
        "    good_vecs = np.array([vec for val, vec in zip(s_vals, s_vecs) if val > threshold])\n",
        "    h_reg = good_vecs.conj() @ h @ good_vecs.T\n",
        "    s_reg = good_vecs.conj() @ s @ good_vecs.T\n",
        "    if k == 1:\n",
        "        if return_dimn:\n",
        "            return sp.linalg.eigh(h_reg, s_reg)[0][0], len(good_vecs)\n",
        "        else:\n",
        "            return sp.linalg.eigh(h_reg, s_reg)[0][0]\n",
        "    else:\n",
        "        if return_dimn:\n",
        "            return sp.linalg.eigh(h_reg, s_reg)[0][:k], len(good_vecs)\n",
        "        else:\n",
        "            return sp.linalg.eigh(h_reg, s_reg)[0][:k]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7dce32ec-33eb-474d-88bf-e07f6563d6a2",
      "metadata": {},
      "source": [
        "Al menos en la evaluación comparativa inicial, es útil conocer una solución clásica exacta para comprobar el comportamiento de la convergencia. La siguiente función calcula la energía del estado fundamental de un Hamiltoniano, utilizando el Hamiltoniano y el número de qubits como argumentos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 66,
      "id": "f2d43bcc-5210-448d-b3d9-996796f782f6",
      "metadata": {},
      "outputs": [],
      "source": [
        "def single_particle_gs(H_op, n_qubits):\n",
        "    \"\"\"\n",
        "    Find the ground state of the single particle(excitation) sector\n",
        "    \"\"\"\n",
        "    H_x = []\n",
        "    for p, coeff in H_op.to_list():\n",
        "        H_x.append(set([i for i, v in enumerate(Pauli(p).x) if v]))\n",
        "\n",
        "    H_z = []\n",
        "    for p, coeff in H_op.to_list():\n",
        "        H_z.append(set([i for i, v in enumerate(Pauli(p).z) if v]))\n",
        "\n",
        "    H_c = H_op.coeffs\n",
        "\n",
        "    print(\"n_sys_qubits\", n_qubits)\n",
        "\n",
        "    n_exc = 1\n",
        "    sub_dimn = int(sp.special.comb(n_qubits + 1, n_exc))\n",
        "    print(\"n_exc\", n_exc, \", subspace dimension\", sub_dimn)\n",
        "\n",
        "    few_particle_H = np.zeros((sub_dimn, sub_dimn), dtype=complex)\n",
        "\n",
        "    sparse_vecs = [\n",
        "        set(vec) for vec in it.combinations(range(n_qubits + 1), r=n_exc)\n",
        "    ]  # list all of the possible sets of n_exc indices of 1s in n_exc-particle states\n",
        "\n",
        "    m = 0\n",
        "    for i, i_set in enumerate(sparse_vecs):\n",
        "        for j, j_set in enumerate(sparse_vecs):\n",
        "            m += 1\n",
        "\n",
        "            if len(i_set.symmetric_difference(j_set)) <= 2:\n",
        "                for p_x, p_z, coeff in zip(H_x, H_z, H_c):\n",
        "                    if i_set.symmetric_difference(j_set) == p_x:\n",
        "                        sgn = ((-1j) ** len(p_x.intersection(p_z))) * (\n",
        "                            (-1) ** len(i_set.intersection(p_z))\n",
        "                        )\n",
        "                    else:\n",
        "                        sgn = 0\n",
        "\n",
        "                    few_particle_H[i, j] += sgn * coeff\n",
        "\n",
        "    gs_en = min(np.linalg.eigvalsh(few_particle_H))\n",
        "    print(\"single particle ground state energy: \", gs_en)\n",
        "    return gs_en"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c70998c4-e65c-456e-b3b4-61a289c8dd84",
      "metadata": {},
      "source": [
        "<span id=\"41-step-1-map-problem-to-quantum-circuits-and-operators\" />\n",
        "\n",
        "### 4.1 Paso 1: Asignar el problema a circuitos y operadores cuánticos\n",
        "\n",
        "Ahora definiremos un Hamiltoniano. Se diferencia de la función anterior en que la función anterior toma un Hamiltoniano como argumento y devuelve sólo el estado fundamental, y lo hace de forma clásica. Este Hamiltoniano que definimos aquí determina los niveles de energía de todos los eigenestados energéticos, y este Hamiltoniano puede construirse utilizando operadores de Pauli e implementarse en un ordenador cuántico.\n",
        "\n",
        "Elegimos un Hamiltoniano correspondiente a una cadena de espines que puede tener cualquier orientación en el espacio, llamada \"cadena de Heisenberg\". Asumimos que el espín $i^\\text{th}$ puede ser influenciado por sus vecinos más cercanos (los espines $(i-1)^\\text{th}$ y $(i+1)^\\text{th}$ ) pero no por vecinos más distantes. También tenemos en cuenta la posibilidad de que la interacción entre los espines sea diferente cuando los espines apuntan a lo largo de ejes diferentes. A veces, esta asimetría se debe, por ejemplo, a la estructura de la red cristalina en la que están incrustados los espines.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 67,
      "id": "8b547f8a-df47-4e56-921b-3955eb7c19a9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[('ZZIIIIIIII', 2), ('IZZIIIIIII', 2), ('IIZZIIIIII', 2), ('IIIZZIIIII', 2), ('IIIIZZIIII', 2), ('IIIIIZZIII', 2), ('IIIIIIZZII', 2), ('IIIIIIIZZI', 2), ('IIIIIIIIZZ', 2), ('XXIIIIIIII', 1), ('IXXIIIIIII', 1), ('IIXXIIIIII', 1), ('IIIXXIIIII', 1), ('IIIIXXIIII', 1), ('IIIIIXXIII', 1), ('IIIIIIXXII', 1), ('IIIIIIIXXI', 1), ('IIIIIIIIXX', 1), ('YYIIIIIIII', 3), ('IYYIIIIIII', 3), ('IIYYIIIIII', 3), ('IIIYYIIIII', 3), ('IIIIYYIIII', 3), ('IIIIIYYIII', 3), ('IIIIIIYYII', 3), ('IIIIIIIYYI', 3), ('IIIIIIIIYY', 3)]\n"
          ]
        }
      ],
      "source": [
        "# Define problem Hamiltonian.\n",
        "n_qubits = 10\n",
        "# coupling strength for XX, YY, and ZZ interactions\n",
        "JX = 1\n",
        "JY = 3\n",
        "JZ = 2\n",
        "\n",
        "# Define the Hamiltonian:\n",
        "H_int = [[\"I\"] * n_qubits for _ in range(3 * (n_qubits - 1))]\n",
        "for i in range(n_qubits - 1):\n",
        "    H_int[i][i] = \"Z\"\n",
        "    H_int[i][i + 1] = \"Z\"\n",
        "for i in range(n_qubits - 1):\n",
        "    H_int[n_qubits - 1 + i][i] = \"X\"\n",
        "    H_int[n_qubits - 1 + i][i + 1] = \"X\"\n",
        "for i in range(n_qubits - 1):\n",
        "    H_int[2 * (n_qubits - 1) + i][i] = \"Y\"\n",
        "    H_int[2 * (n_qubits - 1) + i][i + 1] = \"Y\"\n",
        "H_int = [\"\".join(term) for term in H_int]\n",
        "H_tot = [\n",
        "    (term, JZ)\n",
        "    if term.count(\"Z\") == 2\n",
        "    else (term, JY)\n",
        "    if term.count(\"Y\") == 2\n",
        "    else (term, JX)\n",
        "    for term in H_int\n",
        "]\n",
        "\n",
        "# Get operator\n",
        "H_op = SparsePauliOp.from_list(H_tot)\n",
        "print(H_tot)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "759b8b55-894b-4d3b-93de-5df4f63f9609",
      "metadata": {},
      "source": [
        "El código siguiente restringe el Hamiltoniano a estados de una sola partícula, y utiliza la norma espectral para establecer un buen tamaño para nuestro paso de tiempo $dt$. Elegimos heurísticamente un valor para el paso temporal `dt` (basándonos en los límites superiores de la norma hamiltoniana). Ref [\\[9\\]](#references) demostró que un paso de tiempo suficientemente pequeño es $\\pi/\\vert \\vert H \\vert \\vert$, y que es preferible hasta cierto punto subestimar este valor en lugar de sobreestimarlo, ya que la sobreestimación puede permitir que las contribuciones de los estados de alta energía corrompan incluso el estado óptimo en el espacio de Krylov. Por otro lado, elegir $dt$ demasiado pequeño conduce a un peor acondicionamiento del subespacio de Krylov, ya que los vectores base de Krylov difieren menos de un paso temporal a otro.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 68,
      "id": "3dd96fbe-47bb-444b-b403-c67c2dcc9d07",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "np.float64(0.17453292519943295)"
            ]
          },
          "execution_count": 68,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Get Hamiltonian restricted to single-particle states\n",
        "single_particle_H = np.zeros((n_qubits, n_qubits))\n",
        "for i in range(n_qubits):\n",
        "    for j in range(i + 1):\n",
        "        for p, coeff in H_op.to_list():\n",
        "            p_x = Pauli(p).x\n",
        "            p_z = Pauli(p).z\n",
        "            if all(p_x[k] == ((i == k) + (j == k)) % 2 for k in range(n_qubits)):\n",
        "                sgn = ((-1j) ** sum(p_z[k] and p_x[k] for k in range(n_qubits))) * (\n",
        "                    (-1) ** p_z[i]\n",
        "                )\n",
        "            else:\n",
        "                sgn = 0\n",
        "            single_particle_H[i, j] += sgn * coeff\n",
        "for i in range(n_qubits):\n",
        "    for j in range(i + 1, n_qubits):\n",
        "        single_particle_H[i, j] = np.conj(single_particle_H[j, i])\n",
        "\n",
        "# Set dt according to spectral norm\n",
        "dt = np.pi / np.linalg.norm(single_particle_H, ord=2)\n",
        "dt"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cdb8c08b-caca-4bd5-ae93-065c42485e13",
      "metadata": {},
      "source": [
        "Especificamos el número de pasos de Trotter que se utilizarán en la evolución temporal. También especificamos una dimensión máxima de Krylov de 4. Esta dimensión de Krylov no es lo suficientemente grande para aplicaciones realistas. Pero es suficiente para este ejemplo. Además, comprobaremos la convergencia en dimensiones aún más pequeñas. En lecciones posteriores exploraremos métodos que nos permiten escalar y proyectar nuestros hamiltonianos en subespacios más grandes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 69,
      "id": "d95c6f17-7275-474a-b1e5-a41c4539ed83",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Set parameters for quantum Krylov algorithm\n",
        "krylov_dim = 4  # size of krylov subspace\n",
        "num_trotter_steps = 4\n",
        "dt_circ = dt / num_trotter_steps"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "29291306-28a3-4c12-94ef-b3ffd5521c76",
      "metadata": {},
      "source": [
        "<span id=\"state-preparation\" />\n",
        "\n",
        "#### Preparación del estado\n",
        "\n",
        "Elija un estado de referencia $\\vert \\psi \\rangle$ que tenga cierto solapamiento con el estado de tierra. Para este Hamiltoniano, Usamos el estado a con una excitación en el qubit del medio $\\vert 00..010...00 \\rangle$ como nuestro estado de referencia.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 70,
      "id": "410192fb-8197-4860-8c3a-2e874e2f9c56",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/410192fb-8197-4860-8c3a-2e874e2f9c56-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 70,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "qc_state_prep = QuantumCircuit(n_qubits)\n",
        "qc_state_prep.x(int(n_qubits / 2) + 1)\n",
        "qc_state_prep.draw(\"mpl\", scale=0.5)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dee3c383-fc2e-4420-8d04-861f5f4f4576",
      "metadata": {},
      "source": [
        "<span id=\"time-evolution\" />\n",
        "\n",
        "#### Evolución temporal\n",
        "\n",
        "Podemos realizar el operador de evolución temporal generado por un Hamiltoniano dado: $U=e^{-iHt}$ mediante la [aproximación de Lie-Trotter](/docs/api/qiskit/qiskit.synthesis.LieTrotter). Para simplificar, utilizamos la dirección `PauliEvolutionGate` integrada en el circuito de evolución temporal. La sintaxis general es la siguiente\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 71,
      "id": "4b3dddd0-391a-412a-9663-9e15a153125a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<qiskit.circuit.instructionset.InstructionSet at 0x7ccaa4664250>"
            ]
          },
          "execution_count": 71,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "t = Parameter(\"t\")\n",
        "\n",
        "## Create the time-evo op circuit\n",
        "evol_gate = PauliEvolutionGate(\n",
        "    H_op, time=t, synthesis=LieTrotter(reps=num_trotter_steps)\n",
        ")\n",
        "\n",
        "qr = QuantumRegister(n_qubits)\n",
        "qc_evol = QuantumCircuit(qr)\n",
        "qc_evol.append(evol_gate, qargs=qr)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "295bf203-2bc0-4c02-912b-423e5ca392bb",
      "metadata": {},
      "source": [
        "Utilizaremos una versión de esto a continuación en la prueba de Hadamard, pero dando un paso adelante para los tiempos $dt$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "03f71981-3a22-4ba6-8281-7b20895dbda3",
      "metadata": {},
      "source": [
        "<span id=\"hadamard-test\" />\n",
        "\n",
        "#### Prueba de Hadamard\n",
        "\n",
        "Recordemos que deseamos calcular los elementos matriciales tanto de $\\tilde{H}$ como de la matriz de Gram $\\tilde{S}$ utilizando la prueba de Hadamard. Repasemos cómo funciona en este contexto, centrándonos primero en la construcción de $\\tilde{H}.$. El proceso general se representa gráficamente a continuación. Las capas de bloques de colores de preparación de estados $\\text{Prep}|\\psi_i\\rangle$ sirven para recordar que este proceso se lleva a cabo para todas las combinaciones de $|\\psi_i\\rangle$ y $|\\psi_j\\rangle$ en nuestro subespacio.\n",
        "\n",
        "![Imagen de un diagrama de circuito cuántico con muchas capas que indica que el circuito debe evaluarse para muchos estados diferentes con el fin de realizar la prueba de Hadamard.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig5.avif)\n",
        "\n",
        "Los estados del sistema en los pasos indicados son:\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "    \\text{Step 0:}\\qquad|\\Psi\\rangle & = |0\\rangle|0\\rangle^N \\\\\n",
        "    \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle + |1\\rangle \\Big)|0\\rangle^N \\\\\n",
        "    \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle|0\\rangle^N+|1\\rangle |\\psi_i\\rangle\\Big)\\\\\n",
        "    \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |0\\rangle^N+|1\\rangle P |\\psi_i\\rangle\\Big) \\\\\n",
        "    \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |\\psi_j\\rangle+|1\\rangle P|\\psi_i\\rangle\\Big)\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "Aquí $P$ es un término de Pauli en la descomposición del Hamiltoniano (nótese que no puede ser una combinación lineal de múltiples términos de Pauli conmutativos ya que eso no sería unitario -- la agrupación es posible usando una construcción diferente que mostraremos más adelante) $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ son operaciones controladas que preparan $|\\psi_i\\rangle$, $|\\psi_j\\rangle$ vectores del espacio unitario de Krylov, con $|\\psi_k\\rangle = e^{-i H k dt } \\vert \\psi \\rangle = e^{-i H k dt } U_{\\psi} \\vert 0 \\rangle^N$. Aplicando medidas de $X$ y $Y$ a este circuito se calculan las partes real e imaginaria, respectivamente, de los elementos matriciales que necesitamos.\n",
        "\n",
        "Empezando por el paso 4 anterior, aplique la puerta Hadamard $H$ al qubit zeroth.\n",
        "\n",
        "$$\n",
        "\\begin{equation*}\n",
        "   |\\Psi\\rangle \\longrightarrow\\quad\\frac{1}{2}|0\\rangle\\Big( |\\psi_j\\rangle + P|\\psi_i\\rangle\\Big) + \\frac{1}{2}|1\\rangle\\Big(|\\psi_j\\rangle - P|\\psi_i\\rangle\\Big)\n",
        "\\end{equation*}\n",
        "$$\n",
        "\n",
        "A continuación, mida $X$ o $Y$.\n",
        "\n",
        "$$\n",
        "\\begin{equation*}\n",
        "\\begin{split}\n",
        "    \\Rightarrow\\quad\\langle X\\rangle &= \\frac{1}{4}\\Bigg(\\Big\\|| \\psi_j\\rangle + P|\\psi_i\\rangle \\Big\\|^2-\\Big\\||\\psi_j\\rangle - P|\\psi_i\\rangle\\Big\\|^2\\Bigg) \\\\\n",
        "    &= \\text{Re}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n",
        "\\end{split}\n",
        "\\end{equation*}\n",
        "$$\n",
        "\n",
        "A partir de la identidad $|a + b\\|^2 = \\langle a + b | a + b \\rangle = \\|a\\|^2 + \\|b\\|^2 + 2\\text{Re}\\langle a | b \\rangle$. Del mismo modo, midiendo $Y$ se obtiene\n",
        "\n",
        "$$\n",
        "\\begin{equation*}\n",
        "    \\langle Y\\rangle = \\text{Im}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n",
        "\\end{equation*}\n",
        "$$\n",
        "\n",
        "Añadiendo estos pasos a la evolución temporal que establecimos anteriormente escribimos lo siguiente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 72,
      "id": "ac536079-96f2-435b-a15f-72c207fa24d1",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Circuit for calculating the real part of the overlap in S via Hadamard test\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/ac536079-96f2-435b-a15f-72c207fa24d1-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 72,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "## Create the time-evo op circuit\n",
        "evol_gate = PauliEvolutionGate(\n",
        "    H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n",
        ")\n",
        "\n",
        "## Create the time-evo op dagger circuit\n",
        "evol_gate_d = PauliEvolutionGate(\n",
        "    H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n",
        ")\n",
        "evol_gate_d = evol_gate_d.inverse()\n",
        "\n",
        "# Put pieces together\n",
        "qc_reg = QuantumRegister(n_qubits)\n",
        "qc_temp = QuantumCircuit(qc_reg)\n",
        "qc_temp.compose(qc_state_prep, inplace=True)\n",
        "for _ in range(num_trotter_steps):\n",
        "    qc_temp.append(evol_gate, qargs=qc_reg)\n",
        "for _ in range(num_trotter_steps):\n",
        "    qc_temp.append(evol_gate_d, qargs=qc_reg)\n",
        "qc_temp.compose(qc_state_prep.inverse(), inplace=True)\n",
        "\n",
        "# Create controlled version of the circuit\n",
        "controlled_U = qc_temp.to_gate().control(1)\n",
        "\n",
        "# Create hadamard test circuit for real part\n",
        "qr = QuantumRegister(n_qubits + 1)\n",
        "qc_real = QuantumCircuit(qr)\n",
        "qc_real.h(0)\n",
        "qc_real.append(controlled_U, list(range(n_qubits + 1)))\n",
        "qc_real.h(0)\n",
        "\n",
        "print(\"Circuit for calculating the real part of the overlap in S via Hadamard test\")\n",
        "qc_real.draw(\"mpl\", fold=-1, scale=0.5)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b88673a4-859a-44f7-ac0c-38ecf17faf36",
      "metadata": {},
      "source": [
        "Ya advertimos de la profundidad que suponen los circuitos Trotter. Realizar la prueba de Hadamard en estas condiciones puede dar lugar a un circuito aún más profundo, especialmente una vez que descomponemos a puertas nativas. Esto aumentará aún más si tenemos en cuenta la topología del dispositivo. Así que antes de utilizar cualquier tiempo en el ordenador cuántico, es una buena idea comprobar la profundidad de 2 qubits de nuestro circuito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 73,
      "id": "f40d206a-9bd5-4355-8007-cecff21f7fbe",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Number of layers of 2Q operations 14401\n"
          ]
        }
      ],
      "source": [
        "print(\n",
        "    \"Number of layers of 2Q operations\",\n",
        "    qc_real.decompose(reps=2).depth(lambda x: x[0].num_qubits == 2),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bbd4f53b-8946-4713-89c2-e6bbbfa80609",
      "metadata": {},
      "source": [
        "Un circuito de esta profundidad no puede devolver resultados utilizables en los modernos ordenadores cuánticos. Si queremos construir $\\tilde{H}$ y $\\tilde{S},$ necesitamos una forma mejor. Esta es la razón de la prueba de Hadamard eficiente que se presenta a continuación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6cda7f15-7c76-40aa-bf2e-bfea141c8474",
      "metadata": {},
      "source": [
        "<span id=\"4-2-step-2-optimize-circuits-and-operators-for-target-hardware\" />\n",
        "\n",
        "### 4. Paso 2. Optimizar circuitos y operadores para el hardware de destino\n",
        "\n",
        "<span id=\"efficient-hadamard-test\" />\n",
        "\n",
        "#### Prueba de Hadamard eficiente\n",
        "\n",
        "Podemos optimizar los circuitos profundos para la prueba de Hadamard que hemos obtenido introduciendo algunas aproximaciones y basándonos en alguna suposición sobre el Hamiltoniano del modelo. Por ejemplo, considere el siguiente circuito para la prueba de Hadamard:\n",
        "\n",
        "![Imagen de un diagrama de circuito cuántico con muchas capas que indica que el circuito debe evaluarse para muchos operadores unitarios diferentes con el fin de realizar la prueba de Hadamard modificada y eficiente.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig6.avif)\n",
        "\n",
        "Supongamos que podemos calcular clásicamente $E_0$, el valor propio de $|0\\rangle^N$ bajo el Hamiltoniano $H$. Esto se cumple cuando el Hamiltoniano preserva la simetría U(1). Aunque esto puede parecer una suposición fuerte, hay muchos casos en los que es seguro asumir que existe un estado de vacío (en este caso se mapea al estado $|0\\rangle^N$ ) que no se ve afectado por la acción del Hamiltoniano. Este es el caso, por ejemplo, de los hamiltonianos químicos que describen moléculas estables (en las que se conserva el número de electrones).\n",
        "Dado que la puerta $\\text{Prep} \\; \\psi_0$, prepara el estado de referencia deseado $\\ket{\\psi_0} = \\text{Prep} \\; \\psi_0 \\ket{0} = e^{-i H 0 dt} U_{\\psi_0} \\ket{0}$, por ejemplo, preparar el estado HF para la química $\\text{Prep} \\; \\psi_0$ sería un producto de NOTs de un solo qubit, por lo que controlada- $\\text{Prep} \\; \\psi_0$ es sólo un producto de CNOTs.\n",
        "Entonces el circuito anterior implementa el siguiente estado antes de la medición:\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "    \\text{Step 0:}\\qquad|\\Psi\\rangle & = \\ket{0} \\ket{0}^{N}\\\\\n",
        "    \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(\\ket{0}\\ket{0}^N+ \\ket{1} \\ket{0}^N\\right)\\\\\n",
        "    \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle|0\\rangle^N+|1\\rangle|\\psi_0\\rangle\\right)\\\\\n",
        "    \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0}\\ket{0}^N+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n",
        "    \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0} \\ket{\\psi_0}+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n",
        "    & = \\frac{1}{2}\\left(\\ket{+}\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)+\\ket{-}\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\\right)\\\\\n",
        "    & = \\frac{1}{2}\\left(\\ket{+i}\\left(e^{i\\phi}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)+\\ket{-i}\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\\right)\n",
        "\\end{aligned}\n",
        "$$\n",
        "\n",
        "donde hemos utilizado el desfase simulable clásico $ U\\ket{0}^N = e^{i\\phi}\\ket{0}^N$ del paso 2 al 3. Por lo tanto, los valores esperados son\n",
        "\n",
        "$$\n",
        "\\begin{aligned}\n",
        "    \\langle X\\otimes P\\rangle&=\\frac{1}{4}\n",
        "    \\Big(\n",
        "    \\left(e^{-i\\phi}\\bra{\\psi_0}+\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)\n",
        "    \\\\\n",
        "    &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\n",
        "    \\Big)\\\\\n",
        "    &=\\text{Re}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right],\n",
        "\\end{aligned}\n",
        "\n",
        "$$\n",
        "\n",
        "$$\n",
        "\n",
        "\\begin{aligned}\n",
        "    \\langle Y\\otimes P\\rangle&=\\frac{1}{4}\n",
        "    \\Big(\n",
        "    \\left(e^{-i\\phi}\\bra{\\psi_0}+i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi_0}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)\n",
        "    \\\\\n",
        "    &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\n",
        "    \\Big)\\\\\n",
        "    &=\\text{Im}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right].\n",
        "\\end{aligned}\n",
        "\n",
        "$$\n",
        "\n",
        "Utilizando estos supuestos pudimos escribir los valores de las expectativas de los operadores de interés con menos operaciones controladas. De hecho, sólo necesitamos implementar la preparación controlada de estados $\\text{Prep} \\; \\psi_0$ y no evoluciones controladas en el tiempo. Si reformulamos nuestro cálculo de la forma anterior, podremos reducir en gran medida la profundidad de los circuitos resultantes.\n",
        "\n",
        "Obsérvese que, como ventaja adicional, dado que el operador de Pauli aparece ahora como una medida al final del circuito en lugar de como una puerta controlada en el medio, puede medirse junto con otros operadores de Pauli conmutantes como en la descomposición $H=\\sum_{\\alpha = 1}^{N_\\text{GCP}}c_\\alpha P_\\alpha $ dada anteriormente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cd24795e-3ef0-4629-87bf-8e04c471d665",
      "metadata": {},
      "source": [
        "<span id=\"decompose-time-evolution-operator-with-trotter-decomposition\" />\n",
        "\n",
        "### Descomponer el operador de evolución temporal con la descomposición de Trotter\n",
        "\n",
        "En lugar de implementar el operador de evolución temporal de forma exacta, podemos utilizar la descomposición de Trotter para implementar una aproximación del mismo. Repitiendo varias veces una determinada orden de descomposición de Trotter, obtenemos una mayor reducción del error introducido por la aproximación. A continuación, construimos directamente la implementación de Trotter de la manera más eficiente para el gráfico de interacción del hamiltoniano que estamos considerando (solo interacciones entre vecinos más cercanos). En la práctica, insertamos rotaciones de Pauli $R_{xx}$, $R_{yy}$, $R_{zz}$ con fuerzas de acoplamiento $J_x,$ $J_y,$ y $J_z$ y un ángulo parametrizado $t$, que corresponden a la implementación aproximada de $e^{-i (J_x XX + J_y YY + J_z ZZ) t}$. Dada la diferencia en la definición de las rotaciones de Pauli y la evolución temporal que estamos tratando de implementar, tendremos que utilizar el parámetro $2*dt$ para lograr una evolución temporal de $dt$. Además, invertimos el orden de las operaciones para un número impar de repeticiones de los pasos de Trotter, lo que es funcionalmente equivalente pero permite sintetizar operaciones adyacentes en una única unidad $SU(2)$. Esto da como resultado un circuito mucho menos profundo que el que se obtiene utilizando la funcionalidad `PauliEvolutionGate()` genérica.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "8d35e6cd-c0f6-4871-83da-86472d66be2c",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/8d35e6cd-c0f6-4871-83da-86472d66be2c-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 74,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "t = Parameter(\"t\")\n",
        "\n",
        "# Create instruction for rotation about XX+YY-ZZ:\n",
        "Rxyz_circ = QuantumCircuit(2)\n",
        "Rxyz_circ.rxx(2 * JX * t, 0, 1)\n",
        "Rxyz_circ.ryy(2 * JY * t, 0, 1)\n",
        "Rxyz_circ.rzz(2 * JZ * t, 0, 1)\n",
        "Rxyz_instr = Rxyz_circ.to_instruction(label=\"R J_x XX + J_y YY + J_z ZZ\")\n",
        "\n",
        "interaction_list = [\n",
        "    [[i, i + 1] for i in range(0, n_qubits - 1, 2)],\n",
        "    [[i, i + 1] for i in range(1, n_qubits - 1, 2)],\n",
        "]  # linear chain\n",
        "\n",
        "qr = QuantumRegister(n_qubits)\n",
        "trotter_step_circ = QuantumCircuit(qr)\n",
        "for i, color in enumerate(interaction_list):\n",
        "    for interaction in color:\n",
        "        trotter_step_circ.append(Rxyz_instr, interaction)\n",
        "    if i < len(interaction_list) - 1:\n",
        "        trotter_step_circ.barrier()\n",
        "reverse_trotter_step_circ = trotter_step_circ.reverse_ops()\n",
        "\n",
        "qc_evol = QuantumCircuit(qr)\n",
        "for step in range(num_trotter_steps):\n",
        "    if step % 2 == 0:\n",
        "        qc_evol = qc_evol.compose(trotter_step_circ)\n",
        "    else:\n",
        "        qc_evol = qc_evol.compose(reverse_trotter_step_circ)\n",
        "\n",
        "qc_evol.decompose().draw(\"mpl\", fold=-1, scale=0.5)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2a3d8d4a-fc0f-4702-ac69-67f63d0c0e30",
      "metadata": {},
      "source": [
        "Preparamos de nuevo un estado inicial para esta prueba Hadamard eficiente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 75,
      "id": "d1d0b9de-65d4-4a46-975d-6cfaaac05f9a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/d1d0b9de-65d4-4a46-975d-6cfaaac05f9a-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 75,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "control = 0\n",
        "excitation = int(n_qubits / 2) + 1\n",
        "controlled_state_prep = QuantumCircuit(n_qubits + 1)\n",
        "controlled_state_prep.cx(control, excitation)\n",
        "controlled_state_prep.draw(\"mpl\", fold=-1, scale=0.5)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "415e94f8-6594-42ab-b55b-80a34852b943",
      "metadata": {},
      "source": [
        "<span id=\"template-circuits-for-calculating-matrix-elements-of-$tilde{s}$-and-$tilde{h}$-via-hadamard-test\" />\n",
        "\n",
        "#### Circuitos modelo para calcular elementos matriciales de $\\tilde{S}$ y $\\tilde{H}$ mediante la prueba de Hadamard\n",
        "\n",
        "La única diferencia entre los circuitos utilizados en la prueba de Hadamard será la fase en el operador de evolución temporal y los observables medidos. Por lo tanto, podemos preparar un circuito plantilla que represente el circuito genérico para la prueba Hadamard, con marcadores de posición para las puertas que dependen del operador de evolución temporal.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 76,
      "id": "27a54efa-affb-41bc-8523-822f8d92d34f",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Parameters for the template circuits\n",
        "parameters = []\n",
        "for idx in range(1, krylov_dim):\n",
        "    parameters.append(dt_circ * (idx))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 77,
      "id": "37382668-1999-4475-b50f-2887449d5c93",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/37382668-1999-4475-b50f-2887449d5c93-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 77,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Create modified hadamard test circuit\n",
        "qr = QuantumRegister(n_qubits + 1)\n",
        "qc = QuantumCircuit(qr)\n",
        "qc.h(0)\n",
        "qc.compose(controlled_state_prep, list(range(n_qubits + 1)), inplace=True)\n",
        "qc.barrier()\n",
        "qc.compose(qc_evol, list(range(1, n_qubits + 1)), inplace=True)\n",
        "qc.barrier()\n",
        "qc.x(0)\n",
        "qc.compose(controlled_state_prep.inverse(), list(range(n_qubits + 1)), inplace=True)\n",
        "qc.x(0)\n",
        "\n",
        "qc.decompose().draw(\"mpl\", fold=-1)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 78,
      "id": "4ad71f04-ec89-473b-9b7a-db4d3936c85e",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The optimized circuit has 2Q gates depth:  50\n"
          ]
        }
      ],
      "source": [
        "print(\n",
        "    \"The optimized circuit has 2Q gates depth: \",\n",
        "    qc.decompose().decompose().depth(lambda x: x[0].num_qubits == 2),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6f60d587-8ae1-4daa-ab76-a032c2221ab7",
      "metadata": {},
      "source": [
        "Esta profundidad se reduce sustancialmente en comparación con la prueba de Hadamard original. Esta profundidad es manejable en los ordenadores cuánticos modernos, aunque sigue siendo bastante elevada. Tendremos que recurrir a la mitigación de errores más avanzada para obtener resultados útiles.\n",
        "\n",
        "Selecciona un backend en el que ejecutar nuestro cálculo cuántico de Krylov, de modo que podamos transpilar nuestro circuito para ejecutarlo en ese ordenador cuántico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5683752b-d520-4cad-9fe9-7ebb7f495ba0",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(operational=True, simulator=False)\n",
        "\n",
        "# Or you may choose a specify backend and channel if necessary for your workflow.\n",
        "# service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n",
        "# backend = service.backend(\"ibm_fez\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0337f370-edff-4dcb-9570-26818ee51d3a",
      "metadata": {},
      "source": [
        "Ahora transpilemos nuestros circuitos y operadores.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 80,
      "id": "8a7c9ea1-5bc3-40b7-aa55-040cbb5f8c28",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "\n",
        "target = backend.target\n",
        "basis_gates = list(target.operation_names)\n",
        "pm = generate_preset_pass_manager(\n",
        "    optimization_level=3, backend=backend, basis_gates=basis_gates\n",
        ")\n",
        "\n",
        "qc_trans = pm.run(qc)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 81,
      "id": "054bcfba-fde3-4213-bbd7-d7a6dd6c03c8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "36\n",
            "OrderedDict([('rz', 410), ('sx', 361), ('cz', 156), ('x', 18), ('barrier', 6)])\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/054bcfba-fde3-4213-bbd7-d7a6dd6c03c8-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 81,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "print(qc_trans.depth(lambda x: x[0].num_qubits == 2))\n",
        "print(qc_trans.count_ops())\n",
        "qc_trans.draw(\"mpl\", fold=-1, idle_wires=False, scale=0.5)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5395cc4a-ce59-4f8f-a126-1f8ca5507f83",
      "metadata": {},
      "source": [
        "Tras la optimización, nuestra profundidad transpilada de dos qubits se reduce aún más.\n",
        "\n",
        "<span id=\"43-step-3-execute-using-an-ibm-quantum-primitive\" />\n",
        "\n",
        "### 4.3 Paso 3. Ejecutar utilizando una primitiva « IBM Quantum »\n",
        "\n",
        "Ahora creamos PUBs para su ejecución con Estimator.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 82,
      "id": "5d949e77-d7af-47aa-91e1-40fcf5940996",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Define observables to measure for S\n",
        "observable_S_real = \"I\" * (n_qubits) + \"X\"\n",
        "observable_S_imag = \"I\" * (n_qubits) + \"Y\"\n",
        "\n",
        "observable_op_real = SparsePauliOp(\n",
        "    observable_S_real\n",
        ")  # define a sparse pauli operator for the observable\n",
        "observable_op_imag = SparsePauliOp(observable_S_imag)\n",
        "\n",
        "layout = qc_trans.layout  # get layout of transpiled circuit\n",
        "observable_op_real = observable_op_real.apply_layout(\n",
        "    layout\n",
        ")  # apply physical layout to the observable\n",
        "observable_op_imag = observable_op_imag.apply_layout(layout)\n",
        "observable_S_real = (\n",
        "    observable_op_real.paulis.to_labels()\n",
        ")  # get the label of the physical observable\n",
        "observable_S_imag = observable_op_imag.paulis.to_labels()\n",
        "\n",
        "observables_S = [[observable_S_real], [observable_S_imag]]\n",
        "\n",
        "\n",
        "# Define observables to measure for H\n",
        "# Hamiltonian terms to measure\n",
        "observable_list = []\n",
        "for pauli, coeff in zip(H_op.paulis, H_op.coeffs):\n",
        "    # print(pauli)\n",
        "    observable_H_real = pauli[::-1].to_label() + \"X\"\n",
        "    observable_H_imag = pauli[::-1].to_label() + \"Y\"\n",
        "    observable_list.append([observable_H_real])\n",
        "    observable_list.append([observable_H_imag])\n",
        "\n",
        "layout = qc_trans.layout\n",
        "\n",
        "observable_trans_list = []\n",
        "for observable in observable_list:\n",
        "    observable_op = SparsePauliOp(observable)\n",
        "    observable_op = observable_op.apply_layout(layout)\n",
        "    observable_trans_list.append([observable_op.paulis.to_labels()])\n",
        "\n",
        "observables_H = observable_trans_list\n",
        "\n",
        "\n",
        "# Define a sweep over parameter values\n",
        "params = np.vstack(parameters).T\n",
        "\n",
        "\n",
        "# Estimate the expectation value for all combinations of\n",
        "# observables and parameter values, where the pub result will have\n",
        "# shape (# observables, # parameter values).\n",
        "pub = (qc_trans, observables_S + observables_H, params)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e0f30365-57d2-4ece-ba62-4e534a810de6",
      "metadata": {},
      "source": [
        "Los circuitos de $t=0$ se pueden calcular de forma clásica. Realizamos esta operación antes de pasar al caso $t\\neq 0$ utilizando un ordenador cuántico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 83,
      "id": "298c43a4-e54c-4ac3-95bd-12f535d44790",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "(10+0j)\n"
          ]
        }
      ],
      "source": [
        "from qiskit.quantum_info import StabilizerState, Pauli\n",
        "\n",
        "\n",
        "qc_cliff = qc.assign_parameters({t: 0})\n",
        "\n",
        "\n",
        "# Get expectation values from experiment\n",
        "S_expval_real = StabilizerState(qc_cliff).expectation_value(\n",
        "    Pauli(\"I\" * (n_qubits) + \"X\")\n",
        ")\n",
        "S_expval_imag = StabilizerState(qc_cliff).expectation_value(\n",
        "    Pauli(\"I\" * (n_qubits) + \"Y\")\n",
        ")\n",
        "\n",
        "# Get expectation values\n",
        "S_expval = S_expval_real + 1j * S_expval_imag\n",
        "\n",
        "H_expval = 0\n",
        "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n",
        "    # Get expectation values from experiment\n",
        "    expval_real = StabilizerState(qc_cliff).expectation_value(\n",
        "        Pauli(pauli[::-1].to_label() + \"X\")\n",
        "    )\n",
        "    expval_imag = StabilizerState(qc_cliff).expectation_value(\n",
        "        Pauli(pauli[::-1].to_label() + \"Y\")\n",
        "    )\n",
        "    expval = expval_real + 1j * expval_imag\n",
        "\n",
        "    # Fill-in matrix elements\n",
        "    H_expval += coeff * expval\n",
        "\n",
        "\n",
        "print(H_expval)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9b86af1c-7e37-427d-b4f0-fbca38d45a21",
      "metadata": {},
      "source": [
        "Aunque pudimos reducir nuestra profundidad de compuerta en órdenes de magnitud utilizando la prueba Hadamard eficiente, la profundidad sigue siendo suficiente para requerir una mitigación de errores de última generación. A continuación, especificamos los atributos de la mitigación utilizada. Todos los métodos utilizados son importantes, pero merece la pena destacar específicamente [la amplificación probabilística de errores (PEA)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-amplification-pea). Esta potente técnica conlleva una gran sobrecarga cuántica. El cálculo realizado aquí puede tardar 20 minutos o más en ejecutarse en un ordenador cuántico real. Si lo desea, puede jugar con los parámetros siguientes para aumentar o disminuir la precisión y, en consecuencia, la sobrecarga. Los ajustes por defecto que se indican a continuación ofrecen resultados de alta fidelidad.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 84,
      "id": "2dff41e1-d417-4af7-aa6c-537a9b0e0c7c",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Experiment options\n",
        "num_randomizations = 300\n",
        "num_randomizations_learning = 20\n",
        "max_batch_circuits = 20\n",
        "shots_per_randomization = 100\n",
        "learning_pair_depths = [0, 4, 24]\n",
        "noise_factors = [1, 1.3, 1.6]\n",
        "\n",
        "# Base option formatting\n",
        "options = {\n",
        "    # Builtin resilience settings for ZNE\n",
        "    \"resilience\": {\n",
        "        \"measure_mitigation\": True,\n",
        "        \"zne_mitigation\": True,\n",
        "        \"zne\": {\"noise_factors\": noise_factors},\n",
        "        # TREX noise learning configuration\n",
        "        \"measure_noise_learning\": {\n",
        "            \"num_randomizations\": num_randomizations_learning,\n",
        "            \"shots_per_randomization\": shots_per_randomization,\n",
        "        },\n",
        "        # PEA noise model configuration\n",
        "        \"layer_noise_learning\": {\n",
        "            \"max_layers_to_learn\": 10,\n",
        "            \"layer_pair_depths\": learning_pair_depths,\n",
        "            \"shots_per_randomization\": shots_per_randomization,\n",
        "            \"num_randomizations\": num_randomizations_learning,\n",
        "        },\n",
        "    },\n",
        "    # Randomization configuration\n",
        "    \"twirling\": {\n",
        "        \"num_randomizations\": num_randomizations,\n",
        "        \"shots_per_randomization\": shots_per_randomization,\n",
        "        \"strategy\": \"all\",\n",
        "    },\n",
        "    # Experimental settings for PEA method\n",
        "    \"experimental\": {\n",
        "        # # Just in case, disable any further qiskit transpilation not related to twirling / DD\n",
        "        # \"skip_transpilation\": True,\n",
        "        # Execution configuration\n",
        "        \"execution\": {\n",
        "            \"max_pubs_per_batch_job\": max_batch_circuits,\n",
        "            \"fast_parametric_update\": True,\n",
        "        },\n",
        "        # Error Mitigation configuration\n",
        "        \"resilience\": {\n",
        "            # ZNE Configuration\n",
        "            \"zne\": {\n",
        "                \"amplifier\": \"pea\",\n",
        "                \"return_all_extrapolated\": True,\n",
        "                \"return_unextrapolated\": True,\n",
        "                \"extrapolated_noise_factors\": [0] + noise_factors,\n",
        "            }\n",
        "        },\n",
        "    },\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5932dc29-7905-4f54-9c97-2bb0fc9c1e39",
      "metadata": {},
      "source": [
        "Por último, ejecutamos los circuitos para $\\tilde{S}$ y $\\tilde{H}$ con Estimator.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 85,
      "id": "01a41068-453f-4ff6-9664-c73be1965dc7",
      "metadata": {},
      "outputs": [],
      "source": [
        "# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate.\n",
        "# Your execution time may vary.\n",
        "\n",
        "with Batch(backend=backend) as batch:\n",
        "    # Estimator\n",
        "    estimator = Estimator(mode=batch, options=options)\n",
        "\n",
        "    job = estimator.run([pub], precision=1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "936865a2-828d-4a45-987e-b62cce0535da",
      "metadata": {},
      "source": [
        "<span id=\"44-step-4-post-process-and-analyze-results\" />\n",
        "\n",
        "### 4.4 Paso 4. Procesar y analizar los resultados\n",
        "\n",
        "Lo que hemos obtenido del ordenador cuántico son los elementos matriciales individuales de $\\tilde{S}$ y los grupos conmutativos de Pauli que componen los elementos matriciales de $\\tilde{H}$. Estos términos deben combinarse para recuperar nuestras matrices, de modo que podamos resolver el problema generalizado de valores propios.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 86,
      "id": "28ed9319-dd36-4104-aba0-f8798cbbd2b6",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Store the outputs as 'results'.\n",
        "results = job.result()[0]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1d3c0af8-fb06-415c-b591-f8e8faedc070",
      "metadata": {},
      "source": [
        "<span id=\"calculate-effective-hamiltonian-and-overlap-matrices\" />\n",
        "\n",
        "#### Calcular el hamiltoniano efectivo y las matrices de superposición\n",
        "\n",
        "En primer lugar, calcule la fase acumulada por el estado $\\vert 0 \\rangle$ durante la evolución temporal no controlada\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 87,
      "id": "d36e8b32-621d-44da-808d-297c0b60754a",
      "metadata": {},
      "outputs": [],
      "source": [
        "prefactors = [\n",
        "    np.exp(-1j * sum([c for p, c in H_op.to_list() if \"Z\" in p]) * i * dt)\n",
        "    for i in range(1, krylov_dim)\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f20402f5-1264-4eeb-9d41-8dd6e53e82d3",
      "metadata": {},
      "source": [
        "Una vez que tenemos los resultados de las ejecuciones de los circuitos podemos post-procesar los datos para calcular los elementos de la matriz de $S$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 88,
      "id": "d1ff8971-2f80-4130-b7f2-e95c3a1b476d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Assemble S, the overlap matrix of dimension D:\n",
        "S_first_row = np.zeros(krylov_dim, dtype=complex)\n",
        "S_first_row[0] = 1 + 0j\n",
        "\n",
        "# Add in ancilla-only measurements:\n",
        "for i in range(krylov_dim - 1):\n",
        "    # Get expectation values from experiment\n",
        "    expval_real = results.data.evs[0][0][i]  # automatic extrapolated evs if ZNE is used\n",
        "    expval_imag = results.data.evs[1][0][i]  # automatic extrapolated evs if ZNE is used\n",
        "\n",
        "    # Get expectation values\n",
        "    expval = expval_real + 1j * expval_imag\n",
        "    S_first_row[i + 1] += prefactors[i] * expval\n",
        "\n",
        "S_first_row_list = S_first_row.tolist()  # for saving purposes\n",
        "\n",
        "\n",
        "S_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n",
        "\n",
        "# Distribute entries from first row across matrix:\n",
        "for i, j in it.product(range(krylov_dim), repeat=2):\n",
        "    if i >= j:\n",
        "        S_circ[j, i] = S_first_row[i - j]\n",
        "    else:\n",
        "        S_circ[j, i] = np.conj(S_first_row[j - i])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 89,
      "id": "bfe2a427-7ec6-48de-9321-fa6bf2dc7c94",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/latex": [
              "$$\n",
              "\\displaystyle \\left[\\begin{matrix}1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i & 0.0940509850777074 - 0.094154537369141 i\\\\0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i\\\\0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i\\\\0.0940509850777074 + 0.094154537369141 i & 0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0\\end{matrix}\\right]\n",
              "$$"
            ],
            "text/plain": [
              "Matrix([\n",
              "[                                     1.0,  0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I, 0.0940509850777074 - 0.094154537369141*I],\n",
              "[ 0.149322296177984 + 0.283023058106896*I,                                      1.0,  0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I],\n",
              "[0.185815978760175 + 0.0910521940394691*I,  0.149322296177984 + 0.283023058106896*I,                                      1.0,  0.149322296177984 - 0.283023058106896*I],\n",
              "[0.0940509850777074 + 0.094154537369141*I, 0.185815978760175 + 0.0910521940394691*I,  0.149322296177984 + 0.283023058106896*I,                                      1.0]])"
            ]
          },
          "execution_count": 89,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from sympy import Matrix\n",
        "\n",
        "Matrix(S_circ)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2a863b6f-183f-4d7a-bac8-399da3e3578e",
      "metadata": {},
      "source": [
        "Y los elementos de la matriz de $\\tilde{H}$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 90,
      "id": "c551fdd0-91ef-4531-83d9-ed399b423bb9",
      "metadata": {},
      "outputs": [],
      "source": [
        "import itertools\n",
        "\n",
        "# Assemble S, the overlap matrix of dimension D:\n",
        "H_first_row = np.zeros(krylov_dim, dtype=complex)\n",
        "H_first_row[0] = H_expval\n",
        "\n",
        "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n",
        "    # Add in ancilla-only measurements:\n",
        "    for i in range(krylov_dim - 1):\n",
        "        # Get expectation values from experiment\n",
        "        expval_real = results.data.evs[2 + 2 * obs_idx][0][\n",
        "            i\n",
        "        ]  # automatic extrapolated evs if ZNE is used\n",
        "        expval_imag = results.data.evs[2 + 2 * obs_idx + 1][0][\n",
        "            i\n",
        "        ]  # automatic extrapolated evs if ZNE is used\n",
        "\n",
        "        # Get expectation values\n",
        "        expval = expval_real + 1j * expval_imag\n",
        "        H_first_row[i + 1] += prefactors[i] * coeff * expval\n",
        "\n",
        "H_first_row_list = H_first_row.tolist()\n",
        "\n",
        "H_eff_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n",
        "\n",
        "# Distribute entries from first row across matrix:\n",
        "for i, j in itertools.product(range(krylov_dim), repeat=2):\n",
        "    if i >= j:\n",
        "        H_eff_circ[j, i] = H_first_row[i - j]\n",
        "    else:\n",
        "        H_eff_circ[j, i] = np.conj(H_first_row[j - i])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 91,
      "id": "d1950927-74d6-4012-81f2-3d0b7f476de8",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/latex": [
              "$$\n",
              "\\displaystyle \\left[\\begin{matrix}10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i & 1.0770511571923 + 0.104340737159455 i\\\\-3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i\\\\0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i\\\\1.0770511571923 - 0.104340737159455 i & 0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0\\end{matrix}\\right]\n",
              "$$"
            ],
            "text/plain": [
              "Matrix([\n",
              "[                                   10.0,  -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I,   1.0770511571923 + 0.104340737159455*I],\n",
              "[ -3.02044405310714 + 2.80721615865252*I,                                    10.0,  -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I],\n",
              "[0.496487054782717 - 0.188101957039621*I,  -3.02044405310714 + 2.80721615865252*I,                                    10.0,  -3.02044405310714 - 2.80721615865252*I],\n",
              "[  1.0770511571923 - 0.104340737159455*I, 0.496487054782717 - 0.188101957039621*I,  -3.02044405310714 + 2.80721615865252*I,                                    10.0]])"
            ]
          },
          "execution_count": 91,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from sympy import Matrix\n",
        "\n",
        "Matrix(H_eff_circ)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7ec60832-f7be-447f-841e-e801241fdeae",
      "metadata": {},
      "source": [
        "Por último, podemos resolver el problema de valores propios generalizado para $\\tilde{H}$ :\n",
        "\n",
        "$\\tilde{H} \\vec{c} = c S \\vec{c}$\n",
        "\n",
        "y obtener una estimación de la energía del estado fundamental $c_{min}$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 92,
      "id": "d6635506-399f-47a3-a837-c5f2558f22b4",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The estimated ground state energy is:  10.0\n",
            "The estimated ground state energy is:  5.933953916292923\n",
            "The estimated ground state energy is:  4.4101773995740645\n",
            "The estimated ground state energy is:  3.921288588521255\n"
          ]
        }
      ],
      "source": [
        "gnd_en_circ_est_list = []\n",
        "for d in range(1, krylov_dim + 1):\n",
        "    # Solve generalized eigenvalue problem\n",
        "    gnd_en_circ_est = solve_regularized_gen_eig(\n",
        "        H_eff_circ[:d, :d], S_circ[:d, :d], threshold=1e-1\n",
        "    )\n",
        "    gnd_en_circ_est_list.append(gnd_en_circ_est)\n",
        "    print(\"The estimated ground state energy is: \", gnd_en_circ_est)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "15865587-85f4-41bc-9bf5-47464cf17298",
      "metadata": {},
      "source": [
        "Para un sector de una sola partícula, podemos calcular eficientemente el estado fundamental de este sector del Hamiltoniano clásicamente\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 93,
      "id": "3303ae29-c288-417a-9cf2-c82d9770de69",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "n_sys_qubits 10\n",
            "n_exc 1 , subspace dimension 11\n",
            "single particle ground state energy:  2.391547869638771\n"
          ]
        }
      ],
      "source": [
        "gs_en = single_particle_gs(H_op, n_qubits)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 94,
      "id": "2f904ea3-38bc-4841-81ce-cdb69f09a0b7",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "27"
            ]
          },
          "execution_count": 94,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "len(H_op)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 95,
      "id": "a5d4a983-1a30-4cea-b695-3e6a67338633",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/krylov/extracted-outputs/a5d4a983-1a30-4cea-b695-3e6a67338633-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "plt.plot(\n",
        "    range(1, krylov_dim + 1),\n",
        "    gnd_en_circ_est_list,\n",
        "    color=\"blue\",\n",
        "    linestyle=\"-.\",\n",
        "    label=\"KQD estimate\",\n",
        ")\n",
        "plt.plot(\n",
        "    range(1, krylov_dim + 1),\n",
        "    [gs_en] * krylov_dim,\n",
        "    color=\"red\",\n",
        "    linestyle=\"-\",\n",
        "    label=\"exact\",\n",
        ")\n",
        "plt.xticks(range(1, krylov_dim + 1), range(1, krylov_dim + 1))\n",
        "plt.legend()\n",
        "plt.xlabel(\"Krylov space dimension\")\n",
        "plt.ylabel(\"Energy\")\n",
        "plt.title(\"Estimating Ground state energy with Krylov Quantum Diagonalization\")\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "543a2d98-1f72-4df1-ae22-f65d60c0d9c5",
      "metadata": {},
      "source": [
        "<span id=\"5-discussion-and-extension\" />\n",
        "\n",
        "## 5. Debate y ampliación\n",
        "\n",
        "Para recapitular, empezamos con un estado de referencia y luego lo evolucionamos durante distintos periodos de tiempo para generar el subespacio unitario de Krylov. Proyectamos nuestro Hamiltoniano sobre ese subespacio. También estimamos los solapamientos de los vectores del subespacio. Por último, resolvemos clásicamente el problema de valores propios generalizado de dimensión inferior.\n",
        "\n",
        "![Un diagrama de flujo general de QKD: empezar con un estado de referencia, evolucionar el estado para aproximar los vectores de Krylov, proyectar en el subespacio de Krylov, diagonalizar el subespacio proyectado clásicamente y determinar las propiedades del estado base.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig7.avif)\n",
        "\n",
        "Comparemos lo que determina los costes computacionales de utilizar la técnica de Krylov de forma clásica y mecánica cuántica. No existen analogías perfectas entre los enfoques clásico y cuántico para todos los pasos. Esta tabla recoge algunas escalas de diferentes pasos para su consideración.\n",
        "\n",
        "![Una tabla que describe el escalado de diferentes procesos de forma clásica y en el enfoque cuántico de los métodos de Krylov. Algunos pasos cuánticos no tienen análogo. Las escalas son las mismas que las indicadas en el texto.](https://eu-de.quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig8.avif)\n",
        "\n",
        "Recordemos que los hamiltonianos suelen tener términos que no pueden medirse simultáneamente (porque no conmutan entre sí). Clasificamos los términos del hamiltoniano en grupos de operadores de Pauli conmutables que pueden medirse simultáneamente, y es posible que necesitemos muchos de estos grupos para tener en cuenta todos los términos que no conmutan entre sí. Para construir $\\tilde{H}$ en un ordenador cuántico se requieren mediciones separadas para cada grupo de cadenas de Pauli conmutativas en el Hamiltoniano, y cada una de ellas requiere muchos disparos. Debemos hacerlo para $r^2$ elementos de matriz diferentes, correspondientes a $r^2$ combinaciones de diferentes factores de evolución temporal. A veces hay maneras de reducir esto, pero en este tratamiento aproximado, el tiempo requerido para esto escala como $N_\\text{shots}\\times N_\\text{GCP} \\times r^2.$ Los elementos de $S$ deben ser estimados, que escala como $O(N_\\text{shots}\\times r^2)$. Por último, la resolución del problema generalizado de valores propios en el espacio proyectado, clásicamente, toma $O(r^3).$\n",
        "\n",
        "Vemos que la diagonalización cuántica de Krylov puede ser útil en casos en los que el número de grupos de Pauli conmutantes en el Hamiltoniano es relativamente pequeño. Estas dependencias de escala sugieren algunas aplicaciones en las que el método de Krylov puede ser útil, y otras en las que probablemente no lo sea.\n",
        "Algunos Hamiltonianos tienen una alta complejidad cuando se mapean a qubits, involucrando muchas cadenas de Pauli no conmutativas que no pueden ser fácilmente particionadas en unos pocos grupos conmutativos. Esto suele ocurrir, por ejemplo, con los problemas de química cuántica. Esta complejidad presenta dos retos principales para los ordenadores cuánticos a corto plazo:\n",
        "\n",
        "* La estimación de cada elemento de $\\tilde{H}$ resulta costosa desde el punto de vista computacional debido al gran número de términos.\n",
        "* Los circuitos Trotter requeridos se vuelven prohibitivamente profundos.\n",
        "\n",
        "Los dos puntos anteriores serán menos problemáticos cuando los ordenadores cuánticos alcancen la tolerancia a fallos, pero hay que tenerlos en cuenta a corto plazo. Incluso los sistemas con mapeos \"más sencillos\" que los de la química cuántica pueden experimentar los mismos impedimentos, si los hamiltonianos tienen demasiados términos no conmutativos.\n",
        "El método de Krylov es más útil cuando el Hamiltoniano puede dividirse en relativamente pocos grupos Pauli conmutativos, y cuando $H$ es fácil de implementar en circuitos trotadores. Ambas condiciones se cumplen, por ejemplo, para muchos modelos reticulares de interés en física. KQD es especialmente útil si se sabe muy poco sobre el estado básico. Esto se debe a sus garantías de convergencia inherentes y a su aplicabilidad en escenarios en los que los métodos alternativos son insostenibles debido a un conocimiento insuficiente del estado del suelo.\n",
        "\n",
        "Aunque el KQD es una herramienta potente, los aspectos del protocolo que consumen mucho tiempo, en particular la estimación de cada elemento del Hamiltoniano proyectado y la superposición de los estados de Krylov, representan oportunidades de mejora. Un enfoque alternativo consiste en aprovechar los métodos de Krylov junto con los métodos basados en el muestreo, que son el tema de la siguiente lección.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7fa7609f-9e96-4b0c-b716-ae9242bb40ca",
      "metadata": {},
      "source": [
        "<span id=\"6-appendices\" />\n",
        "\n",
        "## 6. Apéndices\n",
        "\n",
        "<span id=\"appendix-i-krylov-subspace-from-real-time-evolutions\" />\n",
        "\n",
        "### Apéndice I: Subespacio de Krylov a partir de evoluciones en tiempo real\n",
        "\n",
        "El espacio unitario de Krylov se define como\n",
        "\n",
        "$$\n",
        "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ |\\psi\\rangle,  e^{-iH\\,dt} |\\psi\\rangle, \\dots, e^{-irH\\,dt} |\\psi\\rangle \\right\\}\n",
        "$$\n",
        "\n",
        "para algún paso temporal $dt$ que determinaremos más adelante. Supongamos temporalmente que $r$ es par: definamos entonces $d=r/2$. Obsérvese que cuando proyectamos el Hamiltoniano en el espacio de Krylov anterior, es indistinguible del espacio de Krylov\n",
        "\n",
        "$$\n",
        "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ e^{i\\,d\\,H\\,dt}|\\psi\\rangle,  e^{i(d-1)H\\,dt} |\\psi\\rangle, \\dots, e^{-i(d-1)H\\,dt} |\\psi\\rangle, e^{-i\\,d\\,H\\,dt} |\\psi\\rangle \\right\\},\n",
        "$$\n",
        "\n",
        "es decir, donde todas las evoluciones temporales se desplazan hacia atrás $d$ timesteps.\n",
        "La razón por la que es indistinguible es porque los elementos de la matriz\n",
        "\n",
        "$$\n",
        "\\tilde{H}_{j,k} = \\langle\\psi|e^{i\\,j\\,H\\,dt}He^{-i\\,k\\,H\\,dt}|\\psi\\rangle=\\langle\\psi|He^{i(j-k)H\\,dt}|\\psi\\rangle\n",
        "$$\n",
        "\n",
        "son invariantes bajo desplazamientos globales del tiempo de evolución, ya que las evoluciones temporales conmutan con el Hamiltoniano. Para los impares $r$, podemos utilizar el análisis para $r-1$.\n",
        "\n",
        "Queremos demostrar que en algún lugar de este espacio de Krylov está garantizada la existencia de un estado de baja energía. Lo hacemos mediante el siguiente resultado, que se deriva del Teorema 3.1 de [\\[3\\]](#references) :\n",
        "\n",
        "**Afirmación 1:** existe una función $f$ tal que para energías $E$ en el rango espectral del Hamiltoniano (es decir, entre la energía del estado fundamental y la energía máxima)...\n",
        "\n",
        "1. $f(E_0)=1$\n",
        "2. $|f(E)|\\le2\\left(1 + \\delta\\right)^{-d}$ para todos los valores de $E$ que se encuentran a $\\ge\\delta$ de $E_0$, es decir, se suprime exponencialmente\n",
        "3. $f(E)$ es una combinación lineal de $e^{ijE\\,dt}$ para $j=-d,-d+1,...,d-1,d$\n",
        "\n",
        "A continuación ofrecemos una demostración, pero puede omitirse sin problemas a menos que se desee comprender el argumento completo y riguroso. Por ahora nos centraremos en las implicaciones de la afirmación anterior. Por la propiedad 3 anterior, podemos ver que el espacio de Krylov desplazado anterior contiene el estado $f(H)|\\psi\\rangle$. Este es nuestro estado de baja energía. Para ver por qué, escriba $|\\psi\\rangle$ en la base propia de energía:\n",
        "\n",
        "$$\n",
        "|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_k|E_k\\rangle,\n",
        "$$\n",
        "\n",
        "donde $|E_k\\rangle$ es el k-ésimo eigenestado energético y $\\gamma_k$ es su amplitud en el estado inicial $|\\psi\\rangle$. Expresado en estos términos, $f(H)|\\psi\\rangle$ viene dado por\n",
        "\n",
        "$$\n",
        "f(H)|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_kf(E_k)|E_k\\rangle,\n",
        "$$\n",
        "\n",
        "utilizando el hecho de que podemos sustituir $H$ por $E_k$ cuando actúa sobre el estado propio $|E_k\\rangle$. Por tanto, el error energético de este estado es\n",
        "\n",
        "$$\n",
        "\\text{energy error} = \\frac{\\langle\\psi|f(H)(H-E_0)f(H)|\\psi\\rangle}{\\langle\\psi|f(H)^2|\\psi\\rangle}\n",
        "$$\n",
        "\n",
        "$$\n",
        "= \\frac{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n",
        "$$\n",
        "\n",
        "Para convertir esto en un límite superior más fácil de entender, primero separamos la suma en el numerador en términos con $E_k-E_0\\le\\delta$ y términos con $E_k-E_0>\\delta$ :\n",
        "\n",
        "$$\n",
        "\\text{energy error} = \\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} + \\frac{\\sum_{E_k> E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n",
        "$$\n",
        "\n",
        "Podemos acotar el primer término en $\\delta$,\n",
        "\n",
        "$$\n",
        "\\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} < \\frac{\\delta\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} \\le \\delta,\n",
        "$$\n",
        "\n",
        "donde el primer paso se sigue porque $E_k-E_0\\le\\delta$ para cada $E_k$ en la suma, y el segundo paso se sigue porque la suma en el numerador es un subconjunto de la suma en el denominador. Para el segundo término, primero acotamos a la baja el denominador en $|\\gamma_0|^2$, ya que $f(E_0)^2=1$ : sumando todo, se obtiene\n",
        "\n",
        "$$\n",
        "\\text{energy error} \\le \\delta + \\frac{1}{|\\gamma_0|^2}\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0).\n",
        "$$\n",
        "\n",
        "Para simplificar lo que queda, observe que para todos estos $E_k$, por la definición de $f$ sabemos que $f(E_k)^2 \\le 4\\left(1 + \\delta\\right)^{-2d}$. Además, si acotamos $E_k-E_0<2\\|H\\|$ y acotamos $\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2<1$, obtenemos\n",
        "\n",
        "$$\n",
        "\\text{energy error} \\le \\delta + \\frac{8}{|\\gamma_0|^2}\\|H\\|\\left(1 + \\delta\\right)^{-2d}.\n",
        "$$\n",
        "\n",
        "Esto es válido para cualquier $\\delta>0$, así que si fijamos $\\delta$ igual a nuestro error objetivo, entonces el límite de error anterior converge hacia él exponencialmente con la dimensión de Krylov $2d=r$. Obsérvese también que si $\\delta<E_1-E_0$, el término $\\delta$ desaparece por completo en el límite anterior.\n",
        "\n",
        "Para completar el argumento, primero observamos que lo anterior es sólo el error de energía del estado particular $f(H)|\\psi\\rangle$, en lugar del error de energía del estado de menor energía en el espacio de Krylov. Sin embargo, por el principio variacional (Rayleigh-Ritz), el error de energía del estado de menor energía en el espacio de Krylov está acotado superiormente por el error de energía de cualquier estado en el espacio de Krylov, por lo que lo anterior es también un límite superior en el error de energía del estado de menor energía, es decir, la salida del algoritmo de diagonalización cuántica de Krylov.\n",
        "\n",
        "Se puede realizar un análisis similar al anterior que, además, tenga en cuenta el ruido y el procedimiento de umbralización comentado en el cuaderno. Véase [\\[2\\]](#references) y [\\[4\\]](#references) para este análisis.\n",
        "\n",
        "<span id=\"appendix-ii-proof-of-claim-1\" />\n",
        "\n",
        "### Apéndice II: prueba de la reclamación 1\n",
        "\n",
        "Lo siguiente se deriva en su mayor parte de [\\[3\\]](#references), Teorema 3.1: sea $0 < a < b$ y sea $\\Pi^*_d$ el espacio de polinomios residuales (polinomios cuyo valor en 0 es 1) de grado como máximo $d$. La solución de\n",
        "\n",
        "$$\n",
        "\\beta(a, b, d) = \\min_{p \\in \\Pi^*_d} \\max_{x \\in [a, b]} |p(x)| \\quad\n",
        "$$\n",
        "\n",
        "es\n",
        "\n",
        "$$\n",
        "p^*(x) = \\frac{T_d\\left(\\frac{b + a - 2x}{b - a}\\right)}{T_d\\left(\\frac{b + a}{b - a}\\right)}, \\quad\n",
        "$$\n",
        "\n",
        "y el valor mínimo correspondiente es\n",
        "\n",
        "$$\n",
        "\\beta(a, b, d) = T_d^{-1}\\left(\\frac{b + a}{b - a}\\right).\n",
        "$$\n",
        "\n",
        "Queremos convertir esto en una función que pueda expresarse naturalmente en términos de exponenciales complejos, porque esas son las evoluciones temporales reales que generan el espacio cuántico de Krylov.\n",
        "Para ello, es conveniente introducir la siguiente transformación de energías dentro del rango espectral del Hamiltoniano a números en el rango $[0,1]$ : definir\n",
        "\n",
        "$$\n",
        "g(E) = \\frac{1-\\cos\\big((E-E_0)dt\\big)}{2},\n",
        "$$\n",
        "\n",
        "donde $dt$ es un paso de tiempo tal que $-\\pi < E_0dt < E_\\text{max}dt < \\pi$. Obsérvese que $g(E_0)=0$ y $g(E)$ crecen a medida que $E$ se aleja de $E_0$.\n",
        "\n",
        "Ahora, utilizando el polinomio $p^*(x)$ con los parámetros a, b, d fijados en $a = g(E_0 + \\delta)$, $b = 1$, y d = int( r/2 ), definimos la función:\n",
        "\n",
        "$$\n",
        "f(E) = p^* \\left( g(E) \\right) = \\frac{T_d\\left(1 + 2\\frac{\\cos\\big((E-E_0)dt\\big) - \\cos\\big(\\delta\\,dt\\big)}{1 +\\cos\\big(\\delta\\,dt\\big)}\\right)}{T_d\\left(1 + 2\\frac{1-\\cos\\big(\\delta\\,dt\\big)}{1 + \\cos\\big(\\delta\\,dt\\big)}\\right)}\n",
        "$$\n",
        "\n",
        "donde $E_0$ es la energía del estado básico. Podemos ver insertando $\\cos(x)=\\frac{e^{ix}+e^{-ix}}{2}$ que $f(E)$ es un polinomio trigonométrico de grado $d$, es decir, una combinación lineal de $e^{ijE\\,dt}$ para $j=-d,-d+1,...,d-1,d$. Además, a partir de la definición de $p^*(x)$ anterior tenemos que $f(E_0)=p(0)=1$ y para cualquier $E$ en el rango espectral tal que $\\vert E-E_0 \\vert > \\delta$ tenemos\n",
        "\n",
        "$$\n",
        "|f(E)| \\le \\beta(a, b, d) = T_d^{-1}\\left(1 + 2\\frac{1-\\cos\\big(\\delta\\,dt\\big)}{1 + \\cos\\big(\\delta\\,dt\\big)}\\right)\n",
        "$$\n",
        "\n",
        "$$\n",
        "\\leq 2\\left(1 + \\delta\\right)^{-d} = 2\\left(1 + \\delta\\right)^{-\\lfloor k/2\\rfloor}.\n",
        "$$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "97d292db-83bb-431c-97e3-91212e168ab8",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referencias:\n",
        "\n",
        "\\[1] [https://arxiv.org/abs/2407.14431](https://arxiv.org/abs/2407.14431)\n",
        "\n",
        "\\[2] [https://arxiv.org/abs/1811.09025](https://arxiv.org/abs/1811.09025)\n",
        "\n",
        "\\[3] [https://people.math.ethz.ch/\\~mhg/pub/biksm.pdf](https://people.math.ethz.ch/~mhg/pub/biksm.pdf)\n",
        "\n",
        "\\[4] [https://academic.oup.com/book/36426](https://academic.oup.com/book/36426)\n",
        "\n",
        "\\[5] [https://en.wikipedia.org/wiki/Krylov\\_subespacio](https://en.wikipedia.org/wiki/Krylov_subspace)\n",
        "\n",
        "\\[6] Métodos de subespacios de Krylov: Principios y análisis, Jörg Liesen, Zdenek Strakos [https://academic.oup.com/book/36426](https://academic.oup.com/book/36426)\n",
        "\n",
        "\\[7] Métodos iterativos para sistemas lineales dispersos\" por Yousef Saad\n",
        "\n",
        "\\[8] \"MINRES-QLP: A Krylov Subspace Method for Indefinite or Singular Symmetric Systems\" por Sou-Cheng Choi, Christopher Paige, y Michael Saunders ( [https://epubs.siam.org/doi/10.1137/100787921](https://epubs.siam.org/doi/10.1137/100787921) )\n",
        "\n",
        "\\[9] Ethan N. Epperly, Lin Lin y Yuji Nakatsukasa. \"Una teoría de diagonalización de subespacios cuánticos\". SIAM Journal on Matrix Analysis and Applications 43, 1263-1290 (2022).\n",
        "\n",
        "\\[10] [https://link.aps.org/doi/10.1103/PRXQuantum.4.030319](https://link.aps.org/doi/10.1103/PRXQuantum.4.030319)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "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"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 2
}