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

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

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

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

qiskit[all]~=2.5.2
qiskit-ibm-runtime~=0.47.0

דף זה נותן סקירה כללית של הקלטים והפלטים של פרימיטיב ה-Sampler של qiskit-ibm-runtime, שמריץ עומסי עבודה על IBM Quantum® Compute Service. Sampler מאפשר לכם להגדיר ביעילות עומסי עבודה וקטוריים באמצעות מבנה נתונים הידוע בשם Primitive Unified Bloc (PUB). הם משמשים כקלט למתודה run() של פרימיטיב ה-Sampler, שמריצה את עומס העבודה שהוגדר כמשימה. לאחר סיום המשימה, התוצאות מוחזרות בפורמט שתלוי הן ב-PUBs שבהם נעשה שימוש והן באפשרויות ה-Runtime שהוגדרו בפרימיטיב.

קלטים​

כל PUB הוא בפורמט:

(<single circuit>, <one or more optional parameter value>, <optional shots>),

יכולים להיות פריטי parameter values מרובים, וכל פריט יכול להיות מערך או פרמטר בודד, בהתאם למעגל שנבחר. בנוסף, הקלט חייב להכיל מדידות.

עבור הפרימיטיב Sampler, PUB יכול להכיל לכל היותר שלושה ערכים:

  • QuantumCircuit בודד, שעשוי להכיל אובייקט Parameter אחד או יותר הערה: מעגלים אלה צריכים לכלול גם הוראות מדידה לכל אחד מהקיוביטים שיש לדגום.
  • אוסף ערכי פרמטרים לקשירת המעגל לפיהם θk\theta_k (נדרש רק אם נעשה שימוש באובייקטי Parameter כלשהם שיש לקשור אותם בזמן ריצה)
  • (אופציונלי) מספר shots למדידת המעגל

הקוד הבא מדגים דוגמה של קלטים וקטוריים לפרימיטיב Sampler ומריץ אותם על Backend של IBM® כאובייקט RuntimeJobV2 בודד.

# 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"}
_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.circuit import (
Parameter,
QuantumCircuit,
ClassicalRegister,
QuantumRegister,
)
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives.containers import BitArray

from qiskit_ibm_runtime import QiskitRuntimeService

# Legacy server-side primitive before qiskit-ibm-runtime 0.50.0
# from qiskit_ibm_runtime import Sampler

# Client-side primitive introduced in qiskit-ibm-runtime 0.50.0
from qiskit_ibm_runtime.executor_sampler import Sampler

import numpy as np

# Instantiate runtime service and get
# the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# Define a circuit with two parameters.
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.ry(Parameter("a"), 0)
circuit.rz(Parameter("b"), 0)
circuit.cx(0, 1)
circuit.h(0)
circuit.measure_all()

# Transpile the circuit
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)
layout = transpiled_circuit.layout

# Now define a sweep over parameter values, the last axis of dimension 2 is
# for the two parameters "a" and "b"
params = np.vstack(
[
np.linspace(-np.pi, np.pi, 100),
np.linspace(-4 * np.pi, 4 * np.pi, 100),
]
).T

sampler_pub = (transpiled_circuit, params)

# Instantiate the new Sampler object, then run the transpiled circuit
# using the set of parameters and observables.
sampler = Sampler(mode=backend)
job = sampler.run([sampler_pub])
result = job.result()

פלטים​

לאחר שאחד או יותר PUBs נשלחים ל-QPU לביצוע ומשימה מסתיימת בהצלחה, הנתונים מוחזרים כאובייקט מיכל PrimitiveResult שניגש אליו על ידי קריאה לשיטת RuntimeJobV2.result(). ה-PrimitiveResult מכיל רשימה ניתנת לחזרה של אובייקטי SamplerPubResult המכילים את תוצאות הביצוע לכל PUB. נתונים אלה הם דגימות של פלט המעגל.

כל רכיב ברשימה זו תואם ל-PUB שהוגש לשיטת run() של הפרימיטיב (לדוגמה, משימה שהוגשה עם 20 PUBs תחזיר אובייקט PrimitiveResult המכיל רשימה של 20 אובייקטי SamplerPubResult, אחד לכל PUB).

