Esempio n. 1
0
    def compile(self, seq, registers):
        """Class-specific circuit compilation method.

        If additional compilation logic is required, child classes can redefine this method.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the given circuit cannot be validated to belong to this circuit class
        """
        # registers is not used here, but may be used if the method is overwritten pylint: disable=unused-argument
        if self.graph is not None:
            # check topology
            DAG = pu.list_to_DAG(seq)

            # relabel the DAG nodes to integers, with attributes
            # specifying the operation name. This allows them to be
            # compared, rather than using Command objects.
            mapping = {
                i: n.op.__class__.__name__
                for i, n in enumerate(DAG.nodes())
            }
            circuit = nx.convert_node_labels_to_integers(DAG)
            nx.set_node_attributes(circuit, mapping, name="name")

            def node_match(n1, n2):
                """Returns True if both nodes have the same name"""
                return n1["name"] == n2["name"]

            # check if topology matches
            if not nx.is_isomorphic(circuit, self.graph, node_match):
                # TODO: try and compile the program to match the topology
                # TODO: add support for parameter range matching/compilation
                raise pu.CircuitError(
                    "Program cannot be used with the compiler '{}' "
                    "due to incompatible topology.".format(self.short_name))

        return seq
Esempio n. 2
0
    def compile(self, seq):
        """Device-specific compilation method.

        If additional compilation logic is required, child classes can redefine this method.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit is not valid for the device
        """
        if self.graph is not None:
            # check topology
            DAG = pu.list_to_DAG(seq)

            # relabel the DAG nodes to integers, with attributes
            # specifying the operation name. This allows them to be
            # compared, rather than using Command objects.
            mapping = {
                i: n.op.__class__.__name__
                for i, n in enumerate(DAG.nodes())
            }
            circuit = nx.convert_node_labels_to_integers(DAG)
            nx.set_node_attributes(circuit, mapping, name='name')

            def node_match(n1, n2):
                """Returns True if both nodes have the same name"""
                return n1['name'] == n2['name']

            # check if topology matches
            if not nx.is_isomorphic(circuit, self.graph, node_match):
                # TODO: try and compile the program to match the topology
                # TODO: add support for parameter range matching/compilation
                raise pu.CircuitError(
                    "Program cannot be used with the device '{}' "
                    "due to incompatible topology.".format(self.short_name))

        return seq
Esempio n. 3
0
    def decompose(self, seq: Sequence[Command]) -> Sequence[Command]:
        """Recursively decompose all gates in a given sequence, as allowed
        by the circuit specification.

        This method follows the directives defined in the
        :attr:`~.Compiler.primitives` and :attr:`~.Compiler.decompositions`
        class attributes to determine whether a command should be decomposed.

        The order of precedence to determine whether decomposition
        should be applied is as follows.

        1. First, we check if the operation is in :attr:`~.Compiler.decompositions`.
           If not, decomposition is skipped, and the operation is applied
           as a primitive (if supported by the ``Compiler``).

        2. Next, we check if (a) the operation supports decomposition, and (b) if the user
           has explicitly requested no decomposition.

           - If both (a) and (b) are true, the operation is applied
             as a primitive (if supported by the ``Compiler``).

           - Otherwise, we attempt to decompose the operation by calling
             :meth:`~.Operation.decompose` recursively.

        Args:
            list[strawberryfields.program_utils.Command]: list of commands to
                be decomposed

        Returns:
            list[strawberryfields.program_utils.Command]: list of compiled commands
            for the circuit specification
        """
        compiled = []
        for cmd in seq:
            op_name = cmd.op.__class__.__name__
            if op_name in self.decompositions:
                # target can implement this op decomposed
                if hasattr(cmd.op, "decomp") and not cmd.op.decomp:
                    # user has requested application of the op as a primitive
                    if op_name in self.primitives:
                        compiled.append(cmd)
                        continue

                    raise pu.CircuitError(
                        "The operation {} is not a primitive for the compiler '{}'"
                        .format(cmd.op.__class__.__name__, self.short_name))
                try:
                    kwargs = self.decompositions[op_name]
                    temp = cmd.op.decompose(cmd.reg, **kwargs)
                    # now compile the decomposition
                    temp = self.decompose(temp)
                    compiled.extend(temp)
                except NotImplementedError as err:
                    # Operation does not have _decompose() method defined!
                    # simplify the error message by suppressing the previous exception
                    raise err from None

            elif op_name in self.primitives:
                # target can handle the op natively
                compiled.append(cmd)

            else:
                raise pu.CircuitError(
                    "The operation {} cannot be used with the compiler '{}'.".
                    format(cmd.op.__class__.__name__, self.short_name))

        return compiled
Esempio n. 4
0
    def compile(self, seq: Sequence[Command],
                registers: Sequence[RegRef]) -> Sequence[Command]:
        """Class-specific circuit compilation method.

        If additional compilation logic is required, child classes can redefine this method.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRef]): quantum registers

        Returns:
            Sequence[Command]: modified circuit

        Raises:
            CircuitError: the given circuit cannot be validated to belong to this circuit class
        """
        # registers is not used here, but may be used if the method is overwritten pylint: disable=unused-argument
        if self.graph is not None:
            # check topology
            DAG = pu.list_to_DAG(seq)

            # relabel the DAG nodes to integers, with attributes
            # specifying the operation name. This allows them to be
            # compared, rather than using Command objects.
            mapping_name, mapping_args, mapping_modes = {}, {}, {}
            for i, n in enumerate(DAG.nodes()):
                mapping_name[i] = n.op.__class__.__name__
                mapping_args[i] = n.op.p
                mapping_modes[i] = tuple(m.ind for m in n.reg)

            circuit = nx.convert_node_labels_to_integers(DAG)
            nx.set_node_attributes(circuit, mapping_name, name="name")
            nx.set_node_attributes(circuit, mapping_args, name="args")
            nx.set_node_attributes(circuit, mapping_modes, name="modes")

            def node_match(n1, n2):
                """Returns True if both nodes have the same name and modes"""
                return n1["name"] == n2["name"] and n1["modes"] == n2["modes"]

            GM = nx.algorithms.isomorphism.DiGraphMatcher(
                self.graph, circuit, node_match)

            # check if topology matches
            if not GM.is_isomorphic():
                raise pu.CircuitError(
                    "Program cannot be used with the compiler '{}' "
                    "due to incompatible topology.".format(self.short_name))

            # check if hard-coded parameters match
            G1nodes = self.graph.nodes().data()
            G2nodes = circuit.nodes().data()

            for n1, n2 in GM.mapping.items():
                for x, y in zip(G1nodes[n1]["args"], G2nodes[n2]["args"]):
                    if x != y and not (isinstance(x, sym.Symbol)
                                       or isinstance(y, sym.Expr)):
                        raise CircuitError(
                            "Program cannot be used with the compiler '{}' "
                            "due to incompatible parameter values.".format(
                                self.short_name))

        return seq