주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

시공간 코드를 이용한 낮은 오버헤드 오류 검출

이 튜토리얼에서는 Clifford 회로에서 샘플링하고, 회로 전체에 시공간 Pauli 검사를 적용하여 측정된 분포의 충실도를 높이기 위해 qiskit-paulice 패키지를 사용하는 워크플로를 구현합니다. 시공간 Pauli 검사는 코히어런트 Pauli 검사(CPC)와 비슷하게, Clifford "페이로드" 회로를 보조 큐비트와 얽히게 하여 회로의 특정 불변량을 "검사"합니다. 보조 큐비트를 측정하면 신드롬이 나오며, 이는 회로를 실행하는 동안 논리 오류가 검출되었는지 판단하는 데 사용됩니다. 오류가 검출되지 않은 샘플만 사후 선택하면 사후 선택률이 낮아지는 대신 샘플링된 분포의 충실도를 높일 수 있습니다. CPC와 시공간 검사의 핵심 차이는 CPC 신드롬을 측정하려면 시간적으로 국소화된 높은 무게의 연산자를 측정해야 한다는 점입니다. heavy hex처럼 연결성이 제한된 큐비트 토폴로지에서는 이러한 검사를 구현하려면 SWAP 게이트가 많이 필요하여 회로가 너무 깊어져 실제로 실행하기 어려운 경우가 많습니다. Pauli 검사를 시공간 코드로 구현하면 페이로드 회로 전체에 검사를 분산할 수 있어, 회로 실행 중 논리 오류를 검출하는 데 효과적이면서도 하드웨어 효율적인 큐비트 인코딩이 가능합니다. qiskit-paulice 패키지의 주된 역할은 오류 검출 효과를 최대화하고 큐비트 오버헤드를 최소화하는 회로 내 위치에 유효한 시공간 Pauli 검사를 찾아 삽입하는 것입니다.

이 튜토리얼은 시공간 검사를 사용하여 양자 회로에서 샘플링한 분포의 충실도를 높이는 워크플로를 구현합니다. Clifford 회로를 사용하므로 회로 안정자(stabilizer)의 평균 기댓값으로부터 충실도를 간단하게 추정할 수 있습니다. 다음 단계를 따릅니다.

  1. 회로 만들기: 검사를 시각화하는 데 사용할 얕은 1D 벽돌 쌓기(brickwork) 무작위 Clifford 회로입니다.

  2. 검사 대상이 될 큐비트 식별하기: 회로를 ibm_boston Heron QPU의 물리 큐비트에 매핑하고 인접한 보조 큐비트가 있는 큐비트를 식별합니다.

  3. 회로를 백엔드에 맞게 트랜스파일하기: 큐비트 레이아웃을 선택하고 회로 게이트를 ibm_boston의 네이티브 게이트 집합으로 변환합니다.

  4. 백엔드 데이터로 노이즈 모델 만들기: 백엔드 벤치마크 데이터를 바탕으로 대략적인 노이즈 모델을 생성합니다. 이 노이즈 데이터는 추가되는 각 검사의 효과를 평가하는 데 사용됩니다.

  5. Pauli 검사 시각화하기: 얕은 회로에 Pauli 검사를 몇 개 추가하여 검사가 어떻게 추가되는지 시각화합니다.

  6. 측정할 안정자 결정하기: 게이트 노이즈의 영향을 키우기 위해 회로의 깊이를 늘립니다. 더 깊은 회로의 안정자 중 일부를 무작위로 샘플링합니다.

  7. 전체 상태에 적합한 검사 찾기: 회로가 준비하는 상태의 안정자에 대한 검사 집합 하나를 찾습니다. 이러한 검사는 이후 무엇을 측정하든 유효하므로, 같은 검사된 회로가 샘플링된 모든 안정자에 사용됩니다.

  8. 노이즈가 있는 회로에서 샘플링하기: samplomatic으로 샘플링된 각 안정자의 기저에서 기본 회로와 검사된 회로를 측정하고, 검사를 찾는 데 사용한 것과 같은 탈분극 노이즈를 쓰는 노이즈 시뮬레이터에서 Executor 프리미티브로 샘플링합니다.

  9. 검출된 오류가 없는 검사된 회로 샘플 사후 선택하기: 검사를 하나라도 통과하지 못한 샘플을 제거합니다. 노이즈가 있는 기본 회로와 검사를 포함한 회로의 기댓값을 계산합니다.

  10. 원래 상태와 사후 선택된 노이즈 상태의 충실도 비교하기: 검출된 오류가 없는 샘플만 사후 선택하면 노이즈 상태의 추정 충실도가 향상됨을 보여 줍니다.

