Decompositions

The qdecomp.decompositions subpackage contains various decomposition and approximation algorithms for single and two-qubits quantum gates. The following sections provide an overview of the available decompositions.

SQG Module

This module contains the main function to decompose single qubit gates (SQG) into a sequence of Clifford+T gates up to a given tolerance \(\varepsilon\). This module combines the functions from the qdecomp.decompositions.rz and qdecomp.decompositions.zyz modules to achieve this goal.

Example

>>> from scipy.stats import unitary_group
>>> from qdecomp.decompositions import sqg_decomp

# Decompose a random single qubit gate with tolerance 0.001
>>> sqg = unitary_group.rvs(2, random_state=42)
>>> sequence, alpha = sqg_decomp(sqg, epsilon=0.001, add_global_phase=True)
>>> print(sequence, alpha)
sequence : T H S T H S T [...] S H S W W W W
alpha : 0.27

# Decompose a random single qubit gate with tolerance 0.001 up to a global phase
>>> sqg = unitary_group.rvs(2, random_state=42)
>>> sequence, _ = sqg_decomp(sqg, epsilon=0.001, add_global_phase=False)
>>> print(sequence)
T H S T H S T [...] Z T H Z S H S
qdecomp.decompositions.sqg.zyz_decomp(U: ndarray[tuple[Any, ...], dtype[_ScalarT]]) tuple[float, ...][source]

Any single qubit gate can be decomposed into a series of three rotations around the Z, Y, and Z axis and a global phase factor:

\[U = e^{i \alpha} R_z(\theta_2) R_y(\theta_1) R_z(\theta_0),\]

where \(R_z\) and \(R_y\) are the rotation gates around the Z and Y axes, respectively. This is known as the ZYZ decomposition.

This function performs this decomposition on a given unitary 2 x 2 matrix. It returns the three rotation angles \(\theta_0, \theta_1, \theta_2\) and the phase \(\alpha\). For more details, see [Crooks, 2024].

Parameters:

U (NDArray) – A 2 x 2 unitary matrix.

Returns:

(t0, t1, t2, alpha), the three rotation angles (rad) and the global phase (rad)

Return type:

tuple[float, …]

Raises:
  • ValueError – If the input matrix is not 2 x 2.

  • ValueError – If the input matrix is not unitary.

Examples:

>>> import numpy as np
>>> from qdecomp.decompositions import zyz_decomp

# Define rotation and phase matrices
>>> ry = lambda teta: np.array([[np.cos(teta / 2), -np.sin(teta / 2)],
                                [np.sin(teta / 2), np.cos(teta / 2)]])
>>> rz = lambda teta: np.array([[np.exp(-1.0j * teta / 2), 0],
                                [0, np.exp(1.0j * teta / 2)]])
>>> phase = lambda alpha: np.exp(1.0j * alpha)

# Create a unitary matrix U
>>> a = complex(1, 1) / np.sqrt(3)
>>> b = np.sqrt(complex(1, 0) - np.abs(a) ** 2)  # Ensure that U is unitary
>>> alpha = np.pi/3
>>> U = np.exp(1.0j * alpha) * np.array([[a, -b.conjugate()], [b, a.conjugate()]])

# Compute the decomposition of U
>>> t0, t1, t2, alpha_ = zyz_decomp(U)

# Recreate U from the decomposition
>>> U_calculated = phase(alpha_) * rz(t2) @ ry(t1) @ rz(t0)

# Print the results
>>> print("U =\n", U)
U =
 [[-0.21132487+0.78867513j -0.28867513-0.5j       ]
 [ 0.28867513+0.5j         0.78867513+0.21132487j]]
>>> print("U_calculated =\n", U_calculated)
U_calculated =
 [[-0.21132487+0.78867513j -0.28867513-0.5j       ]
 [ 0.28867513+0.5j         0.78867513+0.21132487j]]
>>> print(f"Error = {np.linalg.norm(U - U_calculated)}")
Error = 1.0007415106216802e-16
qdecomp.decompositions.sqg.rz_decomp(angle: float, epsilon: float, add_global_phase=False) str[source]

This function decomposes a \(R_z\) gate of the form