לכל אובייקט SamplerPubResult יש גם מאפיין data וגם מאפיין metadata.

  • מאפיין data הוא DataBin מותאם אישית המכיל את ערכי המדידה בפועל, סטיות תקן וכן הלאה. מיכלי הנתונים הם אובייקטים דמויי-מילון המכילים BitArray אחד לכל ClassicalRegister במעגל.
  • מחלקה BitArray היא מיכל לנתוני shots מסודרים. היא מאחסנת את מחרוזות הביטים שנדגמו כבתים בתוך מערך דו-ממדי. הציר השמאלי ביותר של מערך זה עובר על פני shots מסודרים, בעוד שהציר הימני ביותר עובר על פני בתים.
  • מאפיין metadata מכיל מידע על אפשרויות ה-Runtime שנעשה בהן שימוש (מוסבר מאוחר יותר בסעיף מטא-דאטה של תוצאות בדף זה).

להלן מתאר חזותי של מבנה הנתונים PrimitiveResult:

└── PrimitiveResult
├── SamplerPubResult[0]
│ ├── metadata
│ └── data ## In the form of a DataBin object
│ ├── NAME_OF_CLASSICAL_REGISTER
│ │ └── BitArray of count data (default is 'meas')
| |
│ └── NAME_OF_ANOTHER_CLASSICAL_REGISTER
│ └── BitArray of count data (exists only if more than one
| ClassicalRegister was specified in the circuit)
├── SamplerPubResult[1]
| ├── metadata
| └── data ## In the form of a DataBin object
| └── NAME_OF_CLASSICAL_REGISTER
| └── BitArray of count data for second pub
├── ...
├── ...
└── ...

במילים פשוטות, משימה בודדת מחזירה אובייקט PrimitiveResult ומכילה רשימה של אחד או יותר אובייקטי SamplerPubResult. אובייקטי SamplerPubResult אלה מאחסנים את נתוני המדידה לכל PUB שהוגש למשימה.

כדוגמה ראשונה, בואו נסתכל על המעגל בעל עשרת הקיוביטים הבא:

# generate a ten-qubit GHZ circuit
circuit = QuantumCircuit(10)
circuit.h(0)
circuit.cx(range(0, 9), range(1, 10))

# append measurements with the `measure_all` method
circuit.measure_all()

# transpile the circuit
transpiled_circuit = pm.run(circuit)

# run the Sampler job and retrieve the results
sampler = Sampler(mode=backend)
job = sampler.run([transpiled_circuit])
result = job.result()

# the data bin contains one BitArray
data = result[0].data
print(f"Databin: {data}\n")

# to access the BitArray, use the key "meas", which is the default name of
# the classical register when this is added by the `measure_all` method
array = data.meas
print(f"BitArray: {array}\n")
print(f"The shape of register `meas` is {data.meas.array.shape}.\n")
print(f"The bytes in register `alpha`, shot by shot:\n{data.meas.array}\n")
Databin: DataBin(meas=BitArray(<shape=(), num_shots=4096, num_bits=10>))

BitArray: BitArray(<shape=(), num_shots=4096, num_bits=10>)

The shape of register `meas` is (4096, 2).

The bytes in register `alpha`, shot by shot:
[[ 3 247]
[ 0 0]
[ 3 223]
...
[ 0 255]
[ 0 0]
[ 0 0]]

לעיתים נוח להמיר מפורמט הבתים ב-BitArray למחרוזות ביטים. שיטת get_count מחזירה מילון שממפה מחרוזות ביטים למספר הפעמים שהן הופיעו.