1. 회로 만들기​

먼저 얽힘 게이트를 벽돌 쌓기 연결 구조로 배치한 얕은 Clifford 회로를 만듭니다. 이 작은 회로로 검사가 회로에 어떻게 추가되는지 시각화합니다.

# 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)

Output of the previous code cell

2. 검사 대상이 될 큐비트 식별하기​

다음으로 Heron r3 QPU인 ibm_boston에서 1D 큐비트 레이아웃을 선택합니다. 커플링 그래프를 대각선으로 지그재그로 지나면서 사용 가능한 보조 큐비트 수를 최대화하는 레이아웃을 선택합니다. 사용 가능한 보조 큐비트를 찾기 위해 대상/보조 큐비트 쌍의 목록을 반환하는 get_check_qubits 함수를 사용합니다. target_qubits[i]에 대한 Pauli 검사는 ancilla_qubits[i]를 사용하여 구현됩니다.

아래 커플링 그래프에서 초록색 큐비트는 페이로드 큐비트이고 주황색 큐비트는 검사를 구현하는 데 사용할 보조 큐비트입니다. 큐비트 68, 69, 89, 91, 111, 113, 133에는 인접한 보조 큐비트가 있으므로, 이 큐비트들(가상 큐비트 0, 1, 3, 5, 7, 9, 11에 해당)을 검사를 구현하기 위한 대상 큐비트로 사용합니다. 페이로드에서 큐비트 134는 큐비트 133에 인접하지만 보조 큐비트로 강조 표시되지 않았다는 점에 유의하세요. 이는 133이 이미 (임의로) 보조 큐비트 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]

Output of the previous code cell

3. 회로를 백엔드에 맞게 트랜스파일하기​

백엔드가 이미 선택되었으므로 가상 회로를 ISA 회로로 바로 트랜스파일할 수 있습니다. 여기서는 큐비트 레이아웃을 설정하고 게이트를 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)

Output of the previous code cell

4. 백엔드 데이터로 노이즈 모델 만들기​

이제 회로가 QPU에 어떻게 배치될지 알았으므로, 백엔드의 게이트 노이즈와 판독 노이즈가 회로 실행에 어떤 영향을 미칠지 모델링할 수 있습니다. 노이즈 모델은 회로에서 Pauli 검사를 배치할 최적의 위치를 결정하는 데 사용됩니다. 검사는 최대한 많은 오류를 포착할 수 있는 위치에 배치하려 합니다. 더 정확한 노이즈 모델은 검사의 검출 능력을 높일 수 있지만, QPU를 샘플링하여 노이즈 모델을 학습할 필요는 보통 없습니다. 여기서는 qiskit-ibm-runtime의 백엔드 벤치마크 데이터만으로 균일한 탈분극 게이트 노이즈 모델을 추정하고, 함께 보고되는 판독 오류는 제외합니다. 7단계에서 찾는 검사는 보조 큐비트만 읽으므로 페이로드 판독 오류는 검사에 보이지 않으며, 10단계의 사후 선택 충실도만 낮출 뿐입니다. 게이트 및 판독 노이즈를 지정하는 방법은 qiskit_paulice.NoiseModel 문서를 참조하세요. 학습된 Pauli-Lindblad 노이즈 모델을 사용하려면 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. Pauli 검사 시각화하기​

이제 회로에 검사를 추가하고 어떻게 보이는지 확인해 봅시다. qiskit_paulice.add_pauli_checks 함수를 사용하여 유효한 검사의 좋은 집합을 찾을 수 있습니다. 입력으로 Clifford 페이로드 회로, 검사를 추가할 페이로드 큐비트 목록(대상 큐비트), 이전 단계에서 만든 노이즈 모델을 제공합니다. 이 예제에서는 사용 가능한 보조 큐비트가 있는 7개의 대상 큐비트 모두에 검사를 추가합니다. 검사는 target_qubits에 나타나는 순서대로 큐비트에 추가되므로 검사된 회로의 최종 레이아웃을 쉽게 파악할 수 있습니다. 즉 final_layout = layout + ancilla_qubits입니다. 사용자가 더 적은 개수(i)의 검사가 있는 출력 회로 중 하나를 실행하려는 경우 최종 레이아웃은 final_layout = layout + ancilla_qubits[:i+1]이 됩니다.

