Skip to main content
IBM Quantum Platform

Entrées et sorties primitives

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

    qiskit[all]~=2.5.1
    

Cette page présente une vue d'ensemble des entrées et des sorties des primitives de l' Qiskit SDK. Grâce à ces primitives, vous pouvez utiliser une structure de données appelée « PUB » (bloc unifié de primitives) pour définir efficacement des charges de travail vectorisées. Ces PUB constituent l'unité de travail fondamentale pour l'exécution des charges de travail. Ils servent d'entrées à la méthode run() des primitives « Sampler » et « Estimator », qui exécutent 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 des PUB utilisés et des options éventuellement spécifiées.


Aperçu des PUB

Lorsqu'on appelle la run() méthode d'une primitive, l'argument principal requis est un tableau list contenant un ou plusieurs tuples — un pour chaque circuit exécuté par la primitive. Chacun de ces tuples est considéré comme une « PUB », et les éléments requis de chaque tuple de la liste dépendent de la primitive utilisée. Les données fournies à ces tuples peuvent également être organisées sous diverses formes afin d'offrir une certaine souplesse dans le traitement d'une charge de travail grâce à la diffusion — dont les règles sont décrites dans la section suivante.

PUB de l'estimateur

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

  • Un seul QuantumCircuit, qui peut contenir un ou plusieurs Parameter objets
  • Une liste d'un ou plusieurs observables, qui spécifient les valeurs d'espérance à estimer, disposées dans un tableau (par exemple, un seul observable représenté comme un tableau de 0-d, une liste d'observables comme un tableau de 1-d, et ainsi de suite). Les données peuvent être dans l'un des formats ObservablesArrayLike tels que Pauli, SparsePauliOp, PauliList ou str.
    Note

    Si vous avez deux observables de trajet dans des PUB différents mais avec le même circuit, ils ne seront pas estimés à l'aide de la même mesure. Chaque PUB représente une base de mesure différente, et par conséquent, des mesures distinctes sont nécessaires pour chaque PUB. Pour garantir que les observables liés aux déplacements domicile-travail sont estimés à l'aide de la même mesure, ils doivent être regroupés au sein de la même unité d' PUB.

  • Une collection de valeurs de paramètres pour lier le circuit. Il peut être spécifié sous la forme d'un objet unique de type tableau dont le dernier index est sur les objets Parameter du circuit, ou être omis (ou, de manière équivalente, prendre la valeur None) si le circuit n'a pas d'objets Parameter .
  • (Optionnellement) une précision cible pour les valeurs d'espérance à estimer

PUB de l'échantillonneur

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

  • Un circuit unique QuantumCircuit, pouvant contenir un ou plusieurs Parameter objets 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 à θ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 prises de vue pour mesurer le circuit avec

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

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 diffusion

Les PUB regroupent des éléments provenant de plusieurs tableaux (observables et valeurs de paramètres) en suivant les mêmes règles de diffusion que NumPy. Cette section résume brièvement ces règles. Pour une explication détaillée, voir la documentation sur les règles de diffusion à l'adresse NumPy.

Règles :

  • Les tableaux d'entrée ne doivent pas nécessairement avoir le même nombre de dimensions.
    • Le tableau résultant aura le même nombre de dimensions que le tableau d'entrée ayant la plus grande dimension.
    • La taille de chaque dimension est la plus grande taille de la dimension correspondante.
    • Les dimensions manquantes sont supposées avoir la taille 1.
  • Les comparaisons de formes commencent par la dimension la plus à droite et se poursuivent vers la gauche.
  • Deux dimensions sont compatibles si leurs tailles sont égales ou si l'une d'entre elles vaut 1.

Exemples de paires de tableaux qui diffusent :

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 qui ne diffusent pas :

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

A1     (2d array):      2 x 1
# The following would work if the middle dimension were 2,
# instead of 5.
A2     (3d array):  6 x 5 x 4

Estimator renvoie une estimation de la valeur attendue pour chaque élément de la matrice diffusée.

Voici quelques exemples de modèles courants exprimés en termes de diffusion de tableaux. Leur représentation visuelle est illustrée dans la figure suivante :

Les ensembles de valeurs de paramètres sont représentés par des tableaux n x m et les tableaux d'observables sont représentés par un ou plusieurs tableaux à colonne unique. Pour chaque exemple du code précédent, les ensembles de valeurs des paramètres sont combinés avec leur tableau d'observables pour créer les estimations des valeurs attendues qui en résultent.

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

  • Exemple 2 : (zip) a un ensemble de valeurs de paramètres 5x1 et un tableau d'observables 5x1. Le résultat est un tableau 5x1 où chaque élément est une combinaison du nième élément de l'ensemble des valeurs des paramètres avec le nième élément du tableau des observables.

  • Exemple 3 : (outer/product) possède un jeu de valeurs de paramètres 1x6 et un tableau d'observables 4x1. Leur combinaison aboutit à un tableau 4x6 qui est créé en combinant chaque élément de l'ensemble des valeurs des paramètres avec chaque élément du tableau des observables, de sorte que chaque valeur de paramètre devient une colonne entière dans le résultat.

  • Exemple 4 : (Standard nd generalization) a un tableau de valeurs de paramètres 3x6 et deux tableaux d'observables 3x1. Ils se combinent pour créer deux tableaux de sortie 3x6 de la même manière que dans l'exemple précédent.

Cette image illustre plusieurs représentations visuelles de la diffusion de tableaux.
Représentation visuelle de la diffusion
# 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, quel que soit le nombre de Paulis contenus dans le SparsePauliOp. Ainsi, aux fins des présentes règles de radiodiffusion, 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'informations contenues, ont des formes différentes :

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

Aperçu des sorties primitives

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. L'objet PrimitiveResult contient une liste itérable PubResult d'objets qui renferment les résultats d'exécution pour chaque PUB. Par exemple, une tâche soumise avec 20 PUB renverra un PrimitiveResult objet contenant une liste de 20 éléments PubResults, chacun correspondant à un PUB.

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

Cet data attribut peut également contenir d'autres informations propres à la mise en œuvre, telles que les écarts-types. L'attribut metadata peut contenir des informations supplémentaires propres à l'implémentation concernant l'exécution de l' PUB associé.

Voici un aperçu visuel de la structure des 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
    ├── ...
    ├── ...
    └── ...
Note

Ce qui précède est un exemple de données pouvant être renvoyées. Les données effectivement renvoyées dépendent de la mise en œuvre.

Sortie de l'estimateur

Comme indiqué précédemment, les données renvoyées par PubResult la primitive Estimator dépendent de l'implémentation. Par exemple, il peut contenir un tableau de valeurs attendues (PubResult.data.evs) et d'écarts-types associés (PubResult.data.stds).

L'extrait de code ci-dessous décrit le format PrimitiveResult (et le format associé PubResult) pour le travail créé ci-dessus.

print(
    f"The result of the submitted job had {len(result)} PUB and "
    f"has a value:\n {result}\n"
)
print(
    f"The associated PubResult of this job has the following data bins:"
    f"\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 number of parameters in the circuit -- "
    "combined with our array of observables having shape (3, 1)."
)

print(
    f"The expectation values measured from this PUB are: \n{result[0].data.evs}"
)

Output:

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 de l'échantillonneur

Lorsqu'une tâche Sampler est terminée avec succès, l'objet PrimitiveResult renvoyé contient une liste de SamplerPubResults, un par PUB Les compartiments de données de ces SamplerPubResult objets sont des objets de type dict qui contiennent un BitArray par ClassicalRegister dans le circuit.

La classe BitArray est un conteneur pour les données de tir ordonnées. Plus précisément, il stocke les chaînes de bits échantillonnées sous forme d'octets dans un tableau à deux dimensions. L'axe le plus à gauche de ce tableau correspond aux plans ordonnés, tandis que l'axe le plus à droite correspond aux octets.

En guise de premier exemple, examinons le circuit à dix qubits suivant :

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

Output:

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

Il peut parfois être pratique de convertir les données au 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 the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")

Output:

Counts: {'1111111111': 517, '0000000000': 507}

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

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

Exploitation BitArray d'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 post-traitement directement sur les BitArray objets plutôt que sur les dictionnaires de comptages. La BitArray classe propose toute une gamme 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 (1024, 1).
The bytes in register `alpha`, shot by shot:
[[1]
 [0]
 [1]
 ...
 [0]
 [1]
 [1]]

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

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

Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
              coeffs=[1.+0.j])` is: 0.01171875
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
              coeffs=[1.+0.j])` is: 0.01171875

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


Métadonnées des résultats

Outre les résultats d'exécution, les objets PrimitiveResult PubResult et contiennent un attribut de métadonnées facultatif concernant le travail qui a été soumis. Les métadonnées renvoyées (le cas échéant) dépendent de 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},")

Output:

The metadata of the PrimitiveResult is:
'version' : 2,

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

Etapes suivantes

Recommandations
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.