Sari la conținutul principal

Retropropagarea operatorului (OBP) pentru estimarea valorilor de așteptare

Estimare de utilizare: 4 minute pe un procesor Heron r3 (NOTĂ: Aceasta este doar o estimare. Timpul tău de execuție poate varia.)

Rezultate de învățare

După parcurgerea acestui tutorial, utilizatorii ar trebui să înțeleagă:

  • Cum să folosească qiskit-addon-obp pentru a reduce adâncimea Circuit-ului cuantic cu prețul unui număr crescut de execuții de circuit
  • Cum să folosească qiskit-addon-utils pentru a construi Hamiltonieni XYZ și Circuit-urile lor de evoluție temporală

Cerințe prealabile

Sugerăm că utilizatorii să fie familiari cu următoarele subiecte înainte de a parcurge acest tutorial:

  • Utilizarea primitivei Estimator pentru a calcula valorile de așteptare ale unui observabil

Fundal

Retropropagarea operatorului este o tehnică care implică absorbția operațiilor de la sfârșitul unui Circuit cuantic în observabilul măsurat, reducând în general adâncimea Circuit-ului cu prețul unor termeni suplimentari în observabil. Scopul este de a retropropaga cât mai mult din Circuit posibil fără a permite observabilului să crească prea mult. O implementare bazată pe Qiskit este disponibilă în addon-ul OBP Qiskit. Citește documentația corespunzătoare pentru mai multe informații.

Să considerăm un exemplu de circuit pentru care un observabil O=PcPPO = \sum_P c_P P urmează să fie măsurat, unde PP sunt Pauli și cPc_P sunt coeficienți. Să notăm Circuit-ul ca un singur unitar UU care poate fi partiționat logic în U=UCUQU = U_C U_Q după cum se arată în figura de mai jos.

Circuit diagram showing Uq followed by Uc

Retropropagarea operatorului absoarbe unitarul UCU_C în observabil prin evoluția sa ca O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Cu alte cuvinte, o parte a calculului este efectuată clasic prin evoluția observabilului de la OO la OO'. Problema originală poate fi acum reformulată ca măsurarea observabilului OO' pentru noul Circuit cu adâncime mai mică al cărui unitar este UQU_Q.

Unitarul UCU_C este reprezentat ca un număr de felii UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. Există mai multe moduri de a defini o felie. De exemplu, în Circuit-ul exemplu de mai sus, fiecare strat de porți RzzR_{zz} și fiecare strat de porți RxR_x poate fi considerat o felie individuală. Retropropagarea implică calculul clasic O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s. Fiecare felie UsU_s poate fi reprezentată ca Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), unde PsP_s este un Pauli pe nn Qubiți și θs\theta_s este un scalar. Este ușor de verificat că

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

În exemplul de mai sus, dacă {P,Ps}=0\{P,P_s\} = 0, atunci trebuie să executăm două Circuit-uri cuantice, în loc de unul, pentru a calcula valoarea de așteptare. Prin urmare, retropropagarea poate crește numărul de termeni din observabil, ducând la un număr mai mare de execuții de circuit. O modalitate de a permite o retropropagare mai adâncă în Circuit, prevenind în același timp creșterea excesivă a operatorului, este să trunchiezi termenii cu coeficienți mici, în loc să îi adaugi operatorului. De exemplu, în exemplul de mai sus, s-ar putea alege să trunchiezi termenul care implică PsPP_sP dacă θs\theta_s este suficient de mic. Trunchierea termenilor poate rezulta în mai puține Circuit-uri cuantice de executat, dar face acest lucru cu prețul unei erori în calculul final al valorii de așteptare, proporțională cu magnitudinea coeficienților termenilor trunchiați.

Cerințe

Înainte de a începe acest tutorial, asigură-te că ai instalat următoarele:

  • Qiskit SDK v2.0 sau ulterior, cu suport visualization
  • Qiskit Runtime v0.22 sau ulterior (pip install qiskit-ibm-runtime)
  • OBP Qiskit addon 0.3 sau ulterior (pip install qiskit-addon-obp)
  • Qiskit addon utils 0.3 sau ulterior (pip install qiskit-addon-utils)

