디버깅과 추적
build() 함수는 주석이 달린 Circuit을 템플릿 Circuit과 samplex 쌍으로 변환합니다. 복잡한 Circuit에서는 원본 Circuit의 어떤 박스가 템플릿의 어떤 배리어, 또는 samplex DAG의 어떤 노드에 해당하는지 알기 어려울 수 있습니다.
Samplomatic은 서로 보완하는 두 가지 추적 도구를 제공합니다.
-
배리어 레이블: 템플릿 Circuit의 모든 배리어에는 그 출처를 인코딩한 레이블이 붙습니다.
debug=True설정 여부와 관계없이 항상 존재합니다. -
samplex 노드의 추적 정보:
build()를debug=True로 호출하면, 모든 samplex 노드가 자신을 만들어 낸 박스를 가리키는TraceInfo객체를 가집니다.
두 도구 모두 박스에 Tag 주석이 있을 때 가장 유용합니다. 이 주석은 박스에 ref 문자열을 붙입니다. 또는 InjectNoise 참조도 함께 붙습니다.
박스에 태그 달기
Tag 주석은 박스에 ref 문자열을 붙입니다. 이 ref는 배리어 레이블과 samplex 노드의 추적 정보에 나타납니다.
다음 예시 Circuit에는 박스가 세 개 있습니다. 서로 겹치지 않는 Qubit 쌍에 놓인 태그가 있는 CX 박스 두 개와, 태그가 없는 오른쪽 드레싱 측정 박스입니다. 두 번째 CX 박스에는 InjectNoise 주석도 달려 있어서, tag ref와 노이즈 ref가 모두 배리어 레이블에 나타납니다. 두 CX 박스는 서로 다른 Qubit을 다루기 때문에 samplex 옵티마이저가 전파 노드를 공유 노드로 병합할 수 있습니다. 이는 추적 정보 섹션에서 확인할 수 있습니다.
from qiskit.circuit import QuantumCircuit
from samplomatic import InjectNoise, Tag, Twirl, build
circuit = QuantumCircuit(4, 4)
with circuit.box([Twirl(), Tag("cx_ab")]):
circuit.cx(0, 1)
with circuit.box([Twirl(), InjectNoise("cx_noise"), Tag("cx_cd")]):
circuit.cx(2, 3)
with circuit.box([Twirl(), Tag("meas_box")]):
circuit.measure(range(4), range(4))
circuit.draw("mpl")
템플릿 Circuit의 배리어 레이블
위 Circuit에 build()를 호출하면 배리어에 식별용 레이블이 붙은 템플릿이 만들어집니다. 각 레이블의 형식은 {side}{scope}@{key=value&...}입니다.
-
Side:
L= 왼쪽 드레싱 경계,M= 박스 내부 콘텐츠 경계,R= 오른쪽 드레싱 경계. -
Scope: 같은 중첩 수준에 있는 여러 박스를 구별하는 정수 인덱스입니다(중첩된 박스의 경우 밑줄로 구분한 목록).
-
Annotations: 박스의 주석에서 가져온
key=value쌍을&로 구분하고@를 앞에 붙인 목록입니다.tag키는Tag주석에서,inject_noise는InjectNoise주석에서 가져옵니다.
태그가 없는 박스의 배리어에는 side와 scope만 있으며(예: L2), @ 접미사는 없습니다.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
배리어 레이블은 프로그램으로도 추출할 수 있습니다.
barrier_labels = [instr.operation.label for instr in template if instr.operation.name == "barrier"]
barrier_labels
['L0@tag=cx_ab',
'M0@tag=cx_ab',
'R0@tag=cx_ab',
'L1@inject_noise=cx_noise&tag=cx_cd',
'M1@inject_noise=cx_noise&tag=cx_cd',
'R1@inject_noise=cx_noise&tag=cx_cd',
'L2@tag=meas_box',
'M2@tag=meas_box',
'R2@tag=meas_box']
samplex 노드의 추적 정보
build()에 debug=True를 전달하면 모든 samplex 노드에 추적 정보가 붙습니다. 각 노드의 trace_info 속성은 TraceInfo 객체이며, 그 trace_refs 딕셔너리는 주석 키(예: "tag", "inject_noise")를 ref 문자열 집합에 대응시킵니다. 해당하는 박스 주석이 없는 노드는 trace_info=None입니다.
디버그 모드로 빌드한 samplex에서 draw()를 호출하면, 대화형 그래프에서 노드 위에 마우스를 올렸을 때 호버 툴팁 안에 해당 노드의 trace_refs가 표시됩니다.
template, samplex = build(circuit, debug=True)
samplex.draw()
추적 정보는 프로그램으로도 확인할 수 있습니다. 일부 노드는 'cx_ab'와 'cx_cd' 양쪽의 참조를 가진다는 점에 주목하세요. 두 박스가 서로 겹치지 않는 Qubit을 다루고, 오른쪽 드레싱 측정 박스의 방출에서 나온 공통 선행 노드를 공유하기 때문에, samplex 옵티마이저가 병렬 전파 노드를 병합했습니다.
for node in samplex.graph.nodes():
if node.trace_info is not None:
tags = node.trace_info.trace_refs.get("tag", set())
merged = " ← merged" if len(tags) > 1 else ""
print(f"{type(node).__name__:40s} tags={tags}{merged}")
TwirlSamplingNode tags={'meas_box'}
InjectNoiseNode tags={'cx_cd'}
CollectZ2ToOutputNode tags={'cx_cd'}
TwirlSamplingNode tags={'cx_cd'}
CombineRegistersNode tags={'cx_cd'}
CollectTemplateValues tags={'cx_cd'}
TwirlSamplingNode tags={'cx_ab'}
CombineRegistersNode tags={'cx_ab', 'cx_cd'} ← merged
CollectTemplateValues tags={'cx_ab', 'cx_cd'} ← merged
특정 박스에서 비롯된 모든 노드를 찾으려면 "tag" 키로 필터링합니다.
tag_ref = "cx_ab"
matching_nodes = [
node
for node in samplex.graph.nodes()
if node.trace_info is not None and tag_ref in node.trace_info.trace_refs.get("tag", set())
]
print(f"Nodes originating from box '{tag_ref}':")
for node in matching_nodes:
print(f" {type(node).__name__}")
Nodes originating from box 'cx_ab':
TwirlSamplingNode
CombineRegistersNode
CollectTemplateValues
samplex DAG
build()가 반환하는 samplex는 엣지가 레지스터 의존성을 나타내는 방향 비순환 그래프(DAG)입니다. 즉 노드 A에서 노드 B로 가는 엣지는, B가 동작하기 전에 A가 공유 가상 레지스터에 먼저 동작을 마쳐야 한다는 뜻입니다. 이는 양자 Circuit의 DAG처럼 시간 순서를 나타내는 것이 아닙니다. 대신 그래프는 무작위화를 생성하는 노드에서 이를 출력으로 합성하는 노드로 흐릅니다.
노드 유형은 세 가지이며, 대화형 플롯에서 각각 고유한 시각 스타일을 갖습니다.
| 모양 | 색상 | 유형 | 역할 |
|---|---|---|---|
| 별 | 빨강 | SamplingNode | 분포 또는 입력으로부터 새 가상 레지스터를 생성 |
| 원 | 초록 | EvaluationNode | 가상 레지스터를 변환, 결합 또는 전파 |
| 보타이 | 파랑 / 보라 | CollectionNode | 레지스터를 읽어 sample() 출력에 기록 |
실행은 세 단계로 진행됩니다.
-
모든 샘플링 노드가 먼저 (병렬로) 실행됩니다.
-
평가 노드가 그다음에 위상 정렬 순서로 실행됩니다(각 세대 안에서는 병렬).
-
컬렉션 노드가 마지막에 (병렬로) 실행됩니다.
graphviz 레이아웃에도 이것이 반영됩니다. 샘플링 노드는 위쪽에, 컬렉션 노드는 아래쪽에 나타납니다.
호버 툴팁
대화형 시각화에서 모든 노드는 마우스를 올리면 툴팁을 표시합니다. 툴팁에는 다음 정보가 들어 있습니다.
-
노드 클래스 이름과 정수 그래프 인덱스.
-
레지스터 매니페스트: 노드가 생성하고, 읽고, 쓰고, 제거하는 레지스터입니다. 노드 간 데이터 흐름을 알 수 있습니다.
-
노드별 세부 정보: 샘플링 노드의 분포 유형, 곱셈 노드의 피연산자, 컬렉션 노드의 템플릿 매개변수 인덱스 등이 포함됩니다.
-
추적 ref(
debug=True일 때만): 노드를 그 출처 박스와 연결하는 주석 키와 ref 문자열입니다.
플롯을 클릭한 다음 개별 노드 위에 마우스를 올려 보는 것이, 소스 코드를 읽지 않고 특정 노드가 하는 일을 이해하는 가장 빠른 방법입니다.
samplex 요약
시각화를 살펴보기 전에 print(samplex)를 실행하면 노드 수, 필요한 입력, 약속된 출력을 간단한 텍스트 요약으로 볼 수 있습니다.
template, samplex = build(circuit)
print(samplex)
Samplex(<17 nodes>)
Inputs:
- 'pauli_lindblad_maps.cx_noise' <PauliLindbladMap>: A PauliLindblad map acting on 2
qubits, with 'num_terms_cx_noise' terms.
Outputs:
* 'measurement_flips.c' <bool['num_randomizations', 1, 4]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 24]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 1]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.
레지스터 살펴보기
sample()에 keep_registers=True를 전달하면 샘플링이 끝난 시점에 살아 있는 중간 VirtualRegister 객체를 유지하여 outputs.metadata["registers"]에 저장합니다. 각 레지스터는 가상 군 원소로 이루어진 2차원 배열(형태는 (num_subsystems, num_randomizations)이며, 뒤에 Gate 형태 차원이 붙을 수 있음)입니다.
가상 Gate가 올바르게 결합되었는지 확인하거나, 회전 각도로 합성되기 전의 원시 Pauli 또는 유니터리 샘플을 살펴볼 때 유용합니다.
from qiskit.quantum_info import PauliLindbladMap
outputs = samplex.sample(
{"pauli_lindblad_maps.cx_noise": PauliLindbladMap.identity(2)},
num_randomizations=3,
keep_registers=True,
)
for name, reg in outputs.metadata["registers"].items():
print(f"{name}: type={reg.TYPE.value}, shape={reg.virtual_gates.shape}")
lhs_0: type=pauli, shape=(4, 3)
rhs_0: type=pauli, shape=(4, 3)
lhs_3: type=pauli, shape=(2, 3)
rhs_3: type=pauli, shape=(2, 3)
inject_noise_1: type=pauli, shape=(2, 3)
sign_1: type=z2, shape=(1, 3)
lhs_7: type=pauli, shape=(2, 3)
rhs_7: type=pauli, shape=(2, 3)
meas_prop_6: type=pauli, shape=(4, 3)
collect_10: type=u2, shape=(2, 3, 2, 2)
meas_prop_z2a_6: type=z2, shape=(4, 3)
collect_9: type=u2, shape=(4, 3, 2, 2)
collect_5: type=u2, shape=(2, 3, 2, 2)
특정 레지스터의 최종 상태 내용을 볼 수 있습니다. 다음 예시에서 레지스터의 유형은 PauliRegister입니다. 저장 형식에 대한 자세한 내용은 API documentation(/apidocs/index)__를 참고하세요.
print("lhs_0:", outputs.metadata["registers"]["lhs_0"])
outputs.metadata["registers"]["lhs_0"].virtual_gates
lhs_0: PauliRegister(<4, 3>)
array([[0, 0, 1],
[3, 1, 1],
[3, 0, 0],
[1, 0, 0]], dtype=uint8)
samplex 노드가 템플릿 매개변수에 대응하는 방식
sample()이 반환하는 outputs["parameter_values"] 배열의 형태는 (num_randomizations, N)이며, 여기서 N은 len(template.parameters)와 같습니다. 각 열은 템플릿 Circuit의 매개변수 하나에 대응하며, i번째 열이 template.parameters[i]를 채웁니다.
CollectTemplateValues 컬렉션 노드는 가상 레지스터와 템플릿 매개변수를 잇는 연결 고리입니다. 이러한 노드는 각각 출력 배열의 어느 열에 기록하는지를 나타내는 인덱스 정보를 가지고 있습니다. 덕분에 어떤 가상 Gate 서브시스템이 어떤 템플릿 매개변수를 구동하는지 추적할 수 있습니다.
샘플링된 매개변수 값을 템플릿 Circuit에 바인딩하고 실험을 실행하는 방법은 {Samplex 입력과 출력}samplex-io 가이드를 참고하세요.
from samplomatic.samplex.nodes import CollectTemplateValues
template, samplex = build(circuit, debug=True)
for node in samplex.graph.nodes():
if isinstance(node, CollectTemplateValues):
tags = node.trace_info.trace_refs.get("tag", set()) if node.trace_info else set()
print(f"tags={tags} → template param indices: {node.template_idxs.tolist()}")
tags={'cx_cd'} → template param indices: [[6, 7, 8], [9, 10, 11]]
tags={'cx_ab', 'cx_cd'} → template param indices: [[12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23]]
tags=set() → template param indices: [[0, 1, 2], [3, 4, 5]]
자동 태그 지정
generate_boxing_pass_manager()를 사용할 때 add_tags 매개변수는 모든 박스에 Tag 주석을 자동으로 추가합니다. 세 가지 모드를 사용할 수 있습니다.
-
"unique_instance": 회로 순서대로 박스에 순차적인ref(t0,t1등)를 할당합니다. 구조와 관계없이 모든 박스가 서로 다른ref를 갖습니다. -
"unique_box": 각 박스 내용의 구조적 해시를 계산하고, 구조적으로 동일한 모든 박스에 같은ref를 할당합니다. 위치가 아닌 유형별로 박스를 그룹화할 때 유용합니다. -
"noise_ref": 각 박스의InjectNoise주석에서ref를 복사하며, 해당 주석이 있는 박스에만 태그를 붙입니다. 의미 있는 노이즈 ref가 이미 있을 때 유용합니다.
다음 예제는 구조적으로 동일한 두 개의 CX box가 있는 회로에 "unique_instance"와 "unique_box"를 적용합니다. "unique_instance"를 사용하면 각각 별개의 ref를 갖고, "unique_box"를 사용하면 하나를 공유합니다.
from samplomatic.transpiler import generate_boxing_pass_manager
base_circuit = QuantumCircuit(3)
base_circuit.cx(0, 1)
base_circuit.cx(1, 2)
base_circuit.measure_all()
# unique_instance: every box gets a distinct tag ref
pm = generate_boxing_pass_manager(add_tags="unique_instance")
boxed = pm.run(base_circuit)
template, _ = build(boxed)
print("unique_instance barrier labels:")
for instr in template:
if instr.operation.name == "barrier" and instr.operation.label:
print(f" {instr.operation.label}")
unique_instance barrier labels:
L0@tag=t0
M0@tag=t0
R0@tag=t0
L1@tag=t1
M1@tag=t1
R1@tag=t1
L2@tag=t2
M2@tag=t2
R2@tag=t2
