Transpiler
Die Funktion generate_boxing_pass_manager() ist ein flexibles und praktisches Werkzeug, um einen qiskit.transpiler.PassManager zu erstellen, der Circuit-Anweisungen in annotierte Boxen gruppieren kann. Das ist eine einfache Alternative zum manuellen Gruppieren und Annotieren von Circuits und nützlich, um einen Workflow zu automatisieren, verschiedene Boxing-Strategien für Circuits zu testen und Ähnliches.
Diese Anleitung zeigt, wie du generate_boxing_pass_manager() und seine Argumente verwendest. Um die Wirkung der einzelnen Argumente der Funktion hervorzuheben, beziehen sich die folgenden Abschnitte hauptsächlich auf den unten gezeigten Circuit.
from qiskit.circuit import Parameter, QuantumCircuit
circuit = QuantumCircuit(4, 7)
circuit.h(1)
circuit.h(2)
circuit.cz(1, 2)
circuit.h(1)
circuit.cx(1, 0)
circuit.cx(2, 3)
circuit.measure(range(1, 4), range(3))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
for qubit in range(4):
circuit.rz(Parameter(f"th_{qubit}"), qubit)
circuit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit.measure(range(4), range(3, 7))
circuit.draw("mpl", scale=0.8)
Operationen in Boxen gruppieren
Das Argument enable_gates kann auf True oder False gesetzt werden, um festzulegen, ob die Zwei-Qubit-Gates in Boxen gruppiert werden sollen. Entsprechend legt enable_measures fest, ob Messungen gruppiert werden sollen oder nicht. Das folgende Snippet zeigt ein Beispiel, in dem sowohl Gates als auch Messungen gruppiert werden.
from samplomatic.transpiler import generate_boxing_pass_manager
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)

