Entradas y salidas del sampler
El código de esta página se ha desarrollado teniendo en cuenta los siguientes requisitos. Recomendamos utilizar estas versiones o posteriores.
qiskit[all]~=2.5.0 qiskit-ibm-runtime~=0.47.0
Esta página ofrece una descripción general de las entradas y salidas de la primitiva « Qiskit Runtime Sampler», que ejecuta cargas de trabajo en recursos de computación de IBM Quantum®. Sampler te permite definir de forma eficiente cargas de trabajo vectorizadas mediante el uso de una estructura de datos conocida como « PUB » (). Se utilizan como entradas para el run() método de la primitiva Sampler, que ejecuta la carga de trabajo definida como un trabajo. A continuación, una vez finalizado el trabajo, los resultados se devuelven en un formato que depende tanto de los PUB utilizados como de las opciones de ejecución especificadas en la primitiva.
Entradas
Cada archivo « PUB » tiene el siguiente formato:
(<single circuit>, <one or more optional parameter value>, <optional shots>),
Puede haber varios parameter values elementos, y cada uno de ellos puede ser una matriz o un único parámetro, dependiendo del circuito elegido. Además, la entrada debe contener medidas.
En el caso de la primitiva «Sampler», un « PUB » puede contener como máximo tres valores:
- Un único *circuito *
QuantumCircuit, que puede contener uno o másParameterobjetos Nota: Estos circuitos también deben incluir instrucciones de medición para cada uno de los qubits que se vayan a muestrear. - Un conjunto de valores de parámetros para vincular el circuito a (solo es necesario si se utilizan
Parameterobjetos que deban vincularse en tiempo de ejecución) - (Opcionalmente) un número de disparos para medir el circuito
El siguiente código muestra un conjunto de entradas vectorizadas para la Sampler primitiva y las ejecuta en un backend de IBM® como un único RuntimeJobV2 objeto.
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()Resultados
Una vez que se envían uno o varios PUB a una QPU para su ejecución y un trabajo se completa con éxito, los datos se devuelven como un objeto PrimitiveResult contenedor al que se accede llamando al RuntimeJobV2.result() método. El objeto PrimitiveResult contiene una lista iterable de SamplerPubResult objetos que recogen los resultados de la ejecución de cada PUB. Estos datos son muestras de la salida del circuito.
Cada elemento de esta lista corresponde a un objeto PUB enviado al método de la run() primitiva (por ejemplo, un trabajo enviado con 20 PUB devolverá un PrimitiveResult objeto que contiene una lista de 20 SamplerPubResult objetos, uno correspondiente a cada objeto PUB).
Cada SamplerPubResult objeto posee un atributo data y un metadata atributo.
- El
dataatributo es un campo personalizadoDataBinque contiene los valores de medición reales, las desviaciones estándar, etc. Los contenedores de datos son objetos similares a diccionarios que contienen unoBitArrayporClassicalRegistercada elemento del circuito. - La
BitArrayclase es un contenedor de datos de tomas ordenados. Almacena las cadenas de bits muestreadas como bytes en una matriz bidimensional. El eje situado más a la izquierda de esta matriz recorre las tomas ordenadas, mientras que el eje situado más a la derecha recorre los bytes. - El
metadataatributo contiene información sobre las opciones de ejecución utilizadas (que se explican más adelante en la sección «Metadatos del resultado» de esta página).
A continuación se muestra un esquema visual de la estructura PrimitiveResult de datos:
└── 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
├── ...
├── ...
└── ...
En pocas palabras, una tarea devuelve un PrimitiveResult objeto y contiene una lista de uno o más SamplerPubResult objetos. A continuación, estos SamplerPubResult objetos almacenan los datos de medición de cada « PUB » que se haya enviado al trabajo.
Como primer ejemplo, veamos el siguiente circuito de diez qubits:
# 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")Output:
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]
[ 0 0]
[ 3 255]
...
[ 0 0]
[ 3 255]
[ 3 239]]
A veces puede resultar conveniente convertir los datos del formato de bytes en cadenas BitArray de bits. El get_count método devuelve un diccionario que asocia cadenas de bits con el número de veces que han aparecido.
# optionally, convert away from the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")Output:
Counts: {'0000000000': 1817, '1111111111': 1652, '0011111111': 19, '0000011111': 6, '0010000111': 1, '0001011111': 2, '1111111100': 12, '1111110111': 24, '0010000000': 49, '0001111111': 42, '0000110000': 2, '1111101111': 34, '1111001111': 1, '1111000000': 14, '1011111111': 27, '0000001111': 14, '1000000000': 41, '0000000111': 10, '1111111011': 11, '1111111000': 15, '0000111111': 25, '0000000011': 9, '1111111110': 31, '1111100000': 8, '1100000000': 8, '0100000000': 12, '0111111111': 34, '1110000000': 54, '0000010000': 3, '1111111101': 20, '0111101011': 1, '0000001011': 1, '0001000000': 4, '0000000001': 12, '1010000000': 1, '1101111000': 1, '1011011111': 1, '0010000001': 1, '1111110000': 3, '1110111111': 8, '0000001000': 5, '0011000000': 1, '0010111111': 1, '0000100000': 5, '0001111100': 1, '1111011111': 14, '1111100111': 1, '0000001110': 3, '0001111011': 1, '0001110000': 2, '0000111110': 1, '0000101111': 1, '1101111111': 4, '1011110111': 1, '0000000100': 3, '0111111011': 1, '0110111111': 1, '1100111111': 1, '1100000001': 1, '1001111111': 2, '0011101111': 1, '1111101101': 1, '1111111010': 1, '0110000000': 3, '1110011111': 1, '0000001101': 1, '0001110111': 1, '1111101000': 1, '1000000001': 1, '1000111111': 1, '0001100000': 1, '1011101111': 1, '0111110111': 1, '0000000010': 1}
Cuando un circuito contiene más de un registro clásico, los resultados se almacenan en diferentes BitArray objetos. El siguiente ejemplo modifica el fragmento anterior dividiendo el registro clásico en dos registros distintos:
# 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}")Output:
BitArray for register 'alpha': BitArray(<shape=(), num_shots=4096, num_bits=1>)
BitArray for register 'beta': BitArray(<shape=(), num_shots=4096, num_bits=9>)
Utiliza BitArray objetos para un posprocesamiento eficaz
Dado que las matrices suelen ofrecer un mejor rendimiento que los diccionarios, es recomendable realizar cualquier procesamiento posterior directamente sobre los BitArray objetos, en lugar de sobre los diccionarios de recuentos. La BitArray clase ofrece una serie de métodos para realizar algunas operaciones habituales de posprocesamiento:
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")Output:
The shape of register `alpha` is (4096, 1).
The bytes in register `alpha`, shot by shot:
[[1]
[1]
[1]
...
[1]
[1]
[0]]
The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 1 255]
[ 1 255]
[ 1 254]
...
[ 1 255]
[ 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]
[7]
[6]
...
[7]
[7]
[0]]
The shape of `beta` after shot-wise slicing is (5, 2).
The bytes in `beta` after shot-wise slicing:
[[ 1 255]
[ 1 255]
[ 1 254]
[ 1 255]
[ 1 240]]
Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
coeffs=[1.+0.j])` is: 0.07568359375
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
coeffs=[1.+0.j])` is: 0.0322265625
The shape of the merged results is (4096, 2).
The bytes of the merged results:
[[ 3 255]
[ 3 255]
[ 3 253]
...
[ 3 255]
[ 3 255]
[ 0 0]]
Metadatos del resultado
Además de los resultados de la ejecución, tanto el objeto PrimitiveResult como SamplerPubResult el contienen un atributo de metadatos sobre el trabajo que se envió. Los metadatos que contienen información sobre todos los PUB enviados (como las distintas opciones de tiempo de ejecución disponibles) se encuentran en el PrimitiveResult.metatada, mientras que los metadatos específicos de cada PUB se encuentran en SamplerPubResult.metadata.
Los metadatos de los resultados del Sampler también incluyen información sobre la duración de la ejecución, denominada «intervalo de ejecución».
En el campo de metadatos, las implementaciones de primitivas pueden devolver cualquier información sobre la ejecución que les resulte relevante, y no hay pares clave-valor garantizados por la primitiva base. Por lo tanto, los metadatos devueltos pueden variar según la implementación de la primitiva.
# 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},")Output:
The metadata of the PrimitiveResult is:
'execution' : {'execution_spans': ExecutionSpans([DoubleSliceSpan(<start='2026-07-15 08:42:24', stop='2026-07-15 08:42:26', size=4096>)])},
'version' : 2,
The metadata of the PubResult result is:
'circuit_metadata' : {},
Ver intervalos de ejecución
Los resultados de SamplerV2 los trabajos ejecutados en Qiskit Runtime incluyen información sobre los tiempos de ejecución en sus metadatos.
Esta información temporal puede utilizarse para establecer los límites superior e inferior de las marcas de tiempo en las que se ejecutaron determinadas operaciones en la QPU.
Las tomas se agrupan en ExecutionSpan objetos, cada uno de los cuales indica una hora de inicio, una hora de finalización y una especificación de las tomas que se recopilaron en ese intervalo.
Un intervalo de ejecución especifica qué datos se ejecutaron durante su ventana mediante un ExecutionSpan.mask método. Este método, dado cualquier índice de bloque unificado primitivo ( PUB ), devuelve una máscara booleana que es True para todas las tomas ejecutadas durante su ventana. Los PUB se indexan según el orden en que se pasaron a la llamada de ejecución del Sampler. Si, por ejemplo, una máscara de « PUB » tiene la forma (2, 3) y se ha ejecutado con cuatro disparos, entonces la forma de la máscara es (2, 3, 4). Consulte la página de la API de execution_span para obtener más información.
Para consultar la información sobre el intervalo de ejecución, revisa los metadatos del resultado devuelto por SamplerV2, que se presenta en forma de un ExecutionSpans objeto. Este objeto es un contenedor similar a una lista que contiene instancias de subclases de ExecutionSpan, como SliceSpan.
Ejemplo:
# 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)Output:
ExecutionSpans([DoubleSliceSpan(<start='2026-07-15 08:43:57', stop='2026-07-15 08:43:58', 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)Los intervalos de ejecución se pueden filtrar para incluir información relativa a PUB específicos, seleccionados por sus índices:
# take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])Output:
ExecutionSpans([DoubleSliceSpan(<start='2026-07-15 08:43:57', stop='2026-07-15 08:43:58', size=24>)])
Ver información general sobre el conjunto de intervalos de ejecución:
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)Output:
Number of execution spans: 1
Start of the first span: 2026-07-15 08:43:57.312863
End of the last span: 2026-07-15 08:43:58.426676
Total duration (s): 1.113813
Extraer y examinar un tramo concreto:
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)Output:
Start of first span: 2026-07-15 08:43:57.312863
End of first span: 2026-07-15 08:43:58.426676
#shots in first span: 24
Es posible que los intervalos de tiempo especificados por distintos periodos de ejecución se solapen. Esto no se debe a que una QPU estuviera realizando varias ejecuciones a la vez, sino que se trata de un efecto secundario de ciertos procesos clásicos que pueden tener lugar simultáneamente con la ejecución cuántica. Lo que se garantiza es que los datos a los que se hace referencia se produjeron efectivamente durante el intervalo de ejecución indicado, pero no necesariamente que los límites de ese intervalo sean lo más precisos posible.