Przejdź do głównej treści

Wsteczna propagacja operatora (OBP) do estymacji wartości oczekiwanych

Szacowane zużycie zasobów: 4 minuty na procesorze Heron r3 (UWAGA: To tylko szacunek. Rzeczywisty czas wykonania może się różnić.)

Cele kształcenia

Po ukończeniu tego samouczka użytkownicy powinni rozumieć:

  • Jak używać qiskit-addon-obp do redukcji głębokości obwodu kwantowego kosztem zwiększonej liczby wykonań obwodu
  • Jak używać qiskit-addon-utils do konstruowania hamiltonianów XYZ i odpowiadających im obwodów ewolucji czasowej

Wymagania wstępne

Zalecamy, aby użytkownicy zapoznali się z następującymi tematami przed przystąpieniem do tego samouczka:

  • Używanie prymitywu Estimator do obliczania wartości oczekiwanych obserwowalnej

Wprowadzenie

Wsteczna propagacja operatora to technika polegająca na wchłanianiu operacji z końca obwodu kwantowego do mierzonej obserwowalnej, co ogólnie zmniejsza głębokość obwodu kosztem dodatkowych składników w obserwowalnej. Celem jest wsteczne propagowanie jak największej części obwodu bez dopuszczenia do nadmiernego wzrostu obserwowalnej. Implementacja oparta na Qiskit jest dostępna w rozszerzeniu OBP Qiskit. Więcej informacji można znaleźć w odpowiedniej dokumentacji.

Rozważmy przykładowy obwód, dla którego mierzona jest obserwowalna O=PcPPO = \sum_P c_P P, gdzie PP są Paulimi, a cPc_P są współczynnikami. Oznaczmy obwód jako pojedynczą unitarną UU, którą można logicznie podzielić na U=UCUQU = U_C U_Q, jak pokazano na rysunku poniżej.

Diagram obwodu przedstawiający Uq a następnie Uc

Wsteczna propagacja operatora wchłania unitarną UCU_C do obserwowalnej poprzez jej ewolucję jako O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Innymi słowy, część obliczenia jest wykonywana klasycznie poprzez ewolucję obserwowalnej z OO do OO'. Pierwotny problem można teraz sformułować na nowo jako pomiar obserwowalnej OO' dla nowego obwodu o mniejszej głębokości, którego unitarna to UQU_Q.

Unitarna UCU_C jest reprezentowana jako liczba przedziałów UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. Istnieje wiele sposobów definiowania przedziału. Na przykład, w powyższym przykładowym obwodzie każda warstwa bramek RzzR_{zz} i każda warstwa bramek RxR_x może być traktowana jako osobny przedział. Wsteczna propagacja polega na klasycznym obliczaniu O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s. Każdy przedział UsU_s można przedstawić jako Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), gdzie PsP_s jest nn-qubitowym Paulim, a θs\theta_s jest skalarem. Łatwo można sprawdzić, że

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

W powyższym przykładzie, jeśli {P,Ps}=0\{P,P_s\} = 0, musimy wykonać dwa obwody kwantowe zamiast jednego, aby obliczyć wartość oczekiwaną. Dlatego wsteczna propagacja może zwiększyć liczbę składników w obserwowalnej, co prowadzi do większej liczby wykonań obwodu. Jednym ze sposobów umożliwienia głębszej wstecznej propagacji do obwodu przy jednoczesnym zapobieganiu nadmiernemu wzrostowi operatora jest obcinanie składników o małych współczynnikach zamiast dodawania ich do operatora. Na przykład w powyższym przykładzie można zdecydować się na obcięcie składnika zawierającego PsPP_sP, pod warunkiem że θs\theta_s jest wystarczająco mały. Obcinanie składników może skutkować mniejszą liczbą obwodów kwantowych do wykonania, jednak wiąże się z pewnym błędem w końcowym obliczeniu wartości oczekiwanej proporcjonalnym do wielkości współczynników obciętych składników.

Wymagania

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące pakiety:

  • Qiskit SDK v2.0 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.22 lub nowszy (pip install qiskit-ibm-runtime)
  • Rozszerzenie OBP Qiskit 0.3 lub nowsze (pip install qiskit-addon-obp)
  • Qiskit addon utils 0.3 lub nowsze (pip install qiskit-addon-utils)

Konfiguracja

# 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

Przykład na małą skalę z symulatorem

