기댓값 개선: 전파된 노이즈 흡수 (PNA)
이 튜토리얼에서는 Qiskit 생태계의 최신 도구를 활용하여 완전히 커스터마이징 가능한 오류 완화 워크플로우를 구현하는 방법을 배웁니다. PNA 기법을 소개하고 이를 사용해 게이트 오류를 완화합니다. 또한 TREX를 사용하여 읽기 오류를 완화하고, 학습된 노이즈 모델에 포함되지 않은 오류를 완화하기 위해 사후 선택(post-selection)을 사용합니다.
개요
PNA에 대한 간략한 개요 제공- Trotterized 양자 Circuit과 Observable 생성. Backend에 트랜스파일하고 사후 선택 측정 포함.
samplomatic을 사용하여 2Q 게이트 레이어와 측정을 트와일링(twirl). 노이즈 학습 비용을 줄이기 위해 고유한 2Q 레이어 탐색.NoiseLearnerV3를 사용하여 2Q 게이트와 측정에 영향을 미치는 오류 모델 학습.qiskit-addon-pna를 사용하여 노이즈 완화 Observable 생성qiskit-ibm-runtime.Executor프리미티브를 사용하여 모든 트와일링 무작위화 및 측정 기저에 대한 모든 샷을 반영하는 원시 QPU 샘플 생성qiskit-addon-utils를 사용하여 데이터를 완화된 기댓값으로 후처리.
전파된 노이즈 흡수 (PNA)란 무엇인가요?
2-큐비트 게이트에 영향을 미치는 노이즈 채널의 역을 Observable을 통해 전파함으로써 게이트 오류를 완화하는 기법으로, 노이즈 완화 Observable을 생성합니다.
실행하려는 실험의 2Q 게이트는 상당한 노이즈의 영향을 받습니다.
노이즈 모델을 학습하면 역 모델을 적용하여 노이즈를 제거할 수 있습니다.
PEC에서처럼 QPU에서 역 노이즈 채널을 샘플링하여 구현하는 대신, Pauli 전파를 사 용하여 측정된 Observable에서 고전적으로 구현할 수 있습니다. 이를 통해 더 복잡한 Observable이 생성되며, 이를 측정하면 학습된 게이트 노이즈를 완화하는 효과가 있습니다.

미러링된 Trotter Circuit과 Observable 생성
이 실험에서는 1D 스핀 체인 위 30사이트 kicked Ising 모델의 시간 동역학을 연구합니다. 고려하는 해밀토니안은 다음과 같습니다:
,
여기서 은 최근접 이웃 스핀()의 결합을 나타내고, 전역 횡방향 자기장 는 로 설정됩니다. 가 Clifford 각도(즉, )에서 멀어질수록, Circuit을 통해 반노이즈 생성자를 전파하기가 더 어려워집니다.
Observable의 선택으로는 평균 단일 사이트 자화, 를 고려합니다. 여기서 은 사이트 수입니다.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-pna qiskit-addon-utils qiskit-ibm-runtime samplomatic
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8
# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)]) / num_qubits
# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

다음으로, ibm_kingston에서 낮은 오류율을 보고하는 Qubit 체인을 선택하고 Circuit을 Backend에 트랜스파일합니다.
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
backend_name = "ibm_kingston"
service = QiskitRuntimeService()
backend = service.backend(backend_name, use_fractional_gates=True)
# Use a chain of low-noise qubits
layout = [
44,
45,
46,
47,
57,
67,
68,
69,
78,
89,
88,
87,
97,
107,
106,
105,
117,
125,
126,
127,
128,
129,
118,
109,
110,
111,
98,
91,
92,
93,
]
pm = generate_preset_pass_manager(backend=backend, initial_layout=layout, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)
qiskit_runtime_service._discover_account:WARNING:2025-11-10 14:30:57,148: Loading account with the given token. A saved account will not be used.

2-큐비트 게이트 레이어와 측정 트와일링 및 고유 레이어 탐색
여기서 패스 매니저가 박스에 Twirl 및 InjectNoise 어노테이션을 달도록 보장합니다. 이를 통해 Circuit에 영향을 미치는 노이즈를 학습하고 해당 노이즈를 해당 Circuit 레이어와 연결할 수 있습니다.
enable_gates/enable_measure: True: 모든 2q 게이트 레이어와 터미널 측정을 박스로 묶습니다. 단일 큐비트 게이트는 박스 내부에서 왼쪽으로 드레싱됩니다.measure_annotations: all측정 박스에Twirl및ChangeBasis어노테이션을 포함합니다.twirling_strategy: active: 얽힘 게이트를 포함하는 각 박스 내의 모든 활성 Qubit을 트와일링합니다.inject_noise_targets: gates:InjectNoise어노테이션은 얽힘 게이트를 포함하는 모든Twirl어노테이션 박스에 추가되어야 합니다.inject_noise_strategy: uniform_modification: 모든 노이즈 레이어는 동일하게 스케일링되어야 합니다.
from samplomatic.transpiler import generate_boxing_pass_manager
# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
remove_barriers=True,
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

