Mit DAGs in Transpiler-Passes arbeiten
In Qiskit werden Schaltkreise innerhalb der Transpiler-Stufen mittels eines DAG (Directed Acyclic Graph) dargestellt. Im Allgemeinen besteht ein DAG aus Knoten (auch "Vertices" genannt) und gerichteten Kanten, die Knotenpaare in einer bestimmten Orientierung verbinden. Diese Darstellung wird in qiskit.dagcircuit.DAGCircuit-Objekten gespeichert, die aus einzelnen DagNode-Objekten zusammengesetzt sind. Der Vorteil dieser Darstellung gegenüber einer reinen Liste von Gates (d.h. einer Netzliste) besteht darin, dass der Informationsfluss zwischen Operationen explizit ist, was Transformationsentscheidungen erleichtert.
Dieser Leitfaden demonstriert, wie du mit DAGs arbeitest und diese zur Entwicklung eigener Transpiler-Passes nutzt. Er beginnt mit dem Aufbau eines einfachen Schaltkreises und der Untersuchung seiner DAG-Darstellung, erkundet dann grundlegende DAG-Operationen und implementiert einen benutzerdefinierten BasicMapper-Pass.
Einen Schaltkreis erstellen und seinen DAG untersuchen
Das folgende Code-Beispiel veranschaulicht den DAG durch die Erstellung eines einfachen Schaltkreises, der einen Bell-Zustand vorbereitet und eine -Rotation anwendet, abhängig vom Messergebnis.
Paketversionen
Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen die Verwendung dieser oder neuerer Versionen.
qiskit[all]~=2.3.0
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.converters import circuit_to_dag
from qiskit.visualization import circuit_drawer
from qiskit.visualization.dag_visualization import dag_drawer
# Create circuit
q = QuantumRegister(3, "q")
c = ClassicalRegister(3, "c")
circ = QuantumCircuit(q, c)
circ.h(q[0])
circ.cx(q[0], q[1])
circ.measure(q[0], c[0])
# Qiskit 2.0 uses if_test instead of c_if
with circ.if_test((c, 2)):
circ.rz(0.5, q[1])
circuit_drawer(circ, output="mpl")
Im DAG gibt es drei Arten von Graphknoten: Qubit-/Clbit-Eingabeknoten (grün), Operationsknoten (blau) und Ausgabeknoten (rot). Jede Kante zeigt den Datenfluss (oder die Abhängigkeit) zwischen zwei Knoten an. Verwende die Funktion qiskit.tools.visualization.dag_drawer(), um den DAG dieses Schaltkreises zu visualisieren. (Installiere dazu die Graphviz-Bibliothek.)
# Convert to DAG
dag = circuit_to_dag(circ)
dag_drawer(dag)

Grundlegende DAG-Operationen
Die folgenden Code-Beispiele demonstrieren gängige Operationen mit DAGs, einschließlich des Zugriffs auf Knoten, des Hinzufügens von Operationen und des Ersetzens von Teilschaltkreisen. Diese Operationen bilden die Grundlage für den Aufbau von Transpiler-Passes.
Alle Operationsknoten im DAG abrufen
Die Methode op_nodes() gibt eine iterierbare Liste von DAGOpNode-Objekten im Schaltkreis zurück:
dag.op_nodes()
[DAGOpNode(op=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qargs=(<Qubit register=(3, "q"), index=0>,), cargs=()),
DAGOpNode(op=Instruction(name='cx', num_qubits=2, num_clbits=0, params=[]), qargs=(<Qubit register=(3, "q"), index=0>, <Qubit register=(3, "q"), index=1>), cargs=()),
DAGOpNode(op=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qargs=(<Qubit register=(3, "q"), index=0>,), cargs=(<Clbit register=(3, "c"), index=0>,)),
DAGOpNode(op=Instruction(name='if_else', num_qubits=1, num_clbits=3, params=[<qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7f912f47db10>, None]), qargs=(<Qubit register=(3, "q"), index=1>,), cargs=(<Clbit register=(3, "c"), index=0>, <Clbit register=(3, "c"), index=1>, <Clbit register=(3, "c"), index=2>))]
Jeder Knoten ist eine Instanz der Klasse DAGOpNode:
node = dag.op_nodes()[3]
print("node name:", node.name)
print("op:", node.op)
print("qargs:", node.qargs)
print("cargs:", node.cargs)
print("condition:", node.op.condition)
node name: if_else
op: Instruction(name='if_else', num_qubits=1, num_clbits=3, params=[<qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7f912f4ceed0>, None])
qargs: (<Qubit register=(3, "q"), index=1>,)
cargs: (<Clbit register=(3, "c"), index=0>, <Clbit register=(3, "c"), index=1>, <Clbit register=(3, "c"), index=2>)
condition: (ClassicalRegister(3, 'c'), 2)
Eine Operation am Ende hinzufügen
Eine Operation wird mit der Methode apply_operation_back() am Ende des DAGCircuit hinzugefügt. Dadurch wird das angegebene Gate so angefügt, dass es nach allen vorhandenen Operationen im Schaltkreis auf die angegebenen Qubits wirkt.
from qiskit.circuit.library import HGate
dag.apply_operation_back(HGate(), qargs=[q[0]])
dag_drawer(dag)

