Writing patterns
Note
If your pattern works for GraphCircuit
, you can
use this pattern for PatternManager
and vice versa.
A pattern is a list of gates. A gate is composed of:
name: name of the gate
qubits: list of qubits on which the gate acts
parameters (optional): some gates are defined by parameters, such as rotation angles
A gate is defined by a tuple of 2 or more arguments. For instance, the following gate can be used in a pattern definition:
from math import pi
# H gate
H_gate = ("H", [0])
# PH gate
PH_gate = ("PH", [0], pi / 2)
Parameters in pattern:
Parameters can be added to the pattern. For instance,
GraphCircuit
may replace all \(R_Z(\pi)\) by a
\(Z\) gate.
# Import pi
from numpy import pi
# Define two patterns
left_pattern = [("RZ", [0], pi)]
right_pattern = [("Z", [0])]
# Replace all patterns
while graph.replace_pattern(left_pattern, right_pattern):
continue
In our example, all \(R_Z(\pi)\) gates are replaced by a \(Z\)
gate. Our pattern is only matching with a gate having the same parameter
\(\pi\). The use of abstract variable can be useful to match a pattern
with any parameter. The class VAR
is used
to match with any parameter. For instance, the following code is used to
merge two rotation gates using abstract parameters. The rule is
\(R_Z(x) - R_Z(y) = R_Z(x + y)\).
# Import VAR
from qat.pbo import VAR
# Create VAR objects
x = VAR()
y = VAR()
# Define two patterns
left_pattern = [("RZ", [0], x), ("RZ", [0], y)]
right_pattern = [("RZ", [0], x + y)]
# Replace all patterns
while graph.replace_pattern(left_pattern, right_pattern):
continue
In the previous example, any rotation will match with the abstract variable.
Moreover, an abstract variable can be used several times in the same pattern. The following code is used to remove patterns \(R_Z(x) - R_Z(-x)\).
# Import VAR
from qat.pbo import VAR
# Create VAR objects
x = VAR()
# Define two patterns
left_pattern = [("RZ", [0], x), ("RZ", [0], -x)]
right_pattern = []
# Replace all patterns
while graph.replace_pattern(left_pattern, right_pattern):
continue
You can apply any function on your abstract variable, but the function
should be casted into a function accepting abstract variables. The method
qat.pbo.VAR.add_function()
can be used to cast any function into a
function accepting abstract variables:
# Import VAR
from qat.pbo import VAR
from math import pi
# Define function to update the angle
@VAR.add_function
def update_angle(angle):
while angle > pi:
angle -= 2 * pi
while angle < -pi:
angle += 2 * pi
return angle
# Define pattern
x = VAR()
y = VAR()
left_pattern = [("RZ", [0], x), ("RZ", [0], y)]
right_pattern = [("RZ", [0], update_angle(x + y))]
Note
When you use an instance of PatternManager
, each
pattern can be a left-hand side pattern. If the pattern cannot be
a left hand-side pattern (i.e. if this pattern does not respect
constraints of a left-hand side pattern), this pattern will never
be replaced.
If you use variables for the left-hand side pattern, the optimizer should be
able to set trivially a value for each parameter. A VAR
object
is either a root or a formula:
if the variable is created using the default constructor (i.e. the code used is
my_var = VAR()
), the variable is a root.otherwise, if the variable is created using an expression (i.e. the code used looks like
my_var = f(x_1, ...)
), the variable is a formula. In our example, our formula depends onx_1, ...
.
The optimizer can set trivially a value for each parameter if and only if each root parameter from which the pattern depends appears in the pattern. For instance, the following code defines compliant left-hand side patterns and not compliant ones.
# Import
from qat.pbo import VAR
from math import log
# Create VAR object
x = VAR()
log_var = VAR.add_function(log)
# Accepted pattern (left_pattern)
[("RZ", [0], -x), ("RZ", [0], x)]
[("RZ", [0], log_var(x)), "RZ", [0], x)]
# Not accepted pattern (left_pattern)
[("RZ", [0], log_var(x))] # The optimizer will not be able to set a value
# to x
[("RZ", [0], -x)] # The optimizer will not be able to set a value to x
# -x is seen as f(x) by the optimizer
Some values may be prohibited. For instance, if a hardware accepts only \(R_X(x), \; \forall x \in \{ \pm \pi ; \pm \frac{\pi}{2} \}\), then other \(R_X\) gates have to be replaced. To perform these changes, some values are going to be prohibited.
# Import
from qat.pbo import VAR
from numpy import pi
# Define a VAR object
prohibited_values = [pi, -pi, pi/2, -pi/2]
x = VAR()
# Prohibit values
for angle in prohibited_values:
x.add_prohibited_value(angle)
# Define two patterns
left_pattern = [("RX", [0], x)]
right_pattern = [("H", [0]), ("RZ", [0], x), ("H", [0])]
# Replace all patterns
while graph.replace_pattern(left_pattern, right_pattern):
continue
In our example, the left-hand side pattern will match with any
"RX"
having an angle different from \(\pm \pi\) and
\(\pm \frac{\pi}{2}\).
Qubits of a gate:
The qubits of a gate are defined by a list or a set of integers. In most of the cases, the order of the qubits is very important, so a list must be used. In few cases, the order of qubits does not matter (for instance, a SWAP gate acting on qubit 0 and 1 is equal to a SWAP gate acting on qubit 1 and 0). If the order of qubits does not matter, please use a set.
When the qubits are defined by a set
, the complexity of searching
a pattern is multiplied by \(q!\) (where \(q\) is the number of
qubits) so please try limit the number of set
is the pattern.
# Non-optimal left pattern
[("SWAP", {0, 1}), ("SWAP", {0, 1})]
# Optimal left pattern
[("SWAP", {0, 1}), ("SWAP", [0, 1])]
In our example, the two patterns are equivalent but since the second pattern
uses only one set
, searching the second pattern is faster than
searching the first one.
If a gate can be used for different arities (e.g. a QFT can be used for any
size of register), you can use AbstractRegister
to match
with any size of register. The use of an abstract register increases the
complexity. By default, abstract registers are disjoint.
Warning
The use of AbstractRegister
is only working for
GraphCircuit
. Using an abstract register with
PatternManager
raises errors.
Name of a gate:
A gate is defined using the name of the gate. In our pattern data model,
the name is a string. One can add prefix to this gate to control or to get
the dagger of this gate. For instance, the gate "PH"
is a phase
gate, the gate "C-PH"
is a controlled phase gate. The following
prefixes are defined:
"C-"
- controlled gate
"D-"
- dagger of the gate
"T-"
- transposed gate
"S-"
- conjugated gate
Warning
"C-X"
will not match with "CNOT"
. If a gate has a name,
the name will not be decomposed. "CNOT"
is a correct name, so the
name will not be decomposed into "C-X"
.
Regular expressions could be used to match any gates. For instance, a user may want to merge two controlled phase gates:
from qat.pbo import GateName, AbstractRegister, VAR
# Define name
ctrl_phase = GateName("(C-)*PH")
register = AbstractRegister()
x = VAR()
y = VAR()
# Define left and right patterns
left_pattern = [(ctrl_phase, register, x), (ctrl_phase, register, y]
right_pattern = [(ctrl_phase, register, x + y)]