Vai al contenuto principale

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.4.0
qiskit-ibm-runtime~=0.46.1

Questa pagina offre una panoramica degli input e output della primitiva Qiskit Runtime Sampler, che esegue carichi di lavoro sulle risorse di calcolo IBM Quantum®. 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:

(<circuito singolo>, <uno o più valori di parametri opzionali>, <shots opzionali>),

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ù oggetti Parameter Nota: 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 θk\theta_k (necessario solo se vengono utilizzati oggetti Parameter che devono essere vincolati al runtime)
  • (Opzionalmente) un numero di shots con cui misurare il circuito

Il codice seguente illustra un esempio di un insieme di input vettorizzati per la primitiva Sampler e li esegue su un backend IBM® come un singolo oggetto RuntimeJobV2.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime
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,
SamplerV2 as 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 è un DataBin personalizzato che contiene i valori di misurazione effettivi, le deviazioni standard e così via. I data bin sono oggetti simili a dizionari che contengono un BitArray per ogni ClassicalRegister nel 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 metadata contiene 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:
[[ 0 0]
[ 3 255]
[ 0 0]
...
[ 3 255]
[ 2 255]
[ 3 255]]

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: {'0000000000': 1649, '1111111111': 1344, '1111111000': 26, '1101111111': 40, '1111110000': 20, '0010000000': 32, '1000000000': 67, '1111110110': 4, '0000011110': 4, '0000000001': 78, '0010100000': 1, '1100000000': 37, '1111111110': 126, '1111110111': 35, '1111011111': 32, '0011111000': 1, '1011110111': 1, '0000011111': 48, '1111000000': 14, '0110000000': 1, '1110111110': 2, '1110011111': 4, '1111100000': 19, '1101111000': 1, '1111111011': 8, '0001011111': 3, '1110000000': 31, '0000000111': 25, '1110000001': 3, '0011111111': 24, '0000100000': 7, '1111111101': 30, '1111101111': 16, '0111111111': 37, '0000011101': 4, '0101111111': 4, '1011111110': 2, '0000000010': 17, '1011111111': 20, '0000100111': 1, '0010000111': 1, '1011010000': 1, '1101101111': 2, '1011110000': 1, '1000000001': 4, '0000001000': 23, '0011111110': 8, '1111111001': 1, '1100111111': 2, '0000011000': 2, '0001111110': 2, '0000111111': 20, '0001111111': 33, '1110111111': 11, '1010000000': 3, '0111011111': 2, '0000000100': 2, '0000000110': 2, '0000001111': 22, '0111101111': 1, '0000010111': 1, '0000000011': 15, '0001000010': 1, '1111111100': 19, '1111101000': 1, '0000001110': 2, '1011110100': 1, '0001000000': 11, '1001111111': 2, '0100000000': 6, '1100000011': 2, '1000001110': 1, '1100001111': 1, '0000010000': 3, '1101111110': 5, '0001111101': 1, '0001110111': 1, '0011000000': 2, '0111101110': 1, '1100000001': 1, '1111000001': 1, '0000000101': 1, '1101110111': 2, '0011111011': 1, '0000111110': 1, '1111101110': 3, '1111001000': 1, '1011111100': 1, '1111110101': 2, '1101001111': 1, '1111011110': 3, '1000011111': 1, '0000001001': 2, '1111010000': 1, '1110100010': 1, '1111110001': 2, '1101110000': 2, '0000010100': 1, '0111111110': 2, '0001000001': 1, '1000010000': 1, '1111011100': 1, '0111111100': 1, '1011101111': 1, '0000111101': 1, '1100011111': 2, '1101100000': 1, '1111011011': 1, '0010011111': 1, '0000110111': 3, '1111100010': 1, '1110111101': 1, '0000111001': 1, '1111100001': 1, '0001111100': 1, '1110011110': 1, '1100000010': 1, '0011110000': 1, '0001100111': 1, '1111010111': 1, '0010000001': 1, '0010000011': 1, '1101000111': 1, '1011111101': 1, '0000001100': 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:
[[0]
[0]
[0]
...
[0]
[0]
[0]]

The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 0 0]
[ 1 248]
[ 0 0]
...
[ 0 0]
[ 0 0]
[ 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:
[[0]
[0]
[0]
...
[0]
[0]
[0]]

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

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

The shape of the merged results is (4096, 2).
The bytes of the merged results:
[[ 0 0]
[ 3 240]
[ 0 0]
...
[ 0 0]
[ 0 0]
[ 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.

I metadati dei risultati del Sampler includono anche informazioni di temporizzazione dell'esecuzione chiamate lo span di esecuzione.

nota

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-05-13 14:23:00', stop='2026-05-13 14:23:02', size=4096>)])},
'version' : 2,

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

Visualizzare gli span di esecuzione

I risultati dei job SamplerV2 eseguiti in Qiskit Runtime contengono informazioni di temporizzazione dell'esecuzione nei loro metadati. Queste informazioni di temporizzazione possono essere utilizzate per posizionare limiti di timestamp superiori e inferiori su quando particolari shots sono stati eseguiti sul QPU. Gli shots sono raggruppati in oggetti ExecutionSpan, ognuno dei quali indica un'ora di inizio, un'ora di fine e una specifica degli shots 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 Primitive Unified Block (PUB), restituisce una maschera booleana che è True per tutti gli shots eseguiti durante la sua finestra. I PUB sono indicizzati dall'ordine in cui sono stati forniti alla chiamata run del Sampler. Se, ad esempio, un PUB ha forma (2, 3) ed è stato eseguito con quattro shots, la forma della maschera è (2, 3, 4). Vedere la pagina API execution_span per i dettagli completi.

Per visualizzare le informazioni sullo span di esecuzione, esamina i metadati del risultato restituito da SamplerV2, che si presentano sotto forma di oggetto ExecutionSpans. Questo oggetto è un contenitore simile a una lista contenente istanze di sottoclassi di ExecutionSpan, come SliceSpan.

Esempio:

# Define two circuits, each with one parameter with two parameters.
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)

params = np.random.uniform(size=(2, 3)).T

sampler_pub = (transpiled_circuit, params)

# Instantiate the new Estimator object, then run the transpiled circuit
# using the set of parameters and observables.

job = sampler.run([sampler_pub], shots=4)

result = job.result()
spans = job.result().metadata["execution"]["execution_spans"]
print(spans)
ExecutionSpans([DoubleSliceSpan(<start='2026-05-13 14:23:20', stop='2026-05-13 14:23:21', size=24>)])
from qiskit.primitives import BitArray

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

# Decide whether the 0th shot of parameter set (1, 2) occurred in this span.
in_this_span = mask[2, 1, 0]

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

Gli span di esecuzione possono essere filtrati per includere informazioni relative a PUB specifici, selezionati tramite i loro indici:

# take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])
ExecutionSpans([DoubleSliceSpan(<start='2026-05-13 14:23:20', stop='2026-05-13 14:23:21', size=24>)])

Visualizzare informazioni globali sulla raccolta 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)
Number of execution spans: 1
Start of the first span: 2026-05-13 14:23:20.441518
End of the last span: 2026-05-13 14:23:21.564845
Total duration (s): 1.123327

Estrarre e ispezionare un particolare span:

spans.sort()
print(" Start of first span:", spans[0].start)
print(" End of first span:", spans[0].stop)
print("#shots in first span:", spans[0].size)
Start of first span: 2026-05-13 14:23:20.441518
End of first span: 2026-05-13 14:23:21.564845
#shots in first span: 24
nota

È possibile che le finestre temporali specificate da span di esecuzione distinti si sovrappongano. Questo non accade perché un QPU stava eseguendo più esecuzioni contemporaneamente, ma è invece un artefatto di un certo 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.