كيفية حساب حدود مخصصة
compute_forward_bounds وcompute_backward_bounds هما غلافان للتسهيل. كلاهما مبني من نفس القطع الثلاث، التي تعرضها الواجهة البرمجية العامة:
-
دالة نظيم، تحدّ حد خطأ باولي مطوّرًا واحدًا
-
مخروط ضوئي، يحدد الوجهة التي تُطوَّر نحوها حدود الخطأ
-
compute_bounds، وهي تمرّ على الدائرة وتوزّع العمل لكل حد
تجميعها بنفسك يعني أنك تقدر تحدّ شيئًا لا تغطيه الأغلفة. هذا الدليل يمرّ على حالة واحدة تستحق ذلك: دائرة مرآة، حيث الحد الخلفي المعتاد فضفاض بلا داعٍ.
ماذا تفعل الأغلفة فعلًا
الغلافان يختلفان في الأماكن الثلاثة المذكورة أعلاه:
compute_forward_bounds | compute_backward_bounds | |
|---|---|---|
| الدائرة | كما أُعطيت | remove_measure(circuit).inverse() |
| دالة النظيم | time_evolved_norm_forward، بالنسبة للمرصودة | time_evolved_norm_backward، بالنسبة للحالة الابتدائية |
| المخروط الضوئي | LightCone.initialize_from_pauli، يُبذر من المرصودة | LightCone.initialize_from_measurements، يُبذر من كل كيوبت نشط |
backwards | False | True |
الحد الأمامي يسأل "كم يقدر هذا الخطأ يغيّر المرصودة اللي بقيسها؟" ويجيب عنه بالنظيم الطيفي للمبدّل. أما الحد الخلفي فيسأل "كم يقدر هذا الخطأ يغيّر الحالة اللي بدأت منها؟" ويجيب بنظيم نووي، مع اعتبار هذه الحالة هي الحالة الصفرية الكاملة على كل كيوبت نشط. هذان سؤالان مختلفان، وكل منهما هو الصحيح لأخطاء نصفه من الدائرة.
حالة دائرة المرآة
دائرة المرآة تطبّق وحدوية ما ثم تلغيها: . تظهر هذه كلما كان لازم أن تُعرف النتيجة المثالية بدقة (عمومًا في أصداء Loschmidt، وقياس الأداء العشوائي، وتجارب التحقق).
دوائر المرآة لها خاصية لا يقدر الحد الخلفي العام يستغلها. لأن النصف الثاني هو معكوس الأول، فإن تطوير خطأ للخلف إلى الحالة الابتدائية يمرّ بنفس البوابات التي يمرّ بها تطويره للأمام إلى المرصودة. السؤالان يتطابقان، فالحد الأمامي الأحدّ صالح للاتجاه الخلفي أيضًا.
# 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
# The circuit of the quickstart guide...
forward_half = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# ...followed by its inverse. The ideal result is the initial state.
circuit = forward_half.compose(forward_half.inverse())
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"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}")
print(f"noisy layers: {len(noise_model_paulis)}")
mirror circuit: 10 qubits, depth 24
noisy layers: 4
جمّع الحد المخصص
الوصفة تُبقي ما يفعله compute_backward_bounds بالدائرة (تزيل القياسات، وتعكسها، وتمرّ عليها بـ backwards=True) وتستبدل القطعتين الأخريين: دالة النظيم الأمامية، ومخروطًا ضوئيًا يُبذر من المرصودة بـ LightCone.initialize_from_pauli، تمامًا كما يبذر compute_forward_bounds مخروطه. كل استبدال سطر واحد.
from functools import partial
from qiskit_addon_slc.bounds import (
LightCone,
compute_bounds,
time_evolved_norm_forward,
)
from qiskit_addon_slc.utils import remove_measure
def compute_mirror_backward_bounds(
circuit,
noise_model_paulis,
/,
observable,
*,
evolution_max_terms=1_000_000,
**kwargs,
):
"""Bounds the backward direction of a mirror circuit with a forward-style bound.
Valid only for circuits whose second half inverts the first: the backward evolution then
traverses the same gates as the forward one, and reproduces the forward bounds.
"""
# As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit
# backwards from its end becomes walking the inverted circuit forwards.
circuit = remove_measure(circuit).inverse()
# The substitution: the forward norm function, bounding against the observable rather than
# against the initial state.
norm_fn = partial(
time_evolved_norm_forward,
observable=observable,
evolution_max_terms=evolution_max_terms,
)
# Seed the light cone from the observable, not from every active qubit.
light_cone = LightCone.initialize_from_pauli(circuit, observable)
return compute_bounds(
circuit,
noise_model_paulis,
light_cone,
norm_fn,
backwards=True,
**kwargs,
)
كم يكسب
بالمقارنة مع الحد الخلفي الجاهز على نفس الدائرة، ملخّصًا بنفس الطريقة كما في دليل الإعداد: متوسط الحد وانحرافه المعياري، ثم نسبة الحدود المحدودة بـ 0، والتي لها حد محسوب بينهما، والتي بقيت عند الحد البديهي 2.0.
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
)
forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)
custom_backward = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable
)
def summarize(label, bounds):
"""Prints a one-line summary of a set of bounds."""
rates = np.concatenate([bound.rates for bound in bounds.values()])
# 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:18} 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%}"
)
summarize("forward", forward_bounds)
summarize("backward (stock)", stock_backward)
summarize("backward (custom)", custom_backward)
forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%
backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
الحد الخلفي الجاهز يقع قرب القيمة البديهية 2.0، ولا يحمل تقريبًا أي معلومة. في دائرة مرآة، كل خطأ هو "قريب من الحالة الابتدائية" بالمعنى الذي يقيسه ذلك الحد، فلا يقدر يميّز بين الحدود. الحد المخصص أحكم بعدة مرات ويحدّ كثيرًا من الحدود بـ 0.
للتحقق من التنفيذ، تقدر تقارن الاثنين صندوقًا بصندوق. لكل صندوق، يخرج الحد الخلفي المخصص مطابقًا للحد الأمامي لنفس الصندوق، إلى حدود خطأ التقريب. كلاهما يحدّ نفس الخطأ عند نفس النقطة في الدائرة، وفي دائرة مرآة تُلغى البوابات بعد تلك النقطة لتصير معكوس البوابات قبلها، فيحسب الحسابان نفس الكمية من طرفين متقابلين:
matches = sum(
np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)
for box_id in forward_bounds
)
print(f"{matches} of {len(forward_bounds)} boxes match the forward bounds")
worst = max(
np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()
for box_id in forward_bounds
)
print(f"largest disagreement across all terms: {worst:.2e}")
16 of 16 boxes match the forward bounds
largest disagreement across all terms: 6.00e-15
ملاحظة: هذه المساواة خاصية في بنية المرآة، وليست في التقنية. في دائرة ليست معكوسة لنفسها، يبقى نفس الاستبدال يعمل لكنه لا يحسب بعد ذلك حدًا صالحًا للاتجاه الخلفي: إنه يجيب عن السؤال الأمامي عن نصف دائرة لا ينطبق عليه ذلك السؤال. لا تستخدم هذا إلا إذا كان النصف الثاني من دائرتك يعكس الأول.
أدخله في بقية سير العمل
النتيجة قاموس Bounds عادي، فكل ما يلي لا يتغير.
في دائرة المرآة ما فيه سبب أيضًا لحساب أي من الحدين للدائرة كلها. الحدود الأمامية مطلوبة فقط لصناديق ، معدودة من النهاية، والحدود الخلفية فقط لصناديق ، معدودة من البداية. لذلك يحدّ max_num_boxes كلًّا منهما بنصف الصناديق الضوضائية، ويضمّ merge_bounds النصفين.
هكذا تُستخدم الحدود عمليًا، وهذا يعني أن المقارنة صندوقًا بصندوق أعلاه لم تعد متاحة: فهي تحتاج الحدّين لكل صندوق. إنها فحص للتنفيذ لأغراض هذا الدليل، وليست خطوة في سير العمل.
from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds
# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}
# Compute each bound for its own half of the mirror circuit only. `forward_bounds` has an entry
# for every noisy box, which are the boxes `max_num_boxes` counts.
half = len(forward_bounds) // 2
half_forward = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
half_stock = compute_backward_bounds(
boxed_circuit, noise_model_paulis, max_num_boxes=half
)
half_custom = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
print(
f"each bound computed for {half} of {len(forward_bounds)} noisy boxes\n"
)
print(f"{'bounds':34} {'overhead':>12} {'residual bias':>14}")
for label, bounds in (
(
"forward + stock backward",
merge_bounds(boxed_circuit, half_forward, half_stock, noise_rates),
),
(
"forward + custom backward",
merge_bounds(boxed_circuit, half_forward, half_custom, noise_rates),
),
("forward only, whole circuit", forward_bounds),
):
_, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
print(f"{label:34} {cost:12.4f} {bias:14.4f}")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
each bound computed for 8 of 16 noisy boxes
bounds overhead residual bias
forward + stock backward 29848.0583 0.0498
forward + custom backward 465.4028 0.0498
forward only, whole circuit 465.4028 0.0498
مع الحدود الخلفية الجاهزة، يكون عبء أخذ العينات أكبر بكثير. يستخدم merge_bounds الحدود الخلفية حتى نقطة تبديل واحدة في الدائرة والحدود الأمامية من هناك فصاعدًا، ويختار النقطة التي تقلّل الانحياز الكلي. مع حساب كل حد لنصف واحد، تقع تلك النقطة في منتصف الدائرة، فتُغطى صناديق بالحدود الخلفية وحدها. والحد الخلفي الجاهز قريب من البديهي 2.0 هناك، وهذا يرفع العبء. أما المخصص فهو بإحكام الحد الأمامي نفسه، فيطابق العبء عبء الحدود الأمامية الكاملة.
وهذا يطرح السؤال البديهي: ليش نتعب، ما دامت الحدود الأمامية الكاملة وحدها تعطي نفس النتيجة؟ الجواب هو التكلفة. الحد الأمامي لصندوق في بداية الدائرة يطوّر خطأه عبر بقية ثم كل ، وهذا أطول وأعرض تطوير في الدائرة. المؤثر المطوّر يمكن أن ينمو أسّيًا مع عدد البوابات غير الكليفوردية التي يمرّ بها، وحساب النظيم الطيفي للمبدّل يصير أغلى أسّيًا كلما اتسع المخروط الضوئي. في نهج النصف بالنصف، لا يُطوَّر أي خطأ عبر أكثر من نصف الدائرة.
لذلك يزداد التوفير مع عمق الدائرة. والأهم أن الحدود الأمامية الكاملة تتوقف عن أن تكون قابلة للحساب قبل وقت طويل من نظيرتها النصفية. عندما تتجاوز تطويرات الصناديق المبكرة evolution_max_terms أو eigval_max_qubits، تُقصّ حدودها أو ترجع إلى متباينة المثلث، وفي النهاية تصير تكلفة حسابها أعلى من اللازم. أينما وقعت تلك النقطة، فنهج النصف بالنصف يحتاج فقط أن تقع في مكان ما من النصف الأول للدائرة. الحدود الأمامية لا يجب أن تصل أبعد من المنتصف، وفي دائرة مرآة، الحدود الخلفية لصناديق تطوّر أخطاءها للخلف إلى البداية عبر نفس مقدار الدائرة الذي تقطعه الحدود الأمامية لصناديق المقابلة إلى النهاية، فينتهي حسابها عند نفس العمق.
ملاحظة: ليش نحسب حدودًا خلفية لصناديق ، ما دامت الحدود الأمامية لصناديق تبدو كأنها تحمل نفس الأرقام؟ لأن الصناديق ليست صورًا مرآوية لبعضها. كل صندوق يحقن ضوضاءه على جانب واحد من بواباته، وطبقات المعكوسة هي صناديق منفصلة بنماذج ضوضاء خاصة بها، فالصندوق الذي يعكس صندوقًا من يرى أخطاءه عند نقطة مختلفة في الدائرة. لذلك تتفق حدودهما جزئيًا فقط، ولازم تُحسب حدود صناديق لتلك الصناديق نفسها.
كتابة حدود مخصصة أخرى
نفس الاستبدالات الثلاثة تغطي حالات أخرى:
-
نظيم مختلف.
time_evolved_norm_forwardيقبلcomm_norm_order؛ تمرير أي قيمة غير2يتخطى حلّال القيم الذاتية ويحسب ذلك النظيم مباشرة. -
مخروط ضوئي مختلف.
LightCone.initialize_from_pauliيبذر مخروطًا ضوئيًا من أي مصفوفة باولي، ويأخذ مُنشئLightConeالكيوبتات المطلوب تتبعها والعمليات التي تبذره، فيمكن حساب حد بالنسبة لأي مؤثر تقدر تعبّر عنه بهذه الطريقة. -
دالة نظيم مختلفة تمامًا.
compute_boundsتتطلب فقط كائنًا قابلًا للاستدعاء: يأخذPauliوRotationGatesويعيدCommutatorBounds. أي شيء يحقق هذا التوقيع يعمل، بما في ذلك حد بصيغة مغلقة خاص بعائلة دوائرك.
مهما استبدلت، لازم تثبت أن النتيجة حد أعلى حقيقي. التقليل من أثر حد الخطأ لا يسبب فشلًا ظاهرًا. إنه ينتج قيمة توقع خاطئة بصمت، لأن الحد سيكون قد أُسقط من نموذج الضوضاء المخفَّف.