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 enumerám 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 enumerám 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