qml.transforms.decompose

decompose(tape, *, gate_set=None, stopping_condition=None, max_expansion=None, fixed_decomps=None, alt_decomps=None)[source]

Decomposes a quantum circuit into a user-specified gate set.

Note

When qml.decomposition.enable_graph() is present, this transform takes advantage of the new graph-based decomposition algorithm that allows for more flexible and resource-efficient decompositions towards any target gate set. The keyword arguments fixed_decomps and alt_decomps are only functional with this toggle present.

See also

For more information on PennyLane’s decomposition tools and features, check out the Compiling Circuits page.

Parameters:
  • tape (QuantumScript or QNode or Callable) – a quantum circuit.

  • gate_set (Iterable[str or type], Dict[type or str, float] or Callable, optional) – The target gate set specified as either (1) a sequence of operator types and/or names, (2) a dictionary mapping operator types and/or names to their respective costs, in which case the total cost will be minimized (only available when the new graph-based decomposition system is enabled), or (3) a function that returns True if the operator belongs to the target gate set (not supported with the new graph-based decomposition system). If None, the gate set is considered to be all available quantum operators.

  • stopping_condition (Callable, optional) – a function that returns True if the operator does not need to be decomposed. If None, the default stopping condition is whether the operator is in the target gate set. See the “Gate Set vs. Stopping Condition” section below for more details.

  • max_expansion (int, optional) – The maximum depth of the decomposition. Defaults to None. If None, the circuit will be decomposed until the target gate set is reached.

  • fixed_decomps (Dict[Type[Operator], DecompositionRule]) – a dictionary mapping operator types to custom decomposition rules. A decomposition rule is a quantum function decorated with register_resources(). The custom decomposition rules specified here will be used in place of the existing decomposition rules defined for this operator. This is only used when enable_graph() is present.

  • alt_decomps (Dict[Type[Operator], List[DecompositionRule]]) – a dictionary mapping operator types to lists of alternative custom decomposition rules. A decomposition rule is a quantum function decorated with register_resources(). The custom decomposition rules specified here will be considered as alternatives to the existing decomposition rules defined for this operator, and one of them may be chosen if they lead to a more resource-efficient decomposition. This is only used when enable_graph() is present.

Returns:

The decomposed circuit. The output type is explained in qml.transform.

Return type:

qnode (QNode) or quantum function (Callable) or tuple[List[QuantumScript], function]

Note

This function does not guarantee a decomposition to the target gate set. If an operation with no defined decomposition is encountered during decomposition, it will be left in the circuit even if it does not belong in the target gate set. In this case, a UserWarning will be raised. To suppress this warning, simply add the operator to the gate set. When qml.decomposition.enabled_graph(), PennyLane errors out with a DecompositionError.

See also

For decomposing into Clifford + T, check out clifford_t_decomposition().

qml.devices.preprocess.decompose is a transform that is intended for device developers. This function will decompose a quantum circuit into a set of basis gates available on a specific device architecture.

Example

Consider the following tape:

>>> ops = [qml.IsingXX(1.2, wires=(0,1))]
>>> tape = qml.tape.QuantumScript(ops, measurements=[qml.expval(qml.Z(0))])

You can decompose the circuit into a set of gates:

>>> batch, fn = qml.transforms.decompose(tape, gate_set={qml.CNOT, qml.RX})
>>> batch[0].circuit
[CNOT(wires=[0, 1]), RX(1.2, wires=[0]), CNOT(wires=[0, 1]), expval(Z(0))]

You can also apply the transform directly on a QNode:

from functools import partial

@partial(qml.transforms.decompose, gate_set={qml.Toffoli, "RX", "RZ"})
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.Hadamard(wires=[0])
    qml.Toffoli(wires=[0,1,2])
    return qml.expval(qml.Z(0))

Since the Hadamard gate is not defined in our gate set, it will be decomposed into rotations:

>>> print(qml.draw(circuit)())
0: ──RZ(1.57)──RX(1.57)──RZ(1.57)─╭●─┤  <Z>
1: ───────────────────────────────├●─┤
2: ───────────────────────────────╰X─┤

You can also use a function to build a decomposition gate set:

@partial(qml.transforms.decompose, gate_set=lambda op: len(op.wires)<=2)
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.Hadamard(wires=[0])
    qml.Toffoli(wires=[0,1,2])
    return qml.expval(qml.Z(0))

The circuit will be decomposed into single or two-qubit operators,

>>> print(qml.draw(circuit)())
0: ──H────────╭●───────────╭●────╭●──T──╭●─┤  <Z>
1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤
2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤

You can use the max_expansion argument to control the number of decomposition stages applied to the circuit. By default, the function will decompose the circuit until the desired gate set is reached.

The example below demonstrates how the user can visualize the decomposition in stages:

phase = 1
target_wires = [0]
unitary = qml.RX(phase, wires=0).matrix()
n_estimation_wires = 3
estimation_wires = range(1, n_estimation_wires + 1)

@qml.qnode(qml.device("default.qubit"))
def circuit():
    # Start in the |+> eigenstate of the unitary
    qml.Hadamard(wires=target_wires)
    qml.QuantumPhaseEstimation(
        unitary,
        target_wires=target_wires,
        estimation_wires=estimation_wires,
    )
