설정 옵션을 조정하는 방법
경계값 계산은 원리상 정확하지만 실제로는 근사적입니다. 회로를 통해 파울리 오류 항을 시간 발전시키면 항의 수가 지수적으로 많은 연산자가 만들어지고, 그 결과로 얻은 교환자의 스펙트럼 노름을 구하는 것 자체도 비용이 큽니다. 아래 옵션들은 이 비용을 제어하며, 세 그룹으로 나뉩니다.
-
정확도 옵션은 경계값의 수치를 바꾸며, 경계의 조밀함과 비용 사이에서 균형을 맞춥니다.
-
범위는 어떤 경계값을 계산할지 결정합니다.
-
실행 옵션은 계산이 어떻게 실행되는지를 바꿉니다(계산 대상은 바뀌지 않습니다).
| 옵션 | 기본값 | 제어 대상 | 결과에 미치는 영향 |
|---|---|---|---|
| 정확도 | |||
evolution_max_terms | 1_000_000 | 시간 발전 후 남는 연산자 항의 수 | 낮추면: 비용 감소, 경계값이 느슨해짐 |
atol_simplify | 1e-8 | 무시할 수 있는 것으로 보고 버리는 교환자 항 | 높이면: 비용 감소, 경계값이 느슨해짐 |
eigval_max_qubits | 14 | 스펙트럼 노름을 계산할 수 있는 교환자의 최대 너비 | 낮추면: 비용 감소, 경계값이 느슨해짐 |
atol_eigenvalue | 1e-10 | 반복 고유값 해법이 수렴했다고 보는 시점 | 높이면: 비용 감소, 경계값이 덜 정확해짐 |
| 범위 | |||
max_num_boxes | None | 경계값을 계산할 박스의 수 | 한도를 넘는 박스는 자명한 경계값 2.0을 유지함 |
| 실행 | |||
timeout | None | 전체 계산에 대한 실제 경과 시간 예산 | 남은 작업은 자명한 경계값 2.0을 유지함 |
num_processes | 1 | 경계값을 병렬로 계산하는 워커 프로세스의 수 | 그 자체로는 영향 없음 |
경계값은 유효할 때만 유용합니다. 즉, 오류 항이 관측량에 미치는 영향을 절대 과소평가해서는 안 됩니다. atol_eigenvalue를 제외한 이 옵션들을 느슨하게 설정하면 경계값은 커지기만 할 뿐(더 보수적이 될 뿐) 작아지지는 않습니다. 따라서 느슨한 설정의 대가는 잘못된 기댓값이 아니라 더 큰 샘플링 오버헤드입니다. 단 atol_eigenvalue는 예외입니다.
회로 정의
다음 실험들은 퀵스타트 가이드와 동일한 트로터화된 이징 회로와 관측량을 사용합니다. 이 크기에서는 모든 옵션이 눈에 띄는 효과를 내면서도 각 계산이 1초 미만으로 끝납니다.
# 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
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
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"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198
작은 도우미 함수가 각 경계값 집합을 한 줄로 요약하므로, 옵션의 효과를 방대한 배열 대신 몇 개의 숫자로 확인할 수 있습니다.
모든 경계값은 0과 자명한 경계값 2.0 사이에 있습니다. 요약에는 경계값의 평균과 표준 편차가 표시되고, 이어서 항이 다음 그룹으로 어떻게 나뉘는지 표시됩니다.
- 시간 발전된 관측량과 교환하므로 경계값이 정확히 0인 항
- 0과 2.0 사이에서 경계값이 계산된 항
- 자명한 2.0으로 남은 항
평균이 낮을수록 더 조밀하고, 조밀할수록 이후의 샘플링 오버헤드가 작아진다는 점에 유의하세요.
하나의 옵션에 대한 스윕에서는 그 옵션의 기본값을 *로 표시합니다.
import inspect
import time
from qiskit_addon_slc.bounds import compute_forward_bounds
def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# 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:28} 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%} │ "
f"{elapsed:4.2f}s"
)
def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates
def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})
baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
정확도
evolution_max_terms
파울리 오류 항을 비클리퍼드 게이트를 통해 시간 발전시키면 파울리 연산자들의 합이 되고, 그 합은 회로 깊이에 따라 커집니다. evolution_max_terms는 그중 몇 개의 항을 유지할지 상한을 정합니다. 버려진 항들의 1-노름은 경계값에 더해지므로 아무것도 조용히 손실되지 않습니다.
스윕은 기본값보다 조밀한 값에서 극단적인 절삭까지 진행됩니다. 상한을 낮추면 계산은 저렴해지고 경계값은 느슨해집니다.
sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s
상한을 기본값 이상으로 높여도 여기서는 아무것도 바뀌지 않습니다. 이 정도 크기의 회로에서는 기본값만으로도 이미 모든 항이 유지되므로, 경계값은 이 옵션이 만들 수 있는 가장 조밀한 상태입니다. 이것이 전형적인 상황입니다. 정확도 옵션을 더 엄격하게 설정하는 것은 기본값이 실제로 제약이 될 때만 효과가 있으며, 이를 확인하는 방법이 위의 실험입니다. 더 엄격한 값으로 다시 실행해 경계값이 움직이는지 확인해 보세요.
경고: 내부 전파 루틴은 evolution_max_terms를 확인해야 할 상한이 아니라 미리 할당할 용량으로 취급합니다. numpy.iinfo(numpy.uint).max처럼 명목상 무제한인 값을 전달하면 할당자가 테라바이트 단위의 메모리를 요청하다가 프로세스가 중단됩니다. 현실적인 숫자로 유지하세요. 기본값인 100만도 이미 충분히 넉넉합니다.
atol_simplify
교환자가 만들어진 후에는 계수가 atol_simplify보다 작은 항이 버려집니다. 시간 발전 상한과 마찬가지로, 이렇게 손실된 1-노름은 경계값에 더해집니다.
sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s
eigval_max_qubits
가장 조밀한 경계값은 교환자의 스펙트럼 노름이며, 이를 구하려면 고유값 계산이 필요합니다. 교환자가 넓어질수록 이 계산은 비용이 커지므로, eigval_max_qubits는 그 너비를 넘으면 시도를 포기하고 삼각 부등식으로 대체하는 기준을 정합니다. 교환자의 계수 에 대한 이 경계값 는 유효하지만 더 느슨합니다.
이 값을 0으로 설정하면 모든 곳에서 대체 방법이 사용되므로, 고유값 해법의 이점을 숫자 하나로 확인할 수 있습니다.
sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s
고유값 해법을 끄더라도 평균은 자명한 2.0보다 훨씬 낮게 유지됩니다. 경계값이 0인 항의 비율은 변하지 않습니다. 시간 발전된 관측량과 정확히 교환하는 항은 교환자가 0이며, 이는 고유값 계산을 고려하기 전에 인식되기 때문입니다. 나머지 항 중 일부는 삼각 부등식 경계값이 2.0에 도달해 거기서 제한되지만, 많은 항은 여전히 더 작아서 2.0 미만의 계산된 경계값을 유지합니다.
atol_eigenvalue
스펙트럼 노름을 계산하는 경우, atol_eigenvalue는 반복 고유값 해법의 수렴 허용 오차입니다. 하지만 대부분의 교환자는 그 해법에 도달하지 않습니다.
-
최대 4개 큐비트에 작용하는 교환자는 직접 정확하게 대각화됩니다.
-
더 넓은 교환자는 먼저 파울리 항이 필요로 하는 최소 개수의 큐비트로 줄여지며, 줄어든 형태가 최대 8개 큐비트에 작용하면 정확하게 대각화됩니다.
그 축소 이후에도 8개를 넘는 큐비트가 필요한 교환자만 반복 해법으로 가며, 이 허용 오차가 적용되는 곳은 여기뿐입니다. 이 가이드에서 사용하는 크기의 회로는 거기까지 가지 않으므로, 이 옵션에 대한 스윕은 모든 값에서 동일한 경계값을 보여 줍니다. 이 옵션은 대형 디바이스의 넓은 라이트콘에서 중요합니다.
다른 정확도 옵션과 달리 atol_eigenvalue를 느슨하게 하는 것은 한 방향으로 안전하지 않습니다. 반복 고유값 해법은 스펙트럼 안쪽에서 극단 고유값에 접근하므로, 완전히 수렴하지 않은 추정값은 스펙트럼 노름과 경계값을 과소평가합니다. 기본값이 의도적으로 엄격한 이유가 이것입니다. 해법이 수렴하지 못하면 삼각 부등식 대체 방법이 대신 사용되며, 이 경우는 다시 유효합니다.
범위
max_num_boxes
유용한 한도가 정확도가 아니라 깊이인 경우도 있습니다. max_num_boxes는 지정한 박스 수를 넘으면 경계값 계산 생성을 중단하며, 그 이후의 모든 레이어에는 대신 자명한 경계값 2.0이 할당됩니다.
이 한도는 게이트 깊이 같은 값이 아니라, 노이즈가 있는 BoxOp 명령(InjectNoise 주석이 있는 것)의 수를 셉니다. 그러면 사용자에게 보이는 회로 구조에 한도가 연결되고, 트랜spile 실행마다 달라질 수 있는 자동 생성 주석 참조에 의존하지 않게 됩니다.
두 절반을 각각 순방향 경계값과 역방향 경계값으로 다루는 회로에는 이것이 자연스러운 선택입니다. 각각 실제로 사용할 깊이까지만 계산하세요.
from qiskit.circuit import BoxOp
from samplomatic import InjectNoise
# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)
for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models
* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s
실행
timeout
timeout은 초 단위의 실제 경과 시간 예산입니다. 이 시간이 만료되면 그때까지 계산된 경계값은 유지되고 남은 모든 항목은 자명한 경계값 2.0을 유지하므로, 결과는 항상 사용할 수 있지만 일찍 중단할수록 더 느슨해집니다.
이것은 엄격한 마감 시한이 아닙니다. 이미 실행 중인 작업은 끝까지 수행되므로 호출이 작업 하나의 소요 시간만큼 예산을 초과할 수 있습니다.
num_processes
서로 다른 오류 항의 경계값은 독립적이므로, num_processes는 이들을 워커 프로세스에 분배합니다. 이것만으로는 계산에 걸리는 시간에만 영향을 주고 결과에는 영향을 주지 않습니다.
참고: timeout과 num_processes를 함께 사용하면 결과가 타이밍에 따라 달라집니다. 예산이 만료되기 전에 어떤 작업이 끝나는지는 결정적이지 않습니다. 경계값은 계속 유효하지만 실행할 때마다 재현되지는 않습니다.
어디서 시작할까
기본값은 비용을 감당할 수 있는 크기의 회로에서 조밀한 경계값을 주도록 정해져 있습니다. 계산이 너무 느릴 때는 대략 다음 순서로 옵션을 적용해 보세요.
-
num_processes: 정확도 손실 없이 얻는 공짜 속도 향상입니다. -
max_num_boxes: 회로의 어느 깊이까지 관심이 있는지 미리 알고 있는 경우입니다. -
eigval_max_qubits: 가장 넓은 교환자에 대한 가장 비싼 단계를 건너뜁니다. 이런 교환자는 보통 전체 항의 소수입니다. -
evolution_max_terms와atol_simplify: 이 둘은 전반적으로 정확도에 영향을 주므로 마지막에 조정하세요. 위와 같은 스윕으로 경계값이 얼마나 움직이는지 확인합니다. -
timeout: 조정 수단이라기보다 강제 중단으로, 품질과 관계없이 마감 시한까지 계산이 끝나야 할 때 사용합니다.
경계값이 느슨할수록 주어진 편향 허용 오차에 대해 compute_local_scales가 보고하는 샘플링 오버헤드가 커집니다. 조정하는 동안 지켜볼 가치가 있는 값이 바로 이 오버헤드이며, 근사가 실제로 얼마의 비용을 치르는지 알려 줍니다.