Przejdź do głównej treści

Wejścia i wyjścia prymitywów

Wersje pakietów

Kod na tej stronie został opracowany przy użyciu poniższych wymagań. Zalecamy korzystanie z tych lub nowszych wersji.

qiskit[all]~=2.4.0

Ta strona zawiera omówienie wejść i wyjść prymitywów Qiskit. Dzięki tym prymitywom możesz używać struktury danych zwanej Primitive Unified Bloc (PUB) do wydajnego definiowania zwektoryzowanych zadań. PUBy stanowią podstawową jednostkę pracy do wykonywania zadań. Używa się ich jako wejść do metody run() prymitywów Sampler i Estimator, które uruchamiają zdefiniowane zadanie jako job. Po jego zakończeniu wyniki są zwracane w formacie zależnym od użytych PUBów i określonych opcji.

Omówienie PUBów

Podczas wywoływania metody run() prymitywu głównym wymaganym argumentem jest list zawierająca jedną lub więcej krotek – po jednej dla każdego Circuit wykonywanego przez prymityw. Każda z tych krotek jest traktowana jako PUB, a wymagane elementy każdej krotki na liście zależą od użytego prymitywu. Dane przekazywane do tych krotek można również układać w różnych kształtach, aby zapewnić elastyczność zadania dzięki mechanizmowi rozgłaszania (broadcasting) – zasady tego mechanizmu opisano w dalszej sekcji.

Estimator PUB

W przypadku prymitywu Estimator format PUB powinien zawierać co najwyżej cztery wartości:

  • Jeden QuantumCircuit, który może zawierać jeden lub więcej obiektów Parameter
  • Listę jednej lub więcej obserwowalnych, określających wartości oczekiwane do oszacowania, ułożonych w tablicę (np. pojedyncza obserwowalna reprezentowana jako tablica 0-wymiarowa, lista obserwowalnych jako tablica 1-wymiarowa itd.). Dane mogą być w dowolnym z formatów ObservablesArrayLike, takich jak Pauli, SparsePauliOp, PauliList lub str.
    uwaga

    Jeśli masz dwie przemieniające się obserwowalne w różnych PUBach, ale z tym samym Circuit, nie zostaną one oszacowane przy użyciu tego samego pomiaru. Każdy PUB reprezentuje inną bazę pomiarową, dlatego dla każdego PUBu wymagane są osobne pomiary. Aby przemieniające się obserwowalne były oszacowywane przy użyciu tego samego pomiaru, muszą być zgrupowane w tym samym PUBie.

  • Zbiór wartości parametrów do powiązania z Circuit. Można go podać jako pojedynczy obiekt tablicopodobny, gdzie ostatni indeks odpowiada obiektom Parameter Circuit, lub pominąć (albo równoważnie ustawić na None), jeśli Circuit nie zawiera obiektów Parameter.
  • (Opcjonalnie) docelową precyzję szacowania wartości oczekiwanych

Sampler PUB

W przypadku prymitywu Sampler format krotki PUB zawiera co najwyżej trzy wartości:

  • Jeden QuantumCircuit, który może zawierać jeden lub więcej obiektów Parameter Uwaga: Te obwód muszą również zawierać instrukcje pomiarowe dla każdego Qubitu, który ma być próbkowany.
  • Zbiór wartości parametrów do powiązania z Circuit θk\theta_k (potrzebny tylko, jeśli używane są obiekty Parameter, które muszą być powiązane w czasie wykonywania)
  • (Opcjonalnie) liczbę strzałów (shots), z którą Circuit ma być mierzony

Poniższy kod demonstruje przykładowy zestaw zwektoryzowanych wejść dla prymitywu 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()

Reguły rozgłaszania (broadcasting)

PUBy agregują elementy z wielu tablic (obserwowalne i wartości parametrów), stosując te same reguły rozgłaszania co NumPy. Ta sekcja pokrótce je podsumowuje. Szczegółowe wyjaśnienie znajdziesz w dokumentacji reguł rozgłaszania NumPy.

Reguły:

  • Tablice wejściowe nie muszą mieć tej samej liczby wymiarów.
    • Tablica wynikowa będzie miała tę samą liczbę wymiarów co tablica wejściowa o największej liczbie wymiarów.
    • Rozmiar każdego wymiaru jest największym rozmiarem odpowiedniego wymiaru.
    • Brakujące wymiary przyjmuje się jako mające rozmiar jeden.
  • Porównania kształtów zaczyna się od wymiaru najbardziej wysuniętego na prawo i kontynuuje w lewo.
  • Dwa wymiary są zgodne, jeśli ich rozmiary są równe lub jeśli jeden z nich wynosi 1.

Przykłady par tablic, które się rozgłaszają:

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

Przykłady par tablic, które się nie rozgłaszają:

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 zwraca jedną wartość oczekiwaną dla każdego elementu rozgłoszonego kształtu.

Poniżej przedstawiono kilka przykładów typowych wzorców wyrażonych w kategoriach rozgłaszania tablic. Ich wizualna reprezentacja jest pokazana na poniższym rysunku:

