Aller au contenu principal

Entrées et sorties du Sampler

Versions des packages

Le code de cette page a été développé avec les exigences suivantes. Nous recommandons d'utiliser ces versions ou des versions plus récentes.

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

Cette page donne un aperçu des entrées et sorties de la primitive Qiskit Runtime Sampler, qui exécute des charges de travail sur les ressources de calcul IBM Quantum®. Sampler te permet de définir efficacement des charges de travail vectorisées en utilisant une structure de données connue sous le nom de Primitive Unified Bloc (PUB). Ils sont utilisés comme entrées pour la méthode run() de la primitive Sampler, qui exécute la charge de travail définie comme un job. Ensuite, une fois le job terminé, les résultats sont retournés dans un format qui dépend à la fois des PUBs utilisés et des options de runtime spécifiées depuis la primitive.

Entrées

Chaque PUB est au format :

(<circuit unique>, <une ou plusieurs valeurs de paramètres optionnelles>, <shots optionnels>),

Il peut y avoir plusieurs éléments parameter values, et chaque élément peut être soit un tableau, soit un paramètre unique, selon le circuit choisi. De plus, l'entrée doit contenir des mesures.

Pour la primitive Sampler, un PUB peut contenir au maximum trois valeurs :

  • Un seul QuantumCircuit, qui peut contenir un ou plusieurs objets Parameter Remarque : Ces circuits doivent également inclure des instructions de mesure pour chacun des qubits à échantillonner.
  • Une collection de valeurs de paramètres pour lier le circuit contre θk\theta_k (seulement nécessaire si des objets Parameter sont utilisés et doivent être liés au moment de l'exécution)
  • (Optionnellement) un nombre de shots pour mesurer le circuit

Le code suivant illustre un exemple d'entrées vectorisées pour la primitive Sampler et les exécute sur un backend IBM® comme un seul objet 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()

Sorties

Après qu'un ou plusieurs PUBs aient été envoyés à un QPU pour exécution et qu'un job se soit terminé avec succès, les données sont retournées sous forme d'objet conteneur PrimitiveResult accessible en appelant la méthode RuntimeJobV2.result(). Le PrimitiveResult contient une liste itérable d'objets SamplerPubResult qui contiennent les résultats d'exécution pour chaque PUB. Ces données sont des échantillons de la sortie du circuit.

Chaque élément de cette liste correspond à un PUB soumis à la méthode run() de la primitive (par exemple, un job soumis avec 20 PUBs retournera un objet PrimitiveResult contenant une liste de 20 objets SamplerPubResult, un correspondant à chaque PUB).

Chaque objet SamplerPubResult possède à la fois un attribut data et un attribut metadata.

  • L'attribut data est un DataBin personnalisé contenant les valeurs de mesure réelles, les écarts-types, etc. Les data bins sont des objets semblables à des dictionnaires qui contiennent un BitArray par ClassicalRegister dans le circuit.
  • La classe BitArray est un conteneur pour les données de shots ordonnées. Elle stocke les bitstrings échantillonnés sous forme d'octets dans un tableau bidimensionnel. L'axe le plus à gauche de ce tableau parcourt les shots ordonnés, tandis que l'axe le plus à droite parcourt les octets.
  • L'attribut metadata contient des informations sur les options de runtime utilisées (expliqué plus loin dans la section Métadonnées des résultats de cette page).

Voici un aperçu visuel de la structure de données 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 résumé, un seul job retourne un objet PrimitiveResult et contient une liste d'un ou plusieurs objets SamplerPubResult. Ces objets SamplerPubResult stockent ensuite les données de mesure pour chaque PUB soumis au job.

Comme premier exemple, regardons le circuit à dix qubits suivant :

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

Il peut parfois être pratique de convertir le format d'octets du BitArray en bitstrings. La méthode get_count retourne un dictionnaire mappant les bitstrings au nombre de fois qu'ils se sont produits.

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

Lorsqu'un circuit contient plus d'un registre classique, les résultats sont stockés dans différents objets BitArray. L'exemple suivant modifie l'extrait précédent en divisant le registre classique en deux registres distincts :

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

Utiliser les objets BitArray pour un post-traitement performant

Étant donné que les tableaux offrent généralement de meilleures performances par rapport aux dictionnaires, il est conseillé d'effectuer tout post-traitement directement sur les objets BitArray plutôt que sur des dictionnaires de comptages. La classe BitArray offre une gamme de méthodes pour effectuer certaines opérations de post-traitement courantes :

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

Métadonnées des résultats

En plus des résultats d'exécution, les objets PrimitiveResult et SamplerPubResult contiennent un attribut de métadonnées sur le job soumis. Les métadonnées contenant des informations pour tous les PUBs soumis (telles que les diverses options de runtime disponibles) se trouvent dans PrimitiveResult.metatada, tandis que les métadonnées spécifiques à chaque PUB se trouvent dans SamplerPubResult.metadata.

Les métadonnées des résultats du Sampler incluent également des informations de temporisation d'exécution appelées le span d'exécution.

remarque

Dans le champ de métadonnées, les implémentations primitives peuvent retourner toutes les informations sur l'exécution qui leur sont pertinentes, et il n'y a pas de paires clé-valeur garanties par la primitive de base. Ainsi, les métadonnées retournées peuvent être différentes dans différentes implémentations de primitives.

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

Afficher les spans d'exécution

Les résultats des jobs SamplerV2 exécutés dans Qiskit Runtime contiennent des informations de temporisation d'exécution dans leurs métadonnées. Ces informations de temporisation peuvent être utilisées pour placer des bornes de horodatage supérieures et inférieures sur quand des shots particuliers ont été exécutés sur le QPU. Les shots sont regroupés dans des objets ExecutionSpan, chacun indiquant une heure de début, une heure de fin et une spécification des shots collectés dans le span.

Un span d'exécution spécifie quelles données ont été exécutées pendant sa fenêtre en fournissant une méthode ExecutionSpan.mask. Cette méthode, étant donné n'importe quel index de Primitive Unified Block (PUB), retourne un masque booléen qui est True pour tous les shots exécutés pendant sa fenêtre. Les PUBs sont indexés par l'ordre dans lequel ils ont été donnés à l'appel run du Sampler. Si, par exemple, un PUB a la forme (2, 3) et a été exécuté avec quatre shots, alors la forme du masque est (2, 3, 4). Voir la page API execution_span pour tous les détails.

Pour afficher les informations de span d'exécution, examine les métadonnées du résultat retourné par SamplerV2, qui se présentent sous la forme d'un objet ExecutionSpans. Cet objet est un conteneur semblable à une liste contenant des instances de sous-classes de ExecutionSpan, telles que SliceSpan.

Exemple :

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

Les spans d'exécution peuvent être filtrés pour inclure les informations relatives à des PUBs spécifiques, sélectionnés par leurs indices :

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

Afficher les informations globales sur la collection de spans d'exécution :

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

Extraire et inspecter un span particulier :

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
remarque

Il est possible que les fenêtres temporelles spécifiées par des spans d'exécution distincts se chevauchent. Ce n'est pas parce qu'un QPU effectuait plusieurs exécutions à la fois, mais c'est plutôt un artefact de certains traitements classiques qui pourraient se produire en parallèle avec l'exécution quantique. La garantie faite est que les données référencées se sont définitivement produites dans le span d'exécution rapporté, mais pas nécessairement que les limites de la fenêtre temporelle sont aussi précises que possible.