Saltar al contenido principal
Este proyecto está en fase beta. ¿Encontraste un error? Abre un issue en GitHub. ¿Tienes una pregunta o una idea? Pregunta en Discussions — ¡nos encantaría tu opinión!
¿Cómo es la calidad de la traducción en esta página?

Entradas y salidas del Sampler

Versiones de paquetes

El código de esta página fue desarrollado con los siguientes requisitos. Recomendamos usar estas versiones o versiones más recientes.

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

Esta página ofrece una descripción general de las entradas y salidas de la primitiva Sampler de qiskit-ibm-runtime, que ejecuta cargas de trabajo en el IBM Quantum® Compute Service. Sampler te permite definir eficientemente cargas de trabajo vectorizadas usando una estructura de datos conocida como Primitive Unified Bloc (PUB). Se usan como entradas para el método run() de la primitiva Sampler, que ejecuta la carga de trabajo definida como un job. Luego, una vez completado el job, los resultados se devuelven en un formato que depende tanto de los PUBs utilizados como de las opciones de runtime especificadas desde la primitiva.

Entradas​

Cada PUB tiene el formato:

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

Puede haber múltiples elementos parameter values, y cada elemento puede ser un array o un único parámetro, dependiendo del circuito elegido. Además, la entrada debe contener mediciones.

Para la primitiva Sampler, un PUB puede contener como máximo tres valores:

  • Un único QuantumCircuit, que puede contener uno o más objetos Parameter Nota: Estos circuitos también deben incluir instrucciones de medición para cada uno de los qubits que se van a muestrear.
  • Una colección de valores de parámetros θk\theta_k que se vinculan al circuito (solo es necesario si se utilizan objetos Parameter que deben vincularse en tiempo de ejecución)
  • (Opcionalmente) un número de shots para medir el circuito

El siguiente código muestra un ejemplo de conjunto de entradas vectorizadas a la primitiva Sampler y las ejecuta en un backend de IBM® como un único objeto 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()

Salidas​

Después de que uno o más PUBs se envíen a un QPU para su ejecución y un job se complete exitosamente, los datos se devuelven como un objeto contenedor PrimitiveResult al que se accede llamando al método RuntimeJobV2.result(). El PrimitiveResult contiene una lista iterable de objetos SamplerPubResult que contienen los resultados de ejecución para cada PUB. Estos datos son muestras de la salida del circuito.

Cada elemento de esta lista corresponde a un PUB enviado al método run() de la primitiva (por ejemplo, un job enviado con 20 PUBs devolverá un objeto PrimitiveResult que contiene una lista de 20 objetos SamplerPubResult, uno correspondiente a cada PUB).

Cada objeto SamplerPubResult posee tanto un atributo data como un atributo metadata.

  • El atributo data es un DataBin personalizado que contiene los valores de medición reales, las desviaciones estándar, etc. Los data bins son objetos similares a diccionarios que contienen un BitArray por cada ClassicalRegister en el circuito.
  • La clase BitArray es un contenedor para datos de shots ordenados. Almacena los bitstrings muestreados como bytes dentro de un array bidimensional. El eje más a la izquierda de este array recorre los shots ordenados, mientras que el eje más a la derecha recorre los bytes.
  • El atributo metadata contiene información sobre las opciones de runtime utilizadas (explicado más adelante en la sección Metadatos del resultado de esta página).

El siguiente es un esquema visual de la estructura de datos 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
├── ...
├── ...
└── ...

En pocas palabras, un único job devuelve un objeto PrimitiveResult y contiene una lista de uno o más objetos SamplerPubResult. Estos objetos SamplerPubResult almacenan entonces los datos de medición para cada PUB que se envió al job.

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")
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 veces puede ser conveniente convertir el formato de bytes del BitArray a bitstrings. El método get_count devuelve un diccionario que mapea los bitstrings al número de veces que ocurrieron.

# 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}

Cuando un circuito contiene más de un registro clásico, los resultados se almacenan en diferentes objetos BitArray. 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}")
BitArray for register 'alpha': BitArray(<shape=(), num_shots=4096, num_bits=1>)
BitArray for register 'beta': BitArray(<shape=(), num_shots=4096, num_bits=9>)

Usar objetos BitArray para posprocesamiento eficiente​

Dado que los arrays generalmente ofrecen mejor rendimiento en comparación con los diccionarios, es recomendable realizar cualquier posprocesamiento directamente sobre los objetos BitArray en lugar de sobre diccionarios de conteos. La clase BitArray ofrece una variedad de métodos para realizar algunas operaciones comunes 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")
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]]

