Problems in understanding sf.decompositions.triangular

Hello!
I have a confusion with the decomposition.triangular function from the strawberryfields package. I tried to decompose a matrix with it and then build the T-matrices “by hand” and multiply them together to obtain the matrix that I started with. For the sf.decompositions.rectangular() this works fine for me. However, when I try to use the sf.decompositions.triangular() it dees not work and I dont know in which order I would need to multiply the T-Matrices in from t_list. I would be very happy for any help! Here is my code example (I also tried to use reversed(t_list_triangular):

import numpy as np
import strawberryfields as sf

def create_T_mn(t, dimension):

    n = t[0]
    m = t[1]
    theta= t[2]
    phi = t[3]

    T = np.identity(dimension, dtype  = np.complex128)
    T[n][n] = np.exp(1j*phi)*np.cos(theta)
    T[n][m] = -np.sin(theta)
    T[m][n] = np.exp(1j*phi)*np.sin(theta)
    T[m][m] = np.cos(theta)

    return T

N = 5
# Create a random complex matrix
random_matrix = np.random.rand(N, N) + 1j * np.random.rand(N, N)

# Perform QR decomposition to get a unitary matrix
q, r = np.linalg.qr(random_matrix)

# Normalize to ensure unitary property (q should already be unitary, but we'll double-check)
unitary_matrix = q / np.linalg.norm(q, axis=0)

t_list_rectangular, D_rectangular, _ = sf.decompositions.rectangular_phase_end(unitary_matrix) #rectangular_phase_end decomposition
t_list_triangular, D_triangular, _ = sf.decompositions.triangular(unitary_matrix) #triangular decomposition

Ts_rectangular = []
Ts_triangular = []

for t_list in t_list_rectangular:
    Ts_rectangular.append(create_T_mn(t_list, N))


reconstructed_matrix = np.diag(D_rectangular)

for i in range(len(Ts_rectangular) -1 , -1 , -1):
    reconstructed_matrix = reconstructed_matrix @ Ts_rectangular[i]

print(np.allclose(reconstructed_matrix, unitary_matrix)) #is true

#now triangular
for t_list in t_list_triangular:
    Ts_triangular.append(create_T_mn(t_list, N))

reconstructed_matrix_triangular = Ts_triangular[0]

for i in range(1, len(Ts_triangular) ):
    reconstructed_matrix_triangular = reconstructed_matrix_triangular @ Ts_triangular[i]

reconstructed_matrix_triangular = reconstructed_matrix_triangular @ np.diag(D_triangular)

print(np.allclose(reconstructed_matrix_triangular, unitary_matrix)) #is false```

Hi @Jakob, welcome to the Forum!

Let me take a look and get back to you. I’m suspecting this might be a numpy issue but let me confirm first.

Hi Catalina,

thank you for your fast response! I searched a bit and found tests from the decompositions here strawberryfields/tests/frontend/test_decompositions.py at 5d036fdf8b5f28c05990d5c2e782722164f9d145 · XanaduAI/strawberryfieldsThere i found the order in which I have to multiply the T_matrices. Appearently I need to take the inverse_T matrix. I updated my code and it worked! :slight_smile: This is the new version.

# code to test the order in t_list and to reconstruct the unitary matrix from the decomposition

def create_T_mn(n, m, theta, phi, n_max):

    T = np.identity(n_max, dtype  = np.complex128)
    T[n][n] = np.exp(1j*phi)*np.cos(theta)
    T[n][m] = -np.sin(theta)
    T[m][n] = np.exp(1j*phi)*np.sin(theta)
    T[m][m] = np.cos(theta)

    return T

def create_inverse_T_mn(n, m, theta, phi, n_max):

    	return create_T_mn(n, m, theta, -phi, n_max).T


N = 5
# Create a random complex matrix
random_matrix = np.random.rand(N, N) + 1j * np.random.rand(N, N)

# Perform QR decomposition to get a unitary matrix
q, r = np.linalg.qr(random_matrix)

# Normalize to ensure unitary property (q should already be unitary, but we'll double-check)
unitary_matrix = q / np.linalg.norm(q, axis=0)

t_list_rectangular, D_rectangular, _ = sf.decompositions.rectangular_phase_end(unitary_matrix) #rectangular_phase_end decomposition
t_list_triangular, D_triangular, _ = sf.decompositions.triangular(unitary_matrix) #triangular decomposition

Ts_rectangular = []
Ts_triangular = []

for t_list in t_list_rectangular:
    Ts_rectangular.append(create_T_mn(*t_list))


#reconstructed_matrix = np.diag(D_rectangular)
reconstructed_matrix = np.identity(N, dtype = np.complex128)

for i in range(len(Ts_rectangular)):
    reconstructed_matrix = Ts_rectangular[i] @ reconstructed_matrix

reconstructed_matrix = np.diag(D_rectangular) @ reconstructed_matrix

print(np.allclose(reconstructed_matrix, unitary_matrix)) #is true

#now triangular
for t_list in t_list_triangular:
    Ts_triangular.append(create_inverse_T_mn(*t_list))

reconstructed_matrix_triangular = np.diag(D_triangular)

for i in range(len(Ts_rectangular)):
    reconstructed_matrix_triangular = Ts_triangular[i] @ reconstructed_matrix_triangular
 
 #note that we need T_inverse for the triangular decomposition. Since T = BS(theta, 0) @ PS(phi) inverse would be  PS(-phi) @ BS(-theta, 0) when appyling them
 #  but in source code for interferometer this is not done!

print(np.allclose(reconstructed_matrix_triangular, unitary_matrix)) #is true now too

However, I have another weird observation, why I first wanted to check the order. I tried to run this code:

import strawberryfields as sf
from strawberryfields.ops import *
import numpy as np

M = 4
# Create a random complex matrix
random_matrix = np.random.rand(M, M) + 1j * np.random.rand(M, M)

# Perform QR decomposition to get a unitary matrix
q, r = np.linalg.qr(random_matrix)

# Normalize to ensure unitary property (q should already be unitary, but we'll double-check)
unitary_matrix = q / np.linalg.norm(q, axis=0)

boson_sampling_triangular = sf.Program(4)

with boson_sampling_triangular.context as q:
    # prepare the input fock states
    Fock(1) | q[0]
    Fock(1) | q[1]
    Vac     | q[2]
    Fock(1) | q[3]


    Interferometer(unitary_matrix, mesh = 'triangular', tol = 1e-10) | q

boson_sampling_triangular.compile(compiler="fock")
eng = sf.Engine(backend="fock", backend_options={"cutoff_dim": 5})
results = eng.run(boson_sampling_triangular)
# extract the joint Fock probabilities
probs_triangular = results.state.all_fock_probs()

#now try with mesh = 'rectangular'
boson_sampling_rectangular = sf.Program(4)

with boson_sampling_rectangular.context as q:
    # prepare the input fock states
    Fock(1) | q[0]
    Fock(1) | q[1]
    Vac     | q[2]
    Fock(1) | q[3]

    Interferometer(unitary_matrix, mesh = 'rectangular', tol = 1e-10) | q
    
boson_sampling_rectangular.compile(compiler="fock")
eng = sf.Engine(backend="fock", backend_options={"cutoff_dim": 5})
results = eng.run(boson_sampling_rectangular)
# extract the joint Fock probabilities
probs_rect = results.state.all_fock_probs()

print(f"Probability to measure |1 1 0 1> for triangular mesh: {probs_triangular[1,1,0,1]}")
print(f"Probability to measure |1 1 0 1> for triangular mesh: {probs_rect[1,1,0,1]}")

From my point of view this should give the same result as the matrix U for the Interferometer is the same. When I try “rectangular_phase_end” or “rectangular_symmetric” instead of “triangular” they indeed yield the same result. Thats why i originally wanted to understand the order. I checked the documentation of the Interferometer function and their it says the following:

class Interferometer(Decomposition):

 def __init__(self, U, mesh="rectangular", drop_identity=True, tol=1e-6):
        super().__init__([U])
        self.ns = U.shape[0]
        self.mesh = mesh
        self.tol = tol
        self.drop_identity = drop_identity

        allowed_meshes = {
            "rectangular",
            "rectangular_phase_end",
            "rectangular_symmetric",
            "triangular",
            "rectangular_compact",
            "triangular_compact",
            "sun_compact",
        }

        if mesh not in allowed_meshes:
            raise ValueError(
                "Unknown mesh '{}'. Mesh must be one of {}".format(mesh, allowed_meshes)
            )

        self.identity = np.allclose(U, np.identity(len(U)), atol=_decomposition_merge_tol, rtol=0)

    def _decompose(self, reg, **kwargs):
        # pylint: disable=too-many-branches
        mesh = kwargs.get("mesh", self.mesh)
        tol = kwargs.get("tol", self.tol)
        drop_identity = kwargs.get("drop_identity", self.drop_identity)

        cmds = []

        if mesh == "rectangular_compact":
            phases = dec.rectangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _rectangular_compact_cmds(reg, phases)

        elif mesh == "triangular_compact":
            phases = dec.triangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _triangular_compact_cmds(reg, phases)

        elif mesh == "sun_compact":
            parameters, global_phase = dec.sun_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _sun_compact_cmds(reg, parameters, global_phase)

        elif not self.identity or not drop_identity:
            decomp_fn = getattr(dec, mesh)
            BS1, R, BS2 = decomp_fn(self.p[0], tol=tol)

            for n, m, theta, phi, _ in BS1:
                theta = theta if np.abs(theta) >= _decomposition_tol else 0
                phi = phi if np.abs(phi) >= _decomposition_tol else 0

                if "symmetric" in mesh:
                    # Mach-Zehnder interferometers
                    cmds.append(
                        Command(
                            MZgate(np.mod(theta, 2 * np.pi), np.mod(phi, 2 * np.pi)),
                            (reg[n], reg[m]),
                        )
                    )

                else:
                    # Clements style beamsplitters
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(phi), reg[n]))

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(theta, 0), (reg[n], reg[m])))

            for n, expphi in enumerate(R):
                # local phase shifts
                q = np.log(expphi).imag if np.abs(expphi - 1) >= _decomposition_tol else 0
                if not (drop_identity and q == 0):
                    cmds.append(Command(Rgate(np.mod(q, 2 * np.pi)), reg[n]))

            if BS2 is not None:
                # Clements style beamsplitters

                for n, m, theta, phi, _ in reversed(BS2):
                    theta = theta if np.abs(theta) >= _decomposition_tol else 0
                    phi = phi if np.abs(phi) >= _decomposition_tol else 0

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))

        return cmds