# optionally, convert away from the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")
Counts: {'1111110111': 28, '0000000000': 1680, '1111011111': 35, '1111111111': 1467, '1110000000': 28, '0000000010': 15, '1111111011': 15, '1110111111': 7, '1000000000': 52, '1111101111': 16, '1111110000': 22, '0000000101': 3, '0001111101': 1, '0000000111': 17, '0000000001': 66, '1111111110': 119, '0000001000': 15, '0011111111': 24, '1111111101': 38, '0000001111': 32, '1100000000': 39, '1101111111': 51, '0000011111': 34, '1110000010': 2, '0010000000': 25, '0111111111': 21, '0001111111': 34, '1011111111': 16, '1000011111': 3, '0000001101': 1, '1111100000': 22, '1111111100': 22, '1111111000': 8, '0010001011': 1, '0011000000': 1, '0000000011': 16, '0011111100': 2, '1011110001': 1, '1101111100': 1, '1101111101': 1, '0000000110': 2, '0000000100': 2, '0001111110': 6, '0000001110': 4, '0011110111': 2, '1101111110': 3, '0111111011': 1, '0110000000': 1, '0000001011': 2, '1111110110': 3, '0111111000': 1, '1010000000': 2, '0001110111': 1, '1111101000': 1, '0010011110': 1, '1111100111': 1, '0111110111': 1, '0100000000': 5, '1101110111': 1, '0000001001': 1, '0010000001': 1, '1111000001': 1, '0000010000': 3, '1111101110': 1, '1111000000': 8, '0001011111': 3, '1000000001': 1, '1111010111': 2, '0010011111': 1, '1011111110': 1, '1101101111': 1, '1111011110': 2, '1111111010': 1, '1111010000': 1, '1101000000': 1, '1100001111': 1, '1100011111': 1, '0000111111': 4, '0010111111': 1, '1000000110': 1, '1110111110': 1, '1101011111': 2, '0000011000': 1, '1101110000': 2, '1011111011': 1, '1000111111': 1, '1011000000': 1, '1110000001': 2, '0101111111': 2, '1111110101': 1, '0010000011': 1, '0000011110': 1, '0000011101': 1, '0111000000': 1, '0001110000': 1, '0001000000': 4, '1101111000': 1, '1011111000': 1, '0111110000': 1, '1101110110': 1, '0011011111': 1, '0000100000': 2, '1111101100': 1, '1100001000': 1, '1010000011': 1, '0011111110': 1, '1010001110': 1, '1000001000': 1, '1000000011': 1, '1111101011': 1}

כאשר מעגל מכיל יותר מרג'יסטר קלאסי אחד, התוצאות מאוחסנות באובייקטי BitArray שונים. הדוגמה הבאה משנה את הקטע הקודם על ידי פיצול הרג'יסטר הקלאסי לשני רג'יסטרים נפרדים:

# generate a ten-qubit GHZ circuit with two classical registers
circuit = QuantumCircuit(
qreg := QuantumRegister(10),
alpha := ClassicalRegister(1, "alpha"),
beta := ClassicalRegister(9, "beta"),
)
circuit.h(0)
circuit.cx(range(0, 9), range(1, 10))

# append measurements with the `measure_all` method
circuit.measure([0], alpha)
circuit.measure(range(1, 10), beta)

# transpile the circuit
transpiled_circuit = pm.run(circuit)

# run the Sampler job and retrieve the results
sampler = Sampler(mode=backend)
job = sampler.run([transpiled_circuit])
result = job.result()

# the data bin contains two BitArrays, one per register, and can be accessed
# as attributes using the registers' names
data = result[0].data
print(f"BitArray for register 'alpha': {data.alpha}")
print(f"BitArray for register 'beta': {data.beta}")
BitArray for register 'alpha': BitArray(<shape=(), num_shots=4096, num_bits=1>)
BitArray for register 'beta': BitArray(<shape=(), num_shots=4096, num_bits=9>)

שימוש באובייקטי BitArray לעיבוד-המשך יעיל של התוצאות​

מכיוון שמערכים מציעים בדרך כלל ביצועים טובים יותר בהשוואה למילונים, מומלץ לבצע כל עיבוד לאחר ישירות על אובייקטי BitArray ולא על מילונים של ספירות. מחלקת BitArray מציעה מגוון שיטות לביצוע כמה פעולות עיבוד לאחר נפוצות:

print(f"The shape of register `alpha` is {data.alpha.array.shape}.")
print(f"The bytes in register `alpha`, shot by shot:\n{data.alpha.array}\n")

print(f"The shape of register `beta` is {data.beta.array.shape}.")
print(f"The bytes in register `beta`, shot by shot:\n{data.beta.array}\n")

# post-select the bitstrings of `beta` based on having sampled "1" in `alpha`
mask = data.alpha.array == 0b1
ps_beta = data.beta[mask[:, 0]]
print(f"The shape of `beta` after post-selection is {ps_beta.array.shape}.")
print(f"The bytes in `beta` after post-selection:\n{ps_beta.array}")

