Travailler avec les DAGs dans les passes du transpileur
Dans Qiskit, au sein des étapes de transpilation, les circuits sont représentés sous forme de DAG. En général, un DAG est composé de sommets (aussi appelés « nœuds ») et d'arêtes orientées qui relient des paires de sommets dans une direction particulière. Cette représentation est stockée sous forme d'objets qiskit.dagcircuit.DAGCircuit eux-mêmes composés d'objets DagNode individuels. L'avantage de cette représentation par rapport à une simple liste de portes (c'est-à-dire une netlist) est que le flux d'information entre les opérations est explicite, ce qui facilite la prise de décisions lors des transformations.
Ce guide montre comment travailler avec les DAGs et comment les utiliser pour écrire des passes de transpileur personnalisées. Il commence par la construction d'un circuit simple et l'examen de sa représentation en DAG, puis explore les opérations de base sur les DAGs et implémente une passe BasicMapper personnalisée.
Construire un circuit et examiner son DAG
L'extrait de code ci-dessous illustre le DAG en créant un circuit simple qui prépare un état de Bell et applique une rotation en fonction du résultat de la mesure.
Versions des packages
Le code de cette page a été développé avec les dépendances suivantes. Nous recommandons d'utiliser ces versions ou des versions plus récentes.
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")
Dans le DAG, il existe trois types de nœuds : les nœuds d'entrée qubit/clbit (en vert), les nœuds d'opération (en bleu) et les nœuds de sortie (en rouge). Chaque arête représente le flux de données (ou la dépendance) entre deux nœuds. Utilise la fonction qiskit.tools.visualization.dag_drawer() pour visualiser le DAG de ce circuit. (Installe la bibliothèque Graphviz pour l'exécuter.)
# Convert to DAG
dag = circuit_to_dag(circ)
dag_drawer(dag)

Opérations de base sur les DAGs
Les exemples de code ci-dessous illustrent les opérations courantes avec les DAGs, notamment l'accès aux nœuds, l'ajout d'opérations et la substitution de sous-circuits. Ces opérations constituent la base de la création de passes de transpileur.
Obtenir tous les nœuds d'opération du DAG
La méthode op_nodes() retourne une liste itérable d'objets DAGOpNode présents dans le circuit :
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>))]
Chaque nœud est une instance de la classe 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)
Ajouter une opération à la fin
Une opération est ajoutée à la fin du DAGCircuit grâce à la méthode apply_operation_back(). Cela ajoute la porte spécifiée pour agir sur les qubits donnés, après toutes les opérations existantes dans le circuit.
from qiskit.circuit.library import HGate
dag.apply_operation_back(HGate(), qargs=[q[0]])
dag_drawer(dag)

Ajouter une opération au début
Une opération est ajoutée au début du DAGCircuit grâce à la méthode apply_operation_front(). Cela insère la porte spécifiée avant toutes les opérations existantes dans le circuit, ce qui en fait effectivement la première opération exécutée.
from qiskit.circuit.library import CCXGate
dag.apply_operation_front(CCXGate(), qargs=[q[0], q[1], q[2]])
dag_drawer(dag)

Substituer un nœud par un sous-circuit
Un nœud représentant une opération spécifique dans le DAGCircuit est remplacé par un sous-circuit. On commence par construire un nouveau sous-DAG avec la séquence de portes souhaitée, puis le nœud cible est substitué par ce sous-DAG via substitute_node_with_dag(), en préservant les connexions avec le reste du circuit.
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)

Une fois toutes les transformations effectuées, le DAG peut être reconverti en objet QuantumCircuit classique. C'est ainsi que fonctionne le pipeline du transpileur : un circuit est pris en entrée, traité sous forme de DAG, et un circuit transformé est produit en sortie.
from qiskit.converters import dag_to_circuit
new_circ = dag_to_circuit(dag)
circuit_drawer(new_circ, output="mpl")
Implémenter une passe BasicMapper
La structure DAG peut être exploitée pour écrire des passes de transpileur. Dans l'exemple ci-dessous, une passe BasicMapper est implémentée pour faire correspondre un circuit arbitraire à un dispositif avec une connectivité de qubits restreinte. Pour plus d'informations, consulte le guide sur l'écriture de passes de transpileur personnalisées.
La passe est définie comme une TransformationPass, ce qui signifie qu'elle modifie le circuit. Pour ce faire, elle parcourt le DAG couche par couche, en vérifiant si chaque instruction respecte les contraintes imposées par la carte de couplage du dispositif. Si une violation est détectée, un chemin de swap est déterminé et les portes SWAP nécessaires sont insérées en conséquence.
Lors de la création d'une passe de transpileur, la première décision consiste à choisir si la passe doit hériter de TransformationPass ou de AnalysisPass. Les passes de transformation sont conçues pour modifier le circuit, tandis que les passes d'analyse sont destinées uniquement à extraire des informations à l'usage des passes suivantes. La fonctionnalité principale est ensuite implémentée dans la méthode run(dag). Enfin, la passe doit être enregistrée dans le module qiskit.transpiler.passes.
Dans cette passe spécifique, le DAG est parcouru couche par couche (où chaque couche contient des opérations agissant sur des ensembles disjoints de qubits et pouvant donc être exécutées indépendamment). Pour chaque opération, si les contraintes de la carte de couplage ne sont pas respectées, un chemin de swap approprié est identifié et les swaps nécessaires sont insérés pour rapprocher les qubits concernés.
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
La passe peut maintenant être testée sur un petit exemple de circuit. Un gestionnaire de passes est construit avec la nouvelle passe incluse. L'exemple de circuit est ensuite fourni à ce gestionnaire de passes, et un nouveau circuit transformé est obtenu en sortie.
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")
Prochaines étapes
- Consulte le guide sur la création d'une passe de transpileur personnalisée
- Apprends à créer des backends personnalisés et à transpiler vers ceux-ci
- Essaie le guide Comparer les paramètres du transpileur.
- Consulte la documentation de l'API DAG Circuit.