Zum Hauptinhalt springen

Primitive-Eingaben und -Ausgaben

Paketversionen

Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen, diese oder neuere Versionen zu verwenden.

qiskit[all]~=2.4.0

Diese Seite gibt einen Überblick über die Eingaben und Ausgaben der Qiskit-Primitives. Mit diesen Primitives kannst du eine Datenstruktur namens Primitive Unified Bloc (PUB) verwenden, um vektorisierte Workloads effizient zu definieren. Diese PUBs sind die grundlegende Arbeitseinheit für die Workload-Ausführung. Sie werden als Eingaben für die run()-Methode der Sampler- und Estimator-Primitives verwendet, die den definierten Workload als Job ausführen. Sobald der Job abgeschlossen ist, werden die Ergebnisse in einem Format zurückgegeben, das von den verwendeten PUBs und etwaigen festgelegten Optionen abhängt.

Überblick über PUBs

Wenn du die run()-Methode eines Primitives aufrufst, ist das Hauptargument eine list mit einem oder mehreren Tupeln – eines für jeden Circuit, der vom Primitive ausgeführt wird. Jedes dieser Tupel gilt als PUB, und die erforderlichen Elemente jedes Tupels in der Liste hängen vom verwendeten Primitive ab. Die in diesen Tupeln bereitgestellten Daten können auch in verschiedenen Formen angeordnet werden, um durch Broadcasting Flexibilität in einem Workload zu bieten – die entsprechenden Regeln werden in einem nachfolgenden Abschnitt beschrieben.

Estimator PUB

Für das Estimator Primitive sollte das Format des PUBs höchstens vier Werte enthalten:

  • Einen einzelnen QuantumCircuit, der ein oder mehrere Parameter-Objekte enthalten kann
  • Eine Liste mit einem oder mehreren Observablen, die die zu schätzenden Erwartungswerte angeben, in einem Array angeordnet (zum Beispiel ein einzelnes Observable als 0-d-Array, eine Liste von Observablen als 1-d-Array usw.). Die Daten können in einem der ObservablesArrayLike-Formate wie Pauli, SparsePauliOp, PauliList oder str vorliegen.
    hinweis

    Wenn du zwei kommutierende Observablen in verschiedenen PUBs, aber mit demselben Circuit hast, werden sie nicht mit derselben Messung geschätzt. Jeder PUB repräsentiert eine andere Messbasis, daher sind für jeden PUB separate Messungen erforderlich. Um sicherzustellen, dass kommutierende Observablen mit derselben Messung geschätzt werden, müssen sie innerhalb desselben PUBs gruppiert werden.

  • Eine Sammlung von Parameterwerten, gegen die der Circuit gebunden wird. Dies kann als ein einzelnes array-ähnliches Objekt angegeben werden, bei dem der letzte Index über die Parameter-Objekte des Circuits läuft, oder es kann weggelassen werden (bzw. äquivalent auf None gesetzt werden), wenn der Circuit keine Parameter-Objekte hat.
  • (Optional) eine Zielpräzision für die zu schätzenden Erwartungswerte

Sampler PUB

Für das Sampler Primitive enthält das PUB-Tupel höchstens drei Werte:

  • Einen einzelnen QuantumCircuit, der ein oder mehrere Parameter-Objekte enthalten kann Hinweis: Diese Circuits müssen auch Messanweisungen für jeden der zu sampelnden Qubits enthalten.
  • Eine Sammlung von Parameterwerten, gegen die der Circuit gebunden wird θk\theta_k (nur erforderlich, wenn Parameter-Objekte verwendet werden, die zur Laufzeit gebunden werden müssen)
  • (Optional) eine Anzahl von Shots, mit denen der Circuit gemessen werden soll

Der folgende Code zeigt ein Beispiel für vektorisierte Eingaben an das Estimator Primitive.

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

Broadcasting-Regeln

