Lavorare con i DAG nei pass del transpiler
In Qiskit, durante le fasi di transpilazione, i circuiti sono rappresentati tramite un DAG. In generale, un DAG è composto da vertici (detti anche "nodi") e da archi diretti che collegano coppie di vertici in una direzione specifica. Questa rappresentazione viene memorizzata usando oggetti qiskit.dagcircuit.DAGCircuit, composti da singoli oggetti DagNode. Il vantaggio di questa rappresentazione rispetto a una semplice lista di gate (cioè una netlist) è che il flusso di informazioni tra le operazioni è esplicito, rendendo più facile prendere decisioni di trasformazione.
Questa guida mostra come lavorare con i DAG e come usarli per scrivere pass personalizzati del transpiler. Si parte dalla costruzione di un circuito semplice e dall'esame della sua rappresentazione DAG, per poi esplorare le operazioni di base sui DAG e implementare un pass BasicMapper personalizzato.
Costruire un circuito ed esaminare il suo DAG​
Il frammento di codice seguente illustra il DAG creando un circuito semplice che prepara uno stato di Bell e applica una rotazione in base al risultato della misura.
Versioni dei pacchetti
Il codice in questa pagina è stato sviluppato usando i seguenti requisiti. Si consiglia di usare queste versioni o versioni più recenti.
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")
Nel DAG esistono tre tipi di nodi del grafo: nodi di input per qubit/clbit (verdi), nodi di operazione (blu) e nodi di output (rossi). Ogni arco indica il flusso di dati (o dipendenza) tra due nodi. Usa la funzione qiskit.tools.visualization.dag_drawer() per visualizzare il DAG di questo circuito. (Per eseguirlo, installa la libreria Graphviz.)
# Convert to DAG
dag = circuit_to_dag(circ)
dag_drawer(dag)

Operazioni di base sui DAG​
Gli esempi di codice seguenti mostrano le operazioni più comuni sui DAG, tra cui l'accesso ai nodi, l'aggiunta di operazioni e la sostituzione di sottocircuiti. Queste operazioni costituiscono la base per la costruzione di pass del transpiler.
Ottenere tutti i nodi di operazione del DAG​
Il metodo op_nodes() restituisce una lista iterabile di oggetti DAGOpNode presenti nel circuito:
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>))]
Ogni nodo è un'istanza della 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)
Aggiungere un'operazione in fondo​
Un'operazione viene aggiunta alla fine del DAGCircuit usando il metodo apply_operation_back(). Questo accoda il gate specificato in modo che agisca sui qubit indicati dopo tutte le operazioni già presenti nel circuito.
from qiskit.circuit.library import HGate
dag.apply_operation_back(HGate(), qargs=[q[0]])
dag_drawer(dag)

Aggiungere un'operazione in testa​
Un'operazione viene aggiunta all'inizio del DAGCircuit usando il metodo apply_operation_front(). Questo inserisce il gate specificato prima di tutte le operazioni già presenti nel circuito, rendendola di fatto la prima operazione eseguita.
from qiskit.circuit.library import CCXGate
dag.apply_operation_front(CCXGate(), qargs=[q[0], q[1], q[2]])
dag_drawer(dag)

Sostituire un nodo con un sottocircuito​
Un nodo che rappresenta un'operazione specifica nel DAGCircuit viene sostituito con un sottocircuito. Prima si costruisce un nuovo sub-DAG con la sequenza di gate desiderata, poi il nodo target viene sostituito da questo sub-DAG tramite substitute_node_with_dag(), preservando le connessioni con il resto del circuito.
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)

Completate tutte le trasformazioni, il DAG può essere riconvertito in un normale oggetto QuantumCircuit. È così che funziona la pipeline del transpiler: viene preso un circuito, elaborato in forma DAG, e come output viene prodotto un circuito trasformato.
from qiskit.converters import dag_to_circuit
new_circ = dag_to_circuit(dag)
circuit_drawer(new_circ, output="mpl")
Implementare un pass BasicMapper​
La struttura DAG può essere sfruttata per scrivere pass del transpiler. Nell'esempio seguente viene implementato un pass BasicMapper per mappare un circuito arbitrario su un dispositivo con connettività dei qubit limitata. Per ulteriori indicazioni, consulta la guida sulla scrittura di pass personalizzati del transpiler.
Il pass è definito come TransformationPass, il che significa che modifica il circuito. Lo fa scorrendo il DAG strato per strato, verificando se ogni istruzione soddisfa i vincoli imposti dalla coupling map del dispositivo. Se viene rilevata una violazione, viene determinato un percorso di swap e i gate SWAP necessari vengono inseriti di conseguenza.
Quando si crea un pass del transpiler, la prima decisione riguarda la scelta tra far ereditare il pass da TransformationPass o da AnalysisPass. I pass di trasformazione sono progettati per modificare il circuito, mentre i pass di analisi servono solo a estrarre informazioni da usare nei pass successivi. La funzionalità principale viene poi implementata nel metodo run(dag). Infine, il pass dovrebbe essere registrato all'interno del modulo qiskit.transpiler.passes.
In questo pass specifico, il DAG viene percorso strato per strato (dove ogni strato contiene operazioni che agiscono su insiemi disgiunti di qubit e possono quindi essere eseguite in modo indipendente). Per ogni operazione, se i vincoli della coupling map non sono rispettati, viene identificato un percorso di swap adeguato e gli swap necessari vengono inseriti per portare i qubit coinvolti in adiacenza.
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
Il pass può ora essere testato su un piccolo circuito di esempio. Viene costruito un pass manager con il pass appena definito. Il circuito di esempio viene poi fornito a questo pass manager, e come output si ottiene un nuovo circuito trasformato.
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")
Passi successivi​
- Consulta la guida sulla creazione di un pass personalizzato del transpiler
- Scopri come creare e transpilare su backend personalizzati
- Prova la guida Confrontare le impostazioni del transpiler.
- Consulta la documentazione API di DAG Circuit.