회로 시각화를 보면 지정한 대상/보조 큐비트 쌍을 사용하여 검사가 올바르게 구현되었음을 알 수 있습니다.

qiskit_paulice.add_pauli_checks에 대한 자세한 설명입니다.

  • 첫 번째 입력은 Clifford 회로여야 합니다. 여기서는 측정이 없고 stabilizers="all"을 전달하므로, 회로가 준비하는 상태의 안정자에 대한 검사를 찾습니다. 검사의 시공간 Pauli 곱이 그러한 안정자를 측정하고, 신드롬이 그 보조 큐비트 비트 하나뿐이며, 이후 페이로드를 어떤 기저에서 측정하더라도 유효하다면 그 검사는 유효합니다. 샘플링된 각 안정자를 서로 다른 기저에서 측정하므로 충실도 추정에는 이러한 검사가 필요합니다. (stabilizers를 생략하면 대신 회로의 최종 측정에 대한 검사를 찾습니다.)
  • 두 번째 위치 인자는 대상 큐비트 인덱스의 목록입니다. 각 대상 큐비트는 보조 큐비트와 얽힘 게이트를 공유하여 검사를 형성합니다. 검사는 이 인자에 주어진 순서대로 찾아서 회로에 추가됩니다.
  • 마지막 위치 인자는 실행 중 회로에 영향을 미칠 것으로 예상되는 노이즈를 나타내는 노이즈 모델입니다. 이 모델은 주어진 검사의 효과를 평가하는 데 사용됩니다.
  • ancilla_qubits: 입력 회로에 레이아웃이 있는 경우, 이 키워드 인자는 add_pauli_checks가 새로 추가되는 보조 큐비트에 물리 큐비트를 어떻게 할당할지 알려 줍니다. ancilla_qubits는 target_qubits(두 번째 위치 인자)와 길이가 같아야 하며, ancilla_qubits[i]는 target_qubits[i]에 대한 검사를 구현하는 데 사용됩니다.
  • cost: 검사를 평가하는 데 사용하는 비용 함수입니다. gamma 또는 LER(논리 오류율)일 수 있으며, 모두 노이즈 채널 중 검사로 검출되지 않은 부분이 얼마나 되는지를 나타내는 방식입니다. stabilizers="all"에서는 오류가 준비된 상태를 바꾸면 논리 오류로 간주되며, 이는 충실도가 측정하는 바로 그것입니다.
  • method: 유효한 검사를 찾는 데 사용하는 방법입니다. 대상 큐비트의 지지집합(support, 와이어)의 공간 윈도 부분집합을 많이 무작위로 샘플링하는 windowed를 사용합니다. 다른 유효한 값은 genetic과 windowed_genetic입니다.
  • add_pauli_checks의 출력에는 검사가 없는 회로부터 모든 대상 큐비트에 검사가 있는 회로(유효한 검사가 발견된다고 가정)까지, 검사 개수가 점점 늘어나는 회로들이 포함됩니다.

좋은 시공간 Pauli 검사를 찾는 방법에 대한 자세한 내용은 보충 정보의 II-IV절을 참조하세요.

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:

Output of the previous code cell

6. 측정할 안정자 결정하기​

오류 검출 성능을 시험하기 위해, 회로가 이상적으로 준비하는 안정자 상태 ∣ψ⟩=U∣0⟩⊗n|\psi\rangle = U|0\rangle^{\otimes n}와 하드웨어가 실제로 출력하는 노이즈 상태 ρ\rho 사이의 충실도를 추정합니다. 순수 안정자 상태 ∣ψ⟩|\psi\rangle로의 사영 연산자는 안정자 군 S\mathcal{S}의 2n2^n개 원소에 대한 균일 평균으로 정의됩니다.

∣ψ⟩⟨ψ∣=12n∑G∈SG|\psi\rangle\langle\psi| = \frac{1}{2^n}\sum_{G \in \mathcal{S}} G

이를 충실도 식에 대입하면, 노이즈 상태 ρ\rho의 충실도는 ρ\rho에 대한 모든 안정자 G∈SG \in \mathcal{S}의 평균 기댓값과 같음을 알 수 있습니다.

