انتقل إلى المحتوى الرئيسي

الانتشار الخلفي للمؤثرات (OBP) لتقدير قيم التوقع

تقدير الاستخدام: 4 دقائق على معالج Heron r3 (ملاحظة: هذا تقدير فحسب. قد يختلف وقت التشغيل الفعلي لديك.)

نتائج التعلم

بعد إتمام هذا الدليل التعليمي، يجب أن يفهم المستخدمون:

  • كيفية استخدام qiskit-addon-obp لتقليل عمق الدائرة الكمية على حساب زيادة عدد تنفيذات الدائرة
  • كيفية استخدام qiskit-addon-utils لبناء هاملتونيات XYZ ودوائر تطورها الزمني

المتطلبات المسبقة

نقترح أن يكون المستخدمون على دراية بالمواضيع التالية قبل الشروع في هذا الدليل:

  • استخدام الأداة الأولية Estimator لحساب قيم التوقع للمؤثرات

الخلفية النظرية

الانتشار الخلفي للمؤثرات هو أسلوب يقوم على استيعاب العمليات من نهاية الدائرة الكمية في المؤثر المقاس، مما يُقلّص عمق الدائرة بشكل عام على حساب زيادة عدد الحدود في المؤثر. والهدف هو إعادة الانتشار الخلفي لأكبر قدر ممكن من الدائرة دون السماح للمؤثر بالنمو بشكل مفرط. يتوفر تطبيق قائم على Qiskit في إضافة OBP الخاصة بـ Qiskit. اقرأ التوثيق المقابل للمزيد من المعلومات.

لنأخذ مثالاً لدائرة يُراد فيها قياس المؤثر O=PcPPO = \sum_P c_P P، حيث PP هي عوامل Pauli و 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 هو عامل Pauli على 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 الإصدار 2.0 أو أحدث، مع دعم التصور
  • Qiskit Runtime الإصدار 0.22 أو أحدث (pip install qiskit-ibm-runtime)
  • إضافة OBP الخاصة بـ Qiskit الإصدار 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 لمحاكاة الديناميكا الكمية لسلسلة دوران Heisenberg باستخدام إضافة OBP الخاصة بـ Qiskit. لاحظ أنه في محاكٍ خالٍ من الضوضاء، تكون قيمة التوقع المحصّلة مع الانتشار الخلفي ودونه متطابقة.

الخطوة 1: تحويل المدخلات الكلاسيكية إلى مسألة كمية

تحويل التطور الزمني لنموذج Heisenberg الكمي إلى تجربة كمية

أولاً، سنستخدم دالة generate_xyz_hamiltonian من qiskit-addon-utils لتوليد هاملتوني شبيه بـ Heisenberg على رسم بياني لشبكة اتصال محددة. يمكن أن يكون هذا الرسم البياني إما 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

بعد ذلك، نُولّد مؤثر Pauli يُمثّل هاملتونيان 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}),}

حيث 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 هو عدد الشرائح. عندما لا يتبادل حدّان في المؤثر التبديل على مستوى الكيوبتات، نحتاج إلى دوائر منفصلة للحصول على قيم التوقع المقابلة لكل منهما. فعلى سبيل المثال، إذا كان لدينا مؤثر يعمل على كيوبتين 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 المطلوب) المُخصَّصة، نحدّد الحد الأقصى لعدد مجموعات Pauli المتبادلة كيوبتياً التي يُسمح للمؤثر المُعاد انتشاره الخلفي بامتلاكها. هنا نُحدّد أن الانتشار الخلفي يجب أن يتوقف حين يتجاوز عدد مجموعات Pauli المتبادلة كيوبتياً في المؤثر الحد ثمانية.

op_budget = OperatorBudget(max_qwc_groups=8)

إعادة الانتشار الخلفي للشرائح من الدائرة

أولاً نُحدّد المؤثر المقاس على أنه MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle، حيث NN هو عدد الكيوبتات. سنُعيد الانتشار الخلفي للشرائح من دائرة التطور الزمني حتى لا تتمكن حدود المؤثر من الاندماج ضمن ثماني مجموعات أو أقل من مجموعات Pauli المتبادلة كيوبتياً.

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

ستلاحظ فيما يلي أننا أعدنا الانتشار الخلفي لست شرائح، وأن الحدود اندمجت في ست مجموعات لا ثماني. وهذا يعني أن إعادة الانتشار الخلفي لشريحة واحدة إضافية ستتجاوز حد الثماني مجموعات من Pauli. يمكننا التحقق من ذلك بفحص البيانات الوصفية المُعادة. لاحظ أيضاً أن تحويل الدائرة في هذا الجزء دقيق تماماً، أي أنه لم يُقتطع أي حد من المؤثر الجديد 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 "
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

في مثال المحاكي الصغير الحجم، لن نستخدم الاقتطاع. ذلك لأنه في غياب الضوضاء، تُعطي الدائرة مع الانتشار الخلفي ودونه النتيجة نفسها، والاقتطاع يُفاقم النتيجة بسبب التقريب الإضافي الذي يُدخله.

ضبط الدوائر للتوافق مع مجموعة البوابات الأساسية

الآن نضبط كلاً من الدائرة الأصلية والدائرة التي خضعت للانتشار الخلفي لتتوافق مع بوابات الـ backend. لا نحتاج إلى الضبط على الـ backend الفعلي لأننا سنُشغّل المحاكي للمثال الصغير.

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 primitives

أولاً، نُنشئ اثنين من Primitive Unified Blocs (PUBs) يُقابلان الدائرة الأصلية والدائرة التي خضعت للانتشار الخلفي. ثم نُنفّذ هذه الـ PUBs على 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. لا تتجلى فائدة الانتشار الخلفي إلا على العتاد الفعلي ذي الضوضاء، حيث تتراكم أخطاء أقل على الدائرة ذات العمق الأصغر الناتجة عن الانتشار الخلفي، كما يُبيّن مثال العتاد الكبير الحجم أدناه.

مثال العتاد الكبير الحجم

عند تطوير تجربة، من المفيد البدء بدائرة صغيرة لتسهيل التصور والمحاكاة. الآن ننتقل إلى الانتشار الخلفي للمؤثر على هاملتوني Heisenberg مكوّن من 50 كيوبت بالقيم ذاتها لمعاملات JJ وhh والمؤثر MZM_Z، لكن مع أربع خطوات Trotter. لا يمكن حساب قيمة التوقع المثالية بهذا الحجم بطريقة القوة الغاشمة، لذا نستخدم شبكة موترات ونحصل على قيمة التوقع المثالية 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: "
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

الخطوات التالية

إن وجدت هذا العمل مثيراً للاهتمام، فقد تستفيد من المواد التالية:

توصيات