qml.decomposition

This module implements the infrastructure for PennyLane’s new graph-based decomposition system.

Warning

This module is experimental and is subject to change in the future.

To activate and deactivate the new experimental graph-based decomposition system, use the switches enable_graph() and disable_graph(). Whether the graph-based decomposition system is currently being used can be queried with enabled_graph(). By default, this system is disabled.

enable_graph()

A global toggle for enabling the experimental graph-based decomposition system in PennyLane (introduced in v0.41).

disable_graph()

A global toggle for disabling the experimental graph-based decomposition system in PennyLane (introduced in v0.41).

enabled_graph()

A global toggle for checking the status of the experimental graph-based decomposition system in PennyLane (introduced in v0.41).

>>> import pennylane as qml
>>> qml.decomposition.enabled_garph()
False
>>> qml.decomposition.enable_graph()
>>> qml.decomposition.enabled_garph()
True
>>> qml.decomposition.disable_graph()
>>> qml.decomposition.enabled_garph()
False

Defining Decomposition Rules

register_resources(resources[, qfunc])

Binds a quantum function to its required resources.

resource_rep(op_type, **params)

Binds an operator type with additional resource parameters.

controlled_resource_rep(base_class, ...[, ...])

Creates a CompressedResourceOp representation of a general controlled operator.

adjoint_resource_rep(base_class[, base_params])

Creates a CompressedResourceOp representation of the adjoint of an operator.

pow_resource_rep(base_class, base_params, z)

Creates a CompressedResourceOp representation of the power of an operator.

DecompositionRule(func, resources)

Represents a decomposition rule for an operator.

Resources([gate_counts])

Stores resource estimates.

In the new decomposition system, a decomposition rule must be defined as a quantum function that accepts (*op.parameters, op.wires, **op.hyperparameters) as arguments, where op is an instance of the operator type that the decomposition is for. Additionally, a decomposition rule must declare its resource requirements using the register_resources decorator:

import pennylane as qml

@qml.register_resources({qml.H: 2, qml.CZ: 1})
def my_cnot(wires):
    qml.H(wires=wires[1])
    qml.CZ(wires=wires)
    qml.H(wires=wires[1])

Inspecting and Managing Decomposition Rules

add_decomps(op_type, *decomps)

Globally registers new decomposition rules with an operator class.

list_decomps(op_type)

Lists all stored decomposition rules for an operator class.

has_decomp(op_type)

Checks whether an operator has decomposition rules defined.

PennyLane maintains a global dictionary of decomposition rules. New decomposition rules can be registered under an operator using add_decomps, and list_decomps can be called to inspect a list of known decomposition rules for a given operator. In the new system, an operator can be associated with multiple decomposition rules, and the one that leads to the most resource-efficient decomposition towards a target gate set is chosen.

Integration with the Decompose Transform

The decompose() transform takes advantage of this new graph-based decomposition algorithm when enable_graph() is present, and allows for more flexible decompositions towards any target gate set. For example, the current system does not guarentee a decomposition to the desired target gate set:

import pennylane as qml

with qml.queuing.AnnotatedQueue() as q:
    qml.CRX(0.5, wires=[0, 1])

tape = qml.tape.QuantumScript.from_queue(q)
[new_tape], _ = qml.transforms.decompose([tape], gate_set={"RX", "RY", "RZ", "CZ"})
>>> new_tape.operations
[RZ(1.5707963267948966, wires=[1]),
 RY(0.25, wires=[1]),
 CNOT(wires=[0, 1]),
 RY(-0.25, wires=[1]),
 CNOT(wires=[0, 1]),
 RZ(-1.5707963267948966, wires=[1])]

With the new system enabled, the transform produces the expected outcome.

>>> qml.decomposition.enable_graph()
>>> [new_tape], _ = qml.transforms.decompose([tape], gate_set={"RX", "RY", "RZ", "CZ"})
>>> new_tape.operations
[RX(0.25, wires=[1]), CZ(wires=[0, 1]), RX(-0.25, wires=[1]), CZ(wires=[0, 1])]

Customizing Decompositions

The new system also enables specifying custom decomposition rules. When enable_graph() is present, the decompose() transform accepts two additional keyword arguments: fixed_decomps and alt_decomps. The user can define custom decomposition rules as explained in the Defining Decomposition Rules section, and provide them to the transform via these arguments.

The fixed_decomps forces the transform to use the specified decomposition rules for certain operators, wheras the alt_decomps is used to provide alternative decomposition rules for operators that may be chosen if they lead to a more resource-efficient decomposition.

In the following example, isingxx_decomp will always be used to decompose qml.IsingXX gates; when it comes to qml.CNOT, the system will choose the most efficient decomposition rule among my_cnot1, my_cnot2, and all existing decomposition rules defined for qml.CNOT.

import pennylane as qml

qml.decomposition.enable_graph()

@qml.register_resources({qml.CNOT: 2, qml.RX: 1})
def isingxx_decomp(phi, wires, **__):
    qml.CNOT(wires=wires)
    qml.RX(phi, wires=[wires[0]])
    qml.CNOT(wires=wires)

@qml.register_resources({qml.H: 2, qml.CZ: 1})
def my_cnot1(wires, **__):
    qml.H(wires=wires[1])
    qml.CZ(wires=wires)
    qml.H(wires=wires[1])

@qml.register_resources({qml.RY: 2, qml.CZ: 1, qml.Z: 2})
def my_cnot2(wires, **__):
    qml.RY(np.pi/2, wires[1])
    qml.Z(wires[1])
    qml.CZ(wires=wires)
    qml.RY(np.pi/2, wires[1])
    qml.Z(wires[1])

@partial(
    qml.transforms.decompose,
    gate_set={"RX", "RZ", "CZ", "GlobalPhase"},
    alt_decomps={qml.CNOT: [my_cnot1, my_cnot2]},
    fixed_decomps={qml.IsingXX: isingxx_decomp},
)
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.CNOT(wires=[0, 1])
    qml.IsingXX(0.5, wires=[0, 1])
    return qml.state()
>>> qml.specs(circuit)()["resources"].gate_types
defaultdict(int, {'RZ': 12, 'RX': 7, 'GlobalPhase': 6, 'CZ': 3})

To register alternative decomposition rules under an operator to be used globally, use add_decomps(). See Inspecting and Managing Decomposition Rules for details.

Graph-based Decomposition Solver

DecompositionGraph(operations, target_gate_set)

A graph that models a decomposition problem.

The decomposition graph is a directed graph of operators and decomposition rules. Dijkstra’s algorithm is used to explore the graph and find the most efficient decomposition of a given operator towards a target gate set.

op = qml.CRX(0.5, wires=[0, 1])
graph = DecompositionGraph(
    operations=[op],
    target_gate_set={"RZ", "RX", "CNOT", "GlobalPhase"},
)
graph.solve()
>>> with qml.queuing.AnnotatedQueue() as q:
...     graph.decomposition(op)(0.5, wires=[0, 1])
>>> q.queue
[H(1), CRZ(0.5, wires=Wires([0, 1])), H(1)]
>>> graph.resource_estimate(op)
<num_gates=14, gate_counts={RZ: 6, GlobalPhase: 4, RX: 2, CNOT: 2}>