Pular para o conteúdo principal

Broadcasting do Executor

Os dados fornecidos à primitiva Executor podem ser organizados em diversas formas para oferecer flexibilidade em uma carga de trabalho por meio de broadcasting. Este guia explica como o Executor lida com entradas e saídas em array usando semântica de broadcasting. Compreender esses conceitos ajudará você a varrer valores de parâmetros de forma eficiente, combinar múltiplas configurações e interpretar a forma dos dados retornados.

nota

Os exemplos neste tópico não podem ser executados de forma independente. Eles pressupõem que você definiu os circuitos adequados, utilizou o pass manager do Samplomatic para adicionar boxes e anotações, e usou o método build do Samplomatic para obter um circuito template e samplex para cada bloco de código, conforme necessário.

Exemplo de início rápido​

Este exemplo demonstra a ideia central. Ele cria um circuito paramétrico e cinco configurações de parâmetros diferentes. O Executor executa todas as cinco configurações e retorna os dados organizados por configuração, com um resultado por registrador clássico em cada item do programa quântico.

O restante deste guia faz referência a este exemplo para explicar como isso funciona e como construir varreduras mais complexas, incluindo entradas e randomização baseadas no Samplomatic.

import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from qiskit.transpiler import generate_preset_pass_manager

# A circuit with 2 parameters
# This circuit is used throughout the rest of this guide.
circuit = QuantumCircuit(4)
circuit.rx(Parameter("a"), 0)
circuit.rx(Parameter("b"), 1)
circuit.h(2)
circuit.cx(2, 3)
circuit.measure_all()

# 5 different parameter configurations (shape: 5 configurations × 2 parameters)
parameter_values = np.linspace(0, np.pi, 10).reshape(5, 2)

# Initialize the service and choose a backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# Transpile to ISA circuit
preset_pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
)
isa_circuit = preset_pass_manager.run(circuit)

# This program is used throughout the rest of this guide.
program = QuantumProgram(shots=1024)
program.append_circuit_item(isa_circuit, circuit_arguments=parameter_values)

# initialize an Executor with default options
executor = Executor(mode=backend)

# Run and get results
result = executor.run(program).result()

# result is a list with one entry per program item
# result[0] is a dict mapping classical register names to data arrays
# Output bool arrays have shape (5, 1024, 4)
# 5 = number of parameter configurations
# 1024 = number of shots
# 4 = bits in the classical register
result[0]["meas"]

Eixos intrínsecos e extrínsecos​

O broadcasting se aplica apenas aos eixos extrínsecos. Os eixos intrínsecos são sempre preservados conforme especificado.

  • Eixos intrínsecos (mais à direita): Determinados pelo tipo de dado. Por exemplo, se o seu circuito tem três parâmetros, os valores de parâmetro requerem três números, resultando em uma forma intrínseca de (3,).

  • Eixos extrínsecos (mais à esquerda): Suas dimensões de varredura. Elas definem quantas configurações você deseja executar.

Tipo de entradaForma intrínsecaExemplo de forma completa
Valores de parâmetros (n parâmetros)(n,)(5, 3) para cinco configurações e três parâmetros
Entradas escalares (por exemplo, fator de escala de ruído)()(4,) para quatro configurações
Observáveis (se aplicável)variávelDepende do tipo de observável

Exemplo​

Considere um circuito com dois parâmetros que você deseja varrer em uma grade 4x3 de configurações, variando valores de parâmetros e um fator de escala de ruído:

import numpy as np

# Parameter values: 4 configurations along axis 0, intrinsic shape (2,)
# Full shape: (4, 1, 2) - the "1" allows broadcasting with noise_scale
parameter_values = np.array([
[[0.1, 0.2]],
[[0.3, 0.4]],
[[0.5, 0.6]],
[[0.7, 0.8]],
]) # shape (4, 1, 2)

# Noise scale: 3 configurations, intrinsic shape () (scalar)
# Full shape: (3,)
noise_scale = np.array([0.8, 1.0, 1.2]) # shape (3,)

