Detecção de erros de baixa sobrecarga usando códigos de espaço-tempo
Neste tutorial, vamos implementar um fluxo de trabalho para amostrar de um circuito de Clifford e usar o pacote qiskit-paulice para aumentar a fidelidade da distribuição medida aplicando verificações de Pauli de espaço-tempo ao longo do circuito. As verificações de Pauli de espaço-tempo são semelhantes às verificações de Pauli coerentes (CPC), no sentido de que um circuito "carga útil" de Clifford é emaranhado com alguns qubits auxiliares para "verificar" certos invariantes no circuito. A medição dos auxiliares produz uma síndrome que é usada para determinar se um erro lógico foi detectado durante a execução do circuito. Pós-selecionar apenas as amostras em que nenhum erro foi detectado pode melhorar a fidelidade das distribuições amostradas ao custo de reduzir as taxas de pós-seleção. A principal diferença entre as CPC e as verificações de espaço-tempo é que medir síndromes de CPC exige medir operadores de alto peso localizados no tempo. Em topologias de qubits com conectividade limitada, como a heavy hex, implementar essas verificações exige muitas portas SWAP, muitas vezes tornando o circuito profundo demais para ser executado na prática. Implementar as verificações de Pauli como códigos de espaço-tempo permite distribuí-las ao longo do circuito de carga útil, possibilitando uma codificação de qubits eficiente para o hardware que continua eficaz na detecção de erros lógicos durante a execução do circuito. O papel principal do pacote qiskit-paulice é encontrar e inserir verificações de Pauli de espaço-tempo válidas em posições do circuito que ao mesmo tempo maximizem a eficácia da detecção de erros e minimizem a sobrecarga de qubits.
Este tutorial implementa um fluxo de trabalho para aumentar a fidelidade da distribuição amostrada de um circuito quântico usando verificações de espaço-tempo. Como usaremos um circuito de Clifford, podemos estimar a fidelidade de forma direta a partir dos valores esperados médios dos estabilizadores do circuito. Seguiremos estas etapas:
-
Criar um circuito: circuito de Clifford aleatório raso em padrão de tijolos 1D, usado para visualizar as verificações.
-
Identificar qubits como alvos das verificações: mapear o circuito para qubits físicos na QPU Heron
ibm_bostone identificar qubits com auxiliares vizinhos. -
Transpilar o circuito para o backend: selecionar um layout de qubits e traduzir as portas do circuito para o conjunto de portas nativo do
ibm_boston. -
Criar um modelo de ruído a partir dos dados do backend: gerar um modelo de ruído aproximado com base nos dados de benchmark do backend. Esses dados de ruído serão usados para pontuar a eficácia de cada verificação adicional.
-
Visualizar as verificações de Pauli: adicionar algumas verificações de Pauli ao circuito raso, para que possamos visualizar como as verificações são adicionadas.
-
Determinar os estabilizadores a medir: aumentar a profundidade do circuito para aumentar o impacto do ruído das portas. Amostrar aleatoriamente um subconjunto de estabilizadores do circuito mais profundo.
-
Encontrar boas verificações para o estado inteiro: encontrar um conjunto de verificações para os estabilizadores do estado que o circuito prepara. Essas verificações são válidas independentemente do que for medido depois, então o mesmo circuito verificado serve para todos os estabilizadores amostrados.
-
Amostrar do circuito ruidoso: medir os circuitos simples e verificados na base de cada estabilizador amostrado com o samplomatic e amostrá-los com a primitiva
Executorem um simulador ruidoso que usa o mesmo ruído despolarizante que foi usado para encontrar as verificações. -
Pós-selecionar as amostras do circuito verificado sem erros detectados: remover as amostras que falham em pelo menos uma verificação. Calcular os valores esperados do circuito ruidoso simples e do circuito que contém as verificações.
-
Comparar as fidelidades dos estados ruidosos original e pós-selecionado: mostrar que pós-selecionar apenas as amostras sem erros detectados melhora a fidelidade estimada do estado ruidoso.
1. Crie o circuito
Primeiro, vamos criar um circuito de Clifford raso com suas portas emaranhadoras dispostas em uma conectividade de padrão de tijolos. Usaremos esse pequeno circuito para visualizar como as verificações são adicionadas ao circuito.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_paulice": "qiskit-paulice", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit
def random_clifford_circuit(
num_qubits: int, depth: int, rng: np.random.Generator
) -> QuantumCircuit:
"""Brickwork random Clifford on `num_qubits`, with `depth` CZ layers."""
qc = QuantumCircuit(num_qubits)
qc.h(range(num_qubits))
for d in range(depth):
for i in range(d % 2, num_qubits - 1, 2):
qc.cz(i, i + 1)
for q in range(num_qubits):
if rng.integers(0, 2):
qc.sx(q)
if rng.integers(0, 2):
qc.s(q)
if rng.integers(0, 2):
qc.sx(q)
return qc
num_qubits = 12
depth = 4
seed = 1764
rng = np.random.default_rng(seed)
np.random.seed(seed)
circuit = random_clifford_circuit(num_qubits, depth, rng)
circuit.draw("mpl", fold=-1, scale=0.6)

