Lumaktaw sa pangunahing nilalaman

Operator backpropagation (OBP) para sa pagtatantya ng expectation values

Tantya sa paggamit: 4 na minuto sa isang Heron r3 processor (PAALALA: Ito ay tantya lamang. Maaaring mag-iba ang iyong runtime.)

Mga layunin sa pag-aaral

Pagkatapos ng tutorial na ito, dapat maunawaan ng mga gumagamit ang:

  • Paano gamitin ang qiskit-addon-obp upang mabawasan ang lalim ng quantum circuit sa halaga ng mas maraming circuit execution
  • Paano gamitin ang qiskit-addon-utils upang bumuo ng mga XYZ Hamiltonian at ang kanilang mga time-evolution circuit

Mga kinakailangan

Iminumungkahi naming maging pamilyar ang mga gumagamit sa mga sumusunod na paksa bago gawin ang tutorial na ito:

  • Paggamit ng Estimator primitive upang kalkulahin ang mga expectation value ng isang observable

Background

Ang operator backpropagation ay isang teknikang nagsasangkot ng pag-absorb ng mga operasyon mula sa dulo ng isang quantum circuit tungo sa sinusukat na observable, na karaniwang nagpapababa ng lalim ng circuit sa halaga ng karagdagang mga term sa observable. Ang layunin ay mag-backpropagate ng kasing dami ng circuit hangga't maaari nang hindi pinapayagang lumaki nang labis ang observable. Ang isang Qiskit-based na implementasyon ay available sa OBP Qiskit addon. Basahin ang kaukulang dokumentasyon para sa karagdagang impormasyon.

Isaalang-alang ang isang halimbawang circuit kung saan ang isang observable O=PcPPO = \sum_P c_P P ay susukatiin, kung saan ang PP ay mga Pauli at ang cPc_P ay mga coefficient. Itakda natin ang circuit bilang isang unitary UU na maaaring lohikal na hatiin sa U=UCUQU = U_C U_Q tulad ng ipinapakita sa figure sa ibaba.

Circuit diagram showing Uq followed by Uc

Ang operator backpropagation ay nag-aabsorb ng unitary UCU_C sa observable sa pamamagitan ng pag-evolve nito bilang O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Sa madaling salita, ang bahagi ng computation ay ginagawa nang classical sa pamamagitan ng evolusyon ng observable mula sa OO tungo sa OO'. Ang orihinal na problema ay maaari ngayong muling iformula bilang pagsukat ng observable OO' para sa bagong mas mababang lalim na circuit na ang unitary ay UQU_Q.

Ang unitary UCU_C ay kinakatawan bilang isang bilang ng mga slice UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. May maraming paraan para tukuyin ang isang slice. Halimbawa, sa halimbawang circuit sa itaas, ang bawat layer ng RzzR_{zz} at bawat layer ng RxR_x gates ay maaaring ituring bilang indibidwal na slice. Ang backpropagation ay nagsasangkot ng pagkalkula ng O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s nang classical. Ang bawat slice UsU_s ay maaaring katawanin bilang Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), kung saan ang PsP_s ay isang nn-qubit Pauli at ang θs\theta_s ay isang scalar. Madaling ma-verify na

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

Sa halimbawa sa itaas, kung {P,Ps}=0\{P,P_s\} = 0, kailangan nating magsagawa ng dalawang quantum circuit, sa halip na isa, upang kalkulahin ang expectation value. Samakatuwid, ang backpropagation ay maaaring magdagdag ng bilang ng mga term sa observable, na nagreresulta sa mas mataas na bilang ng circuit execution. Isang paraan upang payagan ang mas malalim na backpropagation sa circuit, habang pinipigilan ang operator mula sa paglaki nang labis, ay ang mag-truncate ng mga term na may maliliit na coefficient, sa halip na idagdag ang mga ito sa operator. Halimbawa, sa halimbawa sa itaas, maaari tayong pumili na mag-truncate ng term na nagsasangkot ng PsPP_sP kung sakaling ang θs\theta_s ay sapat na maliit. Ang pag-truncate ng mga term ay maaaring magresulta sa mas kakaunting quantum circuit na isasagawa, ngunit ang paggawa nito ay nagreresulta ng ilang error sa panghuling pagkalkula ng expectation value na proporsyonal sa magnitude ng mga coefficient ng mga na-truncate na term.

Requirements

Bago simulan ang tutorial na ito, siguraduhing mayroon ka ng mga sumusunod na naka-install:

  • Qiskit SDK v2.0 o mas bago, na may suporta sa visualization
  • Qiskit Runtime v0.22 o mas bago (pip install qiskit-ibm-runtime)
  • OBP Qiskit addon 0.3 o mas bago (pip install qiskit-addon-obp)
  • Qiskit addon utils 0.3 o mas bago (pip install qiskit-addon-utils)