F=Tr(ρ∣ψ⟩⟨ψ∣)=Tr(12n∑G∈SρG)=12n∑G∈STr(ρG)=12n∑G∈S⟨G⟩ρF = \mathrm{Tr}(\rho|\psi\rangle\langle\psi|) = \mathrm{Tr}({\frac{1}{2^n}\sum_{G \in \mathcal{S}} \rho G}) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \mathrm{Tr}(\rho G) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \langle G \rangle_\rho

더 큰 문제에서는 2n2^n개의 안정자를 모두 측정(또는 나열조차)하는 것이 불가능하므로, 충실도는 안정자의 무작위 표본을 측정하여 추정하는 경우가 많습니다. S\mathcal{S}에서 균일하게 무작위로 MM개의 안정자 G1,…,GMG_1, \ldots, G_M을 샘플링하면 충실도의 비편향 추정량 F^M\hat F_M을 얻습니다.

F^M=1M∑i=1M⟨Gi⟩ρ\hat F_M = \frac{1}{M} \sum_{i=1}^M \langle G_i \rangle_\rho 이 실험에서는 게이트 노이즈의 영향을 키우기 위해 회로를 훨씬 더 깊게 만들고, 인접한 보조 큐비트가 있는 모든 큐비트에 검사를 추가합니다. 충실도를 추정하기 위해 안정자를 샘플링하는 더 정교한 방법도 있지만, 이 예제에서는 균일한 확률로 100100개의 안정자를 샘플링합니다.

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. 전체 상태에 적합한 검사 찾기​

5단계와 똑같이 깊은 회로에 대한 검사 집합 하나를 찾습니다. 검사는 보조 큐비트만 읽으므로 같은 검사된 회로가 샘플링된 모든 안정자에 사용됩니다. 반환된 회로는 검사 보조 큐비트만 측정하므로 페이로드 측정은 다음 단계에서 추가합니다.

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. 노이즈가 있는 회로에서 샘플링하기​

샘플링된 각 안정자에 대해 기본 페이로드부터 7개의 검사가 모두 있는 회로까지 모든 변형을 샘플링합니다. 검사가 0개인 변형은 같은 측정을 가진 기본 페이로드이므로 노이즈 기준으로 사용됩니다. 모든 안정자의 기저 회전을 모든 변형의 복사본에 덧붙이는 대신, samplomatic이 샘플링 시점에 측정 기저를 선택하도록 합니다. 각 변형에 대해 다음을 수행합니다.

  • 페이로드를 측정하고, samplomatic의 박싱 패스 매니저로 최종 측정을 박스로 묶습니다. 이 박스에는 ChangeBasis 주석이 달려 있으므로 build는 기저 변환 게이트가 매개변수화된 템플릿 회로와, 요청된 기저에 대한 매개변수를 계산하는 samplex를 반환합니다.

  • 템플릿과 samplex를 QuantumProgram에 추가합니다. samplex는 박스 안의 큐비트마다 물리적 순서가 오름차순인 측정 기저 하나를 I=0I=0, Z=1Z=1, X=2X=2, Y=3Y=3으로 인코딩하여 받습니다. 길이가 100인 선행 축을 가진 기저 배열을 전달하면 하나의 항목이 100개의 안정자를 모두 측정합니다. 페이로드는 각 안정자의 기저에서, 보조 큐비트는 ZZ에서 측정합니다. 안정자의 지지집합 밖에 있는 큐비트도 ZZ에서 측정되며, 안정자의 기댓값을 계산할 때 그 비트는 무시됩니다.

qiskit-ibm-runtime의 Executor 프리미티브는 전체 프로그램을 하나의 작업으로 실행합니다. QPU 대신 AerSimulator를 주면 해당 시뮬레이터에서 로컬로 실행되므로, 검사를 찾는 데 사용한 것과 같은 탈분극 게이트 노이즈를 가진 시뮬레이터를 전달하고, 모든 회로가 Clifford이므로 안정자 방법을 사용합니다. Qiskit Aer는 이 시뮬레이터를 만드는 데만 필요합니다. 로컬 모드는 Executor 옵션에서 시드를 가져오지만 noise_model은 무시하므로 노이즈는 시뮬레이터 자체에 설정해야 합니다. 대신 QPU 백엔드를 전달하면 같은 프로그램이 하드웨어에서 실행됩니다. 시뮬레이션이 완료되면 데이터를 시각화하기 전에 사후 선택률과 기댓값을 계산하여 오류 검출이 얼마나 잘 작동했는지 감을 잡아 봅니다.

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. 검사된 회로 샘플 사후 선택하기​

