Repository: XanaduAI/strawberryfields

Issue:
Bloch-messiah decomposition sometimes returns decomposed matrices with permuted rows and columns
The Bloch-Messiah decomposition should return two orthogonal symplectic matrices O1 and O2. In some cases, for instance when the squeezing values are degenerate, the `bloch_messiah` function returns the correct orthogonal symplectic matrices, up to a permutation of rows and columns.

Minimal non-working example:

```python
>>> S = np.array([[ 1.,  0.,  0.,  0.],
                  [ 1.,  1.,  0.,  0.],
                  [ 0.,  0.,  1., -1.],
                  [ 0.,  0.,  0.,  1.]])

>>> O1, Z, O2 = bloch_messiah(S)
```

* *Expected behavior:* 

```python
>>> O1 @ sympmat(2) @ O1.T == sympmat(2)
True
```

* *Actual behavior:*

```python
>>> O1 @ sympmat(2) @ O1.T == sympmat(2)
False
>>> O1 @ sympmat(2) @ O1.T
array([[-0., -1.,  0.,  0.],
       [ 1., -0.,  0.,  0.],
       [ 0.,  0.,  0., -1.],
       [ 0.,  0.,  1., -0.]])
```

A possible solution would be to check whether the returned matrices from `bloch_messiah` are symplectic, and if not, calculate the required row and column permutations.


Discussion:

Resolving patch:
diff --git a/strawberryfields/decompositions.py b/strawberryfields/decompositions.py
--- a/strawberryfields/decompositions.py
+++ b/strawberryfields/decompositions.py
@@ -229,7 +229,7 @@ def williamson(V, tol=11):
     return Db, np.linalg.inv(S).T
 
 
-def bloch_messiah(S, tol=10):
+def bloch_messiah(S, tol=10, rounding=9):
     r""" Performs the Bloch-Messiah decomposition of a symplectic matrix in terms of
     two symplectic unitaries and squeezing transformation.
 
@@ -241,6 +241,9 @@ def bloch_messiah(S, tol=10):
 
     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).
+
     For more info see:
     https://math.stackexchange.com/questions/1886038/finding-euler-decomposition-of-a-symplectic-matrix
 
@@ -278,16 +281,29 @@ def bloch_messiah(S, tol=10):
     ## Apply a second permutation matrix to permute s
     ## (and their corresonding inverses) to get the canonical symplectic form
     qomega = np.transpose(ut) @ (omega) @ ut
-    perm1 = list(range(2*n))
+    st = pmat @ np.diag(ss) @ pmat
 
-    for i in range(n):
-        if qomega[i, i+n] < 0:
-            perm1[i] = i+n
-            perm1[i+n] = i
+    # 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))
 
-    perm1 = np.array(perm1)
-    pmat1 = np.identity(2*n)[perm1, :]
-    st1 = pmat1 @ pmat @ np.diag(ss) @ pmat @ pmat1
+    st1 = pmat1.T @ pmat @ np.diag(ss) @ pmat @ pmat1
     ut1 = uss @ pmat @ pmat1
     v1 = np.transpose(ut1) @ u
     return ut1, st1, v1


Test patch:
diff --git a/tests/test_decompositions.py b/tests/test_decompositions.py
--- a/tests/test_decompositions.py
+++ b/tests/test_decompositions.py
@@ -32,6 +32,42 @@ def random_degenerate_symmetric():
     symmat=U @ np.diag(dd) @ np.transpose(U)
     return symmat
 
+def random_degen_symplectic(N, passive=False):
+    r"""Returns a random symplectic matrix with degenerate symplectic values
+
+    The squeezing parameters :math:`r` for active transformations are randomly
+    sampled from the standard normal distribution and repeated uniform-randomly
+    many times, while passive transformations are randomly sampled from the Haar
+    measure.
+
+    Args:
+        N (int): number of modes
+        passive (bool): if True, returns a passive Gaussian transformation (i.e.,
+            one that preserves photon number). If False (default), returns an active
+            transformation.
+    Returns:
+        array: random :math:`2N\times 2N` symplectic matrix
+    """
+    U = random_interferometer(N)
+    O = np.vstack([np.hstack([U.real, -U.imag]), np.hstack([U.imag, U.real])])
+
+    if passive:
+        return O
+
+    U = random_interferometer(N)
+    P = np.vstack([np.hstack([U.real, -U.imag]), np.hstack([U.imag, U.real])])
+
+    # Generate list of normal random numbers repeated randomly-many times.
+    # Then take the first N values of this list.
+    rep_list = []
+    for _ in range(N):
+        rep_list.append(np.random.randint(1,N))
+    r = np.hstack([np.repeat(np.random.randn(1),rep) for rep in rep_list])[:N]
+
+    Sq = np.diag(np.concatenate([np.exp(-r), np.exp(r)]))
+
+    return O @ Sq @ P
+
 
 class DecompositionsModule(BaseTest):
     num_subsystems = 1
@@ -113,6 +149,24 @@ def test_williamson_BM_random_circuit(self):
             self.assertAlmostEqual(np.linalg.norm(np.transpose(s) @ omega @ s -omega),0)
             self.assertAlmostEqual(np.linalg.norm(O @ s @Oo -S),0)
 
+    def test_BM_random_degen_symplectic(self):
+        self.logTestName()
+        for k in range(nsamples):
+            n=20
+            S = random_degen_symplectic(n)
+            O, s, Oo = dec.bloch_messiah(S)
+            omega = dec.sympmat(n)
+
+            self.assertAlmostEqual(np.linalg.norm(S @ omega @ S.T -omega),0)
+    
+            self.assertAlmostEqual(np.linalg.norm(np.transpose(O) @ omega @ O -omega),0)
+            self.assertAlmostEqual(np.linalg.norm(np.transpose(O) @ O -np.identity(2*n)),0)
+
+            self.assertAlmostEqual(np.linalg.norm(np.transpose(Oo) @ omega @ Oo -omega),0)
+            self.assertAlmostEqual(np.linalg.norm(np.transpose(Oo) @ Oo -np.identity(2*n)),0)
+            self.assertAlmostEqual(np.linalg.norm(np.transpose(s) @ omega @ s -omega),0)
+            self.assertAlmostEqual(np.linalg.norm(O @ s @Oo -S),0)
+
     def test_williamson_BM_random_circuit_pure(self):
         self.logTestName()
         for k in range(nsamples):
@@ -131,8 +185,6 @@ def test_williamson_BM_random_circuit_pure(self):
             self.assertAlmostEqual(np.linalg.norm(S @ omega @ S.T -omega),0)
             self.assertAlmostEqual(np.linalg.norm(S @ D @ S.T -V),0)
 
-
-
             O, s, Oo = dec.bloch_messiah(S)
             self.assertAlmostEqual(np.linalg.norm(np.transpose(O) @ omega @ O -omega),0)
             self.assertAlmostEqual(np.linalg.norm(np.transpose(O) @ O -np.identity(2*n)),0)