Перейти до основного вмісту

Зворотне поширення оператора (OBP) для оцінки значень очікування

Оцінка використання: 4 хвилини на процесорі Heron r3 (ПРИМІТКА: Це лише оцінка. Твій час виконання може відрізнятися.)

Результати навчання

Після проходження цього посібника користувачі повинні розуміти:

  • Як використовувати qiskit-addon-obp для зменшення глибини квантового ланцюга за рахунок збільшення кількості виконань ланцюга
  • Як використовувати qiskit-addon-utils для побудови гамільтоніанів XYZ та їхніх ланцюгів еволюції в часі

Передумови

Ми рекомендуємо ознайомитися з такими темами перед проходженням цього посібника:

  • Використання примітиву Estimator для обчислення значень очікування спостережуваної величини

Теоретична основа

Зворотне поширення оператора — це техніка, яка передбачає поглинання операцій з кінця квантового ланцюга у вимірювану спостережувану величину, що зазвичай зменшує глибину ланцюга за рахунок додаткових термів у спостережуваній величині. Мета полягає у зворотному поширенні якомога більшої частини ланцюга без надмірного зростання спостережуваної величини. Реалізація на основі Qiskit доступна в доповненні OBP Qiskit addon. Читай відповідну документацію для отримання додаткової інформації.

Розглянемо приклад ланцюга, для якого необхідно виміряти спостережувану величину O=PcPPO = \sum_P c_P P, де PP — це паулі, а cPc_P — коефіцієнти. Позначимо ланцюг як єдиний унітарний оператор UU, який можна логічно розділити на U=UCUQU = U_C U_Q, як показано на малюнку нижче.

Circuit diagram showing Uq followed by Uc

Зворотне поширення оператора поглинає унітарний оператор UCU_C у спостережувану величину шляхом її еволюції як O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Іншими словами, частина обчислення виконується класично через еволюцію спостережуваної величини від OO до OO'. Початкову задачу тепер можна переформулювати як вимірювання спостережуваної величини OO' для нового ланцюга меншої глибини, унітарний оператор якого є UQU_Q.

Унітарний оператор UCU_C представлений у вигляді кількох зрізів UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1. Існує декілька способів визначення зрізу. Наприклад, у наведеному вище прикладі ланцюга кожен шар RzzR_{zz} та кожен шар вентилів RxR_x можна розглядати як окремий зріз. Зворотне поширення передбачає обчислення O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s класичним способом. Кожен зріз UsU_s можна представити як Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}), де PsP_s є nn-кубітним Паулі, а θs\theta_s — скаляром. Легко перевірити, що

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

У наведеному вище прикладі, якщо {P,Ps}=0\{P,P_s\} = 0, тоді нам потрібно виконати два квантових ланцюги замість одного для обчислення значення очікування. Таким чином, зворотне поширення може збільшити кількість термів у спостережуваній величині, що призводить до більшої кількості виконань ланцюга. Одним із способів дозволити глибше зворотне поширення у ланцюг, запобігаючи надмірному зростанню оператора, є усічення термів з малими коефіцієнтами замість їх додавання до оператора. Наприклад, у наведеному вище прикладі можна вибрати усічення терму, що містить PsPP_sP, за умови, що θs\theta_s достатньо малий. Усічення термів може призвести до меншої кількості квантових ланцюгів для виконання, але це призводить до певної похибки у кінцевому обчисленні значення очікування, пропорційної величині коефіцієнтів усічених термів.

Вимоги

Перед початком цього посібника переконайся, що встановив наступне:

  • Qiskit SDK v2.0 або пізніше, з підтримкою візуалізації
  • Qiskit Runtime v0.22 або пізніше (pip install qiskit-ibm-runtime)
  • OBP Qiskit addon 0.3 або пізніше (pip install qiskit-addon-obp)
  • Утиліти доповнення Qiskit 0.3 або пізніше (pip install qiskit-addon-utils)

Налаштування

# 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

Маломасштабний приклад на симуляторі

Цей посібник реалізує патерн Qiskit для симуляції квантової динаміки спінового ланцюга Гейзенберга за допомогою OBP Qiskit addon. Зверни увагу, що в безшумному симуляторі значення очікування, отримані з зворотним поширенням та без нього, будуть однаковими.

Крок 1: Відображення класичних вхідних даних на квантову задачу

Відображення еволюції в часі квантової моделі Гейзенберга на квантовий експеримент

Спочатку ми використаємо функцію generate_xyz_hamiltonian з qiskit-addon-utils для генерації гамільтоніана типу Гейзенберга на заданому графі зв'язності. Цей граф може бути або rustworkx.PyGraph, або CouplingMap. Далі ми використаємо лінійний ланцюг CouplingMap з 10 кубітів.

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

Далі ми генеруємо оператор Паулі, що моделює гамільтоніан Гейзенберга 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) — граф карти зв'язності. У цьому посібнику ми використали Jx,Jy,JzJ_x, J_y, J_z рівними π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2} відповідно, а hx,hy,hzh_x, h_y, h_z рівними π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])

З кубітного оператора ми можемо згенерувати квантовий ланцюг, який моделює його еволюцію в часі. Ми використали generate_time_evolution_circuit з розкладом Трожого (Lie Trotter) для побудови ланцюга еволюції в часі.

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

Крок 2: Оптимізація задачі для виконання на квантовому обладнанні

Створення зрізів ланцюга для зворотного поширення

Функція backpropagate виконує зворотне поширення цілих зрізів ланцюга за один раз. Тому вибір способу розділення на зрізи може вплинути на ефективність зворотного поширення для даної задачі. Тут ми згрупуємо вентилі за глибиною у зрізи за допомогою функції slice_by_depth.

Для більш детального обговорення розділення ланцюга на зрізи ознайомся з цим практичним посібником пакета 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.