# get a slice of `beta` to retrieve the first three bits
beta_sl_bits = data.beta.slice_bits([0, 1, 2])
print(
f"The shape of `beta` after bit-wise slicing is {beta_sl_bits.array.shape}."
)
print(f"The bytes in `beta` after bit-wise slicing:\n{beta_sl_bits.array}\n")

# get a slice of `beta` to retrieve the bytes of the first five shots
beta_sl_shots = data.beta.slice_shots([0, 1, 2, 3, 4])
print(
f"The shape of `beta` after shot-wise slicing is {beta_sl_shots.array.shape}."
)
print(
f"The bytes in `beta` after shot-wise slicing:\n{beta_sl_shots.array}\n"
)

# calculate the expectation value of diagonal operators on `beta`
ops = [SparsePauliOp("ZZZZZZZZZ"), SparsePauliOp("IIIIIIIIZ")]
exp_vals = data.beta.expectation_values(ops)
for o, e in zip(ops, exp_vals):
print(f"Exp. val. for observable `{o}` is: {e}")

# concatenate the bitstrings in `alpha` and `beta` to "merge" the results of the two
# registers
merged_results = BitArray.concatenate_bits([data.alpha, data.beta])
print(f"\nThe shape of the merged results is {merged_results.array.shape}.")
print(f"The bytes of the merged results:\n{merged_results.array}\n")
The shape of register `alpha` is (4096, 1).
The bytes in register `alpha`, shot by shot:
[[1]
[0]
[1]
...
[0]
[1]
[0]]

The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 1 255]
[ 0 0]
[ 1 255]
...
[ 0 0]
[ 1 255]
[ 0 0]]

The shape of `beta` after post-selection is (0, 2).
The bytes in `beta` after post-selection:
[]
The shape of `beta` after bit-wise slicing is (4096, 1).
The bytes in `beta` after bit-wise slicing:
[[7]
[0]
[7]
...
[0]
[7]
[0]]

The shape of `beta` after shot-wise slicing is (5, 2).
The bytes in `beta` after shot-wise slicing:
[[ 1 255]
[ 0 0]
[ 1 255]
[ 1 255]
[ 1 255]]

Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
coeffs=[1.+0.j])` is: 0.0595703125
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
coeffs=[1.+0.j])` is: 0.02783203125

The shape of the merged results is (4096, 2).
The bytes of the merged results:
[[ 3 255]
[ 0 0]
[ 3 255]
...
[ 0 0]
[ 3 255]
[ 0 0]]

מטא-דאטה של תוצאות​

בנוסף לתוצאות הביצוע, גם אובייקט PrimitiveResult וגם אובייקט SamplerPubResult מכילים מאפיין מטא-דאטה על המשימה שהוגשה. המטא-דאטה המכילה מידע לכל ה-PUBs שהוגשו (כגון אפשרויות ה-Runtime השונות הזמינות) ניתן למצוא ב-PrimitiveResult.metatada, בעוד שהמטא-דאטה הספציפית לכל PUB נמצאת ב-SamplerPubResult.metadata.

ה-Sampler בצד הלקוח שולח את עומס העבודה שלו כמשימת Executor. המטא-דאטה של תוצאת ה-Executor הבסיסית, הכוללת מידע על עיתוי ביצוע שנקרא execution spans, זמינה תחת המפתח "executor" של PrimitiveResult.metadata.

הערה

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

# Print out the results metadata
print("The metadata of the PrimitiveResult is:")
for key, val in result.metadata.items():
print(f"'{key}' : {val},")

print("\nThe metadata of the PubResult result is:")
for key, val in result[0].metadata.items():
print(f"'{key}' : {val},")
The metadata of the PrimitiveResult is:
'execution' : {'execution_spans': ExecutionSpans([DoubleSliceSpan(<start='2026-09-01 07:46:47', stop='2026-09-01 07:46:49', size=4096>)])},
'version' : 2,

The metadata of the PubResult result is:
'circuit_metadata' : {},

הצגת טווחי ביצוע​

