Aller au contenu principal

Entrées et sorties des primitives

Versions des paquets

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

qiskit[all]~=2.4.0

Cette page donne un aperçu des entrées et sorties des primitives Qiskit. Avec ces primitives, tu peux utiliser une structure de données appelée Primitive Unified Bloc (PUB) pour définir des charges de travail vectorisées de manière efficace. Ces PUBs sont l'unité fondamentale de travail pour l'exécution des charges de travail. Ils sont utilisés comme entrées de la méthode run() des primitives Sampler et Estimator, qui exécutent 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 des PUBs utilisés et des options spécifiées.

Aperçu des PUBs

Lors de l'invocation de la méthode run() d'une primitive, l'argument principal requis est une list d'un ou plusieurs tuples — un pour chaque circuit exécuté par la primitive. Chacun de ces tuples est considéré comme un PUB, et les éléments requis de chaque tuple dans la liste dépendent de la primitive utilisée. Les données fournies à ces tuples peuvent également être organisées sous différentes formes pour offrir de la flexibilité à une charge de travail via le broadcasting — dont les règles sont décrites dans une section ultérieure.

PUB de l'Estimator

Pour la primitive Estimator, le format du PUB doit contenir au maximum quatre valeurs :

  • Un seul QuantumCircuit, qui peut contenir un ou plusieurs objets Parameter
  • Une liste d'un ou plusieurs observables qui spécifient les valeurs d'espérance à estimer, organisés en un tableau (par exemple, un seul observable représenté comme un tableau de dimension 0, une liste d'observables comme un tableau de dimension 1, etc.). Les données peuvent être dans n'importe quel format ObservablesArrayLike comme Pauli, SparsePauliOp, PauliList ou str.
    remarque

    Si tu as deux observables qui commutent dans des PUBs différents mais avec le même circuit, ils ne seront pas estimés en utilisant la même mesure. Chaque PUB représente une base de mesure différente et, par conséquent, des mesures séparées sont nécessaires pour chaque PUB. Pour garantir que les observables qui commutent soient estimés en utilisant la même mesure, ils doivent être regroupés dans le même PUB.

  • Une collection de valeurs de paramètres à lier au circuit. Cela peut être spécifié comme un seul objet de type tableau où le dernier indice correspond aux objets Parameter du circuit, ou omis (ou de manière équivalente, défini comme None) si le circuit n'a pas d'objets Parameter.
  • (Optionnellement) une précision cible pour les valeurs d'espérance à estimer

PUB du Sampler

Pour la primitive Sampler, le format du tuple PUB contient au maximum trois valeurs :

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

Le code suivant montre un exemple d'entrées vectorisées pour la primitive Estimator.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit
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.primitives import StatevectorEstimator

import numpy as np

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

# Transpile the circuit without providing a backend
pm = generate_preset_pass_manager(optimization_level=1)
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, 10),
np.linspace(-4 * np.pi, 4 * np.pi, 10),
]
).T

# Define three observables. The inner length-1 lists cause this array of
# observables to have shape (3, 1), rather than shape (3,) if they were
# omitted.
observables = [
[SparsePauliOp(["XX", "IY"], [0.5, 0.5])],
[SparsePauliOp("XX")],
[SparsePauliOp("IY")],
]
# Apply the same layout as the transpiled circuit.
observables = [
[observable.apply_layout(layout) for observable in observable_set]
for observable_set in observables
]

# Estimate the expectation value for all 300 combinations of observables
# and parameter values, where the pub result will have shape (3, 100).
#
# This shape is due to our array of parameter bindings having shape
# (100, 2), combined with our array of observables having shape (3, 1).
estimator = StatevectorEstimator()
estimator_pub = (transpiled_circuit, observables, params)

# Run the transpiled circuit
# using the set of parameters and observables.

job = estimator.run([estimator_pub])
result = job.result()

Règles de broadcasting

Les PUBs agrègent des éléments de plusieurs tableaux (observables et valeurs de paramètres) en suivant les mêmes règles de broadcasting que NumPy. Cette section résume brièvement ces règles. Pour une explication détaillée, consulte la documentation des règles de broadcasting de NumPy.

