{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Simulation de la diffusion neutronique à l'aide d'un workflow sans serveur combinant AQC et la dynamique de Trotter\"\n",
        "description: \"Calculez le facteur de structure dynamique S(q, w) de l’aimant quantique KCuF3 en exécutant un workflow Trotter avec la compression AQC sous forme de modèle de fonction déployé.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore Trotter Trotterization spinon spinons KCuF quimb DMRG magnon antiferromagnetic Suzuki fidelities isa COBYQA Gjjc qpoints viridis fontsize vmax vmin */}\n",
        "\n",
        "<span id=\"simulate-neutron-scattering-with-an-aqc-+-trotter-dynamics-serverless-workflow\" />\n",
        "\n",
        "# Simulation de la diffusion neutronique à l'aide d'un workflow sans serveur combinant AQC et la dynamique de Trotter\n",
        "\n",
        "Estimation de la durée *d'exécution : 18 minutes sur un processeur Heron r3 (REMARQUE : il ne s'agit que d'une estimation. (Votre temps d'exécution peut varier.)*\n",
        "\n",
        "<Admonition type=\"note\" title=\"Quel tutoriel devrais-je suivre?\">\n",
        "  Suivez ce tutoriel pour réaliser l'expérience de diffusion neutronique à l'aide d'une fonction « Qiskit Serverless » déployée, qui regroupe la construction, la compression et l'exécution du circuit en un seul appel. La compression utilise les ressources de calcul et de mémoire du worker « serverless » et peut se poursuivre après la fermeture de votre notebook; la préparation de l'état fondamental et le post-traitement s'exécutent toujours localement. Vous devez d'abord [déployer le modèle de fonction](/docs/guides/function-template-aqc-trotter). Pour découvrir la mise en œuvre étape par étape, consultez le [tutoriel d'origine](/docs/tutorials/simulate-neutron-scattering).\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "learning-outcomes",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Acquis d'apprentissage\n",
        "\n",
        "* Comment un spectre de diffusion inélastique des neutrons permet de déterminer le facteur de structure dynamique $S(q, \\omega)$ d'un aimant quantique de type « 1D ».\n",
        "* Comment préparer l' KCuF$_3$ (état de Heisenberg isotrope) à l'aide du groupe de renormalisation de la matrice de densité (DMRG) et de la maximisation de la fidélité des états de produit matriciel (MPS).\n",
        "* Comment exécuter l'évolution temporelle de Trotter, la compression de circuits par compilation quantique approximative (AQC) et l'exécution atténuée en un seul appel de fonction.\n",
        "* Comment traiter les séries chronologiques d' $\\langle \\sigma_z \\rangle(t)$ s par site pour obtenir des données de type « $S(q, \\omega)$ » et identifier le continuum à deux spinons.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "prerequisites",
      "metadata": {},
      "source": [
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prérequis\n",
        "\n",
        "* Connaissance des [modèles Qiskit](/docs/guides/intro-to-patterns) et [`SparsePauliOp`](/docs/api/qiskit/qiskit.quantum_info.SparsePauliOp)de [l'évolution temporelle de Trotter](/learning/courses/utility-scale-quantum-computing/quantum-simulation).\n",
        "* Une connaissance de base des méthodes des réseaux de tenseurs (DMRG et MPS) est utile mais pas indispensable, tout comme la maîtrise de la bibliothèque [`qiskit-addon-aqc-tensor`](https://github.com/Qiskit/qiskit-addon-aqc-tensor) utilisée par la fonction pour compresser les circuits de Trotter.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "background",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Arrière-plan\n",
        "\n",
        "La diffusion inélastique des neutrons permet de mesurer le facteur de structure dynamique $S(q, \\omega)$, qui correspond à la transformée de Fourier spatio-temporelle de la fonction de corrélation spin-spin; par conséquent, la reproduction de l’ $S(q, \\omega)$ issue d’un modèle de spin microscopique constitue un test direct et falsifiable d’une simulation quantique. Ce tutoriel porte sur KCuF$_3$, une chaîne de Heisenberg antiferromagnétique à spin $\\frac{1}{2}$, dont les excitations ne sont pas des inversions de spin isolées, mais des paires de spinons fractionnés : au lieu d’une dispersion de magnons bien définie, $S(q, \\omega)$ présente un large *continuum à deux spinons*, limité en bas par $\\tfrac{\\pi}{2}|\\sin q|$ et en haut par $\\pi|\\sin(q/2)|$. Il s’agit des courbes en pointillés sur les graphiques ci-dessous. Les aspects physiques dans leur intégralité, ainsi que la comparaison avec les données mesurées sur les neutrons, sont abordés dans le [tutoriel original](/docs/tutorials/simulate-neutron-scattering) et dans l'article de Lee et al., [arXiv:2603.15608](https://arxiv.org/abs/2603.15608).\n",
        "\n",
        "Le déroulement des opérations quantiques reflète l'expérience de diffusion :\n",
        "\n",
        "1. Préparez l' $|\\psi_0\\rangle$ de l'état fondamental de la chaîne.\n",
        "2. Commencez par une perturbation locale au niveau du site central, une rotation de type « $\\pi/2$ » $Z$, imitant le transfert de quantité de mouvement et d’énergie provenant du neutron.\n",
        "3. Évolution temporelle sous l'hamiltonien de Heisenberg, $e^{-iHt}$, à l'aide d'une formule de produit de Trotter.\n",
        "4. Mesurer l’ $\\langle \\sigma_z^j \\rangle(t)$ de magnétisation par site. En fonction de l’ $j$ du site et du temps $t$, il s’agit exactement de la fonction de Green retardée $G^R(j, j_c, t)$; aucune conversion n’est donc nécessaire avant la transformée de Fourier à l’étape 5.\n",
        "5. La transformée de Fourier de $G^R$ donne $S(q, \\omega)$.\n",
        "\n",
        "Des problèmes peuvent survenir à l'étape 3, lorsque les circuits de Trotter exacts pour les longues évolutions deviennent trop profonds pour le matériel. L'AQC avec réseaux de tenseurs résout ce problème en compressant un bloc d'étapes de Trotter en un ansatz paramétré fixe et peu profond, dont la fidélité de l'état par rapport à l'évolution exacte est maximisée de manière classique à l'aide d'un simulateur MPS ( [arXiv:2301.08609](https://arxiv.org/abs/2301.08609) ). Le [modèle AQC Dynamics](/docs/guides/function-template-aqc-trotter) regroupe l'ensemble de ce noyau quantique (synthèse de Trotter, compression AQC et exécution atténuée) en un seul appel :\n",
        "\n",
        "| PRE (ce cahier)                                                                                                                                               | FONCTION (`aqc-dynamics-function`)                                                                                                                 | PUBLIER (ce cahier)                                |\n",
        "| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n",
        "| État fondamental obtenu par la méthode DMRG combinée à la maximisation de la fidélité MPS, l'effet de « kick » neutronique étant intégré dans le même circuit | Synthèse de Trotter → compression AQC → exécution sur `statevector`, `fake`, ou `runtime`, renvoyant l' $\\langle \\sigma_z^j \\rangle(t)$ s par site | $S(q, \\omega)$ : le facteur de structure dynamique |\n",
        "\n",
        "Les travaux spécifiques à l'expérience sont consignés ici, dans le cahier de laboratoire : la préparation à l'état fondamental (PRE) et le post-traitement de l' $S(q, \\omega)$ e (POST). Les deux étapes les plus gourmandes en ressources quantiques, la compression et l'exécution, s'exécutent à l'intérieur de la fonction.\n",
        "\n",
        "Ce tutoriel accompagne l'article « [Simuler la diffusion neutronique dans les matériaux quantiques à l'aide de circuits quantiques](/docs/tutorials/simulate-neutron-scattering) », qui présente en détail la réalisation de cette même expérience : le même modèle KCuF$_3$, la préparation de l'état fondamental, la projection neutronique et le post-traitement, avec la synthèse de Trotter, la compression AQC et l'exécution atténuée décrites étape par étape. Lisez ce tutoriel pour découvrir le fonctionnement de la compression AQC. Lisez cet article pour réaliser la même expérience à l'aide d'un modèle de fonction déployé : le cœur quantique se résume alors à un simple appel de fonction, et la compression AQC, qui dure plusieurs heures, s'exécute au sein du travailleur « serverless » plutôt que sur votre machine; vous n'avez donc pas besoin d'un système HPC ni d'un noyau ouvert pendant son exécution. Cet appel de fonds finance également d'autres expériences sur la dynamique des « 1D s ».\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "requirements",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Exigences\n",
        "\n",
        "Avant de commencer ce tutoriel, assurez-vous de disposer des éléments suivants :\n",
        "\n",
        "* La fonction déployée sur votre compte Qiskit Serverless. Exécutez d'abord le modèle de fonction associé : [déployez et exécutez le modèle de fonction « AQC + dynamique de Trotter](/docs/guides/function-template-aqc-trotter) ». Ce guide vous explique comment récupérer les fichiers sources et mettre en ligne la fonction sur votre compte. Ce tutoriel se contente d'appeler la fonction déployée.\n",
        "\n",
        "* IBM Quantum® identifiants enregistrés pour `QiskitServerless` (voir le modèle de fonction). Les deux exemples de ce tutoriel appellent la fonction déployée; ils en ont donc tous les deux besoin.\n",
        "\n",
        "* Qiskit SDK v2.0 ou version ultérieure (`pip install qiskit`).\n",
        "\n",
        "* Le client « Catalog » de Qiskit IBM (`pip install qiskit-ibm-catalog`).\n",
        "\n",
        "* NumPy, SciPy, et Matplotlib (`pip install numpy scipy matplotlib`). SciPy 1.14 ou une version ultérieure est nécessaire pour l'optimiseur COBYQA utilisé dans la préparation de l'état fondamental.\n",
        "\n",
        "* La pile de réseaux de tenseurs AQC, car la préparation de l'état fondamental à l'étape 1 s'exécute localement dans ce cahier : `pip install 'qiskit-addon-aqc-tensor[quimb-jax]==0.3.1'`.\n",
        "\n",
        "Le premier appel à une fonction nouvellement déployée est mis en attente le temps que le worker Serverless installe ses dépendances; il faut donc s'attendre à une latence supplémentaire lors de cette exécution.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "setup-md",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuration\n",
        "\n",
        "Importez les bibliothèques et définissez les fonctions auxiliaires spécifiques à l'expérience qui seront utilisées par la suite : `build_gs_ansatz` (l'approche variationnelle hamiltonienne, ou HVA, pour la préparation de l'état fondamental), `prepare_ground_state` (DMRG associé à la maximisation de la fidélité MPS), ainsi que `get_spectrum`, `plot_green`, et `plot_spectrum` (le post-traitement « $S(q, \\omega)$ »). Ces éléments sont tirés du [tutoriel](/docs/tutorials/simulate-neutron-scattering) original sur la diffusion des neutrons.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "setup-imports",
      "metadata": {},
      "outputs": [],
      "source": [
        "from functools import partial\n",
        "\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "import scipy.optimize\n",
        "\n",
        "import quimb.tensor as qtn\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import SparsePauliOp\n",
        "from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit\n",
        "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n",
        "from qiskit_ibm_catalog import QiskitServerless"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "setup-helpers",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Setup complete - helpers defined.\n"
          ]
        }
      ],
      "source": [
        "#  Dynamical structure factor via discrete Fourier transform\n",
        "\n",
        "\n",
        "def get_spectrum(n, Gjjc, dt, time_steps, q_steps, w_steps):\n",
        "    \"\"\"Compute the dynamical structure factor from the retarded Green's function.\n",
        "\n",
        "    Uses the center-site approximation and a discrete Fourier transform.\n",
        "    \"\"\"\n",
        "    green = Gjjc / 4  # sigma -> S=1/2\n",
        "    omega_max = np.pi / dt\n",
        "    qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)\n",
        "    omegas = np.arange(0, omega_max, omega_max / w_steps)\n",
        "    green_map = np.zeros((omegas.shape[0], qpoints.shape[0]))\n",
        "    center = n // 2 - 1\n",
        "    for iw, w in enumerate(omegas):\n",
        "        exponent = np.exp(1j * w * dt * np.arange(1, time_steps + 1))\n",
        "        S_w = np.dot(green.T, exponent) * dt\n",
        "        for iq, q in enumerate(qpoints):\n",
        "            q_matrix = np.exp(-1j * q * np.arange(-center, center + 2, 1))\n",
        "            green_map[iw, iq] = np.imag(np.dot(S_w, q_matrix))\n",
        "    return green_map\n",
        "\n",
        "\n",
        "#  Plotting helpers\n",
        "\n",
        "\n",
        "def plot_spectrum(\n",
        "    dsf,\n",
        "    dt,\n",
        "    q_steps,\n",
        "    w_steps,\n",
        "    lower_bound=False,\n",
        "    upper_bound=False,\n",
        "    title=None,\n",
        "):\n",
        "    \"\"\"Heat-map of the dynamical structure factor.\"\"\"\n",
        "    omega_max = np.pi / dt\n",
        "    qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)\n",
        "    omegas = np.arange(0, omega_max, omega_max / w_steps)\n",
        "    x, y = np.meshgrid(qpoints, omegas)\n",
        "    fig, ax = plt.subplots(figsize=(8, 5))\n",
        "    c = ax.pcolormesh(x, y, dsf / np.max(dsf), cmap=\"viridis\", shading=\"auto\")\n",
        "    fig.colorbar(c, ax=ax, label=\"Normalized intensity\")\n",
        "    if lower_bound:\n",
        "        ax.plot(\n",
        "            qpoints,\n",
        "            np.pi * np.abs(np.sin(qpoints)) / 2,\n",
        "            \"--\",\n",
        "            color=\"white\",\n",
        "            lw=1.5,\n",
        "            label=\"Lower bound\",\n",
        "        )\n",
        "    if upper_bound:\n",
        "        ax.plot(\n",
        "            qpoints,\n",
        "            np.pi * np.abs(np.sin(qpoints / 2)),\n",
        "            \"--\",\n",
        "            color=\"red\",\n",
        "            lw=1.5,\n",
        "            label=\"Upper bound\",\n",
        "        )\n",
        "    ax.set_ylim(0, 3.6)\n",
        "    ax.set_xlim(0, 2 * np.pi - 2 * np.pi / q_steps)\n",
        "    ax.set_xlabel(r\"$q$\", fontsize=16)\n",
        "    ax.set_ylabel(r\"$\\tilde{\\omega} = \\omega / J$\", fontsize=16)\n",
        "    ax.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi])\n",
        "    ax.set_xticklabels([\"0\", r\"$\\pi/2$\", r\"$\\pi$\", r\"$3\\pi/2$\", r\"$2\\pi$\"])\n",
        "    if lower_bound or upper_bound:\n",
        "        ax.legend(loc=\"upper right\", fontsize=11)\n",
        "    if title:\n",
        "        ax.set_title(title, fontsize=14)\n",
        "    plt.tight_layout()\n",
        "    plt.show()\n",
        "\n",
        "\n",
        "def plot_green(n, Gjjc, time_steps, dt, title=None):\n",
        "    \"\"\"Heat-map of the retarded Green's function in real space and time.\"\"\"\n",
        "    fig, ax = plt.subplots(figsize=(8, 6))\n",
        "    t_axis = np.arange(1, time_steps + 1) * dt\n",
        "    site_axis = np.arange(n)\n",
        "    x, y = np.meshgrid(t_axis, site_axis)\n",
        "    c = ax.pcolormesh(\n",
        "        x,\n",
        "        y,\n",
        "        np.real(Gjjc).T,\n",
        "        cmap=\"RdBu\",\n",
        "        vmax=0.5,\n",
        "        vmin=-0.5,\n",
        "        shading=\"auto\",\n",
        "    )\n",
        "    fig.colorbar(c, ax=ax, label=r\"Re $G^R(j, j_c, t)$\")\n",
        "    ax.set_xlabel(r\"Time  ($t / J^{-1}$)\", fontsize=16)\n",
        "    ax.set_ylabel(\"Site index $j$\", fontsize=16)\n",
        "    if title:\n",
        "        ax.set_title(title, fontsize=14)\n",
        "    plt.tight_layout()\n",
        "    plt.show()\n",
        "\n",
        "\n",
        "#  Variational ground-state ansatz (HVA)\n",
        "\n",
        "\n",
        "def _apply_xxz_pair_gate(qc, q0, q1, theta):\n",
        "    \"\"\"Apply the parameterized XXZ-type two-qubit gate used in the HVA.\"\"\"\n",
        "    qc.cx(q0, q1)\n",
        "    qc.rz(theta, q1)\n",
        "    qc.h(q0)\n",
        "    qc.rz(theta + np.pi / 2, q0)\n",
        "    qc.cx(q0, q1)\n",
        "    qc.rz(-theta, q1)\n",
        "    qc.h(q1)\n",
        "    qc.cx(q1, q0)\n",
        "    qc.rz(np.pi / 2, q1)\n",
        "    qc.rz(-np.pi / 2, q0)\n",
        "    qc.h(q1)\n",
        "    qc.h(q0)\n",
        "\n",
        "\n",
        "def build_gs_ansatz(n, params, layers):\n",
        "    \"\"\"Build the Hamiltonian variational ansatz (HVA) circuit for\n",
        "    ground-state preparation of the 1D Heisenberg model.\n",
        "\n",
        "    Starts from a product of singlet pairs and applies alternating\n",
        "    odd/even layers of parameterized XXZ gates. For layer r,\n",
        "    params[2 * r] is the odd-layer (inter-pair) angle and\n",
        "    params[2 * r + 1] is the even-layer (intra-pair) angle.\n",
        "    \"\"\"\n",
        "    qc = QuantumCircuit(n)\n",
        "    # Initial singlet product state\n",
        "    for i in range(n // 2):\n",
        "        qc.x(2 * i)\n",
        "        qc.x(2 * i + 1)\n",
        "        qc.h(2 * i + 1)\n",
        "        qc.cx(2 * i + 1, 2 * i)\n",
        "    # Variational layers\n",
        "    for r in range(layers):\n",
        "        for i in range(1, (n + 1) // 2):  # odd layer\n",
        "            _apply_xxz_pair_gate(qc, 2 * i - 1, 2 * i, params[2 * r])\n",
        "        for i in range(n // 2):  # even layer\n",
        "            _apply_xxz_pair_gate(qc, 2 * i, 2 * i + 1, params[2 * r + 1])\n",
        "    return qc\n",
        "\n",
        "\n",
        "def prepare_ground_state(n, gs_layers=5, max_bond=128, cutoff=1e-8):\n",
        "    \"\"\"Prepare the KCuF3 (isotropic Heisenberg) ground state as a QuantumCircuit.\n",
        "\n",
        "    Runs DMRG (quimb MPO + DMRG2) to get the chain's ground state, then optimizes\n",
        "    the HVA angles to maximize the MPS overlap |<psi_ansatz|psi_DMRG>|^2. No exact\n",
        "    diagonalization, so it scales to larger n.\n",
        "    \"\"\"\n",
        "    J = Jz = 1.0\n",
        "    builder = qtn.SpinHam1D(S=1 / 2)\n",
        "    builder += J * 0.5, \"+\", \"-\"\n",
        "    builder += J * 0.5, \"-\", \"+\"\n",
        "    builder += Jz, \"Z\", \"Z\"\n",
        "    H_mpo = builder.build_mpo(L=n)\n",
        "    dmrg = qtn.DMRG2(H_mpo)\n",
        "    dmrg.solve(tol=1e-8, verbosity=0)\n",
        "\n",
        "    gs_sim = QuimbSimulator(\n",
        "        quimb_circuit_factory=partial(\n",
        "            qtn.CircuitMPS, gate_opts=dict(cutoff=cutoff, max_bond=max_bond)\n",
        "        ),\n",
        "        autodiff_backend=\"jax\",\n",
        "    )\n",
        "\n",
        "    def gs_infidelity(params):\n",
        "        psi = tensornetwork_from_circuit(\n",
        "            build_gs_ansatz(n, params, gs_layers), gs_sim\n",
        "        ).psi\n",
        "        return 1 - abs(psi.H @ dmrg.state) ** 2\n",
        "\n",
        "    # Seed and optimizer match the original tutorial. Each layer starts at\n",
        "    # [0, pi/2]: an odd-layer angle of 0 makes the inter-pair gate the identity,\n",
        "    # and an even-layer angle of pi/2 makes the intra-pair gate a SWAP (since\n",
        "    # 0.5 * (XX + YY + ZZ) = SWAP - I/2). That puts the seed at the singlet-pair\n",
        "    # product limit, which is already a decent approximation to the Heisenberg\n",
        "    # ground state, so the optimizer only has to refine it. The small jitter\n",
        "    # (fixed RNG seed, so runs are reproducible) breaks the exact symmetry\n",
        "    # between layers; COBYQA then runs for up to 100 iterations.\n",
        "    rng = np.random.default_rng(12345)\n",
        "    x0 = np.tile([0.0, np.pi / 2], gs_layers) + rng.normal(\n",
        "        scale=0.1, size=2 * gs_layers\n",
        "    )\n",
        "    result_gs = scipy.optimize.minimize(\n",
        "        gs_infidelity, x0, method=\"COBYQA\", options={\"maxiter\": 100}\n",
        "    )\n",
        "    print(f\"DMRG ground-state energy: {dmrg.energy:.6f}\")\n",
        "    print(f\"GS fidelity: {1 - result_gs.fun:.4f}\")\n",
        "    return build_gs_ansatz(n, result_gs.x, gs_layers)\n",
        "\n",
        "\n",
        "print(\"Setup complete - helpers defined.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "setup-load-md",
      "metadata": {},
      "source": [
        "<span id=\"load-the-function-template\" />\n",
        "\n",
        "### Charger le modèle de fonction\n",
        "\n",
        "Connectez-vous à Qiskit Serverless et chargez le fichier déployé `aqc-dynamics-function`. Les deux exemples de ce tutoriel font appel au même `fn` handle; la fonction n'est donc chargée qu'une seule fois, ici.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "setup-connect",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Credentials are read from the account saved once via QiskitServerless.save_account(...)\n",
        "serverless = QiskitServerless()\n",
        "fn = serverless.load(\"aqc-dynamics-function\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-md",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Exemple de simulateur à petite échelle\n",
        "\n",
        "Nous avons d'abord exécuté l'intégralité du workflow sur une petite chaîne de 10 sites en utilisant le `statevector` backend exact. Cela permet de valider le pipeline PRE → FUNCTION → POST avant de mobiliser des ressources du QPU.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-s1-md",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique\n",
        "\n",
        "Construisez l'hamiltonien KCuF$_3$ sous la forme `SparsePauliOp` d'un hamiltonien de Heisenberg isotrope : $XX + YY + ZZ$ avec un couplage de $\\tfrac14$ sur chaque liaison entre voisins immédiats; les chaînes sont des opérateurs de Pauli, donc $\\tfrac14$ donne le couplage de spin $\\frac{1}{2}$. Préparez l'état fondamental à l'aide de la méthode DMRG combinée à la maximisation de la fidélité MPS, puis intégrez l'effet neutronique : une rotation de type « $\\pi/2$ » $Z$ au niveau du site central. C'est ce circuit préparé que nous transmettons à la fonction sous la forme `initial_state`. Nous conservons la valeur par `observables` défaut ( $Z$ par site), qui correspond exactement à la valeur « $\\langle \\sigma_z^j \\rangle(t)$ » requise par le flux de travail sur les neutrons.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "small-s1-code",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "DMRG ground-state energy: -4.258035\n",
            "GS fidelity: 0.9841\n",
            "Prepared 10-qubit ground state with the neutron kick at site 4.\n"
          ]
        }
      ],
      "source": [
        "n = 10\n",
        "dt = 0.6  # physical time per Trotter step (also the omega-axis unit in POST)\n",
        "time_steps = 10\n",
        "center = n // 2 - 1\n",
        "\n",
        "# MPS-simulator settings, shared by the ground-state prep here and the AQC\n",
        "# compression inside the function (matches the original tutorial).\n",
        "mps_max_bond = 32\n",
        "mps_cutoff = 1e-8\n",
        "\n",
        "# 1D isotropic Heisenberg (KCuF3) Hamiltonian on n qubits\n",
        "H = SparsePauliOp.from_sparse_list(\n",
        "    [(p, [i, i + 1], 0.25) for i in range(n - 1) for p in (\"XX\", \"YY\", \"ZZ\")],\n",
        "    num_qubits=n,\n",
        ")\n",
        "\n",
        "# Ground state (DMRG + fidelity max) + neutron kick baked into the same circuit\n",
        "gs_circuit = prepare_ground_state(\n",
        "    n, gs_layers=3, max_bond=mps_max_bond, cutoff=mps_cutoff\n",
        ")\n",
        "gs_circuit.rz(\n",
        "    np.pi / 2, center\n",
        ")  # exp(-i (pi/2)/2 Z_center): the neutron perturbation\n",
        "print(\n",
        "    f\"Prepared {n}-qubit ground state with the neutron kick at site {center}.\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-s3-md",
      "metadata": {},
      "source": [
        "<span id=\"steps-2-and-3-compress-and-execute-with-the-function-template\" />\n",
        "\n",
        "### Étapes 2 et 3 : Compresser et exécuter à l'aide du modèle de fonction\n",
        "\n",
        "Dans un processus manuel, il s'agit de deux étapes distinctes : l'optimisation des circuits pour le matériel (étape 2) et leur exécution (étape 3). Le modèle de fonction regroupe les deux en un seul appel. Il effectue la synthèse de Trotter, la compression AQC et la transpilation matérielle, puis exécute les circuits (ici sur le simulateur exact, puis ultérieurement avec un système intégré d'atténuation des erreurs sur le matériel). Les deux paramètres de réglage sont `aqc_segments` (le plan de compression) et `aqc_options` (les paramètres du MPS et de l'optimiseur). Chaque segment `{\"n_steps\": k, \"ansatz_steps\": m}` compresse des étapes de Trotter `k` consécutives en un ansatz construit à partir d'une cible de Trotter à `m`étapes, et toutes les étapes au-delà de sont `sum(n_steps)` exécutées selon la méthode de Trotter classique. Les premières étapes, caractérisées par un faible niveau d'intrication, se compressent bien en un ansatz peu profond (`ansatz_steps=1`), c'est pourquoi nous compressons ici les trois premières étapes en un ansatz à une seule couche et les deux suivantes en un ansatz plus profond à deux couches; les cinq étapes restantes parmi les dix étapes de Trotter sont exécutées selon la méthode de Trotter classique. En effet, nous `aqc_options` reprenons le tutoriel d'origine : la dimension de la liaison MPS `max_bond=32`, `cutoff=1e-8`, et un optimiseur L-BFGS-B plafonné à 100 itérations.\n",
        "\n",
        "Appelez la fonction chargée dans Setup. `backend=\"statevector\"` exécute le chemin de référence exact : aucun temps de traitement sur le QPU, les circuits s'exécutant sur un simulateur de vecteurs d'état exact au sein du worker « serverless » (un compte Qiskit Serverless enregistré reste toutefois nécessaire pour l'appeler). La fonction `initial_state` reprend l'état fondamental préparé (y compris le « kick »); `observables` est omis afin que la fonction mesure l' $Z$ par site par défaut.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "small-s3-run",
      "metadata": {},
      "outputs": [],
      "source": [
        "job = fn.run(\n",
        "    t_steps=time_steps,\n",
        "    aqc_segments=[\n",
        "        {\n",
        "            \"n_steps\": 3,\n",
        "            \"ansatz_steps\": 1,\n",
        "        },  # early steps -> shallow 1-layer ansatz\n",
        "        {\n",
        "            \"n_steps\": 2,\n",
        "            \"ansatz_steps\": 2,\n",
        "        },  # later steps -> deeper 2-layer ansatz\n",
        "    ],\n",
        "    aqc_options={\n",
        "        \"max_bond\": mps_max_bond,  # MPS bond dimension for AQC compression\n",
        "        \"cutoff\": mps_cutoff,\n",
        "        \"optimizer_settings\": {\n",
        "            \"method\": \"L-BFGS-B\",\n",
        "            \"jac\": True,\n",
        "            \"options\": {\"maxiter\": 100},\n",
        "        },\n",
        "    },\n",
        "    dt=dt,\n",
        "    hamiltonian=H,\n",
        "    initial_state=gs_circuit,  # prepared ground state including the neutron kick\n",
        "    # observables omitted -> default per-site Z (the neutron sigma_z readout)\n",
        "    backend=\"statevector\",\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "small-status",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "DONE\n"
          ]
        }
      ],
      "source": [
        "print(job.status())  # rerun this cell until status says DONE"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "small-s3-result",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "AQC fidelities: {'1': 1.0, '2': 0.9999, '3': 0.9992, '4': 0.9998, '5': 0.9995}\n",
            "Green's function shape: (10, 10)\n"
          ]
        }
      ],
      "source": [
        "# The per-site <sigma_z>(t) the function returns is the retarded Green's function\n",
        "# G(j, j_c, t). The workflow samples t = 1..time_steps, so drop the t = 0 row (the\n",
        "# prepared+kicked state before any evolution) before post-processing.\n",
        "result = job.result()\n",
        "print(\n",
        "    \"AQC fidelities:\",\n",
        "    {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n",
        ")\n",
        "\n",
        "ev = np.array(result[\"expectation_values\"])\n",
        "Gjjc = ev[1:]  # shape (time_steps, n)\n",
        "print(\"Green's function shape:\", Gjjc.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-s4-md",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Étape 4 : Traitement ultérieur et restitution du résultat dans le format classique souhaité\n",
        "\n",
        "Appliquer la transformée de Fourier à la fonction de Green pour obtenir $S(q, \\omega)$, effectuer une symétrie par réflexion et supprimer les valeurs négatives : c'est le post-traitement standard des données neutroniques. La symétrie est exacte car $S(q, \\omega) = S(-q, \\omega)$ pour ce modèle, et les valeurs négatives qui subsistent sont des artefacts issus de la transformation de Fourier d'une série temporelle finie et échantillonnée de manière discrète; elles sont donc tronquées à zéro. Dans cette petite simulation exacte, le continuum à deux spinons n'est que grossièrement résolu, mais le principe est identique à celui de la simulation sur matériel qui suit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "small-s4-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "q_res, w_res = 100, 100\n",
        "spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)\n",
        "spectrum = -(spectrum + spectrum[:, ::-1]) / 2  # mirror symmetry\n",
        "spectrum = np.clip(spectrum, a_min=0, a_max=None)  # clip negatives\n",
        "\n",
        "plot_green(\n",
        "    n,\n",
        "    Gjjc,\n",
        "    time_steps,\n",
        "    dt,\n",
        "    title=f\"Retarded Green's function - {n} qubits (AQC, statevector)\",\n",
        ")\n",
        "plot_spectrum(\n",
        "    spectrum,\n",
        "    dt,\n",
        "    q_res,\n",
        "    w_res,\n",
        "    lower_bound=True,\n",
        "    upper_bound=True,\n",
        "    title=f\"Dynamical structure factor - {n} qubits (AQC, statevector)\",\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-md",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Exemple de matériel à grande échelle\n",
        "\n",
        "Ce même flux de travail peut être étendu sans modifier le code scientifique : une chaîne de 30 sites, une profondeur de Trotter doublée (20 étapes), un plan de compression qui fait varier la profondeur de l’ansatz (un ansatz plus profond pour les étapes ultérieures, où l’intrication est plus forte), et une exécution sur un processeur IBM Quantum avec les mécanismes intégrés de mitigation des erreurs de la fonction (découplage dynamique, « Pauli twirling » et extinction des erreurs de lecture par rotation (TREX)). Nous suivons les quatre mêmes étapes que dans l'exemple du simulateur, en réutilisant le `fn` handle créé lors de la configuration.\n",
        "\n",
        "|                                                  | À petite échelle | À grande échelle                    |\n",
        "| ------------------------------------------------ | ---------------- | ----------------------------------- |\n",
        "| Qubits                                           | 10               | 30                                  |\n",
        "| Pas de trotteur                                  | 10               | 20                                  |\n",
        "| Étapes de compression AQC (1 couche + 2 couches) | 3 + 2 = 5        | 6 + 4 = 10                          |\n",
        "| Couches d'ansatz de l'état fondamental           | 3                | 5                                   |\n",
        "| Dimension maximale des liaisons MPS              | 32               | 128                                 |\n",
        "| Back-end                                         | `statevector`    | QPU avec DD, effet de Pauli et TREX |\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-s1-md",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique\n",
        "\n",
        "Construisez le même KCuF$_3$ Heisenberg et `SparsePauliOp` préparez l'état fondamental, en utilisant cette fois un `gs_layers=5` ansatz plus profond pour la chaîne plus longue, puis intégrez la impulsion neutronique $\\pi/2$ $Z$ au site central. C'est identique à la cartographie à petite échelle, mais à l'adresse $n = 30$.\n",
        "\n",
        "Il faut s'attendre à une fidélité de l'état fondamental inférieure à celle obtenue pour la simulation à 10 sites : environ 0.82 ici, contre 0.98 pour la chaîne plus courte, car cinq couches HVA ne permettent pas de reproduire intégralement l'état fondamental d'une chaîne de 30 sites. C'est tout à fait normal, et ce n'est pas un problème; d'ailleurs, le tutoriel d'origine admet environ 0.65 sur 50 sites pour la même raison. L'augmentation `gs_layers` de la limite d'itération COBYQA permet d'améliorer les résultats, mais entraîne un surcoût en termes de calcul classique.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 34,
      "id": "large-code",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "DMRG ground-state energy: -13.111355\n",
            "GS fidelity: 0.8201\n",
            "Prepared 30-qubit ground state with the neutron kick at site 14.\n"
          ]
        }
      ],
      "source": [
        "n = 30\n",
        "dt = 0.6\n",
        "time_steps = 20\n",
        "center = n // 2 - 1\n",
        "\n",
        "# Same MPS settings as the original large-scale run: a larger bond for the\n",
        "# longer, more-entangled chain (shared by GS prep and AQC compression).\n",
        "mps_max_bond = 128\n",
        "mps_cutoff = 1e-8\n",
        "\n",
        "# Same KCuF3 Hamiltonian and ground-state prep, on a larger chain\n",
        "H = SparsePauliOp.from_sparse_list(\n",
        "    [(p, [i, i + 1], 0.25) for i in range(n - 1) for p in (\"XX\", \"YY\", \"ZZ\")],\n",
        "    num_qubits=n,\n",
        ")\n",
        "gs_circuit = prepare_ground_state(\n",
        "    n, gs_layers=5, max_bond=mps_max_bond, cutoff=mps_cutoff\n",
        ")\n",
        "gs_circuit.rz(np.pi / 2, center)  # neutron kick at the center site\n",
        "print(\n",
        "    f\"Prepared {n}-qubit ground state with the neutron kick at site {center}.\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-s3-md",
      "metadata": {},
      "source": [
        "<span id=\"steps-2-and-3-compress-and-execute-with-the-function-template\" />\n",
        "\n",
        "### Étapes 2 et 3 : Compresser et exécuter à l'aide du modèle de fonction\n",
        "\n",
        "Il s'agit du même appel que dans l'exemple du simulateur, mais cette fois-ci avec `backend_name` pointant vers un processeur IBM Quantum, de sorte que la fonction soit compilée et exécutée à cet endroit. Le plan de compression fait varier la profondeur de l'ansatz : les six premières étapes de Trotter (à faible enchevêtrement) sont compressées en un ansatz simple de faible profondeur, les quatre suivantes en un ansatz à deux couches plus profond, et les 10 étapes restantes sur les 20 sont exécutées selon la méthode de Trotter standard. `aqc_options` augmente la dimension de la liaison MPS à `max_bond=128` pour la chaîne plus longue et plus enchevêtrée (conformément à l'original), tout en conservant le même optimiseur L-BFGS-B plafonné à 100 itérations. `estimator_options` Activer les mécanismes intégrés d'atténuation des erreurs : le découplage dynamique ( XY4 ), le « gate twirling » et l'atténuation des mesures TREX. Les valeurs par défaut de la fonction correspondent déjà à celles du tutoriel d'origine pour tous ces éléments, à l'exception du budget d'apprentissage TREX (`measure_noise_learning`). L'ensemble du bloc est tout de même affiché, car un paramètre fourni par l'appelant `estimator_options` remplace intégralement les valeurs par défaut de la fonction au lieu de s'y fondre; ainsi, si une clé était omise, la valeur par défaut de `IBM Quantum Compute` serait utilisée à la place de celle de la fonction.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "large-s3-run",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "job ID (save this to reconnect later): 43ed8d07-6d7d-4f33-b70a-7f31b765b310\n"
          ]
        }
      ],
      "source": [
        "# Steps 2 + 3: the function compresses (varied ansatz) and executes on hardware.\n",
        "job = fn.run(\n",
        "    t_steps=time_steps,\n",
        "    aqc_segments=[\n",
        "        {\n",
        "            \"n_steps\": 6,\n",
        "            \"ansatz_steps\": 1,\n",
        "        },  # early steps -> shallow 1-layer ansatz\n",
        "        {\n",
        "            \"n_steps\": 4,\n",
        "            \"ansatz_steps\": 2,\n",
        "        },  # later steps -> deeper 2-layer ansatz\n",
        "    ],\n",
        "    aqc_options={\n",
        "        \"max_bond\": mps_max_bond,  # 128 for the longer chain\n",
        "        \"cutoff\": mps_cutoff,\n",
        "        \"optimizer_settings\": {\n",
        "            \"method\": \"L-BFGS-B\",\n",
        "            \"jac\": True,\n",
        "            \"options\": {\"maxiter\": 100},\n",
        "        },\n",
        "    },\n",
        "    dt=dt,\n",
        "    hamiltonian=H,\n",
        "    initial_state=gs_circuit,\n",
        "    backend_name=\"ibm_pittsburgh\",\n",
        "    # Mitigation settings from the original tutorial. Only the two\n",
        "    # measure_noise_learning values differ from the function's defaults; the rest\n",
        "    # restates them, because a caller-supplied estimator_options dict replaces the\n",
        "    # function's defaults wholesale rather than merging into them.\n",
        "    estimator_options={\n",
        "        \"environment\": {\"job_tags\": [\"TUT-SNS\"]},\n",
        "        \"dynamical_decoupling\": {\"enable\": True, \"sequence_type\": \"XY4\"},\n",
        "        \"twirling\": {\n",
        "            \"enable_gates\": True,\n",
        "            \"num_randomizations\": 1000,\n",
        "            \"shots_per_randomization\": 128,\n",
        "        },\n",
        "        \"resilience\": {\n",
        "            \"measure_mitigation\": True,\n",
        "            \"measure_noise_learning\": {\n",
        "                \"num_randomizations\": 32,\n",
        "                \"shots_per_randomization\": 100,\n",
        "            },\n",
        "        },\n",
        "    },\n",
        ")\n",
        "print(\"job ID (save this to reconnect later):\", job.job_id)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-reconnect-md",
      "metadata": {},
      "source": [
        "<Admonition type=\"note\" title=\"Se reconnecter à une tâche en cours d'exécution depuis longtemps\">\n",
        "  Le calcul à grande échelle n'est pas rapide, et se déroule la plupart du temps en mode classique plutôt que sur le QPU. La compression AQC s'effectue au sein de la fonction avant que les données n'atteignent le QPU : sur 30 sites, `max_bond=128` cette opération a pris près de quatre heures lors de notre exécution, contre les quelque 18 minutes de temps de QPU indiquées dans l' *estimation* d'utilisation en haut de ce tutoriel. À cela s'ajoute le temps d'attente dans la file d'attente. Il n'est pas nécessaire de laisser ce bloc-notes ou ce noyau ouvert pendant son exécution.\n",
        "\n",
        "  Copiez l'identifiant de la tâche affiché dans la cellule précédente et enregistrez-le. Les trois cases suivantes vous permettent de reprendre la partie plus tard :\n",
        "\n",
        "  1. Se reconnecter (nécessaire uniquement lors d'une nouvelle session du noyau) : relancez les cellules [de configuration](#setup) pour les recréer `serverless`, puis reconstruisez le `job` descripteur à partir de l'ID que vous avez enregistré. Ignorez cette cellule si vous êtes toujours dans la session au cours de laquelle vous avez effectué votre soumission, car le identifiant est déjà actif.\n",
        "  2. Vérifier l'état : relancer jusqu'à ce que le message s'affiche `DONE`.\n",
        "  3. Récupérer le résultat : n'exécuter cette opération que lorsque le statut est `DONE`.\n",
        "\n",
        "  La cellule de reconnexion suivante contient un espace réservé. Remplacez-le par le vôtre `job_id`:\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "large-reconnect",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel\n",
        "# session; if you are still in the session where you submitted, the `job` handle\n",
        "# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.\n",
        "job = serverless.get_job_by_id(\"<your job ID>\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "large-status",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "DONE\n"
          ]
        }
      ],
      "source": [
        "# Check where the job is. Re-run this until it reports DONE before fetching the\n",
        "# result in the following cell: QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE ->\n",
        "# RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING\n",
        "# -> DONE.\n",
        "print(job.status())"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "large-s3-result",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "AQC fidelities: {'1': 1.0, '2': 0.9994, '3': 0.9944, '4': 0.9853, '5': 0.9747, '6': 0.959, '7': 0.9495, '8': 0.9542, '9': 0.9533, '10': 0.9451}\n"
          ]
        }
      ],
      "source": [
        "# Run this only once the preceding status cell reports DONE. result() blocks until\n",
        "# the job finishes, so calling it earlier just waits (possibly for hours).\n",
        "result = job.result()\n",
        "print(\n",
        "    \"AQC fidelities:\",\n",
        "    {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n",
        ")\n",
        "\n",
        "ev = np.array(result[\"expectation_values\"])\n",
        "Gjjc = ev[1:]  # drop the t = 0 row -> shape (time_steps, n)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-s4-md",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Étape 4 : Traitement ultérieur et restitution du résultat dans le format classique souhaité\n",
        "\n",
        "Traitement post-calcul identique à celui effectué lors de la simulation : appliquer la transformée de Fourier à la fonction de Green pour obtenir l $S(q, \\omega)$ s, effectuer une symétrie par réflexion et écrêter les valeurs négatives. Grâce à la chaîne plus longue et à l'évolution, le continuum à deux spinons est bien mieux résolu. Elle devrait remplir la bande comprise entre les limites en pointillés, avec une luminosité maximale près de $q = \\pi$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 33,
      "id": "large-result",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "n = result[\"metadata\"][\"n\"]\n",
        "q_res, w_res = 100, 100\n",
        "spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)\n",
        "spectrum = -(spectrum + spectrum[:, ::-1]) / 2  # mirror symmetry\n",
        "spectrum = np.clip(spectrum, a_min=0, a_max=None)  # clip negatives\n",
        "\n",
        "plot_green(\n",
        "    n,\n",
        "    Gjjc,\n",
        "    time_steps,\n",
        "    dt,\n",
        "    title=f\"Retarded Green's function - {n} qubits (AQC, hardware)\",\n",
        ")\n",
        "plot_spectrum(\n",
        "    spectrum,\n",
        "    dt,\n",
        "    q_res,\n",
        "    w_res,\n",
        "    lower_bound=True,\n",
        "    upper_bound=True,\n",
        "    title=f\"Dynamical structure factor - {n} qubits (AQC, hardware)\",\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "appendix-md",
      "metadata": {},
      "source": [
        "<span id=\"appendix\" />\n",
        "\n",
        "## Appendice\n",
        "\n",
        "L'exemple de matériel ci-dessus utilise une seule longueur de chaîne. Les trois spectres ci-dessous proviennent de simulations antérieures de ce même flux de travail `ibm_pittsburgh` sur 10, 20 et 30 sites, tous les autres paramètres d'entrée étant maintenus fixes : 20 étapes de Trotter à `dt = 0.6`, le plan de compression comprenant six étapes à une couche et quatre étapes à deux couches compressées par AQC, et `max_bond = 128`. Il s'agit de résultats enregistrés, et non du résultat des cellules précédentes.\n",
        "\n",
        "Les mêmes paramètres sont utilisés pour les trois tailles, ce qui permet de comparer directement les spectres. Les ajuster en fonction de la longueur de la chaîne, par exemple en augmentant le nombre de couches de l'ansatz de l'état fondamental ou en choisissant une valeur plus grande `max_bond`, peut donner de meilleurs résultats que ceux présentés ici.\n",
        "\n",
        "![Facteur de structure dynamique à 10 sites : un seul pic vif et net à q = π, proche de la limite](https://quantum.cloud.ibm.com/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-10.avif \" inférieure de 10 qubits\")\n",
        "\n",
        "![Facteur de structure dynamique à 20 sites, pondération spectrale comblant la bande entre les deux limites](https://quantum.cloud.ibm.com/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-20.avif \" en pointillés à deux spinons — 20 qubits\")\n",
        "\n",
        "![Facteur de structure dynamique à 30 sites; le continuum est représenté de manière plus fine, avec un contraste plus faible et une certaine pondération en dehors des limites](https://quantum.cloud.ibm.com/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-30.avif \" des 30 qubits.\")\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "nextsteps",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Etapes suivantes\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommandations\">\n",
        "  * Adaptez ce flux de travail à votre propre système : la fonction accepte n'importe quelle « 1D » de type « plus proche voisin » `SparsePauliOp`; ainsi, une chaîne hamiltonienne, un état initial ou un ensemble d'observables différents permettent d'exécuter le même pipeline PRE → FUNCTION → POST. Consultez le contrat complet d'entrée-sortie dans le [modèle AQC Dynamics disponible sur GitHub](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter).\n",
        "  * Consultez l'article dont est tiré ce benchmark : Lee et al., [*Comparaison de la simulation quantique avec des expériences de diffusion des neutrons*](https://arxiv.org/abs/2603.15608) ( arXiv:2603.15608 ).\n",
        "  * Par rapport au [tutoriel original « Simuler la diffusion neutronique »](/docs/tutorials/simulate-neutron-scattering), ce workflow intégré transpose celui-ci dans un modèle de fonction déployé.\n",
        "  * Approfondissez vos connaissances sur les [techniques d'atténuation et de suppression des erreurs](/docs/guides/error-mitigation-and-suppression-techniques) appliquées lors de l'exécution matérielle : découplage dynamique, « Pauli twirling » et TREX.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "hours": 4,
    "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"
    },
    "qpuSeconds": 1080
  },
  "nbformat": 4,
  "nbformat_minor": 5
}