דלג לתוכן הראשי
פרויקט זה בשלב בטא. מצאת באג? פתח issue ב-GitHub. יש שאלה או רעיון? שאל ב-Discussions — נשמח לשמוע ממך!
מה איכות התרגום בדף זה?

קלטים ופלטים של Executor

גרסאות חבילות

הקוד בדף זה פותח תוך שימוש בדרישות הבאות. אנחנו ממליצים להשתמש בגרסאות אלה או חדשות יותר.

qiskit[all]~=2.6.1
qiskit-ibm-runtime~=0.50.0
samplomatic~=0.21.0

ה-primitive של Executor הוא חלק ממודל ההרצה המכוונת, שמספק גמישות רבה יותר בהתאמה אישית של תהליך עבודת הפחתת שגיאות.

הקלטים והפלטים של ה-primitive של Executor שונים מאוד מאלו של ה-primitives של Sampler ו-Estimator. לדוגמה, במקום לקבל רשימת PUBs כקלט, Executor מקבל QuantumProgram, שמכיל רשימת אובייקטי QuantumProgramItem. מחלקות מכילות אלה נותנות לך גמישות רבה יותר מ-PUB, שהוא מבנה נתונים tuple פשוט.

הפלט של Executor הוא QuantumProgramResult, שהוא iterable ומכיל אלמנט אחד לכל QuantumProgramItem קלט.

קלטים: תוכניות קוונטיות​

כפי שצוין קודם, הקלט ל-primitive של Executor הוא QuantumProgram, שהוא iterable של אובייקטי QuantumProgramItem. אובייקטים אלה יכולים להיות משני סוגים:

  • CircuitItem, שבדרך כלל מאחסן מעגל וערכי הפרמטרים שלו (אם יש).
  • SamplexItem, שבדרך כלל מאחסן את הבאים:
    • מעגל template
    • אובייקט samplex, שמשמש לייצור סדרות פרמטרים אקראיות בזמן ריצה (לדוגמה לביצוע twirling או הזרקת רעש)
    • ארגומנטים עבור ה-samplex, שעשויים לכלול ערכי פרמטרים עבור המעגל המקורי

כל אחד מהפריטים הללו מייצג משימה שונה שעל Executor לבצע.

לפני שמתחילים​

חלק מדוגמאות הקוד בדף זה משתמשות ב-samplex, שהוא חלק מחבילת Samplomatic. לכן, לפני הרצת בלוקי קוד אלה, חייבים להתקין את Samplomatic, כפי שמוצג בבלוק הקוד הבא. למידע נוסף, ראו את תיעוד Samplomatic.

pip install samplomatic

# For visualization support, include the visualization dependencies.
# pip install samplomatic[vis]

דוגמה: יצירת QuantumProgram עם שתי משימות שונות​

קודם מאתחלים את התוכנית הקוונטית שלכם, ואז מוסיפים לה פריטי תוכנית באמצעות append_circuit_item או append_samplex_item (אם יש samplex), כפי שמוצג בדוגמאות הבאות.

התא הבא מאתחל QuantumProgram ומציין שצריך להריץ 1024 shots עבור כל תצורה של כל פריט בתוכנית.

הערה

בשונה מ-Sampler, QuantumProgram מקבל רק ערך shot בודד. אם רוצים ערך shot שונה, צריך QuantumProgram נפרד, שיהיה עבודה נפרדת.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "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")
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from qiskit_ibm_runtime import Executor, QiskitRuntimeService
from qiskit.circuit import Parameter, QuantumCircuit
import numpy as np
from samplomatic import build
from samplomatic.transpiler import generate_boxing_pass_manager

# Initialize an empty program
program = QuantumProgram(shots=1024)

# Initialize and transpile a 3-qubit quantum circuit with 2 parameters.
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.rz(Parameter("theta"), 0)
circuit.rz(Parameter("phi"), 1)