Metadatos del resultado​

Además de los resultados de ejecución, tanto los objetos PrimitiveResult como SamplerPubResult contienen un atributo de metadatos sobre el job enviado. Los metadatos que contienen información para todos los PUBs enviados (como las diversas opciones de runtime disponibles) se pueden encontrar en PrimitiveResult.metatada, mientras que los metadatos específicos de cada PUB se encuentran en SamplerPubResult.metadata.

El Sampler del lado del cliente envía su carga de trabajo como un job de Executor. Los metadatos del resultado de Executor subyacente, que incluyen información de temporización de ejecución llamada spans de ejecución, están disponibles bajo la clave "executor" de PrimitiveResult.metadata.

nota

En el campo de metadatos, las implementaciones primitivas pueden devolver cualquier información sobre la ejecución que sea relevante para ellas, y no hay pares clave-valor garantizados por la primitiva base. Por lo tanto, los metadatos devueltos pueden ser diferentes en diferentes implementaciones de primitivas.

# 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' : {},

Ver los spans de ejecución​

El Sampler del lado del cliente usa Executor para ejecutar jobs, que ejecuta su trabajo en el QPU en fragmentos (chunks). La temporización de cada fragmento está en result.metadata["executor"].chunk_timing, una lista de objetos ChunkSpan. Cada ChunkSpan tiene marcas de tiempo start y stop y una lista de parts, donde cada parte indica un índice de elemento (idx_item) y el número de configuraciones de ese elemento (size) que se ejecutaron en el fragmento.

Sampler crea un elemento de programa de Executor por cada PUB, de modo que idx_item es el índice del PUB, en el orden en que se pasaron los PUBs a run(). Si el twirling está habilitado, cada aleatorización de cada conjunto de valores de parámetros cuenta como una configuración independiente.

Para visualizar las temporizaciones, pasa la lista a draw_chunk_timings, como se muestra en el siguiente ejemplo.

Para saber qué significan las marcas de tiempo de los fragmentos, cómo leer el gráfico y por qué pueden aparecer huecos entre fragmentos, consulta Ver spans de ejecución en la guía de entradas y salidas de 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

Spans de ejecución en el Sampler heredado​

A partir de qiskit-ibm-runtime v0.50.0, el Sampler heredado del lado del servidor está obsoleto. El Sampler del lado del cliente no devuelve objetos ExecutionSpans; usa en su lugar la temporización de fragmentos descrita anteriormente. Consulta Migrar a primitivas del lado del cliente para ver las instrucciones de migración.

Haz clic para saber más.

Los resultados de los jobs del Sampler heredado del lado del servidor contienen información de temporización de ejecución en result.metadata["execution"]["execution_spans"], en forma de un objeto ExecutionSpans. Este objeto es un contenedor similar a una lista de objetos ExecutionSpan, cada uno de los cuales indica una hora de inicio, una hora de fin y una especificación de qué shots se recopilaron en el span.

Un span de ejecución especifica qué datos se ejecutaron durante su ventana proporcionando un método ExecutionSpan.mask. Este método, dado cualquier índice de PUB, devuelve una máscara booleana que es True para todos los shots ejecutados durante su ventana. Si, por ejemplo, un PUB tiene forma (2, 3) y se ejecutó con cuatro shots, entonces la forma de la máscara es (2, 3, 4). Consulta la página API de execution_span para obtener todos los detalles.

from qiskit.primitives import BitArray

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

# Obtén la máscara del 1.er PUB para el span 0.
mask = spans[0].mask(0)

# Crea un nuevo bit array que contenga solo los datos del PUB 0 recopilados durante este span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)

# Toma el subconjunto de spans que hacen referencia a datos de los PUBs 0 o 2
spans.filter_by_pub([0, 2])

# Consulta la información global de la colección de spans 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)

Para visualizar los spans de ejecución heredados, usa la función draw_execution_spans.

Es posible que las ventanas de tiempo especificadas por distintos spans de ejecución se superpongan. Esto no se debe a que un QPU esté realizando múltiples ejecuciones simultáneamente, sino que es un artefacto de cierto procesamiento clásico que puede ocurrir al mismo tiempo que la ejecución cuántica. La garantía es que los datos referenciados ocurrieron con certeza en el span de ejecución indicado, pero no necesariamente que los límites de la ventana de tiempo sean lo más ajustados posible.