If the mesh = “triangular” we would get BS1, R, BS2 from the decomposition. For the triangular that would be BS1 = t_list, R = local(V) and BS = None
Next we would iterate over BS1 and add our gates to the commands list. However, we would add them like BS_clements = BS(theta, o) PS(phi) which is the normal order and not the inverse order. I might confuse something here but as the results differ there might be something weird with the conventions here. I would be more than happy if you could help me with that and check that. Thank you very much for your patience!

Cheers,
Jakob

Hello again,

I couldn’t stop investigating it further. I added this small quick and dirty change to your interferometer class (see also below):

 elif mesh == "triangular":

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))

                     # Clements style beamsplitters
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))

With that I could fix my code and now the “triangular” and “rectangular” mesh yield the same result as expected. It also aligns now with my separate implementation in the python module called tenpy. I implemented BeamSplitter and PhaseShifters there by hand and get the exact same results there too. From my point of view this also aligns with the fact, that we have to multiply T_inverses together to obtain the original unitary matrix that we decomposed. I would be very happy if you could investigate that and check my findings. I am more than happy to discuss that further with you!
Below you can find my updated Interferometer class below. I have to admit that the code is maybe not the nicest one yet, but alteast it could fix my problem.

 def __init__(self, U, mesh="rectangular", drop_identity=True, tol=1e-6):
        super().__init__([U])
        self.ns = U.shape[0]
        self.mesh = mesh
        self.tol = tol
        self.drop_identity = drop_identity

        allowed_meshes = {
            "rectangular",
            "rectangular_phase_end",
            "rectangular_symmetric",
            "triangular",
            "rectangular_compact",
            "triangular_compact",
            "sun_compact",
        }

        if mesh not in allowed_meshes:
            raise ValueError(
                "Unknown mesh '{}'. Mesh must be one of {}".format(mesh, allowed_meshes)
            )

        self.identity = np.allclose(U, np.identity(len(U)), atol=_decomposition_merge_tol, rtol=0)

    def _decompose(self, reg, **kwargs):
        # pylint: disable=too-many-branches
        mesh = kwargs.get("mesh", self.mesh)
        tol = kwargs.get("tol", self.tol)
        drop_identity = kwargs.get("drop_identity", self.drop_identity)

        cmds = []

        if mesh == "rectangular_compact":
            phases = dec.rectangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _rectangular_compact_cmds(reg, phases)

        elif mesh == "triangular_compact":
            phases = dec.triangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _triangular_compact_cmds(reg, phases)

        elif mesh == "sun_compact":
            parameters, global_phase = dec.sun_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _sun_compact_cmds(reg, parameters, global_phase)

        elif not self.identity or not drop_identity:
            decomp_fn = getattr(dec, mesh)
            BS1, R, BS2 = decomp_fn(self.p[0], tol=tol)

            for n, m, theta, phi, _ in BS1:
                theta = theta if np.abs(theta) >= _decomposition_tol else 0
                phi = phi if np.abs(phi) >= _decomposition_tol else 0

                if "symmetric" in mesh:
                    # Mach-Zehnder interferometers
                    cmds.append(
                        Command(
                            MZgate(np.mod(theta, 2 * np.pi), np.mod(phi, 2 * np.pi)),
                            (reg[n], reg[m]),
                        )
                    )

                elif mesh == "triangular":

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))

                     # Clements style beamsplitters
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))


                else:
                    # Clements style beamsplitters
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(phi), reg[n]))

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(theta, 0), (reg[n], reg[m])))

            for n, expphi in enumerate(R):
                # local phase shifts
                q = np.log(expphi).imag if np.abs(expphi - 1) >= _decomposition_tol else 0
                if not (drop_identity and q == 0):
                    cmds.append(Command(Rgate(np.mod(q, 2 * np.pi)), reg[n]))

            if BS2 is not None:
                # Clements style beamsplitters

                for n, m, theta, phi, _ in reversed(BS2):
                    theta = theta if np.abs(theta) >= _decomposition_tol else 0
                    phi = phi if np.abs(phi) >= _decomposition_tol else 0

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))

        return cmds