이 패키지에서 검사는 보조 큐비트 하나와 대상 큐비트 하나 사이의 얽힘 게이트로 구현됩니다. 각 보조 큐비트는 ∣0⟩|0\rangle에서 시작하므로 ZancZ_\text{anc}가 그 입력 상태를 안정화합니다. 보조 큐비트의 시작 지점부터 검사된 회로 전체를 지나 ZancZ_\text{anc}를 순방향으로 전파하면 ZancZ_\text{anc}에 준비된 상태의 안정자를 곱한 것이 나오므로, 보조 큐비트 비트가 00이면 검사를 통과합니다. 모든 검사를 통과하면 샘플을 유지합니다.

검사된 회로는 보조 큐비트를 checks_c 레지스터에 측정하므로 그 레지스터의 모든 비트가 0이면 샘플을 유지합니다. 일반적으로 add_pauli_checks가 출력하는 CheckedCircuit 인스턴스의 get_postselection_method 메서드는 샷의 비트열을 모든 검사의 신드롬으로 한꺼번에 매핑합니다. 준비된 상태의 안정자에 대해 찾은 검사에서는 각 신드롬이 그 보조 큐비트 비트일 뿐이므로 레지스터를 직접 읽습니다.

차트를 보면 검사를 더 추가할수록 사후 선택률이 내려감을 알 수 있습니다. 오류 검출의 목표는 가능한 한 많은 오류를 잡는 것이지만, 사후 선택률이 낮아지면 목표 정확도를 달성하는 데 더 많은 샷이 필요할 수 있습니다. 사후 선택률이 너무 낮아지면 논리 샘플 하나를 모으는 데 필요한 샷 수가 감당할 수 없을 만큼 커질 수 있습니다. 이 예제에서는 사후 선택률이 수렴하는 것으로 보이며, 이는 추가 검사가 더하는 검출 능력이 점점 줄어든다는 것을 의미합니다. Pauli 검사를 더 추가하면 일반적으로 오류 검출 능력이 높아지지만, 회로 깊이가 늘어나고 샘플링 비용이 증가하는 대가가 따릅니다.

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()

Output of the previous code cell

10. 원래 상태와 사후 선택된 노이즈 상태의 충실도 비교하기​

검출된 오류가 없는 샘플만 사후 선택하면 모든 안정자의 기댓값이 크게 개선되며, 따라서 안정자 상태의 추정 충실도도 향상됨을 알 수 있습니다. 사후 선택된 값은 모든 검사를 통과한 샘플만으로 계산했지만 기댓값이 훨씬 더 정확하고 샘플링된 분산도 더 낮습니다. 또한 위의 사후 선택률이 실제로는 노이즈가 있는 회로의 충실도와 비슷한 값으로 수렴하고 있었음을 이제 볼 수 있습니다. 검사가 회로 실행 중 실제로 발생하는 대부분의 오류를 잡아내고 있었기 때문에 사후 선택률이 수렴한 것입니다.

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

Output of the previous code cell

Output of the previous code cell

하나의 검사 집합이 모든 안정자에 사용되므로 살펴볼 gamma 곡선은 하나뿐입니다. 각 검사가 확정된 뒤의 비용 함수 값입니다. 다른 물리 큐비트 집합에서의 다른 문제나 불완전한 노이즈 모델(이 튜토리얼의 노이즈 모델은 샘플에 영향을 준 노이즈를 정확히 모델링했다는 점을 기억하세요)의 경우, 대상 큐비트에 검사를 추가하는 순서가 곡선이 수렴하는 방식에 더 뚜렷한 영향을 줄 수 있습니다.

이 gamma 곡선이 1.0으로 수렴한다는 것은 검사로 검출되지 않은 모델링된 오류가 거의 없으므로 추가 검사로 검출할 수 있는 오류가 많지 않다는 뜻입니다. 이는 사후 선택률의 수렴이 회로에 검출되지 않은 오류가 부족하기 때문이라는 앞선 관찰을 뒷받침합니다.

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()

Output of the previous code cell