Règles :

  • Les tableaux d'entrée n'ont pas besoin d'avoir le même nombre de dimensions.
    • Le tableau résultant aura le même nombre de dimensions que le tableau d'entrée avec le plus grand nombre de dimensions.
    • La taille de chaque dimension est la taille la plus grande de la dimension correspondante.
    • Les dimensions manquantes sont supposées avoir une taille de un.
  • Les comparaisons de forme commencent par la dimension la plus à droite et continuent vers la gauche.
  • Deux dimensions sont compatibles si leurs tailles sont égales ou si l'une d'entre elles est 1.

Exemples de paires de tableaux compatibles avec le broadcasting :

A1     (1d array):      1
A2 (2d array): 3 x 5
Result (2d array): 3 x 5

A1 (3d array): 11 x 2 x 7
A2 (3d array): 11 x 1 x 7
Result (3d array): 11 x 2 x 7

Exemples de paires de tableaux non compatibles avec le broadcasting :

A1     (1d array):  5
A2 (1d array): 3

A1 (2d array): 2 x 1
A2 (3d array): 6 x 5 x 4 # This would work if the middle dimension were 2, but it is 5.

Estimator retourne une estimation de la valeur d'espérance pour chaque élément de la forme résultant du broadcasting.

Voici quelques exemples de motifs courants exprimés en termes de broadcasting de tableaux. Leur représentation visuelle correspondante est montrée dans la figure suivante :

