사용자 지정 경계값을 계산하는 방법
compute_forward_bounds와 compute_backward_bounds는 편의용 래퍼입니다. 둘 다 공개 API가 제공하는 동일한 세 가지 구성 요소로 만들어집니다.
-
단일 시간 발전된 파울리 오류 항의 경계를 구하는 노름 함수
-
오류 항이 어느 방향으로 시간 발전되는지의 시드를 정하는 라이트콘
-
회로를 순회하며 항별 작업을 분배하는
compute_bounds
이를 직접 조립하면 래퍼가 다루지 않는 것의 경계도 구할 수 있습니다. 이 가이드는 그럴 만한 가치가 있는 한 가지 사례를 다룹니다. 일반적인 역방향 경계값이 불필요하게 느슨한 미러 회로입니다.
래퍼가 실제로 하는 일
두 래퍼는 위에 나열한 세 부분에서 차이가 있습니다.
compute_forward_bounds | compute_backward_bounds | |
|---|---|---|
| 회로 | 주어진 그대로 | remove_measure(circuit).inverse() |
| 노름 함수 | time_evolved_norm_forward, 관측량 기준 | time_evolved_norm_backward, 초기 상태 기준 |
| 라이트콘 | LightCone.initialize_from_pauli, 관측량에서 시드 | LightCone.initialize_from_measurements, 모든 활성 큐비트에서 시드 |
backwards | False | True |
순방향 경계값은 *"이 오류가 앞으로 측정할 관측량을 얼마나 바꿀 수 있는가?"*를 묻고, 교환자의 스펙트럼 노름으로 답합니다. 역방향 경계값은 *"이 오류가 처음 시작한 상태를 얼마나 바꿀 수 있는가?"*를 묻고, 그 상태를 모든 활성 큐비트의 전부 0인 상태로 보아 핵 노름으로 답합니다. 두 질문은 서로 다르며, 각각 회로의 해당 절반에서 발생하는 오류에 맞는 질문입니다.
미러 회로의 경우
미러 회로는 어떤 유니터리를 적용한 다음 그것을 되돌립니다: . 이상적인 결과를 정확히 알아야 할 때(일반적으로 로슈미트 에코, 무작위 벤치마킹, 검증 실험) 이런 회로가 나타납니다.
미러 회로에는 일반적인 역방향 경계값이 활용할 수 없는 성질이 있습니다. 두 번째 절반이 첫 번째 절반의 역이므로, 오류를 초기 상태까지 역방향으로 시간 발전시키는 것은 오류를 관측량까지 순방향으로 시간 발전시키는 것과 같은 게이트들을 지납니다. 두 질문이 일치하므로, 더 날카로운 순방향 방식의 경계값이 역방향에도 유효합니다.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "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
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit
num_qubits = 10
# The circuit of the quickstart guide...
forward_half = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# ...followed by its inverse. The ideal result is the initial state.
circuit = forward_half.compose(forward_half.inverse())
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])
boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)
print(f"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}")
print(f"noisy layers: {len(noise_model_paulis)}")
mirror circuit: 10 qubits, depth 24
noisy layers: 4
사용자 지정 경계값 조립하기
이 방법은 compute_backward_bounds가 회로에 대해 하는 일(측정을 제거하고, 뒤집고, backwards=True로 순회하는 것)은 그대로 유지하고, 나머지 두 구성 요소를 교체합니다. 즉 순방향 노름 함수와, compute_forward_bounds가 자체 라이트콘을 시드하는 것과 똑같이 LightCone.initialize_from_pauli로 관측량에서 시드한 라이트콘입니다. 각 교체는 한 줄이면 됩니다.
from functools import partial
from qiskit_addon_slc.bounds import (
LightCone,
compute_bounds,
time_evolved_norm_forward,
)
from qiskit_addon_slc.utils import remove_measure
def compute_mirror_backward_bounds(
circuit,
noise_model_paulis,
/,
observable,
*,
evolution_max_terms=1_000_000,
**kwargs,
):
"""Bounds the backward direction of a mirror circuit with a forward-style bound.
Valid only for circuits whose second half inverts the first: the backward evolution then
traverses the same gates as the forward one, and reproduces the forward bounds.
"""
# As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit
# backwards from its end becomes walking the inverted circuit forwards.
circuit = remove_measure(circuit).inverse()
# The substitution: the forward norm function, bounding against the observable rather than
# against the initial state.
norm_fn = partial(
time_evolved_norm_forward,
observable=observable,
evolution_max_terms=evolution_max_terms,
)
# Seed the light cone from the observable, not from every active qubit.
light_cone = LightCone.initialize_from_pauli(circuit, observable)
return compute_bounds(
circuit,
noise_model_paulis,
light_cone,
norm_fn,
backwards=True,
**kwargs,
)
얼마나 향상되는가
같은 회로에서 기본 역방향 경계값과 비교하며, 설정 가이드와 같은 방식으로 요약합니다. 경계값의 평균과 표준 편차에 이어, 경계값이 0인 항, 그 사이에서 경계값이 계산된 항, 자명한 2.0으로 남은 항의 비율을 보여 줍니다.
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
)
forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)
custom_backward = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable
)
def summarize(label, bounds):
"""Prints a one-line summary of a set of bounds."""
rates = np.concatenate([bound.rates for bound in bounds.values()])
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:18} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%}"
)
summarize("forward", forward_bounds)
summarize("backward (stock)", stock_backward)
summarize("backward (custom)", custom_backward)
forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%
backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
기본 역방향 경계값은 정보가 거의 없는 자명한 값 2.0 근처에 있습니다. 미러 회로에서는 그 경계값이 측정하는 의미에서 모든 오류가 "초기 상태에 가깝기" 때문에 항들을 구별하지 못합니다. 사용자 지정 경계값은 몇 배 더 조밀하며 많은 항의 경계값을 0으로 잡아 줍니다.
구현을 확인하기 위해 둘을 박스별로 비교할 수 있습니다. 모든 박스에서 사용자 지정 역방향 경계값은 반올림 오차 범위 내에서 같은 박스의 순방향 경계값과 동일하게 나옵니다. 둘 다 회로의 같은 지점에서 같은 오류의 경계를 구하며, 미러 회로에서는 그 지점 이후의 게이트들이 그 이전 게이트들의 역으로 상쇄되므로, 두 계산은 같은 양을 반대쪽 끝에서 평가하는 셈입니다.
matches = sum(
np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)
for box_id in forward_bounds
)
print(f"{matches} of {len(forward_bounds)} boxes match the forward bounds")
worst = max(
np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()
for box_id in forward_bounds
)
print(f"largest disagreement across all terms: {worst:.2e}")
16 of 16 boxes match the forward bounds
largest disagreement across all terms: 6.00e-15
참고: 이 동등성은 기법의 성질이 아니라 미러 구조의 성질입니다. 자기 역이 아닌 회로에서는 같은 교체를 해도 실행은 되지만, 더 이상 역방향에 대해 유효한 경계값을 계산하지 못합니다. 그 질문이 적용되지 않는 회로 절반에 대해 순방향 질문에 답하는 것이기 때문입니다. 회로의 두 번째 절반이 첫 번째 절반을 되돌리는 경우에만 사용하세요.
나머지 워크플로에 연결하기
결과는 일반적인 Bounds 딕셔너리이므로 이후의 모든 과정은 그대로입니다.
미러 회로에서는 어느 쪽 경계값도 회로 전체에 대해 계산할 이유가 없습니다. 순방향 경계값은 끝에서부터 센 의 박스에만, 역방향 경계값은 처음부터 센 의 박스에만 필요합니다. 따라서 max_num_boxes가 각각을 노이즈 박스의 절반으로 제한하고, merge_bounds가 두 절반을 합칩니다.
이것이 실제로 경계값을 사용하는 방식이며, 위의 박스별 비교는 모든 박스에 대해 두 경계값이 모두 필요하므로 더 이상 할 수 없다는 뜻입니다. 이는 이 가이드를 위한 구현 확인이지 워크플로 단계가 아닙니다.
from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds
# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}
# Compute each bound for its own half of the mirror circuit only. `forward_bounds` has an entry
# for every noisy box, which are the boxes `max_num_boxes` counts.
half = len(forward_bounds) // 2
half_forward = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
half_stock = compute_backward_bounds(
boxed_circuit, noise_model_paulis, max_num_boxes=half
)
half_custom = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
print(
f"each bound computed for {half} of {len(forward_bounds)} noisy boxes\n"
)
print(f"{'bounds':34} {'overhead':>12} {'residual bias':>14}")
for label, bounds in (
(
"forward + stock backward",
merge_bounds(boxed_circuit, half_forward, half_stock, noise_rates),
),
(
"forward + custom backward",
merge_bounds(boxed_circuit, half_forward, half_custom, noise_rates),
),
("forward only, whole circuit", forward_bounds),
):
_, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
print(f"{label:34} {cost:12.4f} {bias:14.4f}")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
each bound computed for 8 of 16 noisy boxes
bounds overhead residual bias
forward + stock backward 29848.0583 0.0498
forward + custom backward 465.4028 0.0498
forward only, whole circuit 465.4028 0.0498
기본 역방향 경계값을 쓰면 샘플링 오버헤드가 훨씬 커집니다. merge_bounds는 회로의 한 전환 지점까지는 역방향 경계값을, 그 이후로는 순방향 경계값을 사용하며, 전체 편향을 최소화하는 지점을 선택합니다. 각 경계값을 한 절반에 대해 계산하면 그 지점은 회로 중간에 놓이므로, 의 박스는 역방향 경계값만으로 다뤄집니다. 그곳에서 기본 역방향 경계값은 자명한 2.0에 가까워 오버헤드를 끌어올립니다. 사용자 지정 경계값은 순방향 경계값만큼 조밀하므로, 오버헤드는 완전한 순방향 경계값의 오버헤드와 같아집니다.
그렇다면 완전한 순방향 경계값만으로도 같은 결과가 나오는데 왜 굳이 이렇게 하느냐는 당연한 의문이 생깁니다. 답은 비용입니다. 회로 초반 박스의 순방향 경계값은 그 오류를 의 나머지 부분과 전체를 거쳐 시간 발전시키는데, 이는 회로에서 가장 길고 가장 넓은 시간 발전입니다. 시간 발전된 연산자는 지나가는 비클리퍼드 게이트의 수에 대해 지수적으로 커질 수 있고, 라이트콘이 넓어질수록 교환자의 스펙트럼 노름 계산도 지수적으로 비싸집니다. 절반씩 나누는 방식에서는 어떤 오류도 회로의 절반을 넘어 시간 발전되지 않습니다.
따라서 절약 효과는 회로 깊이에 따라 커집니다. 더 중요한 점은, 완전한 순방향 경계값은 절반 방식보다 훨씬 먼저 계산할 수 없게 된다는 것입니다. 초반 박스의 시간 발전이 evolution_max_terms나 eigval_max_qubits를 넘으면 그 경계값은 잘리거나 삼각 부등식으로 대체되고, 결국 계산 비용이 너무 커집니다. 그 지점이 어디든, 절반 방식은 그것이 회로 전반부 어딘가에 있기만 하면 됩니다. 순방향 경계값은 중간까지만 가면 되고, 미러 회로에서 의 박스에 대한 역방향 경계값은 의 대응하는 박스에 대한 순방향 경계값이 끝까지 가는 만큼 회로를 지나 시작점까지 오류를 시간 발전시키므로, 같은 깊이에서 한계에 다다릅니다.
참고: 의 박스에 대한 순방향 경계값이 같은 숫자를 가질 것 같은데, 왜 의 박스에 대해 역방향 경계값을 계산할까요? 박스들이 서로의 거울상이 아니기 때문입니다. 각 박스는 게이트의 한쪽에서 노이즈를 주입하고, 의 역 레이어는 고유한 노이즈 모델을 가진 별도의 박스이므로, 의 박스와 대응하는 박스는 회로의 다른 지점에서 그 오류를 보게 됩니다. 따라서 둘의 경계값은 부분적으로만 일치하며, 의 박스에 대한 경계값은 그 박스들에 대해 직접 계산해야 합니다.
다른 사용자 지정 경계값 작성하기
같은 세 가지 교체로 다른 경우도 다룰 수 있습니다.
-
다른 노름.
time_evolved_norm_forward는comm_norm_order를 받습니다.2가 아닌 값을 전달하면 고유값 해법을 건너뛰고 그 노름을 직접 계산합니다. -
다른 라이트콘.
LightCone.initialize_from_pauli는 임의의 파울리 연산자로 라이트콘을 시드하며,LightCone생성자는 추적할 큐비트와 시드가 되는 연산을 받으므로, 그런 식으로 표현할 수 있는 어떤 연산자에 대해서도 경계값을 계산할 수 있습니다. -
완전히 다른 노름 함수.
compute_bounds는Pauli와RotationGates를 받아CommutatorBounds를 반환하는 호출 가능 객체만 요구합니다. 이 시그니처를 만족하는 것이라면, 회로 계열에 특화된 닫힌 형태의 경계값을 포함해 무엇이든 동작합니다.
무엇을 대체하든 그 결과가 진짜 상한임을 증명해야 합니다. 오류 항의 영향을 과소평가해도 눈에 보이는 실패는 일어나지 않습니다. 그 항이 완화된 노이즈 모델에서 빠지게 되므로, 조용히 틀린 기댓값이 만들어질 뿐입니다.