ה-Sampler בצד הלקוח משתמש ב-Executor להרצת משימות, והוא מבצע את עבודתו על ה-QPU ב-chunks. העיתוי של כל chunk נמצא ב-result.metadata["executor"].chunk_timing, רשימה של אובייקטי ChunkSpan. לכל ChunkSpan יש חותמות זמן start ו-stop ורשימה של parts, שבה כל part מציין אינדקס פריט (idx_item) ואת מספר התצורות מאותו פריט (size) שרצו ב-chunk.

Sampler יוצר פריט תוכנית Executor אחד לכל PUB, ולכן idx_item הוא האינדקס של ה-PUB, לפי הסדר שבו ה-PUBs נמסרו ל-run(). אם twirling מופעל, כל randomization של כל קבוצת ערכי פרמטרים נחשבת לתצורה נפרדת.

כדי להציג את העיתוי בצורה חזותית, העבר את הרשימה ל-draw_chunk_timings, כמו בדוגמה הבאה.

כדי להבין מה משמעות חותמות הזמן של ה-chunks, איך לקרוא את הגרף, ולמה עלולים להופיע פערים בין chunks, ראה הצגת execution spans במדריך הקלטים והפלטים של Executor.

# Define a circuit with one parameter.
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.ry(Parameter("a"), 0)
circuit.cx(0, 1)
circuit.h(0)
circuit.measure_all()

pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)

# Define two PUBs, each with a different set of parameter values
sampler_pubs = [
(transpiled_circuit, np.random.uniform(size=(10, 1))),
(transpiled_circuit, np.random.uniform(size=(20, 1))),
]

job = sampler.run(sampler_pubs, shots=1024)
result = job.result()
from qiskit_ibm_runtime.visualization import draw_chunk_timings

# Note the "executor" key: the timing comes from the underlying Executor job
chunk_timing = result.metadata["executor"].chunk_timing

for idx, span in enumerate(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" PUB {part.idx_item}: {part.size} configurations")

fig = draw_chunk_timings(chunk_timing, names="Sampler job")

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

# Save to a file
# fig.write_html("chunk_timings.html")
Chunk 0: 2026-11-06 01:36:36.203348+00:00 to 2026-11-06 01:36:45.420844+00:00 (9.217 s)
PUB 0: 10 configurations
PUB 1: 20 configurations

Execution spans ב-Sampler הישן​

החל מ-qiskit-ibm-runtime גרסה v0.50.0, ה-Sampler הישן בצד השרת הוצא משימוש. ה-Sampler בצד הלקוח אינו מחזיר אובייקטי ExecutionSpans; השתמש במקום זאת ב-chunk timing שתואר קודם. ראה מעבר ל-primitives בצד הלקוח להוראות המעבר.

לחץ כדי ללמוד עוד.

תוצאות של משימות מה-Sampler הישן בצד השרת מכילות מידע על עיתוי ביצוע ב-result.metadata["execution"]["execution_spans"], בצורת אובייקט ExecutionSpans. אובייקט זה הוא מכל דמוי רשימה של אובייקטי ExecutionSpan, שכל אחד מהם מציין זמן התחלה, זמן עצירה ופירוט של אילו shots נאספו בטווח.

execution span מציין אילו נתונים בוצעו במהלך חלון הזמן שלו באמצעות המתודה ExecutionSpan.mask. בהינתן אינדקס PUB כלשהו, מתודה זו מחזירה מסיכה בוליאנית שהיא True לכל ה-shots שבוצעו במהלך חלון הזמן שלו. אם, למשל, ל-PUB יש צורה (2, 3) והוא הורץ עם ארבעה shots, הצורה של המסיכה היא (2, 3, 4). ראה את דף ה-API של execution_span לפרטים מלאים.

from qiskit.primitives import BitArray

spans = result.metadata["execution"]["execution_spans"]

# Get the mask of the 1st PUB for the 0th span.
mask = spans[0].mask(0)

# Create a new bit array containing only the PUB-0 data collected during this span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)

# Take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])

# View global information about the collection of execution spans
print("Number of execution spans:", len(spans))
print(" Start of the first span:", spans.start)
print(" End of the last span:", spans.stop)
print(" Total duration (s):", spans.duration)

כדי להציג בצורה חזותית execution spans ישנים, השתמש בפונקציה draw_execution_spans.

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