Die PUBs fassen Elemente aus mehreren Arrays (Observablen und Parameterwerte) zusammen, indem sie dieselben Broadcasting-Regeln wie NumPy befolgen. Dieser Abschnitt fasst diese Regeln kurz zusammen. Eine ausführliche Erklärung findest du in der NumPy-Dokumentation zu Broadcasting-Regeln.

Regeln:

  • Eingabe-Arrays müssen nicht die gleiche Anzahl an Dimensionen haben.
    • Das resultierende Array hat die gleiche Anzahl an Dimensionen wie das Eingabe-Array mit der größten Dimension.
    • Die Größe jeder Dimension ist die größte Größe der entsprechenden Dimension.
    • Fehlende Dimensionen werden als Größe eins angenommen.
  • Dimensionsvergleiche beginnen mit der rechtesten Dimension und gehen nach links.
  • Zwei Dimensionen sind kompatibel, wenn ihre Größen gleich sind oder wenn eine davon 1 ist.

Beispiele für Array-Paare, die Broadcasting unterstützen:

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

Beispiele für Array-Paare, die kein Broadcasting unterstützen:

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 gibt einen Erwartungswert für jedes Element der gebroadcasteten Form zurück.

Hier sind einige Beispiele für gängige Muster, ausgedrückt in Bezug auf Array-Broadcasting. Ihre begleitende visuelle Darstellung ist in der nachfolgenden Abbildung zu sehen:

Parameterwert-Sets werden durch n × m-Arrays dargestellt, und Observable-Arrays werden durch eine oder mehrere einspaltige Arrays dargestellt. In jedem der vorherigen Code-Beispiele werden die Parameterwert-Sets mit ihrem Observable-Array kombiniert, um die resultierenden Erwartungswert-Schätzungen zu erstellen.

  • Beispiel 1: (einzelnes Observable broadcasten) hat ein Parameterwert-Set als 5×1-Array und ein 1×1-Observables-Array. Das eine Element im Observables-Array wird mit jedem Element im Parameterwert-Set kombiniert, um ein einzelnes 5×1-Array zu erstellen, bei dem jedes Element eine Kombination des ursprünglichen Elements im Parameterwert-Set mit dem Element im Observables-Array ist.

  • Beispiel 2: (Zip) hat ein 5×1-Parameterwert-Set und ein 5×1-Observables-Array. Die Ausgabe ist ein 5×1-Array, bei dem jedes Element eine Kombination des n-ten Elements im Parameterwert-Set mit dem n-ten Element im Observables-Array ist.

  • Beispiel 3: (Äußeres Produkt) hat ein 1×6-Parameterwert-Set und ein 4×1-Observables-Array. Ihre Kombination ergibt ein 4×6-Array, das durch Kombinieren jedes Elements im Parameterwert-Set mit jedem Element im Observables-Array erstellt wird – daher wird jeder Parameterwert zu einer ganzen Spalte in der Ausgabe.

  • Beispiel 4: (Standard-nd-Verallgemeinerung) hat ein 3×6-Parameterwert-Set-Array und zwei 3×1-Observables-Arrays. Diese kombinieren sich zu zwei 3×6-Ausgabe-Arrays, ähnlich wie im vorherigen Beispiel.

Dieses Bild zeigt mehrere visuelle Darstellungen des Array-Broadcastings.

# 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

Jeder SparsePauliOp zählt in diesem Kontext als einzelnes Element, unabhängig von der Anzahl der im SparsePauliOp enthaltenen Paulis. Daher haben für die Zwecke dieser Broadcasting-Regeln alle folgenden Elemente die gleiche Form:

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

Die folgenden Listen von Operatoren, obwohl sie hinsichtlich der enthaltenen Informationen äquivalent sind, haben unterschiedliche Formen:

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

Überblick über Primitive-Ausgaben