템플릿 Circuit과 samplex 생성, Circuit 샘플링 방법 정의
여기서는 Executor에서 출력된 샘플에 대해 사후 선택을 수행하는 데 필요한 관찰자(spectator) 및 사후 선택 측정도 추가합니다.
import samplomatic
from qiskit.transpiler import PassManager
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Learn the noise
실험을 실행하기 전에, 회로 내 얽힘 게이트와 측정에 영향을 미치는 노이즈 모델을 학습합니다. 정확한 노이즈 모델을 확보하는 것은 오류를 효과적으로 완화하는 데 필수적입니다. 실험 실행 직전에 노이즈를 학습하면, 실제 실행 중 게이트에 영향을 미치는 노이즈를 노이즈 모델이 충실히 반영할 가능성이 가장 높아집니다.
노이즈를 학습하기 전에, 회로 내 고유한 2-큐비트 레이어를 찾아야 합니다. 이를 통해 전체 회로의 노이즈를 학습하는 데 필요한 샷 수를 최소화할 수 있습니다. samplomatic의 find_unique_box_instructions를 사용하여 측정 레이어를 포함한 박스 처리된 회로에서 고유 레이어를 추출합니다. 이 레이어들이 노이즈 학습기에 전달됩니다.
레이어를 파악한 후 노이즈를 학습할 수 있습니다. 고려해야 할 몇 가지 파라미터가 있습니다:
num_randomizations: 학습 회로 설정당 사용할 랜덤 회로 수shots_per_randomization: 랜덤 학습 회로당 사용할 총 샷 수layer_pair_depths: 학습 실험에 사용할 회로 깊이(쌍의 수로 측정)post_selection: 학습 중rx게이트를 사용하여 측정 후 펄스를 구현하는 엣지 기반 사후 선택(post-selection)을 적용합니다
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3 import NoiseLearnerV3
from qiskit_ibm_runtime.options import NoiseLearnerV3Options
from samplomatic.utils import find_unique_box_instructions
# Load noise learner data from a shared job
load_saved_nl_result = True
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
strategy = "edge"
enable_postsel = True
x_pulse_type = "rx"
# Find the unique instructions (layers) from boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
noise_learner_params = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": [1, 2, 4, 8, 12, 16, 24, 32, 40, 48],
"post_selection": {
"enable": enable_postsel,
"strategy": strategy,
"x_pulse_type": x_pulse_type,
},
"experimental": {},
}
# set the options
noise_learner_options = NoiseLearnerV3Options(**noise_learner_params)
# run the noise learner job
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()
nl_metadata = noise_learner_params | {"layout": layout}
import matplotlib.pyplot as plt
hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1]
hw_rates_2q += [rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()
<matplotlib.legend.Legend at 0x321dd63f0>

Associate circuit boxes with learned noise
여기서는 각 박스의 InjectNoise 참조 ID와, 해당 박스 내 얽힘 게이트에 영향을 미치는 학습된 노이즈 모델(PauliLindbladMap) 사이의 매핑을 생성합니다.
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation
# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(unique_2q_layers_and_meas, noise_learner_result, strict=False):
if inject_noise_annot := get_annotation(instruction.operation, InjectNoise):
refs_to_noise_models[inject_noise_annot.ref] = result.to_pauli_lindblad_map()
Propagate the observable through the learned anti-noise to get a noise-mitigating observable
위에서 설명한 대로, 이 과정은 두 단계로 이루어집니다. 먼저 안티-노이즈 생성자를 회로 끝까지 전파합니다. 그런 다음 해당 진화된 생성자를 통해 observable을 전파합니다. 이 과정은 회로 내 각 안티-노이즈 생성자에 대해 반복됩니다. 이 구현에서는 주어진 레이어의 각 생성자가 병렬로 회로 끝까지 전파됩니다. 또한 Python 멀티프로세싱을 사용하여 안티-노이즈의 순방향 전파와 observable의 역방향 전파를 병렬로 수행합니다. 이를 통해 메모리 내 진화된 생성자 누적을 방지하고 컴퓨팅 자원을 최대화합니다.
PNA를 실행할 때는 항상 노이즈가 있는 회로와 observable을 제공해야 합니다. 노이즈가 있는 회로가 InjectNoise 어노테이션이 있는 박스 처리된 회로라면, 위 단계에서 생성한 매핑을 제공해야 합니다. qiskit-aer의 PauliLindbladError 명령어를 포함하는 박스 처리되지 않은 회로를 전달할 수도 있습니다. 이 경우 refs_to_noise_models를 제공할 필요가 없습니다. 주요 입력 외에도 다음 사항을 고려해야 합니다:
max_err_terms: 순방향 전파 중 각 안티-노이즈 생성자에서 유지할 항의 수입니다. 이 값을 크게 하면 일반적으로 정확도가 높아지지만, 단조 증가를 보장하지는 않습니다.max_obs_terms: 진화된 안티-노이즈를 통해 역방향 전파되는 노이즈 완화 observable 에서 유지할 항의 수입니다. 값이 클수록 일반적으로 정확도가 높아지지만, 단조 증가를 보장하지는 않습니다.num_processes: 이 프로세스에 할당할 코어 수입니다. 생성자는 observable에 적용되는 동시에 병렬로 순방향 전파됩니다.search_step: 역방향 전파 단계에서는 탐욕적(greedy) 방법을 사용하여 Pauli 기저에서 두 연산자를 근사적으로 켤레 변환합니다.search_step을 늘리면 이 방법을 빠르게 할 수 있습니다. 자세한 내용은 pauli-prop docs를 참조하세요.num_to_measure: 이 변수는generate_noise_mitigating_observable의 입력이 아니지만, 실제로 측정할 의 항 수를 제어하는 데 사용합니다. 여기서는 원래 observable의 항인 상위 30개 항만 측정합니다. 이 항들은 이제 재조정되어, 측정 시 학습된 게이트 노이즈를 완화하는 효과가 있습니다. 에서 30개의 항만 측정하더라도, 선두 항의 스케일링 인수 정밀도를 높이기 위해 를 크게 키우는 것이 여전히 유용한 경우가 많습니다.
from qiskit_addon_pna import generate_noise_mitigating_observable
# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits
obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]][
:num_to_measure
]
Finished! 13560 / 13560 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")
[<matplotlib.lines.Line2D at 0x16b69e840>]