Hi @Jakob,

I’m glad to see that you found a solution to your issues! I’m guessing your suggestion is to add this in the _decompose method right? Would you be able to copy the full code for the method here and show where this should be added? That way I can test it too and make sure we’re doing the exact same thing.

Hi @CatalinaAlbornoz ,

I am more than happy to provide the full code of the _decompose _method, where I added a small code snippet! To make sure you can excatly reproduce what I encountered I will try to give you a step by step instructions what I did so that you can understand it.

  1. Start by running the following code:
import strawberryfields as sf
from strawberryfields.ops import *
import numpy as np

M = 4
# Create a random complex matrix
random_matrix = np.random.rand(M, M) + 1j * np.random.rand(M, M)

# Perform QR decomposition to get a unitary matrix
q, r = np.linalg.qr(random_matrix)

# Normalize to ensure unitary property (q should already be unitary, but we'll double-check)
unitary_matrix = q / np.linalg.norm(q, axis=0)

boson_sampling_triangular = sf.Program(4)

with boson_sampling_triangular.context as q:
    # prepare the input fock states
    Fock(1) | q[0]
    Fock(1) | q[1]
    Vac     | q[2]
    Fock(1) | q[3]


    Interferometer(unitary_matrix, mesh = 'triangular', tol = 1e-10) | q

