Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

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 RZR_Z 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.5.2
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"qiskit": "qiskit"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
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")

Output of the previous code cell

Nel DAG, ci sono tre tipi di nodi del grafo: nodi di input 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. (Installa la libreria Graphviz per eseguirlo.)

# Convert to DAG
dag = circuit_to_dag(circ)
dag_drawer(dag)

Output of the previous code cell

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 0x12559c410>, 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 0x12559f490>, 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 aggiunge il gate specificato in modo che agisca sui qubit indicati dopo tutte le operazioni esistenti nel circuito.

from qiskit.circuit.library import HGate

dag.apply_operation_back(HGate(), qargs=[q[0]])
dag_drawer(dag)

Output of the previous code cell

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 esistenti nel circuito, rendendolo 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)

Output of the previous code cell

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 di destinazione viene sostituito da questo sub-DAG usando 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)

Output of the previous code cell

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")

Output of the previous code cell

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")

Output of the previous code cell

Passi successivi​