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 argumentsfixed_decomps
andalt_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). IfNone
, 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. IfNone
, 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
. IfNone
, 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 whenenable_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 whenenable_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. Whenqml.decomposition.enabled_graph()
, PennyLane errors out with aDecompositionError
.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))†──────────────────┤
Integration with the Graph-Based Decomposition System
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, thestopping_condition
is a function that determines whether an operator instance needs to be decomposed. In short, thegate_set
is specified in terms of operator types, whereas thestopping_condition
is specified in terms of operator instances.Here is an example of using
stopping_condition
to not decompose aqml.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, despiteQubitUnitary
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
andalt_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.See also
The
fixed_decomps
forces the transform to use the specified decomposition rules for certain operators, whereas thealt_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 decomposeqml.IsingXX
gates; when it comes toqml.CNOT
, the system will choose the most efficient decomposition rule amongmy_cnot1
,my_cnot2
, and all existing decomposition rules defined forqml.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})