# `measure_all` adds a 3-bit classical register named "meas"
circuit.measure_all()

# Choose the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# Generate a preset pass manager
# This will be used to convert the abstract circuit to an
# equivalent Instruction Set Architecture (ISA) circuit.
preset_pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=0
)

# Transpile the circuit
isa_circuit = preset_pass_manager.run(circuit)

הוספת CircuitItem​

לאחר מכן, הוסיפו את מעגל היעד, שבוצע עליו transpile לפי ה-instruction set architecture (ISA) של ה-backend, ל-QuantumProgram. מכיוון שלמעגל זה יש שני פרמטרים, עלינו גם לספק את ערכי הפרמטרים (10 סדרות בדוגמה זו). הרצת CircuitItem זה היא המשימה הראשונה שהתוכנית תבצע.

# Append the transpiled circuit and an array
# containing 10 sets of parameter values to the program
program.append_circuit_item(
isa_circuit,
circuit_arguments=np.random.rand(
10, 2
), # 10 sets of parameter values and 2 parameters
)

הוספת SamplexItem​

פריטי מעגל מורצים ללא שום רנדומיזציה. לעומת זאת, פריטי samplex מאפשרים לכם לקבוע כיצד לבצע רנדומיזציה לתוכן שלהם. התא הבא משתמש בפונקציה generate_boxing_pass_manager() כדי לקבץ את שערי המעגל והמדידות ל-boxes ולהוסיף annotation של twirling לכל box. לאחר מכן הוא מייצר זוג של מעגל template ו-samplex באמצעות הפונקציה build().

הרצת SamplexItem זה היא המשימה השנייה שהתוכנית תבצע.

ראו את תיעוד ה-API של Samplomatic לפרטים מלאים על samplex וארגומנטיו. ראו את מדריך ה-Transpiler של Samplomatic למידע על שימוש בפונקציה generate_boxing_pass_manager().

# Transpile the circuit, additionally grouping gates and measurements into annotated boxes
preset_pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=0
)

# Use the boxing pass manager to group gates
# and measurements into boxes and add
# a`Twirl` annotation.
preset_pass_manager.post_scheduling = generate_boxing_pass_manager(
# Add gate twirling
enable_gates=True,
# Add measurement twirling
enable_measures=True,
)
boxed_circuit = preset_pass_manager.run(circuit)

# Build the template circuit and the samplex. The template circuit has parametric gates
# without fixed values and the samplex randomly generates the parameter
# values on the server side at runtime to perform twirling.
template_circuit, samplex = build(boxed_circuit)

# Determine what arguments are required by the samplex.
# Input the arguments in samplex_arguments.
print(samplex.inputs())
TensorInterface(<
- 'parameter_values' <float64[2]>: Input parameter values to use during sampling.
>)
# Append the template circuit and samplex as a samplex item
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
# the arguments required by the samplex.sample method
"parameter_values": np.random.rand(10, 2),
},
shape=(28, 10), # 28 randomizations and 10 sets of parameter values
)
# Initialize an Executor with the default options
executor = Executor(mode=backend)

# Submit the job
job = executor.run(program)

# Retrieve the result
result = job.result()

פלטים​

הפלט של Executor הוא QuantumProgramResult, שהוא iterable. הוא מכיל ערך אחד לכל QuantumProgramItem קלט באותו סדר כמו פריטי הקלט. כל אחד מפריטי הפלט הללו הוא מילון שבו המפתחות הם מחרוזות המתאימות לשמות הרגיסטרים הקלאסיים במעגלי הקלט (בין היתר), כך שכבר אין צורך לזכור שמות אלה כפי שהיה צריך עם פלט Sampler. ערכי המילון הם מסוג np.ndarray.

התוצאה עבור הדוגמה הקודמת מכילה את הפריטים הבאים:

תוצאת CircuitItem​