>>> print(qml.draw(qml.transforms.decompose(circuit, max_expansion=0))())
0: ──H─╭QuantumPhaseEstimation─┤
1: ────├QuantumPhaseEstimation─┤
2: ────├QuantumPhaseEstimation─┤
3: ────╰QuantumPhaseEstimation─┤
>>> print(qml.draw(qml.transforms.decompose(circuit, max_expansion=1))())
0: ──H─╭U(M0)⁴─╭U(M0)²─╭U(M0)¹───────┤
1: ──H─╰●──────│───────│───────╭QFT†─┤
2: ──H─────────╰●──────│───────├QFT†─┤
3: ──H─────────────────╰●──────╰QFT†─┤
>>> print(qml.draw(qml.transforms.decompose(circuit, max_expansion=2))())
0: ──H──RZ(4.71)──RY(1.14)─╭X──RY(-1.14)──RZ(-3.14)─╭X──RZ(-1.57)──RZ(1.57)──RY(1.00)─╭X ···
1: ──H─────────────────────╰●───────────────────────╰●────────────────────────────────│─ ···
2: ──H────────────────────────────────────────────────────────────────────────────────╰● ···
3: ──H────────────────────────────────────────────────────────────────────────────────── ···

0: ··· ──RY(-1.00)──RZ(-6.28)─╭X──RZ(4.71)──RZ(1.57)──RY(0.50)─╭X──RY(-0.50)──RZ(-6.28)─╭X ···
1: ··· ───────────────────────│────────────────────────────────│────────────────────────│─ ···
2: ··· ───────────────────────╰●───────────────────────────────│────────────────────────│─ ···
3: ··· ────────────────────────────────────────────────────────╰●───────────────────────╰● ···

0: ··· ──RZ(4.71)────────────────────────────────────────────────────┤
1: ··· ─╭SWAP†─────────────────────────╭(Rϕ(0.79))†─╭(Rϕ(1.57))†──H†─┤
2: ··· ─│─────────────╭(Rϕ(1.57))†──H†─│────────────╰(Rϕ(1.57))†─────┤
3: ··· ─╰SWAP†─────H†─╰(Rϕ(1.57))†─────╰(Rϕ(0.79))†──────────────────┤

This transform takes advantage of the new graph-based decomposition algorithm when qml.decomposition.enable_graph() is present, which allows for more flexible decompositions towards any target gate set. For example, the current system does not guarantee 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])]

Weighted Gate Sets

With the graph based decomposition enabled, gate weights can be provided in the gate_set parameter. For example:

@partial(
    qml.transforms.decompose,
    gate_set={qml.Toffoli: 1.23, qml.RX: 4.56, qml.CZ: 0.01, qml.H: 420, qml.CRZ: 100}
)
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.CRX(0.1, wires=[0, 1])
    qml.Toffoli(wires=[0, 1, 2])
    return qml.expval(qml.Z(0))
>>> print(qml.draw(circuit)())
0: ───────────╭●────────────╭●─╭●─┤  <Z>
1: ──RX(0.05)─╰Z──RX(-0.05)─╰Z─├●─┤
2: ────────────────────────────╰X─┤
@partial(
    qml.transforms.decompose,
    gate_set={qml.Toffoli: 1.23, qml.RX: 4.56, qml.CZ: 0.01, qml.H: 0.1, qml.CRZ: 0.1}
)
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.CRX(0.1, wires=[0, 1])
    qml.Toffoli(wires=[0, 1, 2])
    return qml.expval(qml.Z(0))
>>> print(qml.draw(circuit)())
0: ────╭●───────────╭●─┤  <Z>
1: ──H─╰RZ(0.10)──H─├●─┤
2: ─────────────────╰X─┤

Here, when the Hadamard and CRZ have relatively high weights, a decomposition involving them is considered less efficient. When they have relatively low weights, a decomposition involving them is considered more efficient.

Gate Set vs. Stopping Condition

With the new graph-based decomposition system enabled, we make the distinction between a target gate set and a stopping condition. The gate_set is a collection of operator types and/or names that is required by the graph-based decomposition solver, which chooses a decomposition rule for each operator that ultimately minimizes the total number of gates in terms of the target gate set (or the total cost if weights are provided). On the other hand, the stopping_condition is a function that determines whether an operator instance needs to be decomposed. In short, the gate_set is specified in terms of operator types, whereas the stopping_condition is specified in terms of operator instances.

Here is an example of using stopping_condition to not decompose a qml.QubitUnitary instance if it’s equivalent to the identity matrix.

from functools import partial
import pennylane as qml

qml.decomposition.enable_graph()

# Prepare a unitary matrix that we want to decompose
U = qml.matrix(qml.Rot(0.1, 0.2, 0.3, wires=0) @ qml.Identity(wires=1))

def stopping_condition(op):

    if isinstance(op, qml.QubitUnitary):
        identity = qml.math.eye(2 ** len(op.wires))
        return qml.math.allclose(op.matrix(), identity)

    return False

Note that the stopping_condition does not need to check whether the operator is in the target gate set. This will always be checked.

@partial(
    qml.transforms.decompose,
    gate_set={qml.RZ, qml.RY, qml.GlobalPhase, qml.CNOT},
    stopping_condition=stopping_condition,
)
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.QubitUnitary(U, wires=[0, 1])
    return qml.expval(qml.PauliZ(0))
>>> print(qml.draw(circuit)())
0: ──RZ(0.10)──RY(0.20)──RZ(0.30)─┤  <Z>
1: ──U(M0)────────────────────────┤

M0 =
[[1.+0.j 0.+0.j]
 [0.+0.j 1.+0.j]]

We can see that the QubitUnitary on wire 1 is not decomposed due to the stopping condition, despite QubitUnitary not being in the target gate set.

Customizing Decompositions

The new system also enables specifying custom decomposition rules. When qml.decomposition.enable_graph() is present, this transform accepts two additional keyword arguments: fixed_decomps and alt_decomps. The user can define custom decomposition rules as quantum functions decorated with @qml.register_resources, and provide them to the transform via these arguments.

The fixed_decomps forces the transform to use the specified decomposition rules for certain operators, whereas 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})