Eine Operation am Anfang hinzufügen
Eine Operation wird mit der Methode apply_operation_front() am Anfang des DAGCircuit hinzugefügt. Dadurch wird das angegebene Gate vor allen vorhandenen Operationen im Schaltkreis eingefügt, sodass es effektiv als erste Operation ausgeführt wird.
from qiskit.circuit.library import CCXGate
dag.apply_operation_front(CCXGate(), qargs=[q[0], q[1], q[2]])
dag_drawer(dag)

Einen Knoten durch einen Teilschaltkreis ersetzen
Ein Knoten, der eine bestimmte Operation im DAGCircuit repräsentiert, wird durch einen Teilschaltkreis ersetzt. Zunächst wird ein neuer Sub-DAG mit der gewünschten Abfolge von Gates erstellt, dann wird der Zielknoten mit substitute_node_with_dag() durch diesen Sub-DAG ersetzt, wobei die Verbindungen zum Rest des Schaltkreises erhalten bleiben.
from qiskit.dagcircuit import DAGCircuit
from qiskit.circuit.library import CHGate, U2Gate, CXGate
# Build sub-DAG
mini_dag = DAGCircuit()
p = QuantumRegister(2, "p")
mini_dag.add_qreg(p)
mini_dag.apply_operation_back(CHGate(), qargs=[p[1], p[0]])
mini_dag.apply_operation_back(U2Gate(0.1, 0.2), qargs=[p[1]])
# Replace CX with mini_dag
cx_node = dag.op_nodes(op=CXGate).pop()
dag.substitute_node_with_dag(cx_node, mini_dag, wires=[p[0], p[1]])
dag_drawer(dag)