Configurare

# 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

Exemplu la scară mică pe simulator

Acest tutorial implementează un pattern Qiskit pentru simularea dinamicii cuantice a unui lanț de spin Heisenberg folosind addon-ul OBP Qiskit. Rețineți că într-un simulator fără zgomot, valoarea de așteptare obținută cu și fără backpropagare va fi aceeași.

Pasul 1: Maparea intrărilor clasice la o problemă cuantică

Maparea evoluției temporale a unui model cuantic Heisenberg la un experiment cuantic

Mai întâi, vom folosi funcția generate_xyz_hamiltonian din qiskit-addon-utils pentru a genera un Hamiltonian de tip Heisenberg pe un graf de conectivitate dat. Acest graf poate fi fie un rustworkx.PyGraph, fie un CouplingMap. În cele ce urmează, vom folosi un CouplingMap de tip lanț liniar cu 10 Qubiți.

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

Apoi, generăm un operator Pauli care modelează un Hamiltonian 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}),}

unde G(V,E)G(V,E) este graful hărții de cuplare. Pentru acest tutorial, am folosit Jx,Jy,JzJ_x, J_y, J_z ca π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2}, respectiv, și hx,hy,hzh_x, h_y, h_z ca π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}, respectiv.

# 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])

Din operatorul de qubit, putem genera un circuit cuantic care modelează evoluția sa temporală. Am folosit generate_time_evolution_circuit cu descompunerea Lie Trotter pentru a construi Circuit-ul de evoluție temporală.

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

Pasul 2: Optimizarea problemei pentru execuția pe hardware cuantic

Crearea feliilor de circuit pentru retropropagare

Funcția backpropagate retropropagă felii întregi de circuit la un moment dat. Prin urmare, alegerea modului de tăiere în felii poate influența cât de bine funcționează retropropagarea pentru o problemă dată. Aici, vom grupa porțile de același tip în felii folosind funcția slice_by_depth.

Pentru o discuție mai detaliată despre tăierea în felii a Circuit-urilor, consultați acest ghid practic al pachetului 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.

Constrângerea dimensiunii maxime pe care operatorul o poate atinge în timpul retropropagării

În timpul retropropagării, numărul de termeni din operator va tinde în general să se apropie rapid de 2L2^L, unde LL este numărul de felii. Când doi termeni din operator nu comutează qubit-wise, avem nevoie de circuit-uri separate pentru a obține valorile de așteptare corespunzătoare. De exemplu, dacă avem un observabil pe doi Qubiți O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, atunci deoarece [XX,IX]=0[XX,IX] = 0, măsurarea într-o singură bază este suficientă pentru a calcula valorile de așteptare pentru acești doi termeni. Cu toate acestea, IZIZ anti-comutează cu ceilalți doi termeni, deci avem nevoie de o măsurare în bază separată pentru a calcula valoarea de așteptare a IZIZ. Cu alte cuvinte, avem nevoie de două Circuit-uri în loc de unul pentru a calcula O\langle O \rangle. Pe măsură ce numărul de termeni din operator crește, există posibilitatea ca numărul necesar de execuții de circuit să crească și el.

Dimensiunea operatorului poate fi limitată specificând argumentul keyword operator_budget al funcției backpropagate, care acceptă o instanță OperatorBudget.

Pentru a controla cantitatea de resurse suplimentare (numărul de execuții de circuit, și deci timpul QPU necesar) alocate, restricționăm numărul maxim de grupuri Pauli care comutează qubit-wise pe care observabilul retropropagat are voie să le aibă. Aici specificăm că retropropagarea ar trebui să se oprească când numărul de grupuri Pauli care comutează qubit-wise din operator depășește opt.

op_budget = OperatorBudget(max_qwc_groups=8)

Retropropagarea feliilor din Circuit

Mai întâi specificăm observabilul ca MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, NN fiind numărul de Qubiți. Vom retropropaga felii din Circuit-ul de evoluție temporală până când termenii din observabil nu mai pot fi combinați în opt sau mai puține grupuri Pauli care comutează qubit-wise.

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])

