{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Évaluation de la fidélité du processus QFT+M à l'aide d'Orbit, une fonction Qiskit développée par Quantum Elements\"\n",
        "description: \"Estimer la fidélité du processus QFT suivi d'une mesure (QFT+M) en fonction de la taille des circuits; comparer les implémentations unitaires brutes, dynamiques brutes et dynamiques optimisées par Orbit\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore minexp, succ, fontsize, labelsize */}\n",
        "\n",
        "<span id=\"benchmark-qft+m-process-fidelity-with-orbit-a-qiskit-function-by-quantum-elements\" />\n",
        "\n",
        "# Évaluation de la fidélité du processus QFT+M à l'aide d'Orbit, une fonction Qiskit développée par Quantum Elements\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "warning",
      "metadata": {},
      "source": [
        "*Estimation du temps d'exécution :* 2 minutes sur un processeur Heron r3. (REMARQUE : il ne s'agit que d'une estimation. (Votre temps d'exécution peut varier.) Par défaut, ce tutoriel soumet trois tâches de la fonction Orbit dans une seule charge de travail en mode batch du service de calcul d’ IBM Quantum, avec 300 PUB par tâche, soit 900 PUB et 921 600 plans au total.\n",
        "\n",
        "*Avertissement :* les circuits dynamiques constituent actuellement une fonctionnalité expérimentale et sont soumis à certaines restrictions dans Quantum Compute [\\[3\\]](#references) susceptibles d'entraîner l'échec des tâches. Par exemple, l'erreur 6073 indique qu'une tâche a dépassé la limite de mémoire du matériel de contrôle classique [\\[4\\]](#references). Ce notebook réduit ce risque en répartissant la taille des circuits entre trois tâches de calcul quantique au sein d'un même lot [\\[5\\]](#references). Chaque comparaison de taille fixe est traitée dans un seul travail, tandis que les tailles grandes et petites sont associées afin d'équilibrer les charges de travail en mode « contrôle classique » des travaux.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "learning",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Acquis d'apprentissage\n",
        "\n",
        "Dans le cadre de ce tutoriel,\n",
        "vous apprendrez à :\n",
        "\n",
        "* Préparez les états du produit $\\mathrm{QFT}^\\dagger|x\\rangle$ utilisés par l'estimateur de fidélité du processus par échantillonnage de la figure 2a de la référence [\\[1\\]](#references).\n",
        "* Réaliser des implémentations unitaires et dynamiques équivalentes de la transformée de Fourier quantique suivie d'une mesure (QFT+M).\n",
        "* Sélectionnez les qubits physiques destinés aux circuits dynamiques à l'aide des données actuelles d'étalonnage et de connectivité.\n",
        "* Comparez les estimations de fidélité du processus QFT+M (théorie quantique des champs et de la matière) : « unitaire brute », « dynamique brute » et « dynamique améliorée par Orbit », à mesure que la taille du circuit augmente.\n",
        "* Utilisez l'API de transpilation simplifiée d'Orbit avec `mode=\"raw\"` et `transpilation_mode=\"validate\"`.\n",
        "* Soumettez plusieurs charges de travail Orbit via l'API en mode batch, tout en regroupant chaque comparaison à trois stratégies de taille fixe au sein d'un seul travail.\n",
        "* Vérifiez les métadonnées d'Orbit afin de confirmer si le découplage dynamique (DD) et l'atténuation des erreurs de mesure (MEM) ont été appliqués.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "background",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Arrière-plan\n",
        "\n",
        "La figure 2a de la référence [\\[1\\]](#references) compare la fidélité du processus du canal idéal « QFT+M » à celle d'implémentations unitaires et dynamiques soumises à du bruit. Pour une étiquette échantillonnée de base de calcul $x$, le benchmark prépare $\\mathrm{QFT}^\\dagger|x\\rangle$, applique l'implémentation bruyante de QFT+M et estime la probabilité $p_x$ d'obtenir le résultat idéal correspondant. Ces états de la théorie quantique de champ inverse sont séparables et peuvent être préparés efficacement à l'aide de portes de Hadamard et de rotations de phase virtuelles.\n",
        "\n",
        "Pour les étiquettes échantillonnées de manière indépendante selon une distribution de type « $m$ », le notebook utilise l'estimateur non biaisé dérivé dans la référence [\\[1\\]](#references) :\n",
        "\n",
        "$$\n",
        "\\widehat{\\mathcal{F}}_{\\mathrm{proc}} = \\frac{m}{m-1}\\left(\\frac{1}{m}\\sum_{\\ell=1}^{m}\\sqrt{p_{x_\\ell}}\\right)^2 - \\frac{1}{m(m-1)}\\sum_{\\ell=1}^{m}p_{x_\\ell}.\n",
        "$$\n",
        "\n",
        "Cette construction dynamique remplace les portes à phase contrôlée de la théorie quantique des champs unitaire avec masse (QFT+M) par des mesures en milieu de circuit et des rotations de phase conditionnées de manière classique [\\[1\\]](#references). En mesure différée, les deux circuits présentent la même distribution de sortie idéale. La forme dynamique supprime la nécessité d'une porte à deux qubits de type « tous-à-tous » et utilise à la place des mesures intermédiaires de type « $O(n)$ » avec propagation vers l'avant et sans contrainte de connectivité. La mesure et la projection vers l'avant entraînent également de longues périodes d'inactivité pour les qubits qui n'ont pas encore été mesurés, ce qui rend la méthode DD particulièrement pertinente.\n",
        "\n",
        "**Lien avec la figure 2a.** Ce cahier suit le protocole de fidélité au processus présenté dans l'article, mais il s'agit d'une adaptation pédagogique axée sur Orbit plutôt que d'une reproduction à part entière. Par exemple, alors que la figure 2a utilisait `ibm_kyiv` un dispositif comportant 2 000 tirs, nous utilisons un dispositif `ibm_aachen` moderne comportant un nombre réduit de tirs (1 024) afin de réduire le temps de calcul sur le QPU.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5ae4122d",
      "metadata": {},
      "source": [
        "<span id=\"example-results\" />\n",
        "\n",
        "## Exemple de résultats\n",
        "\n",
        "Le graphique statique ci-dessous présente les courbes moyennes de fidélité au processus issues de trois tâches de développement consécutives exécutées selon `ibm_aachen` le processus décrit ci-dessous. Comme démontré ici, Orbit permet d’améliorer considérablement la qualité des circuits dynamiques; la QFT dynamique atteint le niveau de qualité des tests de référence publiés et présente une amélioration par rapport à la QFT unitaire standard. Comme nous le verrons, ces résultats découlent d'un choix judicieux et automatisé des qubits, de l'insertion automatique de mécanismes de découplage dynamique (non optimisés manuellement pour ce problème) et de l'atténuation des erreurs de mesure. Pour le plaisir, n'oubliez pas de comparer ces résultats aux vôtres à la fin, surtout si vous choisissez un backend différent.\n",
        "\n",
        "**Remarque :** ces résultats constituent un aperçu illustratif d'une exécution antérieure réussie avec Orbit; ils ne constituent en aucun cas une garantie de performances. Les résultats ci-dessous devraient présenter des similitudes sur le plan qualitatif, mais les détails dépendent du périphérique choisi et de ses propriétés, notamment des erreurs de mesure et de veille, lors de l'exécution.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1bb67ce9",
      "metadata": {},
      "source": [
        "![Fidélité du processus QFT sur « ibm\\_aachen »](https://quantum.cloud.ibm.com/docs/images/tutorials/quantum-elements-orbit/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "requirements",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Exigences\n",
        "\n",
        "Installez les versions les plus récentes des paquets suivants avant de suivre ce tutoriel :\n",
        "\n",
        "* `numpy`\n",
        "* `matplotlib`\n",
        "* `qiskit`\n",
        "* `qiskit-ibm-runtime`\n",
        "* `qiskit-ibm-catalog`\n",
        "\n",
        "```bash\n",
        "pip install qiskit qiskit-ibm-runtime qiskit-ibm-catalog numpy matplotlib\n",
        "```\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "setup-md",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuration\n",
        "\n",
        "`ibm_aachen`Connectez-vous à [IBM Quantum® Platform](), lancez la charge, puis chargez Quantum Elements Orbit depuis [Qiskit Functions Catalog](/functions). L'analyse par défaut porte sur 15 tailles de circuits, 20 chaînes de bits échantillonnées par taille et trois stratégies. `NUM_BATCH_JOBS=3` répartit les tailles entre trois tâches au sein d'un même [lot](/docs/guides/run-jobs-batch). Réduire `N_VALUES` ou `M`, ou augmenter `NUM_BATCH_JOBS`, si une tâche à circuit dynamique atteint toujours la limite de mémoire de contrôle classique du backend. Réduire `SHOTS` lorsque l'objectif est de diminuer la consommation en temps d'exécution plutôt que le nombre ou la complexité des circuits.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "setup-code",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "qiskit_runtime_service._discover_account:WARNING:2026-07-21 15:57:39,310: Loading account with the given token. A saved account will not be used.\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'backend': 'ibm_aachen',\n",
              " 'num_qubits': 156,\n",
              " 'n_values': [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40],\n",
              " 'm': 20,\n",
              " 'shots': 1024,\n",
              " 'num_function_jobs': 3,\n",
              " 'n_groups': [[40, 2, 7, 15, 10], [35, 3, 6, 20, 9], [30, 4, 5, 25, 8]],\n",
              " 'pubs_per_job': [300, 300, 300],\n",
              " 'total_pubs': 900,\n",
              " 'total_shots': 921600}"
            ]
          },
          "execution_count": 15,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "import warnings\n",
        "from collections import Counter, defaultdict\n",
        "\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "from qiskit import (\n",
        "    ClassicalRegister,\n",
        "    QuantumCircuit,\n",
        "    QuantumRegister,\n",
        "    transpile,\n",
        ")\n",
        "from qiskit.circuit import IfElseOp\n",
        "from qiskit.synthesis.qft import synth_qft_full\n",
        "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n",
        "from qiskit_ibm_runtime import Batch, QiskitRuntimeService\n",
        "\n",
        "IBM_BACKEND_NAME = \"ibm_aachen\"\n",
        "\n",
        "N_VALUES = [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40]\n",
        "M = 20\n",
        "SHOTS = 1024\n",
        "RNG_SEED = 12345\n",
        "OPTIMIZATION_LEVEL = 0\n",
        "NUM_BATCH_JOBS = 3\n",
        "STRATEGY_LABELS = (\"unitary/raw\", \"dynamic/raw\", \"dynamic/orbit\")\n",
        "\n",
        "\n",
        "def balanced_n_groups(\n",
        "    n_values: list[int], num_jobs: int = 3\n",
        ") -> list[list[int]]:\n",
        "    values = sorted(n_values)\n",
        "    if len(set(values)) != len(values):\n",
        "        raise ValueError(\"N_VALUES must not contain duplicates\")\n",
        "    if not 1 <= num_jobs <= len(values):\n",
        "        raise ValueError(\"NUM_BATCH_JOBS must be between 1 and len(N_VALUES)\")\n",
        "\n",
        "    max_group_size = (len(values) + num_jobs - 1) // num_jobs\n",
        "    groups = [[] for _ in range(num_jobs)]\n",
        "    loads = [0] * num_jobs\n",
        "    pair_counts = [0] * num_jobs\n",
        "    remaining = values.copy()\n",
        "\n",
        "    while len(remaining) >= 2:\n",
        "        candidates = [\n",
        "            i\n",
        "            for i, group in enumerate(groups)\n",
        "            if len(group) + 2 <= max_group_size\n",
        "        ]\n",
        "        if not candidates:\n",
        "            break\n",
        "        smallest = remaining.pop(0)\n",
        "        largest = remaining.pop()\n",
        "        job_index = min(\n",
        "            candidates, key=lambda i: (loads[i], len(groups[i]), i)\n",
        "        )\n",
        "        pair = (\n",
        "            [largest, smallest]\n",
        "            if pair_counts[job_index] % 2 == 0\n",
        "            else [smallest, largest]\n",
        "        )\n",
        "        groups[job_index].extend(pair)\n",
        "        loads[job_index] += smallest + largest\n",
        "        pair_counts[job_index] += 1\n",
        "\n",
        "    while remaining:\n",
        "        value = remaining.pop()\n",
        "        candidates = [\n",
        "            i for i, group in enumerate(groups) if len(group) < max_group_size\n",
        "        ]\n",
        "        job_index = min(\n",
        "            candidates, key=lambda i: (loads[i], len(groups[i]), i)\n",
        "        )\n",
        "        groups[job_index].append(value)\n",
        "        loads[job_index] += value\n",
        "\n",
        "    return groups\n",
        "\n",
        "\n",
        "N_GROUPS = balanced_n_groups(N_VALUES, NUM_BATCH_JOBS)\n",
        "\n",
        "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n",
        "backend = service.backend(IBM_BACKEND_NAME)\n",
        "if \"if_else\" not in backend.target.operation_names:\n",
        "    backend.target.add_instruction(IfElseOp, name=\"if_else\")\n",
        "\n",
        "catalog = QiskitFunctionsCatalog(channel=\"ibm_quantum_platform\")\n",
        "quantum_elements_orbit = catalog.load(\"quantum-elements/orbit\")\n",
        "if quantum_elements_orbit is None:\n",
        "    raise RuntimeError(\n",
        "        \"Quantum Elements Orbit is not enabled for this IBM Quantum instance.\"\n",
        "    )\n",
        "\n",
        "required_qubits = max(N_VALUES)\n",
        "if backend.num_qubits < required_qubits:\n",
        "    raise ValueError(\n",
        "        f\"Backend {backend.name} has {backend.num_qubits} qubits, \"\n",
        "        f\"but this benchmark needs at least {required_qubits}.\"\n",
        "    )\n",
        "\n",
        "{\n",
        "    \"backend\": backend.name,\n",
        "    \"num_qubits\": backend.num_qubits,\n",
        "    \"n_values\": N_VALUES,\n",
        "    \"m\": M,\n",
        "    \"shots\": SHOTS,\n",
        "    \"num_function_jobs\": NUM_BATCH_JOBS,\n",
        "    \"n_groups\": N_GROUPS,\n",
        "    \"pubs_per_job\": [\n",
        "        len(group) * M * len(STRATEGY_LABELS) for group in N_GROUPS\n",
        "    ],\n",
        "    \"total_pubs\": len(N_VALUES) * M * len(STRATEGY_LABELS),\n",
        "    \"total_shots\": len(N_VALUES) * M * len(STRATEGY_LABELS) * SHOTS,\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "build-md",
      "metadata": {},
      "source": [
        "<span id=\"build-qft+m-circuits\" />\n",
        "\n",
        "## Construire des circuits QFT+M\n",
        "\n",
        "Pour chaque entier échantillonné $x$, `bit_inv_qft` on prépare l'état produit $\\mathrm{QFT}^\\dagger|x\\rangle$ à l'aide d'opérateurs de Hadamard suivis de rotations de phase. Le cahier ajoute ensuite soit la théorie quantique des champs unitaire standard, soit son équivalent semi-classique dynamique « QFT+M ».\n",
        "\n",
        "Ces deux implémentations omettent le réseau d'échange final. L'ordre d'affichage classique des bits dans Qiskit fait donc que la chaîne mesurée attendue est l'inverse de la représentation binaire complétée par des zéros de $x$, qui est codée par `format(x, f\"0{n}b\")[::-1]`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "circuits-code",
      "metadata": {},
      "outputs": [],
      "source": [
        "def bit_inv_qft(circuit: QuantumCircuit, x: int, conv: str = \"LSB\") -> None:\n",
        "    num_qubits = circuit.num_qubits\n",
        "    circuit.h(range(num_qubits))\n",
        "    for j in range(num_qubits):\n",
        "        phase = (\n",
        "            2 * np.pi * x / 2 ** (num_qubits - j)\n",
        "            if conv == \"LSB\"\n",
        "            else 2 * np.pi * x / 2 ** (j + 1)\n",
        "        )\n",
        "        circuit.p(-phase, j)\n",
        "\n",
        "\n",
        "def build_unitary_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:\n",
        "    if not 0 <= x < 2**num_qubits:\n",
        "        raise ValueError(\n",
        "            f\"x={x} is outside the {num_qubits}-qubit basis range\"\n",
        "        )\n",
        "    qreg = QuantumRegister(num_qubits, \"q\")\n",
        "    creg = ClassicalRegister(num_qubits, \"c\")\n",
        "    circuit = QuantumCircuit(qreg, creg, name=f\"unitary_qft_{num_qubits}q\")\n",
        "    bit_inv_qft(circuit, x)\n",
        "    circuit.append(\n",
        "        synth_qft_full(num_qubits, do_swaps=False), range(num_qubits)\n",
        "    )\n",
        "    circuit.measure(range(num_qubits), range(num_qubits))\n",
        "    return circuit\n",
        "\n",
        "\n",
        "def _warn_if_precision_loss(max_num_entanglements: int) -> None:\n",
        "    if max_num_entanglements > -np.finfo(float).minexp:\n",
        "        warnings.warn(\n",
        "            \"precision loss in QFT.\"\n",
        "            f\" The rotation needed to represent {max_num_entanglements} entanglements\"\n",
        "            \" is smaller than the smallest normal floating-point number.\",\n",
        "            category=RuntimeWarning,\n",
        "            stacklevel=4,\n",
        "        )\n",
        "\n",
        "\n",
        "def synth_dynamic_qft(\n",
        "    circuit: QuantumCircuit, *, do_swaps: bool = False\n",
        ") -> QuantumCircuit:\n",
        "    num_qubits = circuit.num_qubits\n",
        "    creg = circuit.cregs[0]\n",
        "    _warn_if_precision_loss(num_qubits - 1)\n",
        "\n",
        "    for j in reversed(range(num_qubits)):\n",
        "        circuit.h(j)\n",
        "        circuit.measure([j], [j])\n",
        "\n",
        "        if j > 0:\n",
        "            with circuit.if_test((creg[j], 1)):\n",
        "                for k in reversed(range(j)):\n",
        "                    circuit.p(np.pi * (2.0 ** (k - j)), k)\n",
        "\n",
        "    if do_swaps:\n",
        "        for i in range(num_qubits // 2):\n",
        "            circuit.swap(i, num_qubits - i - 1)\n",
        "    return circuit\n",
        "\n",
        "\n",
        "def build_dynamic_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:\n",
        "    if not 0 <= x < 2**num_qubits:\n",
        "        raise ValueError(\n",
        "            f\"x={x} is outside the {num_qubits}-qubit basis range\"\n",
        "        )\n",
        "    qreg = QuantumRegister(num_qubits, \"q\")\n",
        "    creg = ClassicalRegister(num_qubits, \"c\")\n",
        "    circuit = QuantumCircuit(qreg, creg, name=f\"dynamic_qft_{num_qubits}q\")\n",
        "    bit_inv_qft(circuit, x)\n",
        "    synth_dynamic_qft(circuit, do_swaps=False)\n",
        "    return circuit\n",
        "\n",
        "\n",
        "def target_output_bitstring(x: int, n_qubits: int) -> str:\n",
        "    return format(int(x), f\"0{n_qubits}b\")[::-1]\n",
        "\n",
        "\n",
        "def process_fidelity_from_success_probabilities(\n",
        "    success_probabilities: list[float],\n",
        ") -> float:\n",
        "    m = len(success_probabilities)\n",
        "    if m <= 1:\n",
        "        raise ValueError(\n",
        "            \"m must be larger than 1 for the process-fidelity estimator\"\n",
        "        )\n",
        "    succ = np.asarray(success_probabilities, dtype=float)\n",
        "    return float(\n",
        "        (m / (m - 1)) * (np.mean(np.sqrt(succ)) ** 2)\n",
        "        - np.sum(succ) / (m * (m - 1))\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "select-md",
      "metadata": {},
      "source": [
        "<span id=\"select-dynamic-circuit-physical-qubits\" />\n",
        "\n",
        "## Sélectionner des qubits physiques à circuit dynamique\n",
        "\n",
        "La mise en œuvre dynamique ne nécessite pas de portes à deux qubits; ses qubits physiques n'ont donc pas besoin de former un sous-graphe connecté. Pour chaque taille de circuit, le sélecteur classe les qubits du backend actuels à l’aide d’un score pondéré à 80 % en faveur d’un taux d’erreur de lecture plus faible et à 10 % respectivement en faveur d’une « $T_1$ » et d’une « $T_2$ » plus élevées. Il choisit d’abord, dans la mesure du possible, les qubits ayant obtenu les meilleurs scores et ne présentant aucun couplage direct entre eux, ce qui permet de réduire l’exposition à la diaphonie entre voisins immédiats, puis comble les positions restantes en fonction du score.\n",
        "\n",
        "Les `dynamic/raw` variantes et `dynamic/orbit` utilisent exactement la même mise en page sélectionnée pour une taille donnée, ce qui fait que leur comparaison dépend de la mise en page. Le `unitary/raw` circuit est en revanche mappé et acheminé par le transpileur, car il nécessite une connectivité à deux qubits. Ce choix, fondé sur un étalonnage en temps réel, est spécifique à ce tutoriel; il ne s'agit pas de la configuration fixe à 40 qubits `ibm_kyiv` utilisée pour les expériences présentées dans la figure 2a de l'article.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "select-code",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The top 3 qubits (according to our scoring): \n",
            "[{'qubit': 0, 't1': 0.0002514242577986401, 't2': 0.00037559012475638467, 'measurement_error': 0.0028076171875, 'score': 1.0}, {'qubit': 20, 't1': 0.0002526252407383437, 't2': 0.00038493543771861573, 'measurement_error': 0.00390625, 'score': 1.0}, {'qubit': 25, 't1': 0.0002712841332005567, 't2': 0.00025793268824583597, 'measurement_error': 0.0040283203125, 'score': 1.0}]\n",
            "Worst  3 qubits (according to our scoring): \n",
            "[{'qubit': 146, 't1': 7.619772882181663e-05, 't2': 0.00014166983578724752, 'measurement_error': 0.0802001953125, 'score': 0.05893378230453208}, {'qubit': 51, 't1': 0.00014200221819602618, 't2': 1.930870507157441e-06, 'measurement_error': 0.054443359375, 'score': 0.04600110909801309}, {'qubit': 35, 't1': 7.116087485472031e-05, 't2': 9.463696242928626e-05, 'measurement_error': 0.14501953125, 'score': 0.03289891864200328}]\n"
          ]
        }
      ],
      "source": [
        "def value_from_property(raw):\n",
        "    if raw is None:\n",
        "        return None\n",
        "    if isinstance(raw, tuple):\n",
        "        return raw[0]\n",
        "    return getattr(raw, \"value\", raw)\n",
        "\n",
        "\n",
        "def qubit_property_value(properties, qubit: int, *names: str) -> float | None:\n",
        "    for name in names:\n",
        "        try:\n",
        "            value = value_from_property(\n",
        "                properties.qubit_property(qubit, name)\n",
        "            )\n",
        "        except Exception:\n",
        "            value = None\n",
        "        if value is not None:\n",
        "            return float(value)\n",
        "    return None\n",
        "\n",
        "\n",
        "def measurement_error(properties, qubit: int) -> float | None:\n",
        "    readout = qubit_property_value(properties, qubit, \"readout_error\")\n",
        "    if readout is not None:\n",
        "        return readout\n",
        "    p01 = qubit_property_value(properties, qubit, \"prob_meas0_prep1\")\n",
        "    p10 = qubit_property_value(properties, qubit, \"prob_meas1_prep0\")\n",
        "    if p01 is not None and p10 is not None:\n",
        "        return 0.5 * (p01 + p10)\n",
        "    return None\n",
        "\n",
        "\n",
        "def coupling_edges(backend) -> list[tuple[int, int]]:\n",
        "    coupling_map = getattr(backend, \"coupling_map\", None)\n",
        "    if coupling_map is not None:\n",
        "        try:\n",
        "            return [(int(a), int(b)) for a, b in coupling_map.get_edges()]\n",
        "        except Exception:\n",
        "            pass\n",
        "    built = backend.target.build_coupling_map()\n",
        "    return [(int(a), int(b)) for a, b in built.get_edges()]\n",
        "\n",
        "\n",
        "def neighbor_map(backend) -> dict[int, set[int]]:\n",
        "    neighbors = {qubit: set() for qubit in range(backend.num_qubits)}\n",
        "    for a, b in coupling_edges(backend):\n",
        "        neighbors[a].add(b)\n",
        "        neighbors[b].add(a)\n",
        "    return neighbors\n",
        "\n",
        "\n",
        "def anchored_score(\n",
        "    value: float | None, *, good: float, bad: float, higher_is_better: bool\n",
        ") -> float:\n",
        "    if value is None:\n",
        "        return 0.0\n",
        "    if higher_is_better:\n",
        "        low, high = sorted((bad, good))\n",
        "        score = (value - low) / (high - low)\n",
        "    else:\n",
        "        low, high = sorted((good, bad))\n",
        "        score = (high - value) / (high - low)\n",
        "    return float(min(1.0, max(0.0, score)))\n",
        "\n",
        "\n",
        "def qubit_metrics(backend) -> list[dict]:\n",
        "    properties = backend.properties()\n",
        "    rows = []\n",
        "    for qubit in range(backend.num_qubits):\n",
        "        t1 = qubit_property_value(properties, qubit, \"T1\", \"t1\")\n",
        "        t2 = qubit_property_value(properties, qubit, \"T2\", \"t2\")\n",
        "        meas_error = measurement_error(properties, qubit)\n",
        "        measurement_score = anchored_score(\n",
        "            meas_error, good=0.005, bad=0.05, higher_is_better=False\n",
        "        )\n",
        "        t1_score = anchored_score(\n",
        "            t1, good=0.00025, bad=0.00005, higher_is_better=True\n",
        "        )\n",
        "        t2_score = anchored_score(\n",
        "            t2, good=0.00025, bad=0.00005, higher_is_better=True\n",
        "        )\n",
        "        rows.append(\n",
        "            {\n",
        "                \"qubit\": qubit,\n",
        "                \"t1\": t1,\n",
        "                \"t2\": t2,\n",
        "                \"measurement_error\": meas_error,\n",
        "                \"score\": 0.8 * measurement_score\n",
        "                + 0.1 * t1_score\n",
        "                + 0.1 * t2_score,\n",
        "            }\n",
        "        )\n",
        "    return sorted(rows, key=lambda row: row[\"score\"], reverse=True)\n",
        "\n",
        "\n",
        "def select_dynamic_qubits(backend, n_qubits: int) -> list[int]:\n",
        "    ranked = qubit_metrics(backend)\n",
        "    neighbors = neighbor_map(backend)\n",
        "    selected = []\n",
        "    blocked = set()\n",
        "    for row in ranked:\n",
        "        qubit = row[\"qubit\"]\n",
        "        if qubit in blocked:\n",
        "            continue\n",
        "        selected.append(qubit)\n",
        "        blocked.add(qubit)\n",
        "        blocked.update(neighbors.get(qubit, set()))\n",
        "        if len(selected) == n_qubits:\n",
        "            return selected\n",
        "\n",
        "    for row in ranked:\n",
        "        qubit = row[\"qubit\"]\n",
        "        if qubit not in selected:\n",
        "            selected.append(qubit)\n",
        "        if len(selected) == n_qubits:\n",
        "            return selected\n",
        "    raise RuntimeError(f\"Could not select {n_qubits} physical qubits\")\n",
        "\n",
        "\n",
        "print(\"The top 3 qubits (according to our scoring): \")\n",
        "print(qubit_metrics(backend)[0:3])\n",
        "print(\"Worst  3 qubits (according to our scoring): \")\n",
        "print(qubit_metrics(backend)[-3:])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "prepare-md",
      "metadata": {},
      "source": [
        "<span id=\"prepare-the-benchmark-pubs\" />\n",
        "\n",
        "## Préparer les PUB de référence\n",
        "\n",
        "Pour chaque `(N, x)` paire, le notebook transpose d'abord les circuits logiques et crée un échantillonneur PUB pour chaque stratégie :\n",
        "\n",
        "* `unitary/raw`: théorie quantique des champs unitaire + M sur la configuration choisie par le transpileur, avec routage selon les besoins et sans Orbit DD ni MEM.\n",
        "* `dynamic/raw`: QFT+M dynamique sur les qubits physiques sélectionnés par étalonnage, sans Orbit DD ni MEM.\n",
        "* `dynamic/orbit`: le même circuit dynamique transpilé sur les mêmes qubits physiques, avec les fonctions Orbit DD et MEM activées.\n",
        "\n",
        "L' PUB, optimisée par Orbit, utilise `transpilation_mode=\"validate\"` car son mappage a déjà été défini. Orbit valide le circuit physique fourni au lieu de le remapper, puis applique ses pipelines DD et MEM. Étant donné que seule la courbe dynamique améliorée nécessite l'utilisation de la mémoire MEM, cela ne doit pas être interprété comme une comparaison isolée entre la méthode DD et l'absence de DD.\n",
        "\n",
        "Les fichiers PUB, les options « par- PUB » et les enregistrements de résultats sont stockés par index de tâche batch. `unitary/raw`Pour chaque « $N$ » fixe, les PUB, `dynamic/raw`, et `dynamic/orbit` sont regroupés dans le même travail. L'assistant de regroupement associe des circuits de grande et de petite taille, alterne leur ordre et équilibre la somme des « $N$ » entre les trois tâches, ce qui constitue un indicateur simple de la charge de travail d'un système de contrôle classique.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 31,
      "id": "prepare-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'num_function_jobs': 3,\n",
              " 'n_groups': {0: [40, 2, 7, 15, 10],\n",
              "  1: [35, 3, 6, 20, 9],\n",
              "  2: [30, 4, 5, 25, 8]},\n",
              " 'n_load_per_job': {0: 74, 1: 73, 2: 72},\n",
              " 'pubs_per_job': {0: 300, 1: 300, 2: 300},\n",
              " 'expected_executions_per_job': {0: 307200, 1: 307200, 2: 307200},\n",
              " 'first_pub_record_by_job': {0: {'job_index': 0,\n",
              "   'n_qubits': 40,\n",
              "   'target_decimal': 853235401719,\n",
              "   'target_bitstring': '1110111111000000110100110001010101100011',\n",
              "   'label': 'unitary/raw',\n",
              "   'pub_options': {'mode': 'raw'},\n",
              "   'dynamic_qubits': None,\n",
              "   'transpiled_depth': 4778,\n",
              "   'transpiled_size': 28259},\n",
              "  1: {'job_index': 1,\n",
              "   'n_qubits': 35,\n",
              "   'target_decimal': 26888951661,\n",
              "   'target_bitstring': '10110110111010110010110101000010011',\n",
              "   'label': 'unitary/raw',\n",
              "   'pub_options': {'mode': 'raw'},\n",
              "   'dynamic_qubits': None,\n",
              "   'transpiled_depth': 3614,\n",
              "   'transpiled_size': 21204},\n",
              "  2: {'job_index': 2,\n",
              "   'n_qubits': 30,\n",
              "   'target_decimal': 620442965,\n",
              "   'target_bitstring': '101010101010110011011111001001',\n",
              "   'label': 'unitary/raw',\n",
              "   'pub_options': {'mode': 'raw'},\n",
              "   'dynamic_qubits': None,\n",
              "   'transpiled_depth': 2835,\n",
              "   'transpiled_size': 15078}},\n",
              " 'largest_dynamic_qubit_set': [0,\n",
              "  20,\n",
              "  25,\n",
              "  27,\n",
              "  33,\n",
              "  59,\n",
              "  74,\n",
              "  80,\n",
              "  95,\n",
              "  144,\n",
              "  151,\n",
              "  155,\n",
              "  79,\n",
              "  90,\n",
              "  60,\n",
              "  68,\n",
              "  114,\n",
              "  107,\n",
              "  13,\n",
              "  126,\n",
              "  133,\n",
              "  103,\n",
              "  3,\n",
              "  87,\n",
              "  53,\n",
              "  41,\n",
              "  130,\n",
              "  5,\n",
              "  98,\n",
              "  135,\n",
              "  153,\n",
              "  15,\n",
              "  116,\n",
              "  45,\n",
              "  7,\n",
              "  48,\n",
              "  136,\n",
              "  11,\n",
              "  147,\n",
              "  77]}"
            ]
          },
          "execution_count": 31,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "strategy_options = {\n",
        "    \"unitary/raw\": {\"mode\": \"raw\"},\n",
        "    \"dynamic/raw\": {\"mode\": \"raw\"},\n",
        "    \"dynamic/orbit\": {\"mode\": \"orbit\", \"transpilation_mode\": \"validate\"},\n",
        "}\n",
        "rng = np.random.default_rng(RNG_SEED)\n",
        "pubs_by_job = [[] for _ in N_GROUPS]\n",
        "pub_options_by_job = [[] for _ in N_GROUPS]\n",
        "pub_records_by_job = [[] for _ in N_GROUPS]\n",
        "layout_summary = {}\n",
        "\n",
        "target_decimals_by_n = {\n",
        "    n_qubits: [int(x) for x in rng.integers(0, 2**n_qubits, size=M)]\n",
        "    for n_qubits in N_VALUES\n",
        "}\n",
        "\n",
        "for job_index, n_group in enumerate(N_GROUPS):\n",
        "    for n_qubits in n_group:\n",
        "        dynamic_qubits = select_dynamic_qubits(backend, n_qubits)\n",
        "        layout_summary[str(n_qubits)] = {\"dynamic_qubits\": dynamic_qubits}\n",
        "\n",
        "        for x in target_decimals_by_n[n_qubits]:\n",
        "            target_bitstring = target_output_bitstring(x, n_qubits)\n",
        "            unitary_logical = build_unitary_qft_circuit(n_qubits, x)\n",
        "            dynamic_logical = build_dynamic_qft_circuit(n_qubits, x)\n",
        "\n",
        "            unitary_transpiled = transpile(\n",
        "                unitary_logical,\n",
        "                backend=backend,\n",
        "                optimization_level=OPTIMIZATION_LEVEL,\n",
        "                seed_transpiler=RNG_SEED,\n",
        "            )\n",
        "            dynamic_transpiled = transpile(\n",
        "                dynamic_logical,\n",
        "                backend=backend,\n",
        "                optimization_level=OPTIMIZATION_LEVEL,\n",
        "                seed_transpiler=RNG_SEED,\n",
        "                initial_layout=dynamic_qubits,\n",
        "            )\n",
        "\n",
        "            circuits_by_label = {\n",
        "                \"unitary/raw\": unitary_transpiled,\n",
        "                \"dynamic/raw\": dynamic_transpiled,\n",
        "                \"dynamic/orbit\": dynamic_transpiled,\n",
        "            }\n",
        "            for label in STRATEGY_LABELS:\n",
        "                circuit = circuits_by_label[label]\n",
        "                options = dict(strategy_options[label])\n",
        "                pubs_by_job[job_index].append((circuit, None, SHOTS))\n",
        "                pub_options_by_job[job_index].append(options)\n",
        "                pub_records_by_job[job_index].append(\n",
        "                    {\n",
        "                        \"job_index\": job_index,\n",
        "                        \"n_qubits\": n_qubits,\n",
        "                        \"target_decimal\": x,\n",
        "                        \"target_bitstring\": target_bitstring,\n",
        "                        \"label\": label,\n",
        "                        \"pub_options\": options,\n",
        "                        \"dynamic_qubits\": (\n",
        "                            dynamic_qubits\n",
        "                            if label.startswith(\"dynamic/\")\n",
        "                            else None\n",
        "                        ),\n",
        "                        \"transpiled_depth\": circuit.depth(),\n",
        "                        \"transpiled_size\": circuit.size(),\n",
        "                    }\n",
        "                )\n",
        "\n",
        "{\n",
        "    \"num_function_jobs\": len(N_GROUPS),\n",
        "    \"n_groups\": {\n",
        "        job_index: group for job_index, group in enumerate(N_GROUPS)\n",
        "    },\n",
        "    \"n_load_per_job\": {\n",
        "        job_index: sum(group) for job_index, group in enumerate(N_GROUPS)\n",
        "    },\n",
        "    \"pubs_per_job\": {\n",
        "        job_index: len(pubs) for job_index, pubs in enumerate(pubs_by_job)\n",
        "    },\n",
        "    \"expected_executions_per_job\": {\n",
        "        job_index: len(pubs) * SHOTS\n",
        "        for job_index, pubs in enumerate(pubs_by_job)\n",
        "    },\n",
        "    \"first_pub_record_by_job\": {\n",
        "        job_index: records[0]\n",
        "        for job_index, records in enumerate(pub_records_by_job)\n",
        "    },\n",
        "    \"largest_dynamic_qubit_set\": layout_summary[str(max(N_VALUES))][\n",
        "        \"dynamic_qubits\"\n",
        "    ],\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "run-md",
      "metadata": {},
      "source": [
        "<span id=\"run-the-benchmark\" />\n",
        "\n",
        "## Lancer le test de performance\n",
        "\n",
        "Créez un [lot](/docs/guides/run-jobs-batch), puis soumettez-y trois tâches utilisant la fonction Orbit.\n",
        "\n",
        "Les tâches sont réparties par groupes en fonction du nombre de qubits, de manière à pouvoir comparer les trois stratégies pour une « $N$ » donnée. Autrement dit, les 60 PUB correspondant à une « $N$ » donnée — soit 20 entrées échantillonnées multipliées par les trois stratégies — s’exécutent donc dans la même tâche et peuvent ainsi être comparées de la manière la plus équitable possible (sinon, si elles étaient exécutées dans des tâches différentes, le dispositif pourrait subir une dérive pendant son passage dans la file d’attente). Les groupes par défaut combinent des circuits de grande et de petite taille et contiennent chacun 300 PUB, ce qui réduit le risque qu'une tâche accumule tous les programmes dynamiques les plus volumineux tout en préservant les comparaisons au sein de la même tâche.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 32,
      "id": "run-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',\n",
              " 'jobs': {0: {'backend': 'ibm_aachen',\n",
              "   'function_job_id': '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',\n",
              "   'status': 'QUEUED',\n",
              "   'n_values': [40, 2, 7, 15, 10],\n",
              "   'num_pubs': 300},\n",
              "  1: {'backend': 'ibm_aachen',\n",
              "   'function_job_id': '4f046fd4-80e4-460b-87c7-e7252691f764',\n",
              "   'status': 'QUEUED',\n",
              "   'n_values': [35, 3, 6, 20, 9],\n",
              "   'num_pubs': 300},\n",
              "  2: {'backend': 'ibm_aachen',\n",
              "   'function_job_id': '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be',\n",
              "   'status': 'QUEUED',\n",
              "   'n_values': [30, 4, 5, 25, 8],\n",
              "   'num_pubs': 300}}}"
            ]
          },
          "execution_count": 32,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "runtime_batch = Batch(backend=backend)\n",
        "jobs = []\n",
        "try:\n",
        "    for job_index, pubs in enumerate(pubs_by_job):\n",
        "        jobs.append(\n",
        "            quantum_elements_orbit.run(\n",
        "                primitive=\"sampler\",\n",
        "                pubs=pubs,\n",
        "                backend_name=backend.name,\n",
        "                options={\n",
        "                    \"pub_options\": pub_options_by_job[job_index],\n",
        "                    \"save_backend_info\": True,\n",
        "                },\n",
        "            )\n",
        "        )\n",
        "except Exception:\n",
        "    runtime_batch.close()\n",
        "    raise\n",
        "\n",
        "{\n",
        "    \"runtime_batch_id\": runtime_batch.session_id,\n",
        "    \"jobs\": {\n",
        "        job_index: {\n",
        "            \"backend\": backend.name,\n",
        "            \"function_job_id\": job.job_id,\n",
        "            \"status\": job.status(),\n",
        "            \"n_values\": N_GROUPS[job_index],\n",
        "            \"num_pubs\": len(pubs_by_job[job_index]),\n",
        "        }\n",
        "        for job_index, job in enumerate(jobs)\n",
        "    },\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "results-md",
      "metadata": {},
      "source": [
        "<span id=\"retrieve-results-and-compute-process-fidelity\" />\n",
        "\n",
        "## Récupérer les résultats et calculer la fidélité du processus\n",
        "\n",
        "Récupérez et validez indépendamment chaque résultat de groupe de qubits, puis fusionnez les trois flux de tâches à l'aide de leurs enregistrements indexés par tâche. Le lot reste ouvert tant que tous les résultats des fonctions sont en cours de récupération, puis il est fermé en bloc `finally` une fois que chaque tâche a été exécutée. Pour chaque PUB, $p_x$ est la probabilité attribuée à la chaîne de bits attendue. `extract_counts` lit les comptes renvoyés à l'appelant; pour `dynamic/orbit`, il s'agit des comptes ajustés en fonction de la mémoire (MEM) lorsque l'atténuation réussit. `extract_raw_counts` récupère également les nombres bruts correspondants enregistrés dans les métadonnées d'Orbit. Le code regroupe les 20 valeurs de « $p_x$ » pour chaque `(N, label)` paire et applique l'estimateur présenté ci-dessus.\n",
        "\n",
        "Le dictionnaire représenté `process_fidelity` graphiquement utilise donc les fréquences brutes pour `unitary/raw` et `dynamic/raw`, mais les fréquences ajustées selon la méthode MEM pour `dynamic/orbit`. Le dictionnaire parallèle `raw_process_fidelity` conserve un calcul sans ajustement pour chaque stratégie et s'avère utile pour isoler l'effet de MEM du reste du pipeline Orbit. La fonction MEM corrige l'histogramme de sortie renvoyé; elle ne peut pas modifier rétroactivement un résultat de mesure en cours de circuit qui a déjà été utilisé par la commande anticipative en temps réel.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 35,
      "id": "results-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',\n",
              " 'function_job_ids': {0: '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',\n",
              "  1: '4f046fd4-80e4-460b-87c7-e7252691f764',\n",
              "  2: '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be'},\n",
              " 'n_groups': {0: [40, 2, 7, 15, 10],\n",
              "  1: [35, 3, 6, 20, 9],\n",
              "  2: [30, 4, 5, 25, 8]},\n",
              " 'process_fidelity': {'2': {'dynamic/orbit': 0.9870551835473073,\n",
              "   'dynamic/raw': 0.9912537998030566,\n",
              "   'unitary/raw': 0.9884650767434809},\n",
              "  '3': {'dynamic/orbit': 0.9662998634131841,\n",
              "   'dynamic/raw': 0.9699631603283018,\n",
              "   'unitary/raw': 0.9388637172865901},\n",
              "  '4': {'dynamic/orbit': 0.9271266520750502,\n",
              "   'dynamic/raw': 0.7334377020091254,\n",
              "   'unitary/raw': 0.9010122207121433},\n",
              "  '5': {'dynamic/orbit': 0.8883501513887149,\n",
              "   'dynamic/raw': 0.6577660260669806,\n",
              "   'unitary/raw': 0.7806443417987445},\n",
              "  '6': {'dynamic/orbit': 0.8524225652033044,\n",
              "   'dynamic/raw': 0.4444025126308521,\n",
              "   'unitary/raw': 0.7167426842521228},\n",
              "  '7': {'dynamic/orbit': 0.832962085697061,\n",
              "   'dynamic/raw': 0.2253787798698553,\n",
              "   'unitary/raw': 0.5746335601063436},\n",
              "  '8': {'dynamic/orbit': 0.7881895956180588,\n",
              "   'dynamic/raw': 0.16909516699831612,\n",
              "   'unitary/raw': 0.5408263851227074},\n",
              "  '9': {'dynamic/orbit': 0.7422635627368794,\n",
              "   'dynamic/raw': 0.0242474245097341,\n",
              "   'unitary/raw': 0.4855953298367578},\n",
              "  '10': {'dynamic/orbit': 0.7002274273149545,\n",
              "   'dynamic/raw': 0.033718865729016285,\n",
              "   'unitary/raw': 0.3607634828181049},\n",
              "  '15': {'dynamic/orbit': 0.4694995355699914,\n",
              "   'dynamic/raw': 7.70970394736842e-05,\n",
              "   'unitary/raw': 0.054582117352985286},\n",
              "  '20': {'dynamic/orbit': 0.24118032284867608,\n",
              "   'dynamic/raw': 4.235164736271502e-22,\n",
              "   'unitary/raw': 0.0},\n",
              "  '25': {'dynamic/orbit': 0.027122712989729438,\n",
              "   'dynamic/raw': 0.0,\n",
              "   'unitary/raw': 0.0},\n",
              "  '30': {'dynamic/orbit': 0.0003581886014704875,\n",
              "   'dynamic/raw': 0.0,\n",
              "   'unitary/raw': 0.0},\n",
              "  '35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n",
              "  '40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}},\n",
              " 'mean_success_probability': {'2': {'dynamic/orbit': 0.987060546875,\n",
              "   'dynamic/raw': 0.991259765625,\n",
              "   'unitary/raw': 0.9884765625},\n",
              "  '3': {'dynamic/orbit': 0.96630859375,\n",
              "   'dynamic/raw': 0.969970703125,\n",
              "   'unitary/raw': 0.939013671875},\n",
              "  '4': {'dynamic/orbit': 0.9271484375,\n",
              "   'dynamic/raw': 0.7337890625,\n",
              "   'unitary/raw': 0.901318359375},\n",
              "  '5': {'dynamic/orbit': 0.88837890625,\n",
              "   'dynamic/raw': 0.657861328125,\n",
              "   'unitary/raw': 0.78115234375},\n",
              "  '6': {'dynamic/orbit': 0.85244140625,\n",
              "   'dynamic/raw': 0.44453125,\n",
              "   'unitary/raw': 0.71728515625},\n",
              "  '7': {'dynamic/orbit': 0.8330078125,\n",
              "   'dynamic/raw': 0.22568359375,\n",
              "   'unitary/raw': 0.575390625},\n",
              "  '8': {'dynamic/orbit': 0.788232421875,\n",
              "   'dynamic/raw': 0.169189453125,\n",
              "   'unitary/raw': 0.541796875},\n",
              "  '9': {'dynamic/orbit': 0.742333984375,\n",
              "   'dynamic/raw': 0.0244140625,\n",
              "   'unitary/raw': 0.487353515625},\n",
              "  '10': {'dynamic/orbit': 0.70029296875,\n",
              "   'dynamic/raw': 0.033935546875,\n",
              "   'unitary/raw': 0.363037109375},\n",
              "  '15': {'dynamic/orbit': 0.4697265625,\n",
              "   'dynamic/raw': 0.00029296875,\n",
              "   'unitary/raw': 0.055615234375},\n",
              "  '20': {'dynamic/orbit': 0.241357421875,\n",
              "   'dynamic/raw': 4.8828125e-05,\n",
              "   'unitary/raw': 0.0},\n",
              "  '25': {'dynamic/orbit': 0.041015625, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n",
              "  '30': {'dynamic/orbit': 0.0013671875,\n",
              "   'dynamic/raw': 0.0,\n",
              "   'unitary/raw': 0.0},\n",
              "  '35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n",
              "  '40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}}}"
            ]
          },
          "execution_count": 35,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "def extract_counts(pub_result) -> dict[str, int]:\n",
        "    data = getattr(pub_result, \"data\", None)\n",
        "    if data is None:\n",
        "        raise TypeError(\"pub_result.data is missing\")\n",
        "\n",
        "    for name in dir(data):\n",
        "        if name.startswith(\"_\"):\n",
        "            continue\n",
        "        register = getattr(data, name)\n",
        "        get_counts = getattr(register, \"get_counts\", None)\n",
        "        if callable(get_counts):\n",
        "            counts = get_counts()\n",
        "            if counts:\n",
        "                return counts\n",
        "\n",
        "    raise TypeError(\n",
        "        \"No classical register with get_counts() found in pub_result.data\"\n",
        "    )\n",
        "\n",
        "\n",
        "def extract_raw_counts(pub_result) -> dict[str, int]:\n",
        "    orbit_metadata = pub_result.metadata.get(\"quantum_elements_orbit\", {})\n",
        "    mem_report = orbit_metadata.get(\"measurementErrorMitigation\", {})\n",
        "    return mem_report.get(\"rawCounts\") or extract_counts(pub_result)\n",
        "\n",
        "\n",
        "def probability_for_bitstring(\n",
        "    counts: dict[str, int], bitstring: str, n_qubits: int\n",
        ") -> float:\n",
        "    total = sum(counts.values())\n",
        "    if total <= 0:\n",
        "        return 0.0\n",
        "    normalized = Counter()\n",
        "    for measured, count in counts.items():\n",
        "        key = measured.replace(\" \", \"\")[-n_qubits:].zfill(n_qubits)\n",
        "        normalized[key] += count\n",
        "    return float(normalized.get(bitstring, 0) / total)\n",
        "\n",
        "\n",
        "results_by_job = {}\n",
        "job_failures = []\n",
        "try:\n",
        "    for job_index, job in enumerate(jobs):\n",
        "        try:\n",
        "            job_result = job.result()\n",
        "        except Exception as exc:\n",
        "            job_logs = getattr(job, \"logs\", lambda: \"\")()\n",
        "            if job_logs:\n",
        "                print(f\"Logs for job {job_index} ({job.job_id}):\\n{job_logs}\")\n",
        "            job_failures.append(\n",
        "                f\"job {job_index} ({job.job_id}) failed: {type(exc).__name__}: {exc}\"\n",
        "            )\n",
        "            continue\n",
        "\n",
        "        expected_results = len(pub_records_by_job[job_index])\n",
        "        if len(job_result) != expected_results:\n",
        "            job_failures.append(\n",
        "                f\"job {job_index} ({job.job_id}) returned {len(job_result)} PUB results; \"\n",
        "                f\"expected {expected_results}\"\n",
        "            )\n",
        "            continue\n",
        "        results_by_job[job_index] = job_result\n",
        "finally:\n",
        "    runtime_batch.close()\n",
        "\n",
        "if job_failures:\n",
        "    raise RuntimeError(\n",
        "        \"One or more batched Orbit jobs failed:\\n\" + \"\\n\".join(job_failures)\n",
        "    )\n",
        "\n",
        "grouped_success = defaultdict(list)\n",
        "grouped_raw_success = defaultdict(list)\n",
        "pub_summaries = []\n",
        "\n",
        "for job_index, job_result in sorted(results_by_job.items()):\n",
        "    records = pub_records_by_job[job_index]\n",
        "    for record, pub_result in zip(records, job_result, strict=True):\n",
        "        label = record[\"label\"]\n",
        "        n_qubits = record[\"n_qubits\"]\n",
        "        counts = extract_counts(pub_result)\n",
        "        raw_counts = extract_raw_counts(pub_result)\n",
        "        success = probability_for_bitstring(\n",
        "            counts, record[\"target_bitstring\"], n_qubits\n",
        "        )\n",
        "        raw_success = probability_for_bitstring(\n",
        "            raw_counts, record[\"target_bitstring\"], n_qubits\n",
        "        )\n",
        "        key = (n_qubits, label)\n",
        "        grouped_success[key].append(success)\n",
        "        grouped_raw_success[key].append(raw_success)\n",
        "\n",
        "        orbit_report = pub_result.metadata.get(\"quantum_elements_orbit\", {})\n",
        "        mem_report = orbit_report.get(\"measurementErrorMitigation\", {})\n",
        "        pub_summaries.append(\n",
        "            {\n",
        "                **record,\n",
        "                \"function_job_id\": jobs[job_index].job_id,\n",
        "                \"runtime_batch_id\": runtime_batch.session_id,\n",
        "                \"success_probability\": success,\n",
        "                \"raw_success_probability\": raw_success,\n",
        "                \"orbit_mode\": orbit_report.get(\"mode\"),\n",
        "                \"transpilation_mode\": orbit_report.get(\"transpilationMode\"),\n",
        "                \"physical_layout\": orbit_report.get(\"physicalLayout\"),\n",
        "                \"dd_status\": orbit_report.get(\"status\", \"not_applied\"),\n",
        "                \"num_sequences_added\": orbit_report.get(\n",
        "                    \"numSequencesAdded\", 0\n",
        "                ),\n",
        "                \"num_gaps_filled\": orbit_report.get(\"numGapsFilled\", 0),\n",
        "                \"dynamic_dd_seq\": orbit_report.get(\"dynamicDdSeq\"),\n",
        "                \"mem_status\": mem_report.get(\"status\", \"not_requested\"),\n",
        "                \"warnings\": orbit_report.get(\"warnings\", [])\n",
        "                + mem_report.get(\"warnings\", []),\n",
        "            }\n",
        "        )\n",
        "\n",
        "process_fidelity = defaultdict(dict)\n",
        "raw_process_fidelity = defaultdict(dict)\n",
        "mean_success_probability = defaultdict(dict)\n",
        "raw_mean_success_probability = defaultdict(dict)\n",
        "\n",
        "for (n_qubits, label), probabilities in sorted(grouped_success.items()):\n",
        "    n_key = str(n_qubits)\n",
        "    process_fidelity[n_key][label] = (\n",
        "        process_fidelity_from_success_probabilities(probabilities)\n",
        "    )\n",
        "    mean_success_probability[n_key][label] = float(np.mean(probabilities))\n",
        "\n",
        "for (n_qubits, label), probabilities in sorted(grouped_raw_success.items()):\n",
        "    n_key = str(n_qubits)\n",
        "    raw_process_fidelity[n_key][label] = (\n",
        "        process_fidelity_from_success_probabilities(probabilities)\n",
        "    )\n",
        "    raw_mean_success_probability[n_key][label] = float(np.mean(probabilities))\n",
        "\n",
        "process_fidelity = dict(process_fidelity)\n",
        "raw_process_fidelity = dict(raw_process_fidelity)\n",
        "mean_success_probability = dict(mean_success_probability)\n",
        "raw_mean_success_probability = dict(raw_mean_success_probability)\n",
        "\n",
        "{\n",
        "    \"runtime_batch_id\": runtime_batch.session_id,\n",
        "    \"function_job_ids\": {\n",
        "        job_index: job.job_id for job_index, job in enumerate(jobs)\n",
        "    },\n",
        "    \"n_groups\": {\n",
        "        job_index: group for job_index, group in enumerate(N_GROUPS)\n",
        "    },\n",
        "    \"process_fidelity\": process_fidelity,\n",
        "    \"mean_success_probability\": mean_success_probability,\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "metadata-md",
      "metadata": {},
      "source": [
        "<span id=\"inspect-orbit-dd-metadata\" />\n",
        "\n",
        "## Consulter les métadonnées d'Orbit DD\n",
        "\n",
        "Le résumé ci-dessous vérifie les `dynamic/orbit` métadonnées de `PUB` au lieu de partir du principe que le DD demandé a bien été inséré. Vérifiez l'état, la séquence DD dynamique signalée, les avertissements, ainsi que le nombre de lacunes comblées et de séquences ajoutées. Une insertion réussie devrait générer des comptes non nuls pour au moins certains PUB, mais les valeurs exactes dépendent du circuit planifié, des contraintes de synchronisation du backend et de la taille du circuit. Ces métadonnées décrivent la séquence appliquée par Orbit; elles ne doivent pas être considérées comme le protocole FC-DD de l'article, à moins que le rapport n'établisse explicitement cette équivalence.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 36,
      "id": "metadata-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'dynamic_orbit_dd_status_counts': {'40': {'dd_inserted': 20},\n",
              "  '2': {'dd_inserted': 20},\n",
              "  '7': {'dd_inserted': 20},\n",
              "  '15': {'dd_inserted': 20},\n",
              "  '10': {'dd_inserted': 20},\n",
              "  '35': {'dd_inserted': 20},\n",
              "  '3': {'dd_inserted': 20},\n",
              "  '6': {'dd_inserted': 20},\n",
              "  '20': {'dd_inserted': 20},\n",
              "  '9': {'dd_inserted': 20},\n",
              "  '30': {'dd_inserted': 20},\n",
              "  '4': {'dd_inserted': 20},\n",
              "  '5': {'dd_inserted': 20},\n",
              "  '25': {'dd_inserted': 20},\n",
              "  '8': {'dd_inserted': 20}},\n",
              " 'dynamic_orbit_sequences_added': {'40': 31200,\n",
              "  '2': 40,\n",
              "  '7': 840,\n",
              "  '15': 4200,\n",
              "  '10': 1800,\n",
              "  '35': 23800,\n",
              "  '3': 120,\n",
              "  '6': 600,\n",
              "  '20': 7600,\n",
              "  '9': 1440,\n",
              "  '30': 17400,\n",
              "  '4': 240,\n",
              "  '5': 400,\n",
              "  '25': 12000,\n",
              "  '8': 1120},\n",
              " 'warning_examples': [{'n_qubits': 40,\n",
              "   'target_decimal': 853235401719,\n",
              "   'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',\n",
              "    'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},\n",
              "  {'n_qubits': 40,\n",
              "   'target_decimal': 954673909846,\n",
              "   'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',\n",
              "    'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},\n",
              "  {'n_qubits': 40,\n",
              "   'target_decimal': 524641045908,\n",
              "   'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',\n",
              "    'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},\n",
              "  {'n_qubits': 40,\n",
              "   'target_decimal': 185651043478,\n",
              "   'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',\n",
              "    'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},\n",
              "  {'n_qubits': 40,\n",
              "   'target_decimal': 587114273567,\n",
              "   'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',\n",
              "    'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']}]}"
            ]
          },
          "execution_count": 36,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "dd_summary = defaultdict(lambda: Counter())\n",
        "sequence_totals = defaultdict(int)\n",
        "warning_examples = []\n",
        "\n",
        "for summary in pub_summaries:\n",
        "    if summary[\"label\"] != \"dynamic/orbit\":\n",
        "        continue\n",
        "    n_key = str(summary[\"n_qubits\"])\n",
        "    dd_summary[n_key][summary[\"dd_status\"]] += 1\n",
        "    sequence_totals[n_key] += int(summary.get(\"num_sequences_added\") or 0)\n",
        "    if summary.get(\"warnings\") and len(warning_examples) < 5:\n",
        "        warning_examples.append(\n",
        "            {\n",
        "                \"n_qubits\": summary[\"n_qubits\"],\n",
        "                \"target_decimal\": summary[\"target_decimal\"],\n",
        "                \"warnings\": summary[\"warnings\"],\n",
        "            }\n",
        "        )\n",
        "\n",
        "{\n",
        "    \"dynamic_orbit_dd_status_counts\": {\n",
        "        key: dict(value) for key, value in dd_summary.items()\n",
        "    },\n",
        "    \"dynamic_orbit_sequences_added\": dict(sequence_totals),\n",
        "    \"warning_examples\": warning_examples,\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "plot-md",
      "metadata": {},
      "source": [
        "<span id=\"plot-process-fidelity-curves\" />\n",
        "\n",
        "## Tracer des courbes de fidélité du processus\n",
        "\n",
        "Le graphique présente l'estimation ponctuelle de la fidélité du processus QFT+M, obtenue par échantillonnage, en fonction du nombre de qubits pour les trois stratégies. `dynamic/raw` et `dynamic/orbit` partagent une même configuration physique pour chaque taille; `unitary/raw` utilisent la configuration et le routage du transpilateur.\n",
        "\n",
        "Contrairement à la figure 2a, ce graphique ne présente ni courbe unitaire avec DD ni bandes d'incertitude, et ses courbes brutes n'ont pas fait l'objet d'une correction liée à la lecture. Il convient de considérer ce document comme une comparaison d'échelle à l' Figure-2a-style pour ce flux de travail Orbit, et non comme une reproduction directe des courbes publiées.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 51,
      "id": "1972ceb9",
      "metadata": {},
      "outputs": [],
      "source": [
        "from datetime import datetime\n",
        "from zoneinfo import ZoneInfo\n",
        "\n",
        "closed_at = runtime_batch.details()[\"closed_at\"]  # \"2026-07-22T00:08:54.89Z\"\n",
        "closed_dt = datetime.fromisoformat(closed_at.replace(\"Z\", \"+00:00\"))\n",
        "closed_local = closed_dt.astimezone(ZoneInfo(\"America/Los_Angeles\"))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 53,
      "id": "plot-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/quantum-elements-orbit/extracted-outputs/plot-code-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "labels = [\"dynamic/orbit\", \"dynamic/raw\", \"unitary/raw\"]\n",
        "colors = {\n",
        "    \"dynamic/orbit\": \"#26735b\",\n",
        "    \"dynamic/raw\": \"#9b1c31\",\n",
        "    \"unitary/raw\": \"#6e6e6e\",\n",
        "}\n",
        "pretty_labels = {\n",
        "    \"dynamic/orbit\": \"Dynamic QFT+M with Orbit\",\n",
        "    \"dynamic/raw\": \"Dynamic QFT+M\",\n",
        "    \"unitary/raw\": \"Unitary QFT+M\",\n",
        "}\n",
        "\n",
        "series = []\n",
        "for label in labels:\n",
        "    values = [process_fidelity[str(n)][label] for n in N_VALUES]\n",
        "    log_values = [value if value > 0.0 else float(\"nan\") for value in values]\n",
        "    series.append((label, values, log_values))\n",
        "\n",
        "nonzero_values = [\n",
        "    value\n",
        "    for _, _, log_values in series\n",
        "    for value in log_values\n",
        "    if value > 0.0\n",
        "]\n",
        "if not nonzero_values:\n",
        "    raise RuntimeError(\n",
        "        \"No nonzero process-fidelity values found for log inset\"\n",
        "    )\n",
        "log_floor = min(nonzero_values) / 2\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(9.8, 5.6))\n",
        "for label, values, _ in series:\n",
        "    ax.plot(\n",
        "        N_VALUES,\n",
        "        values,\n",
        "        marker=\"o\",\n",
        "        linewidth=2.0,\n",
        "        markersize=5,\n",
        "        color=colors[label],\n",
        "        label=pretty_labels[label],\n",
        "    )\n",
        "\n",
        "ax.set_xlabel(\"N qubits\")\n",
        "ax.set_ylabel(\"Process fidelity\")\n",
        "finished_time_for_title = globals().get(\"finished_local\", closed_local)\n",
        "ax.set_title(\n",
        "    f\"Dynamic QFT Orbit results on {IBM_BACKEND_NAME}\\n\"\n",
        "    f\"Job finished {finished_time_for_title:%Y-%m-%d %H:%M %Z}\"\n",
        ")\n",
        "ax.set_xticks(N_VALUES)\n",
        "ax.set_ylim(bottom=0)\n",
        "ax.grid(axis=\"both\", alpha=0.25)\n",
        "ax.legend(loc=\"upper right\")\n",
        "\n",
        "inset = ax.inset_axes([0.53, 0.31, 0.44, 0.43])\n",
        "for label, _, log_values in series:\n",
        "    inset.plot(\n",
        "        N_VALUES,\n",
        "        log_values,\n",
        "        marker=\"o\",\n",
        "        linewidth=2.0,\n",
        "        markersize=5,\n",
        "        color=colors[label],\n",
        "    )\n",
        "inset.set_yscale(\"log\")\n",
        "inset.set_ylim(bottom=log_floor)\n",
        "inset.set_xlim(min(N_VALUES), max(N_VALUES))\n",
        "inset.set_title(\"Log scale; zeros omitted\", fontsize=9)\n",
        "inset.grid(axis=\"both\", alpha=0.25)\n",
        "inset.tick_params(axis=\"both\", labelsize=8)\n",
        "inset.patch.set_alpha(0.96)\n",
        "\n",
        "fig.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "refs",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Références\n",
        "\n",
        "1. [E. Bäumer *et al.*, « Transformée de Fourier quantique à l'aide de circuits dynamiques », arXiv:2403.09514; *Physical Review Letters* **133**, 150602 (2024)](https://arxiv.org/abs/2403.09514)\n",
        "\n",
        "2. [Présentation d' Qiskit Functions](/docs/guides/functions)\n",
        "\n",
        "3. [Limites de l'informatique quantique pour les variables extensibles](/docs/guides/stretch#qiskit-runtime-limitations)\n",
        "\n",
        "4. [IBM Quantum codes d'erreur : 6073](https://ibm.biz/error_codes#6073)\n",
        "\n",
        "5. [Exécuter des tâches par lots](/docs/guides/run-jobs-batch)\n",
        "\n",
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Etapes suivantes\n",
        "\n",
        "* Consultez le [guide Orbit](/docs/guides/quantum-elements-orbit) et la documentation [de référence de l'API](/docs/api/functions/quantum-elements-orbit).\n",
        "* Essayez un autre backend, une mise en page différente, ou testez d'autres séquences de découplage dynamique compatibles avec « orbit » en modifiant l'option `dd_strategy` . Gardez à l'esprit qu'en raison du caractère expérimental des circuits dynamiques, vous devez être attentif aux modes de défaillance possibles (voir [\\[3\\]](#references) et [\\[4\\]](#references) ). Si vous constatez des valeurs d'étirement négatives [\\[3\\]](#references), essayez une séquence DD plus courte (avec moins d'impulsions). Si vous rencontrez [\\[4\\]](#references), augmentez `NUM_BATCH_JOBS`, réduisez `M`, ou réduisez les valeurs les plus élevées de `N_VALUES`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 1,
    "qpuSeconds": 120
  },
  "nbformat": 4,
  "nbformat_minor": 5
}