ניפוי שגיאות ומעקב
הפונקציה build() הופכת מעגל מוער לזוג של מעגל תבנית ו-samplex. במעגל מורכב קשה לפעמים לדעת איזו קופסה במעגל המקורי מתאימה לאילו barriers בתבנית, או לאילו צמתים ב-DAG של ה-samplex.
Samplomatic מספקת שני כלי מעקב משלימים:
-
תוויות barrier: כל barrier במעגל התבנית נושא תווית שמקודדת את המקור שלו. התוויות האלה תמיד קיימות, בין אם
debug=Trueמוגדר ובין אם לא. -
מידע מעקב בצמתי samplex: כש-
build()נקראת עםdebug=True, כל צומת samplex נושא אובייקטTraceInfoשמקשר אותו בחזרה לקופסה או לקופסאות שיצרו אותו.
שני הכלים שימושיים ביותר כשהקופסאות נושאות הערת Tag, שמצמידה מחרוזת ref לקופסה. לחלופין, מוצמדות גם הפניות של InjectNoise.
קופסאות עם תגית
הערת Tag מצמידה מחרוזת ref לקופסה. ה-ref מופיע אחר כך בתוויות ה-barrier ובמידע המעקב בצמתי ה-samplex.
במעגל הדוגמה הבא יש שלוש קופסאות: שתי קופסאות CX עם תגית על זוגות Qubit-ים זרים, וקופסת מדידה בלי תגית עם dressing ימני. בקופסת ה-CX השנייה יש גם הערת InjectNoise — גם ה-ref של tag שלה וגם ה-ref של הרעש שלה יופיעו בתוויות ה-barrier. שתי קופסאות ה-CX מכסות Qubit-ים שונים, ולכן ה-optimizer של ה-samplex יכול למזג את צמתי ההפצה שלהן לצמתים משותפים. את זה רואים בסעיף מידע המעקב.
from qiskit.circuit import QuantumCircuit
from samplomatic import InjectNoise, Tag, Twirl, build
circuit = QuantumCircuit(4, 4)
with circuit.box([Twirl(), Tag("cx_ab")]):
circuit.cx(0, 1)
with circuit.box([Twirl(), InjectNoise("cx_noise"), Tag("cx_cd")]):
circuit.cx(2, 3)
with circuit.box([Twirl(), Tag("meas_box")]):
circuit.measure(range(4), range(4))
circuit.draw("mpl")
תוויות barrier במעגל התבנית
קריאה ל-build() על המעגל שלמעלה מפיקה תבנית שה-barriers שלה נושאים תוויות מזהות. לכל תווית יש הצורה {side}{scope}@{key=value&...}:
-
צד (Side):
L= גבול dressing שמאלי,M= גבול התוכן הפנימי של הקופסה,R= גבול dressing ימני. -
היקף (Scope): אינדקס שלם (או רשימה מופרדת בקווים תחתונים לקופסאות מקוננות) שמבדיל בין כמה קופסאות באותה רמת קינון.
-
הערות (Annotations): רשימה שמתחילה ב-
@, מופרדת ב-&, של זוגותkey=valueשנגזרים מההערות של הקופסה. המפתחtagבא מהערתTag, ו-inject_noiseבא מהערתInjectNoise.
ל-barriers מקופסאות בלי תגית יש רק צד והיקף (לדוגמה, L2), בלי סיומת @.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
אפשר גם לחלץ את תוויות ה-barrier בתוכנית:
barrier_labels = [instr.operation.label for instr in template if instr.operation.name == "barrier"]
barrier_labels
['L0@tag=cx_ab',
'M0@tag=cx_ab',
'R0@tag=cx_ab',
'L1@inject_noise=cx_noise&tag=cx_cd',
'M1@inject_noise=cx_noise&tag=cx_cd',
'R1@inject_noise=cx_noise&tag=cx_cd',
'L2@tag=meas_box',
'M2@tag=meas_box',
'R2@tag=meas_box']
מידע מעקב בצמתי samplex
העברת debug=True ל-build() מצמידה מידע מעקב לכל צומת samplex. התכונה trace_info של כל צומת היא אובייקט TraceInfo שמילון ה-trace_refs שלו ממפה מפתחות הערה (לדוגמה, "tag" ו-"inject_noise") לקבוצות של מחרוזות ref. לצמתים בלי הערת קופסה מתאימה יש trace_info=None.
כש-draw() נקראת על samplex שנבנה במצב ניפוי שגיאות, ריחוף מעל כל צומת בגרף האינטראקטיבי חושף את ה-trace_refs שלו בתוך חלונית המידע הצפה.
template, samplex = build(circuit, debug=True)
samplex.draw()
אפשר גם לבדוק את מידע המעקב בתוכנית. שימו לב שחלק מהצמתים נושאים הפניות גם מ-'cx_ab' וגם מ-'cx_cd'. ה-optimizer של ה-samplex מיזג את צמתי ההפצה המקבילים שלהם, כי שתי הקופסאות מכסות Qubit-ים זרים ומשתפות קודם משותף מהפליטה של קופסת המדידה עם ה-dressing הימני.
for node in samplex.graph.nodes():
if node.trace_info is not None:
tags = node.trace_info.trace_refs.get("tag", set())
merged = " ← merged" if len(tags) > 1 else ""
print(f"{type(node).__name__:40s} tags={tags}{merged}")
TwirlSamplingNode tags={'meas_box'}
InjectNoiseNode tags={'cx_cd'}
CollectZ2ToOutputNode tags={'cx_cd'}
TwirlSamplingNode tags={'cx_cd'}
CombineRegistersNode tags={'cx_cd'}
CollectTemplateValues tags={'cx_cd'}
TwirlSamplingNode tags={'cx_ab'}
CombineRegistersNode tags={'cx_ab', 'cx_cd'} ← merged
CollectTemplateValues tags={'cx_ab', 'cx_cd'} ← merged
כדי למצוא את כל הצמתים שמקורם בקופסה מסוימת, מסננים לפי המפתח "tag":
tag_ref = "cx_ab"
matching_nodes = [
node
for node in samplex.graph.nodes()
if node.trace_info is not None and tag_ref in node.trace_info.trace_refs.get("tag", set())
]
print(f"Nodes originating from box '{tag_ref}':")
for node in matching_nodes:
print(f" {type(node).__name__}")
Nodes originating from box 'cx_ab':
TwirlSamplingNode
CombineRegistersNode
CollectTemplateValues
ה-DAG של ה-samplex
ה-samplex ש-build() מחזירה הוא גרף מכוון חסר מעגלים (DAG) שבו קשתות מציינות תלות ברגיסטרים. כלומר, קשת מצומת A לצומת B אומרת ש-B חייב לחכות ש-A יפעל על הרגיסטרים הווירטואליים המשותפים לפני ש-B רשאי לפעול. זה לא סדר זמנים כמו ה-DAG של מעגל קוואנטי. במקום זאת, הגרף זורם מצמתים שאחראים ליצירת רנדומיזציות אל צמתים שאחראים לסנתז אותן כפלט.
יש שלושה סוגי צמתים, לכל אחד סגנון חזותי ייחודי בתרשים האינטראקטיבי:
| צורה | צבע | סוג | תפקיד |
|---|---|---|---|
| כוכב | אדום | SamplingNode | יוצר רגיסטרים וירטואליים חדשים מהתפלגות או מקלט |
| עיגול | ירוק | EvaluationNode | הופך, משלב או מפיץ רגיסטרים וירטואליים |
| פפיון | כחול / סגול | CollectionNode | קורא רגיסטרים וכותב לפלטי sample() |
ההרצה מתקדמת בשלושה שלבים:
-
כל צמתי ה-sampling רצים ראשונים (במקביל).
-
צמתי ה-evaluation רצים אחריהם, בסדר טופולוגי (במקביל בתוך כל דור).
-
צמתי ה-collection רצים אחרונים (במקביל).
פריסת graphviz משקפת את זה. צמתי sampling מופיעים למעלה, וצמתי collection למטה.
חלוניות מידע בריחוף
כל צומת בהדמיה האינטראקטיבית מציג חלונית מידע כשמרחפים מעליו. החלונית מכילה את המידע הבא:
-
שם מחלקת הצומת ואינדקס הגרף השלם שלו.
-
מניפסטי רגיסטרים, כלומר אילו רגיסטרים הצומת יוצר, קורא מהם, כותב אליהם ומסיר. הם מראים איך הנתונים זורמים בין צמתים.
-
פרטים ספציפיים לצומת, כולל סוג ההתפלגות בצמתי sampling, האופרנד בצמתי כפל, אינדקסי פרמטרי התבנית בצמתי collection, וכן הלאה.
-
Trace refs (רק כש-
debug=True), שהם מפתחות ההערה ומחרוזות ה-ref שמקשרים את הצומת בחזרה לקופסאות שיצרו אותו.
לחיצה על התרשים ואז ריחוף מעל צמתים בודדים היא הדרך המהירה ביותר להבין מה צומת נתון עושה בלי לקרוא קוד מקור.
סיכום samplex
לפני בחינת ההדמיה, print(samplex) נותן סיכום טקסט קצר של מספר הצמתים, הקלטים הנדרשים והפלטים המובטחים.
template, samplex = build(circuit)
print(samplex)
Samplex(<17 nodes>)
Inputs:
- 'pauli_lindblad_maps.cx_noise' <PauliLindbladMap>: A PauliLindblad map acting on 2
qubits, with 'num_terms_cx_noise' terms.
Outputs:
* 'measurement_flips.c' <bool['num_randomizations', 1, 4]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 24]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 1]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.
בדיקת רגיסטרים
העברת keep_registers=True ל-sample() משמרת את אובייקטי VirtualRegister הביניים שחיים בסוף הדגימה, ושומרת אותם ב-outputs.metadata["registers"]. כל רגיסטר הוא מערך דו-ממדי (צורה (num_subsystems, num_randomizations), עם ממדי צורת שער נוספים אפשריים בסוף) של איברי חבורה וירטואליים.
זה שימושי כדי לוודא ששערים וירטואליים שולבו נכון, או כדי לבדוק את דגימות הפאולי או היוניטריות הגולמיות לפני שהן מסונתזות לזוויות סיבוב.
from qiskit.quantum_info import PauliLindbladMap
outputs = samplex.sample(
{"pauli_lindblad_maps.cx_noise": PauliLindbladMap.identity(2)},
num_randomizations=3,
keep_registers=True,
)
for name, reg in outputs.metadata["registers"].items():
print(f"{name}: type={reg.TYPE.value}, shape={reg.virtual_gates.shape}")
lhs_0: type=pauli, shape=(4, 3)
rhs_0: type=pauli, shape=(4, 3)
lhs_3: type=pauli, shape=(2, 3)
rhs_3: type=pauli, shape=(2, 3)
inject_noise_1: type=pauli, shape=(2, 3)
sign_1: type=z2, shape=(1, 3)
lhs_7: type=pauli, shape=(2, 3)
rhs_7: type=pauli, shape=(2, 3)
meas_prop_6: type=pauli, shape=(4, 3)
collect_10: type=u2, shape=(2, 3, 2, 2)
meas_prop_z2a_6: type=z2, shape=(4, 3)
collect_9: type=u2, shape=(4, 3, 2, 2)
collect_5: type=u2, shape=(2, 3, 2, 2)
אפשר לראות את התוכן של מצב הסיום של רגיסטר מסוים. בדוגמה הבאה הרגיסטר הוא מסוג PauliRegister. לפרטים על פורמט האחסון ראו את API documentation(/apidocs/index)__.
print("lhs_0:", outputs.metadata["registers"]["lhs_0"])
outputs.metadata["registers"]["lhs_0"].virtual_gates
lhs_0: PauliRegister(<4, 3>)
array([[0, 0, 1],
[3, 1, 1],
[3, 0, 0],
[1, 0, 0]], dtype=uint8)
איך צמתי samplex ממופים לפרמטרי התבנית
למערך outputs["parameter_values"] ש-sample() מחזירה יש צורה (num_randomizations, N), כאשר N שווה ל-len(template.parameters). כל עמודה מתאימה לפרמטר אחד במעגל התבנית — העמודה ה-i ממלאת את template.parameters[i].
צמתי ה-collection מסוג CollectTemplateValues הם הקשר בין רגיסטרים וירטואליים לפרמטרי תבנית. כל צומת כזה מחזיק מידע אינדקסים שמתעד לאילו עמודות של מערך הפלט הוא כותב. כך אפשר לעקוב אחר אילו תת-מערכות של שערים וירטואליים מניעות אילו פרמטרי תבנית.
במדריך {Samplex inputs and outputs}samplex-io תלמדו איך לקשור את ערכי הפרמטרים שנדגמו למעגל התבנית ולהריץ ניסויים.
from samplomatic.samplex.nodes import CollectTemplateValues
template, samplex = build(circuit, debug=True)
for node in samplex.graph.nodes():
if isinstance(node, CollectTemplateValues):
tags = node.trace_info.trace_refs.get("tag", set()) if node.trace_info else set()
print(f"tags={tags} → template param indices: {node.template_idxs.tolist()}")
tags={'cx_cd'} → template param indices: [[6, 7, 8], [9, 10, 11]]
tags={'cx_ab', 'cx_cd'} → template param indices: [[12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23]]
tags=set() → template param indices: [[0, 1, 2], [3, 4, 5]]
תיוג אוטומטי
כשמשתמשים ב-generate_boxing_pass_manager(), הפרמטר add_tags מוסיף אוטומטית הערות Tag לכל הקופסאות. שלושה מצבים זמינים:
-
"unique_instance": מקצהrefרציפים (t0,t1וכן הלאה) ל-boxes לפי סדר המעגל. כל box מקבלrefייחודי, בלי קשר למבנה שלו. -
"unique_box": מחשב hash מבני של תוכן כל box ומקצה את אותוrefלכל ה-boxes השקולים מבחינה מבנית. זה שימושי לקיבוץ boxes לפי סוג ולא לפי מיקום. -
"noise_ref": מעתיק את ה-refמההערהInjectNoiseשל כל box ומתייג רק boxes שיש להם כזו. זה שימושי כשכבר קיימים noise refs משמעותיים.
הדוגמה הבאה מפעילה את "unique_instance" ואת "unique_box" על מעגל ששני ה-CX boxes שלו שקולים מבחינה מבנית. עם "unique_instance" כל אחד מקבל ref נפרד, ואילו עם "unique_box" הם חולקים אחד.
from samplomatic.transpiler import generate_boxing_pass_manager
base_circuit = QuantumCircuit(3)
base_circuit.cx(0, 1)
base_circuit.cx(1, 2)
base_circuit.measure_all()
# unique_instance: every box gets a distinct tag ref
pm = generate_boxing_pass_manager(add_tags="unique_instance")
boxed = pm.run(base_circuit)
template, _ = build(boxed)
print("unique_instance barrier labels:")
for instr in template:
if instr.operation.name == "barrier" and instr.operation.label:
print(f" {instr.operation.label}")
unique_instance barrier labels:
L0@tag=t0
M0@tag=t0
R0@tag=t0
L1@tag=t1
M1@tag=t1
R1@tag=t1
L2@tag=t2
M2@tag=t2
R2@tag=t2
