Aller au contenu principal

Rétropropagation d'opérateurs (OBP) pour l'estimation des valeurs attendues

Estimation d'utilisation : 4 minutes sur un processeur Heron r3 (REMARQUE : Ceci est uniquement une estimation. Votre temps d'exécution peut varier.)

Résultats d'apprentissage

Après avoir suivi ce tutoriel, les utilisateurs devraient comprendre :

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

Prérequis

Nous suggérons aux utilisateurs de se familiariser avec les sujets suivants avant de suivre ce tutoriel :

  • Utiliser la primitive Estimator pour calculer les valeurs attendues d'un observable

Contexte

La rétropropagation d'opérateurs est une technique qui consiste à absorber des opérations depuis la fin d'un circuit quantique dans l'observable mesuré, réduisant généralement la profondeur du circuit au prix de termes supplémentaires dans l'observable. L'objectif est de rétropropager autant du circuit que possible sans permettre à l'observable de devenir trop volumineux. Une implémentation basée sur Qiskit est disponible dans l'addon OBP de Qiskit. Consultez la documentation correspondante pour plus d'informations.

Considérons un exemple de circuit pour lequel un observable O=PcPPO = \sum_P c_P P doit être mesuré, où PP sont des opérateurs de Pauli et cPc_P sont des coefficients. Notons le circuit comme un unitaire unique UU, qui peut être logiquement partitionné en U=UCUQU = U_C U_Q comme illustré dans la figure ci-dessous.

Circuit diagram showing Uq followed by Uc

La rétropropagation d'opérateurs absorbe l'unitaire UCU_C dans l'observable en le faisant évoluer comme O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Autrement dit, une partie du calcul est effectuée classiquement à travers l'évolution de l'observable de OO à OO'. Le problème original peut maintenant être reformulé comme la mesure de l'observable OO' pour le nouveau circuit de profondeur réduite dont l'unitaire est UQU_Q.

L'unitaire UCU_C est représenté sous forme d'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 ci-dessus, chaque couche de portes RzzR_{zz} et chaque couche de portes RxR_x peut être considérée comme une tranche individuelle. La rétropropagation implique le calcul classique de 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 comme Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), où PsP_s est un opérateur 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 {P,Ps}=0\{P,P_s\} = 0, alors nous devons exécuter deux circuits quantiques, au lieu d'un seul, pour calculer la valeur attendue. Par conséquent, la rétropropagation peut augmenter le nombre de termes dans l'observable, entraînant un nombre plus élevé d'exécutions de circuits. Une façon de permettre une rétropropagation plus profonde dans le circuit, tout en empêchant l'opérateur de devenir trop volumineux, est de tronquer les termes avec de petits coefficients, plutôt que de les ajouter à l'opérateur. Par exemple, dans le cas ci-dessus, on pourrait choisir de tronquer le terme impliquant PsPP_sP à condition que θs\theta_s soit suffisamment petit. La troncature de termes peut entraîner moins de circuits quantiques à exécuter, mais cela introduit une certaine erreur dans le calcul final de la valeur attendue, proportionnelle à la magnitude des coefficients des termes tronqués.

Prérequis techniques

Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :

  • Qiskit SDK v2.0 ou ultérieur, avec le support de la visualisation
  • Qiskit Runtime v0.22 ou ultérieur (pip install qiskit-ibm-runtime)
  • Addon OBP de Qiskit 0.3 ou ultérieur (pip install qiskit-addon-obp)
  • Utilitaires des addons Qiskit 0.3 ou ultérieur (pip install qiskit-addon-utils)

Configuration

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
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 simulateur à petite échelle

Ce tutoriel implémente un pattern Qiskit pour simuler la dynamique quantique d'une chaîne de spins de Heisenberg en utilisant l'addon OBP de Qiskit. Notez que dans un simulateur sans bruit, la valeur attendue obtenue avec et sans rétropropagation sera la même.

Étape 1 : Mapper les entrées classiques vers un problème quantique

Mapper l'évolution temporelle d'un modèle quantique de Heisenberg vers une expérience quantique

Premièrement, nous utiliserons la fonction generate_xyz_hamiltonian de qiskit-addon-utils pour générer un hamiltonien de type Heisenberg sur un graphe de connectivité donné. Ce graphe peut être soit un rustworkx.PyGraph, soit un CouplingMap. Dans ce qui suit, nous utiliserons un CouplingMap en chaîne 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 of the previous code cell

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

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 le graphe du coupling map. Pour ce tutoriel, nous avons utilisé Jx,Jy,JzJ_x, J_y, J_z égaux à π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2} respectivement, et hx,hy,hzh_x, h_y, h_z égaux à π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)
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, nous pouvons générer un circuit quantique qui modélise son évolution temporelle. Nous avons utilisé generate_time_evolution_circuit avec 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 of the previous code cell

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

Créer des tranches de circuit pour la rétropropagation

La fonction backpropagate rétropropage des tranches entières de circuit à la fois. Par conséquent, le choix du découpage peut avoir un impact sur les performances de la rétropropagation pour un problème donné. Ici, nous regrouperons les portes du même type en tranches en utilisant la fonction slice_by_depth.

Pour une discussion plus détaillée sur le découpage de circuits, consultez ce guide pratique du package qiskit-addon-utils.

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

Contraindre la taille maximale de l'opérateur pendant la rétropropagation

