Exemple #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
    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
    def compile(self, *, device=None, compiler=None, **kwargs):
        """Compile the program given a Strawberry Fields photonic compiler, or
        hardware device specification.

        The compilation process can involve up to three stages:

        1. **Validation:** Validates properties of the program, including number of modes and
           allowed operations, making sure all the :doc:`/introduction/ops` used are accepted by the
           compiler.

        2. **Decomposition:** Once the program has been validated, decomposition are performed,
           transforming certain gates into sequences of simpler gates.

        3. **General compilation:** Finally, the compiler might specify bespoke compilation logic
           for transforming the  quantum circuit into an equivalent circuit which can be executed
           by the target device.

        **Example:**

        The ``gbs`` compile target will
        compile a circuit consisting of Gaussian operations and Fock measurements
        into canonical Gaussian boson sampling form.

        >>> prog2 = prog.compile(compiler="gbs")

        For a hardware device a :class:`~.DeviceSpec` object, and optionally a specified compile strategy,
        must be supplied. If no compile strategy is supplied the default compiler from the device
        specification is used.

        >>> eng = sf.RemoteEngine("X8")
        >>> device = eng.device_spec
        >>> prog2 = prog.compile(device=device, compiler="Xcov")

        Args:
            device (~strawberryfields.DeviceSpec): device specification object to use for
                program compilation
            compiler (str, ~strawberryfields.compilers.Compiler): Compiler name or compile strategy
                to use. If a device is specified, this overrides the compile strategy specified by
                the hardware :class:`~.DeviceSpec`.

        Keyword Args:
            optimize (bool): If True, try to optimize the program by merging and canceling gates.
                The default is False.
            warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
                connected. The default is True.
            shots (int): Number of times the program measurement evaluation is repeated. Passed
                along to the compiled program's ``run_options``.

        Returns:
            Program: compiled program
        """
        # pylint: disable=too-many-branches
        if device is None and compiler is None:
            raise ValueError(
                "Either one or both of 'device' and 'compiler' must be specified"
            )

        def _get_compiler(compiler_or_name):
            if compiler_or_name in compiler_db:
                return compiler_db[compiler_or_name]()

            if isinstance(compiler_or_name, Compiler):
                return compiler_or_name

            raise ValueError(f"Unknown compiler '{compiler_or_name}'.")

        if device is not None:
            target = device.target

            if compiler is None:
                # get the default compiler from the device spec
                compiler = compiler_db[device.default_compiler]()
            else:
                compiler = _get_compiler(compiler)

            if device.modes is not None:
                if isinstance(device.modes, int):
                    # check that the number of modes is correct, if device.modes
                    # is provided as an integer
                    self.assert_number_of_modes(device)
                else:
                    # check that the number of measurements is within the allowed
                    # limits for each measurement type; device.modes will be a dictionary
                    self.assert_max_number_of_measurements(device)

        else:
            compiler = _get_compiler(compiler)
            target = compiler.short_name

        seq = compiler.decompose(self.circuit)

        if kwargs.get("warn_connected", True):
            DAG = pu.list_to_DAG(seq)
            temp = nx.algorithms.components.number_weakly_connected_components(
                DAG)
            if temp > 1:
                warnings.warn(
                    "The circuit consists of {} disconnected components.".
                    format(temp))

        # run optimizations
        if kwargs.get("optimize", False):
            seq = pu.optimize_circuit(seq)

        seq = compiler.compile(seq, self.register)

        # create the compiled Program
        compiled = self._linked_copy()
        compiled.circuit = seq
        compiled._target = target
        compiled._compile_info = (device, compiler.short_name)

        # Get run options of compiled program.
        run_options = {
            k: kwargs[k]
            for k in ALLOWED_RUN_OPTIONS if k in kwargs
        }
        compiled.run_options.update(run_options)

        # set backend options of the program
        backend_options = {
            k: kwargs[k]
            for k in kwargs if k not in ALLOWED_RUN_OPTIONS
        }
        compiled.backend_options.update(backend_options)

        # validate gate parameters
        if device is not None and device.gate_parameters:
            bb_device = bb.loads(device.layout)
            bb_compiled = sf.io.to_blackbird(compiled)

            try:
                user_parameters = match_template(bb_device, bb_compiled)
            except bb.utils.TemplateError as e:
                raise CircuitError(
                    "Program cannot be used with the compiler '{}' "
                    "due to incompatible topology.".format(
                        compiler.short_name)) from e

            device.validate_parameters(**user_parameters)

        return compiled
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)
Exemple #5
0
    def compile(self, backend='fock', **kwargs):
        """Compile the program for the given backend.

        The compilation step validates the program, making sure all the Operations
        used are accepted by the target backend.
        Additionally it may decompose certain gates into sequences of simpler gates.

        The compiled program shares its RegRefs with the original, which makes it easier
        to access the measurement results, but also necessitates the locking of both the
        compiled program and the original to make sure the RegRef state remains consistent.

        Args:
            backend (str): target backend

        Keyword Args:
            optimize (bool): If True, try to optimize the program by merging and canceling gates.
                The default is False.
            warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
                connected. The default is True.

        Returns:
            Program: compiled program
        """
        if backend in specs.backend_specs:
            db = specs.backend_specs[backend]()
        else:
            raise ValueError(
                "Could not find backend {} in Strawberry Fields database".
                format(backend))

        if db.modes is not None:
            # subsystems may be created and destroyed, this is total number that has ever existed
            if len(self.reg_refs) > db.modes:
                raise CircuitError(
                    "This program requires {} modes, but the {} backend "
                    "only supports a {}-mode program".format(
                        len(self.reg_refs), backend, db.modes))

        def compile_sequence(seq):
            """Compiles the given Command sequence."""
            compiled = []
            for cmd in seq:
                op_name = cmd.op.__class__.__name__

                if op_name in db.decompositions:
                    # backend requests an op decomposition

                    # TODO: allow the user to selectively turn off decomposition
                    # by passing the kwarg `decomp=False` to more
                    # operations (currently only ops.Gaussian allows this).
                    #
                    # For example, the 'gaussian' backend supports setting the state
                    # via passing directly the (mu, cov) OR by first having the
                    # frontend decompose into other primitive gates.
                    # That is, ops.Gaussian is both a primitive _and_ a decomposition
                    # for the 'gaussian' backend, and it's behaviour can be chosen
                    # by the user.
                    if (op_name in db.primitives) and hasattr(
                            cmd.op, 'decomp'):
                        # op is a backend primitive, AND backend also
                        # supports decomposition of this primitive.
                        if not cmd.op.decomp:
                            # However, user has requested to bypass decomposition
                            compiled.append(cmd)
                            continue

                    try:
                        kwargs = db.decompositions[op_name]
                        temp = cmd.op.decompose(cmd.reg, **kwargs)
                        # now compile the decomposition
                        temp = compile_sequence(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 db.primitives:
                    # backend can handle the op natively
                    compiled.append(cmd)

                else:
                    raise CircuitError(
                        'The operation {} cannot be used with the {} backend.'.
                        format(cmd.op.__class__.__name__, backend))

            return compiled

        seq = compile_sequence(self.circuit)

        if kwargs.get('warn_connected', True):
            DAG = pu.list_to_DAG(seq)
            temp = nx.algorithms.components.number_weakly_connected_components(
                DAG)
            if temp > 1:
                warnings.warn(
                    'The circuit consists of {} disconnected components.'.
                    format(temp))

        # does the device have its own compilation method?
        if db.compile is not None:
            seq = db.compile(seq)

        self.lock()
        compiled = copy.copy(self)  # shares RegRefs with the source
        compiled.backend = backend
        compiled.circuit = seq

        # link to the original source Program
        if self.source is None:
            compiled.source = self
        else:
            compiled.source = self.source

        if kwargs.get('optimize', False):
            compiled.optimize()

        return compiled
    def compile(self, target, **kwargs):
        """Compile the program targeting the given circuit template.

        Validates the program against the given target, making sure all the Operations
        used are accepted by the target template.
        Additionally, depending on the target, the compilation may modify the quantum circuit
        into an equivalent circuit, e.g., by decomposing certain gates into sequences
        of simpler gates, or optimizing the gate ordering using commutation rules.

        The returned compiled Program shares its :class:`RegRefs <RegRef>` with the original,
        which makes it easier to access the measurement results, but also necessitates the
        :meth:`locking <lock>` of both the compiled program and the original to make sure the
        RegRef state remains consistent.

        Args:
            target (str, ~strawberryfields.circuitspecs.CircuitSpecs): short name of the target circuit specification, or the specification object itself

        Keyword Args:
            optimize (bool): If True, try to optimize the program by merging and canceling gates.
                The default is False.
            warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
                connected. The default is True.

        Returns:
            Program: compiled program
        """
        if isinstance(target, specs.CircuitSpecs):
            db = target
            target = db.short_name
        elif target in specs.circuit_db:
            db = specs.circuit_db[target]()
        else:
            raise ValueError(
                "Could not find target '{}' in the Strawberry Fields circuit database."
                .format(target))

        if db.modes is not None:
            # subsystems may be created and destroyed, this is total number that has ever existed
            modes_total = len(self.reg_refs)
            if modes_total > db.modes:
                raise CircuitError(
                    "This program requires {} modes, but the target '{}' "
                    "only supports a {}-mode program".format(
                        modes_total, target, db.modes))

        seq = db.decompose(self.circuit)

        if kwargs.get('warn_connected', True):
            DAG = pu.list_to_DAG(seq)
            temp = nx.algorithms.components.number_weakly_connected_components(
                DAG)
            if temp > 1:
                warnings.warn(
                    'The circuit consists of {} disconnected components.'.
                    format(temp))

        # run optimizations
        if kwargs.get('optimize', False):
            seq = pu.optimize_circuit(seq)

        # does the circuit spec  have its own compilation method?
        if db.compile is not None:
            seq = db.compile(seq, self.register)

        # create the compiled Program
        compiled = self._linked_copy()
        compiled.circuit = seq
        compiled.target = target

        # get run options of compiled program
        # for the moment, shots is the only supported run option.
        if "shots" in kwargs:
            compiled.run_options["shots"] = kwargs["shots"]

        return compiled
Exemple #7
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
    def merge_a_gaussian_op(self, registers):
        """
        Main function to merge a gaussian operation with its gaussian neighbours.
        If merge is achieved, the method updates self.curr_seq and returns ``True``
        else (merge cannot be achieved), the method returns ``False``.

        Program Flow:
            - For each operation (op) check and obtain Gaussian operations that can be merged
              (get_valid_gaussian_merge_ops).
            - If the operation has successor gaussian operations that can be merged,
              then merge them using gaussian_unitary.py.
            - Determine displacement gates, from gaussian unitary merge, and map them to the qumodes acted upon
              (add_displacement_gates).
            - Attach predecessor operations of the main operation (op) to new Gaussian transform operations.
            - Attach successor non Gaussian operations of op to a displacement gate, if present,
              or a gaussian transform operation from the merged operations (add_non_gaussian_successor_gates).
            - Attach all non-merged predecessor and successor of the merged operations to the new gaussian
              transform and displacement gates (add_gaussian_pre_and_succ_gates).
            - Remove nodes of operations that were merged in and convert DAG to sequence.

        """
        self.DAG = pu.list_to_DAG(self.curr_seq)

        for op in list(self.DAG.nodes):
            successors = list(self.DAG.successors(op))
            predecessors = list(self.DAG.predecessors(op))
            # If operation is a Gaussian operation
            if get_op_name(op) in self.gaussian_ops:
                merged_gaussian_ops = self.get_valid_gaussian_merge_ops(op)

                # If there are successor operations that are Gaussian and can be merged
                if merged_gaussian_ops:
                    self.new_DAG = self.DAG.copy()
                    # Fix order of operations
                    unified_operations = self.organize_merge_ops(
                        [op] + merged_gaussian_ops)
                    gaussian_transform = GaussianUnitary().compile(
                        unified_operations, registers)
                    self.new_DAG.add_node(gaussian_transform[0])

                    # Logic to add displacement gates. Returns a dictionary,
                    # where the value is a displacement gate added and its key is the qumode its operating upon.
                    displacement_mapping = self.add_displacement_gates(
                        gaussian_transform)

                    # If there are predecessors: Attach predecessor edges to new gaussian transform
                    if predecessors:
                        self.new_DAG.add_edges_from([(pre,
                                                      gaussian_transform[0])
                                                     for pre in predecessors])

                    # Add edges to all successor operations not merged
                    self.add_non_gaussian_successor_gates(
                        gaussian_transform, successors, displacement_mapping)

                    # Add edges for all successor/predecessor operations of the merged operations
                    self.add_gaussian_pre_and_succ_gates(
                        gaussian_transform, merged_gaussian_ops,
                        displacement_mapping)

                    self.new_DAG.remove_nodes_from([op] + merged_gaussian_ops)

                    self.curr_seq = pu.DAG_to_list(self.new_DAG)
                    return True
        return False
Exemple #9
0
    def compile(self, target, **kwargs):
        """Compile the program targeting the given circuit template.

        Validates the program against the given target, making sure all the Operations
        used are accepted by the target template.
        Additionally, depending on the target, the compilation may modify the quantum circuit
        into an equivalent circuit, e.g., by decomposing certain gates into sequences
        of simpler gates, or optimizing the gate ordering using commutation rules.

        The returned compiled Program shares its :class:`RegRefs <RegRef>` with the original,
        which makes it easier to access the measurement results, but also necessitates the
        :meth:`locking <lock>` of both the compiled program and the original to make sure the
        RegRef state remains consistent.

        Args:
            target (str, DeviceSpecs): short name of the target circuit template, or the template itself

        Keyword Args:
            optimize (bool): If True, try to optimize the program by merging and canceling gates.
                The default is False.
            warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
                connected. The default is True.

        Returns:
            Program: compiled program
        """
        if isinstance(target, specs.DeviceSpecs):
            db = target
            target = db.short_name
        elif target in specs.backend_specs:
            db = specs.backend_specs[target]()
        else:
            raise ValueError(
                "Could not find target '{}' in Strawberry Fields template database"
                .format(target))

        if db.modes is not None:
            # subsystems may be created and destroyed, this is total number that has ever existed
            modes_total = len(self.reg_refs)
            if modes_total > db.modes:
                raise CircuitError(
                    "This program requires {} modes, but the target '{}' "
                    "only supports a {}-mode program".format(
                        modes_total, target, db.modes))

        def compile_sequence(seq):
            """Compiles the given Command sequence."""
            compiled = []
            for cmd in seq:
                op_name = cmd.op.__class__.__name__
                if op_name in db.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 db.primitives:
                            compiled.append(cmd)
                            continue
                        else:
                            raise CircuitError(
                                "The operation {} is not a primitive for the target '{}'"
                                .format(cmd.op.__class__.__name__, target))
                    try:
                        kwargs = db.decompositions[op_name]
                        temp = cmd.op.decompose(cmd.reg, **kwargs)
                        # now compile the decomposition
                        temp = compile_sequence(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 db.primitives:
                    # target can handle the op natively
                    compiled.append(cmd)

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

            return compiled

        seq = compile_sequence(self.circuit)

        if kwargs.get('warn_connected', True):
            DAG = pu.list_to_DAG(seq)
            temp = nx.algorithms.components.number_weakly_connected_components(
                DAG)
            if temp > 1:
                warnings.warn(
                    'The circuit consists of {} disconnected components.'.
                    format(temp))

        # run optimizations
        if kwargs.get('optimize', False):
            seq = pu.optimize_circuit(seq)

        # does the device have its own compilation method?
        if db.compile is not None:
            seq = db.compile(seq)

        # create the compiled Program
        compiled = self._linked_copy()
        compiled.circuit = seq
        compiled.target = target
        return compiled
Exemple #10
0
    def compile(self, target, **kwargs):
        """Compile the program targeting the given circuit specification.

        Validates the program against the given target, making sure all the
        :doc:`/introduction/ops` used are accepted by the target specification.

        Additionally, depending on the target, the compilation may modify the quantum circuit
        into an equivalent circuit, e.g., by decomposing certain gates into sequences
        of simpler gates, or optimizing the gate ordering using commutation rules.

        **Example:**

        The ``gbs`` compile target will
        compile a circuit consisting of Gaussian operations and Fock measurements
        into canonical Gaussian boson sampling form.

        >>> prog2 = prog.compile('gbs')

        Args:
            target (str, ~strawberryfields.circuitspecs.CircuitSpecs): short name of the target
                circuit specification, or the specification object itself

        Keyword Args:
            optimize (bool): If True, try to optimize the program by merging and canceling gates.
                The default is False.
            warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
                connected. The default is True.

        Returns:
            Program: compiled program
        """
        if isinstance(target, specs.CircuitSpecs):
            db = target
            target = db.short_name
        elif target in specs.circuit_db:
            db = specs.circuit_db[target]()
        else:
            raise ValueError(
                "Could not find target '{}' in the Strawberry Fields circuit database."
                .format(target))

        if db.modes is not None:
            # subsystems may be created and destroyed, this is total number that has ever existed
            modes_total = len(self.reg_refs)
            if modes_total > db.modes:
                raise CircuitError(
                    "This program requires {} modes, but the target '{}' "
                    "only supports a {}-mode program".format(
                        modes_total, target, db.modes))

        seq = db.decompose(self.circuit)

        if kwargs.get("warn_connected", True):
            DAG = pu.list_to_DAG(seq)
            temp = nx.algorithms.components.number_weakly_connected_components(
                DAG)
            if temp > 1:
                warnings.warn(
                    "The circuit consists of {} disconnected components.".
                    format(temp))

        # run optimizations
        if kwargs.get("optimize", False):
            seq = pu.optimize_circuit(seq)

        # does the circuit spec  have its own compilation method?
        if db.compile is not None:
            seq = db.compile(seq, self.register)

        # create the compiled Program
        compiled = self._linked_copy()
        compiled.circuit = seq
        compiled._target = db.short_name

        # get run options of compiled program
        # for the moment, shots is the only supported run option.
        if "shots" in kwargs:
            compiled.run_options["shots"] = kwargs["shots"]

        compiled.backend_options = {}
        if "cutoff_dim" in kwargs:
            compiled.backend_options["cutoff_dim"] = kwargs["cutoff_dim"]

        return compiled