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.
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 entrada | Forma intrínseca | Exemplo 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ável | Depende 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:
| Entrada | Forma completa | Forma extrínseca | Forma intrínseca |
|---|---|---|---|
parameter_values | (4, 1, 2) | (4, 1) | (2,) |
noise_scale | (3,) | (3,) | () |
| Broadcast | Nenhuma | (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ída | Forma intrínseca | Descriçã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)
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.
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
shapeque 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 entrada | Shape | Resultado |
|---|---|---|
| (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
- Revise a visão geral de broadcasting.
- Entenda as entradas e saídas do Executor.