Les ensembles de valeurs de paramètres sont représentés comme des tableaux de n x m, et les tableaux d'observables sont représentés comme un ou plusieurs tableaux à une seule colonne. Pour chaque exemple dans le code précédent, les ensembles de valeurs de paramètres sont combinés avec leur tableau d'observables pour créer les estimations de valeurs d'espérance résultantes.

  • Exemple 1 : (broadcast d'un seul observable) possède un ensemble de valeurs de paramètres qui est un tableau de 5x1 et un tableau d'observables de 1x1. L'unique élément du tableau d'observables est combiné avec chaque élément de l'ensemble de valeurs de paramètres pour créer un seul tableau de 5x1 où chaque élément est une combinaison de l'élément original dans l'ensemble de valeurs de paramètres avec l'élément du tableau d'observables.

  • Exemple 2 : (zip) possède un ensemble de valeurs de paramètres de 5x1 et un tableau d'observables de 5x1. La sortie est un tableau de 5x1 où chaque élément est une combinaison du n-ième élément de l'ensemble de valeurs de paramètres avec le n-ième élément du tableau d'observables.

  • Exemple 3 : (produit extérieur) possède un ensemble de valeurs de paramètres de 1x6 et un tableau d'observables de 4x1. Leur combinaison produit un tableau de 4x6 créé en combinant chaque élément de l'ensemble de valeurs de paramètres avec chaque élément du tableau d'observables, de sorte que chaque valeur de paramètre devient une colonne complète dans la sortie.

  • Exemple 4 : (généralisation nd standard) possède un tableau de valeurs de paramètres de 3x6 et deux tableaux d'observables de 3x1. Ceux-ci sont combinés pour créer deux tableaux de sortie de 3x6 de manière similaire à l'exemple précédent.

Cette image illustre plusieurs représentations visuelles du broadcasting de tableaux.

# Broadcast single observable
parameter_values = np.random.uniform(size=(5,)) # shape (5,)
observables = SparsePauliOp("ZZZ") # shape ()
# >> pub result has shape (5,)

# Zip
parameter_values = np.random.uniform(size=(5,)) # shape (5,)
observables = [
SparsePauliOp(pauli) for pauli in ["III", "XXX", "YYY", "ZZZ", "XYZ"]
] # shape (5,)
# >> pub result has shape (5,)

# Outer/Product
parameter_values = np.random.uniform(size=(1, 6)) # shape (1, 6)
observables = [
[SparsePauliOp(pauli)] for pauli in ["III", "XXX", "YYY", "ZZZ"]
] # shape (4, 1)
# >> pub result has shape (4, 6)

# Standard nd generalization
parameter_values = np.random.uniform(size=(3, 6)) # shape (3, 6)
observables = [
[
[SparsePauliOp(["XII"])],
[SparsePauliOp(["IXI"])],
[SparsePauliOp(["IIX"])],
],
[
[SparsePauliOp(["ZII"])],
[SparsePauliOp(["IZI"])],
[SparsePauliOp(["IIZ"])],
],
] # shape (2, 3, 1)
# >> pub result has shape (2, 3, 6)
SparsePauliOp

Chaque SparsePauliOp compte comme un seul élément dans ce contexte, indépendamment du nombre de Paulis que contient le SparsePauliOp. Par conséquent, pour les besoins de ces règles de broadcasting, tous les éléments suivants ont la même forme :

a = SparsePauliOp("Z") # shape ()
b = SparsePauliOp("IIIIZXYIZ") # shape ()
c = SparsePauliOp.from_list(["XX", "XY", "IZ"]) # shape ()

Les listes d'opérateurs suivantes, bien qu'équivalentes en termes d'information contenue, ont des formes différentes :

list1 = SparsePauliOp.from_list(["XX", "XY", "IZ"]) # shape ()
list2 = [SparsePauliOp("XX"), SparsePauliOp("XY"), SparsePauliOp("IZ")] # shape (3, )

Aperçu des sorties des primitives

Une fois qu'un ou plusieurs PUBs sont envoyés à un QPU pour exécution et que le job se termine avec succès, les données sont retournées sous forme d'objet conteneur PrimitiveResult. Le PrimitiveResult contient une liste itérable d'objets PubResult qui contiennent les résultats d'exécution de chaque PUB. Par exemple, un job soumis avec 20 PUBs retournera un objet PrimitiveResult contenant une liste de 20 PubResults, un pour chaque PUB.

Chacun de ces objets PubResult possède à la fois un attribut data et un attribut metadata optionnel. L'attribut data est un DataBin personnalisé qui contient les estimations de valeurs d'espérance dans le cas de l'Estimator, ou des échantillons de la sortie du circuit dans le cas du Sampler.

L'attribut data peut également inclure d'autres informations spécifiques à l'implémentation, comme les écarts-types. L'attribut metadata peut contenir des informations supplémentaires spécifiques à l'implémentation concernant l'exécution du PUB associé.

Voici un schéma visuel de la structure de données de PrimitiveResult :

└── PrimitiveResult
├── PubResult[0]
│ ├── metadata
│ └── data ## In the form of a DataBin object,
| | ## which includes data such as the following:
│ ├── evs
│ │ └── List of estimated expectation values in the shape
| | specified by the first pub
│ └── stds
│ └── List of calculated standard deviations in the
| same shape as above
├── PubResult[1]
| ├── metadata
| └── data ## In the form of a DataBin object,
| | ## which includes data such as the following:
| ├── evs
| │ └── List of estimated expectation values in the shape
| | specified by the second pub
| └── stds
| └── List of calculated standard deviations in the
| same shape as above
├── ...
├── ...
└── ...
remarque

Ce qui précède est un exemple de données pouvant être retournées. Les données réellement retournées dépendent de l'implémentation.

Sortie de l'Estimator

Comme indiqué précédemment, les données retournées dans le PubResult pour la primitive Estimator dépendent de l'implémentation. Par exemple, elles peuvent contenir un tableau de valeurs d'espérance (PubResult.data.evs) et les écarts-types associés (PubResult.data.stds).

Le fragment de code suivant décrit le format de PrimitiveResult (et le PubResult associé) pour le job créé précédemment.

print(
f"The result of the submitted job had {len(result)} PUB and has a value:\n {result}\n"
)
print(
f"The associated PubResult of this job has the following data bins:\n {result[0].data}\n"
)
print(f"And this DataBin has attributes: {result[0].data.keys()}")
print(
"Recall that this shape is due to our array of parameter binding sets having shape (100, 2) -- where 2 is the\n\
number of parameters in the circuit -- combined with our array of observables having shape (3, 1). \n"
)
print(
f"The expectation values measured from this PUB are: \n{result[0].data.evs}"
)
The result of the submitted job had 1 PUB and has a value:
PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(3, 10), dtype=float64>), stds=np.ndarray(<shape=(3, 10), dtype=float64>), shape=(3, 10)), metadata={'target_precision': 0.0, 'circuit_metadata': {}})], metadata={'version': 2})

The associated PubResult of this job has the following data bins:
DataBin(evs=np.ndarray(<shape=(3, 10), dtype=float64>), stds=np.ndarray(<shape=(3, 10), dtype=float64>), shape=(3, 10))