Обмеження розміру оператора під час зворотного поширення

Під час зворотного поширення кількість термів в операторі, як правило, швидко наближатиметься до 2L2^L, де LL — кількість зрізів. Коли два терми в операторі не комутують покубітно, нам потрібні окремі ланцюги для отримання значень очікування, що відповідають їм. Наприклад, якщо ми маємо 2-кубітну спостережувану величину O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX, то оскільки [XX,IX]=0[XX,IX] = 0, вимірювання в одному базисі достатньо для обчислення значень очікування для цих двох термів. Однак, IZIZ антикомутує з двома іншими термами, тому нам потрібне окреме вимірювання в базисі для обчислення значення очікування IZIZ. Іншими словами, нам потрібно два ланцюги замість одного для обчислення O\langle O \rangle. Зі збільшенням кількості термів в операторі існує можливість, що необхідна кількість виконань ланцюга також збільшується.

Розмір оператора може бути обмежений шляхом вказівки аргументу operator_budget функції backpropagate, яка приймає екземпляр OperatorBudget.

Щоб контролювати кількість додаткових ресурсів (кількість виконань ланцюга, а отже й необхідного QPU часу), ми обмежуємо максимальну кількість покубітно комутуючих груп Паулі, яку дозволено мати зворотно поширеній спостережуваній величині. Тут ми вказуємо, що зворотне поширення має зупинитися, коли кількість покубітно комутуючих груп Паулі в операторі перевищить вісім.

op_budget = OperatorBudget(max_qwc_groups=8)

Зворотне поширення зрізів з ланцюга

Спочатку ми вказуємо спостережувану величину як MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle, де NN — кількість кубітів. Ми виконаємо зворотне поширення зрізів з ланцюга еволюції в часі, поки терми у спостережуваній величині більше не можна буде об'єднати у вісім або менше покубітно комутуючих груп Паулі.

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

Нижче ти побачиш, що ми виконали зворотне поширення шести зрізів, і терми були об'єднані у шість, а не вісім груп. Це означає, що зворотне поширення ще одного зрізу призведе до того, що кількість груп Паулі перевищить вісім. Ми можемо переконатися в цьому, перевіривши повернені метадані. Також зауваж, що в цій частині перетворення ланцюга є точним — жоден з термів нової спостережуваної величини OO' не було усічено. Ланцюг зі зворотним поширенням та оператор зі зворотним поширенням дають точний результат, як початковий ланцюг та оператор.

# 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 {len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in {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

Для маломасштабного прикладу на симуляторі ми не будемо використовувати усічення. Це тому, що за відсутності шуму ланцюг з зворотним поширенням та без нього дає однаковий результат, а усічення погіршує результат через додаткове наближення.

Транспіляція ланцюгів у набір базисних вентилів

Тепер ми транспілюємо як оригінальний, так і ланцюг зі зворотним поширенням у базисний набір вентилів бекенду. Нам не потрібно транспілювати на реальний бекенд, оскільки ми будемо запускати на симуляторі для маломасштабного прикладу.

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)

Крок 3: Виконання за допомогою примітивів Qiskit

Спочатку ми створюємо два Примітивних уніфікованих блоки (PUB) — для оригінального ланцюга та ланцюга зі зворотним поширенням. Потім ми виконуємо PUB на ідеальному Estimator для отримання значень очікування.

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

Крок 4: Постобробка та повернення результату до бажаного класичного формату

Тепер ми отримуємо значення очікування для оригінального ланцюга та ланцюга зі зворотним поширенням.

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

Як і очікувалося, два значення очікування збігаються. Оскільки ми запускаємо на безшумному симуляторі векторів стану, зворотне поширення є точним перетворенням пари ланцюг-спостережувана величина, тому оригінальні та зворотно поширені робочі процеси повинні давати однакове значення MZM_Z. Переваги зворотного поширення стають очевидними лише на зашумленому обладнанні, де коротший ланцюг зі зворотним поширенням накопичує менше похибок, як показано у великомасштабному прикладі на обладнанні нижче.

Великомасштабний приклад на обладнанні

При розробці експерименту корисно починати з малого ланцюга для полегшення візуалізації та симуляції. Тепер ми розглянемо зворотне поширення оператора для 50-кубітного гамільтоніана Гейзенберга з тим самим набором значень параметрів JJ та hh і тією самою спостережуваною величиною MZM_Z, але для чотирьох кроків Трожого. Ідеальне значення очікування у цьому масштабі не може бути обчислено методом грубої сили, тому ми використовуємо тензорну мережу і отримуємо ідеальне значення очікування 0.89\simeq 0.89.

Разом зі зворотним поширенням у цьому великомасштабному прикладі ми також вводимо зворотне поширення з усіченням. В ідеалі ми хочемо виконувати зворотне поширення якомога глибше для зменшення глибини ефективного ланцюга. Однак це часто призводить до великої кількості некомутуючих термів в оновленій спостережуваній величині, збільшуючи квантові накладні витрати. Тому ми можемо видаляти терми спостережуваної величини з малими коефіцієнтами за допомогою практики усічення. Хоча усічення дозволяє більш глибоке поширення за рахунок зменшення кількості термів в оновленій спостережуваній величині, воно також вносить деяке наближення. Тому необхідно обмежувати усічення в певних межах, щоб похибка наближення не переважала зменшення шуму від більш глибокого зворотного поширення.

Щоб обмежити кількість усічення, ми виділяємо бюджет похибки для кожного зрізу, а також загальний бюджет похибки для всього ланцюга зі зворотним поширенням за допомогою функції setup_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: {isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: {isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: {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

Наступні кроки

Якщо ця робота тебе зацікавила, можливо, тебе зацікавить наступний матеріал:

Рекомендації