การดีบักและการติดตาม
ฟังก์ชัน build() แปลง circuit ที่มี annotation เป็นคู่ของ template circuit และ samplex ใน circuit ที่ซับซ้อนอาจยากที่จะบอกว่า box ใดใน circuit ต้นฉบับตรงกับ barrier ใดใน template หรือ node ใดใน samplex DAG
Samplomatic มีเครื่องมือติดตามที่เสริมกันสองอย่าง:
-
ป้ายกำกับ barrier: ทุก barrier ใน template circuit มีป้ายกำกับที่เข้ารหัสต้นกำเนิดของมัน ป้ายกำกับเหล่านี้มีอยู่เสมอไม่ว่าจะตั้งค่า
debug=Trueหรือไม่ -
ข้อมูล trace บน samplex node: เมื่อเรียก
build()ด้วยdebug=Trueทุก samplex node จะมีออบเจ็กต์TraceInfoที่เชื่อมโยงกลับไปยัง box ที่สร้างมัน
เครื่องมือทั้งสองมีประโยชน์ที่สุดเมื่อ box มี annotation Tag ซึ่งแนบสตริง ref ให้กับ box นอกจากนี้ ยังมีการแนบการอ้างอิงของ InjectNoise ด้วย
แท็ก box
annotation Tag แนบสตริง ref ให้กับ box จากนั้น ref จะปรากฏในป้ายกำกับ barrier และในข้อมูล trace บน samplex node
circuit ตัวอย่างต่อไปนี้มีสาม box: box CX ที่มีแท็กสองใบบนคู่ qubit ที่ไม่ซ้อนกัน และ box การวัดแบบ right-dressed ที่ไม่มีแท็กหนึ่งใบ box CX ใบที่สองยังมี annotation InjectNoise ด้วย ทั้ง tag ref และ noise ref ของมันจะปรากฏในป้ายกำกับ barrier box CX ทั้งสองครอบคลุม qubit ต่างกัน ซึ่งทำให้ตัวเพิ่มประสิทธิภาพ samplex รวม propagation node ของมันเป็น node ที่ใช้ร่วมกันได้ ซึ่งแสดงในส่วนข้อมูล trace
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")
ป้ายกำกับ barrier ใน template circuit
การเรียก build() บน circuit ข้างต้นจะได้ template ที่ barrier มีป้ายกำกับระบุตัวตน แต่ละป้ายกำกับมีรูปแบบ {side}{scope}@{key=value&...}:
-
Side:
L= ขอบเขต left-dressing,M= ขอบเขตเนื้อหาภายใน box,R= ขอบเขต right-dressing -
Scope: ดัชนีจำนวนเต็ม (หรือรายการที่คั่นด้วยขีดล่างสำหรับ box ที่ซ้อนกัน) ที่แยก box หลายใบในระดับการซ้อนเดียวกัน
-
Annotations: รายการคู่
key=valueที่ขึ้นต้นด้วย@และคั่นด้วย&ซึ่งได้จาก annotation ของ box คีย์tagมาจาก annotationTagและinject_noiseมาจาก annotationInjectNoise
barrier จาก box ที่ไม่มีแท็กมีเพียง side และ scope (เช่น L2) โดยไม่มีส่วนต่อท้าย @
template, samplex = build(circuit)
template.draw("mpl", fold=100)
ยังดึงป้ายกำกับ barrier ออกมาด้วยโปรแกรมได้:
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']
ข้อมูล trace บน samplex node
การส่ง debug=True ให้ build() จะแนบข้อมูล trace ให้ทุก samplex node แอตทริบิวต์ trace_info ของแต่ละ node เป็นออบเจ็กต์ TraceInfo ซึ่งดิกชันนารี trace_refs จับคู่คีย์ annotation (เช่น "tag" และ "inject_noise") กับเซตของสตริง ref node ที่ไม่มี annotation ของ box ที่สอดคล้องจะมี trace_info=None
เมื่อเรียก draw() บน samplex ที่สร้างในโหมดดีบัก การวางเมาส์เหนือ node ใดๆ ในกราฟแบบโต้ตอบจะแสดง trace_refs ของมันใน tooltip
template, samplex = build(circuit, debug=True)
samplex.draw()
ยังตรวจสอบข้อมูล trace ด้วยโปรแกรมได้ สังเกตว่า node บางตัวมีการอ้างอิงจากทั้ง 'cx_ab' และ 'cx_cd' ตัวเพิ่มประสิทธิภาพ samplex รวม propagation node แบบขนานของมัน เพราะ box ทั้งสองครอบคลุม qubit ที่ไม่ซ้อนกันและมี predecessor ร่วมกันจากการปล่อยของ box การวัดแบบ right-dressed
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
หากต้องการหา node ทั้งหมดที่มาจาก box ที่ระบุ ให้กรองด้วยคีย์ "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
samplex ที่ build() คืนค่าเป็นกราฟมีทิศทางไม่มีวัฏจักร (DAG) ที่ เส้นเชื่อมแสดงการพึ่งพา register กล่าวคือ เส้นเชื่อมจาก node A ไป node B หมายความว่า B ต้องรอให้ A ทำงานกับ virtual register ที่ใช้ร่วมกันเสร็จก่อน B จึงจะทำงานได้ นี่ไม่ใช่การเรียงลำดับตามเวลาเหมือน DAG ของ quantum circuit แต่กราฟไหลจาก node ที่รับผิดชอบการสร้างการสุ่มไปยัง node ที่รับผิดชอบการสังเคราะห์เป็นเอาต์พุต
มี node สามชนิด แต่ละชนิดมีรูปแบบภาพต่างกันในพล็อตแบบโต้ตอบ:
| รูปร่าง | สี | ชนิด | บทบาท |
|---|---|---|---|
| ดาว | แดง | SamplingNode | สร้าง virtual register ใหม่จากการแจกแจงหรืออินพุต |
| วงกลม | เขียว | EvaluationNode | แปลง รวม หรือส่งต่อ virtual register |
| โบว์ไทด์ | น้ำเงิน / ม่วง | CollectionNode | อ่าน register และเขียนไปยังเอาต์พุตของ sample() |
การทำงานดำเนินไปเป็นสามระยะ:
-
sampling node ทั้งหมดรันก่อน (แบบขนาน)
-
evaluation node รันถัดมาตามลำดับทอพอโลยี (ขนานกันภายในแต่ละรุ่น)
-
collection node รันท้ายสุด (แบบขนาน)
การจัดวางของ graphviz สะท้อนเรื่องนี้ sampling node อยู่ด้านบน collection node อยู่ด้านล่าง
Tooltip เมื่อวางเมาส์
ทุก node ในการแสดงภาพแบบโต้ตอบจะแสดง tooltip เมื่อคุณวางเมาส์เหนือมัน tooltip มีข้อมูลต่อไปนี้:
-
ชื่อคลาสของ node และดัชนีกราฟที่เป็นจำนวนเต็มของมัน
-
Register manifest ซึ่งบอกว่า node สร้าง อ่าน เขียน และลบ register ใด สิ่งเหล่านี้บอกว่าข้อมูลไหลระหว่าง node อย่างไร
-
รายละเอียดเฉพาะของ node รวมถึงชนิดการแจกแจงสำหรับ sampling node ตัวถูกดำเนินการสำหรับ multiplication node ดัชนีพารามิเตอร์ของ template สำหรับ collection node และอื่นๆ
-
Trace ref (เฉพาะเมื่อ
debug=True) ซึ่งคือคีย์ annotation และสตริง ref ที่เชื่อมโยง node กลับไปยัง box ต้นกำเนิดของมัน
การคลิกที่พล็อตแล้ววางเมาส์เหนือแต่ละ node เป็นวิธีที่เร็วที่สุดในการเข้าใจว่า node หนึ่งทำอะไรโดยไม่ต้องอ่านซอร์สโค้ด
สรุป samplex
ก่อนตรวจสอบการแสดงภาพ print(samplex) จะให้สรุปแบบข้อความอย่างรวดเร็วของจำนวน node อินพุตที่จำเป็น และเอาต์พุตที่รับประกัน
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.
ตรวจสอบ register
การส่ง keep_registers=True ให้ sample() จะเก็บออบเจ็กต์ VirtualRegister ระหว่างทางที่ยังมีชีวิตเมื่อสิ้นสุดการสุ่ม และเก็บไว้ใน outputs.metadata["registers"] แต่ละ register เป็นอาร์เรย์ 2 มิติ (shape (num_subsystems, num_randomizations) โดยอาจมีมิติ gate-shape ต่อท้าย) ของสมาชิกกลุ่มเสมือน
สิ่งนี้มีประโยชน์สำหรับการตรวจว่า virtual gate ถูกรวมอย่างถูกต้อง หรือสำหรับตรวจสอบตัวอย่าง Pauli หรือ unitary ดิบก่อนที่จะถูกสังเคราะห์เป็นมุมการหมุน
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)
คุณดูเนื้อหาของสถานะสุดท้ายของ register ที่ระบุได้ ในตัวอย่างต่อไปนี้ register มีชนิดเป็น 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 node จับคู่กับพารามิเตอร์ของ template อย่างไร
อาร์เรย์ outputs["parameter_values"] ที่ sample() คืนค่ามี shape (num_randomizations, N) โดย N ตรงกับ len(template.parameters) แต่ละคอลัมน์สอดคล้องกับหนึ่งพารามิเตอร์ใน template circuit คือคอลัมน์ที่ i ใส่ค่าให้ template.parameters[i]
collection node CollectTemplateValues เป็นตัวเชื่อมระหว่าง virtual register กับพารามิเตอร์ของ template แต่ละ node ดังกล่าวเก็บข้อมูลดัชนีที่บันทึกว่ามันเขียนไปยังคอลัมน์ใดของอาร์เรย์เอาต์พุต ทำให้ตามรอยได้ว่า subsystem ของ virtual gate ใดขับเคลื่อนพารามิเตอร์ของ template ใด
ดูคู่มือ {Samplex inputs and outputs}samplex-io เพื่อเรียนรู้วิธีผูกค่าพารามิเตอร์ที่สุ่มได้เข้ากับ template circuit และรันการทดลอง
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 จะเพิ่ม annotation Tag ให้ทุก box โดยอัตโนมัติ มีสามโหมดให้เลือก:
-
"unique_instance": กำหนดrefตามลำดับ (t0,t1และต่อไปเรื่อย ๆ) ให้กับ box ตามลำดับใน circuit ทุก box จะได้refที่ไม่ซ้ำกัน ไม่ว่าโครงสร้างจะเป็นอย่างไร -
"unique_box": คำนวณ structural hash ของเนื้อหาในแต่ละ box แล้วกำหนดrefเดียวกันให้กับทุก box ที่มีโครงสร้างเทียบเท่ากัน มีประโยชน์เมื่อต้องการจัดกลุ่ม box ตามชนิดแทนที่จะตามตำแหน่ง -
"noise_ref": คัดลอกrefจาก annotationInjectNoiseของแต่ละ box และแท็กเฉพาะ box ที่มี annotation นี้เท่านั้น มีประโยชน์เมื่อมี noise ref ที่มีความหมายอยู่แล้ว
ตัวอย่างต่อไปนี้ใช้ "unique_instance" และ "unique_box" กับ circuit ที่มี CX 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
