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

ابدأ مع قطع الدوائر باستخدام قطع الأسلاك

إصدارات الحزم

الكود في هذه الصفحة تم تطويره باستخدام المتطلبات التالية. نوصي باستخدام هذه الإصدارات أو أحدث منها.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0

يوضح هذا الدليل مثالاً عملياً لقطع الأسلاك باستخدام حزمة qiskit-addon-cutting. يتناول إعادة بناء قيم التوقع لدائرة من سبعة كيوبتات باستخدام قطع الأسلاك.

يُمثَّل قطع السلك في هذه الحزمة كتعليمة من كيوبتَين Move، والتي تُعرَّف بأنها إعادة تعيين للكيوبت الثاني الذي تعمل عليه التعليمة، يتبعه تبادل لكلا الكيوبتَين. هذه العملية تعادل نقل حالة الكيوبت الأول إلى الكيوبت الثاني، مع التخلص في آنٍ واحد من الحالة الواردة للكيوبت الثاني.

صُممت الحزمة لتكون متوافقة مع الطريقة التي يجب عليك التعامل بها مع قطع الأسلاك عند العمل على الكيوبتات الفيزيائية. على سبيل المثال، قد يأخذ قطع السلك حالة الكيوبت الفيزيائي nn ويستمر بها ككيوبت فيزيائي mm بعد القطع. يمكنك التفكير في "قطع التعليمات" كإطار موحد للنظر في كل من قطع الأسلاك وقطع البوابات ضمن نفس الصياغة الرياضية (إذ إن قطع السلك هو مجرد تعليمة Move مقطوعة). كما يتيح استخدام هذا الإطار لقطع الأسلاك إعادة استخدام الكيوبتات، وهو ما يُشرح في قسم قطع الأسلاك يدوياً.

تعمل تعليمة الكيوبت الواحد CutWire كواجهة أبسط وأكثر تجريداً للتعامل مع قطع الأسلاك. تتيح لك تحديد مكان قطع السلك في الدائرة على مستوى عالٍ، وتقوم إضافة قطع الدوائر تلقائياً بإدراج تعليمات Move المناسبة نيابةً عنك.

يوضح المثال التالي إعادة بناء قيمة التوقع بعد قطع الأسلاك. ستنشئ دائرة تحتوي على عدة بوابات غير محلية وتحدد المؤثرات التي تريد تقديرها.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2

from qiskit_addon_cutting.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)

qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)

# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])

# Draw circuit
qc_0.draw("mpl")

Output of the previous code cell

قطع الأسلاك باستخدام تعليمة CutWire عالية المستوى

بعد ذلك، اقطع الأسلاك باستخدام تعليمة الكيوبت الواحد CutWire على الكيوبت q3q_3. بمجرد أن تكون التجارب الفرعية جاهزة للتنفيذ، استخدم دالة cut_wires() لتحويل CutWire إلى تعليمات Move على كيوبتات مخصصة جديدة.

qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

qc_1.draw("mpl")

Output of the previous code cell

ملاحظة حول توسيع المؤثرات

عندما يتمدد الدائرة عبر قطع سلك واحد أو أكثر، يجب تحديث المؤثر ليأخذ في الاعتبار الكيوبتات الإضافية التي يتم إدخالها. تحتوي حزمة qiskit-addon-cutting على دالة مساعدة expand_observables()، التي تأخذ كائنات PauliList والدوائر الأصلية والمتمددة كمعطيات، وتُرجع PauliList جديدة.

لن تحتوي PauliList المُعادة على أي معلومات عن معاملات المؤثر الأصلي، لكن يمكن تجاهل ذلك حتى مرحلة إعادة بناء قيمة التوقع النهائية.

# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)

# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']

Output of the previous code cell

تقسيم الدائرة والمؤثر

يمكن الآن تقسيم المسألة إلى أجزاء. يتم ذلك باستخدام دالة partition_problem() مع مجموعة اختيارية من تسميات الأجزاء لتحديد كيفية فصل الدائرة. تُجمَّع الكيوبتات التي تشترك في نفس تسمية الجزء معاً، وتُقطع أي بوابات غير محلية تمتد عبر أكثر من جزء واحد.

