Avaliação comparativa em tempo real para seleção de qubits
Estimativa de uso: 4 minutos em um processador Eagle r2 (OBSERVAÇÃO: essa é apenas uma estimativa. Seu tempo de execução pode variar)
Segundo plano
Este tutorial mostra como executar experimentos de caracterização em tempo real e atualizar as propriedades de back-end para melhorar a seleção de qubit ao mapear um circuito para os qubits físicos em uma QPU. Você aprenderá os experimentos básicos de caracterização que são usados para determinar as propriedades da QPU, como fazê-los no Qiskit e como atualizar as propriedades salvas no objeto backend que representa a QPU com base nesses experimentos.
As propriedades relatadas pelo QPU são atualizadas uma vez por dia, mas o sistema pode se desviar mais rapidamente do que o tempo entre as atualizações. Isso pode afetar a confiabilidade das rotinas de seleção de qubit no estágio Layout do gerenciador de passagem, pois elas estariam usando propriedades relatadas que não representam o estado atual da QPU. Por esse motivo, pode valer a pena dedicar algum tempo da QPU a experimentos de caracterização, que podem ser usados para atualizar as propriedades da QPU usadas pela rotina Layout .
Requisitos
Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:
- Qiskit SDK v2.0 ou posterior, com suporte para visualização
- Qiskit Runtime v0.40 ou posterior (
pip install qiskit-ibm-runtime) - Qiskit Experiments v0.12 ou posterior (
pip install qiskit-experiments) - Biblioteca de gráficos Rustworkx (
pip install rustworkx)
Instalação
from qiskit_ibm_runtime import SamplerV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import hellinger_fidelity
from qiskit.transpiler import InstructionProperties
from qiskit_experiments.library import (
T1,
T2Hahn,
LocalReadoutError,
StandardRB,
)
from qiskit_experiments.framework import BatchExperiment, ParallelExperiment
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session
from datetime import datetime
from collections import defaultdict
import numpy as np
import rustworkx
import matplotlib.pyplot as plt
import copyPasso 1: Mapear entradas clássicas para um problema quântico
Para avaliar a diferença de desempenho, consideramos um circuito que prepara um estado Bell em uma cadeia linear de comprimento variável. A fidelidade do estado de Bell nas extremidades da cadeia é medida.
from qiskit import QuantumCircuit
ideal_dist = {"00": 0.5, "11": 0.5}
num_qubits_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 127]
circuits = []
for num_qubits in num_qubits_list:
circuit = QuantumCircuit(num_qubits, 2)
circuit.h(0)
for i in range(num_qubits - 1):
circuit.cx(i, i + 1)
circuit.barrier()
circuit.measure(0, 0)
circuit.measure(num_qubits - 1, 1)
circuits.append(circuit)
circuits[-1].draw(output="mpl", style="clifford", fold=-1)Output:
Configurar backend e mapa de acoplamento
Primeiro, selecione um backend
# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
qubits = list(range(backend.num_qubits))Em seguida, obtenha seu mapa de acoplamento
coupling_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
# Get unidirectional coupling map
one_dir_coupling_map = coupling_graph.edge_list()Para avaliar o maior número possível de portas de dois qubits simultaneamente, separamos o mapa de acoplamento em um layered_coupling_map. Esse objeto contém uma lista de camadas em que cada camada é uma lista de bordas nas quais portas de dois qubits podem ser executadas ao mesmo tempo. Isso também é chamado de coloração de borda do mapa de acoplamento.
# Get layered coupling map
edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)
layered_coupling_map = defaultdict(list)
for edge_idx, color in edge_coloring.items():
layered_coupling_map[color].append(
coupling_graph.get_edge_endpoints_by_index(edge_idx)
)
layered_coupling_map = [
sorted(layered_coupling_map[i])
for i in sorted(layered_coupling_map.keys())
]Experimentos de caracterização
Uma série de experimentos é usada para caracterizar as principais propriedades dos qubits em uma QPU. São eles: , , erro de leitura e erro de porta de um e dois qubits. Resumiremos brevemente quais são essas propriedades e faremos referência aos experimentos no pacote qiskit-experiments que são usados para caracterizá-las.
T1
é o tempo característico que leva para um qubit excitado cair para o estado fundamental devido aos processos de decoerência de amortecimento de amplitude. Em um experimento , medimos um qubit excitado após um atraso. Quanto maior for o tempo de atraso, maior será a probabilidade de o qubit cair para o estado fundamental. O objetivo do experimento experimento é caracterizar a taxa de decaimento do qubit em direção ao estado fundamental.
T2
representa o tempo necessário para que a projeção do vetor Bloch de um único qubit de um único qubit no plano XY caia para aproximadamente 37% ( ) de de sua amplitude inicial devido aos processos de decoerência de defasagem. Em um experimento Hahn Echo, podemos estimar a taxa desse decaimento.
Caracterização do erro de preparação e medição do estado (SPAM)
Em um experimento de caracterização de erro SPAM, os qubits são preparados em um determinado estado ( ou ) e medidos. A probabilidade de medir um estado diferente do preparado fornece a probabilidade do erro.
Benchmarking aleatório de um qubit e dois qubits
O benchmarking aleatório (RB) é um protocolo popular para caracterizar a taxa de erro dos processadores quânticos. Um experimento RB consiste na geração de circuitos aleatórios de Clifford aleatórios nos qubits fornecidos, de modo que a unitária computada pelos circuitos seja a identidade. Depois de executar os circuitos, o número de disparos que resultam em um erro (ou seja, uma saída diferente do estado fundamental) é contado e, a partir desses dados, é possível inferir estimativas de erro para o dispositivo quântico, calculando o erro por Clifford.
# Create T1 experiments on all qubit in parallel
t1_exp = ParallelExperiment(
[
T1(
physical_qubits=[qubit],
delays=[1e-6, 20e-6, 40e-6, 80e-6, 200e-6, 400e-6],
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create T2-Hahn experiments on all qubit in parallel
t2_exp = ParallelExperiment(
[
T2Hahn(
physical_qubits=[qubit],
delays=[1e-6, 20e-6, 40e-6, 80e-6, 200e-6, 400e-6],
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create readout experiments on all qubit in parallel
readout_exp = LocalReadoutError(qubits)
# Create single-qubit RB experiments on all qubit in parallel
singleq_rb_exp = ParallelExperiment(
[
StandardRB(
physical_qubits=[qubit], lengths=[10, 100, 500], num_samples=10
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create two-qubit RB experiments on the three layers of disjoint edges of the heavy-hex
twoq_rb_exp_batched = BatchExperiment(
[
ParallelExperiment(
[
StandardRB(
physical_qubits=pair,
lengths=[10, 50, 100],
num_samples=10,
)
for pair in layer
],
backend,
analysis=None,
)
for layer in layered_coupling_map
],
backend,
flatten_results=True,
analysis=None,
)Propriedades da QPU ao longo do tempo
Observando as propriedades de QPU relatadas ao longo do tempo (consideraremos uma única semana abaixo), vemos como elas podem flutuar em uma escala de um único dia. Pequenas flutuações podem ocorrer até mesmo em um dia. Nesse cenário, as propriedades informadas (atualizadas uma vez por dia) não capturarão com precisão o status atual da QPU. Além disso, se um trabalho for transpilado localmente (usando as propriedades atuais informadas) e enviado, mas executado somente em um momento posterior (minutos ou dias), ele poderá correr o risco de ter usado propriedades desatualizadas para a seleção de qubit na etapa de transpilação. Isso destaca a importância de ter informações atualizadas sobre a QPU no momento da execução. Primeiro, vamos recuperar as propriedades em um determinado intervalo de tempo.
instruction_2q_name = "cz" # set the name of the default 2q of the device
errors_list = []
for day_idx in range(10, 17):
calibrations_time = datetime(
year=2025, month=8, day=day_idx, hour=0, minute=0, second=0
)
targer_hist = backend.target_history(datetime=calibrations_time)
t1_dict, t2_dict = {}, {}
for qubit in range(targer_hist.num_qubits):
t1_dict[qubit] = targer_hist.qubit_properties[qubit].t1
t2_dict[qubit] = targer_hist.qubit_properties[qubit].t2
errors_dict = {
"1q": targer_hist["sx"],
"2q": targer_hist[f"{instruction_2q_name}"],
"spam": targer_hist["measure"],
"t1": t1_dict,
"t2": t2_dict,
}
errors_list.append(errors_dict)Em seguida, vamos plotar os valores
fig, axs = plt.subplots(5, 1, figsize=(10, 20), sharex=False)
# Plot for T1 values
for qubit in range(targer_hist.num_qubits):
t1s = []
for errors_dict in errors_list:
t1_dict = errors_dict["t1"]
try:
t1s.append(t1_dict[qubit] / 1e-6)
except:
print(f"missing t1 data for qubit {qubit}")
axs[0].plot(t1s)
axs[0].set_title("T1")
axs[0].set_ylabel(r"Time ($\mu s$)")
axs[0].set_xlabel("Days")
# Plot for T2 values
for qubit in range(targer_hist.num_qubits):
t2s = []
for errors_dict in errors_list:
t2_dict = errors_dict["t2"]
try:
t2s.append(t2_dict[qubit] / 1e-6)
except:
print(f"missing t2 data for qubit {qubit}")
axs[1].plot(t2s)
axs[1].set_title("T2")
axs[1].set_ylabel(r"Time ($\mu s$)")
axs[1].set_xlabel("Days")
# Plot SPAM values
for qubit in range(targer_hist.num_qubits):
spams = []
for errors_dict in errors_list:
spam_dict = errors_dict["spam"]
spams.append(spam_dict[tuple([qubit])].error)
axs[2].plot(spams)
axs[2].set_title("SPAM Errors")
axs[2].set_ylabel("Error Rate")
axs[2].set_xlabel("Days")
# Plot 1Q Gate Errors
for qubit in range(targer_hist.num_qubits):
oneq_gates = []
for errors_dict in errors_list:
oneq_gate_dict = errors_dict["1q"]
oneq_gates.append(oneq_gate_dict[tuple([qubit])].error)
axs[3].plot(oneq_gates)
axs[3].set_title("1Q Gate Errors")
axs[3].set_ylabel("Error Rate")
axs[3].set_xlabel("Days")
# Plot 2Q Gate Errors
for pair in one_dir_coupling_map:
twoq_gates = []
for errors_dict in errors_list:
twoq_gate_dict = errors_dict["2q"]
twoq_gates.append(twoq_gate_dict[pair].error)
axs[4].plot(twoq_gates)
axs[4].set_title("2Q Gate Errors")
axs[4].set_ylabel("Error Rate")
axs[4].set_xlabel("Days")
plt.subplots_adjust(hspace=0.5)
plt.show()Output:
Você pode ver que, ao longo de vários dias, algumas das propriedades do qubit podem mudar consideravelmente. Isso destaca a importância de ter informações atualizadas sobre o status da QPU para poder selecionar os qubits de melhor desempenho para um experimento.
Etapa 2: Otimizar o problema para execução em hardware quântico
Neste tutorial, não é feita nenhuma otimização dos circuitos ou operadores.
Passo 3: Execute usando Qiskit primitives
Executar um circuito quântico com seleção padrão de qubits
Como resultado de referência de desempenho, executaremos um circuito quântico em uma QPU usando os qubits padrão, que são os qubits selecionados com as propriedades de backend solicitadas. Usaremos o site optimization_level = 3. Essa configuração inclui a otimização de transpilação mais avançada e usa propriedades de destino (como erros de operação) para selecionar os qubits de melhor desempenho para execução.
pm = generate_preset_pass_manager(target=backend.target, optimization_level=3)
isa_circuits = pm.run(circuits)
initial_qubits = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in isa_circuits
]Execute um circuito quântico com seleção de qubits em tempo real
Nesta seção, investigaremos a importância de ter informações atualizadas sobre as propriedades de qubit da QPU para obter resultados ideais. Primeiro, realizaremos um conjunto completo de experimentos de caracterização de QPU ( , , SPAM, RB de um e dois qubits), que poderemos usar para atualizar as propriedades de backend. Isso permite que o gerenciador de passagens selecione qubits para a execução com base em novas informações sobre a QPU, possivelmente melhorando o desempenho da execução. Em segundo lugar, executamos o circuito do par de Bell e comparamos a fidelidade obtida após a seleção dos qubits com a atualização das propriedades da QPU com a fidelidade obtida anteriormente quando usamos as propriedades relatadas padrão para a seleção de qubits.
Observe que alguns dos experimentos de caracterização podem falhar quando a rotina de ajuste não consegue ajustar uma curva aos dados medidos. Se você vir avisos provenientes desses experimentos, inspecione-os para entender qual caracterização falhou em quais qubits e tente ajustar os parâmetros do experimento (como os tempos para , , ou o número de comprimentos dos experimentos RB).
# Prepare characterization experiments
batches = [t1_exp, t2_exp, readout_exp, singleq_rb_exp, twoq_rb_exp_batched]
batches_exp = BatchExperiment(batches, backend) # , analysis=None)
run_options = {"shots": 1e3, "dynamic": False}
with Session(backend=backend) as session:
sampler = SamplerV2(mode=session)
# Run characterization experiments
batches_exp_data = batches_exp.run(
sampler=sampler, **run_options
).block_for_results()
EPG_sx_result_list = batches_exp_data.analysis_results("EPG_sx")
EPG_sx_result_q_indices = [
result.device_components.index for result in EPG_sx_result_list
]
EPG_x_result_list = batches_exp_data.analysis_results("EPG_x")
EPG_x_result_q_indices = [
result.device_components.index for result in EPG_x_result_list
]
T1_result_list = batches_exp_data.analysis_results("T1")
T1_result_q_indices = [
result.device_components.index for result in T1_result_list
]
T2_result_list = batches_exp_data.analysis_results("T2")
T2_result_q_indices = [
result.device_components.index for result in T2_result_list
]
Readout_result_list = batches_exp_data.analysis_results(
"Local Readout Mitigator"
)
EPG_2q_result_list = batches_exp_data.analysis_results(
f"EPG_{instruction_2q_name}"
)
# Update target properties
target = copy.deepcopy(backend.target)
for i in range(target.num_qubits - 1):
qarg = (i,)
if qarg in EPG_sx_result_q_indices:
target.update_instruction_properties(
instruction="sx",
qargs=qarg,
properties=InstructionProperties(
error=EPG_sx_result_list[i].value.nominal_value
),
)
if qarg in EPG_x_result_q_indices:
target.update_instruction_properties(
instruction="x",
qargs=qarg,
properties=InstructionProperties(
error=EPG_x_result_list[i].value.nominal_value
),
)
err_mat = Readout_result_list.value.assignment_matrix(i)
readout_assignment_error = (
err_mat[0, 1] + err_mat[1, 0]
) / 2 # average readout error
target.update_instruction_properties(
instruction="measure",
qargs=qarg,
properties=InstructionProperties(error=readout_assignment_error),
)
if qarg in T1_result_q_indices:
target.qubit_properties[i].t1 = T1_result_list[
i
].value.nominal_value
if qarg in T2_result_q_indices:
target.qubit_properties[i].t2 = T2_result_list[
i
].value.nominal_value
for pair_idx, pair in enumerate(one_dir_coupling_map):
qarg = tuple(pair)
try:
target.update_instruction_properties(
instruction=instruction_2q_name,
qargs=qarg,
properties=InstructionProperties(
error=EPG_2q_result_list[pair_idx].value.nominal_value
),
)
except:
target.update_instruction_properties(
instruction=instruction_2q_name,
qargs=qarg[::-1],
properties=InstructionProperties(
error=EPG_2q_result_list[pair_idx].value.nominal_value
),
)
# transpile circuits to updated target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
isa_circuit_updated = pm.run(circuits)
updated_qubits = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in isa_circuit_updated
]
n_trials = 3 # run multiple trials to see variations
# interleave circuits
interleaved_circuits = []
for original_circuit, updated_circuit in zip(
isa_circuits, isa_circuit_updated
):
interleaved_circuits.append(original_circuit)
interleaved_circuits.append(updated_circuit)
# Run circuits
# Set simple error suppression/mitigation options
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = "XY4"
job_interleaved = sampler.run(interleaved_circuits * n_trials)Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado
Por fim, vamos comparar a fidelidade do estado de Bell obtido nas duas configurações diferentes:
originalou seja, com os qubits padrão escolhidos pelo transpilador com base nas propriedades relatadas do backend.updatedou seja, com os qubits escolhidos com base nas propriedades atualizadas do backend após a execução dos experimentos de caracterização.
results = job_interleaved.result()
all_fidelity_list, all_fidelity_updated_list = [], []
for exp_idx in range(n_trials):
fidelity_list, fidelity_updated_list = [], []
for idx, num_qubits in enumerate(num_qubits_list):
pub_result_original = results[
2 * exp_idx * len(num_qubits_list) + 2 * idx
]
pub_result_updated = results[
2 * exp_idx * len(num_qubits_list) + 2 * idx + 1
]
fid = hellinger_fidelity(
ideal_dist, pub_result_original.data.c.get_counts()
)
fidelity_list.append(fid)
fid_up = hellinger_fidelity(
ideal_dist, pub_result_updated.data.c.get_counts()
)
fidelity_updated_list.append(fid_up)
all_fidelity_list.append(fidelity_list)
all_fidelity_updated_list.append(fidelity_updated_list)plt.figure(figsize=(8, 6))
plt.errorbar(
num_qubits_list,
np.mean(all_fidelity_list, axis=0),
yerr=np.std(all_fidelity_list, axis=0),
fmt="o-.",
label="original",
color="b",
)
# plt.plot(num_qubits_list, fidelity_list, '-.')
plt.errorbar(
num_qubits_list,
np.mean(all_fidelity_updated_list, axis=0),
yerr=np.std(all_fidelity_updated_list, axis=0),
fmt="o-.",
label="updated",
color="r",
)
# plt.plot(num_qubits_list, fidelity_updated_list, '-.')
plt.xlabel("Chain length")
plt.xticks(num_qubits_list)
plt.ylabel("Fidelity")
plt.title("Bell pair fidelity at the edge of N-qubits chain")
plt.legend()
plt.grid(
alpha=0.2,
linestyle="-.",
)
plt.show()Output:
Nem todas as execuções mostrarão melhorias no desempenho devido à caracterização em tempo real e, com o aumento do comprimento da cadeia e, portanto, menos liberdade para escolher qubits físicos, a importância das informações atualizadas do dispositivo se torna menos substancial. No entanto, é uma boa prática coletar novos dados sobre as propriedades do dispositivo para entender seu desempenho. Ocasionalmente, sistemas transitórios de dois níveis podem afetar o desempenho de alguns dos qubits. Os dados em tempo real podem nos informar quando esses eventos estão ocorrendo e nos ajudar a evitar falhas experimentais nesses casos.
Tente aplicar esse método em suas execuções e determine o quanto você se beneficia! Você também pode experimentar e ver quantas melhorias obtém com diferentes backends.
Pesquisa tutorial
Responda a esta breve pesquisa para fornecer feedback sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e a experiência do usuário.