Zestawy wartości parametrów są reprezentowane przez tablice n x m, a tablice obserwowalnych – przez jedną lub więcej tablic jednokolumnowych. W każdym z poniższych przykładów zestawy wartości parametrów są łączone z odpowiadającą im tablicą obserwowalnych, tworząc wynikowe oszacowania wartości oczekiwanych.

  • Przykład 1: (rozgłoszenie pojedynczej obserwowalnej) – zestaw wartości parametrów to tablica 5x1, a tablica obserwowalnych to tablica 1x1. Jeden element tablicy obserwowalnych jest łączony z każdym elementem zestawu wartości parametrów, tworząc pojedynczą tablicę 5x1, gdzie każdy element jest kombinacją oryginalnego elementu zestawu wartości parametrów z elementem tablicy obserwowalnych.

  • Przykład 2: (zip) – zestaw wartości parametrów to tablica 5x1, a tablica obserwowalnych to tablica 5x1. Wyjściem jest tablica 5x1, gdzie każdy element jest kombinacją n-tego elementu zestawu wartości parametrów z n-tym elementem tablicy obserwowalnych.

  • Przykład 3: (iloczyn zewnętrzny/product) – zestaw wartości parametrów to tablica 1x6, a tablica obserwowalnych to tablica 4x1. Ich kombinacja daje tablicę 4x6, tworzoną przez połączenie każdego elementu zestawu wartości parametrów z każdym elementem tablicy obserwowalnych – każda wartość parametru staje się w ten sposób całą kolumną w wyjściu.

  • Przykład 4: (standardowe uogólnienie nd) – tablica zestawu wartości parametrów to tablica 3x6, a dwie tablice obserwowalnych to tablice 3x1. Łączą się one, tworząc dwie wyjściowe tablice 3x6 w sposób podobny do poprzedniego przykładu.

Ten obraz ilustruje kilka wizualnych reprezentacji rozgłaszania tablic.

# 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

Każdy SparsePauliOp liczy się jako pojedynczy element w tym kontekście, niezależnie od liczby operatorów Pauliego zawartych w SparsePauliOp. Zatem na potrzeby tych reguł rozgłaszania wszystkie poniższe elementy mają ten sam kształt:

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

Poniższe listy operatorów, choć równoważne pod względem zawartych informacji, mają różne kształty:

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

Przegląd wyjść prymitywów

Po wysłaniu jednego lub więcej PUBów do QPU i pomyślnym ukończeniu zadania dane są zwracane jako obiekt kontenera PrimitiveResult. PrimitiveResult zawiera iterowalną listę obiektów PubResult, które przechowują wyniki wykonania dla każdego PUBu. Na przykład zadanie zgłoszone z 20 PUBami zwróci obiekt PrimitiveResult zawierający listę 20 obiektów PubResult, po jednym dla każdego PUBu.

Każdy z tych obiektów PubResult posiada zarówno atrybut data, jak i opcjonalny atrybut metadata. Atrybut data to dostosowany obiekt DataBin, który zawiera oszacowania wartości oczekiwanych w przypadku Estimatora lub próbki wyjścia Circuit w przypadku Samplera.

Atrybut data może również zawierać inne informacje specyficzne dla implementacji, takie jak odchylenia standardowe. Atrybut metadata może zawierać dodatkowe informacje specyficzne dla implementacji dotyczące wykonania powiązanego PUBu.

Poniżej przedstawiono wizualny zarys struktury danych 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
├── ...
├── ...
└── ...
uwaga

Powyższy przykład ilustruje dane, które mogą zostać zwrócone. Faktyczne zwrócone dane zależą od implementacji.

Wyjście Estimatora

Jak wspomniano wcześniej, dane zwrócone w PubResult dla prymitywu Estimator zależą od implementacji. Na przykład mogą zawierać tablicę wartości oczekiwanych (PubResult.data.evs) oraz powiązane odchylenia standardowe (PubResult.data.stds).

Poniższy fragment kodu opisuje format PrimitiveResult (oraz powiązanego PubResult) dla zadania utworzonego powyżej.

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

Wyjście Samplera

Po pomyślnym ukończeniu zadania przez Sampler zwracany obiekt PrimitiveResult zawiera listę obiektów SamplerPubResult — po jednym na każdy PUB. Zasobniki danych tych obiektów SamplerPubResult są obiektami słownikopodobnymi zawierającymi po jednym BitArray na każdy ClassicalRegister w Circuit.

Klasa BitArray to kontener na uporządkowane dane pomiarów. Dokładniej: przechowuje próbkowane ciągi bitów jako bajty wewnątrz dwuwymiarowej tablicy. Lewa oś tej tablicy przebiega po kolejnych pomiarach (shots), prawa — po bajtach.

Jako pierwszy przykład przyjrzyjmy się poniższemu dziesięcio-qubitowemu Circuit:

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

Czasami wygodnie jest przekonwertować format bajtów z BitArray na ciągi bitów. Metoda get_count zwraca słownik odwzorowujący ciągi bitów na liczbę ich wystąpień.

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

Gdy Circuit zawiera więcej niż jeden rejestr klasyczny, wyniki są przechowywane w oddzielnych obiektach BitArray. Poniższy przykład modyfikuje poprzedni fragment, dzieląc rejestr klasyczny na dwa odrębne rejestry:

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

Wykorzystanie obiektów BitArray do wydajnego post-processingu

Ponieważ tablice generalnie oferują lepszą wydajność niż słowniki, zaleca się wykonywanie wszelkiego post-processingu bezpośrednio na obiektach BitArray, a nie na słownikach zliczeń. Klasa BitArray oferuje szereg metod do wykonywania typowych operacji post-processingu:

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

Metadane wyników

Oprócz wyników wykonania, obiekty PrimitiveResult i PubResult zawierają opcjonalny atrybut metadanych dotyczący przesłanego zadania. Zwracane metadane (jeśli są) są specyficzne dla implementacji.

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

Następne kroki

Zalecenia