Pular para o conteúdo principal

Entradas e saídas do Sampler

Versões dos pacotes

O código desta página foi desenvolvido com os seguintes requisitos. Recomendamos usar essas versões ou versões mais recentes.

qiskit[all]~=2.4.0
qiskit-ibm-runtime~=0.46.1

Esta página fornece uma visão geral das entradas e saídas da primitiva Qiskit Runtime Sampler, que executa cargas de trabalho nos recursos de computação IBM Quantum®. O Sampler permite definir eficientemente cargas de trabalho vetorizadas usando uma estrutura de dados conhecida como Primitive Unified Bloc (PUB). Eles são usados como entradas para o método run() da primitiva Sampler, que executa a carga de trabalho definida como um job. Em seguida, após o job ser concluído, os resultados são retornados em um formato que depende tanto dos PUBs usados quanto das opções de runtime especificadas pela primitiva.

Entradas

Cada PUB está no formato:

(<circuito único>, <um ou mais valores de parâmetros opcionais>, <shots opcionais>),

Pode haver múltiplos itens parameter values, e cada item pode ser um array ou um único parâmetro, dependendo do circuito escolhido. Além disso, a entrada deve conter medições.

Para a primitiva Sampler, um PUB pode conter no máximo três valores:

  • Um único QuantumCircuit, que pode conter um ou mais objetos Parameter Nota: Esses circuitos também devem incluir instruções de medição para cada um dos qubits a serem amostrados.
  • Uma coleção de valores de parâmetros para vincular o circuito contra θk\theta_k (necessário apenas se objetos Parameter forem usados e precisarem ser vinculados em tempo de execução)
  • (Opcionalmente) um número de shots para medir o circuito

O código a seguir demonstra um exemplo de um conjunto de entradas vetorizadas para a primitiva Sampler e as executa em um backend IBM® como um único objeto 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()

Saídas

Após um ou mais PUBs serem enviados a um QPU para execução e um job ser concluído com sucesso, os dados são retornados como um objeto contêiner PrimitiveResult acessado chamando o método RuntimeJobV2.result(). O PrimitiveResult contém uma lista iterável de objetos SamplerPubResult que contêm os resultados de execução para cada PUB. Esses dados são amostras da saída do circuito.

Cada elemento desta lista corresponde a um PUB enviado ao método run() da primitiva (por exemplo, um job enviado com 20 PUBs retornará um objeto PrimitiveResult contendo uma lista de 20 objetos SamplerPubResult, um correspondendo a cada PUB).

Cada objeto SamplerPubResult possui tanto um atributo data quanto um atributo metadata.

  • O atributo data é um DataBin personalizado que contém os valores de medição reais, desvios padrão e assim por diante. Os data bins são objetos semelhantes a dicionários que contêm um BitArray por ClassicalRegister no circuito.
  • A classe BitArray é um contêiner para dados de shots ordenados. Ela armazena as bitstrings amostradas como bytes dentro de um array bidimensional. O eixo mais à esquerda desse array percorre os shots ordenados, enquanto o eixo mais à direita percorre os bytes.
  • O atributo metadata contém informações sobre as opções de runtime usadas (explicado mais adiante na seção Metadados do resultado desta página).

A seguir está um esboço visual da estrutura de dados 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
├── ...
├── ...
└── ...

Em termos simples, um único job retorna um objeto PrimitiveResult e contém uma lista de um ou mais objetos SamplerPubResult. Esses objetos SamplerPubResult armazenam então os dados de medição para cada PUB enviado ao job.

Como primeiro exemplo, vamos examinar o seguinte circuito de dez 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:
[[ 0 0]
[ 3 255]
[ 0 0]
...
[ 3 255]
[ 2 255]
[ 3 255]]

Às vezes pode ser conveniente converter do formato de bytes do BitArray para bitstrings. O método get_count retorna um dicionário que mapeia bitstrings para o número de vezes que ocorreram.

# 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 um circuito contém mais de um registro clássico, os resultados são armazenados em diferentes objetos BitArray. O exemplo a seguir modifica o snippet anterior dividindo o registro clássico em dois 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 pós-processamento eficiente

Como os arrays geralmente oferecem melhor desempenho em comparação com dicionários, é aconselhável realizar qualquer pós-processamento diretamente nos objetos BitArray em vez de nos dicionários de contagens. A classe BitArray oferece uma variedade de métodos para realizar algumas operações comuns de pós-processamento:

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]]

Metadados do resultado

Além dos resultados de execução, tanto os objetos PrimitiveResult quanto SamplerPubResult contêm um atributo de metadados sobre o job enviado. Os metadados contendo informações para todos os PUBs enviados (como as várias opções de runtime disponíveis) podem ser encontrados em PrimitiveResult.metatada, enquanto os metadados específicos de cada PUB estão em SamplerPubResult.metadata.

Os metadados de resultado do Sampler também incluem informações de temporização de execução chamadas de span de execução.

nota

No campo de metadados, as implementações de primitivas podem retornar qualquer informação sobre execução que seja relevante para elas, e não há pares chave-valor garantidos pela primitiva base. Assim, os metadados retornados podem ser diferentes em diferentes implementações 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-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' : {},

Ver os spans de execução

Os resultados de jobs SamplerV2 executados no Qiskit Runtime contêm informações de temporização de execução em seus metadados. Essas informações de temporização podem ser usadas para estabelecer limites de timestamp superiores e inferiores sobre quando shots particulares foram executados no QPU. Os shots são agrupados em objetos ExecutionSpan, cada um dos quais indica uma hora de início, uma hora de término e uma especificação de quais shots foram coletados no span.

Um span de execução especifica quais dados foram executados durante sua janela fornecendo um método ExecutionSpan.mask. Esse método, dado qualquer índice de Primitive Unified Block (PUB), retorna uma máscara booleana que é True para todos os shots executados durante sua janela. Os PUBs são indexados pela ordem em que foram dados à chamada run do Sampler. Se, por exemplo, um PUB tem forma (2, 3) e foi executado com quatro shots, a forma da máscara é (2, 3, 4). Consulte a página API execution_span para obter detalhes completos.

Para ver informações sobre spans de execução, examine os metadados do resultado retornado por SamplerV2, que vêm na forma de um objeto ExecutionSpans. Esse objeto é um contêiner semelhante a uma lista que contém instâncias de subclasses de ExecutionSpan, como SliceSpan.

Exemplo:

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

Os spans de execução podem ser filtrados para incluir informações relacionadas a PUBs específicos, selecionados por seus índices:

# 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>)])

Ver informações globais sobre a coleção de spans de execução:

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

Extrair e inspecionar um span específico:

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

É possível que as janelas de tempo especificadas por spans de execução distintos se sobreponham. Isso não ocorre porque um QPU estava realizando múltiplas execuções ao mesmo tempo, mas é um artefato de certo processamento clássico que pode ocorrer simultaneamente com a execução quântica. A garantia que está sendo feita é que os dados referenciados definitivamente ocorreram no span de execução reportado, mas não necessariamente que os limites da janela de tempo são tão precisos quanto possível.