Pular para o conteúdo principal

Entradas e saídas dos primitivos

Versões dos pacotes

O código desta página foi desenvolvido usando os seguintes requisitos. Recomendamos usar estas versões ou mais recentes.

qiskit[all]~=2.4.0

Esta página apresenta uma visão geral das entradas e saídas dos primitivos Qiskit. Com esses primitivos, você pode usar uma estrutura de dados conhecida como Primitive Unified Bloc (PUB) para definir eficientemente cargas de trabalho vetorizadas. Esses PUBs são a unidade fundamental de trabalho para a execução de cargas de trabalho. Eles são usados como entradas para o método run() dos primitivos Sampler e Estimator, que executam a carga de trabalho definida como um job. Em seguida, após a conclusão do job, os resultados são retornados em um formato que depende dos PUBs utilizados e de quaisquer opções especificadas.

Visão geral dos PUBs

Ao invocar o método run() de um primitivo, o principal argumento necessário é uma list de uma ou mais tuplas — uma para cada Circuit sendo executado pelo primitivo. Cada uma dessas tuplas é considerada um PUB, e os elementos obrigatórios de cada tupla na lista dependem do primitivo utilizado. Os dados fornecidos a essas tuplas também podem ser organizados em diversas formas para oferecer flexibilidade em uma carga de trabalho por meio de broadcasting — cujas regras são descritas em uma seção seguinte.

PUB do Estimator

Para o primitivo Estimator, o formato do PUB deve conter no máximo quatro valores:

  • Um único QuantumCircuit, que pode conter um ou mais objetos Parameter
  • Uma lista de um ou mais observáveis, que especificam os valores esperados a estimar, organizados em um array (por exemplo, um único observável representado como um array 0-d, uma lista de observáveis como um array 1-d, e assim por diante). Os dados podem estar em qualquer um dos formatos ObservablesArrayLike, como Pauli, SparsePauliOp, PauliList ou str.
    nota

    Se você tiver dois observáveis comutativos em PUBs diferentes, mas com o mesmo Circuit, eles não serão estimados usando a mesma medição. Cada PUB representa uma base diferente para medição e, portanto, medições separadas são necessárias para cada PUB. Para garantir que observáveis comutativos sejam estimados usando a mesma medição, eles devem ser agrupados dentro do mesmo PUB.

  • Uma coleção de valores de parâmetros para vincular ao Circuit. Isso pode ser especificado como um único objeto semelhante a array, onde o último índice é sobre os objetos Parameter do Circuit, ou omitido (ou equivalentemente, definido como None) se o Circuit não tiver objetos Parameter.
  • (Opcionalmente) uma precisão alvo para os valores esperados a estimar

PUB do Sampler

Para o primitivo Sampler, o formato da tupla PUB contém no máximo três valores:

  • Um único QuantumCircuit, que pode conter um ou mais objetos Parameter Nota: Esses Circuits devem incluir instruções de medição para cada um dos qubits a serem amostrados.
  • Uma coleção de valores de parâmetros para vincular ao Circuit θk\theta_k (necessário somente se algum objeto Parameter for usado e precisar ser vinculado em tempo de execução)
  • (Opcionalmente) um número de shots para medir o Circuit

O código a seguir demonstra um conjunto de exemplo de entradas vetorizadas para o primitivo 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()
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

Regras de broadcasting

Os PUBs agregam elementos de múltiplos arrays (observáveis e valores de parâmetros) seguindo as mesmas regras de broadcasting do NumPy. Esta seção resume brevemente essas regras. Para uma explicação detalhada, consulte a documentação de regras de broadcasting do NumPy.

Regras:

  • Arrays de entrada não precisam ter o mesmo número de dimensões.
    • O array resultante terá o mesmo número de dimensões que o array de entrada com a maior dimensão.
    • O tamanho de cada dimensão é o maior tamanho da dimensão correspondente.
    • Dimensões ausentes são assumidas como tendo tamanho um.
  • As comparações de forma começam pela dimensão mais à direita e continuam para a esquerda.
  • Duas dimensões são compatíveis se seus tamanhos forem iguais ou se um deles for 1.

Exemplos de pares de arrays que fazem 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.

Exemplos de pares de arrays que não fazem broadcasting:

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

Estimator retorna uma estimativa de valor esperado para cada elemento da forma com broadcasting aplicado.

Aqui estão alguns exemplos de padrões comuns expressos em termos de broadcasting de arrays. Sua representação visual correspondente é mostrada na figura a seguir:

Os conjuntos de valores de parâmetros são representados por arrays n x m, e os arrays de observáveis são representados por uma ou mais arrays de coluna única. Para cada exemplo no código anterior, os conjuntos de valores de parâmetros são combinados com seu array de observáveis para criar as estimativas de valores esperados resultantes.

  • Exemplo 1: (broadcasting de observável único) tem um conjunto de valores de parâmetros que é um array 5x1 e um array de observáveis 1x1. O único item no array de observáveis é combinado com cada item no conjunto de valores de parâmetros para criar um único array 5x1, onde cada item é uma combinação do item original no conjunto de valores de parâmetros com o item no array de observáveis.

  • Exemplo 2: (zip) tem um conjunto de valores de parâmetros 5x1 e um array de observáveis 5x1. A saída é um array 5x1, onde cada item é uma combinação do n-ésimo item no conjunto de valores de parâmetros com o n-ésimo item no array de observáveis.

  • Exemplo 3: (produto externo/cartesiano) tem um conjunto de valores de parâmetros 1x6 e um array de observáveis 4x1. Sua combinação resulta em um array 4x6 criado combinando cada item no conjunto de valores de parâmetros com cada item no array de observáveis, e assim cada valor de parâmetro se torna uma coluna inteira na saída.

  • Exemplo 4: (generalização nd padrão) tem um array de conjunto de valores de parâmetros 3x6 e dois arrays de observáveis 3x1. Esses se combinam para criar dois arrays de saída 3x6 de forma semelhante ao exemplo anterior.

Esta imagem ilustra várias representações visuais de broadcasting de arrays.

SparsePauliOp

Cada SparsePauliOp conta como um único elemento neste contexto, independentemente do número de Paulis contidos no SparsePauliOp. Portanto, para fins dessas regras de broadcasting, todos os seguintes elementos têm a mesma forma:

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

As seguintes listas de operadores, embora equivalentes em termos de informação contida, têm formas diferentes:

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

Visão geral das saídas dos primitivos

Depois que um ou mais PUBs são enviados a uma QPU para execução e um job é concluído com sucesso, os dados são retornados como um objeto contêiner PrimitiveResult. O PrimitiveResult contém uma lista iterável de objetos PubResult que contêm os resultados de execução para cada PUB. Por exemplo, um job submetido com 20 PUBs retornará um objeto PrimitiveResult que contém uma lista de 20 PubResults, um correspondendo a cada PUB.

Cada um desses objetos PubResult possui atributos data e um metadata opcional. O atributo data é um DataBin personalizado que contém as estimativas de valores esperados no caso do Estimator, ou amostras da saída do Circuit no caso do Sampler.

O atributo data também pode incluir outras informações específicas de implementação, como desvios padrão. O atributo metadata pode conter informações adicionais específicas de implementação sobre a execução do PUB associado.

A seguir está um esboço visual da estrutura de dados 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
├── ...
├── ...
└── ...
nota

O acima é um exemplo dos dados que podem ser retornados. Os dados reais retornados dependem da implementação.

Saída do Estimator

Conforme mencionado anteriormente, os dados retornados no PubResult para o primitivo Estimator dependem da implementação. Por exemplo, pode conter um array de valores esperados (PubResult.data.evs) e desvios padrão associados (PubResult.data.stds).

O trecho de código abaixo descreve o formato PrimitiveResult (e o PubResult associado) para o job criado acima.

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

Saída do Sampler

Quando um job do Sampler é concluído com sucesso, o objeto PrimitiveResult retornado contém uma lista de SamplerPubResults, um por PUB. Os data bins desses objetos SamplerPubResult são objetos semelhantes a dicionários que contêm um BitArray por ClassicalRegister no Circuit.

A classe BitArray é um contêiner para dados de shots ordenados. Em mais detalhes, ela armazena as bitstrings amostradas como bytes dentro de um array bidimensional. O eixo mais à esquerda deste array percorre os shots ordenados, enquanto o eixo mais à direita percorre os bytes.

Como primeiro exemplo, vamos examinar o seguinte Circuit de dez 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]]

Às vezes pode ser conveniente converter do formato de bytes do BitArray para bitstrings. O método get_count retorna um dicionário mapeando bitstrings para o número de vezes que elas ocorreram.

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

Quando um Circuit contém mais de um registrador clássico, os resultados são armazenados em diferentes objetos BitArray. O exemplo a seguir modifica o trecho anterior dividindo o registrador clássico em dois registradores distintos:

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

Aproveitando objetos BitArray para pós-processamento eficiente

Como arrays geralmente oferecem melhor desempenho em comparação a dicionários, é aconselhável realizar qualquer pós-processamento diretamente nos objetos BitArray em vez de em dicionários de contagens. A classe BitArray oferece uma variedade de métodos para realizar algumas operações comuns de pós-processamento:

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

Metadados do resultado

Além dos resultados de execução, os objetos PrimitiveResult e PubResult contêm um atributo de metadados opcional sobre o job submetido. Os metadados retornados (se houver) são específicos da implementação.

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

Próximos passos

Recomendações