Mai jos vei vedea că am retropropagat șase felii, iar termenii au fost combinați în șase și nu opt grupuri. Asta implică că retropropagarea încă unei felii ar determina numărul de grupuri Pauli să depășească opt. Putem verifica că acesta este cazul inspectând metadatele returnate. De asemenea, rețineți că în această porțiune transformarea Circuit-ului este exactă. Adică, niciun termen al noului observabil OO' nu a fost trunchiat. Circuit-ul retropropagat și operatorul retropropagat dau exact același rezultat ca Circuit-ul original și operatorul original.

# 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

Pentru exemplul la scară mică pe un simulator, nu vom folosi trunchiere. Aceasta deoarece în absența zgomotului, Circuit-ul cu și fără backpropagare duce la același rezultat, iar trunchierea înrăutățește rezultatul din cauza aproximării adăugate.

Transpilarea Circuit-urilor în setul de porți de bază

Acum transpilăm atât Circuit-ul original cât și Circuit-ul retropropagat în porțile de bază ale backend-ului. Nu trebuie să transpilăm pe backend-ul real deoarece vom rula pe un simulator pentru instanța mică.

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)

Pasul 3: Execuție folosind primitivele Qiskit

Mai întâi, creăm două Primitive Unified Blocs (PUB-uri) corespunzătoare Circuit-ului original și Circuit-ului retropropagat. Apoi executăm PUB-urile pe un Estimator ideal pentru a obține valorile de așteptare.

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

Pasul 4: Post-procesare și returnarea rezultatului în formatul clasic dorit

Acum obținem valorile de așteptare ale Circuit-urilor original și retropropagat.

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

Conform așteptărilor, cele două valori de așteptare sunt de acord. Deoarece rulăm pe un simulator statevector fără zgomot, backpropagarea este o transformare exactă a perechii circuit-observabil, deci fluxurile de lucru original și retropropagat trebuie să producă aceeași valoare a MZM_Z. Beneficiul backpropagării devine evident abia pe hardware cu zgomot, unde Circuit-ul retropropagat mai scurt acumulează mai puțin zgomot, după cum este ilustrat în exemplul de hardware la scară mare de mai jos.

Exemplu de hardware la scară mare

Când se dezvoltă un experiment, este util să se înceapă cu un circuit mic pentru a facilita vizualizările și simulările. Acum analizăm backpropagarea operatorului pentru un Hamiltonian Heisenberg cu 50 de Qubiți, cu aceleași valori ale parametrilor JJ și hh și același observabil MZM_Z, dar pentru patru pași Trotter. Valoarea de așteptare ideală la această scară nu poate fi calculată printr-o metodă de forță brută, deci folosim o rețea tensorială și obținem că valoarea de așteptare ideală este 0.89\simeq 0.89.

Alături de backpropagare, în acest exemplu la scară mare introducem și backpropagarea cu trunchiere. În mod ideal, dorim să retropropagăm cât mai mult posibil pentru a reduce adâncimea Circuit-ului efectiv. Cu toate acestea, acest lucru duce adesea la un număr mare de termeni necomutanți în observabilul actualizat, crescând efortul cuantic. Prin urmare, putem elimina termenii observabilului cu coeficienți mici printr-o practică numită trunchiere. Deși trunchierea permite o propagare mai adâncă prin reducerea numărului de termeni în observabilul actualizat, introduce și o anumită aproximare. Prin urmare, este necesar să restricționăm trunchierea în anumite limite astfel încât eroarea de aproximare să nu depășească reducerea zgomotului obținută din backpropagarea mai adâncă.

Pentru a restricționa cantitatea de trunchiere, alocăm un buget de eroare pentru fiecare felie, precum și bugetul total de eroare pe întregul Circuit retropropagat, folosind funcția setup_budget. Aceasta asigură că trunchierea este controlată atât pentru fiecare felie, cât și pentru întregul Circuit. Consultați și acest ghid pentru alte modalități de alocare a bugetului.

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

Pași următori

Dacă ai găsit acest lucru interesant, s-ar putea să fii interesat de materialele următoare:

Recomandări