Entrées et sorties de l'échantillonneur
Le code présenté sur cette page a été développé en tenant compte des exigences suivantes. Nous vous recommandons d'utiliser ces versions ou des versions plus récentes.
qiskit[all]~=2.5.1 qiskit-ibm-runtime~=0.47.0
Cette page présente une vue d'ensemble des entrées et sorties de la primitive « Sampler qiskit-ibm-runtime », qui exécute des charges de travail sur le service de calcul IBM Quantum®. Sampler vous permet de définir efficacement des charges de travail vectorisées à l'aide d'une structure de données appelée « PUB » (bloc unifié primitif). Ils servent d'entrées à la méthode run() de la primitive « Sampler », qui exécute la charge de travail définie sous forme de tâche. Ensuite, une fois la tâche terminée, les résultats sont renvoyés dans un format qui dépend à la fois des PUB utilisés et des options d'exécution spécifiées au niveau de la primitive.
Entrées
Chaque « PUB » se présente sous la forme suivante :
(<single circuit>, <one or more optional parameter value>, <optional shots>),
Il peut y avoir plusieurs parameter values éléments, et chaque élément peut être soit un tableau, soit un paramètre unique, selon le circuit choisi. De plus, les données saisies doivent comporter des mesures.
Pour la primitive Sampler, une instance de type « PUB » peut contenir au maximum trois valeurs :
- Un circuit unique
QuantumCircuit, pouvant contenir un ou plusieursParameterobjets Remarque : ces circuits doivent également inclure des instructions de mesure pour chacun des qubits à échantillonner. - Ensemble de valeurs de paramètres permettant de lier le circuit à (nécessaire uniquement si des
Parameterobjets doivent être liés lors de l'exécution) - (Facultatif) un nombre de mesures pour analyser le circuit
Le code suivant présente un exemple d'ensemble d'entrées vectorisées pour la Sampler primitive et les exécute sur un backend IBM® en tant qu'objet RuntimeJobV2 unique.
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
Une fois qu'un ou plusieurs PUB ont été envoyés à une QPU pour exécution et qu'une tâche s'est achevée avec succès, les données sont renvoyées sous la forme d'un objet PrimitiveResult conteneur accessible en appelant la RuntimeJobV2.result() méthode. L'objet PrimitiveResult contient une liste itérable SamplerPubResult d'objets qui renferment les résultats d'exécution pour chaque PUB. Ces données constituent des échantillons de la sortie du circuit.
Chaque élément de cette liste correspond à une PUB soumise à la méthode run() de la primitive (par exemple, une tâche soumise avec 20 PUB renverra un PrimitiveResult objet contenant une liste de 20 SamplerPubResult objets, chacun correspondant à une PUB ).
Chaque SamplerPubResult objet possède à la fois un attribut data et un metadata attribut.
- L'attribut
dataest un champ personnaliséDataBinqui contient les valeurs de mesure réelles, les écarts-types, etc. Les conteneurs de données sont des objets de type dictionnaire qui contiennent un élémentBitArrayparClassicalRegisterdans le circuit. - Cette
BitArrayclasse sert de conteneur pour les données de prise de vue classées par ordre. Il stocke les chaînes de bits échantillonnées sous forme d'octets dans un tableau bidimensionnel. L'axe le plus à gauche de ce tableau couvre les images classées par ordre, tandis que l'axe le plus à droite couvre les octets. - L'attribut
metadatacontient des informations sur les options d'exécution utilisées (expliquées plus loin dans la section « Métadonnées du résultat » de cette page).
Voici une représentation schématique de la structure PrimitiveResult de données :
└── 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 termes simples, une tâche renvoie un PrimitiveResult objet et contient une liste d'un ou plusieurs SamplerPubResult objets. Ces SamplerPubResult objets stockent ensuite les données de mesure pour chaque PUB ion soumise au travail.
À titre d'exemple, examinons 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")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:
[[ 3 255]
[ 0 0]
[ 0 1]
...
[ 3 0]
[ 0 0]
[ 3 254]]
Il peut parfois être pratique de convertir les données du format octet en chaînes BitArray de bits. Cette get_count méthode renvoie un dictionnaire qui associe des chaînes de bits au nombre de fois où elles sont apparues.
# optionally, convert away from the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")Output:
Counts: {'1111111111': 1346, '0000000000': 1754, '0000000001': 55, '1000000000': 56, '1111111110': 92, '0111111111': 23, '1011111111': 15, '0001111111': 23, '1111011011': 1, '1111111101': 45, '1111111011': 108, '1111110111': 32, '0100000000': 10, '0000000111': 21, '0011111111': 21, '1111110000': 26, '1101111111': 47, '1111011111': 23, '1111111010': 6, '1100000000': 45, '1111100000': 32, '1110000000': 21, '1111101111': 13, '0010000000': 14, '0000000011': 19, '0000000101': 2, '0000001110': 2, '0000100000': 4, '0000001111': 20, '1111111100': 22, '0000010000': 5, '1101110111': 4, '1011111101': 1, '0000000010': 15, '0000001000': 12, '1111110110': 7, '1111000000': 3, '0010000001': 1, '0111011111': 3, '1001111111': 3, '1101111011': 3, '0000011111': 16, '0000011110': 3, '0001111011': 1, '1011111011': 3, '1111110011': 4, '1111101011': 2, '0000000100': 6, '1110111111': 12, '1111111000': 17, '0000111111': 5, '0001111101': 2, '1101100000': 2, '1101110001': 1, '1000001111': 2, '1111101110': 1, '1110111101': 1, '1101111101': 2, '1110000100': 1, '0100011111': 1, '1110000010': 1, '0011111110': 2, '0111111110': 1, '1111110010': 1, '0111110111': 1, '0000000110': 1, '0101111111': 1, '1101011111': 1, '1111001111': 1, '1110011111': 1, '0011111000': 2, '1101111110': 3, '1110111110': 1, '0110000000': 2, '1110000111': 1, '0000010111': 3, '0001000000': 3, '0111101111': 1, '0000011100': 1, '1000000001': 1, '1111011010': 1, '0000001010': 1, '1111100111': 2, '1111100011': 2, '0000001101': 1, '0111001111': 1, '1111111001': 1, '1101111000': 1, '0111110000': 1, '1111000111': 1, '1010000000': 1, '0011110000': 1, '1100000001': 1, '1011001101': 1, '0000001100': 1, '1100111111': 1, '1110111011': 1, '1111011101': 1, '1000011111': 1, '1101111001': 1, '0101101111': 1, '0000011011': 1, '0000111011': 1, '0111111100': 1, '1011100000': 1, '0011111011': 1, '0000010010': 1, '1001111011': 1}
Lorsqu'un circuit contient plusieurs registres classiques, les résultats sont stockés dans différents BitArray objets. 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}")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>)
Utilisez BitArray des objets pour un post-traitement performant
Étant donné que les tableaux offrent généralement de meilleures performances que les dictionnaires, il est conseillé d'effectuer tout traitement ultérieur directement sur les BitArray objets plutôt que sur des dictionnaires de comptes. Cette BitArray classe propose toute une série de méthodes permettant d'effectuer certaines opérations courantes de post-traitement :
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:
[[0]
[0]
[0]
...
[1]
[0]
[1]]
The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 0 0]
[ 0 0]
[ 1 255]
...
[ 1 255]
[ 0 0]
[ 1 255]]
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]
[7]
...
[7]
[0]
[7]]
The shape of `beta` after shot-wise slicing is (5, 2).
The bytes in `beta` after shot-wise slicing:
[[ 0 0]
[ 0 0]
[ 1 255]
[ 0 0]
[ 1 255]]
Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
coeffs=[1.+0.j])` is: 0.115234375
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
coeffs=[1.+0.j])` is: 0.02392578125
The shape of the merged results is (4096, 2).
The bytes of the merged results:
[[ 0 0]
[ 0 0]
[ 3 254]
...
[ 3 255]
[ 0 0]
[ 3 255]]
Métadonnées des résultats
Outre les résultats d'exécution, les objets PrimitiveResult SamplerPubResult et contiennent tous deux un attribut de métadonnées concernant le travail qui a été soumis. Les métadonnées contenant des informations sur toutes les publications soumises (telles que les différentes options d'exécution disponibles) se trouvent dans le PrimitiveResult.metatada, tandis que les métadonnées spécifiques à chaque publication PUB se trouvent dans SamplerPubResult.metadatale.
Les métadonnées des résultats du Sampler comprennent également des informations sur la durée d'exécution, appelées « durée d'exécution ».
Dans le champ des métadonnées, les implémentations de primitives peuvent renvoyer toute information relative à l'exécution qui leur est pertinente, et aucune paire clé-valeur n'est garantie par la primitive de base. Ainsi, les métadonnées renvoyées peuvent varier selon les implémentations des 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},")Output:
The metadata of the PrimitiveResult is:
'execution' : {'execution_spans': ExecutionSpans([DoubleSliceSpan(<start='2026-08-01 08:21:10', stop='2026-08-01 08:21:13', size=4096>)])},
'version' : 2,
The metadata of the PubResult result is:
'circuit_metadata' : {},
Afficher les intervalles d'exécution
Les résultats des tâches SamplerV2 exécutées dans le service de calcul « IBM Quantum » contiennent, dans leurs métadonnées, des informations relatives à la durée d'exécution.
Ces informations temporelles peuvent être utilisées pour définir des limites supérieure et inférieure quant à la date et l'heure auxquelles des plans spécifiques ont été exécutés sur le QPU.
Les plans sont regroupés en « objets ExecutionSpan », dont chacun indique une heure de début, une heure de fin et précise quels plans ont été enregistrés pendant cette période.
Une fenêtre d'exécution précise quelles données ont été exécutées pendant sa durée en fournissant une ExecutionSpan.mask méthode. Cette méthode, à partir d'un index de bloc unifié primitif ( PUB ), renvoie un masque booléen qui est True vrai pour tous les plans exécutés pendant sa fenêtre. Les PUB sont indexés selon l'ordre dans lequel ils ont été transmis à l'appel d'exécution du Sampler. Si, par exemple, une image de fond ( PUB ) a la forme (2, 3) et a été traitée avec quatre passes, alors la forme du masque est (2, 3, 4). Consultez la page de l'API execution_span pour plus de détails.
Pour consulter les informations relatives à la durée d'exécution, examinez les métadonnées du résultat renvoyé par SamplerV2, qui se présente sous la forme d'un ExecutionSpans objet. Cet objet est un conteneur de type 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)Output:
ExecutionSpans([DoubleSliceSpan(<start='2026-08-01 08:21:37', stop='2026-08-01 08:21:38', 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 intervalles d'exécution peuvent être filtrés pour inclure les informations relatives à des PUB spécifiques, sélectionnés en fonction de leurs indices :
# take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])Output:
ExecutionSpans([DoubleSliceSpan(<start='2026-08-01 08:21:37', stop='2026-08-01 08:21:38', size=24>)])
Afficher les informations générales sur l'ensemble des intervalles 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)Output:
Number of execution spans: 1
Start of the first span: 2026-08-01 08:21:37.606114
End of the last span: 2026-08-01 08:21:38.960352
Total duration (s): 1.354238
Extraire et inspecter une portée spécifique :
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-08-01 08:21:37.606114
End of first span: 2026-08-01 08:21:38.960352
#shots in first span: 24
Il est possible que des plages horaires définies par des durées d'exécution distinctes se chevauchent. Cela ne tient pas au fait qu'un QPU effectuait plusieurs exécutions simultanément, mais résulte plutôt d'un artefact lié à certains traitements classiques pouvant se produire en parallèle de l'exécution quantique. La garantie donnée est que les données concernées se sont bel et bien produites au cours de la période d'exécution indiquée, mais pas nécessairement que les limites de cette fenêtre temporelle sont aussi précises que possible.