Source code for graphqomb.euler

"""Euler angles and related functions.

This module provides:

- `euler_decomposition`: Decompose a 2x2 unitary matrix into Euler angles.
- `bloch_sphere_coordinates`: Get the Bloch sphere coordinates corresponding to a vector.
- `LocalUnitary`: Class to represent a local unitary.
- `LocalClifford`: Class to represent a local Clifford.
- `meas_basis_info`: Return the measurement plane and angle corresponding to a vector.
- `update_lc_lc`: Update a `LocalClifford` object with another `LocalClifford` object.
- `update_lc_basis`: Update a `LocalClifford` object with a MeasBasis object.
"""

from __future__ import annotations

import cmath
import math
from typing import TYPE_CHECKING

import numpy as np
import typing_extensions

from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis, is_clifford_angle, is_close_angle

if TYPE_CHECKING:
    from numpy.typing import NDArray


[docs] def euler_decomposition(u: NDArray[np.complex128]) -> tuple[float, float, float]: r"""Decompose a 2x2 unitary matrix into Euler angles. :math:`U \rightarrow R_z(\gamma)R_x(\beta)R_z(\alpha)` Parameters ---------- u : `numpy.typing.NDArray`\[`numpy.complex128`\] unitary 2x2 matrix Returns ------- `tuple`\[`float`, `float`, `float`\] euler angles (:math:`\alpha`, :math:`\beta`, :math:`\gamma`) """ global_phase = cmath.sqrt(np.linalg.det(u)) u /= global_phase u00 = complex(u[0, 0]) u01 = complex(u[0, 1]) u10 = complex(u[1, 0]) u11 = complex(u[1, 1]) if np.isclose(u10, 0): gamma = 2 * cmath.phase(u11) beta = 0.0 alpha = 0.0 elif np.isclose(u11, 0): gamma = 2 * cmath.phase(u01 / (-1j)) beta = math.pi alpha = 0.0 else: gamma_p_alpha = cmath.phase(u11 / u00) gamma_m_alpha = cmath.phase(u10 / u01) gamma = (gamma_p_alpha + gamma_m_alpha) / 2 alpha = (gamma_p_alpha - gamma_m_alpha) / 2 cos_term = (u11 / cmath.exp(1j * gamma_p_alpha / 2)).real sin_term = (u10 / (-1j * cmath.exp(1j * gamma_m_alpha / 2))).real beta = 2 * cmath.phase(cos_term + 1j * sin_term) return alpha, beta, gamma
[docs] def bloch_sphere_coordinates(vector: NDArray[np.complex128]) -> tuple[float, float]: r"""Get the Bloch sphere coordinates corresponding to a vector. :math:`|\psi\rangle = \cos(\theta/2)|0\rangle + \exp(i\phi)\sin(\theta/2)|1\rangle` Parameters ---------- vector : `numpy.typing.NDArray`\[`numpy.complex128`\] 1 qubit state vector Returns ------- `tuple`\[`float`, `float`] Bloch sphere coordinates (:math:`\theta`, :math:`\phi`) """ # normalize vector /= np.linalg.norm(vector) v0 = complex(vector[0]) v1 = complex(vector[1]) if np.isclose(v0, 0): theta = math.pi phi = cmath.phase(v1) else: global_phase = cmath.phase(v0) v0 /= cmath.exp(1j * global_phase) v1 /= cmath.exp(1j * global_phase) phi = 0 if np.isclose(v1, 0) else cmath.phase(v1) cos_term = v0.real sin_term = (v1 / cmath.exp(1j * phi)).real theta = 2 * cmath.phase(cos_term + 1j * sin_term) return theta, phi
[docs] class LocalUnitary: r"""Class to represent signle-qubit unitaries. :math:`U(\alpha, \beta, \gamma) = R_z(\gamma)R_x(\beta)R_z(\alpha)` Attributes ---------- alpha : `float` angle for the first :math:`R_z`, by default 0 beta : `float` angle for the :math:`R_x`, by default 0 gamma : `float` angle for the last :math:`R_z`, by default 0 """ alpha: float beta: float gamma: float
[docs] def __init__(self, alpha: float = 0, beta: float = 0, gamma: float = 0) -> None: self.alpha = alpha self.beta = beta self.gamma = gamma
[docs] def print_angles(self) -> None: """Print the Euler angles.""" print(f"alpha: {self.alpha}, beta: {self.beta}, gamma: {self.gamma}") # noqa: T201
[docs] def conjugate(self) -> LocalUnitary: """Return the conjugate of the `LocalUnitary` object. Returns ------- `LocalUnitary` conjugate `LocalUnitary` """ return LocalUnitary(-self.gamma, -self.beta, -self.alpha)
[docs] def matrix(self) -> NDArray[np.complex128]: r"""Return the 2x2 unitary matrix corresponding to the Euler angles. Returns ------- `numpy.typing.NDArray`\[`numpy.complex128`\] 2x2 unitary matrix """ return np.asarray(_rz(self.gamma) @ _rx(self.beta) @ _rz(self.alpha), dtype=np.complex128)
[docs] class LocalClifford(LocalUnitary): r"""Class to represent a local Clifford. :math:`U(\alpha, \beta, \gamma) = R_z(\gamma)R_x(\beta)R_z(\alpha)` Each angle must be integer multiples of :math:`\pi/2`. Attributes ---------- alpha : `float` angle for the first :math:`R_z`. The angle must be a multiple of :math:`\pi/2`, by default 0 beta : `float` angle for the :math:`R_x`. The angle must be a multiple of :math:`\pi/2`, by default 0 gamma : `float` angle for the last :math:`R_z`. The angle must be a multiple of :math:`\pi/2`, by default 0 """ alpha: float beta: float gamma: float
[docs] def __init__(self, alpha: float = 0, beta: float = 0, gamma: float = 0) -> None: self._angle_check(alpha, beta, gamma) super().__init__(alpha, beta, gamma)
@classmethod def _angle_check(cls, alpha: float, beta: float, gamma: float, atol: float = 1e-9) -> None: """Check if the angles are Clifford angles. Parameters ---------- alpha : `float` angle for the first Rz beta : `float` angle for the Rx gamma : `float` angle for the last Rz atol : `float`, optional absolute tolerance, by default 1e-9 Raises ------ ValueError if any of the angles is not a Clifford angle """ if not all(is_clifford_angle(angle, atol=atol) for angle in [alpha, beta, gamma]): msg = "The angles must be integer multiples of pi/2" raise ValueError(msg)
[docs] @typing_extensions.override def conjugate(self) -> LocalClifford: """Return the conjugate of the `LocalClifford` object. Returns ------- `LocalClifford` conjugate `LocalClifford` """ return LocalClifford(-self.gamma, -self.beta, -self.alpha)
[docs] def meas_basis_info(vector: NDArray[np.complex128]) -> tuple[Plane, float]: r"""Return the measurement plane and angle corresponding to a vector. Parameters ---------- vector : `numpy.typing.NDArray`\[`numpy.complex128`\] 1 qubit state vector Returns ------- `tuple`\[`Plane`, `float`] measurement plane and angle Raises ------ ValueError if the vector does not lie on any of 3 planes """ theta, phi = bloch_sphere_coordinates(vector) if is_clifford_angle(phi): # YZ or XZ plane if is_clifford_angle(phi / 2): # 0 or pi if is_close_angle(phi, math.pi): theta = -theta return Plane.XZ, theta if is_close_angle(phi, 3 * math.pi / 2): theta = -theta return Plane.YZ, theta if is_clifford_angle(theta) and not is_clifford_angle(theta / 2): # XY plane if is_close_angle(theta, 3 * math.pi / 2): phi += math.pi return Plane.XY, phi msg = "The vector does not lie on any of 3 planes" raise ValueError(msg)
# TODO(masa10-f): Algebraic backend for this computation(#023)
[docs] def update_lc_lc(lc1: LocalClifford, lc2: LocalClifford) -> LocalClifford: """Update a `LocalClifford` object with another `LocalClifford` object. Parameters ---------- lc1 : `LocalClifford` left `LocalClifford` lc2 : `LocalClifford` right `LocalClifford` Returns ------- `LocalClifford` multiplied `LocalClifford` """ matrix1 = lc1.matrix() matrix2 = lc2.matrix() matrix = np.asarray(matrix1 @ matrix2, dtype=np.complex128) alpha, beta, gamma = euler_decomposition(matrix) return LocalClifford(alpha, beta, gamma)
# TODO(masa10-f): Algebraic backend for this computation(#023)
[docs] def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis: """Update a `MeasBasis` object with an action of `LocalClifford` object. Parameters ---------- lc : `LocalClifford` `LocalClifford` basis : `MeasBasis` `MeasBasis` Returns ------- `PlannerMeasBasis` updated `PlannerMeasBasis` """ matrix = lc.matrix() vector = basis.vector() updated_vector = np.asarray(matrix @ vector, dtype=np.complex128) plane, angle = meas_basis_info(updated_vector) return PlannerMeasBasis(plane, angle)
def _rx(angle: float) -> NDArray[np.complex128]: return np.asarray( [ [math.cos(angle / 2), -1j * math.sin(angle / 2)], [-1j * math.sin(angle / 2), math.cos(angle / 2)], ], dtype=np.complex128, ) def _rz(angle: float) -> NDArray[np.complex128]: return np.asarray( [ [cmath.exp(-1j * angle / 2), 0], [0, cmath.exp(1j * angle / 2)], ], dtype=np.complex128, )