Debugowanie i śledzenie
Funkcja build() przekształca obwód z adnotacjami w parę: obwód szablonowy i samplex. W złożonym obwodzie trudno stwierdzić, który box w oryginalnym obwodzie odpowiada którym barierom w szablonie lub którym węzłom w grafie DAG samplexu.
Samplomatic udostępnia dwa uzupełniające się narzędzia śledzenia:
-
Etykiety barier: Każda bariera w obwodzie szablonowym ma etykietę kodującą jej pochodzenie. Są one zawsze obecne, niezależnie od tego, czy ustawiono
debug=True. -
Informacje śledzenia w węzłach samplexu: Gdy
build()jest wywoływane zdebug=True, każdy węzeł samplexu zawiera obiektTraceInfołączący go z boxem lub boxami, które go utworzyły.
Oba narzędzia są najbardziej przydatne, gdy boxy mają adnotację Tag, która dołącza do boxa łańcuch ref. Dołączane są również odwołania InjectNoise.
Tagowanie boxów
Adnotacja Tag dołącza do boxa łańcuch ref. Ten ref pojawia się następnie w etykietach barier i w informacjach śledzenia w węzłach samplexu.
Poniższy przykładowy obwód ma trzy boxy: dwa otagowane boxy CX na rozłącznych parach kubitów oraz nieotagowany box pomiarowy z prawostronnym dressingiem. Drugi box CX ma również adnotację InjectNoise — zarówno jego odwołanie tag, jak i odwołanie ref szumu pojawią się w etykietach barier. Oba boxy CX obejmują różne kubity, co pozwala optymalizatorowi samplexu połączyć ich węzły propagacji we wspólne węzły. Pokazuje to sekcja informacji śledzenia.
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")
Etykiety barier w obwodzie szablonowym
Wywołanie build() na powyższym obwodzie daje szablon, którego bariery mają etykiety identyfikujące. Każda etykieta ma postać {side}{scope}@{key=value&...}:
-
Strona:
L= granica dressingu lewostronnego,M= granica wewnętrznej zawartości boxa,R= granica dressingu prawostronnego. -
Zakres: Indeks całkowitoliczbowy (lub lista rozdzielona podkreśleniami dla zagnieżdżonych boxów), który odróżnia wiele boxów na tym samym poziomie zagnieżdżenia.
-
Adnotacje: Lista par
key=valuepoprzedzona@i rozdzielona&, utworzona z adnotacji boxa. Klucztagpochodzi z adnotacjiTag, ainject_noisez adnotacjiInjectNoise.
Bariery z nieotagowanych boxów zawierają tylko stronę i zakres (na przykład L2), bez sufiksu @.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Etykiety barier można również wyodrębnić programowo:
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']
Informacje śledzenia w węzłach samplexu
Przekazanie debug=True do build() dołącza informacje śledzenia do każdego węzła samplexu. Atrybut trace_info każdego węzła to obiekt TraceInfo, którego słownik trace_refs odwzorowuje klucze adnotacji (na przykład "tag" i "inject_noise") na zbiory łańcuchów ref. Węzły bez odpowiadającej adnotacji boxa mają trace_info=None.
Gdy draw() jest wywoływane na samplexie zbudowanym w trybie debugowania, najechanie na dowolny węzeł w interaktywnym grafie pokazuje jego trace_refs w podpowiedzi.
template, samplex = build(circuit, debug=True)
samplex.draw()
Informacje śledzenia można również sprawdzać programowo. Zauważ, że niektóre węzły zawierają odwołania zarówno do 'cx_ab', jak i 'cx_cd'. Optymalizator samplexu połączył ich równoległe węzły propagacji, ponieważ oba boxy obejmują rozłączne kubity i mają wspólnego poprzednika pochodzącego z emisji boxa pomiarowego z prawostronnym dressingiem.
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
Aby znaleźć wszystkie węzły pochodzące z określonego boxa, przefiltruj po kluczu "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
Graf DAG samplexu
Samplex zwracany przez build() to skierowany graf acykliczny (DAG), w którym krawędzie oznaczają zależność rejestrów. Oznacza to, że krawędź od węzła A do węzła B znaczy, że B musi poczekać, aż A zadziała na wspólnych rejestrach wirtualnych, zanim B będzie mógł działać. Nie jest to porządek czasowy, jak w grafie DAG obwodu kwantowego. Zamiast tego graf biegnie od węzłów odpowiedzialnych za generowanie randomizacji do węzłów odpowiedzialnych za ich syntezę jako wyników.
Istnieją trzy typy węzłów, każdy z odrębnym stylem wizualnym na interaktywnym wykresie:
| Kształt | Kolor | Typ | Rola |
|---|---|---|---|
| Gwiazda | Czerwony | SamplingNode | Tworzy nowe rejestry wirtualne z rozkładu lub z danych wejściowych |
| Koło | Zielony | EvaluationNode | Przekształca, łączy lub propaguje rejestry wirtualne |
| Muszka | Niebieski / Fioletowy | CollectionNode | Odczytuje rejestry i zapisuje do wyników sample() |
Wykonanie przebiega w trzech fazach:
-
Najpierw działają wszystkie węzły próbkujące (równolegle).
-
Następnie działają węzły ewaluacji, w porządku topologicznym (równolegle w obrębie każdej generacji).
-
Na końcu działają węzły zbierające (równolegle).
Układ graphviz to odzwierciedla. Węzły próbkujące są na górze, węzły zbierające na dole.
Podpowiedzi po najechaniu
Każdy węzeł w interaktywnej wizualizacji pokazuje podpowiedź po najechaniu na niego kursorem. Podpowiedź zawiera następujące informacje:
-
Nazwa klasy węzła i jego całkowitoliczbowy indeks w grafie.
-
Manifesty rejestrów, czyli rejestry, które węzeł tworzy, odczytuje, zapisuje i usuwa. Pokazują one, jak dane przepływają między węzłami.
-
Szczegóły specyficzne dla węzła, w tym typ rozkładu dla węzłów próbkujących, operand dla węzłów mnożenia, indeksy parametrów szablonu dla węzłów zbierających i tak dalej.
-
Trace refs (tylko gdy
debug=True), czyli klucze adnotacji i łańcuchy ref łączące węzeł z boxami, z których pochodzi.
Kliknięcie wykresu, a następnie najeżdżanie na poszczególne węzły to najszybszy sposób, aby zrozumieć, co robi dany węzeł, bez czytania kodu źródłowego.
Podsumowanie samplexu
Przed przejrzeniem wizualizacji print(samplex) daje szybkie tekstowe podsumowanie liczby węzłów, wymaganych danych wejściowych i obiecanych wyników.
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.
Sprawdzanie rejestrów
Przekazanie keep_registers=True do sample() zachowuje pośrednie obiekty VirtualRegister, które są aktywne na końcu próbkowania, i zapisuje je w outputs.metadata["registers"]. Każdy rejestr to tablica 2D (kształtu (num_subsystems, num_randomizations), z ewentualnymi końcowymi wymiarami kształtu bramki) wirtualnych elementów grupy.
Jest to przydatne do sprawdzenia, czy wirtualne bramki zostały połączone poprawnie, lub do przejrzenia surowych próbek Pauliego bądź unitarnych, zanim zostaną zsyntetyzowane w kąty obrotu.
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)
Możesz obejrzeć zawartość stanu końcowego wybranego rejestru. W poniższym przykładzie rejestr ma typ PauliRegister. Szczegóły dotyczące formatu przechowywania znajdziesz w 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)
Jak węzły samplexu odwzorowują się na parametry szablonu
Tablica outputs["parameter_values"] zwracana przez sample() ma kształt (num_randomizations, N), gdzie N odpowiada len(template.parameters). Każda kolumna odpowiada jednemu parametrowi w obwodzie szablonowym — i-ta kolumna wypełnia template.parameters[i].
Węzły zbierające CollectTemplateValues są łącznikiem między rejestrami wirtualnymi a parametrami szablonu. Każdy taki węzeł przechowuje informacje o indeksach, które zapisują, do których kolumn tablicy wyjściowej zapisuje. Dzięki temu można prześledzić, które podsystemy wirtualnych bramek sterują którymi parametrami szablonu.
Zobacz przewodnik {Samplex inputs and outputs}samplex-io, aby dowiedzieć się, jak powiązać próbkowane wartości parametrów z obwodem szablonowym i uruchamiać eksperymenty.
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]]
Automatyczne tagowanie
Podczas używania generate_boxing_pass_manager() parametr add_tags automatycznie dodaje adnotacje Tag do wszystkich boxów. Dostępne są trzy tryby:
-
"unique_instance": Przypisuje kolejneref(t0,t1i tak dalej) boxom w kolejności obwodu. Każdy box dostaje innyrefniezależnie od swojej struktury. -
"unique_box": Oblicza strukturalny skrót zawartości każdego boxa i przypisuje ten samrefwszystkim strukturalnie równoważnym boxom. Jest to przydatne do grupowania boxów według typu, a nie według pozycji. -
"noise_ref": Kopiujerefz adnotacjiInjectNoisekażdego boxa i oznacza tylko te boxy, które ją mają. Jest to przydatne, gdy znaczące noise ref już istnieją.
Poniższy przykład stosuje "unique_instance" i "unique_box" do obwodu, w którym dwa boxy CX są strukturalnie równoważne. Przy "unique_instance" każdy z nich dostaje inny ref, a przy "unique_box" oba współdzielą jeden.
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