הפריט הראשון מכיל את תוצאות הרצת המשימה הראשונה (CircuitItem) בתוכנית. הוא מכיל מפתח בודד, meas, שהוא שם הרגיסטר הקלאסי במעגל הקלט. ערך מפתח זה ממפה ל-np.ndarray בצורה (parameter sets, shots, register bits), שהיא (10, 1024, 3) בדוגמה לעיל.

הקוד הבא ממחיש כיצד לגשת למידע זה:

# Access the results of the classical register of task #0, a CircuitItem
result_0 = result[0]["meas"]
print(f"Result shape: {result_0.shape}")
Result shape: (10, 1024, 3)

הציר הימני ביותר של result_0 עובר על הסיביות הקלאסיות בסדר little-endian. לכן, אינדקס 0 מכיל את תוצאת הסיבית הקלאסית 0, הסיבית הכי פחות משמעותית.

תוצאת SamplexItem​

הפריט השני מכיל את תוצאות הרצת המשימה השנייה (SamplexItem) בתוכנית. פריט זה מכיל מספר מפתחות. המפתח meas, שהוא שם הרגיסטר הקלאסי של מעגל הקלט, ממפה למערך תוצאות של אותו רגיסטר. למערך זה יש צורה (randomizations, parameter sets, shots, classical bits), או (28, 10, 1024, 3) בדוגמה זו.

בנוסף, הפלט מכיל מפתח measurement_flips.meas, שהוא תיקוני היפוך סיביות לביטול ה-measurement twirling עבור רגיסטר ה-meas. צורת פלט זו תהיה (28, 10, 1, 3) בדוגמה שלנו מכיוון שנדרש רק shot אחד לביצוע ההיפוך.

שימו לב שהציר הימני ביותר של result_1 עובר על הסיביות הקלאסיות בסדר little-endian.

# Access the results of the classical register of task #1
result_1 = result[1]["meas"]
print(f"Result shape: {result_1.shape}")

# Access the bit-flip corrections
flips_1 = result[1]["measurement_flips.meas"]
print(f"Bit-flip corrections shape: {flips_1.shape}")

# Undo the bit flips via classical XOR
unflipped_result_1 = result_1 ^ flips_1
Result shape: (28, 10, 1024, 3)
Bit-flip corrections shape: (28, 10, 1, 3)

הצגת execution spans​

קריאה אחת ל-Executor.run() שולחת משימה אחת. אולם בצד השרת, השירות לא בהכרח מריץ כל תצורה של התוכנית הקוונטית בו-זמנית. במקום זאת, הוא מקבץ את המעגלים הקשורים של פריטי התוכנית ל_נתחים_ (chunks), ומריץ נתח אחד בכל פעם על ה-QPU. אין צורך לפצל את התוכנית או לשלוח משימות נוספות; החלוקה לנתחים מתרחשת אוטומטית בתוך המשימה האחת. התכונה metadata של התוצאה מכילה מידע תזמון על הנתחים האלה ב-result.metadata.chunk_timing. חותמות הזמן האלה הן זמני ההתחלה והסיום בפועל של כל נתח על ה-QPU. אפשר להשתמש במידע הזה כדי לראות מתי נאספו נתונים מסוימים על ה-QPU, להבין איך Executor חילק את העבודה, או לאבחן בעיות ביצועים.

chunk_timing היא רשימה שמכילה ChunkSpan אחד לכל נתח שהורץ. לכל ChunkSpan יש את התכונות הבאות:

  • start ו-stop: חותמות הזמן ב-UTC שבהן הנתח התחיל והפסיק לרוץ על ה-QPU.

  • parts: רשימה של אובייקטי ChunkPart שמתארים אילו פריטי תוכנית תרמו לנתח. כל ChunkPart מכיל את הבא:

    • idx_item: האינדקס של פריט התוכנית, לפי הסדר שבו הפריטים נוספו ל-QuantumProgram.

    • size: מספר התצורות (איברים בצורה של הפריט) מאותו פריט שהורצו בנתח הזה. לדוגמה, ל-SamplexItem בדוגמה הקודמת יש צורה (28, 10), ולכן יש לו 280 תצורות בסך הכול.

