Debugging und Tracing
Die Funktion build() wandelt ein annotiertes Circuit in ein Template-Circuit und ein Samplex-Paar um. In einem komplexen Circuit kann es schwierig sein zu erkennen, welche Box im ursprünglichen Circuit welchen Barrieren im Template oder welchen Knoten im Samplex-DAG entspricht.
Samplomatic bietet zwei sich ergänzende Tracing-Werkzeuge:
-
Barrier-Labels: Jede Barrier im Template-Circuit trägt ein Label, das ihren Ursprung kodiert. Diese sind immer vorhanden, unabhängig davon, ob
debug=Truegesetzt ist. -
Trace-Informationen an Samplex-Knoten: Wird
build()mitdebug=Trueaufgerufen, trägt jeder Samplex-Knoten einTraceInfo-Objekt, das ihn mit der Box oder den Boxen verknüpft, die ihn erzeugt haben.
Beide Werkzeuge sind am nützlichsten, wenn Boxen eine Tag-Annotation tragen, die der Box einen ref-String zuweist. Alternativ werden auch InjectNoise-Referenzen angehängt.
Boxen taggen
Eine Tag-Annotation weist einer Box einen ref-String zu. Der ref erscheint dann in Barrier-Labels und in den Trace-Informationen an Samplex-Knoten.
Das folgende Beispiel-Circuit hat drei Boxen: zwei getaggte CX-Boxen auf disjunkten Qubit-Paaren und eine ungetaggte rechts-gedressete Messbox. Die zweite CX-Box trägt außerdem eine InjectNoise-Annotation – sowohl ihr tag-Ref als auch ihr Noise-ref erscheinen in den Barrier-Labels. Die beiden CX-Boxen decken unterschiedliche Qubits ab, wodurch der Samplex-Optimizer ihre Propagationsknoten zu gemeinsamen Knoten zusammenführen kann. Das wird im Abschnitt zu den Trace-Informationen gezeigt.
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-Labels im Template-Circuit
Der Aufruf von build() auf dem obigen Circuit erzeugt ein Template, dessen Barriers identifizierende Labels tragen. Jedes Label hat die Form {side}{scope}@{key=value&...}:
-
Side:
L= linke Dressing-Grenze,M= innere Grenze des Box-Inhalts,R= rechte Dressing-Grenze. -
Scope: Ein ganzzahliger Index (bei verschachtelten Boxen eine durch Unterstriche getrennte Liste), der mehrere Boxen auf derselben Verschachtelungsebene unterscheidet.
-
Annotations: Eine mit
@beginnende, durch&getrennte Liste vonkey=value-Paaren, abgeleitet aus den Annotationen der Box. Der Schlüsseltagstammt aus einerTag-Annotation undinject_noiseaus einerInjectNoise-Annotation.
Barriers von ungetaggten Boxen tragen nur Side und Scope (zum Beispiel L2), ohne @-Suffix.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Die Barrier-Labels lassen sich auch programmatisch extrahieren:
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-Informationen an Samplex-Knoten
Wenn du debug=True an build() übergibst, werden an jeden Samplex-Knoten Trace-Informationen angehängt. Das Attribut trace_info jedes Knotens ist ein TraceInfo-Objekt, dessen trace_refs-Dictionary Annotationsschlüssel (zum Beispiel "tag" und "inject_noise") auf Mengen von Ref-Strings abbildet. Knoten ohne entsprechende Box-Annotation haben trace_info=None.
Wird draw() auf einem im Debug-Modus gebauten Samplex aufgerufen, zeigt das Überfahren eines beliebigen Knotens im interaktiven Graphen dessen trace_refs im Hover-Tooltip an.
template, samplex = build(circuit, debug=True)
samplex.draw()
Trace-Informationen lassen sich auch programmatisch untersuchen. Beachte, dass einige Knoten Referenzen sowohl von 'cx_ab' als auch von 'cx_cd' tragen. Der Samplex-Optimizer hat ihre parallelen Propagationsknoten zusammengeführt, weil die beiden Boxen disjunkte Qubits abdecken und einen gemeinsamen Vorgänger aus der Emission der rechts-gedresseten Messbox teilen.
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
Um alle Knoten zu finden, die von einer bestimmten Box stammen, filtere nach dem Schlüssel "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
Der Samplex-DAG
Der von build() zurückgegebene Samplex ist ein gerichteter azyklischer Graph (DAG), bei dem Kanten Registerabhängigkeiten bezeichnen. Das heißt, eine Kante von Knoten A zu Knoten B bedeutet, dass B warten muss, bis A auf den gemeinsamen virtuellen Registern gewirkt hat, bevor B wirken darf. Das ist keine zeitliche Ordnung wie beim DAG eines Quantencircuits. Stattdessen fließt der Graph von den Knoten, die für das Erzeugen von Randomisierungen zuständig sind, zu den Knoten, die sie als Ausgaben synthetisieren.
Es gibt drei Knotentypen, jeder mit einem eigenen visuellen Stil im interaktiven Plot:
| Form | Farbe | Typ | Rolle |
|---|---|---|---|
| Stern | Rot | SamplingNode | Instanziiert neue virtuelle Register aus einer Verteilung oder Eingabe |
| Kreis | Grün | EvaluationNode | Transformiert, kombiniert oder propagiert virtuelle Register |
| Fliege | Blau / Violett | CollectionNode | Liest Register und schreibt in sample()-Ausgaben |
Die Ausführung verläuft in drei Phasen:
-
Zuerst laufen alle Sampling-Knoten (parallel).
-
Danach laufen die Evaluation-Knoten in topologischer Reihenfolge (parallel innerhalb jeder Generation).
-
Zuletzt laufen die Collection-Knoten (parallel).
Das Graphviz-Layout spiegelt das wider. Sampling-Knoten erscheinen oben, Collection-Knoten unten.
Hover-Tooltips
Jeder Knoten in der interaktiven Visualisierung zeigt einen Tooltip, wenn du mit der Maus darüberfährst. Der Tooltip enthält die folgenden Informationen:
-
Klassenname des Knotens und sein ganzzahliger Graphindex.
-
Register-Manifeste, also welche Register der Knoten instanziiert, liest, beschreibt und entfernt. Sie zeigen dir, wie Daten zwischen Knoten fließen.
-
Knotenspezifische Details, darunter der Verteilungstyp bei Sampling-Knoten, der Operand bei Multiplikationsknoten, die Template-Parameterindizes bei Collection-Knoten und so weiter.
-
Trace-Refs (nur bei
debug=True), also die Annotationsschlüssel und Ref-Strings, die den Knoten mit den Boxen verknüpfen, aus denen er stammt.
Auf den Plot zu klicken und dann einzelne Knoten zu überfahren, ist der schnellste Weg zu verstehen, was ein bestimmter Knoten tut, ohne Quellcode zu lesen.
Samplex-Zusammenfassung
Bevor du die Visualisierung untersuchst, liefert print(samplex) eine kurze Textzusammenfassung der Knotenanzahl, der benötigten Eingaben und der zugesicherten Ausgaben.
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 untersuchen
Die Übergabe von keep_registers=True an sample() behält die zwischenzeitlichen VirtualRegister-Objekte bei, die am Ende des Samplings aktiv sind, und speichert sie in outputs.metadata["registers"]. Jedes Register ist ein 2D-Array (Form (num_subsystems, num_randomizations), mit möglichen nachgestellten Gate-Form-Dimensionen) aus virtuellen Gruppenelementen.
Das ist nützlich, um zu überprüfen, ob virtuelle Gates korrekt kombiniert wurden, oder um die rohen Pauli- oder unitären Samples zu untersuchen, bevor sie zu Rotationswinkeln synthetisiert werden.
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)
Du kannst den Inhalt des Endzustands eines bestimmten Registers ansehen. Im folgenden Beispiel hat das Register den Typ PauliRegister. Details zum Speicherformat findest du in der 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)
Wie Samplex-Knoten auf Template-Parameter abgebildet werden
Das von sample() zurückgegebene Array outputs["parameter_values"] hat die Form (num_randomizations, N), wobei N mit len(template.parameters) übereinstimmt. Jede Spalte entspricht einem Parameter im Template-Circuit – die i-te Spalte füllt template.parameters[i].
Die CollectTemplateValues-Collection-Knoten sind die Verbindung zwischen virtuellen Registern und Template-Parametern. Jeder dieser Knoten hält Indexinformationen, die festhalten, in welche Spalten des Ausgabe-Arrays er schreibt. So lässt sich nachverfolgen, welche virtuellen Gate-Subsysteme welche Template-Parameter steuern.
Siehe die Anleitung {Samplex inputs and outputs}samplex-io, um zu erfahren, wie du die gesampelten Parameterwerte an das Template-Circuit bindest und Experimente ausführst.
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]]
Automatisches Tagging
Bei Verwendung von generate_boxing_pass_manager() fügt der Parameter add_tags allen Boxen automatisch Tag-Annotationen hinzu. Es stehen drei Modi zur Verfügung:
-
"unique_instance": Weist den Boxen in der Reihenfolge des Circuits fortlaufenderef-Werte (t0,t1usw.) zu. Jede Box erhält unabhängig von ihrer Struktur ein eigenesref. -
"unique_box": Berechnet einen strukturellen Hash des Inhalts jeder Box und weist allen strukturell äquivalenten Boxen dasselberefzu. Das ist nützlich, um Boxen nach Typ statt nach Position zu gruppieren. -
"noise_ref": Kopiert dasrefaus derInjectNoise-Annotation jeder Box und versieht nur Boxen mit einem Tag, die eine solche Annotation haben. Das ist nützlich, wenn bereits sinnvolle Noise-Refs existieren.
Das folgende Beispiel wendet "unique_instance" und "unique_box" auf einen Circuit an, dessen zwei CX-Boxen strukturell äquivalent sind. Mit "unique_instance" erhält jede ein eigenes ref, mit "unique_box" teilen sie sich eines.
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
