Example #1
0
    def add_loss(self, program, device):
        """Adds realistic Borealis loss to circuit."""
        eta_glob = device.certificate["common_efficiency"]
        etas_loop = device.certificate["loop_efficiencies"]
        etas_ch_rel = device.certificate["relative_channel_efficiencies"]

        prog_length = len(program.tdm_params[0])
        reps = int(np.ceil(prog_length / 16))
        etas_ch_rel = np.tile(etas_ch_rel, reps)[:prog_length]

        program.tdm_params.append(etas_ch_rel)
        program.locked = False

        seq = []

        loop = 0
        for i, s in enumerate(program.circuit):
            # apply loss before MeasureFock
            if isinstance(s.op, MeasureFock):
                program.loop_vars.append(program.params(f"p{len(program.tdm_params) - 1}"))
                seq.append(Command(LossChannel(program.loop_vars[-1]), program.circuit[i].reg))

            seq.append(s)

            # apply loss after Sgate and BSgate
            if isinstance(s.op, Sgate):
                seq.append(Command(LossChannel(eta_glob), program.circuit[i].reg))
            if isinstance(s.op, BSgate):
                seq.append(Command(LossChannel(etas_loop[loop]), program.circuit[i].reg[1]))
                loop += 1

        program.circuit = seq
        program.lock()
Example #2
0
 def decompose(self, reg):
     """Return the decomposed commands"""
     cmds = []
     cmds += [Command(GaussianTransform(self.S), reg)]
     if self.disp:
         cmds += [
             Command(Xgate(x), reg) for x in self.d[:self.ns] if x != 0.
         ]
         cmds += [
             Command(Zgate(z), reg) for z in self.d[self.ns:] if z != 0.
         ]
     return cmds
Example #3
0
def _program_in_CJ_rep(prog, cutoff_dim: int):
    """Convert a Program object to Choi-Jamiolkowski representation.

    Doubles the number of modes of a Program object and prepends to its circuit
    the preparation of the maximally entangled ket state.

    The core idea is that when we apply any quantum channel (e.g. a unitary gate)
    to the density matrix of the maximally entangled state, we obtain the Choi matrix
    of the channel as the result.

    If the channel is unitary, applying it on the maximally entangled ket yields
    the corresponding unitary matrix, reshaped.

    Args:
        prog (Program): quantum program
        cutoff_dim (int): the Fock basis truncation

    Returns:
        Program: modified program
    """
    prog = copy.deepcopy(prog)
    prog.locked = False  # unlock the copy so we can modify it
    N = prog.init_num_subsystems
    prog._add_subsystems(N)  # pylint: disable=protected-access
    prog.init_num_subsystems = 2 * N
    I = _interleaved_identities(N, cutoff_dim)
    # prepend the circuit with the I ket preparation
    prog.circuit.insert(0, Command(Ket(I), list(prog.reg_refs.values())))
    return prog
Example #4
0
    def decompose(self, reg):
        # make BS gate
        theta = self.layer['BS'][0]
        phi = self.layer['BS'][1]
        BS = BSgate(theta, phi)

        # make cross-Kerr gate
        CK = None
        param = self.layer.get('CK', [0])[0]
        if param != 0:
            CK = CKgate(param)

        # make Kerr gate
        K = None
        param = self.layer.get('K', [0])[0]
        if param != 0:
            K = Kgate(param)

        # make rotation gate
        R = None
        param = self.layer.get('R', [0])[0]
        if param != 0:
            R = Rgate(param)

        cmds = []

        for i in range(self.num_layers):  #pylint: disable=unused-variable
            for q0, q1 in self.layer['BS'][2]:
                cmds.append(Command(BS, (reg[q0], reg[q1])))

            if CK is not None:
                for q0, q1 in self.layer['CK'][1]:
                    cmds.append(Command(CK, (reg[q0], reg[q1])))

            if K is not None:
                for mode in self.layer['K'][1]:
                    cmds.append(Command(K, reg[mode]))

            if R is not None:
                for mode in self.layer['R'][1]:
                    cmds.append(Command(R, reg[mode]))

        return cmds