Sobald ein oder mehrere PUBs zur Ausführung an einen QPU gesendet werden und ein Job erfolgreich abgeschlossen wird, werden die Daten als PrimitiveResult-Container-Objekt zurückgegeben. Das PrimitiveResult enthält eine iterierbare Liste von PubResult-Objekten, die die Ausführungsergebnisse für jeden PUB enthalten. Ein Job, der beispielsweise mit 20 PUBs übermittelt wird, gibt ein PrimitiveResult-Objekt zurück, das eine Liste mit 20 PubResults enthält, eines für jeden PUB.

Jedes dieser PubResult-Objekte besitzt sowohl ein data- als auch ein optionales metadata-Attribut. Das data-Attribut ist ein angepasstes DataBin, das im Fall des Estimators die Erwartungswert-Schätzungen oder im Fall des Samplers Stichproben der Circuit-Ausgabe enthält.

Das data-Attribut kann auch andere implementierungsspezifische Informationen wie Standardabweichungen enthalten. Das metadata-Attribut kann zusätzliche implementierungsspezifische Informationen über die Ausführung des zugehörigen PUBs enthalten.

Die folgende ist eine visuelle Übersicht der PrimitiveResult-Datenstruktur:

└── 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
├── ...
├── ...
└── ...
hinweis

Das obige ist ein Beispiel für Daten, die möglicherweise zurückgegeben werden. Die tatsächlich zurückgegebenen Daten hängen von der Implementierung ab.

Estimator-Ausgabe

Wie bereits erwähnt, hängen die im PubResult für das Estimator Primitive zurückgegebenen Daten von der Implementierung ab. Sie können beispielsweise ein Array von Erwartungswerten (PubResult.data.evs) und zugehörige Standardabweichungen (PubResult.data.stds) enthalten.

Der folgende Code-Ausschnitt beschreibt das PrimitiveResult- (und zugehörige PubResult-) Format für den oben erstellten Job.

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

Sampler-Ausgabe

Wenn ein Sampler-Job erfolgreich abgeschlossen wird, enthält das zurückgegebene PrimitiveResult-Objekt eine Liste von SamplerPubResults, eines pro PUB. Die Data-Bins dieser SamplerPubResult-Objekte sind dict-ähnliche Objekte, die ein BitArray pro ClassicalRegister im Circuit enthalten.

Die BitArray-Klasse ist ein Container für geordnete Shot-Daten. Genauer gesagt speichert sie die gesampelten Bit-Strings als Bytes in einem zweidimensionalen Array. Die äußerst linke Achse dieses Arrays läuft über geordnete Shots, während die äußerst rechte Achse über Bytes läuft.

Als erstes Beispiel schauen wir uns den folgenden Zehn-Qubit-Circuit an:

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

Manchmal ist es praktisch, vom Bytes-Format im BitArray zu Bit-Strings zu konvertieren. Die get_count-Methode gibt ein Dictionary zurück, das Bit-Strings auf die Anzahl ihrer Auftritte abbildet.

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

Wenn ein Circuit mehr als ein klassisches Register enthält, werden die Ergebnisse in verschiedenen BitArray-Objekten gespeichert. Das folgende Beispiel modifiziert den vorherigen Code-Ausschnitt, indem das klassische Register in zwei separate Register aufgeteilt wird:

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

BitArray-Objekte für performante Nachverarbeitung nutzen

Da Arrays im Allgemeinen eine bessere Leistung als Dictionaries bieten, empfiehlt es sich, jegliche Nachverarbeitung direkt auf den BitArray-Objekten statt auf Dictionaries von Zählwerten durchzuführen. Die BitArray-Klasse bietet eine Reihe von Methoden zur Durchführung einiger gängiger Nachverarbeitungsoperationen:

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

Ergebnis-Metadaten

Zusätzlich zu den Ausführungsergebnissen enthalten die PrimitiveResult- und PubResult-Objekte ein optionales Metadaten-Attribut über den übermittelten Job. Die zurückgegebenen Metadaten (falls vorhanden) sind implementierungsspezifisch.

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

Nächste Schritte

Empfehlungen