{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "454a9dfd",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Fórmulas multiproducto para reducir el error de Trotter\"\n",
        "description: \"Utiliza fórmulas multiproducto en la estimación de observables para reducir el error de Trotter o implementa la evolución temporal con un error de Trotter fijo en una profundidad menor.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore ncol circo Layerwise markersize unbiasedness infty ndash lesssim propto tenpy unfused Néel correlator Neel exponentiating gtrsim */}\n",
        "\n",
        "<span id=\"multi-product-formulas-to-reduce-trotter-error\" />\n",
        "\n",
        "# Fórmulas multiproducto para reducir el error de Trotter\n",
        "\n",
        "*Estimación de tiempo de ejecución: cuatro minutos en un procesador Heron r2 (NOTA: Se trata únicamente de una estimación). (El tiempo de ejecución puede variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c4d0b2f2",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados del aprendizaje\n",
        "\n",
        "* Cómo las fórmulas multiproducto (MPF) reducen el error de Trotter en la simulación hamiltoniana mediante la combinación de los valores esperados de múltiples circuitos poco profundos\n",
        "* Cuándo los MPF resultan más ventajosos que las fórmulas estándar de los productos y cuándo no son la herramienta adecuada\n",
        "* Cómo calcular los coeficientes MPF estáticos y dinámicos utilizando el `qiskit_addon_mpf` paquete\n",
        "* Cómo ejecutar un flujo de trabajo MPF de principio a fin en un hardware d IBM Quantum®, incluyendo la transpilación, la corrección de errores y el posprocesamiento\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f5dfd316",
      "metadata": {},
      "source": [
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Requisitos previos\n",
        "\n",
        "* [Métodos de compilación para circuitos de simulación hamiltonianos](/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits) : introducción a los circuitos de Trotter (fórmula del producto) en Qiskit.\n",
        "* Fórmulas de productos en Qiskit, en concreto las [`SuzukiTrotter`](/docs/api/qiskit/qiskit.synthesis.SuzukiTrotter) clases de síntesis y [`LieTrotter`](/docs/api/qiskit/qiskit.synthesis.LieTrotter) .\n",
        "* [Qiskit primitives y la interfaz «Estimator](/docs/guides/primitives) ».\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "07273b26",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## En segundo plano\n",
        "\n",
        "<span id=\"what-are-multi-product-formulas\" />\n",
        "\n",
        "### ¿Qué son las fórmulas multiproducto?\n",
        "\n",
        "Al simular sistemas cuánticos en un ordenador cuántico, una tarea fundamental consiste en aproximar el operador de evolución temporal $e^{-iHt}$ para un hamiltoniano $H$. El enfoque estándar utiliza *fórmulas de producto* (PF), también conocidas como descomposiciones de Trotter-Suzuki. Estos descomponen $H = \\sum_{a=1}^d F_a$ en términos cuyos operadores unitarios individuales $e^{-iF_a t}$ son eficientes de implementar, y a continuación aproximan la evolución completa como un producto ordenado de estos operadores unitarios más sencillos.\n",
        "\n",
        "La fórmula del producto de primer orden (Lie-Trotter) es:\n",
        "\n",
        "$$\n",
        "S_1(t) := \\prod_{a=1}^d e^{-i F_a t},\n",
        "$$\n",
        "\n",
        "lo que da lugar a un error cuadrático: $S_1(t) = e^{-iHt} + \\mathcal{O}(t^2)$. Las fórmulas simétricas de orden superior $S_{2\\chi}(t)$, donde $\\chi$ indica el orden de la fórmula del producto simétrico (véase la ref. [\\[1\\]](#references) ), convergen más rápidamente, como se muestra en $e^{-iHt} + \\mathcal{O}(t^{2\\chi+1})$, pero a costa de circuitos más complejos por paso.\n",
        "\n",
        "Para reducir el error en un orden *fijo* $\\chi$, normalmente se divide el tiempo total de evolución $t$ en $k$ pasos de Trotter más pequeños. Cada paso aproxima $e^{-iHt/k}$ mediante una fórmula de producto y los pasos se encadenan:\n",
        "\n",
        "$$\n",
        "e^{-iHt} \\approx \\left[S_{2\\chi}(t/k)\\right]^k.\n",
        "$$\n",
        "\n",
        "Para una fórmula simétrica de orden $2\\chi$, el error residual de Trotter varía entonces proporcionalmente a $\\mathcal{O}\\!\\left(t^{2\\chi+1} / k^{2\\chi}\\right)$. Por lo tanto, al aumentar $k$ se reduce rápidamente el error de Trotter, pero también se aumenta linealmente la profundidad del circuito, lo que, en un hardware con ruido, se traduce en un mayor ruido acumulado en las puertas. Esta tensión entre **el error de Trotter (que favorece valores más grandes de $k$ )** y **el ruido del hardware (que favorece valores más pequeños de $k$ )** es precisamente lo que las fórmulas multiproducto están diseñadas para resolver. Ten en cuenta que las MPF consisten en combinar los resultados de *diferentes opciones de $k$* en un orden fijo $\\chi$; no modifican el orden de la fórmula del producto subyacente.\n",
        "\n",
        "**Las fórmulas multiproducto (MPF)** [\\[1\\]](#references) construyen una *combinación lineal ponderada* de los valores esperados obtenidos a partir de varios circuitos de Trotter menos profundos, cada uno de los cuales utiliza un número diferente de pasos de Trotter $k_1, k_2, \\ldots, k_r$ (un conjunto de recuentos de pasos $r$ ):\n",
        "\n",
        "$$\n",
        "\\langle A \\rangle_{\\text{MPF}}(t) = \\sum_{j=1}^r x_j \\, \\langle A \\rangle_{k_j}(t),\n",
        "$$\n",
        "\n",
        "donde $\\langle A \\rangle_{k_j}(t)$ es el valor esperado de un observable $A$ en el instante $t$, estimado a partir de un circuito de Trotter con $k_j$ pasos, y los coeficientes $\\{x_j\\}_{j=1}^r$ se eligen de tal forma que los términos principales del error de Trotter en la combinación se anulen. Volveremos sobre esta expresión en [el paso 4](#small-scale-step-4), donde la evaluaremos explícitamente para combinar nuestros resultados de Trotter. El aspecto práctico clave es que el circuito más profundo del MPF solo necesita un $k_{\\max}$ es pasos, lo cual es mucho menor que el único $k$ que se necesitaría para alcanzar directamente el mismo error efectivo de Trotter. Los circuitos menos profundos hacen que el enfoque MPF sea más adecuado para hardware ruidoso.\n",
        "\n",
        "<span id=\"how-are-the-coefficients-determined\" />\n",
        "\n",
        "### ¿Cómo se determinan los coeficientes?\n",
        "\n",
        "Existen dos familias de coeficientes MPF:\n",
        "\n",
        "**Los coeficientes estáticos** son independientes del hamiltoniano, del estado inicial y del tiempo de evolución. Se obtienen resolviendo un sistema lineal $Ax = b$ que garantiza la cancelación de los términos principales del error de Trotter. Para un conjunto de pasos de Trotter $\\{k_j\\}_{j=1}^r$ utilizado con una fórmula de producto simétrico de orden $2\\chi$, al desarrollar el error de Trotter en potencias inversas de $k_j$ se obtienen ecuaciones de restricción de la forma:\n",
        "\n",
        "$$\n",
        "\\sum_{j=1}^r x_j = 1, \\quad \\sum_{j=1}^r \\frac{x_j}{k_j^{\\eta_n}} = 0 \\quad (n = 0, \\ldots, r-2),\n",
        "$$\n",
        "\n",
        "donde los exponentes enteros $\\{\\eta_n\\}$ son los órdenes de los términos sucesivos del error de Trotter para la fórmula de producto elegida. Para un PF *simétrico* de orden $2\\chi$, el error principal en $\\left[S_{2\\chi}(t/k)\\right]^k$ varía proporcionalmente a $1/k^{2\\chi}$, con correcciones posteriores en $1/k^{2\\chi+2}, 1/k^{2\\chi+4}, \\ldots$ — por lo que los exponentes son $\\eta_n = 2\\chi + 2n$. En el caso de los PF no simétricos, contribuyen tanto las potencias impares como las pares y $\\eta_n = 2\\chi + n$. Véase la referencia [\\[1\\]](#references) para la derivación completa. La primera ecuación del sistema anterior garantiza la imparcialidad (el MPF reproduce el valor exacto de la esperanza en el límite « $k_j \\to \\infty$ »), y las ecuaciones restantes $r-1$ cancelan sucesivamente los primeros términos de error de Trotter $r-1$. Cuando la norma $L_1$ resultante $\\|x\\|_1$ es demasiado grande (lo que amplifica el ruido de muestreo), se puede resolver, en su lugar, un problema de optimización aproximada que limite $\\|x\\|_1$ al tiempo que minimice $\\|Ax - b\\|$.\n",
        "\n",
        "**Los coeficientes dinámicos** [\\[2\\]](#references), [\\[3\\]](#references) dependen además del hamiltoniano, del estado inicial y del tiempo de evolución $t$. Estos minimizan la distancia, medida en la norma de Frobenius, entre el estado real evolucionado en el tiempo y la aproximación MPF:\n",
        "\n",
        "$$\n",
        "\\|\\rho(t) - \\mu^D(t)\\|_F^2 = 1 + \\sum_{i,j} M_{ij}(t)\\, x_i(t)\\, x_j(t) - 2\\sum_i L_i(t)\\, x_i(t),\n",
        "$$\n",
        "\n",
        "donde $M_{ij}(t) = \\mathrm{Tr}[\\rho_{k_i}(t)\\,\\rho_{k_j}(t)]$ es la matriz de Gram de solapamientos entre los estados evolucionados según Trotter para diferentes recuentos de pasos $k_i, k_j$, y $L_i(t) = \\mathrm{Tr}[\\rho(t)\\,\\rho_{k_i}(t)]$ mide el solapamiento con el estado exacto (aproximado). En este tutorial, estas magnitudes se calculan de forma eficiente utilizando métodos de redes tensoriales, concretamente los backends de TeNPy-based en `qiskit_addon_mpf`.\n",
        "\n",
        "<span id=\"when-to-use-mpfs\" />\n",
        "\n",
        "### Cuándo utilizar los MPF\n",
        "\n",
        "Los planes de pensiones (MPF) resultan más ventajosos cuando:\n",
        "\n",
        "* **La profundidad del circuito es el cuello de botella.** Si el ruido del hardware limita la profundidad a la que se puede trabajar, utiliza los MPF para conseguir una mayor precisión efectiva de Trotter en circuitos menos profundos.\n",
        "* **Necesitas valores de expectativa precisos, no una preparación completa del estado.** Las MPF operan a nivel de los valores esperados: combinan números clásicos, no estados cuánticos. Por lo tanto, son ideales para la estimación de observables cuando se utiliza la primitiva «Estimator».\n",
        "* **Combinas un número moderado de pasos de trotto.** Por lo general, basta con combinar $r = 3$ – $5$, con diferentes recuentos de pasos $k_j$, para anular varios términos de error de Trotter principales, al tiempo que se mantiene $\\|x\\|_1$ a un nivel manejable.\n",
        "\n",
        "<span id=\"when-mpfs-might-not-help\" />\n",
        "\n",
        "### Cuándo los planes de pensiones de empleo (MPF) podrían no ser de ayuda\n",
        "\n",
        "* **Tiempos de evolución muy cortos.** Cuando $t$ es lo suficientemente pequeño como para que una sola fórmula de Trotter de bajo orden ya sea precisa, no es necesario el esfuerzo adicional que supone ejecutar varios circuitos.\n",
        "* **Tareas de preparación para el examen estatal.** Los MPF producen un *valor esperado* corregido, no un estado cuántico corregido. Si necesitas el estado real a lo largo del tiempo (por ejemplo, como entrada para otra subrutina cuántica), los MPF no son aplicables.\n",
        "* **Recuentos de pasos de trote que incumplen el régimen de convergencia.** La derivación del coeficiente estático expande cada « $\\left[S_{2\\chi}(t/k_j)\\right]^{k_j}$ » individual como una serie en $t/k_j$; esta expansión solo converge bien cuando $t/k_{\\min} \\lesssim 1$. Si se elige un valor demasiado pequeño para $k_{\\min}$ en el caso de un $t$ dado, el circuito más superficial queda muy fuera del régimen perturbativo, los términos de error de orden superior que el MPF deja sin cancelar se vuelven grandes y la cancelación puede requerir coeficientes elevados. La norma « $L_1$ » $\\|x\\|_1$ es el criterio de diagnóstico práctico: cuando $\\|x\\|_1 \\gg 1$, la sobrecarga de muestreo $\\propto \\|x\\|_1^2$ podría superar la reducción del error de Trotter. Consulta la [guía sobre cómo elegir los peldaños Trotter](https://qiskit.github.io/qiskit-addon-mpf/how_tos/choose_trotter_steps.html) para obtener más información.\n",
        "\n",
        "<span id=\"what-this-tutorial-covers\" />\n",
        "\n",
        "### Contenido de este tutorial\n",
        "\n",
        "Este tutorial explica paso a paso un flujo de trabajo completo de MPF en dos fases. En primer lugar, un **ejemplo de simulador a pequeña escala** (cadena de Heisenberg de 10 qubits) muestra cómo plantear el problema, calcular los coeficientes MPF estáticos y dinámicos, y comparar los valores esperados resultantes con los obtenidos mediante la diagonalización exacta. A continuación, un **ejemplo de hardware a gran escala** (cadena XXZ de 50 qubits) muestra cómo realizar la transpilación, ejecutarlo en un hardwar IBM Quantum o con mitigación de errores y procesar posteriormente los resultados utilizando los coeficientes MPF. A lo largo de todo el proceso, utilizamos el `qiskit_addon_mpf` paquete junto con las herramientas estándar de Qiskit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b5d478ce",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de empezar este tutorial, asegúrate de que tienes instalado lo siguiente:\n",
        "\n",
        "* Qiskit SDK v2.0 o posterior, con soporte [para visualización](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.22 o posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Simulador de Qiskit Aer (`pip install qiskit-aer`)\n",
        "* Complemento MPF Qiskit con el backend « TeNPy » (`pip install \"qiskit-addon-mpf[tenpy]\"`)\n",
        "* Utilidades del complemento de Qiskit (`pip install qiskit-addon-utils`)\n",
        "* SciPy (`pip install scipy`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2584c37e",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\n",
        "\n",
        "A continuación, recopilamos en una sola celda *todas* las importaciones de paquetes utilizadas a lo largo de este tutorial. `XXPlusYYGate`También definimos una `CollectAndCollapse` pasada del transpilador que fusiona las rotaciones adyacentes `rxx` y `ryy` en una sola. Esta pasada se aplica tanto durante la construcción del circuito en el paso 1 (para mantener bajo el número de puertas) como, de forma indirecta, cuando extraemos la estructura por capas para el MPF dinámico en el paso 4 (el TeNPy e espera puertas de dos qubits, no pares de rotaciones no fusionadas).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "bf79f9e7",
      "metadata": {},
      "outputs": [],
      "source": [
        "import warnings\n",
        "\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "from functools import partial\n",
        "from copy import deepcopy\n",
        "\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector\n",
        "from qiskit.synthesis import SuzukiTrotter\n",
        "from qiskit.transpiler import CouplingMap, PassManager\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit.circuit.library import XXPlusYYGate\n",
        "from qiskit.transpiler.passes.optimization.collect_and_collapse import (\n",
        "    CollectAndCollapse,\n",
        "    collect_using_filter_function,\n",
        "    collapse_to_operation,\n",
        ")\n",
        "\n",
        "from qiskit_aer import AerSimulator\n",
        "from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService\n",
        "\n",
        "from qiskit_addon_utils.problem_generators import (\n",
        "    generate_xyz_hamiltonian,\n",
        "    generate_time_evolution_circuit,\n",
        ")\n",
        "from qiskit_addon_utils.slicing import slice_by_depth\n",
        "from qiskit_addon_mpf.static import setup_static_lse\n",
        "from qiskit_addon_mpf.dynamic import setup_dynamic_lse\n",
        "from qiskit_addon_mpf.costs import (\n",
        "    setup_exact_problem,\n",
        "    setup_sum_of_squares_problem,\n",
        "    setup_frobenius_problem,\n",
        ")\n",
        "from qiskit_addon_mpf.backends.tenpy_layers import (\n",
        "    LayerModel,\n",
        "    LayerwiseEvolver,\n",
        ")\n",
        "from qiskit_addon_mpf.backends.tenpy_tebd import MPOState, MPS_neel_state\n",
        "\n",
        "from scipy.linalg import expm\n",
        "\n",
        "# Suppress TeNPy's `unit_cell_width` future-API warning. The default\n",
        "# (`unit_cell_width=len(sites)`) is correct for Chain lattices, which is what\n",
        "# `CouplingMap.from_line(...)` produces here, so the warning is informational.\n",
        "warnings.filterwarnings(\n",
        "    \"ignore\",\n",
        "    message=r\".*unit_cell_width.*\",\n",
        "    category=UserWarning,\n",
        ")\n",
        "\n",
        "\n",
        "# --- Helper: collect XX + YY rotations into a single gate ---\n",
        "def filter_function(node):\n",
        "    return node.op.name in {\"rxx\", \"ryy\"}\n",
        "\n",
        "\n",
        "collect_function = partial(\n",
        "    collect_using_filter_function,\n",
        "    filter_function=filter_function,\n",
        "    split_blocks=True,\n",
        "    min_block_size=1,\n",
        ")\n",
        "\n",
        "\n",
        "def collapse_to_xx_plus_yy(block):\n",
        "    param = 0.0\n",
        "    for node in block.data:\n",
        "        param += node.operation.params[0]\n",
        "    return XXPlusYYGate(param)\n",
        "\n",
        "\n",
        "collapse_function = partial(\n",
        "    collapse_to_operation,\n",
        "    collapse_function=collapse_to_xx_plus_yy,\n",
        ")\n",
        "\n",
        "pm = PassManager()\n",
        "pm.append(CollectAndCollapse(collect_function, collapse_function))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "24f08467",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Ejemplo de simulador a pequeña escala\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "378e82ba",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Paso 1: Asignar entradas clásicas a un problema cuántico\n",
        "\n",
        "Comenzamos con un modelo de Heisenberg de 10 qubits en una línea, utilizando el estado de Néel $\\vert 0101\\ldots01 \\rangle$ como estado inicial. El hamiltoniano es:\n",
        "\n",
        "$$\n",
        "\\hat{\\mathcal{H}}_{\\text{Heis}} = J \\sum_{i=1}^{L-1} \\left(X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}\\right),\n",
        "$$\n",
        "\n",
        "donde « $J$ » es la intensidad del acoplamiento entre vecinos más cercanos. Medimos el correlador ZZ $Z_{L/2-1} Z_{L/2}$ en un par de qubits situados en el centro de la cadena, y utilizamos los pasos de Trotter $k_j = [1, 2, 4]$ con una fórmula de producto de segundo orden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "bdd0d4fc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII'],\n",
            "              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n",
            " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n",
            " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])\n"
          ]
        }
      ],
      "source": [
        "L = 10\n",
        "\n",
        "# Generate coupling map and Hamiltonian\n",
        "coupling_map = CouplingMap.from_line(L, bidirectional=False)\n",
        "\n",
        "hamiltonian = generate_xyz_hamiltonian(\n",
        "    coupling_map,\n",
        "    coupling_constants=(1.0, 1.0, 1.0),\n",
        "    ext_magnetic_field=(0.0, 0.0, 0.0),\n",
        ")\n",
        "print(hamiltonian)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "fd3dc9c8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "SparsePauliOp(['IIIIZZIIII'],\n",
            "              coeffs=[1.+0.j])\n"
          ]
        }
      ],
      "source": [
        "# Observable: ZZ on the middle pair of qubits\n",
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n",
        ")\n",
        "print(observable)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "398c33b2",
      "metadata": {},
      "outputs": [],
      "source": [
        "# MPF parameters\n",
        "mpf_trotter_steps = [1, 2, 4]\n",
        "order = 2\n",
        "symmetric = False\n",
        "\n",
        "trotter_times = np.arange(0.5, 1.55, 0.1)\n",
        "exact_evolution_times = np.arange(trotter_times[0], 1.55, 0.05)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1512ba0e",
      "metadata": {},
      "source": [
        "<span id=\"build-trotter-circuits\" />\n",
        "\n",
        "#### Construir circuitos de Trotter\n",
        "\n",
        "Creamos los circuitos aplicando las evoluciones temporales aproximadas de Trotter para cada instante y cada número de pasos de Trotter. El `CollectAndCollapse` paso definido en la sección «Configuración» agrupa las rotaciones XX y YY en puertas únicas de tipo XX+YY, con el fin de facilitar una simulación más eficiente de la red tensorial posteriormente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "1c194d2b",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Initial Neel state preparation\n",
        "initial_state_circ = QuantumCircuit(L)\n",
        "initial_state_circ.x([i for i in range(L) if i % 2 != 0])\n",
        "\n",
        "\n",
        "all_circs = []\n",
        "for total_time in trotter_times:\n",
        "    mpf_trotter_circs = [\n",
        "        generate_time_evolution_circuit(\n",
        "            hamiltonian,\n",
        "            time=total_time,\n",
        "            synthesis=SuzukiTrotter(reps=num_steps, order=order),\n",
        "        )\n",
        "        for num_steps in mpf_trotter_steps\n",
        "    ]\n",
        "\n",
        "    mpf_trotter_circs = pm.run(\n",
        "        mpf_trotter_circs\n",
        "    )  # Collect XX and YY into XX + YY\n",
        "\n",
        "    mpf_circuits = [\n",
        "        initial_state_circ.compose(circuit) for circuit in mpf_trotter_circs\n",
        "    ]\n",
        "    all_circs.append(mpf_circuits)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "c7ee61e7",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/c7ee61e7-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "mpf_circuits[-1].draw(\"mpl\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4cd6c782",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "### Paso 2: Optimizar el problema para la ejecución en hardware cuántico\n",
        "\n",
        "Para el ejemplo a pequeña escala, nos centramos en el simulador Aer. Antes de que los circuitos estén listos para ejecutarse, se producen dos transformaciones:\n",
        "\n",
        "1. **Recopilación de puertas a nivel de simulación hamiltoniana.** `XXPlusYYGate`En la celda «Configuración» hemos creado un `CollectAndCollapse` paso que fusiona las rotaciones adyacentes `rxx` y `ryy` en una sola. Ya aplicamos esta pasada cuando creamos los circuitos de Trotter en el paso 1 (la `pm.run(...)` llamada). Esto reduce el número de puertas de dos qubits y da lugar a una estructura más adecuada para la simulación mediante redes tensoriales, con vistas al cálculo posterior de los coeficientes dinámicos.\n",
        "\n",
        "2. **Reducción a la ISA del simulador.** A continuación, ejecutamos el gestor de pasadas predefinido de Qiskit para `optimization_level=3` adaptar cada circuito de Trotter a la arquitectura del conjunto de instrucciones (ISA) del simulador.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "03590a05",
      "metadata": {},
      "outputs": [],
      "source": [
        "aer_sim = AerSimulator()\n",
        "pm_sim = generate_preset_pass_manager(backend=aer_sim, optimization_level=3)\n",
        "\n",
        "isa_circs_all_times = [\n",
        "    pm_sim.run([deepcopy(c) for c in mpf_circuits])\n",
        "    for mpf_circuits in all_circs\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ab6588d3",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Paso 3: Ejecutar utilizando Qiskit primitives\n",
        "\n",
        "Para el ejemplo a pequeña escala, aplicamos los circuitos de Trotter reducidos a ISA a la `EstimatorV2` primitiva respaldada por Aer. De este modo, obtenemos un valor de referencia *sin ruido* para cada par « $(k_j, t)$ »; estos son los valores « $\\langle A \\rangle_{k_j}(t)$ » que el MPF combinará en el paso 4. Analizamos los periodos de evolución para poder trazar posteriormente la curva completa de la serie temporal de cada fórmula de producto individual y de la MPF.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "7225d782",
      "metadata": {},
      "outputs": [],
      "source": [
        "estimator = Estimator(mode=aer_sim)\n",
        "\n",
        "mpf_expvals_all_times, mpf_stds_all_times = [], []\n",
        "for isa_circuits in isa_circs_all_times:\n",
        "    result = estimator.run(\n",
        "        [(circuit, observable) for circuit in isa_circuits], precision=0.005\n",
        "    ).result()\n",
        "    mpf_expvals_all_times.append([res.data.evs for res in result])\n",
        "    mpf_stds_all_times.append([res.data.stds for res in result])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a384f017",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-step-4\" />\n",
        "\n",
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado\n",
        "\n",
        "El paso 4 es donde se construye realmente el MPF. Aunque aquí se *calculan* los coeficientes $x_j$ (y, en el caso de la variante dinámica, este cálculo puede ser muy exigente), conceptualmente constituyen una fórmula clásica para combinar las mediciones cuánticas del paso 3 en un único valor esperado corregido; por lo tanto, consideramos que todo el proceso de cálculo de coeficientes y combinación forma parte del posprocesamiento.\n",
        "\n",
        "Para evaluar en qué medida el MPF reproduce la dinámica real, calculamos en primer lugar los valores esperados exactos a lo largo del tiempo elevando directamente el hamiltoniano a una potencia exponencial. Esto solo es factible porque $L = 10$; en el ejemplo de hardware a gran escala que se muestra a continuación, tendremos que recurrir, en su lugar, a estimaciones basadas en redes tensoriales.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "a223a360",
      "metadata": {},
      "outputs": [],
      "source": [
        "exact_expvals = []\n",
        "for t in exact_evolution_times:\n",
        "    exp_H = expm(-1j * t * hamiltonian.to_matrix())\n",
        "    initial_state = Statevector(initial_state_circ).data\n",
        "    time_evolved_state = exp_H @ initial_state\n",
        "\n",
        "    exact_obs = (\n",
        "        time_evolved_state.conj()\n",
        "        @ observable.to_matrix()\n",
        "        @ time_evolved_state\n",
        "    ).real\n",
        "    exact_expvals.append(exact_obs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "361e726e",
      "metadata": {},
      "source": [
        "<span id=\"static-mpf-coefficients\" />\n",
        "\n",
        "#### Coeficientes MPF estáticos\n",
        "\n",
        "Los MPF estáticos utilizan coeficientes $x_j$ que son independientes del tiempo de evolución, del hamiltoniano y del estado inicial. Establecemos el sistema lineal $Ax = b$ descrito en la sección «Antecedentes» y resolvemos los coeficientes. La matriz $A$ viene determinada por el número de pasos de Trotter $k_j$, el orden $\\chi$ de la fórmula del producto y si la fórmula es simétrica (lo que determina los exponentes $\\eta_n$ ).\n",
        "\n",
        "Para nuestro ejemplo a pequeña escala utilizamos un modelo de tipo « $k_j = [1, 2, 4]$ » con una fórmula de Suzuki-Trotter de orden no simétrico — $2\\chi=2$ — (por lo que $\\chi=1$ y $\\eta_n = 2 + n$, lo que da $\\eta_0 = 2,\\, \\eta_1 = 3$ ). El sistema queda así:\n",
        "\n",
        "$$\n",
        "A =\n",
        "\\begin{bmatrix}\n",
        "1 & 1 & 1\\\\\n",
        "1 & \\frac{1}{2^2} & \\frac{1}{4^2}  \\\\\n",
        "1 & \\frac{1}{2^3} & \\frac{1}{4^3}  \\\\\n",
        "\\end{bmatrix}, \\quad\n",
        "b =\n",
        "\\begin{bmatrix}\n",
        "1 \\\\\n",
        "0 \\\\\n",
        "0\n",
        "\\end{bmatrix}.\n",
        "$$\n",
        "\n",
        "La primera fila garantiza la imparcialidad ( $\\sum_j x_j = 1$ ); la segunda y la tercera fila eliminan, respectivamente, los términos de error de Trotter de primer orden $1/k^2$ y de orden siguiente $1/k^3$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f2ca1e2",
      "metadata": {},
      "source": [
        "<span id=\"set-up-the-lse\" />\n",
        "\n",
        "##### Configurar el LSE\n",
        "\n",
        "Utilizamos `setup_static_lse` desde `qiskit_addon_mpf.static` para construir la matriz $A$ y el vector del lado derecho $b$ descritos anteriormente. La matriz $A$ depende no solo de $k_j$, sino también de la fórmula del producto que elijamos —en concreto, de su *orden* $\\chi$ y de si es *simétrica—*. El `symmetric` indicador controla el patrón del exponente $\\eta_n$ (las fórmulas simétricas solo producen términos de error de Trotter de potencia par; véase la ref. [\\[1\\]](#references) ). Cabe señalar que, tal y como se muestra en la ref. [\\[2\\]](#references), establecer `symmetric=True` no es estrictamente necesario, incluso cuando la PF subyacente es simétrica: la LSE no simétrica sigue siendo válida (aunque impone restricciones adicionales innecesarias).\n",
        "\n",
        "Para nuestro ejemplo, ya hemos establecido `order = 2` y `symmetric = False` en el paso 1.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "827b0b42",
      "metadata": {},
      "outputs": [],
      "source": [
        "lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "003e4bdd",
      "metadata": {},
      "source": [
        "Comprueba la matriz $A$ y el vector $b$ que se han construido para confirmar que coinciden con el sistema descrito anteriormente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "6f879978",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "array([[1.      , 1.      , 1.      ],\n",
              "       [1.      , 0.25    , 0.0625  ],\n",
              "       [1.      , 0.125   , 0.015625]])"
            ]
          },
          "execution_count": 11,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "lse.A"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "64fe7db9",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "array([1., 0., 0.])"
            ]
          },
          "execution_count": 12,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "lse.b"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16ab9f79",
      "metadata": {},
      "source": [
        "Una vez obtenida la ecuación de LSE, resolvemos los coeficientes estáticos $x_j$ mediante `lse.solve()` (esta es la solución directa $x = A^{-1}b$ ).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "7b69192a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The static coefficients associated with the ansatze are: [ 0.04761905 -0.57142857  1.52380952]\n"
          ]
        }
      ],
      "source": [
        "mpf_coeffs = lse.solve()\n",
        "print(\n",
        "    f\"The static coefficients associated with the ansatze are: {mpf_coeffs}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1c238a59",
      "metadata": {},
      "source": [
        "<span id=\"optimize-for-$x$-using-an-exact-model\" />\n",
        "\n",
        "##### Optimizar para $x$ utilizando un modelo exacto\n",
        "\n",
        "Como alternativa al cálculo de $x = A^{-1}b$, puedes utilizar [`setup\\_exact\\_model`](https://qiskit.github.io/qiskit-addon-mpf/stubs/qiskit_addon_mpf.static.setup_exact_model.html) para construir una instancia de [cvxpy.Problem](https://www.cvxpy.org/api_reference/cvxpy.problems.html#cvxpy.Problem) que utilice el LSE como restricciones y cuya solución óptima dé como resultado $x$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "993465e9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[ 0.04761905 -0.57142857  1.52380952]\n"
          ]
        }
      ],
      "source": [
        "model_exact, coeffs_exact = setup_exact_problem(lse)\n",
        "model_exact.solve()\n",
        "print(coeffs_exact.value)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "eb61ea70",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "L1 norm of the exact coefficients: 2.1428571428556378\n"
          ]
        }
      ],
      "source": [
        "print(\n",
        "    \"L1 norm of the exact coefficients:\",\n",
        "    np.linalg.norm(coeffs_exact.value, ord=1),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dac0472b",
      "metadata": {},
      "source": [
        "<span id=\"optimize-for-$x$-using-an-approximate-model\" />\n",
        "\n",
        "##### Optimizar para $x$ utilizando un modelo aproximado\n",
        "\n",
        "Podría darse el caso de que la norma « $L_1$ » para el conjunto elegido de valores de « $k_j$ » se considere demasiado elevada. Si ese es el caso y no puedes elegir otro conjunto de valores de $k_j$, puedes utilizar una solución aproximada que limite la norma $L_1$ a un umbral elegido, al tiempo que minimiza $\\|Ax - b\\|$. Consulta la guía sobre «[Cómo utilizar el modelo aproximado](https://qiskit.github.io/qiskit-addon-mpf/how_tos/using_approximate_model.html) ».\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "0cd7dea4",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[-1.10294118e-03 -2.48897059e-01  1.25000000e+00]\n",
            "L1 norm of the approximate coefficients: 1.5\n"
          ]
        }
      ],
      "source": [
        "model_approx, coeffs_approx = setup_sum_of_squares_problem(\n",
        "    lse, max_l1_norm=1.5\n",
        ")\n",
        "model_approx.solve()\n",
        "print(coeffs_approx.value)\n",
        "print(\n",
        "    \"L1 norm of the approximate coefficients:\",\n",
        "    np.linalg.norm(coeffs_approx.value, ord=1),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "10fd05eb",
      "metadata": {},
      "source": [
        "<span id=\"dynamic-mpf-coefficients\" />\n",
        "\n",
        "#### Coeficientes MPF dinámicos\n",
        "\n",
        "El MPF estático anula los términos de error de Trotter de una forma independiente del hamiltoniano y del estado, por lo que no produce necesariamente el menor error de aproximación posible para un hamiltoniano y un estado inicial dados. Por el contrario, el MPF dinámico (Ref. [\\[2\\]](#references), [\\[3\\]](#references) ) determina coeficientes dependientes del tiempo $x_i(t)$ que minimizan la distancia de la norma de Frobenius $\\|\\rho(t) - \\mu^D(t)\\|_F^2$ en cada instante $t$. Tal y como se muestra en la sección «Antecedentes», esto requiere la matriz de solapamiento $M_{ij}(t)$ entre los estados evolucionados según Trotter y el solapamiento $L_i(t)$ con el estado exacto; ambos se estiman utilizando backends de redes tensoriales ( TeNPy ) en `qiskit_addon_mpf`.\n",
        "\n",
        "Para configurar el LSE dinámico necesitamos tres elementos:\n",
        "\n",
        "1. Una **fábrica de evolutores aproximada** que el complemento ejecutará para cada $k_j$ con el fin de generar $\\rho_{k_j}(t)$ como MPS/MPO. Lo construimos a partir de la estructura por capas del circuito de Trotter de orden $2$ (una capa por `slice_by_depth`), envuelto como un `LayerwiseEvolver` con parámetros de truncamiento de tipo « TeNPy ».\n",
        "2. **Una fábrica de evolutores exactos** que produce un $\\rho(t)$ de referencia de alta precisión. Utilizamos un circuito de Suzuki-Trotter de cuarto orden con paso de tiempo pequeño (`dt=0.1`, `order=4`) como aproximación a la evolución exacta.\n",
        "3. Una **fábrica de identidades** y un **MPS de estado inicial** que sirven de base para la simulación « TeNPy ».\n",
        "\n",
        "La celda siguiente crea la fábrica de evolutores aproximados.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "52d78403",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Create approximate time-evolution circuits\n",
        "single_2nd_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)\n",
        ")\n",
        "single_2nd_order_circ = pm.run(single_2nd_order_circ)  # collect XX and YY\n",
        "\n",
        "# Find layers in the circuit\n",
        "layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)\n",
        "\n",
        "# Create tensor network models\n",
        "models = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\") for layer in layers\n",
        "]\n",
        "\n",
        "# Create the time-evolution object\n",
        "approx_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=models,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\n",
        "            \"chi_max\": 64,\n",
        "            \"svd_min\": 1e-8,\n",
        "            \"trunc_cut\": None,\n",
        "        },\n",
        "        \"max_delta_t\": 2,\n",
        "    },\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "91d2783e",
      "metadata": {},
      "source": [
        "<Admonition type=\"warning\">\n",
        "  Las opciones de `LayerwiseEvolver` que determinan los detalles de la simulación de la red tensorial deben elegirse con cuidado para evitar plantear un problema de optimización mal definido.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1c702f67",
      "metadata": {},
      "source": [
        "`dt=0.1`Aproximamos el estado exacto evolucionado en el tiempo mediante una fórmula de Suzuki-Trotter de cuarto orden, utilizando un paso de tiempo pequeño. Los parámetros de truncamiento de « TeNPy » pueden afectar a la precisión, por lo que es importante probar diferentes valores.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "abab8bfc",
      "metadata": {},
      "outputs": [],
      "source": [
        "single_4th_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)\n",
        ")\n",
        "single_4th_order_circ = pm.run(single_4th_order_circ)\n",
        "exact_model_layers = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\")\n",
        "    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)\n",
        "]\n",
        "\n",
        "exact_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=exact_model_layers,\n",
        "    dt=0.1,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\n",
        "            \"chi_max\": 64,\n",
        "            \"svd_min\": 1e-8,\n",
        "            \"trunc_cut\": None,\n",
        "        },\n",
        "        \"max_delta_t\": 2,\n",
        "    },\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2486594c",
      "metadata": {},
      "source": [
        "Por último, definimos un `identity_factory` que da lugar al estado MPO inicial y preparamos el estado inicial de Néel como un MPS que se ajusta a la red utilizada por el modelo de Trotter en capas.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "1e216575",
      "metadata": {},
      "outputs": [],
      "source": [
        "def identity_factory():\n",
        "    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)\n",
        "\n",
        "\n",
        "mps_initial_state = MPS_neel_state(models[0].lat)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5658315c",
      "metadata": {},
      "source": [
        "Una vez establecidas las fábricas, calculamos ahora los coeficientes dinámicos en cada instante de evolución. Para cada $t$, `setup_dynamic_lse` se calculan las matrices de solapamiento pertinentes mediante TeNPy, y `setup_frobenius_problem` se devuelve un valor `cvxpy.Problem` que minimiza el coste de la norma de Frobenius. El solucionador devuelve los coeficientes $x_j(t)$ adaptados a ese momento; los recopilamos en `mpf_dynamic_coeffs_list`. Si el solucionador falla para un « $t$ » determinado, recurrimos a coeficientes nulos para que el bucle continúe.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "b05dc012",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Computing dynamic coefficients for time=0.5\n",
            "\n",
            "Computing dynamic coefficients for time=0.6\n",
            "\n",
            "Computing dynamic coefficients for time=0.7\n",
            "\n",
            "Computing dynamic coefficients for time=0.7999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=0.8999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=0.9999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=1.0999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=1.1999999999999997\n",
            "\n",
            "Computing dynamic coefficients for time=1.2999999999999998\n",
            "\n",
            "Computing dynamic coefficients for time=1.4\n",
            "\n",
            "Computing dynamic coefficients for time=1.4999999999999998\n",
            "\n"
          ]
        }
      ],
      "source": [
        "mpf_dynamic_coeffs_list = []\n",
        "for t in trotter_times:\n",
        "    print(f\"Computing dynamic coefficients for time={t}\")\n",
        "    lse = setup_dynamic_lse(\n",
        "        mpf_trotter_steps,\n",
        "        t,\n",
        "        identity_factory,\n",
        "        exact_factory,\n",
        "        approx_factory,\n",
        "        mps_initial_state,\n",
        "    )\n",
        "    problem, coeffs = setup_frobenius_problem(lse)\n",
        "    try:\n",
        "        problem.solve()\n",
        "        mpf_dynamic_coeffs_list.append(coeffs.value)\n",
        "    except Exception as error:\n",
        "        mpf_dynamic_coeffs_list.append(np.zeros(len(mpf_trotter_steps)))\n",
        "        print(error, \"Calculation Failed for time\", t)\n",
        "    print(\"\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f8814e3",
      "metadata": {},
      "source": [
        "<span id=\"combine-trotter-expectation-values-with-the-mpf-coefficients\" />\n",
        "\n",
        "#### Combinar los valores esperados de Trotter con los coeficientes del MPF\n",
        "\n",
        "Ahora calculamos $\\langle A \\rangle_{\\text{MPF}}(t) = \\sum_j x_j \\, \\langle A \\rangle_{k_j}(t)$ para cada conjunto de coeficientes (estático-exacto, estático-aproximado y dinámico), propagamos los errores estándar por circuito y representamos gráficamente las series temporales resultantes en función de la curva de diagonalización exacta.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "35042576",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/35042576-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sym = {1: \"^\", 2: \"s\", 4: \"p\"}\n",
        "# Get expectation values at all times for each Trotter step\n",
        "for k, step in enumerate(mpf_trotter_steps):\n",
        "    trotter_curve, trotter_curve_error = [], []\n",
        "    for trotter_expvals, trotter_stds in zip(\n",
        "        mpf_expvals_all_times, mpf_stds_all_times\n",
        "    ):\n",
        "        trotter_curve.append(trotter_expvals[k])\n",
        "        trotter_curve_error.append(trotter_stds[k])\n",
        "\n",
        "    plt.errorbar(\n",
        "        trotter_times,\n",
        "        trotter_curve,\n",
        "        yerr=trotter_curve_error,\n",
        "        alpha=0.5,\n",
        "        markersize=4,\n",
        "        marker=sym[step],\n",
        "        color=\"grey\",\n",
        "        label=f\"{mpf_trotter_steps[k]} Trotter steps\",\n",
        "    )\n",
        "\n",
        "# Get expectation values at all times for the static MPF with exact coeffs\n",
        "exact_mpf_curve, exact_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(coeffs_exact.value, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    exact_mpf_curve_error.append(mpf_std)\n",
        "    exact_mpf_curve.append(trotter_expvals @ coeffs_exact.value)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    exact_mpf_curve,\n",
        "    yerr=exact_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Static MPF - Exact\",\n",
        "    color=\"purple\",\n",
        ")\n",
        "\n",
        "\n",
        "# Get expectation values at all times for the static MPF with approximate coeffs\n",
        "approx_mpf_curve, approx_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(coeffs_approx.value, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    approx_mpf_curve_error.append(mpf_std)\n",
        "    approx_mpf_curve.append(trotter_expvals @ coeffs_approx.value)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    approx_mpf_curve,\n",
        "    yerr=approx_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Static MPF - Approx\",\n",
        "    color=\"orange\",\n",
        ")\n",
        "\n",
        "\n",
        "# Get expectation values at all times for the dynamic MPF\n",
        "dynamic_mpf_curve, dynamic_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds, dynamic_coeffs in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times, mpf_dynamic_coeffs_list\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(dynamic_coeffs, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    dynamic_mpf_curve_error.append(mpf_std)\n",
        "    dynamic_mpf_curve.append(trotter_expvals @ dynamic_coeffs)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    dynamic_mpf_curve,\n",
        "    yerr=dynamic_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Dynamic MPF\",\n",
        "    color=\"pink\",\n",
        ")\n",
        "\n",
        "\n",
        "# Exact expectation values\n",
        "plt.plot(\n",
        "    exact_evolution_times,\n",
        "    exact_expvals,\n",
        "    color=\"red\",\n",
        "    linestyle=\"--\",\n",
        "    label=\"Exact time-evolution\",\n",
        ")\n",
        "\n",
        "plt.title(f\"$\\\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\\\rangle$ vs time\")\n",
        "plt.xlabel(\"Time\")\n",
        "plt.ylabel(\"Expectation Value\")\n",
        "plt.legend(loc=\"upper center\", bbox_to_anchor=(0.5, -0.2), ncol=2)\n",
        "plt.grid(alpha=0.1)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "34923748",
      "metadata": {},
      "source": [
        "El gráfico anterior ilustra la relación entre el error de Trotter y el error de muestreo.\n",
        "\n",
        "* **Error de Trotter.** Las fórmulas de cada producto (marcadores grises) se desvían cada vez más de la curva exacta a medida que pasa el tiempo. El circuito « $k=1$ » presenta la mayor desviación y es el menos profundo, pero también se encuentra ya en el régimen en el que « $t/k \\gtrsim 1$ », por lo que el término de error principal « $1/k^{2}$ » es grande. Las combinaciones MPF (marcadores de color) anulan varios de estos términos de error de Trotter principales, por lo que siguen la curva exacta mucho más fielmente que cualquier circuito « $k_j$ » por sí solo. La desviación restante refleja los términos de Trotter de orden superior que el MPF *no* cancela: un MPF estático de orden $2$, $r=3$ solo elimina los dos primeros órdenes de error, y a gran $t/k_{\\min}$ la cola no cancelada acaba dominando; por lo tanto, el MPF no garantiza que los circuitos muy poco profundos mantengan su precisión en momentos arbitrarios.\n",
        "\n",
        "* **Error de muestreo.** Las barras de error más anchas en las curvas del MPF son una consecuencia directa de la combinación lineal: al propagar los errores estándar independientes por circuito $\\sigma_{k_j}$ se obtiene una varianza total $\\sigma_{\\text{MPF}}^2 = \\sum_j x_j^2 \\, \\sigma_{k_j}^2$. Por lo tanto, cuanto mayor sea el $\\|x\\|_2$ (y, en la práctica, el $\\|x\\|_1$, que es lo que controlamos), más mediciones se necesitarán para alcanzar una incertidumbre objetivo determinada. Esta es la compensación que subyace a la opción «solucionador aproximado» en «Antecedentes»: limitamos $\\|x\\|_1$ para que esta sobrecarga sea manejable. Es fundamental señalar que, a diferencia del error de Trotter, el error de muestreo se reduce con el « $1/\\sqrt{N_{\\text{shots}}}$ », por lo que siempre puede minimizarse realizando más mediciones.\n",
        "\n",
        "En el ejemplo de hardware a gran escala que se muestra a continuación, el ruido del hardware se introduce como una fuente de error adicional en cada « $\\langle A \\rangle_{k_j}$ », que, de forma similar, se amplifica mediante los coeficientes del MPF. En esa sección veremos cómo interactúa la mitigación de errores con los MPF.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6fa763ff",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Ejemplo de hardware a gran escala\n",
        "\n",
        "En esta sección ampliamos la escala del problema más allá de lo que es posible simular con exactitud. Reproducimos algunos de los resultados presentados en la ref. [\\[3\\]](#references), utilizando una cadena XXZ de 50 qubits en el intervalo de tiempo $t = 3$. Seguimos el mismo procedimiento de cuatro pasos que en el ejemplo a pequeña escala, pero en esta ocasión nos centramos en hardware cuántico real con mitigación de errores. Al igual que en la plantilla, cada paso está marcado directamente en el código, y un mismo paso puede abarcar varias celdas cuando conviene examinar sus resultados intermedios.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "481fea70",
      "metadata": {},
      "source": [
        "La representación sigue el ejemplo a pequeña escala: definir un hamiltoniano, elegir los parámetros de Trotter, calcular los coeficientes MPF (estáticos y dinámicos) y construir circuitos. Las diferencias principales son:\n",
        "\n",
        "* Un **hamiltoniano XXZ** en 50 sitios con acoplamientos aleatorios extraídos de $\\mathcal{U}(0.5, 1.5)$ (Ref. [\\[3\\]](#references) ).\n",
        "* Una fórmula de Trotter **simétrica** de segundo orden con « $k_j = [3, 4, 6]$ » (por lo que $\\chi=1$, `symmetric=True`).\n",
        "* Un único tiempo de evolución fijo $t = 3$. Con $k_{\\min}=3$, esto da como resultado $t/k_{\\min}=1$, lo que mantiene los componentes superficiales dentro del régimen de convergencia de Trotter, donde es válido el modelo de error dominante en el que se basa el MPF.\n",
        "* Una **comparación adicional de un solo circuito ejecutada con pasos de Trotter $k = 10$**, utilizada como línea base. Elegimos « $k = 10$ » porque su profundidad de dos qubits en el hardware es mayor que la del componente MPF más profundo ( $k_{\\max}=6$ ) más la sobrecarga que supone ejecutar múltiples circuitos MPF —lo suficientemente profunda como para estar limitada por el ruido, que es el régimen en el que se espera que la combinación de MPF supere al circuito único de referencia—. Se trata de una comparación de «un solo circuito profundo» frente a la combinación MPF, no de un circuito que tenga como objetivo el error de Trotter efectivo del MPF (lo cual requeriría muchos más pasos).\n",
        "\n",
        "Ten en cuenta que, aunque aquí todavía nos encontramos en el paso 1 (mapeo y construcción del circuito), en esta celda también calculamos previamente los coeficientes dinámicos junto con los estáticos. Los coeficientes dinámicos dependen de $H$ y $t$, pero no de las mediciones cuánticas, por lo que pueden calcularse en cualquier momento antes del paso 4. Lo hacemos ahora para tener toda la configuración específica del MPF en un solo lugar.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "a019ac32",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Static coefficients: [ 0.42857143 -1.82857143  2.4       ]\n",
            "L1 norm: 4.65714285714286\n",
            "Approximate coefficients: [-0.4942491   0.40206845  1.09218065]\n",
            "L1 norm (approx): 1.9884981979026675\n",
            "Computing dynamic coefficients for time=3\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 1-------------------------\n",
        "L = 50\n",
        "coupling_map = CouplingMap.from_line(L, bidirectional=False)\n",
        "\n",
        "# XXZ Hamiltonian with random couplings (Ref. [3])\n",
        "np.random.seed(0)\n",
        "even_edges = list(coupling_map.get_edges())[::2]\n",
        "odd_edges = list(coupling_map.get_edges())[1::2]\n",
        "\n",
        "Js = np.random.uniform(0.5, 1.5, size=L)\n",
        "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n",
        "for i, edge in enumerate(even_edges + odd_edges):\n",
        "    hamiltonian += SparsePauliOp.from_sparse_list(\n",
        "        [\n",
        "            (\"XX\", (edge), 2 * Js[i]),\n",
        "            (\"YY\", (edge), 2 * Js[i]),\n",
        "            (\"ZZ\", (edge), 4 * Js[i]),\n",
        "        ],\n",
        "        num_qubits=L,\n",
        "    )\n",
        "\n",
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n",
        ")\n",
        "\n",
        "total_time = 3\n",
        "mpf_trotter_steps = [3, 4, 6]\n",
        "order = 2\n",
        "symmetric = True\n",
        "\n",
        "# Static coefficients\n",
        "lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)\n",
        "mpf_coeffs = lse.solve()\n",
        "print(f\"Static coefficients: {mpf_coeffs}\")\n",
        "print(f\"L1 norm: {np.linalg.norm(mpf_coeffs, ord=1)}\")\n",
        "\n",
        "model_approx, coeffs_approx = setup_sum_of_squares_problem(\n",
        "    lse, max_l1_norm=2.0\n",
        ")\n",
        "model_approx.solve()\n",
        "print(f\"Approximate coefficients: {coeffs_approx.value}\")\n",
        "print(f\"L1 norm (approx): {np.linalg.norm(coeffs_approx.value, ord=1)}\")\n",
        "\n",
        "# -------------------------Dynamic coefficients-------------------------\n",
        "single_2nd_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)\n",
        ")\n",
        "single_2nd_order_circ = pm.run(single_2nd_order_circ)\n",
        "\n",
        "layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)\n",
        "models = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\") for layer in layers\n",
        "]\n",
        "\n",
        "approx_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=models,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\"chi_max\": 64, \"svd_min\": 1e-8, \"trunc_cut\": None},\n",
        "        \"max_delta_t\": 4,\n",
        "    },\n",
        ")\n",
        "\n",
        "single_4th_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)\n",
        ")\n",
        "single_4th_order_circ = pm.run(single_4th_order_circ)\n",
        "exact_model_layers = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\")\n",
        "    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)\n",
        "]\n",
        "\n",
        "exact_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=exact_model_layers,\n",
        "    dt=0.1,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\"chi_max\": 64, \"svd_min\": 1e-8, \"trunc_cut\": None},\n",
        "        \"max_delta_t\": 3,\n",
        "    },\n",
        ")\n",
        "\n",
        "\n",
        "def identity_factory():\n",
        "    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)\n",
        "\n",
        "\n",
        "mps_initial_state = MPS_neel_state(models[0].lat)\n",
        "\n",
        "print(f\"Computing dynamic coefficients for time={total_time}\")\n",
        "lse_dyn = setup_dynamic_lse(\n",
        "    mpf_trotter_steps,\n",
        "    total_time,\n",
        "    identity_factory,\n",
        "    exact_factory,\n",
        "    approx_factory,\n",
        "    mps_initial_state,\n",
        ")\n",
        "problem, coeffs_dyn = setup_frobenius_problem(lse_dyn)\n",
        "try:\n",
        "    problem.solve()\n",
        "    mpf_dynamic_coeffs = coeffs_dyn.value\n",
        "except Exception as error:\n",
        "    mpf_dynamic_coeffs = np.zeros(len(mpf_trotter_steps))\n",
        "    print(error, \"Calculation Failed\")\n",
        "\n",
        "# -------------------------Step 1 (cont): Build circuits-------------------------\n",
        "mpf_circuits = []\n",
        "for k in mpf_trotter_steps:\n",
        "    circuit = QuantumCircuit(L)\n",
        "    circuit.x([i for i in range(L) if i % 2])\n",
        "    trotter_circ = generate_time_evolution_circuit(\n",
        "        hamiltonian,\n",
        "        synthesis=SuzukiTrotter(reps=k, order=order),\n",
        "        time=total_time,\n",
        "    )\n",
        "    circuit.compose(trotter_circ, qubits=range(L), inplace=True)\n",
        "    mpf_circuits.append(circuit)\n",
        "\n",
        "# Baseline \"single deep circuit\" comparison run with k=10 Trotter steps.\n",
        "# Its two-qubit depth is deeper than the deepest MPF constituent (k_max=6) plus\n",
        "# the overhead of running multiple circuits, pushing it into the noise-limited\n",
        "# regime where MPF is expected to outperform. It does NOT target the MPF's effective\n",
        "# Trotter error (which would require many more steps).\n",
        "comp_circuit = QuantumCircuit(L)\n",
        "comp_circuit.x([i for i in range(L) if i % 2])\n",
        "trotter_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian,\n",
        "    synthesis=SuzukiTrotter(reps=10, order=order),\n",
        "    time=total_time,\n",
        ")\n",
        "comp_circuit.compose(trotter_circ, qubits=range(L), inplace=True)\n",
        "mpf_circuits.append(comp_circuit)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b5d388b1",
      "metadata": {},
      "source": [
        "Ahora optimizamos los circuitos para el backend elegido. `optimization_level=3`Utilizamos el gestor de pasadas preconfigurado de Qiskit, que selecciona automáticamente un buen conjunto de qubits físicos y asigna cada circuito a la topología del dispositivo.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "id": "05bad997",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_fez')>\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 2-------------------------\n",
        "service = QiskitRuntimeService()\n",
        "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits=L)\n",
        "backend = service.backend(\"ibm_fez\")\n",
        "print(backend)\n",
        "\n",
        "transpiler = generate_preset_pass_manager(\n",
        "    optimization_level=3, backend=backend\n",
        ")\n",
        "transpiled_circuits = [transpiler.run(circ) for circ in mpf_circuits]\n",
        "\n",
        "isa_observables = [\n",
        "    observable.apply_layout(circ.layout) for circ in transpiled_circuits\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b578c285",
      "metadata": {},
      "source": [
        "La ejecución de circuitos más complejos en hardware real requiere medidas enérgicas de mitigación de errores. Permitimos el desacoplamiento dinámico, la rotación de puertas y mediciones, la mitigación de errores de medición y la extrapolación sin ruido (ZNE). Cabe señalar que los factores de ruido ZNE que utilizamos aquí (`1, 1.2, 1.4`) son menores que en un escenario de circuito superficial, ya que los componentes MPF más profundos ya se encuentran cerca del umbral de ruido y unas amplificaciones de ruido elevadas los llevarían más allá del punto en el que la extrapolación ZNE resulta fiable.\n",
        "\n",
        "Enviamos los cuatro circuitos (los tres componentes del MPF en $k_j = [3, 4, 6]$ más la línea de referencia de $k = 10$ ) en un único trabajo de Estimator.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "id": "e2722b61",
      "metadata": {},
      "outputs": [],
      "source": [
        "# -------------------------Step 3-------------------------\n",
        "estimator = Estimator(mode=backend)\n",
        "estimator.options.default_shots = 30000\n",
        "\n",
        "# Error suppression/mitigation\n",
        "estimator.options.dynamical_decoupling.enable = True\n",
        "estimator.options.twirling.enable_gates = True\n",
        "estimator.options.twirling.enable_measure = True\n",
        "estimator.options.twirling.num_randomizations = \"auto\"\n",
        "estimator.options.twirling.strategy = \"active-accum\"\n",
        "estimator.options.resilience.measure_mitigation = True\n",
        "estimator.options.experimental.execution_path = \"gen3-turbo\"\n",
        "\n",
        "estimator.options.resilience.zne_mitigation = True\n",
        "estimator.options.resilience.zne.noise_factors = (1, 1.2, 1.4)\n",
        "estimator.options.resilience.zne.extrapolator = \"linear\"\n",
        "\n",
        "estimator.options.environment.job_tags = [\"TUT_MPF\"]\n",
        "\n",
        "job_50 = estimator.run(\n",
        "    [\n",
        "        (circ, observable)\n",
        "        for circ, observable in zip(transpiled_circuits, isa_observables)\n",
        "    ]\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "afc0c029",
      "metadata": {},
      "source": [
        "Extraemos los valores esperados y las desviaciones estándar por circuito de los resultados del trabajo y, a continuación, los combinamos con cada conjunto de coeficientes MPF exactamente igual que en el ejemplo a pequeña escala: $\\langle A \\rangle_{\\text{MPF}} = \\sum_j x_j \\, \\langle A \\rangle_{k_j}$, con la varianza propagada $\\sigma^2 = \\sum_j x_j^2 \\sigma_{k_j}^2$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "id": "a924d79c",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[array(-0.07916195), array(-0.04479681), array(-0.2560756), array(-0.06045848)]\n",
            "[array(0.04605538), array(0.10056336), array(0.14426151), array(0.04059092)]\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 4-------------------------\n",
        "result = job_50.result()\n",
        "evs = [res.data.evs for res in result]\n",
        "std = [res.data.stds for res in result]\n",
        "\n",
        "print(evs)\n",
        "print(std)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "id": "1071de0d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Exact static MPF expectation value:  -0.5665938395816946 +- 0.3925273058119915\n",
            "Approximate static MPF expectation value:  -0.25856647611537903 +- 0.164249927266166\n",
            "Dynamic MPF expectation value:  -0.12667812062949296 +- 0.06059471006973169\n"
          ]
        }
      ],
      "source": [
        "exact_mpf_std = np.sqrt(\n",
        "    sum([(coeff**2) * (std**2) for coeff, std in zip(mpf_coeffs, std[:3])])\n",
        ")\n",
        "print(\n",
        "    \"Exact static MPF expectation value: \",\n",
        "    evs[:3] @ mpf_coeffs,\n",
        "    \"+-\",\n",
        "    exact_mpf_std,\n",
        ")\n",
        "approx_mpf_std = np.sqrt(\n",
        "    sum(\n",
        "        [\n",
        "            (coeff**2) * (std**2)\n",
        "            for coeff, std in zip(coeffs_approx.value, std[:3])\n",
        "        ]\n",
        "    )\n",
        ")\n",
        "print(\n",
        "    \"Approximate static MPF expectation value: \",\n",
        "    evs[:3] @ coeffs_approx.value,\n",
        "    \"+-\",\n",
        "    approx_mpf_std,\n",
        ")\n",
        "dynamic_mpf_std = np.sqrt(\n",
        "    sum(\n",
        "        [\n",
        "            (coeff**2) * (std**2)\n",
        "            for coeff, std in zip(mpf_dynamic_coeffs, std[:3])\n",
        "        ]\n",
        "    )\n",
        ")\n",
        "print(\n",
        "    \"Dynamic MPF expectation value: \",\n",
        "    evs[:3] @ mpf_dynamic_coeffs,\n",
        "    \"+-\",\n",
        "    dynamic_mpf_std,\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "64360d85",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/64360d85-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sym = {3: \"^\", 4: \"s\", 6: \"p\"}\n",
        "for k, step in enumerate(mpf_trotter_steps):\n",
        "    plt.errorbar(\n",
        "        k,\n",
        "        evs[k],\n",
        "        yerr=std[k],\n",
        "        alpha=0.5,\n",
        "        markersize=4,\n",
        "        marker=sym[step],\n",
        "        color=\"grey\",\n",
        "        label=f\"{mpf_trotter_steps[k]} Trotter steps\",\n",
        "    )\n",
        "\n",
        "plt.errorbar(\n",
        "    3,\n",
        "    evs[-1],\n",
        "    yerr=std[-1],\n",
        "    alpha=0.5,\n",
        "    markersize=8,\n",
        "    marker=\"x\",\n",
        "    color=\"blue\",\n",
        "    label=\"10 Trotter steps\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    4,\n",
        "    evs[:3] @ mpf_coeffs,\n",
        "    yerr=exact_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"purple\",\n",
        "    label=\"Static MPF\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    5,\n",
        "    evs[:3] @ coeffs_approx.value,\n",
        "    yerr=approx_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"orange\",\n",
        "    label=\"Approximate static MPF\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    6,\n",
        "    evs[:3] @ mpf_dynamic_coeffs,\n",
        "    yerr=dynamic_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"pink\",\n",
        "    label=\"Dynamic MPF\",\n",
        ")\n",
        "\n",
        "exact_obs = -0.24384471447172074  # Calculated via Tensor Network calculation\n",
        "plt.axhline(\n",
        "    y=exact_obs, linestyle=\"--\", color=\"red\", label=\"Exact time-evolution\"\n",
        ")\n",
        "\n",
        "plt.title(\n",
        "    f\"$\\\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\\\rangle$ at time {total_time} for the different methods\"\n",
        ")\n",
        "plt.xlabel(\"Method\")\n",
        "plt.ylabel(\"Expectation Value\")\n",
        "plt.legend(loc=\"upper center\", bbox_to_anchor=(0.5, -0.2), ncol=2)\n",
        "plt.grid(alpha=0.1)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "31effe5f",
      "metadata": {},
      "source": [
        "Algunas observaciones sobre los resultados de hardware anteriores:\n",
        "\n",
        "* **Profundizar más no es gratuito en lo que respecta al hardware.** Las curvas de referencia de un solo circuito lo dejan claro: el circuito $k = 6$ es prácticamente exacto ( $-0.256$ frente al de referencia $-0.244$ ), mientras que la curva de referencia más profunda $k = 10$ es *peor* ( $-0.061$, con un error de $\\sim 0.18$ ), y no mejor. Una vez que el error de Trotter ya es pequeño, añadir pasos solo sirve, en su mayor parte, para hacer más profundo el circuito y acumular más ruido de puerta y decoherencia. Este es precisamente el régimen para el que se han diseñado los MPF: alcanzar la precisión de un circuito profundo utilizando únicamente componentes superficiales.\n",
        "\n",
        "* **Un MPF de norma pequeña supera al circuito único profundo.** El MPF «aproximadamente estático» (con un límite máximo de $\\|x\\|_1 \\approx 2$ ) se sitúa en $-0.259$, a $\\sim 0.015$ del valor de referencia y mucho más cerca que la referencia de $k = 10$. El MPF dinámico ( $-0.127$ ) también supera con holgura ese nivel de referencia. Ambos combinan únicamente los circuitos superficiales $k_j = [3, 4, 6]$, pero obtienen una respuesta que el circuito único profundo no podía proporcionar.\n",
        "\n",
        "* **La norma del coeficiente es más importante que la optimalidad matemática.** El MPF estático exacto tiene una norma de coeficientes de $\\|x\\|_1 = 4.66$ y es el *peor* estimador de todos ( $-0.567$, con un error de más de $0.3$ ): la elevada norma de coeficientes amplifica el ruido residual de la puerta, la decoherencia y el error ZNE en cada $\\langle A \\rangle_{k_j}$ aproximadamente en el mismo factor, lo que anula la cancelación del error de Trotter que proporciona. La limitación de la norma (el solucionador estático aproximado, $\\|x\\|_1 \\approx 2$ ) elimina esta sobrecarga y ofrece la mejor estimación, aunque sus coeficientes ya no anulen exactamente el error principal de Trotter.\n",
        "\n",
        "* **Los circuitos individuales de poca profundidad pueden seguir siendo competitivos.** El único componente « $k = 6$ » ( $-0.256$ ) es, en sí mismo, prácticamente exacto en este caso; en esta ejecución, incluso se acerca ligeramente más que el MPF «aproximate-static». El problema es que no se sabe de antemano *qué* $k$ concreto se encuentra en el punto óptimo de «convergente pero aún no limitado por el ruido», y la opción que parece más segura —simplemente profundizar más ( $k = 10$ ) para garantizar la convergencia de Trotter— es precisamente la que falla. El MPF ofrece una combinación basada en principios de circuitos poco profundos que no requiere adivinar la profundidad adecuada.\n",
        "\n",
        "En la práctica, esto significa que, en el ámbito del hardware, los MPF deben combinarse con una sólida mitigación de errores en cada « $\\langle A \\rangle_{k_j}$ » individual; la norma del coeficiente $L_1$ debe mantenerse en un nivel moderado (utilizando el solucionador aproximado o el MPF dinámico); y los pasos de Trotter $k_j$ deben elegirse de tal forma que $t/k_{\\min} \\lesssim 1$ —aquí $k_{\\min} = 3$ en $t = 3$ da como resultado $t/k_{\\min} = 1$ —, manteniendo los componentes dentro del régimen de convergencia en el que es válido el modelo de error principal en el que se basa el MPF estático. Con esas opciones, los MPF de norma pequeña aquí se equiparan a un circuito único convergente, mientras que la línea de base «simplemente ir más profundo» no lo hace, recuperando así la ventaja de profundidad frente a precisión mostrada en la ref. [\\[3\\]](#references). Cabe señalar también que las ejecuciones individuales presentan ruido: en un envío diferente del mismo trabajo (o en un backend diferente), el orden exacto puede variar; las tendencias generales indican que los MPF de pequeño $\\|x\\|_1$ dan buenos resultados, que el MPF de gran $\\|x\\|_1$ estático exacto se ve amplificado por el ruido del hardware y que el circuito único excesivamente profundo está limitado por el ruido.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5ac2f8a8",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximos pasos\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendaciones\">\n",
        "  Si este trabajo te ha parecido interesante, quizá te interese el siguiente material:\n",
        "\n",
        "  * [Cómo elegir los pasos de Trotter para un MPF](https://qiskit.github.io/qiskit-addon-mpf/how_tos/choose_trotter_steps.html) : orientación práctica sobre la selección de los valores de l $k_j$ a para evitar inestabilidades\n",
        "  * [Cómo utilizar el modelo aproximado](https://qiskit.github.io/qiskit-addon-mpf/how_tos/using_approximate_model.html) : ajuste de la restricción de la norma « $L_1$ » y de las opciones del solucionador para el MPF estático aproximado\n",
        "  * [`qiskit-addon-mpf` Referencia de la API](https://qiskit.github.io/qiskit-addon-mpf/) : documentación completa sobre los módulos estáticos, dinámicos y de backend\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "70be41e1",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referencias\n",
        "\n",
        "\\[1] Vázquez, A. C., Egger, D. J., Ochsner, D., y Woerner, S. Fórmulas multiproducto bien condicionadas para la simulación hamiltoniana adaptada al hardware. [Quantum, 7, 1067 (2023)](https://quantum-journal.org/papers/q-2023-07-25-1067/)\n",
        "\n",
        "\\[2] Zhuk, S., Robertson, N. F., y Bravyi, S. Límites de error de Trotter y fórmulas dinámicas multiproducto para la simulación hamiltoniana. [Physical Review Research, 6(3), 033309 (2024)](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.033309)\n",
        "\n",
        "\\[3] Robertson, N. F., et al. Fórmulas dinámicas multiproducto mejoradas mediante redes tensoriales. [arXiv:2407.17405 (2024)](https://arxiv.org/abs/2407.17405)\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"
    },
    "hours": 1.5,
    "qpuSeconds": 240
  },
  "nbformat": 4,
  "nbformat_minor": 5
}