def binary_arithmetic(pp, qq, p, q):
    """Test the correctness of basic binary arithmetic expressions."""
    assert par_evaluate(pp + qq) == pytest.approx(p + q)
    assert par_evaluate(pp - qq) == pytest.approx(p - q)
    assert par_evaluate(pp * qq) == pytest.approx(p * q)
    assert par_evaluate(pp / qq) == pytest.approx(p / q)
    assert par_evaluate(pp**qq) == pytest.approx(p**q)
    def test_par_evaluate_dtype_TF(self, p, dtype):
        """Test the TF parameter evaluation works when a dtype is provided"""
        pytest.importorskip("tensorflow", minversion="2.0")
        import tensorflow as tf

        x = FreeParameter("x")
        x.val = tf.Variable(p)
        res = par_evaluate(x, dtype=dtype)
        assert res.dtype is tf.as_dtype(dtype)
    def test_par_evaluate(self, p):
        x = FreeParameter("x")
        with pytest.raises(ParameterError, match="unbound parameter with no default value"):
            par_evaluate(x)

        # val only
        x.val = p
        assert np.all(par_evaluate(x) == p)

        # default only
        x.val = None
        x.default = p
        assert np.all(par_evaluate(x) == p)

        # both val and default
        x.val = p
        x.default = 0.0
        assert np.all(par_evaluate(x) == p)
    def test_zgate_decompose(self, backend, hbar, applied_cmds):
        """Test parameter processing occuring within the Zgate._decompose method."""
        import tensorflow as tf

        mapping = {"p": tf.Variable(0.1)}
        prog = self.create_program(sf.ops.Zgate, mapping)

        # verify bound parameters are correct
        assert prog.free_params["p"].val is mapping["p"]

        # assert executed program is constructed correctly
        eng = sf.LocalEngine(backend)
        result = eng.run(prog, args=mapping)

        assert len(applied_cmds) == 1
        assert isinstance(applied_cmds[0].op, sf.ops.Dgate)
        assert par_evaluate(applied_cmds[0].op.p[0]) == mapping["p"] / np.sqrt(2 * hbar)
        assert applied_cmds[0].op.p[1] == np.pi / 2
Esempio n. 5
0
    def test_gate_dagger(self, G, monkeypatch):
        """Test the dagger functionality of the gates"""
        G2 = G.H
        assert not G.dagger
        assert G2.dagger

        def dummy_apply(self, reg, backend, **kwargs):
            """Dummy apply function, used to store the evaluated params"""
            self.res = par_evaluate(self.p)

        with monkeypatch.context() as m:
            # patch the standard Operation class apply method
            # with our dummy method, that stores the applied parameter
            # in the attribute res. This allows us to extract
            # and verify the parameter was properly negated.
            m.setattr(G2.__class__, "_apply", dummy_apply)
            G2.apply([], None)

        orig_params = par_evaluate(G2.p)
        applied_params = G2.res
        # dagger should negate the first param
        assert applied_params == [-orig_params[0]] + orig_params[1:]
Esempio n. 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
Esempio n. 7
0
 def dummy_apply(self, reg, backend, **kwargs):
     """Dummy apply function, used to store the evaluated params"""
     self.res = par_evaluate(self.p)
 def test_parameter_unary_negation(self, p):
     """Test unary negation works as expected."""
     pp = FreeParameter("x")
     pp.val = p
     assert par_evaluate(-p) == pytest.approx(-p)
     assert par_evaluate(-pp) == pytest.approx(-p)
 def test_par_evaluate_dtype_numpy(self, p, dtype):
     """Test the numpy parameter evaluation works when a dtype is provided"""
     x = FreeParameter("x")
     x.val = p
     res = par_evaluate(x, dtype=dtype)
     assert res.dtype.type is dtype
