aboutsummaryrefslogtreecommitdiff
path: root/python_originals/bloch_messiah.py
blob: a4cc310001bdcb5b10bb024725e65c0df226e344 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def bloch_messiah(S, tol=1e-10, rounding=9):
    r"""Bloch-Messiah decomposition of a symplectic matrix.

    See :ref:`bloch_messiah`.

    Decomposes a symplectic matrix into two symplectic unitaries and squeezing transformation.
    It automatically sorts the squeezers so that they respect the canonical symplectic form.

    Note that it is assumed that the symplectic form is

    .. math:: \Omega = \begin{bmatrix}0&I\\-I&0\end{bmatrix}

    where :math:`I` is the identity matrix and :math:`0` is the zero matrix.

    As in the Takagi decomposition, the singular values of N are considered
    equal if they are equal after np.round(values, rounding).

    If S is a passive transformation, then return the S as the first passive
    transformation, and set the the squeezing and second unitary matrices to
    identity. This choice is not unique.

    For more info see:
    https://math.stackexchange.com/questions/1886038/finding-euler-decomposition-of-a-symplectic-matrix

    Args:
        S (array[float]): symplectic matrix
        tol (float): the tolerance used when checking if the matrix is symplectic:
            :math:`|S^T\Omega S-\Omega| \leq tol`
        rounding (int): the number of decimal places to use when rounding the singular values

    Returns:
        tuple[array]: Returns the tuple ``(ut1, st1, vt1)``. ``ut1`` and ``vt1`` are symplectic orthogonal,
            and ``st1`` is diagonal and of the form :math:`= \text{diag}(s1,\dots,s_n, 1/s_1,\dots,1/s_n)`
            such that :math:`S = ut1  st1  v1`
    """
    (n, m) = S.shape

    if n != m:
        raise ValueError("The input matrix is not square")
    if n % 2 != 0:
        raise ValueError("The input matrix must have an even number of rows/columns")

    n = n // 2
    omega = sympmat(n)
    if np.linalg.norm(np.transpose(S) @ omega @ S - omega) >= tol:
        raise ValueError("The input matrix is not symplectic")

    if np.linalg.norm(np.transpose(S) @ S - np.eye(2 * n)) >= tol:

        u, sigma = polar(S, side="left")
        ss, uss = takagi(sigma, tol=tol, rounding=rounding)

        # Apply a permutation matrix so that the squeezers appear in the order
        # s_1,...,s_n, 1/s_1,...1/s_n
        perm = np.array(list(range(0, n)) + list(reversed(range(n, 2 * n))))

        pmat = np.identity(2 * n)[perm, :]
        ut = uss @ pmat

        # Apply a second permutation matrix to permute s
        # (and their corresonding inverses) to get the canonical symplectic form
        qomega = np.transpose(ut) @ (omega) @ ut
        st = pmat @ np.diag(ss) @ pmat

        # Identifying degenerate subspaces
        result = []
        for _k, g in groupby(np.round(np.diag(st), rounding)[:n]):
            result.append(list(g))

        stop_is = list(np.cumsum([len(res) for res in result]))
        start_is = [0] + stop_is[:-1]

        # Rotation matrices (not permutations) based on svd.
        # See Appendix B2 of Serafini's book for more details.
        u_list, v_list = [], []

        for start_i, stop_i in zip(start_is, stop_is):
            x = qomega[start_i:stop_i, n + start_i : n + stop_i].real
            u_svd, _s_svd, v_svd = np.linalg.svd(x)
            u_list = u_list + [u_svd]
            v_list = v_list + [v_svd.T]

        pmat1 = block_diag(*(u_list + v_list))

        st1 = pmat1.T @ pmat @ np.diag(ss) @ pmat @ pmat1
        ut1 = uss @ pmat @ pmat1
        v1 = np.transpose(ut1) @ u

    else:
        ut1 = S
        st1 = np.eye(2 * n)
        v1 = np.eye(2 * n)

    return ut1.real, st1.real, v1.real