Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

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.5.2
qiskit-ibm-runtime~=0.47.0

Cette page donne un aperçu des entrées et sorties de la primitive qiskit-ibm-runtime Sampler, qui exécute des charges de travail sur IBM Quantum® Compute Service. 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 définies dans la primitive.

Entrées​

Chaque PUB est au format :

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

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 montre un exemple d'ensemble d'entrées vectorisées pour la primitive Sampler et les exécute sur un backend IBM® sous forme d'un unique objet 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()

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:
[[ 3 247]
[ 0 0]
[ 3 223]
...
[ 0 255]
[ 0 0]
[ 0 0]]

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

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

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.

Le Sampler côté client soumet sa charge de travail sous forme de job Executor. Les métadonnées du résultat Executor sous-jacent, qui comprennent des informations temporelles d'exécution appelées execution spans, sont disponibles sous la clé "executor" de PrimitiveResult.metadata.

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

Afficher les spans d'exécution​

Le Sampler côté client utilise Executor pour exécuter les jobs, qui exécute son travail sur le QPU par chunks. Le timing de chaque chunk se trouve dans result.metadata["executor"].chunk_timing, une liste d'objets ChunkSpan. Chaque ChunkSpan a des horodatages start et stop et une liste de parts, où chaque part donne un index d'élément (idx_item) et le nombre de configurations de cet élément (size) exécutées dans le chunk.

Sampler crée un élément de programme Executor par PUB, donc idx_item est l'index du PUB, dans l'ordre où les PUBs ont été donnés à run(). Si le twirling est activé, chaque randomisation de chaque jeu de valeurs de paramètres compte comme une configuration distincte.

Pour visualiser les timings, passe la liste à draw_chunk_timings, comme dans l'exemple suivant.

Pour savoir ce que signifient les horodatages des chunks, comment lire le graphique et pourquoi des écarts peuvent apparaître entre les chunks, voir Afficher les execution spans dans le guide des entrées et sorties d'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

Execution spans dans l'ancien Sampler​

À partir de qiskit-ibm-runtime v0.50.0, l'ancien Sampler côté serveur est obsolète. Le Sampler côté client ne retourne pas d'objets ExecutionSpans ; utilise plutôt le timing des chunks décrit précédemment. Voir Migrer vers les primitives côté client pour les instructions de migration.

Clique pour en savoir plus.

Les résultats des jobs de l'ancien Sampler côté serveur contiennent des informations temporelles d'exécution dans result.metadata["execution"]["execution_spans"], sous la forme d'un objet ExecutionSpans. Cet objet est un conteneur de type liste d'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 execution span 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 PUB, retourne un masque booléen qui est True pour tous les shots exécutés pendant sa fenêtre. 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.

from qiskit.primitives import BitArray

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

# Obtenir le masque du 1er PUB pour le span 0.
mask = spans[0].mask(0)

# Créer un nouveau tableau de bits contenant uniquement les données du PUB 0 collectées pendant ce span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)

# Prendre le sous-ensemble de spans qui référencent des données des PUBs 0 ou 2
spans.filter_by_pub([0, 2])

# Afficher les informations globales sur la collection d'execution spans
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)

Pour visualiser les anciens execution spans, utilise la fonction draw_execution_spans.

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 simultanément, mais c'est plutôt un artefact de certains traitements classiques qui peuvent se produire en parallèle de l'exécution quantique. La garantie est que les données référencées se sont bien produites dans le span d'exécution signalé, mais pas nécessairement que les limites de la fenêtre temporelle sont aussi serrées que possible.