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 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
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