\[\begin{split}R_z = \begin{pmatrix} e^{-i\theta / 2} & 0 \\ 0 & e^{i\theta / 2} \end{pmatrix},\end{split}\]

into a sequence of Clifford+T gates where \(\theta\) is the rotation angle around the Z axis. The decomposition is up to a given tolerance \(\varepsilon\). The algorithm implemented in this function is based on the one presented by Ross and Selinger in [Ross and Selinger, 2014]. Note: when the add_global_phase argument is set to True, the sequence includes global phase gates \(W = e^{i\pi/4}\).

This function uses the qdecomp.utils.exact_synthesis, qdecomp.utils.grid_problem and qdecomp.utils.diophantine modules to achieve this goal.

Parameters:
  • angle (float) – The angle of the RZ gate in radians.

  • epsilon (float) – The tolerance for the approximation based on the operator norm.

  • add_global_phase (bool) – If True, adds global phase gates W to the sequence (default: False).

Returns:

The sequence of Clifford+T gates that approximates the RZ gate.

Return type:

sequence (str)

Example

>>> from qdecomp.decompositions import rz_decomp
>>> from math import pi

# Decompose a RZ gate with angle pi/128 and tolerance 0.001 exactly
>>> sequence = rz_decomp(epsilon=0.001, angle=pi/128, add_global_phase=True)
>>> print(sequence)
H S T H S T H [...] Z S W W W W W W

# Decompose a RZ gate with angle pi/128 and tolerance 0.001 up to a global phase
>>> sequence = rz_decomp(epsilon=0.001, angle=pi/128, add_global_phase=False)
>>> print(sequence)
H S T H S T H [...] Z S H S T H Z S
qdecomp.decompositions.sqg.sqg_decomp(sqg: ndarray | QGate, epsilon: float, add_global_phase: bool = False) tuple[str, float][source]

Decomposes any single qubit gate (SQG) into its optimal sequence of Clifford+T gates up to a given error.

Parameters:
  • sqg (Union[np.ndarray, QGate]) – The matrix representation of the single qubit gate to decompose.

  • epsilon (float) – The error tolerance for the decomposition.

  • add_global_phase (bool) – If True, adds the global phase to the sequence and return statements (default: False).

Returns:

A tuple containing the sequence of gates that approximates the input SQG and the global phase alpha associated with the zyz decomposition of the gate. (if add_global_phase is False, alpha is set to 0 and the sequence doesn’t contain any global phase gate W).

Return type:

tuple(str, float)

Raises:
  • ValueError – If the input is a QGate object with no epsilon value set.

  • ValueError – If the input is not a QGate object or a 2x2 matrix.

TQG Module

This module contains functions to decompose general 2-qubits quantum gates into single-qubit and CNOT gates.

The module contains the following functions:

  • kronecker_decomp(): Decompose a 4 x 4 matrix into two 2 x 2 matrices such that their Kronecker product is the closest to the original matrix.

  • so4_decomp(): Decompose a 4 x 4 matrix in SO(4) into a circuit of 2 CNOT gates and 8 single-qubit gates.

  • o4_det_minus1_decomp(): Decompose a 4 x 4 matrix in O(4) with a determinant of -1 into a circuit of 3 CNOT gates and 8 single-qubit gates.

  • canonical_decomp(): Decompose a 4 x 4 unitary matrix into a global phase, two local 4 x 4 matrices, and the three parameters of the canonical gate.

  • u4_decomp(): Decompose a 4 x 4 matrix in U(4) into a circuit of 3 CNOT and 7 single-qubit gates.

  • known_decomp(): Decompose a 4 x 4 matrix into a circuit of CNOT and single-qubit gates using predefined decompositions for common gates.

  • cnot_decomp(): Decompose any two-qubits gate into a circuit of CNOT and single-qubit gates.

  • sqg_decomp(): Decompose any two-qubits gate into a series of Clifford+T gates.

The function sqg_decomp() is the main function of the module. It decomposes any 4 x 4 unitary matrix into a circuit of Clifford+T gates by using the cnot_decomp() and sqg_decomp() functions.

