Input e output del Sampler
Versioni dei pacchetti
Il codice di questa pagina è stato sviluppato con i seguenti requisiti. Si consiglia di utilizzare queste versioni o versioni più recenti.
qiskit[all]~=2.5.2
qiskit-ibm-runtime~=0.47.0
Questa pagina fornisce una panoramica degli input e degli output della primitiva qiskit-ibm-runtime Sampler, che esegue carichi di lavoro su IBM Quantum® Compute Service. Sampler ti permette di definire efficientemente carichi di lavoro vettorizzati utilizzando una struttura dati nota come Primitive Unified Bloc (PUB). Vengono usati come input per il metodo run() della primitiva Sampler, che esegue il carico di lavoro definito come un job. Poi, dopo che il job è stato completato, i risultati vengono restituiti in un formato che dipende sia dai PUB utilizzati che dalle opzioni di runtime specificate dalla primitiva.
Input
Ogni PUB è nel formato:
(<single circuit>, <one or more optional parameter value>, <optional shots>),
Possono esserci più elementi parameter values, e ciascun elemento può essere un array o un singolo parametro, a seconda del circuito scelto. Inoltre, l'input deve contenere misurazioni.
Per la primitiva Sampler, un PUB può contenere al massimo tre valori:
- Un singolo
QuantumCircuit, che può contenere uno o più oggettiParameterNota: Questi circuiti devono includere anche istruzioni di misurazione per ciascuno dei qubit da campionare. - Una raccolta di valori di parametri per vincolare il circuito contro (necessario solo se vengono utilizzati oggetti
Parameterche devono essere vincolati al runtime) - (Opzionalmente) un numero di shots con cui misurare il circuito
Il codice seguente mostra un esempio di set di input vettorizzati per la primitiva Sampler e li esegue su un backend IBM® come un singolo oggetto 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()
Output
Dopo che uno o più PUB vengono inviati a un QPU per l'esecuzione e un job si completa con successo, i dati vengono restituiti come oggetto contenitore PrimitiveResult accessibile chiamando il metodo RuntimeJobV2.result(). Il PrimitiveResult contiene un elenco iterabile di oggetti SamplerPubResult che contengono i risultati di esecuzione per ogni PUB. Questi dati sono campioni dell'output del circuito.
Ogni elemento di questo elenco corrisponde a un PUB inviato al metodo run() della primitiva (ad esempio, un job inviato con 20 PUB restituirà un oggetto PrimitiveResult che contiene un elenco di 20 oggetti SamplerPubResult, uno corrispondente a ogni PUB).
Ogni oggetto SamplerPubResult possiede sia un attributo data che un attributo metadata.
- L'attributo
dataè unDataBinpersonalizzato che contiene i valori di misurazione effettivi, le deviazioni standard e così via. I data bin sono oggetti simili a dizionari che contengono unBitArrayper ogniClassicalRegisternel circuito. - La classe
BitArrayè un contenitore per i dati di shots ordinati. Memorizza i bitstring campionati come byte all'interno di un array bidimensionale. L'asse più a sinistra di questo array scorre attraverso gli shots ordinati, mentre l'asse più a destra scorre attraverso i byte. - L'attributo
metadatacontiene informazioni sulle opzioni di runtime utilizzate (spiegato più avanti nella sezione Metadati dei risultati di questa pagina).
Di seguito è riportato uno schema visivo della struttura dati 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
├── ...
├── ...
└── ...
In sintesi, un singolo job restituisce un oggetto PrimitiveResult e contiene un elenco di uno o più oggetti SamplerPubResult. Questi oggetti SamplerPubResult memorizzano poi i dati di misurazione per ogni PUB inviato al job.
Come primo esempio, esaminiamo il seguente circuito a dieci qubit:
# 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]]
A volte può essere comodo convertire dal formato byte del BitArray a bitstring. Il metodo get_count restituisce un dizionario che mappa i bitstring al numero di volte che si sono verificati.
# 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}
Quando un circuito contiene più di un registro classico, i risultati vengono memorizzati in diversi oggetti BitArray. L'esempio seguente modifica lo snippet precedente dividendo il registro classico in due registri distinti:
# 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>)
Usare gli oggetti BitArray per il post-processing performante
Poiché gli array offrono generalmente prestazioni migliori rispetto ai dizionari, è consigliabile eseguire qualsiasi post-processing direttamente sugli oggetti BitArray piuttosto che sui dizionari di conteggi. La classe BitArray offre una serie di metodi per eseguire alcune operazioni di post-processing comuni:
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]]
Metadati dei risultati
Oltre ai risultati di esecuzione, sia l'oggetto PrimitiveResult che l'oggetto SamplerPubResult contengono un attributo di metadati sul job inviato. I metadati contenenti informazioni per tutti i PUB inviati (come le varie opzioni di runtime disponibili) si trovano in PrimitiveResult.metatada, mentre i metadati specifici di ogni PUB si trovano in SamplerPubResult.metadata.
Il Sampler lato client invia il suo workload come job Executor. I metadati del risultato Executor sottostante, che includono le informazioni di temporizzazione dell'esecuzione chiamate execution spans, sono disponibili sotto la chiave "executor" di PrimitiveResult.metadata.
Nel campo dei metadati, le implementazioni primitive possono restituire qualsiasi informazione sull'esecuzione che sia rilevante per loro, e non ci sono coppie chiave-valore garantite dalla primitiva di base. Pertanto, i metadati restituiti potrebbero essere diversi in diverse implementazioni primitive.
# 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' : {},
Visualizzare gli span di esecuzione
Il Sampler lato client usa Executor per eseguire i job, che esegue il suo lavoro sulla QPU in chunk. La temporizzazione di ogni chunk si trova in result.metadata["executor"].chunk_timing, una lista di oggetti ChunkSpan. Ogni ChunkSpan ha timestamp start e stop e una lista di parts, dove ogni parte indica un indice di elemento (idx_item) e il numero di configurazioni di quell'elemento (size) eseguite nel chunk.
Sampler crea un elemento di programma Executor per ogni PUB, quindi idx_item è l'indice del PUB, nell'ordine in cui i PUB sono stati passati a run(). Se il twirling è abilitato, ogni randomizzazione di ogni insieme di valori dei parametri conta come una configurazione separata.
Per visualizzare le temporizzazioni, passa la lista a draw_chunk_timings, come mostrato nell'esempio seguente.
Per scoprire cosa significano i timestamp dei chunk, come leggere il grafico e perché possono comparire lacune tra i chunk, consulta View execution spans nella guida Executor inputs and outputs.
# 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
Span di esecuzione nel Sampler legacy
A partire da qiskit-ibm-runtime v0.50.0, il Sampler legacy lato server è deprecato. Il Sampler lato client non restituisce oggetti ExecutionSpans; usa invece i tempi dei chunk descritti in precedenza. Consulta Migrate to client-side primitives per le istruzioni di migrazione.
Fai clic per saperne di più.
I risultati dei job del Sampler legacy lato server contengono informazioni di temporizzazione dell'esecuzione in result.metadata["execution"]["execution_spans"], sotto forma di oggetto ExecutionSpans. Questo oggetto è un contenitore simile a una lista di oggetti ExecutionSpan, ognuno dei quali indica un'ora di inizio, un'ora di fine e una specifica di quali shot sono stati raccolti nello span.
Uno span di esecuzione specifica quali dati sono stati eseguiti durante la sua finestra fornendo un metodo ExecutionSpan.mask. Questo metodo, dato qualsiasi indice di PUB, restituisce una maschera booleana che è True per tutti gli shot eseguiti durante la sua finestra. Se, ad esempio, un PUB ha forma (2, 3) ed è stato eseguito con quattro shot, la forma della maschera è (2, 3, 4). Consulta la pagina API execution_span per i dettagli completi.
from qiskit.primitives import BitArray
spans = result.metadata["execution"]["execution_spans"]
# Ottieni la maschera del 1° PUB per lo span 0.
mask = spans[0].mask(0)
# Crea un nuovo bit array contenente solo i dati del PUB 0 raccolti durante questo span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)
# Prendi il sottoinsieme di span che fanno riferimento a dati nei PUB 0 o 2
spans.filter_by_pub([0, 2])
# Visualizza le informazioni globali sulla collezione di span di esecuzione
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)
Per visualizzare gli span di esecuzione legacy, usa la funzione draw_execution_spans.
È possibile che le finestre temporali specificate da span di esecuzione distinti si sovrappongano. Questo non accade perché una QPU stava eseguendo più esecuzioni simultaneamente, ma è invece un artefatto di una certa elaborazione classica che potrebbe avvenire in parallelo con l'esecuzione quantistica. La garanzia fornita è che i dati referenziati si sono definitivamente verificati nello span di esecuzione riportato, ma non necessariamente che i limiti della finestra temporale siano quanto più stretti possibile.