Pular para o conteúdo principal

Trabalhar com DAGs em passes do transpilador

No Qiskit, durante as etapas de transpilação, os circuitos são representados utilizando um DAG. Em geral, um DAG é composto por vértices (também chamados de "nós") e arestas dirigidas que conectam pares de vértices em uma orientação específica. Essa representação é armazenada por meio de objetos qiskit.dagcircuit.DAGCircuit, que são compostos por objetos DagNode individuais. A vantagem dessa representação em relação a uma lista simples de portas (ou seja, uma netlist) é que o fluxo de informações entre operações fica explícito, facilitando as decisões de transformação.

Este guia demonstra como trabalhar com DAGs e utilizá-los para escrever passes personalizados do transpilador. Começaremos construindo um circuito simples e examinando sua representação em DAG, depois exploraremos operações básicas com DAGs e implementaremos um passe BasicMapper personalizado.

Construir um circuito e examinar seu DAG

O trecho de código abaixo ilustra o DAG criando um circuito simples que prepara um estado de Bell e aplica uma rotação RZR_Z, dependendo do resultado da medição.

Versões dos pacotes

O código nesta página foi desenvolvido com os seguintes requisitos. Recomendamos utilizar estas versões ou mais recentes.

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

Output of the previous code cell

No DAG, há três tipos de nós no grafo: nós de entrada de qubit/clbit (verde), nós de operação (azul) e nós de saída (vermelho). Cada aresta indica o fluxo de dados (ou dependência) entre dois nós. Use a função qiskit.tools.visualization.dag_drawer() para visualizar o DAG deste circuito. (Instale a biblioteca Graphviz para executar isso.)

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

Output of the previous code cell

Operações básicas com DAGs

Os exemplos de código abaixo demonstram operações comuns com DAGs, incluindo o acesso a nós, a adição de operações e a substituição de subcircuitos. Essas operações formam a base para a construção de passes do transpilador.

Obter todos os nós de operação no DAG

O método op_nodes() retorna uma lista iterável de objetos DAGOpNode no 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>))]

Cada nó é uma instância da 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)

Adicionar uma operação ao final

Uma operação é adicionada ao fim do DAGCircuit usando o método apply_operation_back(). Isso anexa a porta especificada para atuar nos qubits fornecidos, após todas as operações existentes no circuito.

from qiskit.circuit.library import HGate

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

Output of the previous code cell

Adicionar uma operação ao início

Uma operação é adicionada ao início do DAGCircuit usando o método apply_operation_front(). Isso insere a porta especificada antes de todas as operações existentes no circuito, tornando-a efetivamente a primeira operação executada.

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

Substituir um nó por um subcircuito

Um nó que representa uma operação específica no DAGCircuit é substituído por um subcircuito. Primeiro, um novo sub-DAG é construído com a sequência desejada de portas; em seguida, o nó alvo é substituído por esse sub-DAG usando substitute_node_with_dag(), preservando as conexões com o restante do 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

Após a conclusão de todas as transformações, o DAG pode ser convertido de volta em um objeto QuantumCircuit regular. É assim que o pipeline do transpilador funciona: um circuito é recebido, processado na forma de DAG e um circuito transformado é produzido como saída.

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

Implementar um passe BasicMapper

A estrutura do DAG pode ser aproveitada para escrever passes do transpilador. No exemplo abaixo, um passe BasicMapper é implementado para mapear um circuito arbitrário em um dispositivo com conectividade restrita entre qubits. Para orientações adicionais, consulte o guia sobre como escrever passes personalizados do transpilador.

O passe é definido como um TransformationPass, o que significa que ele modifica o circuito. Ele faz isso percorrendo o DAG camada por camada, verificando se cada instrução satisfaz as restrições impostas pelo mapa de acoplamento do dispositivo. Se uma violação for detectada, um caminho de swap é determinado e as portas SWAP necessárias são inseridas de acordo.

Ao criar um passe do transpilador, a primeira decisão envolve escolher se o passe deve herdar de TransformationPass ou AnalysisPass. Os passes de transformação são projetados para modificar o circuito, enquanto os passes de análise têm como objetivo apenas extrair informações para uso por passes subsequentes. A funcionalidade principal é então implementada no método run(dag). Por fim, o passe deve ser registrado no módulo qiskit.transpiler.passes.

Neste passe específico, o DAG é percorrido camada por camada (onde cada camada contém operações que atuam em conjuntos disjuntos de qubits e, portanto, podem ser executadas de forma independente). Para cada operação, se as restrições do mapa de acoplamento não forem atendidas, um caminho de swap adequado é identificado e os swaps necessários são inseridos para trazer os qubits envolvidos para adjacência.

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

Agora o passe pode ser testado em um pequeno circuito de exemplo. Um gerenciador de passes é construído com o passe recém-definido incluído. O circuito de exemplo é então fornecido a esse gerenciador de passes e um novo circuito transformado é obtido como saída.

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

Próximos passos