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