The function cnot_decomp is the second most important function of the module. It decomposes any 4 x 4 unitary matrix into a circuit of CNOT and single-qubit gates. The function determines which decomposition to use based on the Lie group of the input matrix (SO(4), O(4), U(4)) or uses a predefined decomposition if the gate is common (SWAP, identity, CNOT). The function returns a list of QGate objects representing the circuit decomposition.

For more details on the theory, see [Crooks, 2024, Loan, 2000, Vatan and Williams, 2004, Zhang et al., 2003].

qdecomp.decompositions.tqg.kronecker_decomp(matrix: ndarray[tuple[Any, ...], dtype[floating]]) tuple[ndarray[tuple[Any, ...], dtype[floating]], ndarray[tuple[Any, ...], dtype[floating]]][source]

Compute the Kronecker decomposition of a 4 x 4 matrix.

Given a 4 x 4 matrix M, find the two 2 x 2 matrix A and B such that their Kronecker product is the closest to the matrix M in the Frobenius norm [Crooks, 2024, Loan, 2000].

Parameters:

matrix (NDArray[float]) – 4 x 4 matrix.

Returns:

The two 2 x 2 matrix of the decomposition.

Return type:

tuple[NDArray[float], NDArray[float]]

Raises:
  • TypeError – If matrix is not a numpy array.

  • ValueError – If matrix is not a 4 x 4 matrix.

Example:

>>> import numpy as np
>>> from qdecomp.decompositions import kronecker_decomp

# Define two 2 x 2 matrices
>>> A = np.array([[1, 2], [3, 4]])
>>> B = np.array([[5, 6], [7, 8]])

# Compute the Kronecker decomposition
>>> a, b = kronecker_decomp(np.kron(A, B))
>>> print(np.allclose(np.kron(A, B), np.kron(a, b)))
True
qdecomp.decompositions.tqg.so4_decomp(U: ndarray[tuple[Any, ...], dtype[floating]] | QGate) list[QGate][source]

Circuit decomposition of SO(4) matrices.

Decompose a 4 x 4 matrix in SO(4) (special orthogonal group) into a circuit of 2 CNOT gates and 8 single-qubit gates [Crooks, 2024, Vatan and Williams, 2004]. The output is a list of QGate objects.

Parameters:

U (NDArray[float]) – 4 x 4 matrix in SO(4).

Returns:

Circuit decomposition of the input matrix. The output is a list of QGate objects.

Return type:

list[QGate]

Raises:
  • TypeError – If the input matrix is not a numpy array or a QGate object.

  • ValueError – If the input matrix is not in SO(4).

qdecomp.decompositions.tqg.o4_det_minus1_decomp(U: ndarray[tuple[Any, ...], dtype[floating]] | QGate) list[QGate][source]

Circuit decomposition of O(4) matrices with a determinant of -1.

Decompose a 4 x 4 matrix in O(4) (orthogonal group) with a determinant of -1 into a circuit of 3 CNOT and 8 single-qubit gates [Crooks, 2024, Vatan and Williams, 2004]. The output is a list of QGate objects.

Parameters:

U (NDArray[float]) – 4 x 4 matrix in O(4) with a determinant of -1.

Returns:

Circuit decomposition of the input matrix. The output is a list of QGate objects.

Return type:

list[QGate]

Raises:
  • TypeError – If the input matrix is not a numpy array or a QGate object.

  • ValueError – If the input matrix is not in O(4) with a determinant of -1.

qdecomp.decompositions.tqg.canonical_decomp(U: ndarray[tuple[Any, ...], dtype[floating]]) CanonicalDecomposition[source]

Perform the canonical decomposition of a given 4 x 4 unitary matrix.

Given a 4 x 4 unitary matrix U, find the phase alpha, the two 4 x 4 local unitaries A and B, and the three parameters of the canonical gate to decompose the input matrix U like

\[U = e^{i \alpha} B \times Can(t_x, t_y, t_z) \times A.\]

Can(tx, ty, tz) is the canonical gate defined as

\[Can(t_x, t_y, t_z) = exp(-i\frac{\pi}{2} (t_x X\otimes X + t_y Y\otimes Y + t_z Z\otimes Z)),\]

