Skip to main content
IBM Quantum Platform

Rétropropagation de l'opérateur (OBP) pour l'estimation des valeurs attendues

Durée estimée : 4 minutes sur un processeur Heron r3 (REMARQUE : il s'agit uniquement d'une estimation.) (Votre temps d'exécution peut varier.)


Résultats d'apprentissage

À l'issue de ce tutoriel, les utilisateurs devraient être en mesure de :

  • Comment utiliser qiskit-addon-obp pour réduire la profondeur du circuit quantique au prix d'une augmentation du nombre d'exécutions du circuit
  • Comment utiliser qiskit-addon-utils pour construire des hamiltoniens XYZ et leurs circuits d'évolution temporelle

Prérequis

Nous recommandons aux utilisateurs de se familiariser avec les sujets suivants avant de suivre ce tutoriel :

  • Utilisation de la primitive « Estimator » pour calculer les valeurs attendues d'une variable observable

Arrière-plan

La rétropropagation des opérateurs est une technique qui consiste à intégrer les opérations de la fin d'un circuit quantique dans l'observable mesurée, ce qui réduit généralement la profondeur du circuit au prix de l'ajout de termes supplémentaires dans l'observable. L'objectif est de faire remonter le plus de couches du circuit possible sans que la valeur observable ne devienne trop importante. Une implémentation basée sur Qiskit est disponible dans l'extension OBP Qiskit. Pour plus d'informations, consultez la documentation correspondante.

Prenons l'exemple d'un circuit dans lequel on souhaite mesurer une grandeur observable O=PcPPO = \sum_P c_P P, où PP sont des quantités de Pauli et cPc_P sont des coefficients. Considérons ce circuit comme une seule « UU » unitaire, qui peut être divisée logiquement en « U=UCUQU = U_C U_Q », comme le montre la figure ci-dessous.

Schéma du circuit montrant Uq suivi de Uc

La rétropropagation des opérateurs intègre l' UCU_C e unitaire dans l'observable en la faisant évoluer selon la relation O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. En d'autres termes, une partie du calcul est effectuée de manière classique par l'évolution de l'observable de OO à OO'. Le problème initial peut désormais être reformulé comme la mesure de l'observable OO' pour le nouveau circuit de profondeur réduite dont l'opérateur unitaire est UQU_Q.

L' UCU_C e unitaire est représentée par un certain nombre de tranches UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. Il existe plusieurs façons de définir une tranche. Par exemple, dans le circuit présenté ci-dessus, chaque couche de transistors de type « RzzR_{zz} » et chaque couche de transistors de type « RxR_x » peuvent être considérées comme une tranche distincte. La rétropropagation implique généralement le calcul d' O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s. Chaque tranche UsU_s peut être représentée sous la forme Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), où PsP_s est un état de Pauli à nn qubits et θs\theta_s est un scalaire. Il est facile de vérifier que

UsPUs=Pif [P,Ps]=0,U_s^{\dagger} P U_s = P \qquad \text{if} ~[P,P_s] = 0, UsPUs=cos(θs)P+isin(θs)PsPif {P,Ps}=0U_s^{\dagger} P U_s = \qquad cos(\theta_s)P + i sin(\theta_s)P_sP \qquad \text{if} ~\{P,P_s\} = 0

Dans l'exemple ci-dessus, si l'on a {P,Ps}=0\{P,P_s\} = 0, il faut alors exécuter deux circuits quantiques, au lieu d'un seul, pour calculer la valeur d'espérance. Par conséquent, la rétropropagation pourrait augmenter le nombre de termes dans l'observable, ce qui entraînerait une augmentation du nombre d'exécutions du circuit. Une façon de permettre une rétropropagation plus profonde dans le circuit, tout en évitant que l'opérateur ne devienne trop volumineux, consiste à tronquer les termes dont les coefficients sont faibles, plutôt que de les ajouter à l'opérateur. Par exemple, dans l'exemple ci-dessus, on pourrait choisir de tronquer le terme contenant PsPP_sP, à condition que θs\theta_s soit suffisamment petit. Le troncage des termes peut permettre de réduire le nombre de circuits quantiques à exécuter, mais cela entraîne une erreur dans le calcul de la valeur d'espérance finale, proportionnelle à l'amplitude des coefficients des termes tronqués.