# Extrinsic shapes: (4, 1) and (3,) → broadcast to (4, 3)
# Result: 12 total configurations in a 4×3 grid
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": parameter_values,
"noise_scales.mod_ref1": noise_scale,
},
)

As formas são as seguintes:

EntradaForma completaForma extrínsecaForma intrínseca
parameter_values(4, 1, 2)(4, 1)(2,)
noise_scale(3,)(3,)()
BroadcastNenhuma(4, 3)Nenhuma

Formas dos arrays de saída​

Os arrays de saída seguem o mesmo padrão extrínseco/intrínseco:

  • Forma extrínseca: Corresponde à forma de broadcast de todas as entradas

  • Forma intrínseca: Determinada pelo tipo de saída

A saída mais comum são dados de bitstring de medições, formatados como um array de valores booleanos:

Tipo de saídaForma intrínsecaDescrição
Dados de registrador clássico(num_shots, creg_size)Dados de bitstring de medições

Exemplo​

Se você fornecer entradas com formas extrínsecas (4, 1) e (3,), a forma extrínseca de broadcast é (4, 3). O código a seguir usa um circuito com 1024 shots e um registrador clássico de 4 bits (conforme definido no exemplo do Início rápido):

# Input extrinsic shapes: (4, 1) and (3,) → (4, 3)
# Output for classical register "meas":
# extrinsic: (4, 3)
# intrinsic: (1024, 4) - shots × bits
# full shape: (4, 3, 1024, 4)

result = executor.run(program).result()
meas_data = result[0]["meas"] # result[0] for first program item
print(meas_data.shape) # (4, 3, 1024, 4)

# Access a specific configuration
config_2_1 = meas_data[2, 1, :, :] # shape (1024, 4)
nota

Cada configuração executa o número total de shots especificado no programa quântico. Os shots não são divididos entre as configurações. Por exemplo, se você solicitar 1024 shots e tiver 10 configurações, cada configuração executa 1024 shots (10.240 shots executados no total).

Randomização e o parâmetro shape​

Ao usar um samplex, cada elemento da forma extrínseca corresponde a uma execução independente do circuito. O samplex tipicamente injeta aleatoriedade (por exemplo, twirling de portas) em cada execução, portanto, mesmo sem solicitar explicitamente múltiplas randomizações, cada elemento recebe uma realização aleatória.

Você pode usar o parâmetro shape para aumentar a forma extrínseca do item, adicionando efetivamente eixos que correspondem especificamente a randomizar a mesma configuração múltiplas vezes. Ele deve ser broadcastable a partir da forma implícita nos seus samplex_arguments. Os eixos onde shape excede a forma implícita enumeram randomizações adicionais independentes.

Sem eixos de randomização explícitos​

Se você omitir shape (ou defini-lo para corresponder às formas de entrada), você obtém uma execução por configuração de entrada. Cada execução ainda é randomizada pelo samplex, mas com apenas uma única realização aleatória, você não se beneficia da média sobre múltiplas randomizações.

nota

Se você está acostumado a ativar o twirling com um flag simples como twirling=True, note que o Executor requer que você solicite explicitamente múltiplas randomizações com o argumento shape para permitir que suas rotinas de pós-processamento obtenham os benefícios da média sobre múltiplas randomizações. Uma única randomização (o padrão quando shape é omitido) aplica Gates aleatórios, mas tipicamente não oferece vantagem sobre executar o circuito base sem randomização.

O exemplo a seguir demonstra o comportamento padrão:

program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
# shape defaults to (10,) - one randomized execution per config
)
# Output shape for "meas": (10, num_shots, creg_size)

Eixo de randomização único​

Para executar múltiplas randomizações por configuração, estenda a forma com eixos adicionais. Por exemplo, o código a seguir executa 20 randomizações para cada uma das 10 configurações de parâmetros:

program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
shape=(20, 10), # 20 randomizations × 10 configurations
)
# Output shape for "meas": (20, 10, num_shots, creg_size)

Múltiplos eixos de randomização​