where X, Y, and Z are the Pauli matrices [Crooks, 2024, Zhang et al., 2003].

Parameters:

U (NDArray[float]) – 4 x 4 unitary matrix.

Returns:

A namedtuple with the following attributes:
  • A (NDArray[float]): 4 x 4 matrix A of the decomposition. A is the Kronecker product of two 2 x 2 matrices.

  • B (NDArray[float]): 4 x 4 matrix B of the decomposition. B is the Kronecker product of two 2 x 2 matrices.

  • t (tuple[float, float, float]): The three coordinates (tx, ty, tz) of the canonical gate.

  • phase (float): Phase of the unitary matrix.

Return type:

CanonicalDecomposition

Raises:
  • TypeError – If the matrix U is not a numpy object.

  • ValueError – If U is not a 4 x 4 unitary matrix.

Example:

>>> from scipy.stats import unitary_group
>>> import numpy as np
>>> from qdecomp.decompositions import canonical_decomp
>>> from qdecomp.utils import gates

# Define a 4 x 4 unitary matrix
>>> U = unitary_group.rvs(4)

# Perform the canonical decomposition and reconstruct the matrix
>>> decomp = canonical_decomp(U)
>>> reconstructed_matrix = np.exp(1.j * decomp.phase) * decomp.B @ gates.canonical_gate(*decomp.t) @ decomp.A

# Check if the decomposition is correct
>>> print(np.allclose(U, reconstructed_matrix))
True
qdecomp.decompositions.tqg.u4_decomp(U: ndarray[tuple[Any, ...], dtype[floating]] | QGate) list[QGate][source]

Circuit decomposition of U(4) matrices.

Decompose a 4 x 4 matrix in U(4) (unitary group) into a circuit of 3 CNOT a 7 single-qubit gates [Crooks, 2024, Vatan and Williams, 2004]. The output is a list of QGate objects.

Parameters:

U (NDArray[float]) – 4 x 4 matrix in U(4).

Returns:

Circuit decomposition of the input matrix. The output is a list of QGate objects.

Return type:

list[QGate]

Raises:
  • TypeError – If the input matrix is not a numpy array or a QGate object.

  • ValueError – If the input matrix is not in U(4).

qdecomp.decompositions.tqg.cnot_decomp(U: ndarray[tuple[Any, ...], dtype[floating]]) list[QGate][source]

Circuit decomposition of 4 x 4 quantum gates.

Decompose any two-qubits gate into a circuit of CNOT and single-qubit gates. The function determines which decomposition to use based on the Lie group of the input matrix (SO(4), O(4), U(4)) or uses a predefined decomposition if the gate is common (SWAP, identity, CNOT, etc.). The output is a list of QGate objects.

Parameters:

U (NDArray[float]) – 4 x 4 unitary matrix.

Returns:

Circuit decomposition of the input matrix. The output is a list of QGate objects.

Return type:

list[QGate]

Raises:
  • TypeError – If the input matrix is not a numpy array or a QGate object.

  • ValueError – If the input matrix is not a 4 x 4 unitary matrix.

Example:

>>> from qdecomp.decompositions import cnot_decomp
>>> from scipy.stats import unitary_group

# Use an arbitrary 4 x 4 unitary matrix
>>> U = unitary_group.rvs(4)

# Decompose the matrix into a circuit of CNOT and single-qubit gates
>>> circuit = cnot_decomp(U)
>>> for gate in circuit:
...     print(f"{gate.target} -> {gate.name}")
(0,) -> A1
(1,) -> A2
(0, 1) -> CNOT1
(0,) -> PZ
(1,) -> PY
(0, 1) -> CNOT
(1,) -> PY
(0, 1) -> CNOT1
(0,) -> B1
(1,) -> B2
qdecomp.decompositions.tqg.known_decomp(U: ndarray[tuple[Any, ...], dtype[floating]] | QGate) list[QGate] | None[source]

Circuit decompositions of common 4 x 4 matrices.

Decompose a 4 x 4 matrix into a circuit of CNOT and single-qubit gates using predefined decompositions for common gates (SWAP, identity, CNOT, etc.). The output is a list of QGate objects. If the decomposition is not known, the function returns None.