Setup

# 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

Small-scale simulator example

Isinasagawa ng tutorial na ito ang isang Qiskit pattern para sa pag-simulate ng quantum dynamics ng isang Heisenberg spin chain gamit ang OBP Qiskit addon. Pansinin na sa isang noiseless simulator, ang expectation value na nakuha nang may at walang backpropagation ay magiging pareho.

Step 1: Map classical inputs to a quantum problem

Map the time-evolution of a quantum Heisenberg model to a quantum experiment

Una, gagamitin natin ang function na generate_xyz_hamiltonian mula sa qiskit-addon-utils upang bumuo ng isang Heisenberg-like Hamiltonian sa isang ibinigay na connectivity graph. Ang graph na ito ay maaaring rustworkx.PyGraph o CouplingMap. Sa sumusunod, gagamitin natin ang isang linear chain na CouplingMap ng 10 qubit.

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

Susunod, bubuo tayo ng isang Pauli operator na nagmomodelo ng Heisenberg XYZ Hamiltonian:

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}),}

kung saan ang G(V,E)G(V,E) ay ang graph ng coupling map. Para sa tutorial na ito, ginamit natin ang Jx,Jy,JzJ_x, J_y, J_z na π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2}, ayon sa pagkakasunod, at hx,hy,hzh_x, h_y, h_z na π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}, ayon sa pagkakasunod.

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

Mula sa qubit operator, maaari tayong lumikha ng quantum circuit na nagmomodelo ng time evolution nito. Ginamit namin ang generate_time_evolution_circuit na may Lie Trotter decomposition upang buuin ang time evolution circuit.

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

Step 2: Optimize problem for quantum hardware execution

Create circuit slices to backpropagate

Ang function na backpropagate ay nagba-backpropagate ng buong circuit slice nang sabay-sabay. Kaya naman, ang pagpili kung paano maghiwa ay maaaring magkaroon ng epekto sa kung gaano kahusay gumagana ang backpropagation para sa isang partikular na problema. Dito, pagsasamahin natin ang mga gate ng parehong uri sa mga slice gamit ang function na slice_by_depth.

Para sa mas detalyadong talakayan tungkol sa circuit slicing, tingnan ang how-to guide na ito ng package na 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.

Constrain how large the operator can grow during backpropagation

Sa panahon ng backpropagation, ang bilang ng mga term sa operator ay karaniwang lalapit sa 2L2^L nang mabilis, kung saan ang LL ay ang bilang ng mga slice. Kapag ang dalawang term sa operator ay hindi nag-commute nang qubit-wise, kailangan natin ng hiwalay na mga circuit upang makuha ang mga expectation value na tumutugma sa kanila. Halimbawa, kung mayroon tayong dalawang-qubit observable na O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, dahil ang [XX,IX]=0[XX,IX] = 0, ang pagsukat sa isang basis lamang ay sapat upang kalkulahin ang mga expectation value para sa dalawang term na ito. Gayunpaman, ang IZIZ ay anti-commute sa dalawang term, kaya kailangan natin ng hiwalay na basis measurement upang kalkulahin ang expectation value ng IZIZ. Sa madaling salita, kailangan natin ng dalawang circuit sa halip na isa upang kalkulahin ang O\langle O \rangle. Habang dumarami ang bilang ng mga term sa operator, may posibilidad na dumarami rin ang kinakailangang bilang ng circuit execution.

Ang laki ng operator ay maaaring limitahan sa pamamagitan ng pagtukoy ng operator_budget kwarg ng function na backpropagate, na tumatanggap ng isang instance ng OperatorBudget.

Upang kontrolin ang dami ng karagdagang resources (bilang ng circuit execution, at samakatuwid ang kinakailangang QPU time) na inilalaan, pinapaghihigpitan natin ang maximum na bilang ng qubit-wise commuting Pauli groups na pinapayagang magkaroon ang backpropagated observable. Dito, tinukoy natin na ang backpropagation ay dapat tumigil kapag ang bilang ng qubit-wise commuting Pauli groups sa operator ay lumampas sa walo.

op_budget = OperatorBudget(max_qwc_groups=8)

Backpropagate slices from the circuit

Una, tinukoy natin ang observable bilang MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, kung saan ang NN ay ang bilang ng mga qubit. Mag-backpropagate tayo ng mga slice mula sa time-evolution circuit hanggang sa ang mga term sa observable ay hindi na maaaring pagsamahin sa walong o mas kaunting qubit-wise commuting Pauli groups.

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