Example #5
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for Gaussian boson sampling.

        This method checks whether the circuit can be implemented as a Gaussian boson sampling
        problem, i.e., if it is equivalent to a circuit A+B, where the sequence A only contains
        Gaussian operations, and B only contains Fock measurements.

        If the answer is yes, the circuit is arranged into the A+B order, and all the Fock
        measurements are combined into a single :class:`MeasureFock` operation.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to GBS
        """
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        # C should be empty
        if C:
            raise CircuitError("Operations following the Fock measurements.")

        # A should only contain Gaussian operations
        # (but this is already guaranteed by group_operations() and our primitive set)

        # without Fock measurements GBS is pointless
        if not B:
            raise CircuitError("GBS circuits must contain Fock measurements.")

        # there should be only Fock measurements in B
        measured = set()
        for cmd in B:
            if not isinstance(cmd.op, ops.MeasureFock):
                raise CircuitError(
                    "The Fock measurements are not consecutive.")

            # combine the Fock measurements
            temp = set(cmd.reg)
            if measured & temp:
                raise CircuitError("Measuring the same mode more than once.")
            measured |= temp

        # replace B with a single Fock measurement
        B = [
            Command(ops.MeasureFock(),
                    sorted(list(measured), key=lambda x: x.ind))
        ]
        return super().compile(A + B, registers)
Example #6
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into the canonical Symplectic form.

        This method checks whether the circuit can be implemented as a sequence of Gaussian operations.
        If the answer is yes it arranges them in the canonical order with displacement at the end.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to a Gaussian unitary
        """

        # Check which modes are actually being used
        used_modes = []
        for operations in seq:
            modes = [modes_label.ind for modes_label in operations.reg]
            used_modes.append(modes)
        # pylint: disable=consider-using-set-comprehension
        used_modes = list(
            set([item for sublist in used_modes for item in sublist]))

        # dictionary mapping the used modes to consecutive non-negative integers
        dict_indices = {used_modes[i]: i for i in range(len(used_modes))}
        nmodes = len(used_modes)

        # This is the identity transformation in phase-space, multiply by the identity and add zero
        Snet = np.identity(2 * nmodes)
        rnet = np.zeros(2 * nmodes)

        # Now we will go through each operation in the sequence `seq` and apply it in quadrature space
        # We will keep track of the net transforation in the Symplectic matrix `Snet` and the quadrature
        # vector `rnet`.
        for operations in seq:
            name = operations.op.__class__.__name__
            params = par_evaluate(operations.op.p)
            modes = [modes_label.ind for modes_label in operations.reg]
            if name == "Dgate":
                rnet = rnet + expand_vector(
                    params[0] *
                    (np.exp(1j * params[1])), dict_indices[modes[0]], nmodes)
            else:
                if name == "Rgate":
                    S = expand(rotation(params[0]), dict_indices[modes[0]],
                               nmodes)
                elif name == "Sgate":
                    S = expand(squeezing(params[0], params[1]),
                               dict_indices[modes[0]], nmodes)
                elif name == "S2gate":
                    S = expand(
                        two_mode_squeezing(params[0], params[1]),
                        [dict_indices[modes[0]], dict_indices[modes[1]]],
                        nmodes,
                    )
                elif name == "Interferometer":
                    S = expand(interferometer(params[0]),
                               [dict_indices[mode] for mode in modes], nmodes)
                elif name == "GaussianTransform":
                    S = expand(params[0],
                               [dict_indices[mode] for mode in modes], nmodes)
                elif name == "BSgate":
                    S = expand(
                        beam_splitter(params[0], params[1]),
                        [dict_indices[modes[0]], dict_indices[modes[1]]],
                        nmodes,
                    )
                elif name == "MZgate":
                    v = np.exp(1j * params[0])
                    u = np.exp(1j * params[1])
                    U = 0.5 * np.array([[u * (v - 1), 1j *
                                         (1 + v)], [1j * u * (1 + v), 1 - v]])
                    S = expand(
                        interferometer(U),
                        [dict_indices[modes[0]], dict_indices[modes[1]]],
                        nmodes,
                    )
                Snet = S @ Snet
                rnet = S @ rnet

        # Having obtained the net displacement we simply convert it into complex notation
        alphas = 0.5 * (rnet[0:nmodes] + 1j * rnet[nmodes:2 * nmodes])
        # And now we just pass the net transformation as a big Symplectic operation plus displacements
        ord_reg = [r for r in list(registers) if r.ind in used_modes]
        ord_reg = sorted(list(ord_reg), key=lambda x: x.ind)
        if np.allclose(Snet, np.identity(2 * nmodes)):
            A = []
        else:
            A = [Command(ops.GaussianTransform(Snet), ord_reg)]
        B = [
            Command(ops.Dgate(np.abs(alphas[i]), np.angle(alphas[i])),
                    ord_reg[i]) for i in range(len(ord_reg))
            if not np.allclose(alphas[i], 0.0)
        ]
        return A + B
