كيفية ضبط خيارات الإعداد
حساب الحدود دقيق من حيث المبدأ لكنه تقريبي عمليًا: تطوير حد خطأ باولي (Pauli) عبر دائرة كمّية ينتج مؤثرًا فيه عدد أسّي من الحدود، وإيجاد النظيم الطيفي للمبدّل الناتج مكلف هو نفسه. الخيارات أدناه تتحكم في هذه التكلفة. وتنقسم إلى ثلاث مجموعات:
-
خيارات الدقة تغيّر القيمة العددية للحدود، وتوازن بين إحكام الحدود والتكلفة.
-
النطاق يحدد أي حدود يتم حسابها.
-
خيارات التنفيذ تغيّر كيف يجري الحساب (وليس ما يحسبه).
| الخيار | القيمة الافتراضية | يتحكم في | الأثر على النتيجة |
|---|---|---|---|
| الدقة | |||
evolution_max_terms | 1_000_000 | كم حدًا من حدود المؤثر يبقى بعد التطوير | أقل: أرخص، حدود أوسع |
atol_simplify | 1e-8 | أي حدود المبدّل تُهمل باعتبارها ضئيلة | أعلى: أرخص، حدود أوسع |
eigval_max_qubits | 14 | أقصى عرض للمبدّل يمكن معه حساب نظيمه الطيفي | أقل: أرخص، حدود أوسع |
atol_eigenvalue | 1e-10 | متى يُعتبر حلّال القيم الذاتية التكراري قد تقارب | أعلى: أرخص، حدود أقل دقة |
| النطاق | |||
max_num_boxes | None | كم صندوقًا تُحسب له حدود | الصناديق التي تتجاوز الحد تبقى بالحد البديهي 2.0 |
| التنفيذ | |||
timeout | None | ميزانية زمنية (ساعة حائط) للحساب كله | أي عمل متبقٍ يبقى بالحد البديهي 2.0 |
num_processes | 1 | كم عملية عاملة تحسب الحدود بالتوازي | لا شيء بمفردها |
الحد يكون مفيدًا فقط إذا كان صالحًا: يجب ألا يقلّل أبدًا من أثر حد الخطأ على المرصودة. تخفيف أي من هذه الخيارات غير atol_eigenvalue لا يمكن إلا أن يجعل الحدود أكبر (أكثر تحفظًا)، ولا يصغّرها أبدًا. وبالتالي، تكلفة الإعداد المتساهل هي عبء أخذ عينات أكبر، وليست قيمة توقع خاطئة - atol_eigenvalue هو الاستثناء.
تعريف الدائرة
التجارب التالية تستخدم نفس دائرة Ising المقسّمة بطريقة Trotter ونفس المرصودة المستخدمة في دليل البدء السريع. عند هذا الحجم، لكل خيار أثر واضح، ومع ذلك يستغرق كل حساب أقل من ثانية.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit
num_qubits = 10
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])
boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)
print(f"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198
دالة مساعدة صغيرة تلخّص كل مجموعة حدود في سطر واحد، فيظهر أثر الخيار في أرقام قليلة بدل جدار من المصفوفات.
كل حد يقع بين 0 والحد البديهي 2.0. الملخص يعرض متوسط الحد وانحرافه المعياري، ثم كيف تتوزع الحدود على هذه المجموعات:
- حدود محدودة بـ 0 بالضبط لأنها تتبادل مع المرصودة المطوّرة
- حدود لها حد محسوب بين 0 و2.0
- حدود بقيت عند الحد البديهي 2.0
لاحظ أن المتوسط الأقل يعني حدًا أحكم، والأحكم يعني عبء أخذ عينات أصغر لاحقًا.
المسح (sweep) على خيار واحد يعلّم القيمة الافتراضية للخيار بـ *:
import inspect
import time
from qiskit_addon_slc.bounds import compute_forward_bounds
def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:28} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%} │ "
f"{elapsed:4.2f}s"
)
def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates
def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})
baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
الدقة
evolution_max_terms
تطوير حد خطأ باولي عبر بوابات غير كليفورد (non-Clifford) يحوّله إلى مجموع من مصفوفات باولي، وهذا المجموع يكبر مع عمق الدائرة. الخيار evolution_max_terms يحدد سقفًا لعدد هذه الحدود المحتفظ بها. النظيم 1 لكل ما يُهمل يُضاف إلى الحد، فلا يضيع شيء بصمت.
المسح يبدأ من قيمة أحكم من الافتراضية وينزل إلى قطع شديد. خفض السقف يجعل الحساب أرخص والحدود أوسع.
sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s
رفع السقف فوق قيمته الافتراضية لا يغيّر شيئًا هنا. بالنسبة لدائرة بهذا الحجم، القيمة الافتراضية أصلًا تحتفظ بكل الحدود، فالحدود هي الأحكم التي يقدر هذا الخيار يعطيها. وهذا هو الوضع المعتاد. إحكام خيار دقة لا يفيد إلا إذا كانت قيمته الافتراضية فعلًا تحدّ من النتيجة، والطريقة لمعرفة ذلك هي التجربة أعلاه. أعد تشغيلها بقيمة أحكم وشوف إذا تحركت الحدود.
تحذير: روتين الانتشار الأساسي يعامل evolution_max_terms على أنه سعة تُحجز مسبقًا، وليس حدًا أعلى يُتحقق منه. تمرير قيمة غير محدودة اسميًا، مثل numpy.iinfo(numpy.uint).max، يجعل المخصِّص يطلب تيرابايتات من الذاكرة ويُنهي العملية. خلّه رقمًا واقعيًا. القيمة الافتراضية، مليون، سخية أصلًا.
atol_simplify
بعد تكوين المبدّل، تُسقط الحدود التي معاملها أقل من atol_simplify. وكما في سقف التطوير، النظيم 1 الضائع بهذه الطريقة يُضاف إلى الحد.
sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s
eigval_max_qubits
أحكم حد متاح هو النظيم الطيفي للمبدّل، وهذا يحتاج حساب قيم ذاتية. يصير مكلفًا كلما اتسع المبدّل، لذلك يحدد eigval_max_qubits العرض الذي بعده تُترك المحاولة ويُستخدم بدلها متباينة المثلث. هذا الحد، على معاملات المبدّل ، صالح لكنه أوسع.
ضبطه على صفر يفرض الرجوع للبديل في كل مكان، وهذا يُظهر فائدة حلّال القيم الذاتية في رقم واحد.
sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s
حتى مع إيقاف حلّال القيم الذاتية، يبقى المتوسط أقل بكثير من الحد البديهي 2.0. نسبة الحدود المحدودة بـ 0 لا تتغير: الحد الذي يتبادل تمامًا مع المرصودة المطوّرة مبدّله صفر، ويُتعرف على ذلك قبل التفكير في أي حساب قيم ذاتية. أما الباقي، فحد متباينة المثلث يصل إلى 2.0 لبعضها ويُقصّ عنده، لكنه لكثير منها يبقى أصغر، فيحتفظ بحد محسوب أقل من 2.0.
atol_eigenvalue
عندما يُحسب النظيم الطيفي، يكون atol_eigenvalue هو تفاوت التقارب لحلّال القيم الذاتية التكراري. ومع ذلك، أغلب المبدّلات لا تصل إلى هذا الحلّال أصلًا:
-
المبدّل الذي يعمل على أربعة كيوبتات على الأكثر يُقطَّر (diagonalize) بدقة ومباشرة.
-
المبدّل الأعرض يُختزل أولًا إلى أقل عدد من الكيوبتات تحتاجه حدود باولي فيه، وإذا كان الشكل المختزل يعمل على ثمانية كيوبتات على الأكثر، يُقطَّر بدقة.
فقط المبدّل الذي ما زال يحتاج أكثر من ثمانية كيوبتات بعد هذا الاختزال يذهب إلى الحلّال التكراري، وهو المكان الوحيد الذي ينطبق فيه هذا التفاوت. الدوائر بالحجم المستخدم في هذا الدليل لا تصل إلى هناك أبدًا، فمسح هذا الخيار سيعرض حدودًا متطابقة لكل القيم. وهو مهم للمخاريط الضوئية العريضة على الأجهزة الكبيرة.
بخلاف خيارات الدقة الأخرى، تخفيف atol_eigenvalue ليس آمنًا في اتجاه واحد. حلّال القيم الذاتية التكراري يقترب من القيمة الذاتية القصوى من داخل الطيف، فالتقدير الذي لم يتقارب بالكامل يقلّل من النظيم الطيفي، ومن الحد. القيمة الافتراضية مشددة عمدًا لهذا السبب. وإذا فشل الحلّال في التقارب، يتولى البديل متباينة المثلث، وهو صالح مرة ثانية.
النطاق
max_num_boxes
أحيانًا الحد المفيد ليس الدقة بل العمق. الخيار max_num_boxes يوقف إطلاق حسابات الحدود بعد عدد معين من الصناديق، وكل طبقة بعده تُعطى الحد البديهي 2.0 بدلًا من ذلك.
الحد يعدّ تعليمات BoxOp الضوضائية (التي تحمل تعليقًا توضيحيًا InjectNoise) بدل أن يعدّ، مثلًا، عمقًا بالبوابات. هذا يربطه ببنية الدائرة التي يراها المستخدم ويتجنب الاقتران بمراجع التعليقات التوضيحية المولّدة تلقائيًا، والتي قد تتغير بين تشغيلات الترجمة (transpilation).
هذا هو الخيار الطبيعي لدائرة نصفاها يُعالجان بحدود أمامية وخلفية على التوالي: احسب كلًّا منهما فقط بالعمق الذي سيُستخدم.
from qiskit.circuit import BoxOp
from samplomatic import InjectNoise
# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)
for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models
* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s
التنفيذ
timeout
timeout هو ميزانية زمنية بالثواني (ساعة حائط). عند انتهائها، تُحفظ الحدود التي حُسبت وكل ما هو معلّق يبقى بالحد البديهي 2.0، فالنتيجة قابلة للاستخدام دائمًا، لكنها أوسع كلما قطعت الحساب أبكر.
ليس موعدًا نهائيًا صارمًا: المهام الجارية أصلًا يُسمح لها بالانتهاء، فالاستدعاء قد يتجاوز الميزانية بمدة مهمة واحدة.
num_processes
حدود حدود الخطأ المختلفة مستقلة عن بعضها، لذلك يوزعها num_processes على عمليات عاملة. بمفرده، هذا يؤثر فقط على مدة الحساب، ولا يؤثر أبدًا على نتيجته.
ملاحظة: استخدام timeout مع num_processes يجعل النتيجة تعتمد على التوقيت: أي المهام تنتهي قبل انتهاء الميزانية غير حتمي. تبقى الحدود صالحة، لكن لم يعد ممكنًا إعادة إنتاجها من تشغيل لآخر.
من أين تبدأ
القيم الافتراضية مختارة لإعطاء حدود محكمة على دوائر بحجم يكون ذلك فيه ميسور التكلفة. إذا كان الحساب بطيئًا جدًا، جرّب تطبيق الخيارات بهذا الترتيب تقريبًا:
-
num_processes: تسريع مجاني، بدون أي تكلفة على الدقة. -
max_num_boxes: إذا كنت تعرف مسبقًا إلى أي عمق في الدائرة يهمّك الأمر. -
eigval_max_qubits: يتخطى الخطوة الأغلى للمبدّلات الأعرض، وهي عادة أقلية صغيرة من الحدود. -
evolution_max_termsوatol_simplify: هذان يؤثران على الدقة في كل مكان، فاضبطهما آخرًا، وتحقق بمسح مثل المسوح أعلاه لتعرف كم تتحرك الحدود. -
timeout: إيقاف قاطع وليس أداة ضبط، لما يجب أن ينتهي الحساب عند موعد نهائي، مهما كانت الجودة.
كلما كانت الحدود أوسع، كان عبء أخذ العينات الذي سيبلّغ عنه compute_local_scales أكبر لتفاوت انحياز معين. هذا العبء هو الكمية التي تستحق المراقبة أثناء الضبط: يخبرك كم تكلّف التقريبات فعلًا.