aboutsummaryrefslogtreecommitdiff
path: root/python_originals
diff options
context:
space:
mode:
authorEugeniy E. Mikhailov <evgmik@gmail.com>2024-11-20 17:38:02 -0500
committerEugeniy E. Mikhailov <evgmik@gmail.com>2024-11-20 17:39:07 -0500
commitdb7ee776d2a10f50752c23f57b11184cb3347a15 (patch)
treeb77da6830cafcaec80cef4568d4d8e87bfa3eb12 /python_originals
parent984d56b4d94bf298a197f1567834451acf49cab9 (diff)
downloadmatlab_strawberryfields-db7ee776d2a10f50752c23f57b11184cb3347a15.tar.gz
matlab_strawberryfields-db7ee776d2a10f50752c23f57b11184cb3347a15.zip
draft of bloch_messiah
Diffstat (limited to 'python_originals')
-rw-r--r--python_originals/bloch_messiah.py96
-rw-r--r--python_originals/polar.py105
-rw-r--r--python_originals/takagi.py75
3 files changed, 276 insertions, 0 deletions
diff --git a/python_originals/bloch_messiah.py b/python_originals/bloch_messiah.py
new file mode 100644
index 0000000..a4cc310
--- /dev/null
+++ b/python_originals/bloch_messiah.py
@@ -0,0 +1,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
+
+
diff --git a/python_originals/polar.py b/python_originals/polar.py
new file mode 100644
index 0000000..69b6a35
--- /dev/null
+++ b/python_originals/polar.py
@@ -0,0 +1,105 @@
+def polar(a, side="right"):
+ """
+ Compute the polar decomposition.
+
+ Returns the factors of the polar decomposition [1]_ `u` and `p` such
+ that ``a = up`` (if `side` is "right") or ``a = pu`` (if `side` is
+ "left"), where `p` is positive semidefinite. Depending on the shape
+ of `a`, either the rows or columns of `u` are orthonormal. When `a`
+ is a square array, `u` is a square unitary array. When `a` is not
+ square, the "canonical polar decomposition" [2]_ is computed.
+
+ Parameters
+ ----------
+ a : (m, n) array_like
+ The array to be factored.
+ side : {'left', 'right'}, optional
+ Determines whether a right or left polar decomposition is computed.
+ If `side` is "right", then ``a = up``. If `side` is "left", then
+ ``a = pu``. The default is "right".
+
+ Returns
+ -------
+ u : (m, n) ndarray
+ If `a` is square, then `u` is unitary. If m > n, then the columns
+ of `a` are orthonormal, and if m < n, then the rows of `u` are
+ orthonormal.
+ p : ndarray
+ `p` is Hermitian positive semidefinite. If `a` is nonsingular, `p`
+ is positive definite. The shape of `p` is (n, n) or (m, m), depending
+ on whether `side` is "right" or "left", respectively.
+
+ References
+ ----------
+ .. [1] R. A. Horn and C. R. Johnson, "Matrix Analysis", Cambridge
+ University Press, 1985.
+ .. [2] N. J. Higham, "Functions of Matrices: Theory and Computation",
+ SIAM, 2008.
+
+ Examples
+ --------
+ >>> import numpy as np
+ >>> from scipy.linalg import polar
+ >>> a = np.array([[1, -1], [2, 4]])
+ >>> u, p = polar(a)
+ >>> u
+ array([[ 0.85749293, -0.51449576],
+ [ 0.51449576, 0.85749293]])
+ >>> p
+ array([[ 1.88648444, 1.2004901 ],
+ [ 1.2004901 , 3.94446746]])
+
+ A non-square example, with m < n:
+
+ >>> b = np.array([[0.5, 1, 2], [1.5, 3, 4]])
+ >>> u, p = polar(b)
+ >>> u
+ array([[-0.21196618, -0.42393237, 0.88054056],
+ [ 0.39378971, 0.78757942, 0.4739708 ]])
+ >>> p
+ array([[ 0.48470147, 0.96940295, 1.15122648],
+ [ 0.96940295, 1.9388059 , 2.30245295],
+ [ 1.15122648, 2.30245295, 3.65696431]])
+ >>> u.dot(p) # Verify the decomposition.
+ array([[ 0.5, 1. , 2. ],
+ [ 1.5, 3. , 4. ]])
+ >>> u.dot(u.T) # The rows of u are orthonormal.
+ array([[ 1.00000000e+00, -2.07353665e-17],
+ [ -2.07353665e-17, 1.00000000e+00]])
+
+ Another non-square example, with m > n:
+
+ >>> c = b.T
+ >>> u, p = polar(c)
+ >>> u
+ array([[-0.21196618, 0.39378971],
+ [-0.42393237, 0.78757942],
+ [ 0.88054056, 0.4739708 ]])
+ >>> p
+ array([[ 1.23116567, 1.93241587],
+ [ 1.93241587, 4.84930602]])
+ >>> u.dot(p) # Verify the decomposition.
+ array([[ 0.5, 1.5],
+ [ 1. , 3. ],
+ [ 2. , 4. ]])
+ >>> u.T.dot(u) # The columns of u are orthonormal.
+ array([[ 1.00000000e+00, -1.26363763e-16],
+ [ -1.26363763e-16, 1.00000000e+00]])
+
+ """
+ if side not in ['right', 'left']:
+ raise ValueError("`side` must be either 'right' or 'left'")
+ a = np.asarray(a)
+ if a.ndim != 2:
+ raise ValueError("`a` must be a 2-D array.")
+
+ w, s, vh = svd(a, full_matrices=False)
+ u = w.dot(vh)
+ if side == 'right':
+ # a = up
+ p = (vh.T.conj() * s).dot(vh)
+ else:
+ # a = pu
+ p = (w * s).dot(w.T.conj())
+ return u, p
+
diff --git a/python_originals/takagi.py b/python_originals/takagi.py
new file mode 100644
index 0000000..4f3fbd9
--- /dev/null
+++ b/python_originals/takagi.py
@@ -0,0 +1,75 @@
+def takagi(N, tol=1e-13, rounding=13):
+ r"""Autonne-Takagi decomposition of a complex symmetric (not Hermitian!) matrix.
+
+ Note that singular values of N are considered equal if they are equal after np.round(values, tol).
+
+ See :cite:`cariolaro2016` and references therein for a derivation.
+
+ Args:
+ N (array[complex]): square, symmetric matrix N
+ rounding (int): the number of decimal places to use when rounding the singular values of N
+ tol (float): the tolerance used when checking if the input matrix is symmetric: :math:`|N-N^T| <` tol
+
+ Returns:
+ tuple[array, array]: (rl, U), where rl are the (rounded) singular values,
+ and U is the Takagi unitary, such that :math:`N = U \diag(rl) U^T`.
+ """
+ (n, m) = N.shape
+ if n != m:
+ raise ValueError("The input matrix must be square")
+ if np.linalg.norm(N - np.transpose(N)) >= tol:
+ raise ValueError("The input matrix is not symmetric")
+
+ N = np.real_if_close(N)
+
+ if np.allclose(N, 0):
+ return np.zeros(n), np.eye(n)
+
+ if np.isrealobj(N):
+ # If the matrix N is real one can be more clever and use its eigendecomposition
+ l, U = np.linalg.eigh(N)
+ vals = np.abs(l) # These are the Takagi eigenvalues
+ phases = np.sqrt(np.complex128([1 if i > 0 else -1 for i in l]))
+ Uc = U @ np.diag(phases) # One needs to readjust the phases
+ list_vals = [(vals[i], i) for i in range(len(vals))]
+ list_vals.sort(reverse=True)
+ sorted_l, permutation = zip(*list_vals)
+ permutation = np.array(permutation)
+ Uc = Uc[:, permutation]
+ # And also rearrange the unitary and values so that they are decreasingly ordered
+ return np.array(sorted_l), Uc
+
+ v, l, ws = np.linalg.svd(N)
+ w = np.transpose(np.conjugate(ws))
+ rl = np.round(l, rounding)
+
+ # Generate list with degenerancies
+ result = []
+ for k, g in groupby(rl):
+ result.append(list(g))
+
+ # Generate lists containing the columns that correspond to degenerancies
+ kk = 0
+ for k in result:
+ for ind, j in enumerate(k): # pylint: disable=unused-variable
+ k[ind] = kk
+ kk = kk + 1
+
+ # Generate the lists with the degenerate column subspaces
+ vas = []
+ was = []
+ for i in result:
+ vas.append(v[:, i])
+ was.append(w[:, i])
+
+ # Generate the matrices qs of the degenerate subspaces
+ qs = []
+ for i in range(len(result)):
+ qs.append(sqrtm(np.transpose(vas[i]) @ was[i]))
+
+ # Construct the Takagi unitary
+ qb = block_diag(*qs)
+
+ U = v @ np.conj(qb)
+ return rl, U
+