Parameters:

U (NDArray[float]) – 4 x 4 matrix in U(4).

Returns:

Circuit decomposition of the input matrix. The output is a list of QGate objects. Return None if the decomposition is not known.

Return type:

(list[QGate] | None)

Raises:
  • TypeError – If the input matrix is not a numpy array or a QGate object.

  • ValueError – If the input matrix is not in U(4).

qdecomp.decompositions.tqg.tqg_decomp(tqg: ndarray | QGate, epsilon: float = 0.01) list[QGate][source]

This function decomposes a two-qubit gate (TQG) into a sequence of CNOT and single qubit gates up to a given tolerance \(\varepsilon\). It uses and combines the qdecomp.decompositions.sqg and qdecomp.decompositions.cnot decomposition algorithms to achieve this goal.

Parameters:
  • tqg (Union[np.array, QGate]) – The matrix representation of the two-qubit gate to decompose.

  • epsilon (float) – The tolerance for the decomposition (default: 0.01).

Returns:

A list of QGate objects representing the decomposed gates with their sequences defined.

Return type:

list[QGate]

Raises:
  • TypeError – If the input is not a numpy array or QGate object.

  • ValueError – If the input is not a 4x4 matrix or QGate object with a 4x4 matrix.

Example

>>> from scipy.stats import unitary_group
>>> from qdecomp.decompositions import tqg_decomp

# Decompose a random two qubit gate with tolerance 0.001
>>> tqg = unitary_group.rvs(4, random_state=42)
>>> circuit = tqg_decomp(tqg, epsilon=0.001)
>>> for gates in circuit:
...     print(f"{gates.target} -> {gates.sequence}")
(0,) -> S T H T [...] H Z S T
(1,) -> S T H T [...] S H S T
(0, 1) -> CNOT1
...
(1,) -> H T H S [...] T H Z S

Circuit Module

This module implements a helper function, simplifying the process of decomposing large circuits which contain SQG and TQG. It uses the qdecomp.decompositions.tqg module to decompose each gate in the circuit.

qdecomp.decompositions.circuit.circuit_decomp(circuit: Iterable[QGate]) list[QGate][source]

Decompose a quantum circuit into the Clifford+T gate set using QGate objects.

Parameters:

circuit (Iterable[QGate]) – An iterable of QGate objects representing the quantum circuit to decompose.

Returns:

A list of QGate objects representing the decomposed gates in the Clifford+T gate set.

Return type:

list[QGate]

Raises:
  • TypeError – If the input circuit is not a list or contains non-QGate objects.

  • ValueError – If a gate in the circuit has an unsupported number of qubits (not 1 or 2).

References

[Cro24] (1,2,3,4,5,6,7)

G. E. Crooks. Quantum Gates. 2024. URL: https://threeplusone.com/pubs/on_gates.pdf.

[Loa00] (1,2)

Charles F.Van Loan. The ubiquitous kronecker product. Journal of Computational and Applied Mathematics, 123(1):85–100, 2000. Numerical Analysis 2000. Vol. III: Linear Algebra. URL: https://www.sciencedirect.com/science/article/pii/S0377042700003939, doi:https://doi.org/10.1016/S0377-0427(00)00393-9.

[RS14]

N. J. Ross and P. Selinger. Optimal ancilla-free clifford+t approximation of z-rotations. Quantum Information & Computation, 2014. URL: https://arxiv.org/abs/1403.2975.

[VW04] (1,2,3,4)

Farrokh Vatan and Colin Williams. Optimal quantum circuits for general two-qubit gates. Physical Review A, march 2004. URL: http://dx.doi.org/10.1103/PhysRevA.69.032315, doi:10.1103/physreva.69.032315.

[ZVSW03] (1,2)

Jun Zhang, Jiri Vala, Shankar Sastry, and K. Birgitta Whaley. Geometric theory of nonlocal two-qubit operations. Physical Review A, april 2003. URL: http://dx.doi.org/10.1103/PhysRevA.67.042313, doi:10.1103/physreva.67.042313.