Esempio n. 10
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
def program_equivalence(prog1, prog2, compare_params=True, atol=1e-6, rtol=0):
    r"""Checks if two programs are equivalent.

    This function converts the program lists into directed acyclic graphs,
    and runs the NetworkX `is_isomorphic` graph function in order
    to determine if the two programs are equivalent.

    Note: when checking for parameter equality between two parameters
    :math:`a` and :math:`b`, we use the following formula:

    .. math:: |a - b| \leq (\texttt{atol} + \texttt{rtol}\times|b|)

    Args:
        prog1 (strawberryfields.program.Program): quantum program
        prog2 (strawberryfields.program.Program): quantum program
        compare_params (bool): Set to ``False`` to turn of comparing
            program parameters; equivalency will only take into
            account the operation order.
        atol (float): the absolute tolerance parameter for checking
            quantum operation parameter equality
        rtol (float): the relative tolerance parameter for checking
            quantum operation parameter equality

    Returns:
        bool: returns ``True`` if two quantum programs are equivalent
    """
    DAG1 = list_to_DAG(prog1.circuit)
    DAG2 = list_to_DAG(prog2.circuit)

    circuit = []
    for G in [DAG1, DAG2]:
        # relabel the DAG nodes to integers
        circuit.append(nx.convert_node_labels_to_integers(G))

        # add node attributes to store the operation name and parameters
        name_mapping = {i: n.op.__class__.__name__ for i, n in enumerate(G.nodes())}
        parameter_mapping = {i: par_evaluate(n.op.p) for i, n in enumerate(G.nodes())}

        # CXgate and BSgate are not symmetric wrt permuting the order of the two
        # modes it acts on; i.e., the order of the wires matter
        wire_mapping = {}
        for i, n in enumerate(G.nodes()):
            if n.op.__class__.__name__ == "CXgate":
                if np.allclose(n.op.p[0], 0):
                    # if the CXgate parameter is 0, wire order doesn't matter
                    wire_mapping[i] = 0
                else:
                    # if the CXgate parameter is not 0, order matters
                    wire_mapping[i] = [j.ind for j in n.reg]

            elif n.op.__class__.__name__ == "BSgate":
                if np.allclose([j % np.pi for j in par_evaluate(n.op.p)], [np.pi / 4, np.pi / 2]):
                    # if the beamsplitter is *symmetric*, then the order of the
                    # wires does not matter.
                    wire_mapping[i] = 0
                else:
                    # beamsplitter is not symmetric, order matters
                    wire_mapping[i] = [j.ind for j in n.reg]

            else:
                # not a CXgate or a BSgate, order of wires doesn't matter
                wire_mapping[i] = 0

        # TODO: at the moment, we do not check for whether an empty
        # wire will match an operation with trivial parameters.
        # Maybe we can do this in future, but this is a subgraph
        # isomorphism problem and much harder.

        nx.set_node_attributes(circuit[-1], name_mapping, name="name")
        nx.set_node_attributes(circuit[-1], parameter_mapping, name="p")
        nx.set_node_attributes(circuit[-1], wire_mapping, name="w")

    def node_match(n1, n2):
        """Returns True if both nodes have the same name and
        same parameters, within a certain tolerance"""
        name_match = n1["name"] == n2["name"]
        p_match = np.allclose(n1["p"], n2["p"], atol=atol, rtol=rtol)
        wire_match = n1["w"] == n2["w"]

        if compare_params:
            return name_match and p_match and wire_match

        return name_match and wire_match

    # check if circuits are equivalent
    return nx.is_isomorphic(circuit[0], circuit[1], node_match)