הקוד הבא מדפיס את משך הזמן של כל נתח ואת התוכן של כל נתח עבור המשימה שנשלחה קודם:

for idx, span in enumerate(result.metadata.chunk_timing):
duration = (span.stop - span.start).total_seconds()
print(f"Chunk {idx}: {span.start} to {span.stop} ({duration:.3f} s)")
for part in span.parts:
print(f" Item {part.idx_item}: {part.size} configurations")
Chunk 0: 2026-11-06 01:47:46.009718+00:00 to 2026-11-06 01:49:05.577720+00:00 (79.568 s)
Item 0: 10 configurations
Item 1: 280 configurations

התכונה QuantumProgramResult.timing עוטפת את אותה רשימה באובייקט ChunkTiming, שמדווח גם על מידע כולל על אוסף הנתחים:

timing = result.timing

print("Number of chunks:", len(timing))
print(" Start of the first chunk:", timing.start)
print(" End of the last chunk:", timing.stop)
print(" Total duration (s):", timing.duration)
Number of chunks: 1
Start of the first chunk: 2026-11-06 01:47:46.009718+00:00
End of the last chunk: 2026-11-06 01:49:05.577720+00:00
Total duration (s): 79.568002

הצגה חזותית של execution spans​

השתמש בפונקציה draw_chunk_timings כדי להציג את תזמוני הנתחים בגרף עמודות אינטראקטיבי. ציר ה-x הוא הזמן, וכל נתח מצויר כקטע אופקי שמשתרע בין זמני ההתחלה והסיום שלו. ציר ה-y מציג את המספר המצטבר של תצורות שהושלמו (או את האחוז מעומס העבודה הכולל, אם מגדירים normalize_y=True). רחף מעל קטע כדי לראות את חותמות הזמן, המשך והחלקים של אותו נתח.

הפונקציה מחזירה איור plotly, שאפשר להציג ישירות, לשמור לקובץ, או שניהם. היא מקבלת את הרשימה chunk_timing או אובייקט ChunkTiming. אפשר להעביר כמה מהם, למשל ממשימות שונות, כדי לצייר אותם באותו גרף. הגדר common_start=True כדי ליישר אותם לזמן התחלה משותף לצורך השוואה.

נדרש Plotly

draw_chunk_timings דורשת את plotly. אפשר להתקין אותה באמצעות הרצת pip install plotly.

רווחים בין נתחים

ההצגה החזותית עשויה להראות רווחים בין הרצות הנתחים. כל המשימות שנשלחות ל-QPU אמיתי עוברות עיבוד מקדים קלאסי כלשהו, כמו תרגום לפולסים שאפשר לשלוח לחומרה. מכיוון שאפשר לבצע את העיבוד המקדים הזה במקביל, השירות מוציא מהתור ומתחיל כמה משימות בו-זמנית. ה-QPU, לעומת זאת, יכול לעבד רק יחידת עבודה אחת בכל פעם. לכן, אם משימה מייצרת כמה נתחים, הנתחים שלה עשויים להשתלב עם נתחים של משימות של משתמשים אחרים, וכל נתח עשוי להמתין לתורו על ה-QPU. זו הסיבה שיכולים להופיע רווחים בין נתחים.

from qiskit_ibm_runtime.visualization import draw_chunk_timings

fig = draw_chunk_timings(result.timing, names="Executor job")

# Alternatively, call the `draw()` method of the ChunkTiming object
# fig = result.timing.draw()

# Uncomment the following line to display the figure
# fig.show(renderer="notebook")

# Save to a file
# fig.write_html("chunk_timings.html")

השלבים הבאים​

המלצות