Você pode organizar randomizações em uma grade multidimensional. Isso é útil para análise estruturada, por exemplo, separando randomizações por tipo ou agrupando-as para processamento estatístico.

Aqui, a forma extrínseca de entrada (10,) faz broadcast para a forma solicitada (2, 14, 10), com os eixos 0 e 1 preenchidos por randomizações independentes.

program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # extrinsic (10,)
},
# 2×14=28 randomizations per configuration, 10 configurations
# Or you could set shape=(28, 10) for the same effect
shape=(2, 14, 10),
)
# Output shape for "meas": (2, 14, 10, num_shots, creg_size)

Como shape e as formas de entrada interagem​

O parâmetro shape deve ser broadcastable a partir das suas formas extrínsecas de entrada. Isso significa:

  • Formas de entrada com dimensões de tamanho 1 podem expandir para corresponder a shape.

  • As formas de entrada devem se alinhar pela direita com shape.

  • Os eixos em shape que excedem as dimensões de entrada enumeram randomizações.

Note que shape pode conter dimensões de tamanho 1 que se expandem para corresponder às dimensões de entrada, conforme ilustrado na última linha da tabela a seguir.

Exemplos:

Extrínseco de entradaShapeResultado
(10,)(10,)10 configurações, 1 randomização cada
(10,)(5, 10)10 configurações, 5 randomizações cada
(10,)(2, 3, 10)10 configurações, 2×3=6 randomizações cada
(4, 1)(4, 5)4 configurações, 5 randomizações cada
(4, 3)(2, 4, 3)4×3=12 configurações, 2 randomizações cada
(4, 3)(2, 1, 3)4×3=12 configurações, 2 randomizações cada (o 1 se expande para 4)

Indexar nos resultados​

Com eixos de randomização, você pode indexar em combinações específicas de randomização/parâmetro:

# Using shape=(2, 14, 10) with input extrinsic shape (10,), and
# 1024 shots and 4 classical registers.
result = executor.run(program).result()
meas_data = result[0]["meas"] # shape (2, 14, 10, 1024, 4)

# Get all shots for randomization (0, 7) and parameter config 3
specific = meas_data[0, 7, 3, :, :] # shape (1024, 4)

# Average over all randomizations for parameter config 5 on bit 2
averaged = meas_data[:, :, 5, :, 2].mean(axis=(0, 1))

Padrões comuns​

Varrer um único parâmetro​

Use um código como o seguinte para varrer um parâmetro mantendo os outros fixos:

# Circuit has 2 parameters, sweep first one over 20 values
sweep_values = np.linspace(0, 2*np.pi, 20)

parameter_values = np.column_stack([
sweep_values,
np.full(20, 0.5),
]) # shape (20, 2)

Criar uma varredura em grade 2D​

Para criar uma grade sobre três parâmetros:

# Sweep param 0 over 10 values, param 1 over 8 values, param 2 fixed
p0 = np.linspace(0, np.pi, 10)[:, np.newaxis, np.newaxis] # (10, 1, 1)
p1 = np.linspace(0, np.pi, 8)[np.newaxis, :, np.newaxis] # (1, 8, 1)
p2 = np.array([[[0.5]]]) # (1, 1, 1)

parameter_values = np.broadcast_arrays(p0, p1, p2)
parameter_values = np.stack(parameter_values, axis=-1).squeeze() # (10, 8, 3)

# Extrinsic shape: (10, 8), intrinsic shape: (3,)

Combinar múltiplas entradas​

Ao combinar entradas com diferentes formas intrínsecas, alinhe as dimensões extrínsecas usando eixos de tamanho 1:

# 4 parameter configurations, 3 noise scales → 4×3 = 12 total configurations
parameter_values = np.random.rand(4, 1, 2) # extrinsic (4, 1), intrinsic (2,)
noise_scale = np.array([0.8, 1.0, 1.2]) # extrinsic (3,), intrinsic ()

# Broadcasted extrinsic shape: (4, 3)

Próximos passos​

Recomendações