boson_sampling_triangular.compile(compiler="fock")
eng = sf.Engine(backend="fock", backend_options={"cutoff_dim": 5})
results = eng.run(boson_sampling_triangular)
# extract the joint Fock probabilities
probs_triangular = results.state.all_fock_probs()

#now try with mesh = 'rectangular'
boson_sampling_rectangular = sf.Program(4)

with boson_sampling_rectangular.context as q:
    # prepare the input fock states
    Fock(1) | q[0]
    Fock(1) | q[1]
    Vac     | q[2]
    Fock(1) | q[3]

    Interferometer(unitary_matrix, mesh = 'rectangular', tol = 1e-10) | q
    
boson_sampling_rectangular.compile(compiler="fock")
eng = sf.Engine(backend="fock", backend_options={"cutoff_dim": 5})
results = eng.run(boson_sampling_rectangular)
# extract the joint Fock probabilities
probs_rect = results.state.all_fock_probs()

print(f"Probability to measure |1 1 0 1> for triangular mesh: {probs_triangular[1,1,0,1]}")
print(f"Probability to measure |1 1 0 1> for triangular mesh: {probs_rect[1,1,0,1]}")

print(f"{np.allclose(probs_triangular, probs_rect)}")

Here the results will differ between the rectangular and the triangular decomposition. But this should not be, as we still want to apply the same unitary matrix.