Pendant la rétropropagation, le nombre de termes dans l'opérateur approchera généralement 2L2^L rapidement, où LL est le nombre de tranches. Lorsque deux termes de l'opérateur ne commutent pas qubit par qubit, nous avons besoin de circuits séparés pour obtenir les valeurs attendues correspondantes. Par exemple, si nous avons un 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, IZIZ anti-commute avec les deux autres termes, donc nous avons besoin d'une mesure de base séparée pour calculer la valeur attendue de IZIZ. En d'autres termes, nous avons besoin de deux circuits au lieu d'un seul pour calculer O\langle O \rangle. À mesure que le nombre de termes dans l'opérateur augmente, il est possible que le nombre requis d'exécutions de circuits augmente également.

La taille de l'opérateur peut être bornée en spécifiant le paramètre operator_budget de la fonction backpropagate, qui accepte une instance de OperatorBudget.

Pour contrôler la quantité de ressources supplémentaires (nombre d'exécutions de circuits, et donc le temps QPU requis) allouées, nous restreignons le nombre maximal de groupes de Pauli commutant qubit par qubit que l'observable rétropropagé est autorisé à avoir. Ici, nous spécifions que la rétropropagation doit s'arrêter lorsque le nombre de groupes de Pauli commutant qubit par qubit dans l'opérateur dépasse huit.

op_budget = OperatorBudget(max_qwc_groups=8)

Rétropropager les tranches du circuit

Nous spécifions d'abord l'observable comme étant 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 ou moins de Pauli commutant qubit par qubit.

observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observable
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])

Ci-dessous, tu verras que nous avons rétropropagé six tranches, et que les termes ont été combinés en six groupes et non huit. Cela implique que rétropropager une tranche supplémentaire ferait dépasser le nombre de groupes de Pauli au-delà de huit. Nous pouvons vérifier que c'est le cas en inspectant les métadonnées retournées. Note également que dans cette partie, la transformation du circuit est exacte. C'est-à-dire qu'aucun terme du nouvel observable OO' n'a été tronqué. Le circuit rétropropagé et l'opérateur rétropropagé donnent exactement le même résultat que le circuit et l'opérateur originaux.

# 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)
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 l'exemple à petite échelle sur un simulateur, nous n'utiliserons pas la troncature. En effet, en l'absence de bruit, le circuit avec et sans rétropropagation conduit au même résultat, et la troncature dégrade le résultat en raison de l'approximation introduite.

Transpiler les circuits dans l'ensemble de portes de base

Nous transpitons maintenant les circuits original et rétropropagé dans la porte de base du backend. Nous n'avons pas besoin de transpiler sur le backend réel puisque nous allons exécuter sur un simulateur pour la petite instance.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<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écuter en utilisant les primitives Qiskit

Premièrement, nous créons deux Primitive Unified Blocs (PUBs) correspondant au circuit original et au circuit rétropropagé. Ensuite, nous exécutons les PUBs sur un Estimator idéal pour 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 : Post-traitement et retour du résultat au format classique souhaité

Nous obtenons maintenant les valeurs attendues des circuits original et rétropropagé.

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)
Text(0, 0.5, '$M_Z$')

Output of the previous code cell

Comme attendu, les deux valeurs attendues concordent. Puisque nous exécutons sur un simulateur de vecteur d'état sans bruit, la rétropropagation est une transformation exacte de la paire circuit-observable, de sorte que les flux de travail original et rétropropagé doivent produire la même valeur de MZM_Z. L'avantage de la rétropropagation n'apparaît que sur du matériel bruité, où le circuit rétropropagé plus court accumule moins d'erreurs, comme illustré dans l'exemple matériel à grande échelle ci-dessous.

Exemple matériel à grande échelle

Lors du développement d'une expérience, il est utile de commencer avec un petit circuit pour faciliter les visualisations et les simulations. Nous examinons maintenant la rétropropagation d'opérateurs pour un hamiltonien de Heisenberg à 50 qubits avec le même ensemble de valeurs pour les paramètres JJ et hh et le même observable MZM_Z, mais pour quatre pas de Trotter. La valeur attendue idéale à cette échelle ne peut pas être calculée par une méthode de force brute, nous utilisons donc un réseau tensoriel et obtenons la valeur attendue idéale à 0,89\simeq 0,89.

Avec la rétropropagation, dans cet exemple à grande échelle, nous introduisons également la rétropropagation avec troncature. Idéalement, nous souhaitons rétropropager autant que possible pour réduire la profondeur du circuit effectif. Cependant, cela conduit souvent à un grand nombre de termes non commutants dans l'observable mis à jour, augmentant la surcharge quantique. Par conséquent, nous pouvons éliminer les termes de l'observable avec de petits coefficients en utilisant une pratique appelée troncature. Bien que la troncature permette une propagation plus profonde en réduisant le nombre de termes dans l'observable mis à jour, elle introduit également une certaine approximation. Il est donc nécessaire de restreindre la troncature dans certaines limites afin que l'erreur d'approximation ne dépasse pas la réduction du bruit obtenue grâce à une rétropropagation plus profonde.

Pour restreindre la quantité de troncature, nous allouons un budget d'erreur pour chaque tranche ainsi qu'un budget d'erreur total sur l'ensemble du circuit rétropropagé en utilisant la fonction setup_budget. Cela garantit que la troncature est contrôlée pour chaque tranche ainsi que pour l'ensemble du circuit. Consultez également ce guide pour d'autres façons d'allouer le 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()
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}")
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)
Text(0, 0.5, '$M_Z$')

Output of the previous code cell

Prochaines étapes

Si tu as trouvé ce travail intéressant, tu pourrais être intéressé par le matériel suivant :

Recommendations