Low-level optimization

The qat.pbo module provides different tools to optimize a quantum circuit using rule-based rewriting. This page presents a low level optimization tool, GraphCircuit. On this page, only simple patterns are used. Nevertheless, this tool accepts complex patterns: to learn more about patterns, please refer to Writing patterns.

The notebook Replacing patterns in a circuit may help to better understand our syntax to describe a pattern, and how to use a pattern with GraphCircuit tool. This notebook optimizes a circuit by replacing some patterns by equivalent ones.

Introduction

The rewriting procedure is described via definitions and rewriting rules. A rule is composed of a left-hand side pattern and a right-hand side pattern. A pattern is a simple (i.e. small) subcircuit and is defined by a list of gates (a gate is defined by a tuple composed of a name, a list of qubits on which the gate acts and [optionally] parameters). The class GraphCircuit will look for the left pattern in the circuit and replace it by the right one.

For instance, the following code is used to remove consecutive H gates:

from qat.pbo import GraphCircuit
from qat.lang.AQASM import Program, H, X

#
# Write circuit
#

# Define initial circuit (X - H - H circuit)
prog = Program()
qubit = prog.qalloc(1)
prog.apply(X, qubit)
prog.apply(H, qubit)
prog.apply(H, qubit)
circ = prog.to_circ()

#
# Optimize circuit
#

# Create a graph object and load circuit
graph = GraphCircuit()
graph.load_circuit(circ)

# Define two patterns
left_pattern = [("H", [0]), ("H", [0])]
right_pattern = []

# Replace left_pattern by right_pattern, i.e. the old one by the new one
graph.replace_pattern(left_pattern, right_pattern)

The pattern left_pattern describes a pattern composed of two "H" gates acting on the same qubit - qubit 0.

The first found subcircuit of the initial circuit matching with left_pattern will be replaced by right_pattern thanks to the method replace_pattern(). The pattern can be found on any qubit of the circuit (not only the qubit 0).

Method replace_pattern() will replace only the first occurence of left_pattern (and return True if the pattern has been found). To replace all occurences of left_pattern, one can use:

while graph.replace_pattern(left_pattern, right_pattern):
    continue

The method to_circ() can be used to transform a GraphCircuit back into a Circuit.

# Get the optimized circuit
optimized_circ = graph.to_circ()

The function to_circ() only works if all gates composing the graph can be cast into a known quantum gate. To add your own quantum gate, please use add_abstract_gate(). See code block below for an example:

from qat.lang.AQASM import Program, X, H
from qat.pbo import GraphCircuit

# Define program
prog = Program()
qbit = prog.qalloc(1)
prog.apply(X, qbit)
prog.apply(H, qbit)
circ = prog.to_circ()

# Replace pattern
left_pattern = [("X", [0]), ("H", [0])]
right_pattern = [("HZ", [0])]

graph = GraphCircuit()
graph.load_circuit(circ)
graph.replace_pattern(left_pattern, right_pattern)

# graph.to_circ() won't work since "HZ" gate is not known

from qat.lang.AQASM import AbstractGate
graph.add_abstract_gate(AbstractGate("HZ", [], 1))

# graph.to_circ() is now working
optimized_circ = graph.to_circ()

In the previous code, a gate "HZ" is used to replace the \(X - H\) pattern. The gate "HZ" is not a common gate, so GraphCircuit cannot transform this name into a real quantum gate. One needs to define an abstract gate called "HZ" to help GraphCircuit to cast "HZ" into a quantum gate.

Advanced matching

Targeting patterns through their index

All instances of GraphCircuit are graphs where each node corresponds to a gate. Each node / gate has an index. Using these indices, one can select which pattern should be replaced.

The method find_pattern() must be used to find patterns in a graph. This function returns a list of indices for this pattern. When a pattern is composed of several gates, its corresponds to the index of the first gate composing it.

from qat.lang.AQASM import Program, H
from qat.pbo import GraphCircuit

# Define program
prog = Program()
qbits = prog.qalloc(2)
prog.apply(H, qbits[0])  # Pattern H - H on qubit 0
prog.apply(H, qbits[0])
prog.apply(H, qbits[1])  # Pattern H - H on qubit 1
prog.apply(H, qbits[1])

# Init graph
graph = GraphCircuit()
graph.load_circuit(prog.to_circ())

# Find pattern
graph.find_pattern([("H", [0]), ("H", [0])])
# Possible output -> [1, 3]

Pattern indices can be used, in a few different ways, to select which pattern one may want to replace. See code block below, for instance:

#
# Remove a single pattern
#

# Remove the first found pattern (pattern of index 1)
graph.replace_pattern([("H", [0]), ("H", [0])], [], pos=1)

# Remove the second found pattern (pattern of index 3)
graph.replace_pattern([("H", [0]), ("H", [0])], [], pos=3)

#
# Remove all patterns
#

# Case 1
graph.replace_pattern([("H", [0]), ("H", [0])], [], pos=[1, 3])

# Case 2
graph.replace_pattern([("H", [0]), ("H", [0])], [], pos=all)

Warning

On the previous example, please execute each line separately. If you execute several lines, you may ask the optimizer to remove several times the same pattern: it will not work.

Patterns specific to the beginning/end of a circuit

Not all circuit simplification patterns concern unitary gates in the middle of a circuit. Some patterns may be only valid at the beginning, for instance, because of the specific \(|0\rangle ^ {\otimes n}\) value of all qubits. For instance, when the initial state of a circuit is \(|0\rangle ^ {\otimes n}\), swap gates present at the beginning of the circuit are useless. For example:

from qat.lang.AQASM import Program, H, SWAP
from qat.pbo import GraphCircuit

# Create a circuit
prog = Program()
qbits = prog.qalloc(2)
prog.apply(SWAP, qbits)
prog.apply(H, qbits[0])
prog.apply(SWAP, qbits)
circ = prog.to_circ()

# Remove useless SWAP at the beginning of the circuit
graph = GraphCircuit()
graph.load_circuit(circ)

# Select pattern connected to the beginning of the circuit on qubits
# 0 and 1
graph.replace_pattern([("SWAP", [0, 1])], [], begin=[0, 1])

On this example, only patterns composed of a single "SWAP" connected to the beginning of the circuit are removed.

The keyword end can be used to select patterns connected to the ending of the circuit.