Now my fix for this problem:

  1. Change the _decompose method to the following:
 def _decompose(self, reg, **kwargs):
        # pylint: disable=too-many-branches
        mesh = kwargs.get("mesh", self.mesh)
        tol = kwargs.get("tol", self.tol)
        drop_identity = kwargs.get("drop_identity", self.drop_identity)

        cmds = []

        if mesh == "rectangular_compact":
            phases = dec.rectangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _rectangular_compact_cmds(reg, phases)

        elif mesh == "triangular_compact":
            phases = dec.triangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _triangular_compact_cmds(reg, phases)

        elif mesh == "sun_compact":
            parameters, global_phase = dec.sun_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _sun_compact_cmds(reg, parameters, global_phase)

        elif mesh == "triangular":

            decomp_fn = getattr(dec, mesh)
            BS1, R, BS2 = decomp_fn(self.p[0], tol=tol)

            for n, expphi in enumerate(R):
                # local phase shifts
                q = np.log(expphi).imag if np.abs(expphi - 1) >= _decomposition_tol else 0
                if not (drop_identity and q == 0):
                    cmds.append(Command(Rgate(np.mod(q, 2 * np.pi)), reg[n]))

            
            for n, m, theta, phi, _ in BS1:

                theta = theta if np.abs(theta) >= _decomposition_tol else 0
                phi = phi if np.abs(phi) >= _decomposition_tol else 0

                if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))

                    # Clements style beamsplitters
                if not (drop_identity and phi == 0):
                    cmds.append(Command(Rgate(-phi), reg[n]))


        elif not self.identity or not drop_identity:
            decomp_fn = getattr(dec, mesh)
            BS1, R, BS2 = decomp_fn(self.p[0], tol=tol)

            for n, m, theta, phi, _ in BS1:
                theta = theta if np.abs(theta) >= _decomposition_tol else 0
                phi = phi if np.abs(phi) >= _decomposition_tol else 0

                if "symmetric" in mesh:
                    # Mach-Zehnder interferometers
                    cmds.append(
                        Command(
                            MZgate(np.mod(theta, 2 * np.pi), np.mod(phi, 2 * np.pi)),
                            (reg[n], reg[m]),
                        )
                    )

                else:
                    # Clements style beamsplitters
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(phi), reg[n]))

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(theta, 0), (reg[n], reg[m])))

            for n, expphi in enumerate(R):
                # local phase shifts
                q = np.log(expphi).imag if np.abs(expphi - 1) >= _decomposition_tol else 0
                if not (drop_identity and q == 0):
                    cmds.append(Command(Rgate(np.mod(q, 2 * np.pi)), reg[n]))

            if BS2 is not None:
                # Clements style beamsplitters

                for n, m, theta, phi, _ in reversed(BS2):
                    theta = theta if np.abs(theta) >= _decomposition_tol else 0
                    phi = phi if np.abs(phi) >= _decomposition_tol else 0

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))

        return cmds

And run the code from step 1 again. This time the output should match as expected.

Why did I change this?

Mathematically speaking I found out that if we use the triangular scheme and want to rebuild the original matrix we have to do the following:

U = T[N-1]^-1 T[N-2]^-1 T[N-3]^-1…T[0]^-1 D

Applying this on a state means that we need to apply D first and then the inverses of T[i]. In the strawberryfields convention a T(theta, phi) = BS(theta, 0) PS(phi). So the inverse would be PS(-phi)* BS(-theta, 0) i.e. changing the order of the operations and the sign of the angle.
This is what i added within the elif mesh == "triangular" block.

Alternatively, you could also change the sf.decomposition.triangular method. Then you would not need to change as much in the _decompose() method from the Interferometer class.
This can be done as follows:

Change the sf.decompositions.triangular method to:

def triangular(V, tol=1e-11):
    r"""Triangular decomposition of a unitary matrix due to Reck et al.

    See :cite:`reck1994` for more details and :cite:`clements2016` for details on notation.

    Args:
        V (array[complex]): unitary matrix of size ``n_size``
        tol (float): the tolerance used when checking if the matrix is unitary:
            :math:`|VV^\dagger-I| \leq` tol

    Returns:
        tuple[array]: returns a tuple of the form ``(tlist,np.diag(localV), None)``
            where:

            * ``tlist``: list containing ``[n,m,theta,phi,n_size]`` of the T unitaries needed
            * ``localV``: Diagonal unitary applied at the beginning of circuit
    """
    localV = V
    (nsize, _) = localV.shape

    if not np.allclose(V @ V.conj().T, np.identity(nsize), atol=tol, rtol=0):
        raise ValueError("The input matrix is not unitary")

    tlist = []
    for i in range(nsize - 2, -1, -1):
        for j in range(i + 1):
            tlist.append(nullT(nsize - j - 1, nsize - i - 2, localV))
            localV = T(*tlist[-1]) @ localV

    return None , np.diag(localV), tlist