Ten samouczek implementuje wzorzec Qiskit do symulacji dynamiki kwantowej łańcucha spinów Heisenberga przy użyciu rozszerzenia OBP Qiskit. Zauważ, że w symulatorze bez szumów wartość oczekiwana uzyskana z wsteczną propagacją i bez niej będzie taka sama.

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

Odwzorowanie ewolucji czasowej kwantowego modelu Heisenberga na eksperyment kwantowy

Najpierw użyjemy funkcji generate_xyz_hamiltonian z qiskit-addon-utils do wygenerowania hamiltonianu podobnego do Heisenberga na danym grafie połączeń. Tym grafem może być rustworkx.PyGraph lub CouplingMap. Poniżej użyjemy liniowej mapy sprzężeń CouplingMap złożonej z 10 qubitów.

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

Wyjście poprzedniej komórki kodu

Następnie generujemy operator Pauliego modelujący hamiltoniana Heisenberga 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}),}

gdzie G(V,E)G(V,E) jest grafem mapy sprzężeń. W tym samouczku użyliśmy Jx,Jy,JzJ_x, J_y, J_z równych odpowiednio π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2}, oraz hx,hy,hzh_x, h_y, h_z równych odpowiednio π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}.

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

Na podstawie operatora qubitowego możemy wygenerować obwód kwantowy modelujący jego ewolucję czasową. Użyliśmy generate_time_evolution_circuit z dekompozycją Lie Trottera do skonstruowania obwodu ewolucji czasowej.

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

Wyjście poprzedniej komórki kodu

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Tworzenie przedziałów obwodu do wstecznej propagacji

Funkcja backpropagate propaguje wstecz całe przedziały obwodu naraz. Dlatego wybór sposobu podziału na przedziały może mieć wpływ na efektywność wstecznej propagacji dla danego problemu. Tutaj pogrupujemy bramki tego samego typu w przedziały przy użyciu funkcji slice_by_depth.

Bardziej szczegółowe omówienie podziału obwodu na przedziały można znaleźć w tym przewodniku pakietu 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.

Ograniczanie wzrostu operatora podczas wstecznej propagacji

Podczas wstecznej propagacji liczba składników w operatorze będzie generalnie szybko zbliżać się do 2L2^L, gdzie LL jest liczbą przedziałów. Gdy dwa składniki w operatorze nie komutują qubitowo, potrzebujemy osobnych obwodów do uzyskania odpowiadających im wartości oczekiwanych. Na przykład, jeśli mamy dwuqubitową obserwowalną O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, to ponieważ [XX,IX]=0[XX,IX] = 0, pomiar w jednej bazie wystarcza do obliczenia wartości oczekiwanych dla tych dwóch składników. Jednak IZIZ antykomutuje z pozostałymi dwoma składnikami, więc potrzebujemy osobnego pomiaru bazowego do obliczenia wartości oczekiwanej IZIZ. Innymi słowy, potrzebujemy dwóch obwodów zamiast jednego, aby obliczyć O\langle O \rangle. Wraz ze wzrostem liczby składników w operatorze istnieje możliwość, że wymagana liczba wykonań obwodu również wzrośnie.

Rozmiar operatora można ograniczyć, podając argument operator_budget funkcji backpropagate, który przyjmuje instancję OperatorBudget.

Aby kontrolować ilość dodatkowych zasobów (liczba wykonań obwodów, a co za tym idzie wymagany czas QPU) przydzielonych do obliczeń, ograniczamy maksymalną liczbę grup Paulich komutujących qubitowo, którą może mieć propagowana wstecznie obserwowalna. Tutaj określamy, że wsteczna propagacja powinna się zatrzymać, gdy liczba qubitowo komutujących grup Paulich w operatorze przekroczy osiem.

op_budget = OperatorBudget(max_qwc_groups=8)

Wsteczne propagowanie przedziałów z obwodu

Najpierw określamy obserwowalną jako MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, gdzie NN jest liczbą qubitów. Będziemy propagować wstecz przedziały z obwodu ewolucji czasowej do momentu, gdy składniki obserwowalnej nie będą mogły być już połączone w osiem lub mniej qubitowo komutujących grup Paulich.

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