Wie in der Abbildung zu sehen ist, erzeugt der Pass Manager Boxen von zwei Typen: solche, die eine einzelne Schicht von Zwei-Qubit-Gates enthalten, und solche, die eine einzelne Schicht von Messungen enthalten. Diese Trennung spiegelt gängige Praxis bei Protokollen zum Lernen und Mindern von Rauschen wider, die in der Regel auf Schichten homogener Operationen abzielen. Die Zwei-Qubit-Gates und Messungen werden in der am weitesten links liegenden Box platziert, die sie aufnehmen kann, und jedes Einzel-Qubit-Gate wird in derselben Box platziert wie das Zwei-Qubit-Gate oder die Messung, auf die es folgt.
Der folgende Code zeigt ein Beispiel, in dem enable_gates auf False gesetzt ist. Wie zu sehen ist, werden die Zwei-Qubit-Gates nicht in Boxen gruppiert, und die Einzel-Qubit-Gates, die ihnen vorausgehen, ebenfalls nicht.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=False,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
Auswählen, wie die Boxen annotiert werden
Alle Boxen mit Zwei-Qubit-Gates und Messungen im zurückgegebenen Circuit besitzen linksseitige Dressed-Annotationen. Insbesondere werden alle Boxen, die Zwei-Qubit-Gates enthalten, mit einem Twirl annotiert. Für Messboxen können Nutzende zwischen Twirl, BasisTranform (mit mode auf "measure" gesetzt) oder beidem wählen. Der folgende Code erzeugt einen Circuit, in dem alle Boxen getwirlt werden und die Messboxen zusätzlich mit BasisTranform annotiert sind.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Den Circuit für die Noise-Injection vorbereiten
Mit inject_noise_targets kannst du festlegen, welche Boxen eine InjectNoise-Annotation erhalten sollen. Der folgende Code erzeugt zum Beispiel einen Circuit, in dem die Boxen mit Zwei-Qubit-Gates eine InjectNoise-Annotation besitzen, die Messboxen aber nicht.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Wenn ein Circuit zwei oder mehr Boxen enthält, die bis auf Einzel-Qubit-Gates äquivalent sind, werden alle mit einer InjectNoise-Annotation mit demselben ref annotiert. Somit entspricht die Anzahl eindeutiger refs im zurückgegebenen Circuit der Anzahl eindeutiger Boxen.
Der Wert für inject_noise_strategy beeinflusst die InjectNoise-Annotationen. Die folgenden Werte sind möglich:
-
modifier_ref='': Empfohlen, wenn du die Noise-Maps vor dem Sampling nicht ändern musst. -
modifier_ref=ref: Empfohlen, wenn alle Noise-Maps einheitlich um denselben Faktor skaliert werden müssen. -
Ein eindeutiger Wert für
modifier_ref: Empfohlen, wenn jede Noise-Map um einen anderen Faktor skaliert werden muss.
Der folgende Code erzeugt einen Circuit, in dem die Boxen mit Zwei-Qubit-Gates eine InjectNoise-Annotation mit eindeutigen modifier_ref-Werten besitzen.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
inject_noise_strategy="individual_modification",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Eine Twirling-Strategie auswählen
Der Boxing-Pass-Manager wählt zunächst aus, welche verschränkenden Gates und Messanweisungen in denselben Boxen erscheinen sollen. Danach sind die Boxen im Allgemeinen nicht mehr volle Breite – sie enthalten wahrscheinlich nur eine Teilmenge der im Circuit aktiven oder im Backend-Target vorhandenen Qubits. In typischen Konfigurationen des Boxing-Pass-Managers werden alle Qubits getwirlt, auf die eine Box-Anweisung wirkt (auch jene, auf die keine Anweisung innerhalb der Box wirkt); daher beeinflussen Boxen mit Teilbreite die rauschanpassende Wirkung des Twirlings. Die untätigen Qubits werden nicht getwirlt und können zum Beispiel kohärente Fehler aufbauen, während die Box angewendet wird. Die twirling_strategy bietet Optionen, um die Qubits zu erweitern, die jeder Box gehören, und dadurch die Anzahl untätiger Qubits zu verändern:
-
"active"(Standard) erweitert die Boxen nicht auf untätige Qubits. -
"active_accum"erweitert alle Boxen auf diejenigen Qubits, auf die bereits eine frühere Anweisung im Circuit gewirkt hat. Das ist nützlich, wenn du Qubits am Anfang des Circuits so lange wie möglich in ihrem Grundzustand belassen möchtest, indem du sie erst twirlst, wenn sie aktiv werden. -
"active_circuit"erweitert alle Boxen auf diejenigen Qubits, auf die irgendeine Anweisung im Circuit wirkt. Das ist nützlich, wenn du einen strukturierten Circuit mit wiederholten verschränkenden Schichten hast und die Anzahl eindeutiger Boxen minimieren möchtest, weil für jede ihr Rauschen gelernt werden muss. -
"all"erweitert alle Boxen auf alle Qubits in allen Quantenregistern des Circuits. Das ist nützlich als Erweiterung von"active_circuit", wenn alle Qubits der QPU immer getwirlt werden sollen.
Im folgenden Beispiel wird jede der Strategien auf den Basis-Circuit angewendet, anschließend werden die Ergebnisse gezeichnet.
circuit = QuantumCircuit(5, 2)
circuit.h(range(4))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.barrier()
circuit.h(range(4))
circuit.measure([1, 2], [0, 1])
circuit.draw("mpl", scale=0.6)
import matplotlib.pyplot as plt
plt.figure(figsize=(9, 6))
for idx, strategy in enumerate(["active", "active_accum", "active_circuit", "all"]):
ax = plt.subplot(2, 2, idx + 1)
pm = generate_boxing_pass_manager(twirling_strategy=strategy, remove_barriers="never")
transpiled_circuit = pm.run(circuit)
fig = transpiled_circuit.draw("mpl", scale=0.8, ax=ax)
ax.set_title(f"twirling_strategy='{strategy}'")
plt.tight_layout()