Here we only changed the return command from reversed(tlist), np.diag(localV), None to None , np.diag(localV), tlist

In the _decomposition method we then only need to include an if BS1 is not None as follows:

def _decompose(self, reg, **kwargs):
        # pylint: disable=too-many-branches
        mesh = kwargs.get("mesh", self.mesh)
        tol = kwargs.get("tol", self.tol)
        drop_identity = kwargs.get("drop_identity", self.drop_identity)

        cmds = []

        if mesh == "rectangular_compact":
            phases = dec.rectangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _rectangular_compact_cmds(reg, phases)

        elif mesh == "triangular_compact":
            phases = dec.triangular_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _triangular_compact_cmds(reg, phases)

        elif mesh == "sun_compact":
            parameters, global_phase = dec.sun_compact(self.p[0], rtol=tol, atol=tol)
            cmds = _sun_compact_cmds(reg, parameters, global_phase)

        elif not self.identity or not drop_identity:
            decomp_fn = getattr(dec, mesh)
            BS1, R, BS2 = decomp_fn(self.p[0], tol=tol)

            if BS1 is not None:
                
                for n, m, theta, phi, _ in BS1:
                    theta = theta if np.abs(theta) >= _decomposition_tol else 0
                    phi = phi if np.abs(phi) >= _decomposition_tol else 0

                    if "symmetric" in mesh:
                        # Mach-Zehnder interferometers
                        cmds.append(
                            Command(
                                MZgate(np.mod(theta, 2 * np.pi), np.mod(phi, 2 * np.pi)),
                                (reg[n], reg[m]),
                            )
                        )

                    else:
                        # Clements style beamsplitters
                        if not (drop_identity and phi == 0):
                            cmds.append(Command(Rgate(phi), reg[n]))

                        if not (drop_identity and theta == 0):
                            cmds.append(Command(BSgate(theta, 0), (reg[n], reg[m])))

            for n, expphi in enumerate(R):
                # local phase shifts
                q = np.log(expphi).imag if np.abs(expphi - 1) >= _decomposition_tol else 0
                if not (drop_identity and q == 0):
                    cmds.append(Command(Rgate(np.mod(q, 2 * np.pi)), reg[n]))

            if BS2 is not None:
                # Clements style beamsplitters

                for n, m, theta, phi, _ in reversed(BS2):
                    theta = theta if np.abs(theta) >= _decomposition_tol else 0
                    phi = phi if np.abs(phi) >= _decomposition_tol else 0

                    if not (drop_identity and theta == 0):
                        cmds.append(Command(BSgate(-theta, 0), (reg[n], reg[m])))
                    if not (drop_identity and phi == 0):
                        cmds.append(Command(Rgate(-phi), reg[n]))

        return cmds

I would say that the second alternative is a bit more clean. You can test both cases and see which one performs better and more reliable.

I hope that this could clarify what I did exactly and I hope that you are able to reproduce this with this instructions. If you need more clarification you can always let me know and I will be happy to help you!

Best wishes,
Jakob

PS: Keep up the good work in the forum. It is really helpful!

Thank you so much for all of the details @Jakob !
This really helps.

I’ve forwarded this to the team.

I’ll let you know what they say regarding next steps or what approach they suggest taking here.

Hi @Jakob ,

Thanks again for making this post and providing alternatives for the solution. A member of our team has volunteered to review and help merge the fix. Would you be able to open a Pull Request in the Strawberry Fields repository, adding your fix to the code? Since you mentioned the second option for the fix would be the best, you can implement that one.

Have you contributed to projects on GitHub before? Of not then let me know and we can guide you on the process.

If you prefer not to add the fix yourself that’s ok, just let me know here.
And if you do make the pull request, can you please post the link here so that I can follow up with our team?
Thanks!

Hi @CatalinaAlbornoz ,

thank you for your answer! I did make a pull request now. You can find that here: Update decomposition function by jama7168 · Pull Request #751 · XanaduAI/strawberryfields. I hope that I thought of everything here. I also mentioned the possible drawback, that since I changed the order of the return in def_triangular() from reversed(tlist), np.diag(localV), None to None , np.diag(localV), tlist as this might affect other functions that I am not aware of. My thought was that the decomposition might only be relevant for the interferometer so thats why i thought it might not be a problem

Let me know if you need anything from my side!

Best,
Jakob

Thanks @Jakob !

I shared your PR with a member of our team. They may take a couple of days to review your PR but should get back to you within a week or so.

Thanks again for making the PR and helping us improve!