قطع الأسلاك لتقدير قيم التوقع
تقدير الاستخدام: دقيقة واحدة على معالج Eagle (ملاحظة: هذا تقدير فحسب. قد يختلف وقت التشغيل الفعلي لديك.)
الخلفية
قطع الدائرة (Circuit-knitting) مصطلح شامل يضم أساليب متعددة لتقسيم الدائرة إلى دوائر فرعية أصغر تتضمن عددًا أقل من البوابات و/أو الكيوبتات. يمكن تنفيذ كل دائرة فرعية باستقلالية، ويُحصل على النتيجة النهائية عبر معالجة كلاسيكية لاحقة لمخرجات كل دائرة فرعية. يمكن الوصول إلى هذه التقنية من خلال إضافة Qiskit لقطع الدوائر؛ وشرح مفصّل للتقنية متاح في التوثيق إلى جانب مواد تمهيدية أخرى.
يتناول هذا الدفتر أسلوبًا يُعرف بـ قطع الأسلاك، حيث تُقسَّم الدائرة على طول السلك [1], [2]. تجدر الإشارة إلى أن التقسيم سهل في الدوائر الكلاسيكية لأن المخرج عند نقطة التقسيم يمكن تحديده بصورة حتمية إما 0 أو 1. غير أن حالة الكيوبت عند نقطة القطع هي في العموم حالة مختلطة. لذلك، يجب قياس كل دائرة فرعية مرات عدة في أسس مختلفة (عادةً مجموعة من الأسس المكتملة طوموغرافيًا كأساس باولي [3], [4])، مع إعداد الحالة المقابلة في حالتها الذاتية. يوضح الشكل أدناه (بإذن من: أطروحة الدكتوراه، Ritajit Majumdar) مثالًا على قطع الأسلاك لحا لة GHZ ذات 4 كيوبتات إلى ثلاث دوائر فرعية. هنا، تشير إلى مجموعة من الأسس (عادةً باولي X وY وZ)، و تشير إلى مجموعة من الحالات الذاتية (عادةً و و و).
نظرًا لأن كل دائرة فرعية تحتوي على عدد أقل من الكيوبتات و/أو البوابات، فمن المتوقع أن تكون أقل عرضةً للضوضاء. يوضح هذا الدفتر مثالًا يمكن فيه استخدام هذه الطريقة لتخفيف الضوضاء في النظام بشكل فعال.
المتطلبات
قبل البدء في هذا الدليل التعليمي، تأكد من تثبيت ما يلي:
- Qiskit SDK الإصدار 2.0 أو أحدث، مع دعم التصور البياني
- Qiskit Runtime الإصدار 0.22 أو أحدث (
pip install qiskit-ibm-runtime) - إضافة Qiskit لقطع الدوائر الإصدار 0.9.0 أو أحدث (
pip install qiskit-addon-cutting)
سنعمل في هذا الدفتر على دائرة Many Body Localization (MBL). دائرة MBL هي دائرة فعّالة على مستوى الأجهزة، وهي معلمة بمعاملين هما و. عندما يُضبط على وتُحضَّر الحالة الابتدائية في لجميع الكيوبتات، تكون قيمة التوقع المثالية لـ هي لكل موقع كيوبت بصرف النظر عن قيم . يمكنك الاطلاع على مزيد من التفاصيل حول دوائر MBL في هذه الورقة البحثية.
الإعداد
# Added by doQumentation — installs packages not in the Binder environment
%pip install -q qiskit-addon-cutting
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)
الجزء الأول. مثال على نطاق صغير
الخطوة 1: تحويل المدخلات الكلاسيكية إلى مسألة كمومية
نبني أولًا دائرة قالبية دون قيم محددة للمعاملات. كما نوفر عناصر تعجيزية تسمى CutWire للإشارة إلى مواضع القطع. في هذا المثال الصغير، نعمل على دائرة MBL ذات 10 كيوبتات.
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)
تذكّر أننا نهدف إلى إيجاد قيمة التوقع للرصيد عندما يكون . سنضع قيمًا عشوائية للمعامل .
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]
الآن نُعلّم الدائرة للقطع عبر إدراج CutWire المناسبة لإنشاء قطعتين متساويتين تقريبًا. نضبط use_cut=True في الدالة ونسمح لها بالتعليم بعد كيوبت، حيث هو عدد الكيوبتات في الدائرة الأصلية.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
الخطوة 2: تحسين المسألة لتنفيذها على الأجهزة الكمومية
بعد ذلك نقطع الدائرة إلى دائرتين فرعيتين أصغر. في هذا المثال، نكتفي بدائرتين فرعيتين فقط. لذلك نستخدم إضافة Qiskit: قطع الدوائر.
قطع الدائرة إلى دوائر فرعية أصغر
يؤدي قطع السلك عند نقطة ما إلى زيادة عدد الكيوبتات بواحد. فبالإضافة إلى الكيوبت الأصلي، يظهر كيوبت إضافي كعنصر تعجيزي في الدائرة بعد القطع. يوضح الشكل التالي هذا المفهوم:
تستخدم هذه الإضافة الدالة cut_wires للتعامل مع الكيوبتات الإضافية الناجمة عن القطع.
mbl_move = cut_wires(mbl_cut)
إنشاء الرصيد وتوسيعه
الآن نبني الرصيد . بما أن القيمة المثالية لـ لكل هي ، فإن القيمة المثالية لـ هي أيضًا .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
غير أن عدد الكيوبتات في الدائرة قد ازداد بعد إدراج عمليات Move الافتراضية ذات الكيوبتين عقب القطع. لذا، يجب توسيع الرصيد أيضًا عبر إدراج هويات لمطابقة الدائرة الحالية.
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
لاحظ أن كل رصيد توسّع الآن ليستوعب سبعة كيوبتات كما في الدائرة التي تحتوي على عملية Move، بدلًا من الستة كيوبتات الأصلية. بعد ذلك، نقسّم الدائرة إلى دائرتين فرعيتين.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
لنرسم الدوائر الفرعية
subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
كما تم تقسيم الرصائد لتناسب الدوائر الفرعية
subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}
لاحظ أن كل دائرة فرعية تُنتج عددًا من العينات. تأخذ عملية إعادة البناء في الاعتبار نتيجة كل من هذه العينات. كل عينة من هذه العينات تُسمى subexperiment.
يستلزم توسيع الرصيد باستخدام عملية Move بنية بيانات PauliList. يمكننا أيضًا إنشاء رصيد باستخدام بنية البيانات الأكثر عمومية SparsePauliOp، وهو ما سيفيدنا لاحقًا أثناء إعادة بناء التج ارب الفرعية.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
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])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
لنرَ مثالين على قياس كيوبتات القطع في أساسين مختلفين. أولًا يُقاس في أساس Z الاعتيادي، ثم يُقاس في أساس X.
subexperiments[0][6].draw("mpl", fold=-1)
subexperiments[0][2].draw("mpl", fold=-1)
نقل كل تجربة فرعية
نحتاج حاليًا إلى نقل (transpile) دوائرنا قبل إرسالها للتنفيذ. لذلك سنقوم أولًا بنقل كل دائرة في التجارب الفرعية.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
نحتاج الآن إلى نقل كل دائرة من دوائر التجارب الفرعية. لذلك نُنشئ أولًا مدير تمرير، ثم نستخدمه لنقل كل دائرة.
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)
الخطوة 3: التنفيذ باستخدام أوليات Qiskit
سنُنفّذ الآن كل دائرة في التجربة الفرعية. تستخدم Qiskit-addon-cutting الـ SamplerV2 لتنفيذ التجارب الفرعية.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
الخطوة 4: المعالجة اللاحقة وإعادة النتيجة بالصيغة الكلاسيكية المطلوبة
بعد تنفيذ الدوائر، نحتاج الآن إلى استرداد النتائج وإعادة بناء قيمة التوقع للدائرة غير المقطوعة والرصيد الأصلي.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803
التحقق المتقاطع
لنُنفّذ الآن الدائرة دون قطع ونتحقق من النتيجة. لاحظ أنه لتنفيذ الدائرة غير المقطوعة يمكننا استخدام EstimatorV2 مباشرةً لحساب قيم التوقع. لكننا سنستخدم ن فس الـ Primitive طوال الدفتر، لذا سنستخدم SamplerV2 للحصول على التوزيع الاحتمالي وحساب قيمة التوقع باستخدام الدالة sampled_expectation_value.
أولًا نحتاج إلى نقل دائرة mbl غير المقطوعة.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
بعد ذلك ننشئ الـ pub ونُشغّل الدائرة غير المقطوعة.
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001
نلاحظ أن قيمة التوقع المحصل عليها عبر قطع الأسلاك أقرب إلى القيمة المثالية مقارنةً بالدائرة غير المقطوعة. لنقم الآن بتوسيع نطاق المسألة.
الجزء الثاني. التوسع!
أظهرنا سابقًا النتائج لدائرة MBL ذات 10 كيوبتات. بعد ذلك، نُبيّن أن التحسن في قيمة التوقع يُحصل عليه أيضًا للدوائر الأكبر. لإثبات ذلك، نكرر العملية لدائرة MBL ذات 60 كيوبت.
الخطوة 1: تحويل المدخلات الكلاسيكية إلى مسألة كمومية
num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
ننشئ مجموعة عشوائية من القيم لـ
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
بعد ذلك ننشئ الدائرة المقطوعة
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
الخطوة 2: تحسين المسألة لتنفيذها على الأجهزة الكمومية
كما هو موضح في المثال الصغير، نقسّم الدائرة والرصيد لتجارب القطع.
mbl_move = cut_wires(mbl_cut)
# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
نُنشئ أيضًا كائن SparsePauliOp للرصيد مع المعاملات المناسبة.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
بعد ذلك نُولّد التجارب الفرعية وننقل كل دائرة في التجربة الفرعية.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
الخطوة 3: التنفيذ باستخدام أوليات Qiskit
نستخدم وضع Batch لتنفيذ جميع الدوائر في التجارب الفرعية.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
الخطوة 4: المعالجة اللاحقة وإعادة النتيجة بالصيغة الكلاسيكية المطلوبة
لنسترد الآن النتائج لكل دائرة في التجربة الفرعية ونُعيد بناء قيمة التوقع المقابلة للدائرة غير المقطوعة والرصيد الأصلي.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409
التحقق المتقاطع
كما في المثال الصغير، سنحصل مرةً أخرى على قيمة التوقع عبر تنفيذ الدائرة غير المقطوعة، ونقارن النتيجة مع قطع الدائرة. سنستخدم SamplerV2 للحفاظ على الاتساق في استخدام الأوليات.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998
التصور البياني
لنرسم التحسن المحصل في قيمة التوقع باستخدام قطع الأسلاك.
ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()
الاستنتاج
نلاحظ أن قطع الأسلاك يُعطي نتيجةً أفضل من الدائرة غير المقطوعة في كلٍّ من المسائل الصغيرة والكبيرة. لاحظ أنه لم يُستخدم أي أسلوب من أساليب تخفيف الأخطاء في هذه التجارب. وبالتالي، فإن التحسن في النتيجة الذي تم الحصول عليه يعود فقط إلى قطع الأسلاك. قد يكون من الممكن تحسين النتائج بشكل أكبر باستخدام أساليب تخفيف مختلفة جنبًا إلى جنب مع قطع الدائرة.
علاوةً على ذلك، في هذا الدفتر حسبنا كلتا الدائرتين الفرعيتين على نفس الجهاز. في [5], [6]، يعرض المؤلفون أسلوبًا لتوزيع الدوائر الفرعية على أجهزة مختلفة باستخدام معلومات الضوضاء بهدف تعظيم قمع الضوضاء وتوازي العملية.
الملحق: اعتبارات توسع الموارد
يزداد عدد الدوائر المطلوب تنفيذها مع زيادة عدد القطوع. لذا، في حين أن القطوع الكثيرة قد تُنتج دوائر فرعية أصغر مما يُحسّن الأداء أكثر، إلا أنها تؤدي أيضًا إلى عدد كبير جدًا من تنفيذات الدوائر، وهو ما قد لا يكون عمليًا في معظم الحالات. أدناه، نُبيّن مثالًا على عدد الدوائر الفرعية المقابل لعدد القطوع لدائرة مكوّنة من 50 كيوبت.
لاحظ أنه حتى مع خمسة قطوع، يبلغ عدد التجارب الفرعية نحو 200 ألف. لذلك، ينبغي استخدام قطع الدائرة فقط عندما يكون عدد القطوع صغيرًا.
مثال على دائرة صديقة للقطع وأخرى غير صديقة له
دائرة صديقة للقطع
كما أشرنا سابقًا، تكون الدائرة صديقةً للقطع عندما يمكن تقسيمها إلى دوائر فرعية منفصلة أصغر بعدد قليل من القطوع. أي دائرة فعّالة على مستوى الأجهزة، أي دائرة تتطلب عددًا قليلًا أو معدومًا من بوابات SWAP عند تعيينها على خريطة الاقتران للجهاز، تكون في العموم صديقةً للقطع. أدناه، نُبيّن مثالًا على ansatz لحفظ الإثارة، يُستخدم في الكيمياء الكمومية. لاحظ أن مثل هذه الدائرة يمكن تقسيمها إلى دائرتين فرعيتين بقطعة واحدة فقط بصرف النظر عن عدد الكيوبتات.

دائرة غير صديقة للقطع
تكون الدائرة غير صديقة للقطع إذا كان عدد القطوع المطلوبة لتشكيل أقسام منفصلة ينمو بشكل ملحوظ مع العمق أو مع عدد الكيوبتات. تذكّر أن كل قطعة تستلزم كيوبتًا إضافيًا. لذا مع زيادة عدد القطوع، يزداد أيضًا العدد الفعلي للكيوبتات. أدناه نُبيّن مثالًا على دائرة Grover ذات 3 كيوبتات مع نموذج قطع محتمل.
نلاحظ أن ثلاثة قطوع مطلوبة، وأن القطع أكثر عموديةً منه أفقيًا. هذا يعني أن عدد القطوع من المتوقع أن يتوسع خطيًا مع عدد الكيوبتات، وهو ما لا يُلائم القطع.
المراجع
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.