Nachdem alle Transformationen abgeschlossen sind, kann der DAG wieder in ein reguläres QuantumCircuit-Objekt umgewandelt werden. So funktioniert die Transpiler-Pipeline: Ein Schaltkreis wird entgegengenommen, in DAG-Form verarbeitet, und ein transformierter Schaltkreis wird als Ausgabe erzeugt.
from qiskit.converters import dag_to_circuit
new_circ = dag_to_circuit(dag)
circuit_drawer(new_circ, output="mpl")
Einen BasicMapper-Pass implementieren
Die DAG-Struktur kann zum Schreiben von Transpiler-Passes genutzt werden. Im folgenden Beispiel wird ein BasicMapper-Pass implementiert, der einen beliebigen Schaltkreis auf ein Gerät mit eingeschränkter Qubit-Konnektivität abbildet. Weitere Hinweise findest du in der Anleitung zum Schreiben benutzerdefinierter Transpiler-Passes.
Der Pass ist als TransformationPass definiert, das heißt, er modifiziert den Schaltkreis. Er durchläuft den DAG Schicht für Schicht und prüft, ob jede Instruktion die Einschränkungen der Coupling Map des Geräts erfüllt. Wird ein Verstoß erkannt, wird ein Swap-Pfad bestimmt und die erforderlichen SWAP-Gates entsprechend eingefügt.
Beim Erstellen eines Transpiler-Passes muss zunächst entschieden werden, ob der Pass von TransformationPass oder AnalysisPass erben soll. Transformations-Passes sind dafür konzipiert, den Schaltkreis zu modifizieren, während Analyse-Passes nur Informationen für nachfolgende Passes extrahieren. Die Hauptfunktionalität wird dann in der run(dag)-Methode implementiert. Abschließend sollte der Pass im qiskit.transpiler.passes-Modul registriert werden.
In diesem speziellen Pass wird der DAG Schicht für Schicht durchlaufen (wobei jede Schicht Operationen enthält, die auf disjunkte Mengen von Qubits wirken und daher unabhängig voneinander ausgeführt werden können). Für jede Operation wird, falls die Coupling-Map-Einschränkungen nicht erfüllt sind, ein geeigneter Swap-Pfad ermittelt und die erforderlichen Swaps eingefügt, um die beteiligten Qubits in Nachbarschaft zu bringen.
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler import Layout
from qiskit.circuit.library import SwapGate
class BasicSwap(TransformationPass):
def __init__(self, coupling_map, initial_layout=None):
super().__init__()
self.coupling_map = coupling_map
self.initial_layout = initial_layout
def run(self, dag):
new_dag = DAGCircuit()
for qreg in dag.qregs.values():
new_dag.add_qreg(qreg)
for creg in dag.cregs.values():
new_dag.add_creg(creg)
if self.initial_layout is None:
self.initial_layout = Layout.generate_trivial_layout(
*dag.qregs.values()
)
current_layout = self.initial_layout.copy()
for layer in dag.serial_layers():
subdag = layer["graph"]
for gate in subdag.two_qubit_ops():
q0, q1 = gate.qargs
p0 = current_layout[q0]
p1 = current_layout[q1]
if self.coupling_map.distance(p0, p1) != 1:
path = self.coupling_map.shortest_undirected_path(p0, p1)
for i in range(len(path) - 2):
wire1, wire2 = path[i], path[i + 1]
qubit1 = current_layout[wire1]
qubit2 = current_layout[wire2]
new_dag.apply_operation_back(
SwapGate(), qargs=[qubit1, qubit2]
)
current_layout.swap(wire1, wire2)
new_dag.compose(
subdag, qubits=current_layout.reorder_bits(new_dag.qubits)
)
return new_dag
Der Pass kann nun an einem kleinen Beispielschaltkreis getestet werden. Ein Pass-Manager wird mit dem neu definierten Pass erstellt. Der Beispielschaltkreis wird diesem Pass-Manager übergeben, und ein neuer, transformierter Schaltkreis wird als Ausgabe erhalten.
from qiskit.transpiler import CouplingMap, PassManager
from qiskit import QuantumRegister, QuantumCircuit
q = QuantumRegister(7, "q")
in_circ = QuantumCircuit(q)
in_circ.h(q[0])
in_circ.cx(q[0], q[4])
in_circ.cx(q[2], q[3])
in_circ.cx(q[6], q[1])
in_circ.cx(q[5], q[0])
in_circ.rz(0.1, q[2])
in_circ.cx(q[5], q[0])
coupling = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]
coupling_map = CouplingMap(couplinglist=coupling)
pm = PassManager()
pm.append(BasicSwap(coupling_map))
out_circ = pm.run(in_circ)
in_circ.draw(output="mpl")
out_circ.draw(output="mpl")
Nächste Schritte
- Lies die Anleitung zum Erstellen eines benutzerdefinierten Transpiler-Passes
- Erfahre, wie du benutzerdefinierte Backends erstellst und gegen sie transpilierst
- Probiere die Anleitung Transpiler-Einstellungen vergleichen aus.
- Sieh dir die DAG Circuit API-Dokumentation an.