إذا لم يتم تحديد تسميات أجزاء، فسيتم تحديد التقسيم تلقائياً بناءً على ترابط الدائرة. اقرأ القسم التالي حول قطع الأسلاك يدوياً لمزيد من المعلومات حول تضمين تسميات الأجزاء.

partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits[1].draw("mpl")

Output of the previous code cell

في مخطط التقسيم هذا، قطعت سلكَين، مما أدى إلى تكلفة أخذ العينات 444^4.

توليد التجارب الفرعية للتنفيذ ومعالجة النتائج

لتقدير قيمة التوقع للدائرة كاملة الحجم، يتم توليد عدة تجارب فرعية من التوزيع شبه الاحتمالي المشترك للبوابات المُفككة، ثم تنفيذها على معالج كمومي واحد أو أكثر. تقوم طريقة generate_cutting_experiments بذلك من خلال استيعاب معطيات قاموسَي subcircuits وsubobservables اللذان أنشأتهما أعلاه، إضافةً إلى عدد العينات المطلوب أخذها من التوزيع.

ملاحظة حول عدد العينات

تحدد وسيطة num_samples عدد العينات التي يتم سحبها من التوزيع شبه الاحتمالي، وتحدد دقة المعاملات المستخدمة في إعادة البناء. تمرير ما لا نهاية (np.inf) يضمن حساب جميع المعاملات بدقة تامة. اقرأ وثائق API حول توليد الأوزان وتوليد تجارب القطع لمزيد من المعلومات.

# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)

# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
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()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

أخيراً، يمكن إعادة بناء قيمة التوقع للدائرة كاملة باستخدام طريقة reconstruct_expectation_values().

يُعيد الكتلة البرمجية أدناه بناء النتائج ويقارنها بقيمة التوقع الدقيقة.

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
ملاحظة حول معاملات المؤثر

لإعادة بناء قيمة التوقع بدقة، يجب تطبيق معاملات المؤثر الأصلي (التي تختلف عن مخرجات generate_cutting_experiments()) على مخرجات إعادة البناء، إذ فُقدت هذه المعلومات عند توليد تجارب القطع أو عند توسيع المؤثر.

عادةً يمكن تطبيق هذه المعاملات من خلال numpy.dot() كما هو موضح سابقاً.

قطع الأسلاك باستخدام تعليمة Move منخفضة المستوى

أحد قيود استخدام تعليمة CutWire عالية المستوى هو أنها لا تتيح إعادة استخدام الكيوبتات. إذا كان هذا مطلوباً في تجربة القطع، يمكنك بدلاً من ذلك وضع تعليمات Move يدوياً. ولكن، نظراً لأن تعليمة Move تتخلص من حالة كيوبت الوجهة، من المهم ألا يشترك هذا الكيوبت في أي تشابك مع بقية النظام. وإلا فإن عملية إعادة التعيين ستتسبب في انهيار جزئي لحالة الدائرة بعد قطع السلك.

تُجري الكتلة البرمجية أدناه قطعاً للسلك على الكيوبت q3q_3 لنفس الدائرة المثالية المُستخدمة سابقاً. الفرق هنا هو أنه يمكنك إعادة استخدام كيوبت عن طريق عكس عملية Move في المكان الذي تم فيه قطع السلك الثاني (رغم أن هذا ليس دائماً ممكناً ويعتمد على الدائرة المُقطَّعة).

qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")

Output of the previous code cell

يمكن الآن تقسيم الدائرة أعلاه وتوليد تجارب القطع. لتحديد كيفية تقسيم الدائرة بشكل صريح، يمكنك إضافة تسميات الأجزاء إلى دالة partition_problem(). تُجمَّع الكيوبتات التي تشترك في نفس تسمية الجزء معاً، وتُقطع أي بوابات غير محلية تمتد عبر أكثر من جزء. ستتطابق مفاتيح القاموس الناتج من partition_problem() مع تلك المحددة في سلسلة التسميات.

partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits["B"].draw("mpl")

Output of the previous code cell

يمكن الآن توليد تجارب القطع وإعادة بناء قيمة التوقع بنفس الطريقة المُتبعة في القسم السابق.

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

توصيات