Transform the measurement bases to canonical form
다음으로, 측정 대상 observable의 모든 Pauli 항을 완전히 커버할 수 있는 최소한의 측정 기저(basis) 집합을 찾겠습니다 (교환 가능한(qubit-wise commute) observable은 동시에 측정할 수 있습니다). 측정할 항은 원래 observable, 즉 모든 단일-Z Pauli의 합이므로, 단 하나의 기저—전체 Z 기저—만 필요합니다.
Pauli 측정 기저 집합을 찾는 것과 더불어, 이 Pauli 항들을 Executor 프리미티브가 기대하는 정규 형식(canonical form)으로 매핑해야 합니다. 정규 큐비트 순서에 대한 자세한 정보는 samplomatic 문서를 참고하세요.
from qiskit_addon_utils.exp_vals.measurement_bases import get_measurement_bases
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx for idx, qubit in enumerate(boxed_circuit.qubits) if qubit in meas_box.qubits
]
c_2_p = {c: p for c, p in enumerate(canonical_qubits)} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8) for base in meas_bases
]
Specify how to sample in the QuantumProgram
QuantumProgram은 실험을 어떻게 샘플링할지 지정하는 곳입니다:
template_circuit: 원하는 모든 무작위화(twirling 무작위화, 파라미터 등)를 구현하는 데 필요한 모든 게이트가 포함된 Circuit입니다.samplex: 샘플링할 모든 가능한 Circuit 무작위화에 대한 확률 분포를 정의하는 객체입니다.samplex_arguments: samplex를 완전히 정의하는 데 필요한 바인딩basis_changes: 측정 대상 observable의 모든 Pauli 항을 커버하는 측정 기저 집합을 지정하는 곳입니다.noise_scales.ref: 각 노이즈 레이어의 스케일을0.0으로 설정하여 샘플에 추가 노이즈가 주입되지 않도록 합니다.pauli_lindblad_maps:noise_scales가 전달될 경우 필수입니다. 노이즈 레이어를 해당 노이즈 모델에 매핑합니다.
shape:samplex_arguments가 암묵적으로 정의하는 형상을 확장하는 형상(shape) 튜플입니다. 이 확장에 의해 도입되는 비자명 축(non-trivial axes)은 무작위화를 열거합니다.
from qiskit_ibm_runtime import QuantumProgram
# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144
# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}
# Specify the bases to measure
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {"basis_changes": {"basis0": bases_broadcastable}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = samplex.inputs().make_broadcastable().bind(**samplex_inputs)
# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec),
)
Sample the circuit using the Executor primitive prototype
QuantumProgram을 정의했으니, 실험 실행은 간단합니다. Executor 객체를 인스턴스화하고 Backend를 제공한 다음 프로그램을 실행하면 됩니다.
from qiskit_ibm_runtime import Executor
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
Post-process the samples to calculate an error-mitigated expectation value
오류 완화된 기댓값을 계산하기 위해 다음을 수행합니다:
- 측정에 영향을 미치는 학습된 노이즈를 기반으로 TREX 스케일링 인수(scaling factor) 계산
- 포스트 선택(post-selected) 샘플만 유지하기 위한 마스크 생성
qiskit-addon-utils의executor_expectation_values함수를 사용하여 모든 데이터를 오류 완화된 기댓값으로 결합
from qiskit_addon_utils.exp_vals.expectation_values import executor_expectation_values
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")
# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)
bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]
evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])
experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated")
plt.ylabel("Expectation value", fontsize=14)
plt.title(r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$", fontsize=14)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