Esempio n. 12
0
def to_xir(prog: Program, **kwargs) -> xir.Program:
    """Convert a Strawberry Fields Program to an XIR Program.

    Args:
        prog (Program): the Strawberry Fields program

    Keyword Args:
        add_decl (bool): Whether gate and output declarations should be added to
            the XIR program. Default is ``False``.

    Returns:
        xir.Program
    """
    xir_prog = xir.Program()
    add_decl = kwargs.get("add_decl", False)

    if isinstance(prog, TDMProgram):
        xir_prog.add_option("_type_", "tdm")
        xir_prog.add_option("N", prog.N)
        for i, p in enumerate(prog.tdm_params):
            xir_prog.add_constant(f"p{i}", _listr(p))

    if prog.name:
        xir_prog.add_option("_name_", prog.name)
    if prog.target:
        xir_prog.add_option("target", prog.target)  # pylint: disable=protected-access
    if "cutoff_dim" in prog.backend_options:
        xir_prog.add_option("cutoff_dim", prog.backend_options["cutoff_dim"])
    if "shots" in prog.run_options:
        xir_prog.add_option("shots", prog.run_options["shots"])

    # fill in the quantum circuit
    for cmd in prog.circuit or []:

        name = cmd.op.__class__.__name__
        wires = tuple(i.ind for i in cmd.reg)

        if "Measure" in name:
            if add_decl:
                output_decl = xir.Declaration(name, type_="out", wires=wires)
                xir_prog.add_declaration(output_decl)

            params = {}
            if cmd.op.p:
                # argument is quadrature phase
                a = cmd.op.p[0]
                if a in getattr(prog, "loop_vars", ()):
                    params["phi"] = a.name
                else:
                    params["phi"] = a

            # special case to take into account 'select' keyword argument
            if cmd.op.select is not None:
                params["select"] = cmd.op.select

            if name == "MeasureFock":
                # special case to take into account 'dark_counts' keyword argument
                if cmd.op.dark_counts is not None:
                    params["dark_counts"] = cmd.op.dark_counts
        else:
            if add_decl:
                if name not in [
                        gdecl.name for gdecl in xir_prog.declarations["gate"]
                ]:
                    params = [f"p{i}" for i, _ in enumerate(cmd.op.p)]
                    gate_decl = xir.Declaration(name,
                                                type_="gate",
                                                params=params,
                                                wires=tuple(range(len(wires))))
                    xir_prog.add_declaration(gate_decl)

            params = []
            for i, a in enumerate(cmd.op.p):
                if sfpar.par_is_symbolic(a):
                    # try to evaluate symbolic parameter
                    try:
                        a = sfpar.par_evaluate(a)
                    except sfpar.ParameterError:
                        # if a tdm param
                        if a in getattr(prog, "loop_vars", ()):
                            a = a.name
                        # if a pure symbol (free parameter), convert to string
                        elif a.is_symbol:
                            a = a.name
                        # else, assume it's a symbolic function and replace all free parameters
                        # with string representations
                        else:
                            symbolic_func = a.copy()
                            for s in symbolic_func.free_symbols:
                                symbolic_func = symbolic_func.subs(s, s.name)
                            a = str(symbolic_func)

                elif isinstance(a, str):
                    pass
                elif isinstance(a, Iterable):
                    # if an iterable, make sure it only consists of lists and Python types
                    a = _listr(a)
                params.append(a)

        op = xir.Statement(name, params, wires)
        xir_prog.add_statement(op)

    return xir_prog
Esempio n. 13
0
    def test_decomposition(self, tol):
        """Test that a graph is correctly decomposed"""
        n = 3
        prog = sf.Program(2*n)

        A = np.zeros([2*n, 2*n])
        B = np.random.random([n, n])

        A[:n, n:] = B
        A += A.T

        sq, U, V = dec.bipartite_graph_embed(B)

        G = ops.BipartiteGraphEmbed(A)
        cmds = G.decompose(prog.register)

        S = np.identity(4 * n)

        # calculating the resulting decomposed symplectic
        for cmd in cmds:
            # all operations should be BSgates, Rgates, or S2gates
            assert isinstance(
                cmd.op, (ops.Interferometer, ops.S2gate)
            )

            # build up the symplectic transform
            modes = [i.ind for i in cmd.reg]

            if isinstance(cmd.op, ops.S2gate):
                # check that the registers are i, i+n
                assert len(modes) == 2
                assert modes[1] == modes[0] + n

                r, phi = par_evaluate(cmd.op.p)
                assert -r in sq
                assert phi == 0

                S = _two_mode_squeezing(r, phi, modes, 2*n) @ S

            if isinstance(cmd.op, ops.Interferometer):
                # check that each unitary only applies to half the modes
                assert len(modes) == n
                assert modes in ([0, 1, 2], [3, 4, 5])

                # check matrix is unitary
                U1 = par_evaluate(cmd.op.p[0])
                assert np.allclose(U1 @ U1.conj().T, np.identity(n), atol=tol, rtol=0)

                if modes[0] == 0:
                    assert np.allclose(U1, U, atol=tol, rtol=0)
                else:
                    assert modes[0] == 3
                    assert np.allclose(U1, V, atol=tol, rtol=0)

                S_U = np.vstack(
                    [np.hstack([U1.real, -U1.imag]), np.hstack([U1.imag, U1.real])]
                )

                S = expand(S_U, modes, 2*n) @ S

        # the resulting covariance state
        cov = S @ S.T
        A_res = Amat(cov)[:2*n, :2*n]

        # The bottom right corner of A_res should be identical to A,
        # up to some constant scaling factor. Check if the ratio
        # of all elements is one
        ratio = np.real_if_close(A_res[n:, :n] / B.T)
        ratio /= ratio[0, 0]

        assert np.allclose(ratio, np.ones([n, n]), atol=tol, rtol=0)
Esempio n. 14
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)]
Esempio n. 15
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 = 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 % 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