Poniżej zobaczysz, że propagowaliśmy wstecz sześć przedziałów, a składniki zostały połączone w sześć, a nie osiem grup. Oznacza to, że propagowanie wstecz jeszcze jednego przedziału spowodowałoby przekroczenie ośmiu grup Paulich. Możemy to zweryfikować, analizując zwrócone metadane. Zauważ też, że w tej części transformacja obwodu jest dokładna. To znaczy, żaden ze składników nowej obserwowalnej OO' nie został obcięty. Propagowany wstecznie obwód i propagowana wstecznie obserwowalna dają dokładnie taki sam wynik jak pierwotny obwód i obserwowalna.

# 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:

Wyjście poprzedniej komórki kodu

W przypadku małoskalowego przykładu na symulatorze nie użyjemy obcinania. Wynika to z faktu, że przy braku szumów obwód z wsteczną propagacją i bez niej prowadzi do tego samego wyniku, a obcinanie pogarsza wynik ze względu na dodaną aproksymację.

Transpilacja obwodów do docelowego zestawu bramek

Teraz transpilujemy zarówno pierwotny, jak i propagowany wstecznie obwód do bramek bazowych backendu. Nie musimy transpilować na rzeczywistym backendzie, ponieważ będziemy uruchamiać na symulatorze dla małej instancji.

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)

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

Najpierw tworzymy dwa Primitive Unified Blocs (PUB) odpowiadające pierwotnemu obwodowi i propagowanemu wstecznie obwodowi. Następnie wykonujemy PUBy na idealnym Estimatorze, aby uzyskać wartości oczekiwane.

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

Krok 4: Post-processing i zwrócenie wyników w żądanym formacie klasycznym

Teraz uzyskujemy wartości oczekiwane pierwotnego i propagowanego wstecznie obwodu.

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$')

Wyjście poprzedniej komórki kodu

Zgodnie z oczekiwaniami obie wartości oczekiwane są zgodne. Ponieważ uruchamiamy na symulatorze stanu wektora bez szumów, wsteczna propagacja jest dokładną transformacją pary obwód-obserwowalna, więc pierwotny i propagowany wstecznie przepływ pracy muszą dawać tę samą wartość MZM_Z. Korzyść z wstecznej propagacji staje się widoczna dopiero na zaszumionym sprzęcie, gdzie krótszy propagowany wstecznie obwód akumuluje mniej błędów, co zilustrowano w poniższym przykładzie dużej skali na sprzęcie.

Przykład dużej skali na sprzęcie

Podczas opracowywania eksperymentu przydatne jest rozpoczęcie od małego obwodu, aby ułatwić wizualizacje i symulacje. Teraz przyjrzymy się wstecznej propagacji operatora dla 50-qubitowego hamiltonianu Heisenberga z tym samym zestawem wartości parametrów JJ i hh oraz tą samą obserwowalną MZM_Z, ale dla czterech kroków Trottera. Idealnej wartości oczekiwanej w tej skali nie można obliczyć metodą brute force, dlatego używamy sieci tensorowej i uzyskujemy idealną wartość oczekiwaną równą 0.89\simeq 0.89.

Wraz z wsteczną propagacją, w tym przykładzie dużej skali wprowadzamy również wsteczną propagację z obcinaniem. Idealnie chcemy propagować wstecz jak najwięcej, aby zmniejszyć głębokość efektywnego obwodu. Jednak często prowadzi to do dużej liczby niekomutujących składników w zaktualizowanej obserwowalnej, co zwiększa narzut kwantowy. Dlatego możemy eliminować składniki obserwowalnej z małymi współczynnikami za pomocą praktyki zwanej obcinaniem. Podczas gdy obcinanie pozwala na głębszą propagację poprzez redukcję liczby składników w zaktualizowanej obserwowalnej, wprowadza również pewną aproksymację. Dlatego konieczne jest ograniczenie obcinania w pewnych granicach, aby błąd aproksymacji nie przeważył nad redukcją szumów uzyskaną dzięki głębszej wstecznej propagacji.

Aby ograniczyć ilość obcinania, przydzielamy budżet błędu dla każdego przedziału, jak również całkowity budżet błędu dla całego propagowanego wstecznie obwodu, używając funkcji setup_budget. Zapewnia to kontrolę nad obcinaniem zarówno dla każdego przedziału, jak i dla całego obwodu. Inne sposoby alokacji budżetu można znaleźć w tym przewodniku.

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$')

Wyjście poprzedniej komórki kodu

Kolejne kroki

Jeśli ta praca wydaje ci się interesująca, możesz zainteresować się następującymi materiałami:

Rekomendacje