And this DataBin has attributes: dict_keys(['evs', 'stds'])
Recall that this shape is due to our array of parameter binding sets having shape (100, 2) -- where 2 is the
number of parameters in the circuit -- combined with our array of observables having shape (3, 1).

The expectation values measured from this PUB are:
[[ 3.06161700e-16 4.52395120e-01 4.36594428e-01 2.16506351e-01
6.33718361e-01 -6.33718361e-01 -2.16506351e-01 -4.36594428e-01
-4.52395120e-01 -3.06161700e-16]
[ 1.22464680e-16 6.42787610e-01 9.84807753e-01 8.66025404e-01
3.42020143e-01 -3.42020143e-01 -8.66025404e-01 -9.84807753e-01
-6.42787610e-01 -1.22464680e-16]
[ 4.89858720e-16 2.62002630e-01 -1.11618897e-01 -4.33012702e-01
9.25416578e-01 -9.25416578e-01 4.33012702e-01 1.11618897e-01
-2.62002630e-01 -4.89858720e-16]]

Sortie du Sampler

Lorsqu'un job de Sampler se termine avec succès, l'objet PrimitiveResult retourné contient une liste de SamplerPubResult, un par PUB. Les data bins de ces objets SamplerPubResult sont des objets similaires à des dictionnaires qui contiennent un BitArray par ClassicalRegister du circuit.

La classe BitArray est un conteneur pour les données de shots ordonnées. Plus précisément, elle stocke les chaînes de bits échantillonnées sous forme de bytes dans un tableau bidimensionnel. L'axe le plus à gauche de ce tableau parcourt les shots dans l'ordre, tandis que l'axe le plus à droite parcourt les bytes.

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

from qiskit.primitives import StatevectorSampler

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

sampler = StatevectorSampler()

# run the Sampler job and retrieve the results

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=1024, num_bits=10>))

BitArray: BitArray(<shape=(), num_shots=1024, num_bits=10>)

The shape of register `meas` is (1024, 2).

The bytes in register `alpha`, shot by shot:
[[ 0 0]
[ 3 255]
[ 0 0]
...
[ 3 255]
[ 3 255]
[ 3 255]]

Parfois, il peut être pratique de convertir le format de bytes du BitArray en chaînes de bits. La méthode get_count retourne un dictionnaire qui associe les chaînes de bits au nombre de fois qu'elles sont apparues.

# optionally, convert away from the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")
Counts: {'0000000000': 492, '1111111111': 532}

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

# 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

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=1024, num_bits=1>)
BitArray for register 'beta': BitArray(<shape=(), num_shots=1024, num_bits=9>)

Exploiter 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 recommandé d'effectuer tout post-traitement directement sur les objets BitArray plutôt que sur des dictionnaires de comptages. La classe BitArray offre une variété de méthodes pour 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")
The shape of register `alpha` is (1024, 1).
The bytes in register `alpha`, shot by shot:
[[1]
[1]
[1]
...
[0]
[0]
[1]]

The shape of register `beta` is (1024, 2).
The bytes in register `beta`, shot by shot:
[[ 1 255]
[ 1 255]
[ 1 255]
...
[ 0 0]
[ 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 (1024, 1).
The bytes in `beta` after bit-wise slicing:
[[7]
[7]
[7]
...
[0]
[0]
[7]]

The shape of `beta` after shot-wise slicing is (5, 2).
The bytes in `beta` after shot-wise slicing:
[[ 1 255]
[ 1 255]
[ 1 255]
[ 1 255]
[ 1 255]]
Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
coeffs=[1.+0.j])` is: -0.017578125
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
coeffs=[1.+0.j])` is: -0.017578125

The shape of the merged results is (1024, 2).
The bytes of the merged results:
[[ 3 255]
[ 3 255]
[ 3 255]
...
[ 0 0]
[ 0 0]
[ 3 255]]

Métadonnées des résultats

En plus des résultats d'exécution, les objets PrimitiveResult et PubResult contiennent un attribut de métadonnées optionnel sur le job soumis. Les métadonnées retournées (le cas échéant) sont spécifiques à l'implémentation.

# 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:
'version' : 2,

The metadata of the PubResult result is:
'shots' : 1024,
'circuit_metadata' : {},

Prochaines étapes

Recommandations