Festlegen, wie Barriers behandelt werden
Standardmäßig werden Barriers aus dem Circuit entfernt, nachdem alle verschränkenden Gates und Messanweisungen wie angewiesen geboxt wurden, einschließlich ihrer Erweiterungen durch die Auswahl von twirling_strategy, aber bevor Einzel-Qubit-Gates, wenn möglich, in die Box unmittelbar rechts davon aufgenommen werden.
Der folgende Circuit enthält zum Beispiel eine Barrier:
circuit_with_barrier = QuantumCircuit(4)
circuit_with_barrier.h(range(4))
circuit_with_barrier.cz(0, 1)
circuit_with_barrier.barrier()
circuit_with_barrier.cz(2, 3)
circuit_with_barrier.measure_all()
circuit_with_barrier.draw("mpl", scale=0.8)
Das Standardverhalten verhindert, dass die beiden verschränkenden Gates in derselben Box landen, lässt aber zu, dass die Einzel-Qubit-Gates trotz der Barrier in die nächste verfügbare Box aufgenommen werden.
boxing_pass_manager = generate_boxing_pass_manager()
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Wird remove_barriers auf 'never' gesetzt, bleiben die Barriers erhalten, sodass die Einzel-Qubit-Gates eingeschränkt sind.
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="never")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Werden die Barriers schließlich sofort entfernt, sind die Verschränker nicht mehr darauf beschränkt, in getrennten Boxen zu landen.
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="immediately")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Wie die Stratifikation funktioniert
Der Boxing-Pass-Manager führt nacheinander zwei zentrale Passes aus:
-
GroupGatesIntoBoxes— Stratifiziert verschränkende Gates in Boxen. Einzel-Qubit-Gates werden in dieser Phase ignoriert; nur Zwei-Qubit-Gates werden gruppiert. -
AbsorbSingleQubitGates— Nimmt jedes Einzel-Qubit-Gate in die nächstgelegene geeignete Box rechts davon auf.
Während der Stratifikation durchläuft GroupGatesIntoBoxes den Circuit-DAG in topologischer Reihenfolge und platziert jedes Zwei-Qubit-Gate in der frühesten Gruppe, deren Qubits noch nicht belegt sind. Drei Arten von Anweisungen wirken als Trennzeichen, die die aktuelle Gruppe für ihre Qubits abschließen und nachfolgende Gates auf diesen Qubits in eine neue Gruppe zwingen:
-
Barrieren
-
Bestehende Box-Anweisungen
-
Measurements
Einzel-Qubit-Gates sind keine Trennzeichen – sie haben keinen Einfluss auf die Gruppierung und werden AbsorbSingleQubitGates zur Behandlung überlassen. Derzeit wird keine Kommutativitätsanalyse durchgeführt; die Gruppierung folgt der topologischen Reihenfolge des Circuits unverändert.
Warum remove_barriers="after_stratification" der empfohlene Standard ist
Der Standardmodus ist "after_stratification", weil er zwei Vorteile verbindet:
-
Barriers beschränken, wie verschränkende Gates gruppiert werden (sie wirken bei der Stratifikation als Trennzeichen).
-
Barriers werden entfernt, bevor
AbsorbSingleQubitGatesläuft, sodass Einzel-Qubit-Gates weiterhin über frühere Barrier-Positionen hinweg wandern und in die nächstgelegene Box aufgenommen werden können.
Im Gegensatz dazu:
-
"never"behält Barriers durchgehend bei, wodurch Einzel-Qubit-Gates zwischen einer Barrier und einer Box eingeschlossen werden, ihre Aufnahme verhindert wird und ungetwirlte Rotationen im Circuit verbleiben. -
"immediately"entfernt Barriers vor der Stratifikation, sodass sie keinen Einfluss auf die Gruppierung haben. -
"finally"ist nützlich, wenn du Barriers im Ausgabe-Circuit haben möchtest (zum Beispiel zur Visualisierung); Einzel-Qubit-Gates zwischen einer Barrier und einer Box werden dann jedoch nicht aufgenommen.
Box-ISA-Circuits mit Barriers
Circuits mit wiederholter Struktur – wie Trotterisierte Hamiltonsche Simulationen, variationelle Ansätze und Fehlerverstärkungssequenzen – werden typischerweise programmatisch aufgebaut und anschließend in die ISA-Form transpiliert. Das manuelle Hinzufügen von Boxen zu einem transpilierten Circuit ist umständlich, daher ist der natürliche Workflow wie folgt:
-
Erstelle den logischen Circuit und füge Barriers an den Schichtgrenzen ein, die zu Box-Grenzen werden sollen.
-
Transpiliere zu ISA-Circuits. Der Transpiler von Qiskit erhält Barriers beim Routing und bei der Optimierung.
-
Führe
generate_boxing_pass_manager()auf dem ISA-Circuit aus.
Barriers werden benötigt, wenn das Ende einer Schicht nicht alle Qubits abdeckt. Der Greedy-Algorithmus würde andernfalls Gates der nächsten Schicht – auf diesen „freien“ Qubits – in dieselbe Gruppe legen. Als konkretes Beispiel betrachten wir eine zweistufige Heisenberg-ZZ-Simulation auf einem Ring mit 5 Qubits. Jeder Trotter-Schritt hat drei Bindungsschichten: gerade Bindungen (0,1), (2,3), ungerade Bindungen (1,2), (3,4) und die Schlussbindung (4,0). Nach der Schicht mit ungeraden Bindungen sind die Qubits 0 und 4 beide frei. Ohne Barrier wird die Schlussbindung (4,0) per Greedy-Gruppierung mit Bindungen des nächsten Trotter-Schritts zusammengefasst, die diese freien Qubits verwenden, sodass Gates aus verschiedenen Schritten in derselben Box landen.
from qiskit.circuit import Parameter, QuantumCircuit
n = 5 # 5-qubit ring: bonds (0,1), (1,2), (2,3), (3,4), (4,0)
dt = Parameter("dt")
heisenberg = QuantumCircuit(n)
for step in range(2):
# Layer 1: even bonds (0,1) and (2,3)
for idx in range(0, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 2: odd bonds (1,2) and (3,4) -- qubits 0 and 4 are now free
for idx in range(1, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 3: wrap bond (4,0) -- both qubits were free after layer 2
heisenberg.rzz(dt, n - 1, 0)
if step < 1:
heisenberg.barrier() # separates the two Trotter steps
heisenberg.measure_all()
heisenberg.draw("mpl", scale=0.7)
Der folgende Code transpiliert den Circuit in die ISA-Form, unter der Annahme, dass das zugrunde liegende Gerät CX als nativen Verschränker hat. Beachte, dass die Barriers die Transpilation überstehen und im Ausgabe-Circuit sichtbar bleiben.
from qiskit.transpiler import generate_preset_pass_manager
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
isa_circuit = preset_pm.run(heisenberg)
isa_circuit.draw("mpl", scale=0.5, fold=200)
Wende als Nächstes den Boxing-Pass-Manager auf den ISA-Circuit an. Jede der drei Bindungsschichten wird in jedem Trotter-Schritt zu einer eigenen Box.
boxing_pm = generate_boxing_pass_manager()
boxed_isa = boxing_pm.run(isa_circuit)
boxed_isa.draw("mpl", scale=0.5, fold=100)
Den Pass-Manager von Samplomatic in die Qiskit-Preset-Pass-Manager einbinden
Der folgende Code zeigt, wie du generate_boxing_pass_manager() in einen Qiskit-Preset-Pass-Manager einbindest. Der transpilierte Circuit ist ISA und enthält Boxen.
from qiskit.transpiler import generate_preset_pass_manager
preset_pass_manager = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2]],
optimization_level=0,
)
boxing_pass_manager = generate_boxing_pass_manager()
# Run the boxing pass manager after the scheduling stage
preset_pass_manager.post_scheduling = boxing_pass_manager
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)
circuit.measure_all()
transpiled_circuit = preset_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
Dieses Muster lässt sich auch natürlich mit dem barrierbasierten ISA-Boxing-Workflow kombinieren. Der Boxing-Pass-Manager läuft als Schritt nach dem Scheduling, nachdem die Barriers des ursprünglichen logischen Circuits die Transpilation überstanden haben, sodass sie die Stratifizierung wie erwartet steuern.
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
preset_pm.post_scheduling = generate_boxing_pass_manager()
boxed_heisenberg = preset_pm.run(heisenberg)
boxed_heisenberg.draw("mpl", scale=0.5, fold=100)
Annotationen prüfen und ändern
Nach dem automatischen Boxing möchtest du vielleicht die Annotationen bestimmter Boxen prüfen oder anpassen, zum Beispiel um Rauschinjektion hinzuzufügen, Annotationen zu entfernen oder eine Twirling-Gruppe zu ändern. Samplomatic stellt dafür eine Reihe von Hilfsfunktionen in samplomatic.utils bereit. Alle geben einen neuen Circuit zurück, ohne das Original zu verändern.
extend_annotations() hängt eine oder mehrere Annotationen an jede Box im Circuit an.
from samplomatic import InjectNoise, Twirl
from samplomatic.utils import extend_annotations
boxed = generate_boxing_pass_manager().run(circuit_with_barrier)
# Add InjectNoise to every box
with_noise = extend_annotations(boxed, InjectNoise("my_noise_ref"))
with_noise.draw("mpl", scale=0.8)
filter_annotations() behält nur Annotationen der angegebenen Typen und entfernt die übrigen. replace_annotations() wendet einen Callable auf jede Annotation an; der Callable gibt eine Liste von Annotationen zurück, die stattdessen eingesetzt werden (gib [] zurück, um zu entfernen, eine Liste mit einem Element, um zu ersetzen, oder eine Liste mit mehreren Elementen, um zu erweitern).
from samplomatic.utils import filter_annotations, replace_annotations
# Keep only Twirl annotations, drop everything else
twirl_only = filter_annotations(with_noise, Twirl)
# Replace the twirling group on every Twirl annotation
updated = replace_annotations(
twirl_only, lambda a: [Twirl(group="local_c1")] if isinstance(a, Twirl) else [a]
)
Die Allzweckfunktion map_annotations() akzeptiert jeden Callable, der eine Liste von Annotationen auf eine neue Liste abbildet, und gibt dir die volle Kontrolle über Transformationen pro Box. strip_annotations() ist ein Komfort-Wrapper, der alle Annotationen von jeder Box entfernt.
Für Massenänderungen, etwa das Hinzufügen von InjectNoise zu allen Gate-Boxen, ist der Parameter inject_noise_targets von generate_boxing_pass_manager() die kompakteste Option. Diese Hilfsfunktionen sind nützlich für gezielte Änderungen pro Box, die die Parameter des Pass-Managers nicht abdecken.
Einen Circuit erstellen
Jeder von generate_boxing_pass_manager() erzeugte Pass-Manager liefert Circuits, die sich mit build() erfolgreich in ein Template/Samplex-Paar umwandeln lassen. Der folgende Code ruft zum Beispiel die Funktion build() auf einem Circuit auf, der von einem Boxing-Pass-Manager erzeugt wurde.
from samplomatic import build
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
template, samplex = build(transpiled_circuit)
Damit sich jeder transpilierte Circuit erfolgreich erstellen lässt, wissen die Pass-Manager, wie sie bei Bedarf zusätzliche Boxen einfügen. Betrachte zum Beispiel den folgenden Circuit, der mit einem nicht gemessenen Qubit 0 endet.
circuit_with_unmeasured_qubit = QuantumCircuit(4, 3)
circuit_with_unmeasured_qubit.cz(0, 1)
circuit_with_unmeasured_qubit.cz(2, 3)
for qubit in range(4):
circuit_with_unmeasured_qubit.rz(Parameter(f"th_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit_with_unmeasured_qubit.measure(range(1, 4), range(3))
circuit_with_unmeasured_qubit.draw("mpl", scale=0.8)
Würde man linksgedressed Boxen um die Gates und Messungen zeichnen, ergäbe das einen Circuit mit nicht eingesammelten virtuellen Gates auf Qubit 0, und der Aufruf von build() auf diesem Circuit würde zu einem Fehler führen. Um das zu vermeiden, können die von generate_boxing_pass_manager() zurückgegebenen Pass-Manager rechtsgedressed Boxen hinzufügen, die als Sammler dienen. Im folgenden Code wird zum Beispiel Qubit 0 durch eine rechtsgedressed Box abgeschlossen, die das nicht eingesammelte virtuelle Gate aufnimmt. Die Single-Qubit-Gates auf Qubit 0 werden ebenfalls in die Box gelegt, um die Tiefe des resultierenden Circuits zu minimieren.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)
In einem weiteren Beispiel wird eine rechtsgedressed Box hinzugefügt, um die virtuellen Gates einzusammeln, die andernfalls wegen der nicht geboxten Messungen nicht eingesammelt blieben.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=False,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)