- Compilation/
Diagonal unitary decomposition
Diagonal unitary decomposition
Diagonal unitary operators appear in various compilation scenarios, most frequently in the context of unitary synthesis and state preparation.
To decompose a diagonal unitary operator (without auxiliary qubits), we may use a recursive decomposition into multiplexed R_Z gates, which is called SelectPauliRot in PennyLane. This is shown in [1], citing earlier work [2]:
Here, \Delta denotes a diagonal operator and the half-filled control nodes indicate that the R_Z rotations are controlled uniformly, or multiplexed. See the Select-U(2) compilation page for a decomposition of these multiplexers.
Alternatively, if we have a clean auxiliary qubit at our disposal, we may leverage phase kickback
for a decomposition into a single multiplexed R_Z gate, or SelectPauliRot:
This decomposition is particularly useful in the context of
phase gradient decompositions,
because decomposing a single SelectPauliRot that has one more control is notably cheaper
(one 2^n-entry QROM and one adder),in this framework than decomposing n-1 smaller multiplexers
and an R_Z rotation (n-1 QROMs with 2^{n-1}, 2^{n-2}\dots 2 entries and n adders).
Inputs
- Diagonal unitary with length N=2^n for n qubits.
Outputs
- Circuit consisting either of
- n-1 multiplexed R_Z rotations (
qp.SelectPauliRot), one each with k controls for 1\leq k\leq n-1, - 1 single-qubit R_Z rotation (
qp.RZ), and - a global phase (
qp.GlobalPhase). or, if we have a clean auxiliary qubit available, - a single multiplexed R_Z rotation (
qp.SelectPauliRot) with n controls.
- n-1 multiplexed R_Z rotations (
Example
Consider a diagonal unitary operator acting on three qubits, consisting of some random phases:
import pennylane as qp
import numpy as np
qp.decomposition.enable_graph()
n = 3
wires = list(range(n))
np.random.seed(211)
D = np.exp(1j * np.random.random(2**n))
@qp.qnode(qp.device("reference.qubit", wires=n))
def qnode():
qp.DiagonalQubitUnitary(D, wires=wires)
return qp.expval(qp.X(0))
>>> print(qp.draw(qnode, show_matrices=False)())
0: ─╭U(M0)─┤ <X>
1: ─├U(M0)─┤
2: ─╰U(M0)─┤
We can decompose the DiagonalQubitUnitary iteratively, which we can control via the
max_expansion keyword argument of
qp.decompose.
In the first step, we obtain a SelectPauliRot gate with two controls and a two-qubit diagonal
unitary operator:
gate_set = {"SelectPauliRot", "RZ", "GlobalPhase"}
dec_node = qp.transforms.decompose(qnode, gate_set=gate_set, max_expansion=1)
>>> print(qp.draw(dec_node, show_matrices=False)())
0: ─╭U(M0)─╭SelectPauliRot(M1)─┤ <X>
1: ─╰U(M0)─├SelectPauliRot(M1)─┤
2: ────────╰SelectPauliRot(M1)─┤
In the next step, the two-qubit diagonal unitary operator is decomposed into a single-qubit
diagonal unitary and a SelectPauliRot gate with one control:
dec_node = qp.transforms.decompose(qnode, gate_set=gate_set, max_expansion=2)
>>> print(qp.draw(dec_node, show_matrices=False)())
0: ──U(M0)─╭SelectPauliRot(M1)─╭SelectPauliRot(M2)─┤ <X>
1: ────────╰SelectPauliRot(M1)─├SelectPauliRot(M2)─┤
2: ────────────────────────────╰SelectPauliRot(M2)─┤
In the last step, the single-qubit diagonal unitary is broken up into a global phase and
a qp.RZ gate:
dec_node = qp.transforms.decompose(qnode, gate_set=gate_set, max_expansion=3)
>>> print(qp.draw(dec_node, show_matrices=False)())
0: ─╭GlobalPhase(-0.69)──RZ(0.14)─╭SelectPauliRot(M0)─╭SelectPauliRot(M1)─┤ <X>
1: ─├GlobalPhase(-0.69)───────────╰SelectPauliRot(M0)─├SelectPauliRot(M1)─┤
2: ─╰GlobalPhase(-0.69)───────────────────────────────╰SelectPauliRot(M1)─┤
If we instead provide a (zeroed) work wire to decompose, we see the decomposition into
a single multiplexer instead:
dec_node = qp.transforms.decompose(qnode, gate_set=gate_set, num_work_wires=1)
>>> print(qp.draw(dec_node, show_matrices=False)())
0: ───────────╭SelectPauliRot(M0)─────────────┤ <X>
1: ───────────├SelectPauliRot(M0)─────────────┤
2: ───────────├SelectPauliRot(M0)─────────────┤
<DynamicWire>: ──Allocate─╰SelectPauliRot(M0)──Deallocate─┤
Typical usage
This decomposition can be useful as part of a compilation routine that splits off diagonal
operators to commute them through control/multiplexing structures. It is also used implicitly in
MottonenStapePreparation,
both in a version with qp.RY rotations to create the correct amplitudes, and as seen
above with qp.RZ rotations to apply phases.
As a concrete example, Select
operators with a single target qubit can be decomposed using arbitrary diagonal unitary
operators; for details, see the
Select-U(2) compilation page.
In turn, this page will also show how to decompose SelectPauliRot obtained from the
decomposition presented here.
References
[1] "Synthesis of quantum logic circuits", Vivek Shende, Stephen Bullock, Igor Markov, quant-ph/0406176, 2004.
[2] "Smaller circuits for arbitrary n-qubit diagonal computations", Stephen S. Bullock, Igor L. Markov, quant-ph/0303039, 2003.
Cite this page
@misc{PennyLane-diagonal-unitary-decomp,
title = "Diagonal unitary decomposition",
author = "David Wierichs",
year = "2025",
howpublished = "\url{https://pennylane.ai/compilation/diagonal-unitary-decomp}"
}
Page author(s)
David Wierichs
I like to think about representations and compilation of quantum programs, and I enjoy coding up research ideas and useful features for anyone to use in PennyLane.