{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "slc-title-0000-4000-8000-000000000000",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Annulation probabiliste des erreurs à l'aide de cônes de lumière ombrés\"\n",
        "description: \"Utiliser la technique d'annulation probabiliste des erreurs (PEC) avec des cônes de lumière ombrés afin de réduire la surcharge liée à l'échantillonnage PEC et d'atténuer les erreurs d'estimation de la valeur attendue au niveau matériel\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore samplomatic samplex Trotterized Trotter Lindblad lightcone lightcones Eddins reverser broadcastable postselection postselect antinoise eigval atol starmap unmitigated NoiseLearner PauliLindblad QubitSparsePauli Kingston kicked qubit canonical isa plm qspl Rzz pauli TREX levelname xticks expvals markerfacecolor ncols */}\n",
        "\n",
        "<span id=\"probabilistic-error-cancellation-with-shaded-lightcones\" />\n",
        "\n",
        "# Annulation probabiliste des erreurs à l'aide de cônes de lumière ombrés\n",
        "\n",
        "*Estimation du temps d'exécution : 10 minutes sur un processeur Heron (REMARQUE : il s'agit uniquement d'une estimation. (Votre temps d'exécution peut varier.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-lout-0000-4000-8000-000000000001",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Acquis d'apprentissage\n",
        "\n",
        "À l'issue de ce tutoriel, les utilisateurs devraient avoir compris :\n",
        "\n",
        "* Qu'est-ce que l'annulation probabiliste d'erreur (PEC) et pourquoi sa surcharge d'échantillonnage $\\gamma^2$ croît de manière exponentielle avec le bruit total agissant sur le circuit?\n",
        "* Comment les cônes de lumière ombragés (SLC) limitent la contribution de chaque terme de bruit à l'observable cible, ce qui vous permet d'allouer votre budget d'atténuation là où cela compte vraiment — et comment, au-delà de la réduction de la surcharge sans biais déjà offerte par un cône de lumière purement géométrique, l'ombrage vous permet d'échanger un biais résiduel limité contre une surcharge d'échantillonnage encore plus faible\n",
        "* Comment apprendre le bruit de couche à l'aide de `NoiseLearnerV3` et injecter de l'anti-bruit via `samplomatic` et la `Executor` primitive\n",
        "* Comment combiner les méthodes PEC et PEC+SLC avec TREX et la post-sélection pour estimer une valeur attendue sur matériel\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prérequis\n",
        "\n",
        "Nous recommandons aux utilisateurs de se familiariser avec les sujets suivants avant de suivre ce tutoriel :\n",
        "\n",
        "* Le flux de travail [« Patterns »](/docs/guides/intro-to-patterns) de Qiskit\n",
        "* Utilisation de la primitive [« Estimator »](/docs/guides/get-started-with-estimator) pour calculer les valeurs attendues d'une variable observable\n",
        "* Les techniques d'atténuation des erreurs telles que le « Pauli twirling » et TREX, abordées dans [la section « Combiner les options d'atténuation des erreurs avec la primitive Estimator »](/docs/tutorials/combine-error-mitigation-techniques)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-bkgd-0000-4000-8000-000000000002",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Arrière-plan\n",
        "\n",
        "Ce tutoriel explique comment limiter les erreurs à l'aide de l'extension « Shaded Light Cone » (SLC). Cet add-on constitue une évolution de la [technique d'annulation probabiliste des erreurs (PEC)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-cancellation-pec), dans laquelle l'utilisateur identifie le bruit présent dans chaque couche d'un circuit, puis l'annule en appliquant des portes à un seul qubit et des techniques de post-traitement. Par rapport à d'autres méthodes, la PEC offre des bornes plus solides sur le biais du résultat corrigé, mais tend à entraîner une charge plus importante en termes de temps de calcul sur le QPU. Au cours du PEC, afin de compenser l'atténuation de la valeur attendue due au bruit, le résultat moyen est rééchelonnné par un facteur de $\\gamma = \\exp(\\sum_{l,\\sigma} 2\\lambda_{l,\\sigma})$, où $\\lambda_{l,\\sigma}$ correspond au taux de bruit d'erreur de Pauli $\\sigma$ appris au niveau de la couche $l$ du circuit. Ce changement d'échelle multiplie la variance par un facteur de $\\gamma^2$ et multiplie donc également le nombre d'exécutions de circuits nécessaires sur le QPU par $\\gamma^2$, ce que nous appelons le coût d'échantillonnage ou la surcharge d'échantillonnage. Comme la fonction « $\\gamma$ » croît de manière exponentielle, le PEC est souvent limité à des circuits peu profonds ou comportant peu de qubits. Pour en savoir plus sur la PEC, consultez l'article « [Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors](https://arxiv.org/abs/2201.09866) ».\n",
        "\n",
        "Si nous parvenons à identifier les erreurs qui ne nécessitent pas de mesures correctives, nous pourrons réduire ce coût d'échantillonnage de manière exponentielle. Une première étape dans cette direction consiste à mettre en œuvre une atténuation des erreurs tenant compte du contexte local, qui utilise un « cône de lumière » conventionnel, rapidement calculable, afin de réduire la surcharge liée au PEC en limitant la sensibilité d'une observable aux erreurs à l'échelle de l'ensemble du circuit, ce qui étend la faisabilité du PEC à des échelles plus grandes pour certains problèmes. Les erreurs situées en dehors de ce cône de lumière ne peuvent pas influencer le résultat mesuré et peuvent donc être exclues de la procédure d'annulation des erreurs. Cette exclusion réduit la charge liée à l'échantillonnage, parfois de manière significative, sans introduire de biais supplémentaire. En particulier, pour mesurer une observable locale $O$ d’un circuit à profondeur fixe, la surcharge d’échantillonnage nécessaire finit par se stabiliser lorsque l’on augmente le nombre de qubits dans le circuit (voir la figure 2b dans «[ Locality and error mitigation of quantum circuits](https://arxiv.org/abs/2303.06496) »).\n",
        "\n",
        "Les « lightcones ombragés » (SLC) vont plus loin : ils utilisent des simulations classiques pour mieux maîtriser la sensibilité aux erreurs sur l'ensemble du circuit. Cela permet d'échanger une partie du temps d'exécution du QPU contre du temps d'exécution du CPU et de réduire la surcharge d'échantillonnage nécessaire à la renormalisation du biais. Au lieu d'une limite stricte, on attribue à chaque erreur potentielle du circuit une « nuance » graduelle qui définit la borne supérieure de la sensibilité de la valeur attendue de l'observable à cette erreur. (Pour les limites obtenues par propagation en arrière présentées ci-dessous, cette susceptibilité dépend également de l'état initial du circuit — à savoir l'état « tout à zéro » dans les flux de travail classiques, ainsi que dans ce tutoriel.) Cette caractérisation affinée permet des applications plus efficaces et mieux ciblées de la PEC, avec une variance réduite, tout en offrant à l'utilisateur la possibilité d'ajuster de manière contrôlée le biais dans l'estimation de l'observable. Pour plus de détails, voir «[ Lightcone shading for classically accelerated quantum error mitigation](https://arxiv.org/abs/2409.04401) ».\n",
        "\n",
        "Notre workflow pour l'extension SLC exploite la [`samplomatic`](https://qiskit.github.io/samplomatic/guides/index.html) bibliothèque ainsi que les `QuantumProgram` classes et `Executor` ajoutées à Qiskit Runtime dans `qiskit-ibm-runtime`v0.47.0, ce qui permet aux utilisateurs de bénéficier d'un contrôle plus modulaire des paramètres d'exécution en matière de suppression et d'atténuation des erreurs, tout en conservant une grande facilité d'utilisation. Pour en savoir plus, consultez le guide sur [le modèle d'exécution dirigée](/docs/guides/directed-execution-model).\n",
        "\n",
        "<span id=\"slc-error-mitigation-workflow-at-a-glance\" />\n",
        "\n",
        "### Aperçu du processus de gestion des erreurs SLC\n",
        "\n",
        "Pour modéliser le bruit du QPU, nous utilisons un modèle de bruit de Pauli-Lindblad clairsemé, avec des taux d'erreur de Pauli à un et deux qubits, générés localement sur chaque qubit et chaque bord du dispositif. Compte tenu de ce choix, le processus de réduction des erreurs SLC présenté dans ce tutoriel se déroule comme suit :\n",
        "\n",
        "a. CPU — Impact par erreur lié aux erreurs de Pauli à un et deux qubits\n",
        "\n",
        "1. Propagation vers l'avant (effet lié sur l'observable). Propager chaque erreur jusqu'à l'extrémité du circuit et calculer son commutateur avec l'observable.\n",
        "   * Tronquer les termes de l'opérateur au cours de l'évolution afin de garantir la gérabilité des calculs.\n",
        "   * Resserrez encore ces limites à l'aide d'une rétropropagation approximative de l'observable, fondée sur les limites de vitesse quantiques.\n",
        "2. Propagation en arrière (effet lié à l'état initial). Propager chaque erreur jusqu'au début du circuit et calculer son commutateur avec l'état initial.\n",
        "\n",
        "b. QPU — Découvrez les taux de bruit. Utilisez `NoiseLearnerV3` pour estimer les taux du modèle de bruit de Pauli-Lindblad.\n",
        "\n",
        "c. CPU — Donner la priorité aux mesures d'atténuation\n",
        "\n",
        "1. Mettre à jour les limites fusionnées en fonction des taux de bruit appris. Combiner les limites avant et arrière calculées précédemment et les mettre à jour à l'aide des taux de bruit appris.\n",
        "2. Classez les composantes de bruit à atténuer en utilisant les limites calculées et les taux appris. Classez par ordre de priorité chaque erreur de bruit potentielle en fonction de son impact estimé sur le biais et du coût associé à sa correction.\n",
        "\n",
        "d. QPU — Insérez l'anti-bruit et lancez l'exécution. Exécutez le circuit de votre choix en spécifiant l'anti-bruit (bruit inverse) à l'aide `Box` d'annotations.\n",
        "\n",
        "e. CPU — Estimation observable. Calculer la valeur attendue en appliquant une post-sélection basée sur la mesure afin de réduire l'impact du bruit non markovien.\n",
        "\n",
        "<span id=\"noise-learning-overview\" />\n",
        "\n",
        "### Présentation de l'apprentissage au bruit\n",
        "\n",
        "L'apprentissage du bruit est une étape courante dans plusieurs méthodes d'atténuation des erreurs, mise en œuvre par [l](/docs/guides/noise-learning) 'apprenant de bruit; elle apparaît également dans le [tutoriel sur l'amplification probabiliste des erreurs](/docs/tutorials/probabilistic-error-amplification). Dans `NoiseLearnerV3`, vous pouvez identifier spécifiquement les couches de bruit à apprendre en tant [`CircuitInstruction`](/docs/api/qiskit/qiskit.circuit.CircuitInstruction) qu’objets, ce qui vous permet de calculer les limites de bruit SLC souhaitées pour chaque couche, selon la méthode décrite ci-dessus. Le modèle théorique de Pauli-Lindblad fournit des coefficients à utiliser dans la hiérarchisation PEC+SLC. `generate_noise_model_paulis`La manière dont les portes sont regroupées en couches peut être déterminée à l'aide des `generate_boxing_pass_manager` fonctions pratiques et `find_unique_box_instructions` , puis transmise à la fonction utilitaire SLC, comme décrit à l'étape 2 ci-dessous.\n",
        "\n",
        "| **Partie 1**                                                                                                             | **Deuxième partie**                                                                                                               | **Composant 3**                                                                                                       |\n",
        "| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n",
        "| Couches de portes à deux qubits de type « Pauli-twirl »                                                                  | Répéter les paires de couches identiques et apprendre le bruit                                                                    | Calculer la fidélité (erreur pour chaque canal de bruit)                                                              |\n",
        "| ![La pirouette de Pauli](https://quantum.cloud.ibm.com/docs/images/tutorials/pec-with-shaded-lightcones/paulitwirl.avif) | ![En savoir plus sur les calques](https://quantum.cloud.ibm.com/docs/images/tutorials/pec-with-shaded-lightcones/learnlayer.avif) | ![Ajustement de courbe](https://quantum.cloud.ibm.com/docs/images/tutorials/pec-with-shaded-lightcones/curvefit.avif) |\n",
        "\n",
        "<span id=\"post-processing-overview\" />\n",
        "\n",
        "### Aperçu du post-traitement\n",
        "\n",
        "Après avoir effectué des calculs sur du matériel quantique à l'aide des `samplomatic` frameworks et `Executor` , nous convertissons nos mesures de chaînes de bits en la valeur observable souhaitée. Dans ce tutoriel, nous présentons le déroulement de l'expérience sur un circuit d'Ising symétrique (construit à l'étape 1 ci-dessous), pour lequel la valeur attendue idéale de l'observable mesurée est égale à 1, car la seconde moitié du circuit annule l'effet de la première. Lors du calcul de la valeur observable à l'aide de la `executor_expectation_values` fonction, nous appliquons quelques techniques de post-traitement qui permettent de réduire l'impact du bruit. Il s'agit notamment de supprimer les images affectées par un bruit non markovien, d'atténuer les erreurs de lecture et de prendre en compte les spécificités de notre implémentation du PEC. Les détails sont abordés à l'étape 4 ci-dessous.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-reqs-0000-4000-8000-000000000003",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Exigences\n",
        "\n",
        "Avant de commencer ce tutoriel, assurez-vous d'avoir installé les éléments suivants :\n",
        "\n",
        "* Qiskit SDK v2.2 ou version ultérieure, avec prise en charge [de la visualisation](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.47 ou version ultérieure (`pip install qiskit-ibm-runtime`)\n",
        "* Modules complémentaires « Shaded lightcones » pour Qiskit v0.1 ou version ultérieure (`pip install qiskit-addon-slc`)\n",
        "* Module complémentaire Qiskit « utils » v0.3 ou version ultérieure (`pip install qiskit-addon-utils`)\n",
        "* Samplomatic v0.13 ou version ultérieure (`pip install samplomatic`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-setup-000-4000-8000-000000000004",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuration\n",
        "\n",
        "Commencez par importer les paquets et les fonctions nécessaires à l'exécution de ce notebook.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "946458b7-ab59-448c-8268-f62d8f53bc6d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "env: OMP_NUM_THREADS=1\n"
          ]
        }
      ],
      "source": [
        "from multiprocessing import set_start_method\n",
        "\n",
        "# Setting this value prevents itertools.starmap deadlock on UNIX systems\n",
        "set_start_method(\"spawn\")\n",
        "\n",
        "# Needed to prevent PySCF from parallelizing internally (SLC only)\n",
        "%set_env OMP_NUM_THREADS=1"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "slc-imports-00-4000-8000-000000000005",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "from matplotlib import pyplot as plt\n",
        "\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import SparsePauliOp\n",
        "from qiskit.transpiler import generate_preset_pass_manager, PassManager\n",
        "\n",
        "from qiskit_ibm_runtime import (\n",
        "    QiskitRuntimeService,\n",
        "    QuantumProgram,\n",
        "    Executor,\n",
        "    NoiseLearnerV3,\n",
        ")\n",
        "\n",
        "import samplomatic\n",
        "from samplomatic.utils import find_unique_box_instructions\n",
        "from samplomatic.transpiler import generate_boxing_pass_manager\n",
        "\n",
        "from qiskit_addon_utils.exp_vals.measurement_bases import (\n",
        "    get_measurement_bases,\n",
        ")\n",
        "from qiskit_addon_utils.exp_vals.expectation_values import (\n",
        "    executor_expectation_values,\n",
        ")\n",
        "from qiskit_addon_utils.noise_management import (\n",
        "    gamma_from_noisy_boxes,\n",
        "    trex_factors,\n",
        ")\n",
        "from qiskit_addon_utils.noise_management.post_selection import PostSelector\n",
        "from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (\n",
        "    AddPostSelectionMeasures,\n",
        "    AddSpectatorMeasures,\n",
        ")\n",
        "\n",
        "from qiskit_addon_slc.bounds import (\n",
        "    compute_backward_bounds,\n",
        "    compute_forward_bounds,\n",
        "    compute_local_scales,\n",
        "    merge_bounds,\n",
        "    tighten_with_speed_limit,\n",
        ")\n",
        "from qiskit_addon_slc.utils import (\n",
        "    generate_noise_model_paulis,\n",
        "    map_modifier_ref_to_ref,\n",
        ")\n",
        "from qiskit_addon_slc.visualization import draw_shaded_lightcone"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-sim-0000-4000-8000-000000000006",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Exemple de simulateur à petite échelle\n",
        "\n",
        "À l’instar d’autres méthodes d’atténuation des erreurs basées sur l’apprentissage, la technique PEC avec cônes de lumière ombrés atténue le bruit *physique* d’un processeur quantique spécifique; elle dépend donc de services matériels qui n’ont pas d’équivalent significatif sur un simulateur idéal :\n",
        "\n",
        "* `NoiseLearnerV3` caractérise expérimentalement le canal de bruit de Pauli-Lindblad clairsemé sur chaque couche unique de deux qubits. Sur un simulateur silencieux, il n'y a pas de bruit à supprimer.\n",
        "* Le `Executor` « primitive » échantillonne les circuits « twirled » et « anti-noise-injected » générés par `samplomatic` sur un backend.\n",
        "\n",
        "Le calcul de la limite du cône de lumière ombragé est classique, mais il n'a de sens que par rapport aux taux de bruit matériels appris, qui déterminent le budget d'atténuation et la surcharge d'échantillonnage. C'est pourquoi nous ne nous attarderons pas sur l'exemple de simulateur à petite échelle et présenterons le flux de travail complet PEC+SLC directement sur le matériel, en détaillant ci-dessous chaque étape du modèle Qiskit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-hw1-0000-4000-8000-000000000007",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Exemple de matériel à grande échelle\n",
        "\n",
        "Nous exécutons le workflow complet PEC+SLC sur un circuit d'Ising en miroir de 20 qubits, sur le matériel d' IBM Quantum®, en suivant les quatre étapes d'un modèle Qiskit.\n",
        "\n",
        "<span id=\"step-1-map-the-problem\" />\n",
        "\n",
        "### Étape 1 : Définir le problème\n",
        "\n",
        "Notre démonstration se présente comme suit :\n",
        "\n",
        "1. Nous avons choisi d'effectuer l'évolution temporelle dans le cadre d'un hamiltonien de chaîne d' 1D -Ising. La chaîne d'Ising « 1D » offre une structure de circuit bien dense, ce qui est pratique pour illustrer les implémentations du PEC.\n",
        "2. Nous avons choisi de mesurer l' $\\langle X_6 Z_{13}\\rangle$ observable, qui comprend une composante $X$ et une composante $Z$. Cela permet de mettre en évidence la manière dont les limites du SLC traitent différemment les différents types d'erreurs.\n",
        "3. Nous souhaitons que la valeur attendue idéale de cette observable soit exactement égale à 1, afin que la qualité des estimations corrigées soit facile à évaluer. En inversant le circuit, on est déjà presque au but : à chaque porte de la seconde moitié du circuit correspond une porte inverse dans la première moitié, de sorte que le circuit idéal agit comme l'identité.\n",
        "4. Enfin, pour obtenir la valeur d'espérance idéale de 1, il faut que le circuit commence — et donc, grâce à la symétrie de réflexion, qu'il se termine idéalement aussi — dans un état propre +1 de l'observable. Étant donné que l'observable contient une composante non- $Z$ e et que la `Executor` primitive définit la base de mesure souhaitée à la fin du circuit, nous ajoutons les portes de préparation de base correspondantes au début du circuit miroir à l'aide d'une `prepare_basis` fonction. Nous utilisons cette `get_measurement_bases` fonction pour déterminer quelles portes sont nécessaires et où les placer, tout en tenant compte des subtilités liées à l'indexation des qubits découlant des `box` conventions d'annotation, comme expliqué dans la section consacrée à la préparation des mesures en base canonique.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "5bfefe99-5958-486e-b8ca-ef2573c7f1e5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Width of the Ising chain (a free parameter of this demonstration)\n",
        "num_qubits = 20\n",
        "\n",
        "# The observable to measure: one X component (on qubit 6) and one Z component\n",
        "# (on qubit 13), as motivated in \"Step 1: Map the problem\" above\n",
        "target_obs_sparse = [(\"XZ\", [6, 13], 1.0)]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "e5d4d6d5-9016-4c9a-916b-5075ff459fbf",
      "metadata": {},
      "outputs": [],
      "source": [
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    target_obs_sparse, num_qubits=num_qubits\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "bb40bad8-cb18-4542-9e18-edf5fdfb5452",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Determine the measurement basis each qubit requires for this observable.\n",
        "# The reverser is used during post-processing in Step 4.\n",
        "bases_virt, reverser_virt = get_measurement_bases(observable)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "5b92fd4e-c559-4c56-8bf6-a6f2e397550c",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Free parameters of the demonstration circuit: the number of Trotter steps\n",
        "# sets the circuit depth, and rx_angle is the transverse-field rotation angle\n",
        "# applied in each step (pi/4 makes the dynamics non-Clifford)\n",
        "num_trotter_steps = 10\n",
        "rx_angle = np.pi / 4"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "cc2b001f-c006-45f1-9942-dcbdabefbec4",
      "metadata": {},
      "outputs": [],
      "source": [
        "def construct_ising_circuit(\n",
        "    num_qubits: int,\n",
        "    num_trotter_steps: int,\n",
        "    rx_angle: float,\n",
        ") -> QuantumCircuit:\n",
        "    circuit = QuantumCircuit(num_qubits)\n",
        "\n",
        "    for _ in range(num_trotter_steps):\n",
        "        circuit.rx(rx_angle, range(num_qubits))\n",
        "        for first_qubit in (1, 2):\n",
        "            for idx in range(first_qubit, num_qubits, 2):\n",
        "                # equivalent to Rzz(-pi/2):\n",
        "                circuit.sdg([idx - 1, idx])\n",
        "                circuit.cz(idx - 1, idx)\n",
        "\n",
        "    return circuit\n",
        "\n",
        "\n",
        "def prepare_basis(\n",
        "    circuit: QuantumCircuit, basis: list[int]\n",
        ") -> QuantumCircuit:\n",
        "    # basis is a list of integer values from 0 to 3. These map to the basis measurement as:\n",
        "    # 0 = I; 1 = Z; 2 = X; 3 = Y\n",
        "    assert len(basis) == circuit.num_qubits\n",
        "\n",
        "    out_circ = circuit.copy_empty_like()\n",
        "    for qb, bas in enumerate(basis):\n",
        "        if bas == 2:\n",
        "            out_circ.h(qb)\n",
        "        elif bas == 3:\n",
        "            out_circ.rx(-np.pi / 2, qb)\n",
        "\n",
        "    out_circ.barrier()\n",
        "    out_circ.compose(circuit, inplace=True)\n",
        "    return out_circ\n",
        "\n",
        "\n",
        "def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:\n",
        "    mirror_circ = circuit.copy_empty_like()\n",
        "    mirror_circ.compose(circuit.inverse(), inplace=True)\n",
        "    mirror_circ.barrier()\n",
        "    mirror_circ.compose(circuit, inplace=True)\n",
        "    mirror_circ.measure_active()\n",
        "    return mirror_circ"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "67553310-f1f0-4736-85ad-bfebd3c00414",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Instantiate the mirrored circuit and prepend the basis-preparation gates\n",
        "circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)\n",
        "mirrored_circuit = mirror_circuit(circuit)\n",
        "mirrored_circuit = prepare_basis(mirrored_circuit, bases_virt[0])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "0dab284e-dbed-4d2f-92aa-c6c9c0e909a6",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/0dab284e-dbed-4d2f-92aa-c6c9c0e909a6-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 9,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "mirrored_circuit.draw(\n",
        "    \"mpl\", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-step2-000-4000-8000-000000000013",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize\" />\n",
        "\n",
        "### Étape 2 : Optimiser\n",
        "\n",
        "Nous optimisons les paramètres relatifs au circuit à faire fonctionner, à la grandeur observable à mesurer et aux paramètres d'apprentissage du bruit. Pour commencer, nous choisissons le processeur Heron le moins sollicité et nous l'instancions en activant les portes fractionnaires. Ces portes fractionnaires permettent d'obtenir une plus grande sensibilité dans certains de nos filtres de post-sélection.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "slc-creds-000-4000-8000-000000000014",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Selected backend: ibm_kingston\n"
          ]
        }
      ],
      "source": [
        "# Initialize IBM Quantum Compute Service using your saved credentials\n",
        "service = QiskitRuntimeService()\n",
        "\n",
        "# Select the least busy Heron device. This tutorial targets Heron processors:\n",
        "# the usage estimate and the non-Markovian-noise post-selection discussed in\n",
        "# Step 4 are specific to this processor family.\n",
        "backend = service.least_busy(\n",
        "    operational=True,\n",
        "    simulator=False,\n",
        "    filters=lambda b: b.processor_type[\"family\"] == \"Heron\",\n",
        ")\n",
        "# Re-fetch with fractional gates enabled (least_busy does not forward this)\n",
        "# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.\n",
        "backend = service.backend(backend.name, use_fractional_gates=True)\n",
        "print(f\"Selected backend: {backend.name}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "81dd2580-d4a8-45bb-bd05-707c64aa4ac7",
      "metadata": {},
      "source": [
        "Tout d'abord, nous allons transcompiler notre circuit en instructions ISA, [comme cela est nécessaire pour son exécution sur nos QPU](/docs/guides/transpile#instruction-set-architecture). Le choix des qubits physiques sur lesquels effectuer les opérations a une grande incidence sur la qualité des résultats, car les temps de cohérence et les fidélités des portes varient d'un dispositif à l'autre et évoluent avec le temps. Pour mener une expérience rigoureuse, il peut s’avérer utile de sélectionner manuellement une chaîne de qubits en s’appuyant sur des expériences de benchmarking rapides (voir, par exemple, le tutoriel «[ Benchmarking en temps réel pour la sélection de qubits](/docs/tutorials/real-time-benchmarking-for-qubit-selection) »). Dans le cadre de ce tutoriel, nous utilisons le transpileur Qiskit pour sélectionner une configuration en fonction de la carte de couplage du backend et de ses dernières données d'étalonnage.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "a7505de6-70ef-4996-9f6b-0025f267e920",
      "metadata": {
        "scrolled": true
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Selected layout: [71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 36, 21, 22, 23, 16, 3, 2]\n"
          ]
        }
      ],
      "source": [
        "isa_pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n",
        "\n",
        "isa_circuit = isa_pm.run(mirrored_circuit)\n",
        "\n",
        "# The chain of physical qubits selected by the transpiler\n",
        "layout = isa_circuit.layout.final_index_layout()\n",
        "print(f\"Selected layout: {layout}\")\n",
        "\n",
        "isa_observable = observable.apply_layout(\n",
        "    layout, num_qubits=isa_circuit.num_qubits\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "87f19e08-0cae-4959-9215-354ab099ab02",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/87f19e08-0cae-4959-9215-354ab099ab02-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 12,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "wire_order = layout + [\n",
        "    q for q in range(isa_circuit.num_qubits) if q not in layout\n",
        "]\n",
        "isa_circuit.draw(\n",
        "    \"mpl\",\n",
        "    fold=-1,\n",
        "    scale=0.3,\n",
        "    idle_wires=False,\n",
        "    wire_order=wire_order,\n",
        "    measure_arrows=False,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7e7135e7-1e86-4d16-b806-30d4a9225e88",
      "metadata": {},
      "source": [
        "<span id=\"box-the-circuit\" />\n",
        "\n",
        "### Encadrez le circuit\n",
        "\n",
        "Pour faciliter la mise en œuvre, nous utilisons la `generate_boxing_pass_manager` phase de transpilation, qui place les instructions du circuit dans des cases annotées. Ces encadrés indiquent clairement où, dans le cas du PEC, il convient d'introduire une contre-perturbation dans le circuit. Pour plus d'informations sur les paramètres, consultez la [documentation](https://qiskit.github.io/samplomatic/) de Samplomatic.\n",
        "\n",
        "Il convient de noter que le flux de travail SLC nécessite l'utilisation de `inject_noise_strategy=\"individual_modification\"` plus tard dans le processus, car cela nous permet d'identifier de manière unique les `InjectNoise` annotations des `BoxOp` instructions du circuit. (Les cases ne comportant aucune `InjectNoise` annotation ne peuvent pas être identifiées de cette manière.)\n",
        "\n",
        "<Admonition type=\"caution\">\n",
        "  Les calculs liés au SLC partent actuellement du principe que les `InjectNoise` annotations sont placées avec `site=\"after\"` (voir [qiskit-addon-slc#33](https://github.com/Qiskit/qiskit-addon-slc/issues/33) ). La valeur par défaut de ce paramètre ayant changé dans les versions récentes de `samplomatic` (voir [samplomatic#372](https://github.com/Qiskit/samplomatic/pull/372) ), nous la définissons `inject_noise_site=\"after\"` explicitement ci-dessous.\n",
        "</Admonition>\n",
        "\n",
        "La `find_unique_box_instructions` fonction parcourt le circuit encapsulé fourni et identifie ceux qui comportent des couches ou des mesures à deux qubits ( 2Q ) uniques, à des fins d'apprentissage du bruit et d'injection de bruit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "440f818c-9417-4855-b31b-db47b8595618",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Box circuit with Twirl and InjectNoise annotations\n",
        "boxes_pm = generate_boxing_pass_manager(\n",
        "    twirling_strategy=\"active\",\n",
        "    inject_noise_strategy=\"individual_modification\",\n",
        "    inject_noise_site=\"after\",\n",
        "    inject_noise_targets=\"gates\",\n",
        "    measure_annotations=\"all\",\n",
        ")\n",
        "\n",
        "\n",
        "boxed_circuit = boxes_pm.run(isa_circuit)\n",
        "\n",
        "# Find the unique instructions (layers) from boxed circuit\n",
        "unique_2q_instructions = find_unique_box_instructions(\n",
        "    boxed_circuit, normalize_annotations=None, undress_boxes=True\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "d662b076-005e-46c7-826d-4c29a4937404",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/d662b076-005e-46c7-826d-4c29a4937404-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 14,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "boxed_circuit.draw(\n",
        "    \"mpl\",\n",
        "    fold=-1,\n",
        "    scale=0.3,\n",
        "    idle_wires=False,\n",
        "    wire_order=wire_order,\n",
        "    measure_arrows=False,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "aa8794ef-7906-4ee3-a9b0-e0399b3e9129",
      "metadata": {},
      "source": [
        "<span id=\"prepare-canonical-bases-measurements\" />\n",
        "\n",
        "### Préparer les mesures des bases canoniques\n",
        "\n",
        "Il convient de noter qu'il faut veiller tout particulièrement à respecter l'ordre des qubits. Nous présentons ci-dessous la notion de `canonical_qubits` afin de mettre à jour correctement l'ordre des qubits lors de leur transmission à Executor, compte tenu de la manière dont cet ordre est pris en compte lors de la conversion des circuits en objets et de la détermination des instructions uniques. Pour plus de détails, consultez la documentation [relative à la convention de numérotation des qubits](https://qiskit.github.io/samplomatic/guides/samplex_io.html#qubit-ordering-convention).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "e242702b-0274-4ebf-837d-4319a6772339",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Determine the canonical qubits order\n",
        "meas_box = boxed_circuit.data[-1]\n",
        "canonical_qubits = [\n",
        "    idx\n",
        "    for idx, qubit in enumerate(boxed_circuit.qubits)\n",
        "    if qubit in meas_box.qubits\n",
        "]\n",
        "\n",
        "# map canonical qubit to physical (isa) qubit\n",
        "c_2_p = {c: p for c, p in enumerate(canonical_qubits)}\n",
        "# map physical (isa) qubit to virtual qubit (index in original circuit)\n",
        "p_2_v = {p: v for v, p in enumerate(layout)}\n",
        "# compute map between virtual and canonical qubit indices.\n",
        "c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}\n",
        "\n",
        "assert len(c_2_v) == num_qubits\n",
        "\n",
        "bases_canon = [\n",
        "    np.array([base_i[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)\n",
        "    for base_i in bases_virt\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "61d2762c",
      "metadata": {},
      "source": [
        "<span id=\"workflow-for-lightcone-shading-noise-learning-and-anti-noise-injection\" />\n",
        "\n",
        "### Flux de travail pour l'ombrage par cône de lumière, l'apprentissage du bruit et l'injection anti-bruit\n",
        "\n",
        "<Admonition type=\"note\">\n",
        "  Dans ce tutoriel, nous effectuons les calculs liés au SLC avant la fin de l'apprentissage du bruit, de sorte que le circuit à optimiser soit exécuté le plus près possible, dans le temps, du modèle de bruit appris. En principe, ce flux de travail peut être encore davantage parallélisé : une tâche d'apprentissage du bruit peut s'exécuter tandis que, en parallèle, les limites du bruit sont estimées. Pour un circuit quantique quelconque, le calcul limité par le bruit peut évoluer selon une dépendance faiblement exponentielle; ainsi, la parallélisation de ce calcul (par exemple, sur plusieurs cœurs de processeur) permet d'obtenir des limites plus strictes pour un temps de calcul donné, et les exécutions sur le QPU ainsi que les calculs limités peuvent eux-mêmes être parallélisés afin d'optimiser l'efficacité du flux de travail.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4a2ec5ed-c339-4273-b407-6bb46b9ede19",
      "metadata": {},
      "source": [
        "<span id=\"predict-to-be-learned-noise-model-paulis\" />\n",
        "\n",
        "#### Prédire les Paulis du modèle de bruit à apprendre\n",
        "\n",
        "Avant de lancer tout apprentissage au bruit sur le matériel, nous déterminons d'abord quels termes d'erreur de Pauli notre modèle de bruit contiendra. Rappelons que nous modélisons le bruit de chaque couche distincte à l'aide d'un modèle de Pauli-Lindblad clairsemé, avec un taux d'erreur pour chaque opérateur de Pauli de poids un ( $X$, $Y$ ou $Z$ ) sur chaque qubit actif, et un autre pour chaque opérateur de Pauli de poids deux sur chaque paire de qubits actifs connectés. La `generate_noise_model_paulis` fonction parcourt chaque couche « boxed » distincte du circuit fourni et génère précisément cette liste de termes de Pauli, en tenant compte de la connectivité des qubits du circuit.\n",
        "\n",
        "En d'autres termes, cette étape permet de prédire la *structure* du modèle de bruit qui `NoiseLearnerV3` sera ensuite complétée par les taux appris. C'est le fait de connaître cette structure à l'avance qui nous permet de calculer une borne avant et une borne arrière pour chaque erreur potentielle avant même que l'apprentissage au bruit ne soit exécuté.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "e6783d37-c91f-4c6e-af4e-fb8f37d79e6b",
      "metadata": {},
      "outputs": [],
      "source": [
        "noise_model_paulis = generate_noise_model_paulis(\n",
        "    unique_2q_instructions, backend.coupling_map, boxed_circuit\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "f9fd1389-8c61-4585-85a6-ad17ab7a2f1c",
      "metadata": {},
      "outputs": [],
      "source": [
        "noise_model_rates = {ref: None for ref in noise_model_paulis}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "942dd468-b2c5-49db-ada5-bcc45024df83",
      "metadata": {},
      "source": [
        "<span id=\"a-compute-forward-bounds\" />\n",
        "\n",
        "##### a. Calculer les limites vers l'avant\n",
        "\n",
        "La `compute_forward_bounds` fonction évalue les relations de commutation entre les portes de chaque couche et les termes de Pauli générés ci-dessus, en tenant compte de la manière dont les erreurs de propagation vers l'avant affectent l'observable souhaitée $A$. Pour les portes qui commutent avec les termes de Pauli, aucune action n'est effectuée. Quant aux portes de Clifford, elles sont placées au début du circuit. Pour les portes non-Clifford, nous estimons leur influence sur les observables cibles qui seront ensuite prioritaires pour la suppression du bruit (une fois que toutes les bornes auront été fusionnées). On obtient cette borne en appliquant d'abord la norme d' L2 (à savoir la racine carrée de la somme des carrés des coefficients des termes de Pauli concernés). Lorsque le nombre de termes de qubits est trop important, nous recourons à une borne moins stricte qui utilise l'inégalité triangulaire.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-bparam-00-4000-8000-000000000030",
      "metadata": {},
      "source": [
        "<span id=\"set-the-bound-computation-parameters\" />\n",
        "\n",
        "#### Définir les paramètres de calcul des limites\n",
        "\n",
        "Les paramètres suivants permettent de contrôler les calculs de limites classiques :\n",
        "\n",
        "* `slc_atol`: la tolérance absolue des calculs. Les termes de l'opérateur dont les coefficients sont inférieurs à ce seuil sont considérés comme négligeables et sont écartés.\n",
        "* `slc_eigval_max_qubits`: le nombre maximal de qubits pour lequel on évalue la borne la plus stricte, basée sur les valeurs propres ( L2-norm ). Les opérateurs pris en charge sur un nombre de qubits supérieur à celui-ci se rabattent sur la borne plus large de l'inégalité triangulaire décrite ci-dessus.\n",
        "* `slc_evolution_max_terms`: le nombre maximal de termes de Pauli conservés lors de la propagation des opérateurs à travers le circuit. Le fait de tronquer les termes les plus petits permet de maintenir le calcul gérable, au prix d'une légère perte de précision des bornes.\n",
        "* `slc_num_processes`: le nombre de processus CPU entre lesquels le calcul lié est parallélisé.\n",
        "* `slc_timeout`: le temps alloué (en secondes) au calcul lié. À toutes les couches non atteintes dans le cadre de ce budget, on attribue la limite triviale de 2.\n",
        "\n",
        "<Admonition type=\"note\">\n",
        "  Dans ce tutoriel, le délai d'expiration est atteint *de manière intentionnelle* : nous le maintenons `slc_timeout` délibérément court afin que le notebook s'exécute rapidement, et pour montrer que le fait d'atteindre ce délai d'expiration ne présente aucun risque. Les nuances calculées constituent des limites supérieures à chaque étape; ainsi, le fait d'atteindre le délai d'attente ne fait qu'élargir ces limites (de manière insignifiante) pour les couches qui n'ont pas été atteintes; cela ne conduit jamais à sous-estimer l'effet d'une erreur. Cela apparaît dans les visualisations ci-dessous sous la forme des couches situées tout à gauche, qui sont entièrement remplies par la borne triviale. L'augmentation du délai d'attente (ou du nombre de processus) resserre les limites et réduit encore davantage la surcharge liée à l'échantillonnage.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "79fe4a21-b80d-496b-ba20-f16d9b11834d",
      "metadata": {},
      "outputs": [],
      "source": [
        "slc_atol = 1e-8\n",
        "slc_eigval_max_qubits = 18\n",
        "slc_evolution_max_terms = 1000\n",
        "slc_num_processes = 8\n",
        "slc_timeout = 60"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "705fcb9f-c753-45a6-97dd-9ff8ecb07018",
      "metadata": {
        "scrolled": true
      },
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "Bounds computation timed out.\n"
          ]
        }
      ],
      "source": [
        "forward_bounds = compute_forward_bounds(\n",
        "    boxed_circuit,\n",
        "    noise_model_paulis,\n",
        "    isa_observable,\n",
        "    evolution_max_terms=slc_evolution_max_terms,\n",
        "    eigval_max_qubits=slc_eigval_max_qubits,\n",
        "    atol=slc_atol,\n",
        "    num_processes=slc_num_processes,\n",
        "    timeout=slc_timeout,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "aaf7c188-c85b-42cf-a096-0a5dc72a68e3",
      "metadata": {},
      "source": [
        "<span id=\"visualize-the-slc-for-manual-inspection\" />\n",
        "\n",
        "#### Visualiser le SLC pour une inspection manuelle\n",
        "\n",
        "Vous pouvez interpréter le comportement des limites ombrées en examinant la manière dont les mesures et les termes de Pauli interagissent avec les erreurs locales. Ces motifs sont caractéristiques de ce problème d'évolution temporelle de l'hamiltonien d'Ising « kicked » et apparaissent également dans l'article [intitulé « Lightcone Shading for Classically Accelerated Quantum Error Mitigation »](https://arxiv.org/abs/2409.04401), avec plusieurs caractéristiques révélatrices :\n",
        "\n",
        "* On peut clairement distinguer les deux cônes issus des deux Paulis non identitaires dans l'observable.\n",
        "* On constate que la mesure X sur le qubit 6 commute avec l'erreur X dans la couche la plus à droite.\n",
        "* On constate que l'erreur Z de Pauli sur le qubit 13 commute avec l'erreur Z de la couche la plus à droite.\n",
        "* Lorsque nous atteignons le délai d'attente indiqué ci-dessus, les couches restantes à gauche sont entièrement remplies de limites triviales égales à deux.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "285edaef-8e79-41eb-a563-4675ea75d5b1",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/285edaef-8e79-41eb-a563-4675ea75d5b1-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/285edaef-8e79-41eb-a563-4675ea75d5b1-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/285edaef-8e79-41eb-a563-4675ea75d5b1-2.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "for p in \"XYZ\":\n",
        "    display(\n",
        "        draw_shaded_lightcone(\n",
        "            boxed_circuit,\n",
        "            forward_bounds,\n",
        "            noise_model_paulis,\n",
        "            pauli_filter=p,\n",
        "            scale=0.15,\n",
        "            fold=-1,\n",
        "            idle_wires=False,\n",
        "            wire_order=wire_order,\n",
        "            measure_arrows=False,\n",
        "        )\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1aee6b29-8077-4019-9e37-3e80292aadf6",
      "metadata": {},
      "source": [
        "<span id=\"b-tighten-forward-bounds-with-quantum-speed-limits\" />\n",
        "\n",
        "#### b. Resserrez les limites supérieures grâce aux limites de vitesse quantiques\n",
        "\n",
        "Nous allons ensuite resserrer ces limites à l'aide de la `tighten_with_speed_limit` fonction. L'étendue géométrique du cône de lumière de l'observable est déjà entièrement prise en compte dans les limites vers l'avant calculées ci-dessus. Cette fonction ajoute une étape de cohérence interne qui tire parti de la vitesse limitée à laquelle l'information peut circuler dans le circuit — le cône de lumière ne s'étend que d'un qubit au maximum par couche de portes à deux qubits — afin de resserrer les limites calculées précédemment les unes par rapport aux autres. L'information circule dans les deux sens :\n",
        "\n",
        "1. Des limites du commutateur aux limites de l'observable rétropropagée : si l'observable commute avec une erreur de « $Z$ » sur un qubit, alors elle ne peut pas comporter de composante « $X$ » ou « $Y$ » significative à cet endroit.\n",
        "2. Des bornes sur l'observable rétropropagée aux bornes du commutateur : si l'observable présente une faible composante d' $X$ et d' $Y$ sur un qubit, alors elle doit presque commuter avec $Z$ à cet endroit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "e060d547-79da-4f54-bf5e-677f70306040",
      "metadata": {
        "scrolled": true
      },
      "outputs": [],
      "source": [
        "forward_bounds_tighter = tighten_with_speed_limit(\n",
        "    forward_bounds, boxed_circuit, noise_model_paulis, isa_observable\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "85f3c9c2-d60c-4044-a496-dae88c3dd749",
      "metadata": {},
      "source": [
        "<span id=\"visualize-the-slc-for-manual-inspection\" />\n",
        "\n",
        "#### Visualiser le SLC pour une inspection manuelle\n",
        "\n",
        "En principe, ce resserrement permet une transition plus fluide entre les limites calculées et les limites par défaut attribuées aux couches au-delà du délai d'expiration. Dans cet exemple, l'effet est à peine perceptible, car les cônes de lumière se sont déjà propagés jusqu'aux bords du circuit au moment où le délai d'attente est atteint.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "6ba4b397-e903-4b85-9482-31a96c689dc0",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/6ba4b397-e903-4b85-9482-31a96c689dc0-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/6ba4b397-e903-4b85-9482-31a96c689dc0-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/6ba4b397-e903-4b85-9482-31a96c689dc0-2.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "for p in \"XYZ\":\n",
        "    display(\n",
        "        draw_shaded_lightcone(\n",
        "            boxed_circuit,\n",
        "            forward_bounds_tighter,\n",
        "            noise_model_paulis,\n",
        "            pauli_filter=p,\n",
        "            scale=0.15,\n",
        "            fold=-1,\n",
        "            idle_wires=False,\n",
        "            wire_order=wire_order,\n",
        "            measure_arrows=False,\n",
        "        )\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ce0f9cf4-565a-4f18-baa5-bec5c7b354a2",
      "metadata": {},
      "source": [
        "<span id=\"c-compute-backward-bounds\" />\n",
        "\n",
        "#### c. Calculer les limites arrière\n",
        "\n",
        "Cette partie de la prédiction du bruit évalue dans quelle mesure une erreur sur une couche donnée peut affecter l' $\\rho$ de l'état d'entrée. La `compute_backward_bounds` fonction commence par inverser le circuit et supprimer les portes de mesure, puis procède à une analyse similaire à celle effectuée pour les calculs en sens direct.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "id": "ff895191-feda-4958-a58a-3d1fb973cb68",
      "metadata": {
        "scrolled": true
      },
      "outputs": [],
      "source": [
        "backward_bounds = compute_backward_bounds(\n",
        "    boxed_circuit,\n",
        "    noise_model_paulis,\n",
        "    evolution_max_terms=slc_evolution_max_terms,\n",
        "    num_processes=slc_num_processes,\n",
        "    timeout=slc_timeout,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c707d352-37fe-4c53-82a3-a1207b7a7bfc",
      "metadata": {},
      "source": [
        "<span id=\"visualize-the-slc-for-manual-inspection\" />\n",
        "\n",
        "#### Visualiser le SLC pour une inspection manuelle\n",
        "\n",
        "En calculant les limites arrière, on constate que la structure de l'état initial détermine le comportement initial de la propagation de l'erreur :\n",
        "\n",
        "* On voit clairement comment les erreurs Z commutent initialement avec l'état initial |0⟩.\n",
        "* Ce n'est que sur le qubit 6, où nous initialisons l'état propre +1 de la base X, qu'une erreur Z ne commute pas, alors qu'une erreur X commute.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "id": "40d0eba9-5e0b-4b1d-8fb2-c239e262062f",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/40d0eba9-5e0b-4b1d-8fb2-c239e262062f-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/40d0eba9-5e0b-4b1d-8fb2-c239e262062f-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/40d0eba9-5e0b-4b1d-8fb2-c239e262062f-2.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "for p in \"XYZ\":\n",
        "    display(\n",
        "        draw_shaded_lightcone(\n",
        "            boxed_circuit,\n",
        "            backward_bounds,\n",
        "            noise_model_paulis,\n",
        "            pauli_filter=p,\n",
        "            scale=0.15,\n",
        "            fold=-1,\n",
        "            idle_wires=False,\n",
        "            wire_order=wire_order,\n",
        "            measure_arrows=False,\n",
        "        )\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c8960e51-733e-4458-b77c-287ba3abdb8d",
      "metadata": {},
      "source": [
        "<span id=\"preview-merged-bounds-without-learned-noise-rates\" />\n",
        "\n",
        "#### Aperçu des limites fusionnées sans taux de bruit appris\n",
        "\n",
        "Cette `merged_bounds` fonction détermine le point du circuit où le passage des limites arrière aux limites avant minimise le biais total estimé sur l'observable souhaitée. Ce biais est calculé comme la somme des contributions « vers l'arrière » pour tous les emplacements de bruit situés avant ce point, à laquelle s'ajoutent les contributions « vers l'avant » pour tous les emplacements de bruit situés après celui-ci. Actuellement, cette opération est effectuée de manière uniforme pour tous les qubits.\n",
        "\n",
        "<Admonition type=\"note\" title=\"Remarque importante\">\n",
        "  Le moment optimal pour passer des limites arrière aux limites avant dépend des taux de bruit appris. À ce stade, nous n'avons encore déterminé aucun taux : `noise_model_rates` a été initialisé à `None` pour chaque couche, ce qui `merge_bounds` correspond à des taux de bruit uniformes. Les limites fusionnées présentées ici ne constituent donc qu'un aperçu; nous les recalculons à l'aide des taux réels appris à l'étape 3.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "id": "b5cd8752-3bb5-44ee-97a8-6edaf5946e9d",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "Missing noise rates. Partitioning backward/forward commutator bounds by assuming uniform error rates.\n",
            "Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.\n"
          ]
        }
      ],
      "source": [
        "merged_bounds = merge_bounds(\n",
        "    boxed_circuit,\n",
        "    forward_bounds_tighter,\n",
        "    backward_bounds,\n",
        "    noise_model_rates,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ace27801-b949-44e0-801d-e74ccb2f877a",
      "metadata": {},
      "source": [
        "<span id=\"visualize-the-slc-for-manual-inspection\" />\n",
        "\n",
        "### Visualiser le SLC pour une inspection manuelle\n",
        "\n",
        "Une fois les limites arrière et avant resserrées fusionnées, le comportement des SLC combinés apparaît clairement :\n",
        "\n",
        "* La fonction ci-dessus indique qu'une partition est choisie à partir de laquelle on passe de limites arrière à des limites avant plus strictes.\n",
        "* On constate ci-dessous que les SLC contiennent désormais des bornes arrière partielles et des bornes avant partiellement resserrées.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "id": "eb2948bc-fa27-4108-b03c-0291d3b2f7c8",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/eb2948bc-fa27-4108-b03c-0291d3b2f7c8-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/eb2948bc-fa27-4108-b03c-0291d3b2f7c8-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/eb2948bc-fa27-4108-b03c-0291d3b2f7c8-2.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "for p in \"XYZ\":\n",
        "    display(\n",
        "        draw_shaded_lightcone(\n",
        "            boxed_circuit,\n",
        "            merged_bounds,\n",
        "            noise_model_paulis,\n",
        "            pauli_filter=p,\n",
        "            scale=0.15,\n",
        "            fold=-1,\n",
        "            idle_wires=False,\n",
        "            wire_order=wire_order,\n",
        "            measure_arrows=False,\n",
        "        )\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-step3-000-4000-8000-000000000050",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute\" />\n",
        "\n",
        "### Étape 3 : Exécuter\n",
        "\n",
        "Dans cette section, nous entamons la partie du flux de travail qui fait appel à un dispositif quantique réel. Cette méthode d'atténuation des erreurs basée sur l'apprentissage comporte deux étapes :\n",
        "\n",
        "1. Apprenez à reconnaître ce bruit en utilisant `NoiseLearnerV3`.\n",
        "2. Mettre en œuvre un circuit d'atténuation des erreurs à l'aide des `samplomatic` frameworks et `Executor` .\n",
        "\n",
        "Grâce aux erreurs bornées générées par notre circuit quantique, nous déterminons les taux de bruit associés afin de hiérarchiser notre budget d'erreurs, de calculer la surcharge liée à l'échantillonnage et d'exécuter le programme sur un QPU.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-nlmd-0000-4000-8000-000000000051",
      "metadata": {},
      "source": [
        "<span id=\"a-learn-the-noise-rates\" />\n",
        "\n",
        "### a. Découvrez les indices de bruit\n",
        "\n",
        "L'algorithme d'apprentissage du bruit caractérise les processus de bruit affectant les portes d'un ou plusieurs circuits d'intérêt, en s'appuyant sur le [modèle de bruit de Pauli-Lindblad](https://arxiv.org/abs/2201.09866) clairsemé. Cette `run()` méthode lance une tâche d'apprentissage du bruit pour les couches à deux qubits uniques fournies, en utilisant les options spécifiées dans la configuration du « noise-learner ». Ces options permettent de contrôler la stratégie de « Pauli-twirling », le nombre de randomisations et de tentatives, les profondeurs d'apprentissage et la post-sélection.\n",
        "\n",
        "Nous choisissons également délibérément le niveau de profondeur de l'apprentissage. Une observation pratique concernant l'atténuation basée sur l'apprentissage `samplomatic` montre qu'il est très avantageux que la profondeur d'apprentissage maximale corresponde à la profondeur du circuit que l'on souhaite atténuer. Étant donné que les NLv3`layer_pair_depths` s sont mesurées par *paires* de couches (une couche et son inverse), nous fixons la valeur la plus profonde à la moitié de la profondeur de la couche à deux qubits du circuit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "slc-psflag-00-4000-8000-000000000052",
      "metadata": {},
      "outputs": [],
      "source": [
        "post_selection_enabled = True"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 28,
      "id": "slc-nlopt-00-4000-8000-000000000053",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Match the deepest noise-learning depth to the depth of the circuit being\n",
        "# mitigated. NLv3 ``layer_pair_depths`` are measured in layer pairs (a layer\n",
        "# plus its inverse), so the deepest value is half the circuit's two-qubit-layer\n",
        "# depth. Learning to this depth markedly improves the quality of the mitigation.\n",
        "#\n",
        "# We measure the two-qubit-layer depth on the pre-boxed ISA circuit: after\n",
        "# boxing, every two-qubit gate is hidden inside a full-width ``BoxOp``, so a\n",
        "# ``num_qubits == 2`` filter on ``boxed_circuit`` matches nothing (and\n",
        "# ``QuantumCircuit.depth`` does not recurse into boxes).\n",
        "depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)\n",
        "max_layer_pair_depth = depth_2q // 2  # dividing by 2 since we want pairs\n",
        "\n",
        "# Use a fixed schedule of learning depths, but drop any that exceed the circuit's\n",
        "# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never\n",
        "# learn deeper than the circuit being mitigated.\n",
        "candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]\n",
        "layer_pair_depths = sorted(\n",
        "    {d for d in candidate_depths if d < max_layer_pair_depth}\n",
        "    | {max_layer_pair_depth}\n",
        ")\n",
        "\n",
        "noise_learner_options = {\n",
        "    \"num_randomizations\": 64,\n",
        "    \"shots_per_randomization\": 128,\n",
        "    \"layer_pair_depths\": layer_pair_depths,\n",
        "    \"post_selection\": {\n",
        "        \"enable\": post_selection_enabled,\n",
        "        \"strategy\": \"edge\",\n",
        "        \"x_pulse_type\": \"rx\",\n",
        "    },\n",
        "    \"environment\": {\"job_tags\": [\"TUT_SLC\"]},\n",
        "}\n",
        "\n",
        "noise_learner = NoiseLearnerV3(backend, noise_learner_options)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 29,
      "id": "slc-nlrun-00-4000-8000-000000000054",
      "metadata": {},
      "outputs": [],
      "source": [
        "noise_learner_job = noise_learner.run(unique_2q_instructions)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "id": "db624045-7abe-41f1-a60c-1ba141aaa6f6",
      "metadata": {},
      "outputs": [],
      "source": [
        "noise_learner_result = noise_learner_job.result()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 31,
      "id": "b08c37a6-5acb-41e9-a76c-abc0195d87f7",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Minimum fraction of shots kept for noise learning experiments: 0.71\n"
          ]
        }
      ],
      "source": [
        "if post_selection_enabled:\n",
        "    print(\n",
        "        \"Minimum fraction of shots kept for noise learning experiments: \",\n",
        "        end=\"\",\n",
        "    )\n",
        "    print(\n",
        "        f\"{min([min(d.values()) for d in [nlr.metadata['post_selection']['fraction_kept'] for nlr in noise_learner_result[:2]]]):.2f}\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 32,
      "id": "99144d7f-b891-4115-8cb3-ecdbedd10a73",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Get a dict mapping each InjectNoise.ref to its learned PauliLindbladMap\n",
        "refs_2_plm = noise_learner_result.to_dict(\n",
        "    unique_2q_instructions, require_refs=False\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e6a790a3-bc11-46e2-8cb4-2e43257926c8",
      "metadata": {},
      "source": [
        "<span id=\"bi-update-merged-bounds-with-actual-learned-noise-rates\" />\n",
        "\n",
        "### b.i. Mettre à jour les limites fusionnées en fonction des taux de bruit réels appris\n",
        "\n",
        "Maintenant que le modèle de bruit spécifique a été appris, nous pouvons appliquer les taux de bruit appris aux limites de bruit prédites et déterminer ainsi définitivement quelles limites ont le plus d'impact sur la minimisation du biais.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 33,
      "id": "bba8fca6-5339-4d4d-be01-45ca8bf94a5a",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.\n"
          ]
        }
      ],
      "source": [
        "merged_bounds = merge_bounds(\n",
        "    boxed_circuit,\n",
        "    forward_bounds_tighter,\n",
        "    backward_bounds,\n",
        "    refs_2_plm,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "649b9640-e89f-4021-9f7d-f1e136c4eb6d",
      "metadata": {},
      "source": [
        "<span id=\"bii-compute-the-local_scales-for-the-hardware-execution\" />\n",
        "\n",
        "#### b.ii. Calculer le `local_scales` pour l'exécution matérielle\n",
        "\n",
        "`compute_local_scales` examine chaque erreur de bruit possible dans le circuit et évalue dans quelle mesure cette erreur pourrait fausser la mesure finale, ainsi que le coût que représenterait sa correction. Il classe ensuite les erreurs en fonction de l'intérêt qu'il y a à les corriger et sélectionne le sous-ensemble qui réduit au maximum le biais, tout en restant dans les limites du budget alloué au coût d'échantillonnage (ou en atteignant la précision souhaitée). On obtient ainsi un ensemble de facteurs d'échelle indiquant quelles erreurs seront activement corrigées et lesquelles ne le seront pas (`local_scales`), ainsi que la surcharge totale prévue liée au coût d'échantillonnage (`sampling_costs`) et le biais résiduel (`residual_bias_bound`).\n",
        "\n",
        "La possibilité de contrôler le biais résiduel souhaité est une caractéristique essentielle de la mise en œuvre du PEC dans le cadre du SLC. Alors que, dans la [mise en œuvre initiale](https://arxiv.org/abs/2201.09866), la surcharge d'échantillonnage visait toujours un biais nul, nous pouvons désormais ajuster la surcharge d'échantillonnage requise en acceptant un compromis sur le biais résiduel attendu. Cela permet à l'utilisateur de respecter un budget d'échantillonnage fixe, ce qui peut s'avérer particulièrement utile lors de la création d'un premier prototype de flux de travail.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 34,
      "id": "8914e00e-ba21-41dc-9e64-b942bb23fde1",
      "metadata": {},
      "outputs": [],
      "source": [
        "id_map = map_modifier_ref_to_ref(boxed_circuit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 35,
      "id": "4c971d31-f2b4-4d17-8a83-4f6d14f81f6f",
      "metadata": {
        "scrolled": true
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Full PEC gamma=12.750876317157042, sampling cost (gamma^2) = 162.58484685543633\n"
          ]
        }
      ],
      "source": [
        "summed_rates = 0.0\n",
        "for box_id, noise_id in id_map.items():\n",
        "    learned_plm = refs_2_plm[noise_id]\n",
        "    summed_rates += np.sum(learned_plm.rates)\n",
        "    # print(f\"{box_id}:\\tgamma = {np.exp(2 * summed_rates):1.6e}\\tsampling cost = {np.exp(4 * summed_rates):1.6e}\")\n",
        "total_gamma = np.exp(2 * summed_rates)\n",
        "print(\n",
        "    f\"Full PEC gamma={total_gamma}, sampling cost (gamma^2) = {total_gamma**2}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 36,
      "id": "fb6f2aa0-8a35-420c-aae3-6fc5e9aa8747",
      "metadata": {},
      "outputs": [],
      "source": [
        "biases = []\n",
        "costs = []\n",
        "for bias in [0.0] + np.arange(0.001, 0.102, 0.01).tolist():\n",
        "    _, cost_, bias_ = compute_local_scales(\n",
        "        boxed_circuit,\n",
        "        merged_bounds,\n",
        "        refs_2_plm,\n",
        "        sampling_cost_budget=np.inf,\n",
        "        bias_tolerance=bias,\n",
        "    )\n",
        "    biases.append(bias_)\n",
        "    costs.append(cost_)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-trade-00-4000-8000-000000000065",
      "metadata": {},
      "source": [
        "<span id=\"trade-off-sampling-overhead-against-residual-bias\" />\n",
        "\n",
        "#### Trouver un compromis entre la charge liée à l'échantillonnage et le biais résiduel\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 37,
      "id": "slc-ovplot-0-4000-8000-000000000066",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')"
            ]
          },
          "execution_count": 37,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/slc-ovplot-0-4000-8000-000000000066-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "xticks = np.arange(0, 11)\n",
        "\n",
        "fig, ax = plt.subplots()\n",
        "ax.scatter(\n",
        "    [0], [total_gamma**2], marker=\"D\", c=\"tab:orange\", label=\"full PEC\"\n",
        ")\n",
        "ax.plot(\n",
        "    100 * np.array(biases),\n",
        "    np.array(costs),\n",
        "    \"o-\",\n",
        "    c=\"tab:blue\",\n",
        "    label=\"PEC+SLC\",\n",
        ")\n",
        "ax.set_yscale(\"log\")\n",
        "ax.set_xticks(xticks, [f\"{x:.1f}\" for x in xticks])\n",
        "\n",
        "ax.set_xlabel(\"Remaining bias [%]\")\n",
        "ax.set_ylabel(r\"Sampling overhead, $\\gamma^2$\")\n",
        "ax.grid()\n",
        "ax.legend()\n",
        "fig.suptitle(\"PEC sampling overhead reduction due to SLC\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 38,
      "id": "1df482f9-c12c-4ea7-a71e-7ffc0837b5a6",
      "metadata": {},
      "outputs": [],
      "source": [
        "chosen_bias_thres = 0.1"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 39,
      "id": "slc-lscale-0-4000-8000-000000000068",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "PEC+SLC sampling cost (gamma^2) = 48.13646509622307 w/ remaining bias = 10.0%\n"
          ]
        }
      ],
      "source": [
        "local_scales, sampling_cost, residual_bias_bound = compute_local_scales(\n",
        "    boxed_circuit,\n",
        "    merged_bounds,\n",
        "    refs_2_plm,\n",
        "    sampling_cost_budget=np.inf,\n",
        "    bias_tolerance=chosen_bias_thres,\n",
        ")\n",
        "print(\n",
        "    f\"PEC+SLC sampling cost (gamma^2) = {sampling_cost} \"\n",
        "    f\"w/ remaining bias = {100 * residual_bias_bound:.1f}%\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8b03c0ed-d026-4b29-bc09-68c4d0c94889",
      "metadata": {},
      "source": [
        "<span id=\"c-execute-the-circuit-of-interest-with-antinoise\" />\n",
        "\n",
        "### c. Exécutez le circuit qui vous intéresse avec antinoise\n",
        "\n",
        "<span id=\"ci-prepare-template-circuit-by-using-samplex\" />\n",
        "\n",
        "#### c.i. Préparez le circuit modèle à l'aide de `samplex`\n",
        "\n",
        "Le `samplex` est le résultat de la `build` méthode de Samplomatic, qui encode toutes les informations nécessaires à la génération de paramètres aléatoires pour `template_circuit`. Celles-ci servent ensuite à configurer les `QuantumProgram` objets, qui sont à leur tour exécutés sur un QPU avec la `Executor` primitive. `samplex`Chacune d'entre elles `QuantumProgram` peut contenir plusieurs éléments, que l'on peut considérer comme des paires (a, `template` b).\n",
        "\n",
        "Pour plus de détails, consultez le tutoriel [« Hello » de Samplomatic](https://github.com/qiskit-community/qdc-challenges-2025/blob/main/day3_tutorials/Track_A/hello_samplomatic/Samplomatic%20-%20Hello%20World.ipynb).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 40,
      "id": "a68109cc-3b38-40e0-b145-edaa06b9934d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Build template circuit and samplex for later use with the \"Executor\"\n",
        "template_circuit, samplex = samplomatic.build(boxed_circuit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 41,
      "id": "0de4922b-b958-48d7-b63a-819edfb354bd",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Set up postselection if it's been enabled\n",
        "if post_selection_enabled:\n",
        "    # Set up post selection PM (to add PS instructions)\n",
        "    post_selection_pm = PassManager(\n",
        "        [\n",
        "            AddSpectatorMeasures(backend.coupling_map),\n",
        "            AddPostSelectionMeasures(x_pulse_type=\"rx\"),\n",
        "        ]\n",
        "    )\n",
        "    final_template_circuit = post_selection_pm.run(template_circuit)\n",
        "else:\n",
        "    final_template_circuit = template_circuit"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f00c61a-4c4d-40d9-9a41-11ea8e9a9337",
      "metadata": {},
      "source": [
        "<span id=\"cii-set-up-the-quantumprogram\" />\n",
        "\n",
        "#### c.ii. Configurez le `QuantumProgram`\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 42,
      "id": "a63d8881-2bbb-4c99-afd3-43f03ee04e5c",
      "metadata": {},
      "outputs": [],
      "source": [
        "num_randomizations = 4096\n",
        "shots_per_randomization = 64\n",
        "chunk_size = 256"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 43,
      "id": "038a04c9-20ba-432c-bfec-cf69e67ac82f",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Set up QuantumProgram\n",
        "program = QuantumProgram(shots=shots_per_randomization, noise_maps=refs_2_plm)\n",
        "\n",
        "# no EM\n",
        "\n",
        "# Collect up a dict of the other arguments that need to be bound to samplex_inputs\n",
        "samplex_inputs = {\n",
        "    f\"noise_scales.{ref}\": float(0) for ref in local_scales.keys()\n",
        "}\n",
        "samplex_inputs |= {\"basis_changes\": {\"basis0\": bases_canon[0]}}\n",
        "\n",
        "# Convert samplex_inputs into a dict to pass to QuantumProgram\n",
        "samplex_arguments = (\n",
        "    samplex.inputs().bind(**samplex_inputs).make_broadcastable()\n",
        ")\n",
        "\n",
        "program.append_samplex_item(\n",
        "    circuit=final_template_circuit,\n",
        "    samplex=samplex,\n",
        "    samplex_arguments=samplex_arguments,\n",
        "    shape=(num_randomizations,),\n",
        "    chunk_size=chunk_size,\n",
        ")\n",
        "\n",
        "# plain PEC\n",
        "\n",
        "# Collect a dict of the other arguments that need to be bound to samplex_inputs\n",
        "samplex_inputs = {\n",
        "    f\"noise_scales.{ref}\": float(-1) for ref in local_scales.keys()\n",
        "}\n",
        "samplex_inputs |= {\"basis_changes\": {\"basis0\": bases_canon[0]}}\n",
        "\n",
        "# Convert samplex_inputs into a dict to pass to QuantumProgram\n",
        "samplex_arguments = (\n",
        "    samplex.inputs().bind(**samplex_inputs).make_broadcastable()\n",
        ")\n",
        "\n",
        "program.append_samplex_item(\n",
        "    circuit=final_template_circuit,\n",
        "    samplex=samplex,\n",
        "    samplex_arguments=samplex_arguments,\n",
        "    shape=(num_randomizations,),\n",
        "    chunk_size=chunk_size,\n",
        ")\n",
        "\n",
        "# PEC+SLC\n",
        "\n",
        "# Collect a dict of the other arguments that need to be bound to samplex_inputs\n",
        "samplex_inputs = {\n",
        "    f\"noise_scales.{ref}\": float(-1) for ref in local_scales.keys()\n",
        "}\n",
        "samplex_inputs |= {\"basis_changes\": {\"basis0\": bases_canon[0]}}\n",
        "samplex_inputs |= {\"local_scales\": local_scales}\n",
        "\n",
        "# Convert samplex_inputs into a dict to pass to QuantumProgram\n",
        "samplex_arguments = (\n",
        "    samplex.inputs().bind(**samplex_inputs).make_broadcastable()\n",
        ")\n",
        "\n",
        "program.append_samplex_item(\n",
        "    circuit=final_template_circuit,\n",
        "    samplex=samplex,\n",
        "    samplex_arguments=samplex_arguments,\n",
        "    shape=(num_randomizations,),\n",
        "    chunk_size=chunk_size,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3100fff6-9fe9-49ad-9662-549cea099e9b",
      "metadata": {},
      "source": [
        "<span id=\"ciii-execute-program-with-the-executor-primitive\" />\n",
        "\n",
        "#### c.iii. Exécuter le programme à l'aide de la `Executor` primitive\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 44,
      "id": "2a1c0fe1-8761-462b-a1d9-b951d5224164",
      "metadata": {},
      "outputs": [],
      "source": [
        "executor = Executor(backend)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 45,
      "id": "slc-exec-000-4000-8000-000000000078",
      "metadata": {},
      "outputs": [],
      "source": [
        "job_exec = executor.run(program)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 46,
      "id": "70dd5d10-9cc5-4341-9642-18ba3a0ee58b",
      "metadata": {},
      "outputs": [],
      "source": [
        "results_exec = job_exec.result()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-step4-000-4000-8000-000000000080",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process\" />\n",
        "\n",
        "### Étape 4 : Post-traitement\n",
        "\n",
        "Lorsque nous calculons la valeur attendue finale qui nous intéresse à l'aide de `executor_expectation_values`, nous mettons en œuvre quelques techniques de post-traitement afin de garantir l'obtention de résultats de la meilleure qualité possible. Tout d'abord, nous appliquons notre [méthode d'extinction des erreurs de lecture par rotation (TREX)](/docs/guides/error-mitigation-and-suppression-techniques#twirled-readout-error-extinction-trex), qui tient compte de toutes les erreurs survenant au cours du processus de lecture. Nous corrigeons ensuite les erreurs dues au bruit non markovien sur nos backends Heron à l'aide d'une méthode de post-sélection. Cette méthode consiste à mesurer les qubits actifs et les qubits spectateurs, puis à appliquer une rotation lente à chaque qubit, avant de procéder à une nouvelle mesure. Lorsque les deux mesures ne confirment pas, comme prévu, l'inversion d'un qubit, ces essais sont écartés en appliquant un `mask` issu du `PostSelector`. Dans le cadre du calcul du masque, il est possible de définir une stratégie spécifique pour filtrer en fonction des nœuds à un seul qubit ou des arêtes spectatrices voisines, ce qui peut influencer à la fois le nombre de tirs filtrés et la qualité des résultats.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 47,
      "id": "147bcd34-f79e-4abb-821d-5ea9d8388c9f",
      "metadata": {},
      "outputs": [],
      "source": [
        "measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()\n",
        "trex_scale_factors = trex_factors(measurement_noise_map, reverser_virt)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 48,
      "id": "981353e7-95a9-48bb-92c8-87b931e2b295",
      "metadata": {},
      "outputs": [],
      "source": [
        "post_selection_strategy = \"node\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 49,
      "id": "826afe02-3308-4d20-a9e1-ac0b2ba25eca",
      "metadata": {},
      "outputs": [],
      "source": [
        "def post_process_conv(datum, steps=16, gamma=None, ps=False, trex=False):\n",
        "    meas = datum[\"meas\"]\n",
        "    flips = datum[\"measurement_flips.meas\"]\n",
        "    signs = datum.get(\"pauli_signs\", None)\n",
        "\n",
        "    meas_basis_axis = None\n",
        "    avg_axis = 0\n",
        "\n",
        "    mask = None\n",
        "    if ps and post_selection_enabled:\n",
        "        # Post-select the results\n",
        "        post_selector = PostSelector.from_circuit(\n",
        "            circuit=final_template_circuit, coupling_map=backend.coupling_map\n",
        "        )\n",
        "\n",
        "        # Compute the ps mask for filtering results\n",
        "        mask = post_selector.compute_mask(\n",
        "            datum, strategy=post_selection_strategy\n",
        "        )\n",
        "\n",
        "        # Compute fraction of shots kept from post selection\n",
        "        total_num_shots = num_randomizations * shots_per_randomization\n",
        "        ps_ratio = np.sum(mask) * 100 / total_num_shots / len(bases_canon)\n",
        "        print(\n",
        "            f\"With {post_selection_strategy}-based post selection ({ps_ratio:.1f}% of shots kept):\"\n",
        "        )\n",
        "\n",
        "    results = []\n",
        "    for i in range(steps, num_randomizations + 1, steps):\n",
        "        # Compute mitigated expvals w/out post-selection\n",
        "        res = executor_expectation_values(\n",
        "            meas[:i],\n",
        "            reverser_virt,\n",
        "            meas_basis_axis,\n",
        "            avg_axis=avg_axis,\n",
        "            measurement_flips=flips[:i],\n",
        "            pauli_signs=signs[:i] if signs is not None else None,\n",
        "            postselect_mask=mask[:i] if mask is not None else None,\n",
        "            rescale_factors=trex_scale_factors if trex else None,\n",
        "            gamma_factor=gamma,\n",
        "        )\n",
        "        results.append(res[0])\n",
        "    return results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 50,
      "id": "57b68d19-cda9-4688-b07e-0c7f9da5f252",
      "metadata": {},
      "outputs": [],
      "source": [
        "gamma_pec = gamma_from_noisy_boxes(refs_2_plm, id_map)\n",
        "gamma_slc = gamma_from_noisy_boxes(refs_2_plm, id_map, local_scales)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 51,
      "id": "b5244ee6-98b2-43da-8238-3d6fb0f4cdd0",
      "metadata": {},
      "outputs": [],
      "source": [
        "steps = 16"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 52,
      "id": "95bedd10-5ce9-4145-9b20-1459aa8e7db9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "With node-based post selection (24.1% of shots kept):\n",
            "With node-based post selection (24.2% of shots kept):\n"
          ]
        }
      ],
      "source": [
        "results = {}\n",
        "\n",
        "for label, result_idx, gamma, use_ps, use_trex in [\n",
        "    (\"PEC\", 1, gamma_pec, True, True),\n",
        "    (\"PEC+SLC\", 2, gamma_slc, True, True),\n",
        "    (\"Unmitigated\", 0, None, False, False),\n",
        "]:\n",
        "    res = post_process_conv(\n",
        "        results_exec[result_idx],\n",
        "        steps=steps,\n",
        "        gamma=gamma,\n",
        "        ps=use_ps,\n",
        "        trex=use_trex,\n",
        "    )\n",
        "    results[label] = res"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1d778145-9ead-4b9f-82a3-cec8aa8c8307",
      "metadata": {},
      "source": [
        "L'analyse des résultats expérimentaux nous permet de comparer directement le comportement des différentes approches : PEC, PEC associée à SLC, et le scénario de référence sans mesures d'atténuation. Quelques détails spécifiques à souligner :\n",
        "\n",
        "* Le résultat brut se situe bien en dehors de la fourchette de biais de 10 % (voir 0.35 ) et n'est pas influencé par le nombre de randomisations.\n",
        "* Sur cet appareil, la méthode PEC complète entraîne une surcharge d'échantillonnage de $\\gamma^2 \\approx 1.6\\times10^2$. Après d'importantes oscillations initiales, l'estimation PEC simple récupère le signal et se stabilise autour de 1.1, dépassant légèrement la valeur exacte. Il convient de noter que la marge de biais de 10 % ne s'applique qu'à l'estimateur PEC+SLC : le PEC seul est en principe non biaisé; cet écart résiduel n'est donc pas couvert par une garantie de biais, mais reflète plutôt des fluctuations statistiques et des imperfections du modèle de bruit appris, ce qui est raisonnable compte tenu de l'ampleur du problème.\n",
        "* La méthode SLC réduit encore davantage la surcharge d' \\~3.4-fold (à $\\gamma^2 \\approx 48$, pour une limite de biais résiduel d'environ 10 %). La valeur de PEC+SLC fluctue également fortement après quelques randomisations, atteignant même initialement une région non physique, mais son estimation converge progressivement vers la valeur exacte, pour se stabiliser près de 1.0, bien à l'intérieur de la bande. Elle atteint, voire dépasse, la précision de la méthode PEC classique pour un coût d'échantillonnage représentant environ un tiers de celui-ci, ce qui démontre l'intérêt de l'ombrage par cône de lumière.\n",
        "* Les barres d'erreur des deux estimations corrigées se rétrécissent à mesure que le nombre de randomisations augmente; pour un nombre fixe de randomisations, l'estimateur PEC+SLC présente la variance statistique la plus faible, ce qui correspond à son « $\\gamma^2$ » plus faible.\n",
        "\n",
        "Il convient de noter que les taux de bruit appris (et donc les surcoûts et les estimations corrigées) dépendent du backend et de son étalonnage au moment de l'exécution; ainsi, une nouvelle exécution de ce notebook peut donner lieu à des valeurs quantitativement différentes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 53,
      "id": "8df6612b-d611-4201-b1f4-6d8de4a192ad",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "(-50.0, 4100.0)"
            ]
          },
          "execution_count": 53,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/pec-with-shaded-lightcones/extracted-outputs/8df6612b-d611-4201-b1f4-6d8de4a192ad-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig, ax = plt.subplots(1, 1, figsize=(12, 6))\n",
        "\n",
        "ax.axhline(1.0, color=\"black\", label=\"Exact\")\n",
        "ax.fill_between(\n",
        "    [-50, 4100], -10, 0, color=\"grey\", alpha=0.25, label=\"Unphysical\"\n",
        ")\n",
        "ax.fill_between([-50, 4100], 1, 10, color=\"grey\", alpha=0.25)\n",
        "ax.fill_between(\n",
        "    [-50, 4100], 0.9, 1.1, color=\"red\", alpha=0.25, label=\"10% bias\"\n",
        ")\n",
        "\n",
        "for label, res in results.items():\n",
        "    ax.errorbar(\n",
        "        list(range(steps, num_randomizations + 1, steps)),\n",
        "        [r[0] for r in res],\n",
        "        yerr=[r[1] for r in res],\n",
        "        alpha=0.75,\n",
        "        marker=\"o\",\n",
        "        linestyle=\"\",\n",
        "        markerfacecolor=\"none\",\n",
        "        label=label,\n",
        "    )\n",
        "\n",
        "ax.set_ylabel(r\"$\\langle X_{6}Z_{13}\\rangle$\")\n",
        "ax.set_xlabel(\"# randomizations\")\n",
        "ax.grid()\n",
        "\n",
        "ax.legend(ncols=2)\n",
        "ax.set_ylim([-0.1, 2.0])\n",
        "ax.set_xlim([-50, 4100])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "slc-next-000-4000-8000-000000000099",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Etapes suivantes\n",
        "\n",
        "Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommandations\">\n",
        "  * [Combiner les options d'atténuation des erreurs avec la primitive Estimator](/docs/tutorials/combine-error-mitigation-techniques)\n",
        "  * [Atténuation des erreurs à grande échelle par amplification probabiliste des erreurs](/docs/tutorials/probabilistic-error-amplification)\n",
        "  * La documentation [`qiskit-addon-slc`](https://github.com/Qiskit/qiskit-addon-slc) et [`samplomatic`](https://github.com/Qiskit/samplomatic) la documentation\n",
        "  * [Ombrage par cône de lumière pour l'atténuation des erreurs quantiques par accélération classique](https://arxiv.org/abs/2409.04401)\n",
        "</Admonition>\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": 3,
    "qpuSeconds": 600
  },
  "nbformat": 4,
  "nbformat_minor": 5
}