Example #7
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for X8.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to X8
        """
        # pylint: disable=too-many-statements,too-many-branches

        # first do general GBS compilation to make sure
        # Fock measurements are correct
        # ---------------------------------------------
        seq = GBSSpecs().compile(seq, registers)
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        if len(B[0].reg) != self.modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))

        regrefs = set()

        if B:
            # get set of circuit registers as a tuple for each S2gate
            regrefs = {(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B}

        # the set of allowed mode-tuples the S2gates must have
        allowed_modes = set(zip(range(0, 4), range(4, 8)))

        if not regrefs.issubset(allowed_modes):
            raise CircuitError("S2gates do not appear on the correct modes.")

        # ensure provided S2gates all have the allowed squeezing values
        allowed_sq_value = {(0.0, 0.0), (self.sq_amplitude, 0.0)}
        sq_params = {(float(np.round(cmd.op.p[0], 3)), float(cmd.op.p[1]))
                     for cmd in B}

        if not sq_params.issubset(allowed_sq_value):
            wrong_params = sq_params - allowed_sq_value
            raise CircuitError(
                "Incorrect squeezing value(s) (r, phi)={}. Allowed squeezing "
                "value(s) are (r, phi)={}.".format(wrong_params,
                                                   allowed_sq_value))

        # determine which modes do not have input S2gates specified
        missing = allowed_modes - regrefs

        for i, j in missing:
            # insert S2gates with 0 squeezing
            seq.insert(0,
                       Command(ops.S2gate(0, 0), [registers[i], registers[j]]))

        # Check if matches the circuit template
        # --------------------------------------------
        # This will avoid superfluous unitary compilation.
        try:
            seq = super().compile(seq, registers)
        except CircuitError:
            # failed topology check. Continue to more general
            # compilation below.
            pass
        else:
            return seq

        # Compile the unitary: combine and then decompose all unitaries
        # -------------------------------------------------------------
        A, B, C = group_operations(
            seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate, ops.MZgate)))

        # begin unitary lists for mode [0, 1, 2, 3] and modes [4, 5, 6, 7] with
        # two identity matrices. This is because multi_dot requires
        # at least two matrices in the list.
        U_list0 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2
        U_list4 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2

        if not B:
            # no interferometer was applied
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))
            A = B  # move the S2gates to A
        else:
            for cmd in B:
                # calculate the unitary matrix representing each
                # rotation gate and each beamsplitter
                modes = [i.ind for i in cmd.reg]
                params = par_evaluate(cmd.op.p)
                U = np.identity(self.modes // 2, dtype=np.complex128)

                if isinstance(cmd.op, ops.Rgate):
                    m = modes[0]
                    U[m % 4, m % 4] = np.exp(1j * params[0])

                elif isinstance(cmd.op, ops.MZgate):
                    m, n = modes
                    U = mach_zehnder(m % 4, n % 4, params[0], params[1],
                                     self.modes // 2)

                elif isinstance(cmd.op, ops.BSgate):
                    m, n = modes

                    t = np.cos(params[0])
                    r = np.exp(1j * params[1]) * np.sin(params[0])

                    U[m % 4, m % 4] = t
                    U[m % 4, n % 4] = -np.conj(r)
                    U[n % 4, m % 4] = r
                    U[n % 4, n % 4] = t

                if set(modes).issubset({0, 1, 2, 3}):
                    U_list0.insert(0, U)
                elif set(modes).issubset({4, 5, 6, 7}):
                    U_list4.insert(0, U)
                else:
                    raise CircuitError(
                        "Unitary must be applied separately to modes [0, 1, 2, 3] and modes [4, 5, 6, 7]."
                    )

        # multiply all unitaries together, to get the final
        # unitary representation on modes [0, 1] and [2, 3].
        U0 = multi_dot(U_list0)
        U4 = multi_dot(U_list4)

        # check unitaries are equal
        if not np.allclose(U0, U4):
            raise CircuitError(
                "Interferometer on modes [0, 1, 2, 3] must be identical to interferometer on modes [4, 5, 6, 7]."
            )

        U = block_diag(U0, U4)

        # replace B with an interferometer
        B = [
            Command(ops.Interferometer(U0), registers[:4]),
            Command(ops.Interferometer(U4), registers[4:]),
        ]

        # decompose the interferometer, using Mach-Zehnder interferometers
        B = self.decompose(B)

        # Do a final circuit topology check
        # ---------------------------------
        seq = super().compile(A + B + C, registers)
        return seq
Example #8
0
    def compile(self, seq, registers):
        # the number of modes in the provided program
        n_modes = len(registers)

        # Number of modes must be even
        if n_modes % 2 != 0:
            raise CircuitError("The X series only supports programs with an even number of modes.")

        # Call the GBS compiler to do basic measurement validation.
        # The GBS compiler also merges multiple measurement commands
        # into a single MeasureFock command at the end of the circuit.
        seq = GBSSpecs().compile(seq, registers)

        # ensure that all modes are measured
        if len(seq[-1].reg) != n_modes:
            raise CircuitError("All modes must be measured.")

        # Use the GaussianUnitary compiler to compute the symplectic
        # matrix representing the Gaussian operations.
        # Note that the Gaussian unitary compiler does not accept measurements,
        # so we append the measurement separately.
        meas_seq = [seq[-1]]
        seq = GaussianUnitary().compile(seq[:-1], registers) + meas_seq

        # determine the modes that are acted on by the symplectic transformation
        used_modes = [x.ind for x in seq[0].reg]

        # extract the compiled symplectic matrix
        S = seq[0].op.p[0]

        if len(used_modes) != n_modes:
            # The symplectic transformation acts on a subset of
            # the programs registers. We must expand the symplectic
            # matrix to one that acts on all registers.
            # simply extract the computed symplectic matrix
            S = expand(seq[0].op.p[0], used_modes, n_modes)

        half_n_modes = n_modes // 2

        # Construct the covariance matrix of the state.
        # Note that hbar is a global variable that is set by the user
        cov = (sf.hbar / 2) * S @ S.T

        # Construct the A matrix
        A = Amat(cov, hbar=sf.hbar)

        # Construct the adjacency matrix represented by the A matrix.
        # This must be an weighted, undirected bipartite graph. That is,
        # B00 = B11 = 0 (no edges between the two vertex sets 0 and 1),
        # and B01 == B10.T (undirected edges between the two vertex sets).
        B = A[:n_modes, :n_modes]
        B00 = B[:half_n_modes, :half_n_modes]
        B01 = B[:half_n_modes, half_n_modes:]
        B10 = B[half_n_modes:, :half_n_modes]
        B11 = B[half_n_modes:, half_n_modes:]

        # Perform unitary validation to ensure that the
        # applied unitary is valid.

        if not np.allclose(B00, 0) or not np.allclose(B11, 0):
            # Not a bipartite graph
            raise CircuitError(
                "The applied unitary cannot mix between the modes {}-{} and modes {}-{}.".format(
                    0, half_n_modes - 1, half_n_modes, n_modes - 1
                )
            )

        if not np.allclose(B01, B10):
            # Not a symmetric bipartite graph
            raise CircuitError(
                "The applied unitary on modes {}-{} must be identical to the applied unitary on modes {}-{}.".format(
                    0, half_n_modes - 1, half_n_modes, n_modes - 1
                )
            )

        # Now that the unitary has been validated, perform the Takagi decomposition
        # to determine the constituent two-mode squeezing and interferometer
        # parameters.
        sqs, U = takagi(B01)
        sqs = np.arctanh(sqs)

        # ensure provided S2gates all have the allowed squeezing values
        if not all(s in self.allowed_sq_ranges for s in sqs):
            wrong_sq_values = [np.round(s, 4) for s in sqs if s not in self.allowed_sq_ranges]
            raise CircuitError(
                "Incorrect squeezing value(s) r={}. Allowed squeezing "
                "value(s) are {}.".format(wrong_sq_values, self.allowed_sq_ranges)
            )

        # Convert the squeezing values into a sequence of S2gate commands
        sq_seq = [
            Command(ops.S2gate(sqs[i]), [registers[i], registers[i + half_n_modes]])
            for i in range(half_n_modes)
        ]

        # NOTE: at some point, it might make sense to add a keyword argument to this method,
        # to allow the user to specify if they want the interferometers decomposed or not.

        # Convert the unitary into a sequence of MZgate and Rgate commands on the signal modes
        U1 = ops.Interferometer(U, mesh="rectangular_symmetric", drop_identity=False)._decompose(
            registers[:half_n_modes]
        )
        U2 = copy.deepcopy(U1)

        for Ui in U2:
            Ui.reg = [registers[r.ind + half_n_modes] for r in Ui.reg]

        return sq_seq + U1 + U2 + meas_seq
Example #9
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for Chip0.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to Chip0
        """
        # pylint: disable=too-many-statements,too-many-branches
        # First, check if provided sequence matches the circuit template.
        # This will avoid superfluous compilation if the user is using the
        # template directly.
        try:
            seq = super().compile(seq, registers)
        except CircuitError:
            # failed topology check. Continue to more general
            # compilation below.
            pass
        else:
            return seq

        # first do general GBS compilation to make sure
        # Fock measurements are correct
        # ---------------------------------------------
        seq = GBSSpecs().compile(seq, registers)
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        if len(B[0].reg) != self.modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))

        if A:
            raise CircuitError("Circuits must start with two S2gates.")

        # get circuit registers
        regrefs = {q for cmd in B for q in cmd.reg}

        if len(regrefs) != self.modes:
            raise CircuitError("S2gates do not appear on the correct modes.")

        # Compile the unitary: combine and then decompose all unitaries
        # -------------------------------------------------------------
        A, B, C = group_operations(
            seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate)))

        # begin unitary lists for mode [0, 1] and modes [2, 3] with
        # two identity matrices. This is because multi_dot requires
        # at least two matrices in the list.
        U_list01 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2
        U_list23 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2

        if not B:
            # no interferometer was applied
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))
            A = B  # move the S2gates to A
        else:
            for cmd in B:
                # calculate the unitary matrix representing each
                # rotation gate and each beamsplitter
                # Note: this is done separately on modes [0, 1]
                # and modes [2, 3]
                modes = [i.ind for i in cmd.reg]
                params = [i.x for i in cmd.op.p]
                U = np.identity(self.modes // 2, dtype=np.complex128)

                if isinstance(cmd.op, ops.Rgate):
                    m = modes[0]
                    U[m % 2, m % 2] = np.exp(1j * params[0])

                elif isinstance(cmd.op, ops.BSgate):
                    m, n = modes

                    t = np.cos(params[0])
                    r = np.exp(1j * params[1]) * np.sin(params[0])

                    U[m % 2, m % 2] = t
                    U[m % 2, n % 2] = -np.conj(r)
                    U[n % 2, m % 2] = r
                    U[n % 2, n % 2] = t

                if set(modes).issubset({0, 1}):
                    U_list01.insert(0, U)
                elif set(modes).issubset({2, 3}):
                    U_list23.insert(0, U)
                else:
                    raise CircuitError(
                        "Unitary must be applied separately to modes [0, 1] and modes [2, 3]."
                    )

        # multiply all unitaries together, to get the final
        # unitary representation on modes [0, 1] and [2, 3].
        U01 = multi_dot(U_list01)
        U23 = multi_dot(U_list23)

        # check unitaries are equal
        if not np.allclose(U01, U23):
            raise CircuitError(
                "Interferometer on modes [0, 1] must be identical to interferometer on modes [2, 3]."
            )

        U = block_diag(U01, U23)

        # replace B with an interferometer
        B = [
            Command(ops.Interferometer(U01), registers[:2]),
            Command(ops.Interferometer(U23), registers[2:]),
        ]

        # decompose the interferometer, using Mach-Zehnder interferometers
        B = self.decompose(B)

        # Do a final circuit topology check
        # ---------------------------------
        seq = super().compile(A + B + C, registers)
        return seq
Example #10
0
    def compile(self, seq, registers):
        """Try to arrange a passive circuit into a single multimode passive operation

        This method checks whether the circuit can be implemented as a sequence of passive gates.
        If the answer is yes it arranges them into a single operation.

        Args:
            seq (Sequence[Command]): passive quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: compiled circuit
        Raises:
            CircuitError: the circuit does not correspond to a passive unitary
        """

        # Check which modes are actually being used
        used_modes = []
        for operations in seq:
            modes = [modes_label.ind for modes_label in operations.reg]
            used_modes.append(modes)

        used_modes = list(
            set(item for sublist in used_modes for item in sublist))

        # dictionary mapping the used modes to consecutive non-negative integers
        dict_indices = {used_modes[i]: i for i in range(len(used_modes))}
        nmodes = len(used_modes)

        # We start with an identity then sequentially update with the gate transformations
        T = np.identity(nmodes, dtype=np.complex128)

        # Now we will go through each operation in the sequence `seq` and apply it to T
        for operations in seq:
            name = operations.op.__class__.__name__
            params = par_evaluate(operations.op.p)
            modes = [modes_label.ind for modes_label in operations.reg]
            if name == "Rgate":
                G = np.exp(1j * params[0])
                T = _apply_one_mode_gate(G, T, dict_indices[modes[0]])
            elif name == "LossChannel":
                G = np.sqrt(params[0])
                T = _apply_one_mode_gate(G, T, dict_indices[modes[0]])
            elif name == "Interferometer":
                U = params[0]
                if U.shape == (1, 1):
                    T = _apply_one_mode_gate(U[0, 0], T,
                                             dict_indices[modes[0]])
                elif U.shape == (2, 2):
                    T = _apply_two_mode_gate(U, T, dict_indices[modes[0]],
                                             dict_indices[modes[1]])
                else:
                    modes = [dict_indices[mode] for mode in modes]
                    U_expand = np.eye(nmodes, dtype=np.complex128)
                    U_expand[np.ix_(modes, modes)] = U
                    T = U_expand @ T
            elif name == "PassiveChannel":
                T0 = params[0]
                if T0.shape == (1, 1):
                    T = _apply_one_mode_gate(T0[0, 0], T,
                                             dict_indices[modes[0]])
                elif T0.shape == (2, 2):
                    T = _apply_two_mode_gate(T0, T, dict_indices[modes[0]],
                                             dict_indices[modes[1]])
                else:
                    modes = [dict_indices[mode] for mode in modes]
                    T0_expand = np.eye(nmodes, dtype=np.complex128)
                    T0_expand[np.ix_(modes, modes)] = T0
                    T = T0_expand @ T
            elif name == "BSgate":
                G = _beam_splitter_passive(params[0], params[1])
                T = _apply_two_mode_gate(G, T, dict_indices[modes[0]],
                                         dict_indices[modes[1]])
            elif name == "MZgate":
                v = np.exp(1j * params[0])
                u = np.exp(1j * params[1])
                U = 0.5 * np.array([[u * (v - 1), 1j *
                                     (1 + v)], [1j * u * (1 + v), 1 - v]])
                T = _apply_two_mode_gate(U, T, dict_indices[modes[0]],
                                         dict_indices[modes[1]])
            elif name == "sMZgate":
                exp_sigma = np.exp(1j * (params[0] + params[1]) / 2)
                delta = (params[0] - params[1]) / 2
                U = exp_sigma * np.array([[np.sin(delta),
                                           np.cos(delta)],
                                          [np.cos(delta), -np.sin(delta)]])
                T = _apply_two_mode_gate(U, T, dict_indices[modes[0]],
                                         dict_indices[modes[1]])

        ord_reg = [r for r in list(registers) if r.ind in used_modes]
        ord_reg = sorted(list(ord_reg), key=lambda x: x.ind)

        return [Command(ops.PassiveChannel(T), ord_reg)]
    def compile(self, seq, registers):
        # the number of modes in the provided program
        n_modes = len(registers)

        # Number of modes must be even
        if n_modes % 2 != 0:
            raise CircuitError(
                "The X series only supports programs with an even number of modes."
            )
        half_n_modes = n_modes // 2
        # Call the GBS compiler to do basic measurement validation.
        # The GBS compiler also merges multiple measurement commands
        # into a single MeasureFock command at the end of the circuit.
        seq = GBS().compile(seq, registers)

        # ensure that all modes are measured
        if len(seq[-1].reg) != n_modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two-mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))
        # If there are no two-mode squeezers add squeezers at the beginning with squeezing param equal to zero.
        if B == []:
            initS2 = [
                Command(ops.S2gate(0, 0),
                        [registers[i], registers[i + half_n_modes]])
                for i in range(half_n_modes)
            ]
            seq = initS2 + seq
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))

        if A != []:
            raise CircuitError(
                "There can be no operations before the S2gates.")

        regrefs = set()

        if B:
            # get set of circuit registers as a tuple for each S2gate
            regrefs = {(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B}

        # the set of allowed mode-tuples the S2gates must have
        allowed_modes = set(
            zip(range(0, half_n_modes), range(half_n_modes, n_modes)))

        if not regrefs.issubset(allowed_modes):
            raise CircuitError("S2gates do not appear on the correct modes.")

        # determine which modes do not have input S2gates specified
        missing = allowed_modes - regrefs

        for i, j in missing:
            # insert S2gates with 0 squeezing
            B.insert(0, Command(ops.S2gate(0, 0),
                                [registers[i], registers[j]]))

        # get list of circuit registers as a tuple for each S2gate
        regrefs = [(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B]

        # merge S2gates
        if len(regrefs) > half_n_modes:
            for mode, indices in list_duplicates(regrefs):
                r = 0
                phi = 0

                for k, i in enumerate(sorted(indices, reverse=True)):
                    removed_cmd = B.pop(i)
                    r += removed_cmd.op.p[0]
                    phi_new = removed_cmd.op.p[1]

                    if k > 0 and phi_new != phi:
                        raise CircuitError(
                            "Cannot merge S2gates with different phase values."
                        )

                    phi = phi_new

                i, j = mode
                B.insert(
                    indices[0],
                    Command(ops.S2gate(r, phi), [registers[i], registers[j]]))

        meas_seq = [C[-1]]
        seq = GaussianUnitary().compile(C[:-1], registers)

        # extract the compiled symplectic matrix
        if seq == []:
            S = np.identity(2 * n_modes)
            used_modes = list(range(n_modes))
        else:
            S = seq[0].op.p[0]
            # determine the modes that are acted on by the symplectic transformation
            used_modes = [x.ind for x in seq[0].reg]

        if not np.allclose(S @ S.T, np.identity(len(S))):
            raise CircuitError(
                "The operations after squeezing do not correspond to an interferometer."
            )

        if len(used_modes) != n_modes:
            # The symplectic transformation acts on a subset of
            # the programs registers. We must expand the symplectic
            # matrix to one that acts on all registers.
            # simply extract the computed symplectic matrix
            S = expand(seq[0].op.p[0], used_modes, n_modes)

        U = S[:n_modes, :n_modes] - 1j * S[:n_modes, n_modes:]
        U11 = U[:half_n_modes, :half_n_modes]
        U12 = U[:half_n_modes, half_n_modes:]
        U21 = U[half_n_modes:, :half_n_modes]
        U22 = U[half_n_modes:, half_n_modes:]
        if not np.allclose(U12, 0) or not np.allclose(U21, 0):
            # Not a bipartite graph
            raise CircuitError(
                "The applied unitary cannot mix between the modes {}-{} and modes {}-{}."
                .format(0, half_n_modes - 1, half_n_modes, n_modes - 1))

        if not np.allclose(U11, U22):
            # Not a symmetric bipartite graph
            raise CircuitError(
                "The applied unitary on modes {}-{} must be identical to the applied unitary on modes {}-{}."
                .format(0, half_n_modes - 1, half_n_modes, n_modes - 1))
        U1 = ops.Interferometer(U11,
                                mesh="rectangular_symmetric",
                                drop_identity=False)._decompose(
                                    registers[:half_n_modes])
        U2 = copy.deepcopy(U1)

        for Ui in U2:
            Ui.reg = [registers[r.ind + half_n_modes] for r in Ui.reg]

        return B + U1 + U2 + meas_seq