def test_rectangular_MZ(self, U, tol):
        """This test checks the function :func:`dec.rectangular_MZ` for
        various unitary matrices.

        A given unitary (identity or random draw from Haar measure) is
        decomposed using the function :func:`dec.rectangular_MZ`
        and the resulting beamsplitters are multiplied together.

        Test passes if the product matches the given unitary.
        """
        nmax, mmax = U.shape
        assert nmax == mmax

        tilist, diags, tlist = dec.rectangular_MZ(U)

        qrec = np.identity(nmax)

        for i in tilist:
            qrec = dec.mach_zehnder(*i) @ qrec

        qrec = np.diag(diags) @ qrec

        for i in reversed(tlist):
            qrec = dec.mach_zehnder_inv(*i) @ qrec

        assert np.allclose(U, qrec, atol=tol, rtol=0)
    def test_decomposition(self, U, tol):
        """This test checks the function :func:`dec.rectangular_symmetric` for
        various unitary matrices.

        A given unitary (identity or random draw from Haar measure) is
        decomposed using the function :func:`dec.rectangular_symmetric`
        and the resulting beamsplitters are multiplied together.

        Test passes if the product matches the given unitary.
        """
        nmax, mmax = U.shape
        assert nmax == mmax
        tlist, diags, _ = dec.rectangular_symmetric(U)
        qrec = np.identity(nmax)
        for i in tlist:
            assert i[2] >= 0 and i[2] < 2 * np.pi  # internal phase
            assert i[3] >= 0 and i[3] < 2 * np.pi  # external phase
            qrec = dec.mach_zehnder(*i) @ qrec
        qrec = np.diag(diags) @ qrec
        assert np.allclose(U, qrec, atol=tol, rtol=0)
Exemple #3
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