2. Identifique qubits como alvos das verificações
Em seguida, escolhemos um layout de qubits 1D na QPU Heron r3, ibm_boston. Escolhemos um layout que percorre o grafo de acoplamento em zigue-zague na diagonal e maximiza o número de qubits auxiliares disponíveis. Para encontrar os auxiliares disponíveis, usamos a função get_check_qubits, que retorna listas de pares de qubits alvo/auxiliar. Uma verificação de Pauli em target_qubits[i] será implementada usando ancilla_qubits[i].
No grafo de acoplamento abaixo, os qubits verdes são os qubits da carga útil e os qubits laranja são os auxiliares que usaremos para implementar as verificações. Os qubits 68, 69, 89, 91, 111, 113 e 133 têm auxiliares adjacentes, então usaremos esses qubits (correspondentes aos qubits virtuais 0, 1, 3, 5, 7, 9 e 11) como qubits alvo para implementar as verificações. Observe que o qubit 134 é adjacente ao qubit 133 na carga útil, mas não está destacado como auxiliar. Isso acontece porque o 133 já foi escolhido (arbitrariamente) para implementar uma verificação com o auxiliar 132.
from qiskit.visualization import plot_coupling_map
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_paulice.layout import get_check_qubits
service = QiskitRuntimeService()
backend = service.backend("ibm_boston")
# Choose a layout and pair each available target qubit with a neighboring ancilla
layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
print(f"Target qubits: {target_qubits}")
print(f"Ancilla qubits: {ancilla_qubits}")
plot_coupling_map(
num_qubits=backend.num_qubits,
qubit_coordinates=getattr(
backend.configuration(), "qubit_coordinates", None
),
coupling_map=backend.configuration().coupling_map,
figsize=(12, 12),
qubit_color=[
"#4CAF50"
if i in set(layout)
else "#FF9800"
if i in set(ancilla_qubits)
else "#DDDDDD"
for i in backend.coupling_map.graph.node_indices()
],
qubit_size=220,
line_width=2,
font_size=90,
)
Target qubits: [68, 69, 89, 91, 111, 113, 133]
Ancilla qubits: [67, 70, 88, 92, 110, 114, 132]

3. Transpile o circuito para o backend
Como o backend já foi escolhido, podemos seguir em frente e transpilar nosso circuito virtual em um circuito ISA. Aqui, apenas definimos o layout de qubits e traduzimos as portas para o conjunto de portas nativo do ibm_boston.
from qiskit.transpiler import generate_preset_pass_manager
pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=layout
)
circuit_isa = pm.run(circuit)
circuit_isa.draw("mpl", fold=-1, scale=0.6)