Exigences

Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :

  • Qiskit SDK v2.0 ou version ultérieure, avec prise en charge de la visualisation
  • Qiskit Runtime v0.22 ou version ultérieure (pip install qiskit-ibm-runtime)
  • Module complémentaire OBP Qiskit 0.3 ou version ultérieure (pip install qiskit-addon-obp)
  • Module complémentaire Qiskit utils 0.3 ou version ultérieure (pip install qiskit-addon-utils)

Configuration

import numpy as np
import matplotlib.pyplot as plt

from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter

from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
    generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget

from rustworkx.visualization import graphviz_draw

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions

Exemple de simulateur à petite échelle

Ce tutoriel met en œuvre un modèle Qiskit permettant de simuler la dynamique quantique d'une chaîne de spins de Heisenberg à l'aide de l 'extension OBP pour Qiskit. Il convient de noter que, dans un simulateur sans bruit, la valeur attendue obtenue avec et sans rétropropagation sera identique.

Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique

Mettre en correspondance l'évolution temporelle d'un modèle quantique de Heisenberg avec une expérience quantique

Tout d'abord, nous utiliserons la generate_xyz_hamiltonian fonction de qiskit-addon-utils pour générer un hamiltonien de type Heisenberg sur un graphe de connectivité donné. Ce graphique peut être soit un graphique de type « rustworkx.PyGraph », soit un graphique de type « CouplingMap ». Dans ce qui suit, nous utiliserons une chaîne CouplingMap linéaire de 10 qubits.

num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")

Output:

Output of the previous code cell

Ensuite, nous générons un opérateur de Pauli modélisant un hamiltonien de Heisenberg XYZ :

H^XYZ=(j,k)E(Jxσjxσkx+Jyσjyσky+Jzσjzσkz)+jV(hxσjx+hyσjy+hzσjz),{\hat{\mathcal{H}}_{XYZ} = \sum_{(j,k)\in E} (J_{x} \sigma_j^{x} \sigma_{k}^{x} + J_{y} \sigma_j^{y} \sigma_{k}^{y} + J_{z} \sigma_j^{z} \sigma_{k}^{z}) + \sum_{j\in V} (h_{x} \sigma_j^{x} + h_{y} \sigma_j^{y} + h_{z} \sigma_j^{z}),}

G(V,E)G(V,E) est la courbe de la fonction de couplage. Pour ce tutoriel, nous avons utilisé Jx,Jy,JzJ_x, J_y, J_z et π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2}, respectivement, ainsi que hx,hy,hzh_x, h_y, h_z et π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}, respectivement.

# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
    coupling_map,
    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)

Output:

SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
              coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
 0.34906585+0.j])

À partir de l'opérateur de qubit, on peut générer un circuit quantique qui modélise son évolution temporelle. Nous avons utilisé generate_time_evolution_circuit la décomposition de Lie-Trotter pour construire le circuit d'évolution temporelle.