Sa ibaba, makikita mo na nag-backpropagate tayo ng anim na slice, at ang mga term ay pinagsama sa anim at hindi walong grupo. Ito ay nagpapahiwatig na ang pag-backpropagate pa ng isang slice ay magiging sanhi ng paglampas ng bilang ng mga Pauli group sa walong. Maaari nating i-verify na ito ang kaso sa pamamagitan ng pagsusuri sa ibinalik na metadata. Pansinin din na sa bahaging ito, ang circuit transformation ay eksakto. Ibig sabihin, walang mga term ng bagong observable na OO' ang na-truncate. Ang backpropagated circuit at ang backpropagated operator ay nagbibigay ng eksaktong kinalabasan tulad ng orihinal na circuit at operator.

# 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

Para sa small-scale na halimbawa sa isang simulator, hindi tayo gagamit ng truncation. Ito ay dahil sa kawalan ng ingay, ang circuit na may at walang backpropagation ay nagdudulot ng parehong resulta, at ang truncation ay nagpapalala ng resulta dahil sa dagdag na approximation.

Transpile the circuits into the basis gate set

Ngayon, ini-transpile natin ang parehong orihinal at backpropagated na mga circuit sa basis gate ng backend. Hindi natin kailangang mag-transpile sa aktwal na backend dahil magpapatakbo tayo sa isang simulator para sa maliit na 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)

Step 3: Execute using Qiskit primitives

Una, lumilikha tayo ng dalawang Primitive Unified Blocs (PUB) na tumutugma sa orihinal na circuit at sa backpropagated na circuit. Pagkatapos, pinapatakbo natin ang mga pub sa isang ideal na Estimator upang makuha ang mga expectation value.

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

Step 4: Post-process and return result in desired classical format

Ngayon nakukuha natin ang mga expectation value ng orihinal at backpropagated na mga circuit.

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

Tulad ng inaasahan, ang dalawang expectation value ay nagkakaayon. Dahil nagpapatakbo tayo sa isang noiseless statevector simulator, ang backpropagation ay isang eksaktong transformation ng circuit-observable pair, kaya ang orihinal at backpropagated na mga workflow ay dapat magbigay ng parehong halaga ng MZM_Z. Ang benepisyo ng backpropagation ay magiging malinaw lamang sa maingay na hardware, kung saan ang mas maikling backpropagated na circuit ay nakakaipon ng mas kaunting error, tulad ng ipinapakita sa large-scale hardware example sa ibaba.

Large-scale hardware example

Kapag nagde-develop ng eksperimento, kapaki-pakinabang na magsimula sa isang maliit na circuit upang mas madaling ma-visualize at ma-simulate. Ngayon, titingnan natin ang operator backpropagation para sa isang 50-qubit na Heisenberg Hamiltonian na may parehong set ng mga halaga para sa mga parameter na JJ at hh at ang parehong observable na MZM_Z, ngunit para sa apat na Trotter step. Ang ideal na expectation value sa sukatang ito ay hindi maaaring kalkulahin sa pamamagitan ng brute force, kaya gumagamit tayo ng tensor network at nakakakuha ng ideal na expectation value na 0.89\simeq 0.89.

Kasama ang backpropagation, sa large-scale na halimbawa na ito, ipinakilala rin natin ang backpropagation na may truncation. Idealmente, gusto nating mag-backpropagate hangga't maaari upang mabawasan ang lalim ng epektibong circuit. Gayunpaman, madalas itong humahantong sa malaking bilang ng mga non-commuting na term sa na-update na observable, na nagpapataas ng quantum overhead. Kaya naman, maaari tayong mag-eliminate ng mga term sa observable na may maliliit na coefficient gamit ang isang paraan na tinatawag na truncation. Habang pinapahintulutan ng truncation ang mas malalim na propagation sa pamamagitan ng pagbabawas ng bilang ng mga term sa na-update na observable, nagdudulot din ito ng ilang approximation. Samakatuwid, kinakailangang limitahan ang truncation sa loob ng ilang hangganan upang ang approximation error ay hindi mangibabaw sa pagbabawas ng ingay na nakuha mula sa mas malalim na backpropagation.

Upang limitahan ang dami ng truncation, naglalaan tayo ng error budget para sa bawat slice pati na rin ang kabuuang error budget para sa buong backpropagated na circuit gamit ang function na setup_budget. Tinitiyak nito na ang truncation ay kontrolado para sa bawat slice pati na rin para sa buong circuit. Tingnan din ang guide na ito para sa iba pang paraan ng paglalaan ng 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

Next steps

Kung nahanap mong kawili-wili ang gawaing ito, maaaring interesado ka sa mga sumusunod na materyal:

Recommendations