Diffusion par exécuteur
Les données transmises à la primitive Executor peuvent être organisées sous diverses formes afin d'offrir une grande souplesse dans le traitement d'une charge de travail grâce à la diffusion. Ce guide explique comment l'Executor gère les entrées et les sorties de tableaux à l'aide de la sémantique de diffusion. La compréhension de ces concepts vous aidera à parcourir efficacement les valeurs des paramètres, à combiner plusieurs configurations et à interpréter la structure des données renvoyées.
Les exemples présentés dans cette rubrique ne peuvent pas être exécutés séparément. Ils partent du principe que vous avez défini les circuits appropriés, utilisé le gestionnaire de passages Samplomatic pour ajouter des encadrés et des annotations, et utilisé la méthode Samplomatic build pour obtenir un circuit type et un samplex pour chaque bloc de code, si nécessaire.
Exemple de démarrage rapide
Cet exemple illustre le principe de base. Il génère un circuit paramétrique et cinq configurations de paramètres différentes. L'exécuteur exécute les cinq configurations et renvoie les données classées par configuration, avec un résultat par registre classique dans chaque élément du programme quantique.
La suite de ce guide s'appuie sur cet exemple pour expliquer le fonctionnement de cette fonctionnalité et comment créer des balayages plus complexes, notamment en utilisant la randomisation et les entrées basées sur Samplomatic.
import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from qiskit.transpiler import generate_preset_pass_manager
# A circuit with 2 parameters
# This circuit is used throughout the rest of this guide.
circuit = QuantumCircuit(4)
circuit.rx(Parameter("a"), 0)
circuit.rx(Parameter("b"), 1)
circuit.h(2)
circuit.cx(2, 3)
circuit.measure_all()
# 5 different parameter configurations (shape: 5 configurations × 2 parameters)
parameter_values = np.linspace(0, np.pi, 10).reshape(5, 2)
# Initialize the service and choose a backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# Transpile to ISA circuit
preset_pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
)
isa_circuit = preset_pass_manager.run(circuit)
# This program is used throughout the rest of this guide.
program = QuantumProgram(shots=1024)
program.append_circuit_item(isa_circuit, circuit_arguments=parameter_values)
# initialize an Executor with default options
executor = Executor(mode=backend)
# Run and get results
result = executor.run(program).result()
# result is a list with one entry per program item
# result[0] is a dict mapping classical register names to data arrays
# Output bool arrays have shape (5, 1024, 4)
# 5 = number of parameter configurations
# 1024 = number of shots
# 4 = bits in the classical register
result[0]["meas"]Axes intrinsèques et extrinsèques
La diffusion ne s'applique qu'aux axes extrinsèques. Les axes intrinsèques sont toujours conservés, comme indiqué.
-
Axes intrinsèques (à l'extrême droite) : déterminés par le type de données. Par exemple, si votre circuit comporte trois paramètres, les valeurs de ces paramètres nécessitent trois nombres, ce qui donne une forme intrinsèque de
(3,). -
Axes extrinsèques (à l'extrême gauche) : les dimensions de votre balayage. Ces paramètres déterminent le nombre de configurations que vous souhaitez exécuter.
Type d'entrée | Forme intrinsèque | Exemple de forme complète |
|---|---|---|
| Valeurs des paramètres (n paramètres) | (n,) | (5, 3) pour cinq configurations et trois paramètres |
| Entrées scalaires (par exemple, échelle de bruit) | () | (4,) pour quatre configurations |
| Observables (le cas échéant) | fluctuations | Cela dépend du type de l'observable |
Exemple
Considérons un circuit comportant deux paramètres que l'on souhaite balayer sur une grille de configurations d' 4x3, en faisant varier les valeurs des paramètres et un facteur d'échelle de bruit :
import numpy as np
# Parameter values: 4 configurations along axis 0, intrinsic shape (2,)
# Full shape: (4, 1, 2) - the "1" allows broadcasting with noise_scale
parameter_values = np.array([
[[0.1, 0.2]],
[[0.3, 0.4]],
[[0.5, 0.6]],
[[0.7, 0.8]],
]) # shape (4, 1, 2)
# Noise scale: 3 configurations, intrinsic shape () (scalar)
# Full shape: (3,)
noise_scale = np.array([0.8, 1.0, 1.2]) # shape (3,)
# Extrinsic shapes: (4, 1) and (3,) → broadcast to (4, 3)
# Result: 12 total configurations in a 4×3 grid
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": parameter_values,
"noise_scales.mod_ref1": noise_scale,
},
)Les formes sont les suivantes :
Entrée | Forme complète | Forme extrinsèque | Forme intrinsèque |
|---|---|---|---|
parameter_values | (4, 1, 2) | (4, 1) | (2,) |
noise_scale | (3,) | (3,) | () |
| Diffusion radiotélévisée | Aucun | (4, 3) | Aucun |
Formats des tableaux de sortie
Les tableaux de sortie suivent le même schéma extrinsèque/intrinsèque :
- Forme extrinsèque : correspond à la forme de diffusion de toutes les entrées
- Forme intrinsèque : déterminée par le type de sortie
Le format de sortie le plus courant est celui des données de mesure sous forme de chaîne de bits, qui sont présentées sous la forme d'un tableau de valeurs booléennes :
Type de sortie | Forme intrinsèque | Description |
|---|---|---|
| Données de registre classiques | (nombre_de_tirs, taille_de_la_zone_de_cible) | Données sous forme de chaînes binaires issues des mesures |
Exemple
Si vous fournissez des entrées avec des formes (4, 1) extrinsèques et (3,), la forme extrinsèque diffusée
est (4, 3). Le code suivant utilise un circuit de 1 024 pas et un registre classique de 4 bits (tel que défini dans l'exemple du guide de démarrage rapide ) :
# Input extrinsic shapes: (4, 1) and (3,) → (4, 3)
# Output for classical register "meas":
# extrinsic: (4, 3)
# intrinsic: (1024, 4) - shots × bits
# full shape: (4, 3, 1024, 4)
result = executor.run(program).result()
meas_data = result[0]["meas"] # result[0] for first program item
print(meas_data.shape) # (4, 3, 1024, 4)
# Access a specific configuration
config_2_1 = meas_data[2, 1, :, :] # shape (1024, 4)Chaque configuration exécute le nombre total de tirs spécifié dans le programme quantique. Les prises ne sont pas réparties entre les différentes configurations. Par exemple, si vous demandez 1 024 prises et que vous disposez de 10 configurations, chacune d'entre elles exécute 1 024 prises (soit un total de 10 240 prises exécutées).
La randomisation et le shape paramètre
Lorsqu'on utilise un samplex, chaque élément de la forme extrinsèque correspond à une exécution de circuit indépendante. Le samplex introduit généralement un élément aléatoire (par exemple, une rotation de porte) dans chaque exécution; ainsi, même sans demander explicitement plusieurs randomisations, chaque élément bénéficie d'une réalisation aléatoire.
Vous pouvez utiliser le shape paramètre pour enrichir la forme extrinsèque de l'élément, ce qui revient en fait
à ajouter des axes qui correspondent précisément à la répétition aléatoire de la même configuration à plusieurs reprises. Il doit être
diffusable à partir de la forme implicite dans votre samplex_arguments. Les axes pour lesquels shape dépasse
la forme implicite énumèrent des randomisations indépendantes supplémentaires.
Aucun axe de randomisation explicite
Si vous ne le précisez pas shape (ou si vous le configurez pour qu'il corresponde à vos formes d'entrée), vous obtenez une exécution par
configuration d'entrée. Chaque exécution est toujours aléatoire grâce au samplex, mais avec une
seule réalisation aléatoire, vous ne bénéficiez pas de la moyenne obtenue à partir de plusieurs aléatisations.
Si vous avez l'habitude d'activer le twirling à l'aide d'un simple indicateur tel que twirling=True, notez que
l'Executor vous oblige à demander explicitement plusieurs randomisations à l'aide de shape l'argument
afin de permettre à vos routines de post-traitement de bénéficier des avantages du calcul de la moyenne sur plusieurs
randomisations. Une randomisation simple (la valeur par défaut lorsque shape est omis) applique
des portes aléatoires, mais n'offre généralement aucun avantage par rapport à l'exécution du circuit de base sans
randomisation.
L'exemple suivant illustre le comportement par défaut :
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
# shape defaults to (10,) - one randomized execution per config
)
# Output shape for "meas": (10, num_shots, creg_size)Axe de randomisation unique
Pour effectuer plusieurs randomisations par configuration, étendez la figure en ajoutant des axes supplémentaires. Par exemple, le code suivant effectue 20 simulations aléatoires pour chacune des 10 configurations de paramètres :
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
shape=(20, 10), # 20 randomizations × 10 configurations
)
# Output shape for "meas": (20, 10, num_shots, creg_size)Plusieurs axes de randomisation
Vous pouvez organiser les randomisations sous la forme d'une grille multidimensionnelle. Cela s'avère utile pour une analyse structurée, par exemple pour classer les randomisations par type ou les regrouper en vue d'un traitement statistique.
(2, 14, 10)Ici, la forme (10,) extrinsèque d'entrée est diffusée vers la forme demandée,
les axes 0 et 1 étant remplis par des randomisations indépendantes.
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
# 2×14=28 randomizations per configuration, 10 configurations
# Or you could set shape=(28, 10) for the same effect
shape=(2, 14, 10),
)
# Output shape for "meas": (2, 14, 10, num_shots, creg_size)Comment shape les formes d'entrée interagissent
Le shape paramètre doit pouvoir être diffusé à partir de vos formes extrinsèques d'entrée. Ce qui signifie :
shapeLes formes d'entrée dont les dimensions sont d' size-1 s peuvent s'étendre pour s'adapter.- Les formes saisies doivent s'aligner à partir de la droite avec
shape. - Les
shapeaxes qui dépassent les dimensions d'entrée énumèrent des randomisations.
Notez que shape peut contenir des dimensions d' size-1
qui s'adaptent aux dimensions de l'entrée, comme l'illustre la dernière ligne du tableau ci-dessous.
Exemples :
Entrée extrinsèque | Forme | Résultat |
|---|---|---|
| (10,) | (10,) | 10 configurations, chacune avec une randomisation |
| (10,) | (5, 10) | 10 configurations, avec 5 randomisations chacune |
| (10,) | (2, 3, 10) | 10 configurations, 2 × 3 = 6 randomisations chacune |
| (4, 1) | (4, 5) | 4 configurations, avec 5 randomisations chacune |
| (4, 3) | (2, 4, 3) | 4 × 3 = 12 configurations, avec 2 randomisations pour chacune |
| (4, 3) | (2, 1, 3) | 4 × 3 = 12 combinaisons, avec 2 randomisations pour chacune (le 1 se décompose en 4) |
Accéder aux résultats
Grâce aux axes de randomisation, vous pouvez accéder à des combinaisons spécifiques de randomisation et de paramètres :
# Using shape=(2, 14, 10) with input extrinsic shape (10,), and
# 1024 shots and 4 classical registers.
result = executor.run(program).result()
meas_data = result[0]["meas"] # shape (2, 14, 10, 1024, 4)
# Get all shots for randomization (0, 7) and parameter config 3
specific = meas_data[0, 7, 3, :, :] # shape (1024, 4)
# Average over all randomizations for parameter config 5 on bit 2
averaged = meas_data[:, :, 5, :, 2].mean(axis=(0, 1))Modèles courants
Balayer un seul paramètre
Utilisez un code similaire à celui-ci pour faire varier un paramètre tout en conservant les autres fixes :
# Circuit has 2 parameters, sweep first one over 20 values
sweep_values = np.linspace(0, 2*np.pi, 20)
parameter_values = np.column_stack([
sweep_values,
np.full(20, 0.5),
]) # shape (20, 2)Création d'un balayage de grille « 2D »
Pour créer une grille à partir de trois paramètres :
# Sweep param 0 over 10 values, param 1 over 8 values, param 2 fixed
p0 = np.linspace(0, np.pi, 10)[:, np.newaxis, np.newaxis] # (10, 1, 1)
p1 = np.linspace(0, np.pi, 8)[np.newaxis, :, np.newaxis] # (1, 8, 1)
p2 = np.array([[[0.5]]]) # (1, 1, 1)
parameter_values = np.broadcast_arrays(p0, p1, p2)
parameter_values = np.stack(parameter_values, axis=-1).squeeze() # (10, 8, 3)
# Extrinsic shape: (10, 8), intrinsic shape: (3,)Combiner plusieurs sources d'entrée
Lorsque vous assemblez des éléments de forme intrinsèque différente, alignez leurs dimensions extrinsèques à l'aide des axes d' size-1 :
# 4 parameter configurations, 3 noise scales → 4×3 = 12 total configurations
parameter_values = np.random.rand(4, 1, 2) # extrinsic (4, 1), intrinsic (2,)
noise_scale = np.array([0.8, 1.0, 1.2]) # extrinsic (3,), intrinsic ()
# Broadcasted extrinsic shape: (4, 3)Etapes suivantes
- Consultez la présentation générale de la diffusion.
- Comprendre les entrées et les sorties de l'Executor.