circuit = generate_time_evolution_circuit(
    hamiltonian,
    time=0.2,
    synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

Output:

Output of the previous code cell

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique

Créer des tranches de circuit pour rétropropager

La backpropagate fonction effectue une rétropropagation par tranches de circuit entières. Par conséquent, le choix du découpage peut avoir une incidence sur l'efficacité de la rétropropagation pour un problème donné. Ici, nous allons regrouper les portes de même type en tranches à l'aide de la slice_by_depth fonction.

Pour en savoir plus sur le découpage de circuits, consultez le guide pratique fourni avec le qiskit-addon-utils logiciel.

slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")

Output:

Separated the circuit into 18 slices.

Limiter la taille maximale que l'opérateur peut atteindre pendant la rétropropagation

Au cours de la rétropropagation, le nombre de termes de l'opérateur tend généralement à atteindre rapidement la valeur d' 2L2^L, où LL correspond au nombre de tranches. Lorsque deux termes de l'opérateur ne commutent pas au niveau des qubits, il faut recourir à des circuits distincts pour obtenir les valeurs d'espérance qui leur correspondent. Par exemple, si nous avons une observable à deux qubits O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, alors, puisque [XX,IX]=0[XX,IX] = 0, une mesure dans une seule base suffit pour calculer les valeurs attendues de ces deux termes. Cependant, l' IZIZ e est anticommutatif avec les deux autres termes; il nous faut donc une mesure de base distincte pour calculer la valeur attendue de l' IZIZ. En d'autres termes, il nous faut deux circuits au lieu d'un seul pour calculer l' O\langle O \rangle. À mesure que le nombre de termes de l'opérateur augmente, il est possible que le nombre d'exécutions de circuits nécessaires augmente également.

La taille de l'opérateur peut être limitée en spécifiant le kwarg operator_budget de la fonction backpropagate , qui accepte un OperatorBudget instance.

Afin de contrôler la quantité de ressources supplémentaires allouées (nombre d'itérations du circuit et, par conséquent, temps de calcul requis par le QPU), nous limitons le nombre maximal de groupes de Pauli commutatifs au niveau des qubits que l'observable rétropropagée est autorisée à posséder. Nous précisons ici que la rétropropagation doit s'arrêter lorsque le nombre de groupes de Pauli commutant au niveau des qubits dans l'opérateur dépasse huit.

op_budget = OperatorBudget(max_qwc_groups=8)

Rétropropagation des tranches à partir du circuit

Tout d'abord, nous spécifions que l'observable doit être MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, NN étant le nombre de qubits. Nous rétropropagerons les tranches du circuit d'évolution temporelle jusqu'à ce que les termes de l'observable ne puissent plus être combinés en huit groupes de Pauli commutés dans le sens du qubit ou moins.

observable = SparsePauliOp.from_sparse_list(
    [("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
    num_qubits=num_qubits,
)
observable

Output:

SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
              coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
 0.1+0.j, 0.1+0.j])

Vous verrez ci-dessous que nous avons effectué une rétropropagation sur six tranches, et que les termes ont été regroupés en six groupes et non en huit. Cela signifie que la rétropropagation d'une tranche supplémentaire ferait passer le nombre de groupes de Pauli au-delà de huit. Nous pouvons vérifier que c'est bien le cas en examinant les métadonnées renvoyées. Notez également que, dans cette partie, la transformation du circuit est exacte. En d'autres termes, aucun terme de la nouvelle observable OO’ n'a été tronqué. Le circuit à rétropropagation et l'opérateur à rétropropagation donnent exactement le même résultat que le circuit et l'opérateur d'origine.

# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
    observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)

print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
    f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
    f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
    f"Note that backpropagating one more slice would result in "
    f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
    f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)

Output:

Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
Output of the previous code cell

Pour cet exemple à petite échelle sur un simulateur, nous n'utiliserons pas la troncature. En effet, en l'absence de bruit, le circuit avec ou sans rétropropagation donne le même résultat, et la troncature détériore ce résultat en raison de l'approximation supplémentaire.

Transposer les circuits dans le jeu de portes de base

Nous transpilons désormais à la fois le circuit d'origine et le circuit issu de la rétropropagation vers la porte de base du backend. Il n'est pas nécessaire de transcompiler sur le backend proprement dit, puisque nous allons utiliser un simulateur pour la petite instance.

service = QiskitRuntimeService()
backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=133
)
print(backend)

Output:

<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
    optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)

Étape 3 : Exécutez à l'aide d' Qiskit primitives

Tout d'abord, nous créons deux blocs unifiés primitifs (PUB) correspondant au circuit d'origine et au circuit issu de la rétropropagation. Nous exécutons ensuite les publications sur un estimateur idéal afin d'obtenir les valeurs attendues.

pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)

Étape 4 : Traiter les données et restituer le résultat dans le format classique souhaité

Nous obtenons maintenant les valeurs attendues des circuits d'origine et des circuits obtenus par rétropropagation.

primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()
methods = [
    "No backpropagation",
    "Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)

Output:

Text(0, 0.5, '$M_Z$')
Output of the previous code cell

Comme on pouvait s'y attendre, les deux valeurs attendues concordent. Comme nous utilisons un simulateur de vecteurs d'état sans bruit, la rétropropagation constitue une transformation exacte de la paire circuit-observable; par conséquent, les flux de travail d'origine et ceux issus de la rétropropagation doivent produire la même valeur d' MZM_Z. L'avantage de la rétropropagation ne se manifeste que sur du matériel bruyant, où le circuit rétropropagé, plus court, accumule moins d'erreurs, comme l'illustre l'exemple de matériel à grande échelle ci-dessous.


Exemple de matériel à grande échelle

Lorsqu'on met au point une expérience, il est utile de commencer par un petit circuit afin de faciliter la visualisation et la simulation. Nous allons maintenant examiner la rétropropagation des opérateurs pour un hamiltonien de Heisenberg à 50 qubits, avec le même ensemble de valeurs pour les paramètres d' JJ et d' hh, ainsi que la même observable MZM_Z, mais en effectuant quatre étapes de Trotter. La valeur attendue idéale à cette échelle ne pouvant être calculée par une méthode de force brute, nous utilisons un réseau de tenseurs et obtenons une valeur attendue idéale de 0.89\simeq 0.89.

Outre la rétropropagation, nous présentons également, dans cet exemple à grande échelle, la rétropropagation avec troncature. Idéalement, nous souhaitons effectuer une rétropropagation aussi loin que possible afin de réduire la profondeur du circuit effectif. Cependant, cela entraîne souvent l'apparition d'un grand nombre de termes non commutatifs dans l'observable mise à jour, ce qui augmente la charge quantique. Nous pouvons donc éliminer les termes observables dont les coefficients sont faibles en recourant à une technique appelée « troncature ». Si la troncature permet une meilleure propagation en réduisant le nombre de termes dans l'observable mise à jour, elle introduit également une certaine approximation. Il est donc nécessaire de limiter la troncature afin que l'erreur d'approximation ne l'emporte pas sur la réduction du bruit obtenue grâce à une propagation en arrière plus profonde.

Afin de limiter l'ampleur de la troncature, nous attribuons un budget d'erreur à chaque tranche ainsi qu'un budget d'erreur total pour l'ensemble du circuit soumis à la rétropropagation à l'aide de la setup_budget fonction. Cela permet de garantir que la coupure est contrôlée aussi bien pour chaque tranche que pour l'ensemble du circuit. Consultez également ce guide pour découvrir d'autres méthodes de répartition du budget.

num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)

hamiltonian = generate_xyz_hamiltonian(
    coupling_map,
    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)

# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
    hamiltonian,
    time=0.2,
    synthesis=LieTrotter(reps=4),
)

# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
    [("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
    num_qubits,
)

slices = slice_by_depth(circuit, max_slice_depth=1)

# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
    max_error_total=0.03, max_error_per_slice=0.005
)

# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
    observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)

# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
    observable,
    slices,
    operator_budget=op_budget,
    truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
    remaining_slices_trunc, include_barriers=False
)

# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)

isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)

isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
    isa_bp_circuit_trunc.layout
)

# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
    f"2-qubit depth without backpropagation: "
    f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
    f"2-qubit depth with backpropagation: "
    f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
    f"2-qubit depth with backpropagation and truncation: "
    f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)

pubs = [
    (isa_circuit, isa_observable),
    (isa_bp_circuit, isa_bp_observable),
    (isa_bp_circuit_trunc, isa_bp_observable_trunc),
]

# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)

estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)

# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()

std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()

Output:

2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")

Output:

Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
    "No backpropagation",
    "Backpropagation",
    "Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)

Output:

Text(0, 0.5, '$M_Z$')
Output of the previous code cell

Etapes suivantes

Si ce travail vous a paru intéressant, les ressources suivantes pourraient vous intéresser :

Recommandations
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.