4. Crie um modelo de ruído a partir dos dados do backend
Agora que sabemos como o circuito será disposto na QPU, podemos modelar como o ruído das portas e de leitura no backend afetará a execução do circuito. O modelo de ruído é usado para determinar os melhores lugares do circuito para colocar as verificações de Pauli. Queremos colocar as verificações em posições onde elas capturem a maior quantidade de erro. Embora um modelo de ruído mais preciso possa melhorar a capacidade de detecção das verificações, geralmente não é necessário aprender um modelo de ruído amostrando a QPU. Aqui inferimos um modelo uniforme de ruído despolarizante de portas usando apenas dados de benchmark do backend do qiskit-ibm-runtime, e descartamos o erro de leitura que ele também informa: as verificações que encontramos na etapa 7 leem apenas seu auxiliar, então os erros de leitura da carga útil são invisíveis para elas e só reduziriam a fidelidade pós-selecionada na etapa 10. Consulte a documentação de qiskit_paulice.NoiseModel para mais informações sobre como especificar o ruído de portas e de leitura. Para quem deseja usar um modelo de ruído de Pauli-Lindblad aprendido, consulte NoiseModel.from_pauli_lindblad_maps.
from dataclasses import replace
from qiskit_paulice.noise_models import NoiseModel
# Model the gate noise as a uniform depolarizing channel based on backend benchmark data
noise_model = replace(
NoiseModel.from_backend(backend, layout, uniform_gate_noise=True),
readout_noise=None,
)
print(noise_model)
NoiseModel(gate_noise=0.0015808382794757693, readout_noise=None, idling_noise=None)
5. Visualize as verificações de Pauli
Agora vamos adicionar algumas verificações ao circuito e ver como elas ficam. Podemos usar a função qiskit_paulice.add_pauli_checks para encontrar um bom conjunto de verificações válidas. Fornecemos como entrada o circuito de Clifford da carga útil, junto com uma lista de qubits da carga útil nos quais adicionar verificações (qubits alvo) e o modelo de ruído que criamos na etapa anterior. Neste exemplo, adicionaremos verificações a todos os 7 qubits alvo com auxiliares disponíveis. As verificações são adicionadas aos qubits na ordem em que aparecem em target_qubits, o que facilita raciocinar sobre o layout final do circuito verificado: final_layout = layout + ancilla_qubits. Se você decidir que prefere executar um dos circuitos de saída com menos (i) verificações, o layout final seria final_layout = layout + ancilla_qubits[:i+1].
A visualização do circuito mostra que as verificações foram implementadas corretamente usando os pares alvo/auxiliar especificados.
Detalhes sobre qiskit_paulice.add_pauli_checks:
- A primeira entrada deve ser um circuito de Clifford. Aqui ele não tem medições e passamos
stabilizers="all", então as verificações são encontradas para os estabilizadores do estado que o circuito prepara: uma verificação é válida se seu produto de Pauli de espaço-tempo mede um desses estabilizadores, sua síndrome é apenas o bit do seu auxiliar e ela continua válida em qualquer base em que a carga útil seja medida depois. Isso é o que a estimativa de fidelidade precisa, já que cada estabilizador amostrado é medido em uma base diferente. (Omitirstabilizersfaz com que sejam encontradas verificações para as medições terminais de um circuito.) - O segundo argumento posicional é uma lista de índices de qubits alvo. Cada qubit alvo compartilhará portas emaranhadoras com um qubit auxiliar para formar uma verificação. As verificações serão encontradas e adicionadas ao circuito na ordem em que são dadas nesse argumento.
- O último argumento posicional é um modelo de ruído que representa o ruído esperado a afetar o circuito durante a execução. Esse modelo é usado para avaliar a eficácia de uma dada verificação.
ancilla_qubits: se o circuito de entrada tiver um layout, esse kwarg instrui oadd_pauli_checkssobre como atribuir qubits físicos aos auxiliares recém-adicionados.ancilla_qubitsdeve ter o mesmo comprimento quetarget_qubits(2º argumento posicional), eancilla_qubits[i]deve ser usado para implementar as verificações emtarget_qubits[i].cost: a função de custo usada para avaliar as verificações. Pode sergammaouLER(taxa de erro lógico), que são formas de descrever quanto do canal de ruído permanece sem ser detectado pelas verificações. Comstabilizers="all", um erro conta como lógico se alterar o estado preparado, que é exatamente o que a fidelidade mede.method: o método usado para encontrar verificações válidas. Usamoswindowed, que amostra aleatoriamente muitos subconjuntos de janelas espaciais do suporte (fios) de um qubit alvo. Outros valores válidos sãogeneticewindowed_genetic.- A saída de
add_pauli_checkscontém circuitos com números crescentes de verificações, de um circuito sem verificações a um circuito com verificações em todos os qubits alvo (supondo que verificações válidas sejam encontradas).
Veja as Seções II-IV das informações suplementares para informações detalhadas sobre como encontrar boas verificações de Pauli de espaço-tempo
import matplotlib.pyplot as plt
from qiskit_paulice import add_pauli_checks
# Find checks on circuit
checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(f"Physical layout of payload+ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)
Physical layout of payload+ancillas: [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133, 67, 70, 88, 92, 110, 114, 132]
Checked circuit:

6. Determine os estabilizadores a medir
Para testar o desempenho da detecção de erros, vamos estimar a fidelidade do estado estabilizador, , que o circuito prepara idealmente, em relação ao estado ruidoso, , que o hardware realmente produz. O projetor sobre o estado estabilizador puro, , é definido como a média uniforme sobre os elementos do seu grupo estabilizador :
Substituindo isso na equação da fidelidade, vemos que a fidelidade do estado ruidoso, , é equivalente ao valor esperado médio de todos os estabilizadores, , em relação a :
Para problemas maiores, medir (ou até enumerar) todos os estabilizadores é inviável, então a fidelidade costuma ser estimada medindo uma amostra aleatória de estabilizadores. Amostrar estabilizadores uniformemente ao acaso de fornece um estimador não enviesado da fidelidade, ,
Neste experimento, tornaremos o circuito muito mais profundo para realçar o efeito do ruído das portas, e adicionaremos verificações a todos os qubits que tenham um qubit auxiliar vizinho. Embora existam formas mais sofisticadas de amostrar estabilizadores para estimar a fidelidade, neste exemplo amostraremos estabilizadores com probabilidade uniforme.
from qiskit.quantum_info import Clifford, Pauli, PauliList
num_stabilizers = 100
num_shots = 1_000
num_checks = len(target_qubits)
depth = 72
circuit = random_clifford_circuit(num_qubits, depth, rng)
# Create all 2^N stabilizers
stabilizers = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circuit).to_labels(mode="S")
):
stabilizers = stabilizers + stabilizers.compose(generator)
# Choose some random stabilizers from the set
keep = np.where(stabilizers.x.any(axis=1) | stabilizers.z.any(axis=1))[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = stabilizers[chosen]
depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of {circuit.num_qubits}-qubit circuit with two-qubit depth of {depth}: {{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)
Sampled 100 stabilizers of 12-qubit circuit with two-qubit depth of 72: {-YIIYZZZIZIYY, -XIXYIIZXYIZX, ...}
7. Encontre boas verificações para o estado inteiro
Encontramos um conjunto de verificações para o circuito profundo exatamente como na etapa 5. Como as verificações leem apenas seus auxiliares, o mesmo circuito verificado serve para todos os estabilizadores amostrados. Os circuitos retornados medem apenas os auxiliares das verificações, então adicionaremos as medições da carga útil na próxima etapa.
import time
# Transpile the deep payload without measurements and find one set of checks for the whole state
payload_isa = pm.run(circuit)
t0 = time.time()
checked = add_pauli_checks(
payload_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(
f"Added {len(checked[-1].check_qubits)} checks in {time.time() - t0:.0f}s."
)
print(
f"2-qubit depth increased from {payload_isa.depth(lambda x: len(x.qubits) == 2)} to "
f"{checked[-1].circuit.depth(lambda x: len(x.qubits) == 2)} when adding {num_checks} checks."
)
Added 7 checks in 3s.
2-qubit depth increased from 72 to 85 when adding 7 checks.
8. Amostre do circuito ruidoso
Amostramos todas as variantes, da carga útil simples ao circuito com todas as 7 verificações, para cada estabilizador amostrado. A variante com zero verificações é a carga útil simples com as mesmas medições, então ela serve como referência ruidosa. Em vez de anexar a rotação de base de cada estabilizador a uma cópia de cada variante, deixamos o samplomatic escolher a base de medição no momento da amostragem. Para cada variante:
-
Medimos a carga útil e agrupamos as medições terminais em uma caixa com o gerenciador de passes de boxing do samplomatic. A caixa carrega uma anotação
ChangeBasis, entãobuildretorna um circuito modelo cujas portas de mudança de base são parametrizadas, junto com um samplex que calcula os parâmetros para uma base solicitada. -
Adicionamos o modelo e o samplex a um
QuantumProgram. O samplex recebe uma base de medição por qubit na caixa, em ordem física crescente, codificada como , , , . Passar um array de bases com um eixo inicial de comprimento100faz com que um único item meça todos os100estabilizadores: a carga útil na base de cada estabilizador e os auxiliares em . Um qubit fora do suporte de um estabilizador também é medido em , e seu bit é ignorado quando o valor esperado do estabilizador é calculado.
A primitiva Executor do qiskit-ibm-runtime executa o programa inteiro em um único job. Ao receber um AerSimulator no lugar de uma QPU, ela executa localmente nesse simulador, então passamos a ela um com o mesmo ruído despolarizante de portas que usamos para encontrar as verificações, e usamos o método estabilizador, já que todos os circuitos são de Clifford. O Qiskit Aer é necessário apenas para construir esse simulador: o modo local pega a semente das opções do Executor, mas ignora o noise_model delas, então o ruído deve ser definido no próprio simulador. Passar um backend de QPU executaria o mesmo programa em hardware. Quando as simulações terminarem, calcularemos a taxa de pós-seleção e os valores esperados para ter uma ideia de quão bem a detecção de erros funcionou antes de visualizar os dados.
from qiskit import ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import depolarizing_error
from qiskit_ibm_runtime import Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from samplomatic.builders import build
from samplomatic.transpiler import generate_boxing_pass_manager
# Group the terminal measurements into a box whose measurement basis is chosen at sampling time
boxing_pm = generate_boxing_pass_manager(
enable_gates=False, measure_annotations="change_basis"
)
# bases[i, q]: basis in which physical qubit q is measured for stabilizer i (I=0, Z=1, X=2, Y=3)
bases = np.ones((len(stabilizers), backend.num_qubits), dtype=np.uint8)
bases[:, layout] = stabilizers.z + 2 * stabilizers.x
program = QuantumProgram(shots=num_shots)
for variant in checked:
circ = variant.circuit.copy()
circ.add_register(meas := ClassicalRegister(num_qubits, "meas"))
circ.measure(layout, meas)
template, samplex = build(boxing_pm.run(circ))
(basis_input,) = samplex.inputs().get_specs()
measured_qubits = sorted(layout + list(variant.check_qubits))
program.append_samplex_item(
template,
samplex=samplex,
samplex_arguments={basis_input.name: bases[:, measured_qubits]},
shape=(len(stabilizers),),
)
# Simulate the program with the same gate noise we used to find the checks
aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)
# outs[k][register]: bits of the variant with k checks, shape (stabilizers, shots, bits)
t0 = time.time()
outs = (
Executor(noisy_sim, options={"simulator": {"seed_simulator": seed}})
.run(program)
.result()
)
print(
f"Sampled {len(checked)} circuits for {len(stabilizers)} stabilizers in {time.time() - t0:.0f}s."
)
Sampled 8 circuits for 100 stabilizers in 77s.
9. Pós-selecione as amostras do circuito verificado
Neste pacote, uma verificação é implementada usando portas emaranhadoras entre um qubit auxiliar e um qubit alvo. Cada auxiliar começa em , então estabiliza seu estado de entrada. Propagar para frente, do início do auxiliar por todo o circuito verificado, produz vezes um estabilizador do estado preparado, então a verificação passa se o bit do auxiliar for . Uma amostra é mantida se todas as verificações passarem.
Os circuitos verificados medem os auxiliares no registrador checks_c, então uma amostra é mantida se todos os bits desse registrador forem 0. Em geral, o método get_postselection_method das instâncias de CheckedCircuit retornadas por add_pauli_checks mapeia a bitstring de um shot para a síndrome de todas as verificações de uma vez; para verificações encontradas para os estabilizadores do estado preparado, cada síndrome é apenas o bit do seu auxiliar, então lemos o registrador diretamente.
No gráfico, vemos que adicionar mais verificações faz a taxa de pós-seleção cair. Embora o objetivo da detecção de erros seja capturar o maior número possível de erros, à medida que a taxa de pós-seleção cai, mais shots podem ser necessários para atingir uma determinada precisão-alvo. Se a taxa de pós-seleção cair demais, o número de shots necessário para coletar uma amostra lógica pode se tornar proibitivo. Neste exemplo, a taxa de pós-seleção parece convergir, o que implica que as verificações adicionais contribuem com cada vez menos capacidade de detecção. Embora adicionar mais verificações de Pauli geralmente proporcione maior capacidade de detecção de erros, isso vem ao custo de maior profundidade do circuito e de maior custo de amostragem.
def keep_mask(out):
"""Per-shot True/False: every ancilla read 0, so every Pauli check passed."""
if "checks_c" not in out: # the bare payload has no checks
return np.ones(out["meas"].shape[:-1], dtype=bool)
return ~out["checks_c"].any(axis=-1)
# masks[k][i]: kept shots of stabilizer i measured on the variant with k checks
masks = [keep_mask(out) for out in outs]
psrs_arr = np.array([mask.mean(axis=-1) for mask in masks]).T
ks = np.arange(psrs_arr.shape[1])
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, psrs_arr.T, color="darkorange", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.median(psrs_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set(
xlabel="Checks committed",
ylabel="Post-selection rate",
ylim=(0, 1.05),
title=f"Per-stabilizer post-selection rate ({len(psrs_arr)} stabilizers)",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

10. Compare as fidelidades dos estados ruidosos original e pós-selecionado
Vemos que pós-selecionar apenas as amostras sem erros detectados melhora bastante o valor esperado de cada estabilizador e, portanto, melhora a fidelidade estimada do estado estabilizador. Embora os valores pós-selecionados tenham sido calculados usando apenas as amostras que passaram em todas as verificações, os valores esperados são muito mais precisos e a variância amostrada é menor. Também podemos ver agora que a taxa de pós-seleção acima estava, na verdade, convergindo para um valor semelhante à fidelidade do circuito ruidoso. A taxa de pós-seleção estava convergindo porque as verificações estavam capturando a grande maioria dos erros que realmente ocorriam durante a execução do circuito.
def expectation_values(out, mask):
"""EV of each stabilizer from the shots of ``out`` kept by ``mask``.
Bit q of ``meas`` is virtual qubit q, so a stabilizer's eigenvalue in a shot is the parity of
the bits in its support, times its sign.
"""
support = stabilizers.x | stabilizers.z
parity = (out["meas"] & support[:, None, :]).sum(axis=-1) % 2
signs = np.where(stabilizers.phase == 2, -1.0, 1.0)
eigenvalues = np.where(parity, -1.0, 1.0) * signs[:, None]
kept = mask.sum(axis=-1)
return np.where(
kept > 0,
(eigenvalues * mask).sum(axis=-1) / np.maximum(kept, 1),
np.nan,
)
evs_noisy = expectation_values(outs[0], masks[0])
evs_post = expectation_values(outs[-1], masks[-1])
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {np.nanmean(evs_noisy):.4f}\n"
f"postselected fidelity: {np.nanmean(evs_post):.4f}\n"
f"mean post-sel rate: {masks[-1].mean():.3f}\n"
)
evs_ideal = np.ones(len(stabilizers))
idx = np.arange(len(stabilizers))
def cum_mean_sem(ys):
valid = ~np.isnan(ys)
s = np.cumsum(np.where(valid, ys, 0.0))
s2 = np.cumsum(np.where(valid, ys**2, 0.0))
n = np.maximum(np.cumsum(valid).astype(float), 1)
mean = s / n
sem = np.sqrt(np.maximum(s2 / n - mean**2, 0) / n)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem
def strip(ax, ys, color, label):
m, s = np.nanmean(ys), np.nanstd(ys)
ax.axhspan(
m - s, m + s, color=color, alpha=0.15, label=f"{label} mean ± std"
)
ax.axhline(m, color=color, linewidth=1, linestyle="--")
fig, ax = plt.subplots(figsize=(8, 4))
ax.axhline(np.nanmean(evs_ideal), color="black", linewidth=1.5, label="ideal")
strip(ax, evs_noisy, "red", "noisy")
strip(ax, evs_post, "green", "post-sel")
ax.scatter(
idx, evs_noisy, color="red", s=22, alpha=0.7, zorder=3, label="noisy <G>"
)
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
zorder=3,
label="post-sel <G>",
)
ax.set(
xlabel="stabilizer index",
ylabel=r"$\langle G \rangle$",
ylim=(-0.1, 1.1),
title="Per-stabilizer EVs",
)
ax.legend(loc="lower left", ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
M = np.arange(1, len(stabilizers) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "post-sel"),
]:
cm, sem = cum_mean_sem(ys)
ax.plot(M, cm, color=color, linewidth=1.5, label=label)
ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)
ax.set(
xlabel="number of stabilizers averaged",
ylabel="running fidelity estimate",
title="Fidelity convergence vs. number of stabilizers",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ideal fidelity: 1.0
noisy fidelity: 0.5582
postselected fidelity: 0.9747
mean post-sel rate: 0.535

Como um único conjunto de verificações serve para todos os estabilizadores, há uma única curva de gamma para observar: o valor da função de custo após cada verificação ser confirmada. Para outros problemas, em um conjunto diferente de qubits físicos e com um modelo de ruído imperfeito (lembre-se de que o modelo de ruído deste tutorial modelou exatamente o ruído que afeta as amostras), a ordem em que as verificações são adicionadas aos qubits alvo pode ter um efeito mais pronunciado sobre como a curva converge.
Essa curva de gamma convergindo para 1.0 implica que verificações adicionais não conseguirão detectar muito mais erro, pois há muito pouco erro modelado que não seja coberto pelas verificações. Isso reforça nossa observação anterior de que a convergência da taxa de pós-seleção se deve à falta de erro descoberto no circuito.
costs = [variant.cost for variant in checked]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(costs)), costs, color="steelblue", marker="o")
ax.set(
xlabel="Checks committed",
ylabel="Gamma",
yscale="log",
title="Gamma of the noise left uncovered by the checks",
)
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()