diff --git a/docs/projectq.cengines.rst b/docs/projectq.cengines.rst index 5a3c963a6..32e6ea763 100755 --- a/docs/projectq.cengines.rst +++ b/docs/projectq.cengines.rst @@ -6,7 +6,7 @@ The ProjectQ compiler engines package. .. autosummary:: projectq.cengines.AutoReplacer projectq.cengines.BasicEngine - projectq.cengines.BasicMapper + projectq.cengines.BasicMapperEngine projectq.cengines.CommandModifier projectq.cengines.CompareEngine projectq.cengines.DecompositionRule @@ -14,13 +14,14 @@ The ProjectQ compiler engines package. projectq.cengines.DummyEngine projectq.cengines.ForwarderEngine projectq.cengines.GridMapper + projectq.cengines.GraphMapper projectq.cengines.InstructionFilter projectq.cengines.IBM5QubitMapper projectq.cengines.LinearMapper projectq.cengines.LocalOptimizer projectq.cengines.ManualMapper projectq.cengines.MainEngine - projectq.cengines.SwapAndCNOTFlipper + projectq.cengines.SwapAndCNOTFlipper projectq.cengines.TagRemover @@ -31,3 +32,14 @@ Module contents :members: :special-members: __init__ :imported-members: + + +Helper sub-modules +------------------ + +Gate manager sub-module +^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: projectq.cengines._gate_manager + :members: + :imported-members: diff --git a/projectq/cengines/__init__.py b/projectq/cengines/__init__.py index 966159e78..703e2955d 100755 --- a/projectq/cengines/__init__.py +++ b/projectq/cengines/__init__.py @@ -32,3 +32,6 @@ from ._tagremover import TagRemover from ._testengine import CompareEngine, DummyEngine from ._twodmapper import GridMapper +from ._graphmapper import GraphMapper +from ._gate_manager import (nearest_neighbours_cost_fun, + look_ahead_parallelism_cost_fun) diff --git a/projectq/cengines/_gate_manager.py b/projectq/cengines/_gate_manager.py new file mode 100644 index 000000000..d24526df7 --- /dev/null +++ b/projectq/cengines/_gate_manager.py @@ -0,0 +1,988 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This is a helper module for the :py:class:`projectq.cengines.GraphMapper` +class. + +Implements a SABRE-like algorithm [1] to generate a list of SWAP operations to +route qubit through an arbitrary graph. + +[1] https://arxiv.org/abs/1809.02573v2 +""" + +import networkx as nx +import math +from projectq.ops import (AllocateQubitGate, DeallocateQubitGate) + +# ============================================================================== + + +class defaults(object): + """ + Class containing default values for some options + """ + + delta = 0.001 + max_lifetime = 5 + near_term_layer_depth = 1 + W = 0.5 + + +# ============================================================================== + + +def _topological_sort(dag): + """ + Returns a generator of nodes in topologically sorted order. + + A topological sort is a nonunique permutation of the nodes such that an + edge from u to v implies that u appears before v in the topological sort + order. + + Args: + dag (networkx.DiGraph): A Directed Acyclic Graph (DAG) + + Returns: + An iterable of node names in topological sorted order. + + Note: + This implementation is based on + :py:func:`networkx.algorithms.dag.topological_sort` + """ + indegree_map = {} + zero_indegree = [] + for node, degree in dag.in_degree(): + if degree > 0: + indegree_map[node] = degree + else: + zero_indegree.append(node) + + while zero_indegree: + node = zero_indegree.pop() + for child in dag[node]: + indegree_map[child] -= 1 + if indegree_map[child] == 0: + zero_indegree.append(child) + del indegree_map[child] + yield node + + +# Coffman-Graham algorithm with infinite width +def _coffman_graham_ranking(dag): + """ + Apply the Coffman-Grapham layering algorithm to a DAG (with infinite width) + + Args: + dag (networkx.DiGraph): A Directed Acyclic Graph (DAG) + + Returns: + A list of layers (Python list of lists). + + Note: + This function does not limit the width of any layers. + """ + layers = [[]] + levels = {} + + for node in _topological_sort(dag): + dependant_level = -1 + for dependant in dag.pred[node]: + level = levels[dependant] + if level > dependant_level: + dependant_level = level + + level = -1 + if dependant_level < len(layers) - 1: + level = dependant_level + 1 + if level < 0: + layers.append([]) + level = len(layers) - 1 + + layers[level].append(node) + levels[node] = level + + for layer in layers: + layer.sort(key=lambda node: node.node_id) + return layers + + +# ============================================================================== + + +def _sum_distance_over_gates(node_list, mapping, distance_matrix): + """ + Calculate the sum of distances between pairs of qubits + + Args: + gate_list (list): List of 2-qubit gates + mapping (dict): Current mapping + distance_matrix (dict): Distance matrix within the hardware coupling + graph + + Returns: + Sum of all pair-wise distances between qubits + """ + return sum([ + distance_matrix[mapping[node.logical_id0]][mapping[node.logical_id1]] + for node in node_list + ]) + + +def nearest_neighbours_cost_fun(gates_dag, mapping, distance_matrix, swap, + opts): + r""" + Nearest neighbours cost function + + .. math:: + + H = \sum_{\mathrm{gate}\ \in\ F} + D(\mathrm{gate}.q_1, \mathrm{gate}.q_2) + + where: + + - :math:`F` is the ensemble of gates in the front layer + - :math:`D` is the distance matrix + - :math:`\mathrm{gate}.q_{1, 2}` are the backend qubit IDs for each gate + + + Args: + gates_dag (CommandDAG): Direct acyclic graph of future quantum gates + mapping (dict): Current mapping + distance_matrix (dict): Distance matrix within the hardware coupling + graph + swap (tuple): Candidate swap (not used by this function) + opts (dict): Miscellaneous parameters for cost function (not used by + this function) + + Returns: + Score of current swap operations + """ + # pylint: disable=unused-argument + return _sum_distance_over_gates(gates_dag.front_layer_2qubit, mapping, + distance_matrix) + + +def look_ahead_parallelism_cost_fun(gates_dag, mapping, distance_matrix, swap, + opts): + r""" + Cost function using nearest-neighbour interactions as well as considering + gates from the near-term layer (provided it has been calculated) in order + to favour swap operations that can be performed in parallel. + + .. math:: + + H = M \left[\frac{1}{|F|}\sum_{\mathrm{gate}\ \in\ F} + D(\mathrm{gate}.q_1, \mathrm{gate}.q_2) + + \frac{W}{|E|}\sum_{\mathrm{gate}\ \in\ E} + D(\mathrm{gate}.q_1, \mathrm{gate}.q_2) \right] + + where: + + - :math:`M` is defined as :math:`\max(\mathrm{decay}(SWAP.q_1), + \mathrm{decay}(SWAP.q_2))` + - :math:`F` is the ensemble of gates in front layer + - :math:`E` is the ensemble of gates in near-term layer + - :math:`D` is the distance matrix + - :math:`\mathrm{gate}.q_{1, 2}` are the backend qubit IDs for each gate + + + Args: + gates_dag (CommandDAG): Direct acyclic graph of future quantum gates + mapping (dict): Current mapping + distance_matrix (dict): Distance matrix within the hardware coupling + graph + swap (tuple): Candidate swap operation + opts (dict): Miscellaneous parameters for cost function + + Returns: + Score of current swap operations + + Note: + ``opts`` must contain the following key-values + + .. list-table:: + :header-rows: 1 + + * - Key + - Type + - Description + * - decay + - :py:class:`.DecayManager` + - | Instance containing current decay information for each + | backend qubit + * - W + - ``float`` + - Weighting factor (see cost function formula) + """ + decay = opts['decay'] + near_term_weight = opts.get('W', defaults.W) + + n_front = len(gates_dag.front_layer_2qubit) + n_near = len(gates_dag.near_term_layer) + + decay_factor = max(decay.get_decay_value(swap[0]), + decay.get_decay_value(swap[1])) + front_layer_term = (1. / n_front * _sum_distance_over_gates( + gates_dag.front_layer_2qubit, mapping, distance_matrix)) + + if n_near == 0: + return decay_factor * front_layer_term + return (decay_factor * + (front_layer_term + + (near_term_weight / n_near * _sum_distance_over_gates( + gates_dag.near_term_layer, mapping, distance_matrix)))) + + +# ============================================================================== + + +def _apply_swap_to_mapping(mapping, logical_id0, logical_id1, backend_id1): + """ + Modify a mapping by applying a SWAP operation + + Args: + mapping (dict): mapping to update + logical_id0 (int): A logical qubit ID + logical_id1 (int): A logical qubit ID + backend_id1 (int): Backend ID corresponding to ``logical_id1`` + + .. note:: + + ``logical_id1`` can be set to -1 to indicate a non-allocated backend qubit + """ + # If the qubit is present in the mapping (ie. second qubit is already + # allocated), update the mapping, otherwise simply assign the new backend + # ID to the qubit being swapped. + + if logical_id1 != -1: + mapping[logical_id0], mapping[logical_id1] \ + = mapping[logical_id1], mapping[logical_id0] + else: + mapping[logical_id0] = backend_id1 + + +# ============================================================================== + + +class DecayManager(object): + """ + Class managing the decay information about a list of backend qubit IDs + + User should call the :py:meth:`step` method each time a swap gate is added + and :py:meth:`remove_decay` once a 2-qubit gate is executed. + """ + def __init__(self, delta, max_lifetime): + """ + Constructor + + Args: + delta (float): Decay parameter + max_lifetime (int): Maximum lifetime of decay information for a + particular qubit + """ + self._delta = delta + self._cutoff = max_lifetime + self._backend_ids = {} + + def clear(self): + """ + Clear the state of a DecayManager + """ + self._backend_ids = {} + + def add_to_decay(self, backend_id): + """ + Add to the decay to a particular backend qubit ID + + Args: + backend_id (int) : Backend qubit ID + """ + # Ignore invalid (ie. non-allocated) backend IDs + if backend_id < 0: + return + + if backend_id in self._backend_ids: + self._backend_ids[backend_id]['lifetime'] = self._cutoff + self._backend_ids[backend_id]['decay'] += self._delta + else: + self._backend_ids[backend_id] = { + 'decay': 1 + self._delta, + 'lifetime': self._cutoff + } + + def remove_decay(self, backend_id): + """ + Remove the decay of a particular backend qubit ID + + Args: + backend_id (int) : Backend qubit ID + """ + if backend_id in self._backend_ids: + del self._backend_ids[backend_id] + + def get_decay_value(self, backend_id): + """ + Retrieve the decay value of a particular backend qubit ID + + Args: + backend_id (int) : Backend qubit ID + """ + if backend_id in self._backend_ids: + return self._backend_ids[backend_id]['decay'] + return 1 + + def step(self): + """ + Step all decay values in time + + Use this method to indicate a SWAP search step has been performed. + """ + backend_ids = list(self._backend_ids) + for backend_id in backend_ids: + self._backend_ids[backend_id]['lifetime'] -= 1 + if self._backend_ids[backend_id]['lifetime'] == 0: + del self._backend_ids[backend_id] + + +# ============================================================================== + + +class _DAGNodeBase(object): + # pylint: disable=too-few-public-methods + def __init__(self, node_id, cmd, *args): + self.node_id = node_id + self.logical_ids = frozenset(args) + self.cmd = cmd + self.compatible_successor_cmds = [] + + def append_compatible_cmd(self, cmd): + """ + Append a compatible commands to this DAG node + + Args: + cmd (Command): A ProjectQ command + """ + self.compatible_successor_cmds.append(cmd) + + +class _DAGNodeSingle(_DAGNodeBase): + """ + Node representing a single qubit gate as part of a Direct Acyclic Graph + (DAG) of quantum gates + """ + + # pylint: disable=too-few-public-methods + def __init__(self, node_id, cmd, logical_id): + super(_DAGNodeSingle, self).__init__(node_id, cmd, logical_id) + self.logical_id = logical_id + + +class _DAGNodeDouble(_DAGNodeBase): + """ + Node representing a 2-qubit gate as part of a Direct Acyclic Graph (DAG) + of quantum gates + """ + + # pylint: disable=too-few-public-methods + def __init__(self, node_id, cmd, logical_id0, logical_id1): + super(_DAGNodeDouble, self).__init__(node_id, cmd, logical_id0, + logical_id1) + self.logical_id0 = logical_id0 + self.logical_id1 = logical_id1 + + +class CommandDAG(object): + """ + Class managing a list of multi-qubit gates and storing them into a Direct + Acyclic Graph (DAG) in order of precedence. + """ + def __init__(self): + self._dag = nx.DiGraph() + self._node_id = 0 + self._logical_ids_in_diag = set() + self.near_term_layer = [] + + self._layers_up_to_date = True + self._front_layer = [] + self._front_layer_2qubit = [] + self._layers = [[]] + self._back_layer = {} + + @property + def front_layer(self): + self.calculate_command_hierarchy() + return self._layers[0] + + @property + def front_layer_2qubit(self): + self.calculate_command_hierarchy() + return self._front_layer_2qubit + + def size(self): + """ + Return the size of the DAG (ie. number of nodes) + + Note: + This may not be equal to the number of commands stored within the + DAG as some nodes might store more than one gate if they are + compatible. + """ + return self._dag.number_of_nodes() + + def clear(self): + """ + Clear the state of a DAG + + Remove all nodes from the DAG and all layers. + """ + self._dag.clear() + self._node_id = 0 + self._logical_ids_in_diag = set() + self.near_term_layer = [] + + self._layers_up_to_date = True + self._front_layer = [] + self._front_layer_2qubit = [] + self._layers = [[]] + self._back_layer = {} + + def calculate_command_hierarchy(self): + if not self._layers_up_to_date: + self._layers = _coffman_graham_ranking(self._dag) + self._front_layer_2qubit = [ + node for node in self._layers[0] + if isinstance(node, _DAGNodeDouble) + ] + self._layers_up_to_date = True + + def add_command(self, cmd): + """ + Add a command to the DAG + + Args: + cmd (Command): A ProjectQ command + """ + logical_ids = [qubit.id for qureg in cmd.all_qubits for qubit in qureg] + + if len(logical_ids) == 2: + logical_id0_in_dag = logical_ids[0] in self._logical_ids_in_diag + logical_id1_in_dag = logical_ids[1] in self._logical_ids_in_diag + + if (logical_id0_in_dag and logical_id1_in_dag and self._back_layer[ + logical_ids[0]] == self._back_layer[logical_ids[1]]): + self._back_layer[logical_ids[1]].append_compatible_cmd(cmd) + return + + new_node = _DAGNodeDouble(self._node_id, cmd, *logical_ids) + self._node_id += 1 + self._dag.add_node(new_node) + + if logical_id0_in_dag: + self._dag.add_edge(self._back_layer[logical_ids[0]], new_node) + self._logical_ids_in_diag.add(logical_ids[1]) + else: + self._logical_ids_in_diag.add(logical_ids[0]) + + if logical_id1_in_dag: + self._dag.add_edge(self._back_layer[logical_ids[1]], new_node) + self._logical_ids_in_diag.add(logical_ids[0]) + else: + self._logical_ids_in_diag.add(logical_ids[1]) + + self._back_layer[logical_ids[0]] = new_node + self._back_layer[logical_ids[1]] = new_node + + self._layers_up_to_date = False + else: + logical_id = logical_ids[0] + logical_id_in_dag = logical_id in self._logical_ids_in_diag + + if isinstance(cmd.gate, (AllocateQubitGate, DeallocateQubitGate)): + new_node = _DAGNodeSingle(self._node_id, cmd, logical_id) + self._node_id += 1 + self._dag.add_node(new_node) + + if logical_id_in_dag: + self._dag.add_edge(self._back_layer[logical_id], new_node) + else: + self._logical_ids_in_diag.add(logical_id) + + self._back_layer[logical_id] = new_node + self._layers_up_to_date = False + else: + if not logical_id_in_dag: + new_node = _DAGNodeSingle(self._node_id, cmd, logical_id) + self._node_id += 1 + self._dag.add_node(new_node) + self._logical_ids_in_diag.add(logical_id) + + self._back_layer[logical_id] = new_node + self._layers_up_to_date = False + else: + self._back_layer[logical_id].append_compatible_cmd(cmd) + + def calculate_near_term_layer(self, mapping, depth=1): + """ + Calculate the first order near term layer. + + This is the set of gates that will become the front layer once these + get executed. + + Args: + mapping (dict): current mapping + """ + self.calculate_command_hierarchy() + self.near_term_layer = [] + if len(self._layers) > 1: + for layer in self._layers[1:depth + 1]: + self.near_term_layer.extend([ + node for node in layer + if (isinstance(node, _DAGNodeDouble) and node.logical_id0 + in mapping and node.logical_id1 in mapping) + ]) + + def calculate_interaction_list(self): + """ + List all known interactions between multiple qubits + + Returns: + List of tuples of logical qubit IDs for each 2-qubit gate present + in the DAG. + """ + return [(node.logical_id0, node.logical_id1) for node in self._dag + if isinstance(node, _DAGNodeDouble)] + + def calculate_qubit_interaction_subgraphs(self, max_order=2): + """ + Calculate qubits interaction graph based on all commands stored. + + The interaction graph has logical qubit IDs as nodes and edges + represent a 2-qubit gate between qubits. + + Args: + max_order (int): Maximum degree of the nodes in the resulting + graph + + Returns: + A list of list of graph nodes corresponding to all the connected + components of the qubit interaction graph. Within each components, + nodes are sorted in decreasing order of their degree. + """ + self.calculate_command_hierarchy() + + graph = nx.Graph() + for layer in self._layers: + for node in layer: + if isinstance(node, _DAGNodeDouble): + node0_in_graph = node.logical_id0 in graph + node1_in_graph = node.logical_id1 in graph + + add_edge = True + if (node0_in_graph + and len(graph[node.logical_id0]) >= max_order): + add_edge = False + if (node1_in_graph + and len(graph[node.logical_id1]) >= max_order): + add_edge = False + + if add_edge or graph.has_edge(node.logical_id0, + node.logical_id1): + graph.add_edge(node.logical_id0, node.logical_id1) + else: + break + else: + continue # only executed if the inner loop did NOT break + break # only executed if the inner loop DID break + + return [ + sorted(graph.subgraph(g), + key=lambda n: len(graph[n]), + reverse=True) for g in sorted( + nx.connected_components(graph), + key=lambda c: (max(len(graph[n]) for n in c), len(c)), + reverse=True) + ] + + def remove_command(self, cmd): + """ + Remove a command from the DAG + + Note: + Only commands present in the front layer of the DAG can be + removed. + + Args: + cmd (Command): A ProjectQ command + + Raises: + RuntimeError if the gate does not exist in the front layer + """ + # First find the gate inside the front layer list + node = next((node for node in self.front_layer if node.cmd is cmd), + None) + if node is None: + raise RuntimeError( + '({}) not found in front layer of DAG'.format(cmd)) + + logical_ids = {qubit.id for qureg in cmd.all_qubits for qubit in qureg} + + descendants = list(self._dag[node]) + + if not descendants: + self._logical_ids_in_diag -= logical_ids + for logical_id in logical_ids: + del self._back_layer[logical_id] + elif len(descendants) == 1 and isinstance(node, _DAGNodeDouble): + logical_id, = logical_ids.difference(descendants[0].logical_ids) + + self._logical_ids_in_diag.remove(logical_id) + del self._back_layer[logical_id] + + # Remove gate from DAG + self._dag.remove_node(node) + + self._layers_up_to_date = False + + +# ============================================================================== + + +class GateManager(object): + """ + Class managing qubit interactions + """ + def __init__(self, graph, decay_opts=None): + """ + Args: + graph (networkx.Graph): an arbitrary connected graph + """ + # Make sure that we start with a valid graph + if not nx.is_connected(graph): + raise RuntimeError("Input graph must be a connected graph") + + if not all([isinstance(n, int) for n in graph]): + raise RuntimeError( + "All nodes inside the graph needs to be integers") + + self.graph = graph + self.distance_matrix = dict( + nx.all_pairs_shortest_path_length(self.graph)) + + if decay_opts is None: + decay_opts = {} + self.dag = CommandDAG() + self._decay = DecayManager( + decay_opts.get('delta', defaults.delta), + decay_opts.get('max_lifetime', defaults.max_lifetime)) + self._stats = { + 'simul_exec': [], + '2qubit_gates_loc': {}, + } + + def __str__(self): + """ + Return the string representation of this MultiQubitGateManager. + + Returns: + A summary (string) about the commands executed. + """ + + max_width = int( + math.ceil(math.log10(max( + self._stats['2qubit_gates_loc'].values()))) + 1) + interactions_str = "" + for (backend_id0, backend_id1), number \ + in sorted(self._stats['2qubit_gates_loc'].items(), + key=lambda x: x[1], reverse=True): + interactions_str += "\n {0}: {1:{2}}".format( + sorted([backend_id0, backend_id1]), number, max_width) + + return ('2-qubit gates locations:{}').format(interactions_str) + + def size(self): + """ + Return the size of the underlying DAG + + .. seealso:: + :py:meth:`.CommandDAG.size` + """ + return self.dag.size() + + def clear(self): + """ + Return the size of the underlying DAG + + .. seealso:: + :py:meth:`.CommandDAG.clear` + :py:meth:`.DecayManager.clear` + """ + self.dag.clear() + self._decay.clear() + + def generate_swaps(self, + current_mapping, + cost_fun, + opts=None, + max_steps=100): + """ + Generate a list of swaps to execute some quantum gates + + Args: + mapping (dict): Current mapping + cost_fun (function): Cost function to rank swap candidates + Must accept the following parameters: + - dag (_GatesDAG) + - new_mapping (dict) + - distance_matrix (dict) + - swap_candidate (tuple) + max_steps (int): (optional) Maximum number of swap steps to + attempt before giving up + opts (dict): (optional) Extra parameters for cost function call + + .. seealso:: + :py:meth:`nearest_neighbours_cost_fun` + :py:meth:`look_ahead_parallelism_cost_fun` + + Returns: + A tuple (list, set) of swap operations (tuples of backend IDs) and + a set of all the backend IDs that are traversed by the SWAP + operations. + """ + + if not self.dag.front_layer_2qubit: + return ([], set()) + + if opts is None: + opts = {} + + self._decay.clear() + opts['decay'] = self._decay + + self.dag.calculate_near_term_layer( + current_mapping, + opts.get('near_term_layer_depth', defaults.near_term_layer_depth)) + + mapping = current_mapping.copy() + swaps = [] + all_swapped_qubits = set() + while not self._can_execute_some_gate(mapping): + (logical_id0, backend_id0, logical_id1, + backend_id1) = self._generate_one_swap_step( + mapping, cost_fun, opts) + swaps.append((mapping[logical_id0], backend_id1)) + all_swapped_qubits.add(backend_id0) + all_swapped_qubits.add(backend_id1) + + self._decay.add_to_decay(backend_id0) + self._decay.add_to_decay(backend_id1) + self._decay.step() + + _apply_swap_to_mapping(mapping, logical_id0, logical_id1, + backend_id1) + + if len(swaps) > max_steps: + raise RuntimeError( + 'Maximum number of steps ({}) to find a list of'.format( + max_steps) + + ' SWAP operations reached without convergence') + + return swaps, all_swapped_qubits + + def add_command(self, cmd): + """ + Add a command to the underlying DAG + + Args: + cmd (Command): A ProjectQ command + + .. seealso:: + :py:meth:`.GatesDAG.add_command` + """ + + self.dag.add_command(cmd) + + def get_executable_commands(self, mapping): + """ + Find as many executable commands as possible given a mapping + + Args: + mapping (dict): Current mapping + + Returns: + A tuple (cmds_to_execute, allocate_cmds) where the first one is a + list of ProjectQ commands that can be executed and the second a + list of allocation commands for qubits not in the current mapping + """ + cmds_to_execute = [] + allocate_cmds = [] + has_command_to_execute = True + self._stats['simul_exec'].append(0) + + def _add_to_execute_list(node): + cmds_to_execute.append(node.cmd) + cmds_to_execute.extend(node.compatible_successor_cmds) + self.dag.remove_command(node.cmd) + + self.dag.calculate_command_hierarchy() + + while has_command_to_execute: + # Reset after each pass + has_command_to_execute = False + + for node in self.dag.front_layer: + if isinstance(node, _DAGNodeSingle): + if isinstance(node.cmd.gate, AllocateQubitGate): + # Allocating a qubit already in mapping is allowed + if node.logical_id in mapping: + has_command_to_execute = True + _add_to_execute_list(node) + elif node not in allocate_cmds: + allocate_cmds.append(node) + elif node.logical_id in mapping: + has_command_to_execute = True + self._stats['simul_exec'][-1] += 1 + _add_to_execute_list(node) + elif (node.logical_id0 in mapping + and node.logical_id1 in mapping): + if self.graph.has_edge(mapping[node.logical_id0], + mapping[node.logical_id1]): + has_command_to_execute = True + _add_to_execute_list(node) + self._stats['simul_exec'][-1] += 1 + key = frozenset((mapping[node.logical_id0], + mapping[node.logical_id1])) + self._stats['2qubit_gates_loc'][key] \ + = self._stats['2qubit_gates_loc'].get(key, 0) + 1 + for cmd in node.compatible_successor_cmds: + if len([ + qubit.id for qureg in cmd.all_qubits + for qubit in qureg + ]) == 2: + self._stats['2qubit_gates_loc'][key] += 1 + + self.dag.calculate_command_hierarchy() + return cmds_to_execute, allocate_cmds + + def execute_allocate_cmds(self, allocate_cmds, mapping): + """ + Executea list of allocate commands (ie. remove them from the front + layer) + + Args: + allocate_cmds (list): A list of Allocate commands (DAG nodes) + mapping (dict): Current mapping + + Returns: + A list of ProjectQ commands to be executed + """ + cmds_to_execute = [] + for node in allocate_cmds: + assert isinstance(node.cmd.gate, AllocateQubitGate) + if node.logical_id in mapping: + cmds_to_execute.append(node.cmd) + cmds_to_execute.extend(node.compatible_successor_cmds) + self.dag.remove_command(node.cmd) + + self.dag.calculate_command_hierarchy() + return cmds_to_execute + + # ========================================================================== + + def calculate_qubit_interaction_subgraphs(self, max_order=2): + """ + Calculate qubits interaction graph based on all commands stored. + + Args: + max_order (int): Maximum degree of the nodes in the resulting + interaction graph + + Returns: + A list of list of graph nodes corresponding to all the connected + components of the qubit interaction graph. Within each components, + nodes are sorted in decreasing order of their degree. + + .. seealso:: + :py:meth:`CommandDAG.calculate_qubit_interaction_subgraphs` + """ + self.dag.calculate_command_hierarchy() + return self.dag.calculate_qubit_interaction_subgraphs(max_order) + + # ========================================================================== + + def _generate_one_swap_step(self, mapping, cost_fun, opts): + """ + Find the most optimal swap operation to perform next + + Args: + mapping (dict): Current mapping + cost_fun (function): Cost function to rank swap candidates + Must accept the following parameters: + - dag (_GatesDAG) + - new_mapping (dict) + - distance_matrix (dict) + - swap_candidate (tuple) + + Returns: + Tuple with (logical_id0, backend_id0, logical_id1, backend_id1) + where logical_id1 can be -1 if backend_id1 does not currently have + a logical qubit associated to it. + """ + + self.dag.calculate_command_hierarchy() + reverse_mapping = {v: k for k, v in mapping.items()} + + # Only consider gates from the front layer and generate a list of + # potential SWAP operations with all qubits that are neighours of + # those concerned by a gate + + swap_candidates = [] + for node in self.dag.front_layer_2qubit: + for logical_id in node.logical_ids: + for backend_id1 in self.graph[mapping[logical_id]]: + swap_candidates.append( + (logical_id, mapping[logical_id], + reverse_mapping.get(backend_id1, -1), backend_id1)) + + # Rank swap candidates using the provided cost function + scores = [] + for (logical_id0, backend_id0, logical_id1, + backend_id1) in swap_candidates: + new_mapping = mapping.copy() + + _apply_swap_to_mapping(new_mapping, logical_id0, logical_id1, + backend_id1) + + scores.append( + cost_fun(self.dag, new_mapping, self.distance_matrix, + (backend_id0, backend_id1), opts)) + + # Return the swap candidate with the lowest score + return swap_candidates[scores.index(min(scores))] + + def _can_execute_some_gate(self, mapping): + """ + Test whether some gate from the front layer can be executed + + Args: + mapping (dict): Current mapping + """ + self.dag.calculate_command_hierarchy() + for node in self.dag.front_layer: + if isinstance(node, _DAGNodeSingle) and node.logical_id in mapping: + return True + + if (isinstance(node, _DAGNodeDouble) and self.graph.has_edge( + mapping[node.logical_id0], mapping[node.logical_id1])): + return True + return False diff --git a/projectq/cengines/_gate_manager_test.py b/projectq/cengines/_gate_manager_test.py new file mode 100644 index 000000000..5b0481d91 --- /dev/null +++ b/projectq/cengines/_gate_manager_test.py @@ -0,0 +1,1018 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.cengines._graphmapper.py.""" + +from copy import deepcopy + +import pytest +import networkx as nx +import re +from projectq.ops import (Allocate, Command, Deallocate, X, H) +from projectq.types import WeakQubitRef + +from projectq.cengines import _gate_manager as gatemgr + +# ============================================================================== + + +# For debugging purposes +def dagnode_to_string(self): + return '{} {}'.format(self.__class__.__name__, tuple(self.logical_ids)) + + +gatemgr._DAGNodeBase.__str__ = dagnode_to_string +gatemgr._DAGNodeBase.__repr__ = dagnode_to_string + +Command.__repr__ = Command.__str__ + +# ============================================================================== + + +def allocate_all_qubits_cmd(num_qubits): + qb = [] + allocate_cmds = [] + for i in range(num_qubits): + qb.append(WeakQubitRef(engine=None, idx=i)) + allocate_cmds.append( + Command(engine=None, gate=Allocate, qubits=([qb[i]], ))) + return qb, allocate_cmds + + +def generate_grid_graph(nrows, ncols): + graph = nx.Graph() + graph.add_nodes_from(range(nrows * ncols)) + + for row in range(nrows): + for col in range(ncols): + node0 = col + ncols * row + + is_middle = ((0 < row < nrows - 1) and (0 < col < ncols - 1)) + add_horizontal = is_middle or (row in (0, nrows - 1) and + (0 < col < ncols - 1)) + add_vertical = is_middle or (col in (0, ncols - 1) and + (0 < row < nrows - 1)) + if add_horizontal: + graph.add_edge(node0, node0 - 1) + graph.add_edge(node0, node0 + 1) + if add_vertical: + graph.add_edge(node0, node0 - ncols) + graph.add_edge(node0, node0 + ncols) + if nrows == 2: + node0 = col + graph.add_edge(node0, node0 + ncols) + if ncols == 2: + node0 = ncols * row + graph.add_edge(node0, node0 + 1) + + return graph + + +def gen_cmd(*logical_ids, **kwargs): + gate = kwargs.get('gate', X) + if len(logical_ids) == 1: + qb0 = WeakQubitRef(engine=None, idx=logical_ids[0]) + return Command(None, gate, qubits=([qb0], )) + if len(logical_ids) == 2: + qb0 = WeakQubitRef(engine=None, idx=logical_ids[0]) + qb1 = WeakQubitRef(engine=None, idx=logical_ids[1]) + return Command(None, gate, qubits=([qb0], ), controls=[qb1]) + raise RuntimeError('Unsupported') + + +def search_cmd(command_dag, cmd): + for node in command_dag._dag: + if node.cmd is cmd: + return node + raise RuntimeError('Unable to find command in DAG') + + +@pytest.fixture(scope="module") +def simple_graph(): + # 2 4 + # / \ / | + # 0 - 1 3 | + # \ / \ | + # 5 6 + graph = nx.Graph() + graph.add_nodes_from(range(7)) + graph.add_edges_from([(0, 1), (1, 2), (1, 5), (2, 3), (5, 3), (3, 4), + (3, 6), (4, 6)]) + return graph + + +@pytest.fixture(scope="module") +def grid33_graph(): + return generate_grid_graph(3, 3) + + +@pytest.fixture +def decay_manager(): + return gatemgr.DecayManager(0.001, 5) + + +@pytest.fixture +def command_dag(): + return gatemgr.CommandDAG() + + +@pytest.fixture +def qubit_manager(): + return gatemgr.GateManager(generate_grid_graph(3, 3)) + + +# ============================================================================== +# DecayManager +# ------------------------------------------------------------------------------ + + +def test_decay_manager_add(decay_manager): + delta = decay_manager._delta + lifetime = decay_manager._cutoff + + decay_manager.add_to_decay(-1) + assert not decay_manager._backend_ids + + decay_manager.add_to_decay(0) + assert list(decay_manager._backend_ids) == [0] + backend_qubit = decay_manager._backend_ids[0] + assert backend_qubit['decay'] == pytest.approx(1 + delta) + assert backend_qubit['lifetime'] == lifetime + + decay_manager.add_to_decay(0) + assert list(decay_manager._backend_ids) == [0] + backend_qubit = decay_manager._backend_ids[0] + assert backend_qubit['decay'] == pytest.approx(1 + 2 * delta) + assert backend_qubit['lifetime'] == lifetime + + decay_manager.add_to_decay(1) + assert sorted(decay_manager._backend_ids) == [0, 1] + backend_qubit = decay_manager._backend_ids[0] + assert backend_qubit['decay'] == pytest.approx(1 + 2 * delta) + assert backend_qubit['lifetime'] == lifetime + + backend_qubit = decay_manager._backend_ids[1] + assert backend_qubit['decay'] == pytest.approx(1 + delta) + assert backend_qubit['lifetime'] == lifetime + + +def test_decay_manager_remove(decay_manager): + decay_manager.add_to_decay(0) + decay_manager.add_to_decay(0) + decay_manager.add_to_decay(1) + assert sorted(list(decay_manager._backend_ids)) == [0, 1] + + decay_manager.remove_decay(0) + assert list(decay_manager._backend_ids) == [1] + decay_manager.remove_decay(1) + assert not decay_manager._backend_ids + + +def test_decay_manager_get_decay_value(decay_manager): + delta = decay_manager._delta + + decay_manager.add_to_decay(0) + decay_manager.add_to_decay(0) + decay_manager.add_to_decay(1) + + assert decay_manager.get_decay_value(0) == pytest.approx(1 + 2 * delta) + assert decay_manager.get_decay_value(1) == pytest.approx(1 + delta) + assert decay_manager.get_decay_value(-1) == 1 + assert decay_manager.get_decay_value(2) == 1 + + +def test_decay_manager_step(decay_manager): + delta = decay_manager._delta + lifetime = decay_manager._cutoff + + decay_manager.add_to_decay(0) + decay_manager.step() + + backend_qubit = decay_manager._backend_ids[0] + assert backend_qubit['decay'] == pytest.approx(1 + delta) + assert backend_qubit['lifetime'] == lifetime - 1 + + decay_manager.add_to_decay(0) + decay_manager.add_to_decay(1) + + decay_manager.step() + + backend_qubit0 = decay_manager._backend_ids[0] + backend_qubit1 = decay_manager._backend_ids[1] + + assert backend_qubit0['decay'] == pytest.approx(1 + 2 * delta) + assert backend_qubit0['lifetime'] == lifetime - 1 + assert backend_qubit1['decay'] == pytest.approx(1 + delta) + assert backend_qubit1['lifetime'] == lifetime - 1 + + decay_manager.step() + assert backend_qubit0['decay'] == pytest.approx(1 + 2 * delta) + assert backend_qubit0['lifetime'] == lifetime - 2 + assert backend_qubit1['decay'] == pytest.approx(1 + delta) + assert backend_qubit1['lifetime'] == lifetime - 2 + + decay_manager.add_to_decay(1) + assert backend_qubit1['decay'] == pytest.approx(1 + 2 * delta) + assert backend_qubit1['lifetime'] == lifetime + + for i in range(3): + decay_manager.step() + + # Qubit 0 decay information should be deleted by now + assert list(decay_manager._backend_ids) == [1] + + for i in range(2): + assert list(decay_manager._backend_ids) == [1] + decay_manager.step() + assert not decay_manager._backend_ids + + +# ============================================================================== +# GatesDAG +# ------------------------------------------------------------------------------ + + +def test_command_dag_init(command_dag): + assert command_dag._dag.number_of_nodes() == 0 + assert command_dag._dag.number_of_edges() == 0 + assert not command_dag.front_layer + assert not command_dag.near_term_layer + + +def test_command_dag_add_1qubit_gate(command_dag): + cmd0a = gen_cmd(0) + cmd0b = gen_cmd(0) + cmd1 = gen_cmd(1) + # ---------------------------------- + + command_dag.add_command(cmd0a) + command_dag.add_command(cmd1) + command_dag.add_command(cmd0b) + dag_node0a = search_cmd(command_dag, cmd0a) + dag_node1 = search_cmd(command_dag, cmd1) + + with pytest.raises(RuntimeError): + search_cmd(command_dag, cmd0b) + + assert command_dag._dag.number_of_nodes() == 2 + assert command_dag._dag.number_of_edges() == 0 + assert command_dag.front_layer + assert not command_dag.near_term_layer + assert dag_node0a.logical_ids == frozenset((0, )) + assert command_dag.front_layer == [dag_node0a, dag_node1] + assert command_dag._logical_ids_in_diag == {0, 1} + assert command_dag._back_layer == {0: dag_node0a, 1: dag_node1} + + +def test_command_dag_add_1qubit_gate_allocate(command_dag): + + allocate2 = gen_cmd(2, gate=Allocate) + cmd2a = gen_cmd(2) + cmd2b = gen_cmd(2) + deallocate2 = gen_cmd(2, gate=Allocate) + + # ---------------------------------- + + command_dag.add_command(allocate2) + command_dag.add_command(cmd2a) + command_dag.add_command(cmd2b) + command_dag.add_command(deallocate2) + dag_allocate = search_cmd(command_dag, allocate2) + dag_deallocate = search_cmd(command_dag, deallocate2) + with pytest.raises(RuntimeError): + search_cmd(command_dag, cmd2a) + with pytest.raises(RuntimeError): + search_cmd(command_dag, cmd2b) + + assert command_dag._dag.number_of_nodes() == 2 + assert command_dag._dag.number_of_edges() == 1 + assert command_dag.front_layer == [dag_allocate] + assert not command_dag.near_term_layer + assert dag_allocate.logical_ids == frozenset((2, )) + assert dag_deallocate.logical_ids == frozenset((2, )) + assert command_dag._logical_ids_in_diag == {2} + assert command_dag._back_layer == {2: dag_deallocate} + + +def test_command_dag_add_2qubit_gate(command_dag): + cmd01 = gen_cmd(0, 1) + cmd56 = gen_cmd(5, 6) + cmd12 = gen_cmd(1, 2) + cmd12b = gen_cmd(1, 2) + cmd26 = gen_cmd(2, 6) + + # ---------------------------------- + + command_dag.add_command(cmd01) + dag_node01 = search_cmd(command_dag, cmd01) + + assert command_dag._dag.number_of_nodes() == 1 + assert command_dag._dag.number_of_edges() == 0 + assert command_dag.front_layer + assert not command_dag.near_term_layer + assert dag_node01.logical_ids == frozenset((0, 1)) + assert command_dag.front_layer == [dag_node01] + assert command_dag._logical_ids_in_diag == {0, 1} + assert command_dag._back_layer == {0: dag_node01, 1: dag_node01} + + # ---------------------------------- + + command_dag.add_command(cmd56) + dag_node56 = search_cmd(command_dag, cmd56) + + assert command_dag._dag.number_of_nodes() == 2 + assert command_dag._dag.number_of_edges() == 0 + assert command_dag.front_layer + assert not command_dag.near_term_layer + + assert dag_node01.logical_ids == frozenset((0, 1)) + assert dag_node56.logical_ids == frozenset((5, 6)) + + assert command_dag.front_layer == [dag_node01, dag_node56] + assert command_dag._logical_ids_in_diag == {0, 1, 5, 6} + assert command_dag._back_layer == { + 0: dag_node01, + 1: dag_node01, + 5: dag_node56, + 6: dag_node56 + } + + # ---------------------------------- + + command_dag.add_command(cmd12) + command_dag.add_command(cmd12b) + dag_node12 = search_cmd(command_dag, cmd12) + with pytest.raises(RuntimeError): + search_cmd(command_dag, cmd12b) + + assert command_dag._dag.number_of_nodes() == 3 + assert command_dag._dag.number_of_edges() == 1 + assert command_dag.front_layer + assert not command_dag.near_term_layer + + assert dag_node01.logical_ids == frozenset((0, 1)) + assert dag_node12.logical_ids == frozenset((1, 2)) + assert dag_node56.logical_ids == frozenset((5, 6)) + + assert command_dag.front_layer == [dag_node01, dag_node56] + assert command_dag._logical_ids_in_diag == {0, 1, 2, 5, 6} + assert command_dag._back_layer == { + 0: dag_node01, + 1: dag_node12, + 2: dag_node12, + 5: dag_node56, + 6: dag_node56 + } + + # ---------------------------------- + + command_dag.add_command(cmd26) + dag_node26 = search_cmd(command_dag, cmd26) + assert command_dag._dag.number_of_nodes() == 4 + assert command_dag._dag.number_of_edges() == 3 + assert command_dag.front_layer + assert not command_dag.near_term_layer + + assert command_dag.front_layer == [dag_node01, dag_node56] + assert command_dag._logical_ids_in_diag == {0, 1, 2, 5, 6} + assert command_dag._back_layer == { + 0: dag_node01, + 1: dag_node12, + 2: dag_node26, + 5: dag_node56, + 6: dag_node26 + } + + +def test_command_dag_add_gate(command_dag): + cmd0 = gen_cmd(0) + cmd01 = gen_cmd(0, 1) + cmd56 = gen_cmd(5, 6) + cmd7 = gen_cmd(7) + + # ---------------------------------- + + command_dag.add_command(cmd0) + command_dag.add_command(cmd01) + dag_node0 = search_cmd(command_dag, cmd0) + + assert len(command_dag.front_layer) == 1 + assert not command_dag.front_layer_2qubit + + assert command_dag._dag.number_of_nodes() == 2 + assert command_dag._dag.number_of_edges() == 1 + assert command_dag.front_layer == [dag_node0] + assert not command_dag.near_term_layer + + command_dag.add_command(cmd56) + command_dag.add_command(cmd7) + dag_node56 = search_cmd(command_dag, cmd56) + + assert len(command_dag.front_layer) == 3 + assert command_dag.front_layer_2qubit == [dag_node56] + + +def test_command_dag_remove_command(command_dag): + allocate0 = gen_cmd(0, gate=Allocate) + cmd0 = gen_cmd(0) + deallocate0 = gen_cmd(0, gate=Deallocate) + + # ---------------------------------- + + command_dag.add_command(allocate0) + command_dag.add_command(cmd0) + command_dag.add_command(deallocate0) + dag_allocate0 = search_cmd(command_dag, allocate0) + dag_deallocate = search_cmd(command_dag, deallocate0) + + with pytest.raises(RuntimeError): + search_cmd(command_dag, cmd0) + + with pytest.raises(RuntimeError): + command_dag.remove_command(cmd0) + + assert command_dag.front_layer == [dag_allocate0] + + command_dag.remove_command(allocate0) + assert command_dag.front_layer == [dag_deallocate] + assert command_dag._logical_ids_in_diag == {0} + + command_dag.remove_command(deallocate0) + assert not command_dag.front_layer + + +def test_command_dag_remove_command2(command_dag): + cmd01 = gen_cmd(0, 1) + cmd56 = gen_cmd(5, 6) + cmd12 = gen_cmd(1, 2) + cmd26 = gen_cmd(2, 6) + cmd78 = gen_cmd(7, 8) + + # ---------------------------------- + + command_dag.add_command(cmd01) + command_dag.add_command(cmd56) + command_dag.add_command(cmd12) + command_dag.add_command(cmd26) + command_dag.add_command(cmd78) + dag_node01 = search_cmd(command_dag, cmd01) + dag_node56 = search_cmd(command_dag, cmd56) + dag_node12 = search_cmd(command_dag, cmd12) + dag_node26 = search_cmd(command_dag, cmd26) + dag_node78 = search_cmd(command_dag, cmd78) + + with pytest.raises(RuntimeError): + command_dag.remove_command(cmd12) + + assert command_dag.front_layer == [dag_node01, dag_node56, dag_node78] + + command_dag.remove_command(cmd78) + assert command_dag.front_layer == [dag_node01, dag_node56] + assert command_dag._logical_ids_in_diag == {0, 1, 2, 5, 6} + assert 7 not in command_dag._back_layer + assert 8 not in command_dag._back_layer + + command_dag.remove_command(cmd01) + assert command_dag.front_layer == [dag_node56, dag_node12] + + command_dag.remove_command(cmd56) + assert command_dag.front_layer == [dag_node12] + + command_dag.remove_command(cmd12) + assert command_dag.front_layer == [dag_node26] + + +def test_command_dag_near_term_layer(command_dag): + cmd23a = gen_cmd(2, 3) + cmd56 = gen_cmd(5, 6) + cmd12 = gen_cmd(1, 2) + cmd34 = gen_cmd(3, 4) + cmd23b = gen_cmd(2, 3) + cmd46 = gen_cmd(4, 6) + cmd45 = gen_cmd(5, 4) + cmd14 = gen_cmd(4, 1) + command_dag.add_command(cmd23a) + command_dag.add_command(cmd56) + command_dag.add_command(cmd12) + command_dag.add_command(cmd34) + command_dag.add_command(cmd23b) + command_dag.add_command(cmd46) + command_dag.add_command(cmd45) + command_dag.add_command(cmd14) + dag_node12 = search_cmd(command_dag, cmd12) + dag_node34 = search_cmd(command_dag, cmd34) + dag_node23b = search_cmd(command_dag, cmd23b) + dag_node46 = search_cmd(command_dag, cmd46) + + command_dag.calculate_near_term_layer({i: i for i in range(7)}, depth=1) + assert command_dag.near_term_layer == [dag_node12, dag_node34] + + command_dag.calculate_near_term_layer({i: i for i in range(7)}, depth=2) + assert command_dag.near_term_layer == [ + dag_node12, dag_node34, dag_node23b, dag_node46 + ] + + +def test_command_dag_calculate_interaction_list(command_dag): + cmd01 = gen_cmd(0, 1) + cmd03 = gen_cmd(0, 3) + cmd34 = gen_cmd(3, 4) + cmd7 = gen_cmd(7, gate=Allocate) + cmd8 = gen_cmd(8) + + command_dag.add_command(cmd01) + command_dag.add_command(cmd34) + command_dag.add_command(cmd03) + command_dag.add_command(cmd8) + command_dag.add_command(cmd7) + + interactions = command_dag.calculate_interaction_list() + + assert (0, 1) in interactions or (1, 0) in interactions + assert (0, 3) in interactions or (3, 0) in interactions + assert (3, 4) in interactions or (4, 3) in interactions + + +def test_command_dag_generate_qubit_interaction_graph(command_dag): + + qb, allocate_cmds = allocate_all_qubits_cmd(9) + cmd0 = Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[1]]) + cmd1 = Command(engine=None, gate=X, qubits=([qb[2]], ), controls=[qb[3]]) + cmd2 = Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[2]]) + cmd3 = Command(engine=None, gate=X, qubits=([qb[1]], )) + + command_dag.add_command(cmd0) + command_dag.add_command(cmd1) + command_dag.add_command(cmd2) + command_dag.add_command(cmd3) + + subgraphs = command_dag.calculate_qubit_interaction_subgraphs(max_order=2) + assert len(subgraphs) == 1 + assert len(subgraphs[0]) == 4 + assert all([n in subgraphs[0] for n in [0, 1, 2, 3]]) + assert subgraphs[0][-2:] in ([1, 3], [3, 1]) + + # -------------------------------------------------------------------------- + + cmd4 = Command(engine=None, gate=X, qubits=([qb[4]], ), controls=[qb[5]]) + cmd5 = Command(engine=None, gate=X, qubits=([qb[5]], ), controls=[qb[6]]) + command_dag.add_command(cmd4) + command_dag.add_command(cmd5) + + subgraphs = command_dag.calculate_qubit_interaction_subgraphs(max_order=2) + assert len(subgraphs) == 2 + assert len(subgraphs[0]) == 4 + + assert all([n in subgraphs[0] for n in [0, 1, 2, 3]]) + assert subgraphs[0][-2:] in ([1, 3], [3, 1]) + assert subgraphs[1] in ([5, 4, 6], [5, 6, 4]) + + # -------------------------------------------------------------------------- + + cmd6 = Command(engine=None, gate=X, qubits=([qb[6]], ), controls=[qb[7]]) + cmd7 = Command(engine=None, gate=X, qubits=([qb[7]], ), controls=[qb[8]]) + command_dag.add_command(cmd6) + command_dag.add_command(cmd7) + + subgraphs = command_dag.calculate_qubit_interaction_subgraphs(max_order=2) + + assert len(subgraphs) == 2 + assert len(subgraphs[0]) == 5 + assert all([n in subgraphs[0] for n in [4, 5, 6, 7, 8]]) + assert subgraphs[0][-2:] in ([4, 8], [8, 4]) + assert len(subgraphs[1]) == 4 + assert all([n in subgraphs[1] for n in [0, 1, 2, 3]]) + assert subgraphs[1][-2:] in ([1, 3], [3, 1]) + + # -------------------------------------------------------------------------- + + command_dag.add_command( + Command(engine=None, gate=X, qubits=([qb[3]], ), controls=[qb[0]])) + subgraphs = command_dag.calculate_qubit_interaction_subgraphs(max_order=3) + + assert len(subgraphs) == 2 + assert len(subgraphs[0]) == 4 + assert all([n in subgraphs[0] for n in [0, 1, 2, 3]]) + assert subgraphs[0][0] == 0 + assert subgraphs[0][-2:] in ([1, 3], [3, 1]) + assert len(subgraphs[1]) == 5 + assert all([n in subgraphs[1] for n in [4, 5, 6, 7, 8]]) + assert subgraphs[1][-2:] in ([4, 8], [8, 4]) + + +# ============================================================================== +# MultiQubitGateManager +# ------------------------------------------------------------------------------ + + +def test_qubit_manager_valid_and_invalid_graphs(simple_graph): + graph = nx.Graph() + graph.add_nodes_from('abcd') + with pytest.raises(RuntimeError): + gatemgr.GateManager(graph=graph) + + graph.add_edges_from([('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'a')]) + with pytest.raises(RuntimeError): + gatemgr.GateManager(graph=graph) + + graph = deepcopy(simple_graph) + graph.remove_edge(0, 1) + with pytest.raises(RuntimeError): + gatemgr.GateManager(graph=graph) + + manager = gatemgr.GateManager(graph=simple_graph) + dist = manager.distance_matrix + + assert dist[0][1] == 1 + assert dist[0][2] == 2 + assert dist[0][3] == 3 + assert dist[0][4] == 4 + assert dist[0][5] == 2 + assert dist[0][6] == 4 + assert dist[1][0] == 1 + assert dist[1][2] == 1 + assert dist[1][3] == 2 + assert dist[1][4] == 3 + assert dist[1][5] == 1 + assert dist[1][6] == 3 + + +def test_qubit_manager_can_execute_gate(qubit_manager): + cmd0 = gen_cmd(0) + cmd01 = gen_cmd(0, 1) + cmd38 = gen_cmd(3, 8) + + mapping = {i: i for i in range(9)} + + manager = deepcopy(qubit_manager) + manager.add_command(cmd38) + assert not manager._can_execute_some_gate(mapping) + manager.add_command(cmd0) + assert manager._can_execute_some_gate(mapping) + + manager = deepcopy(qubit_manager) + manager.add_command(cmd38) + assert not manager._can_execute_some_gate(mapping) + manager.add_command(cmd01) + assert manager._can_execute_some_gate(mapping) + + +def test_qubit_manager_clear(qubit_manager): + cmd0 = gen_cmd(0) + cmd01 = gen_cmd(0, 1) + cmd38 = gen_cmd(3, 8) + + qubit_manager.add_command(cmd38) + qubit_manager.add_command(cmd0) + qubit_manager.add_command(cmd38) + qubit_manager.add_command(cmd01) + + qubit_manager._decay.add_to_decay(0) + + assert qubit_manager._decay._backend_ids + assert qubit_manager.dag._dag + qubit_manager.clear() + assert not qubit_manager._decay._backend_ids + assert not qubit_manager.dag._dag + + +def test_qubit_manager_generate_one_swap_step(qubit_manager): + cmd08 = gen_cmd(0, 8) + cmd01 = gen_cmd(0, 1) + cmd5 = gen_cmd(5) + + # ---------------------------------- + + manager = deepcopy(qubit_manager) + manager.add_command(cmd08) + manager.add_command(cmd5) + + mapping = {i: i for i in range(9)} + (logical_id0, backend_id0, logical_id1, + backend_id1) = manager._generate_one_swap_step( + mapping, gatemgr.nearest_neighbours_cost_fun, {}) + + assert logical_id0 in (0, 8) + if logical_id0 == 0: + assert backend_id1 in (1, 3) + else: + assert backend_id1 in (5, 7) + + mapping = {0: 0, 8: 8} + (logical_id0, backend_id0, logical_id1, + backend_id1) = manager._generate_one_swap_step( + mapping, gatemgr.nearest_neighbours_cost_fun, {}) + + assert logical_id1 == -1 + if logical_id0 == 0: + assert backend_id1 in (1, 3) + else: + assert backend_id1 in (5, 7) + + # ---------------------------------- + + manager = deepcopy(qubit_manager) + manager.add_command(cmd01) + manager.add_command(cmd08) + + mapping = {i: i for i in range(9)} + (logical_id0, backend_id0, logical_id1, + backend_id1) = manager._generate_one_swap_step( + mapping, gatemgr.nearest_neighbours_cost_fun, {}) + + # In this case, the only swap that does not increases the overall distance + # is (0, 1) + assert logical_id0 in (0, 1) + assert backend_id1 in (0, 1) + + +def test_qubit_manager_generate_swaps(qubit_manager): + cmd08 = gen_cmd(0, 8) + cmd01 = gen_cmd(0, 1) + + # ---------------------------------- + + manager = deepcopy(qubit_manager) + mapping = {i: i for i in range(9)} + + swaps, all_qubits = manager.generate_swaps( + mapping, gatemgr.nearest_neighbours_cost_fun) + + assert not swaps + assert not all_qubits + + # ---------------------------------- + + manager.add_command(cmd08) + assert manager.size() == 1 + + with pytest.raises(RuntimeError): + manager.generate_swaps(mapping, + gatemgr.nearest_neighbours_cost_fun, + max_steps=2) + + # ---------------------------------- + + mapping = {i: i for i in range(9)} + swaps, _ = manager.generate_swaps(mapping, + gatemgr.nearest_neighbours_cost_fun) + + # Make sure the original mapping was not modified + assert mapping == {i: i for i in range(9)} + + reverse_mapping = {v: k for k, v in mapping.items()} + for id0, id1 in swaps: + reverse_mapping[id0], reverse_mapping[id1] = (reverse_mapping[id1], + reverse_mapping[id0]) + + mapping = {v: k for k, v in reverse_mapping.items()} + assert manager.graph.has_edge(mapping[0], mapping[8]) + + # ---------------------------------- + + mapping = {i: i for i in range(9)} + swaps, _ = manager.generate_swaps(mapping, + gatemgr.look_ahead_parallelism_cost_fun, + opts={'W': 0.5}) + reverse_mapping = {v: k for k, v in mapping.items()} + for id0, id1 in swaps: + reverse_mapping[id0], reverse_mapping[id1] = (reverse_mapping[id1], + reverse_mapping[id0]) + + mapping = {v: k for k, v in reverse_mapping.items()} + assert manager.graph.has_edge(mapping[0], mapping[8]) + + # ---------------------------------- + + manager = deepcopy(qubit_manager) + mapping = {0: 0, 1: 1, 8: 8} + manager.add_command(cmd08) + manager.add_command(cmd01) + assert manager.size() == 2 + + swaps, all_qubits = manager.generate_swaps( + mapping, gatemgr.look_ahead_parallelism_cost_fun, opts={ + 'W': 0.5, + }) + + mapping = {i: i for i in range(9)} + reverse_mapping = {v: k for k, v in mapping.items()} + all_qubits_ref = set() + for id0, id1 in swaps: + all_qubits_ref.update((id0, id1)) + reverse_mapping[id0], reverse_mapping[id1] = (reverse_mapping[id1], + reverse_mapping[id0]) + + mapping = {v: k for k, v in reverse_mapping.items()} + + assert all_qubits == all_qubits_ref + + # Both gates should be executable at the same time + assert manager.graph.has_edge(mapping[0], mapping[8]) + assert manager.graph.has_edge(mapping[0], mapping[1]) + + +def test_qubit_manager_get_executable_commands(qubit_manager): + cmd0 = gen_cmd(0) + cmd01 = gen_cmd(0, 1) + cmd03 = gen_cmd(0, 3) + cmd34 = gen_cmd(3, 4) + cmd7 = gen_cmd(7, gate=Allocate) + cmd8a = gen_cmd(8, gate=Allocate) + cmd8b = gen_cmd(8) + + manager = deepcopy(qubit_manager) + mapping = {0: 0, 1: 1, 3: 3, 4: 4, 8: 8} + manager.add_command(cmd0) + manager.add_command(cmd01) + manager.add_command(cmd34) + manager.add_command(cmd03) + manager.add_command(cmd8a) + manager.add_command(cmd8b) + manager.add_command(cmd7) + + dag_allocate7 = search_cmd(manager.dag, cmd7) + + assert manager.size() == 6 + + cmds_to_execute, allocate_cmds = manager.get_executable_commands(mapping) + + assert cmds_to_execute == [cmd0, cmd34, cmd8a, cmd8b, cmd01, cmd03] + assert allocate_cmds == [dag_allocate7] + assert manager.size() == 1 + + mapping.update({7: 7}) + cmds_to_execute = manager.execute_allocate_cmds(allocate_cmds, mapping) + + assert cmds_to_execute == [cmd7] + assert manager.size() == 0 + + mapping = {0: 0, 1: 1, 3: 3, 4: 4, 8: 8} + manager.add_command(cmd01) + manager.add_command(cmd03) + manager.add_command(cmd34) + manager.add_command(cmd8a) + manager.add_command(cmd8b) + manager.add_command(cmd7) + + dag_allocate7 = search_cmd(manager.dag, cmd7) + + cmds_to_execute, allocate_cmds = manager.get_executable_commands(mapping) + + assert cmds_to_execute == [cmd01, cmd8a, cmd8b, cmd03, cmd34] + assert allocate_cmds == [dag_allocate7] + assert manager.size() == 1 + + +def test_qubit_manager_generate_qubit_interaction_graph(qubit_manager): + qb, allocate_cmds = allocate_all_qubits_cmd(9) + cmd_list = [ + Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[1]]), + Command(engine=None, gate=X, qubits=([qb[2]], ), controls=[qb[3]]), + Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[2]]), + Command(engine=None, gate=X, qubits=([qb[1]], )), + Command(engine=None, gate=X, qubits=([qb[4]], ), controls=[qb[5]]), + Command(engine=None, gate=X, qubits=([qb[5]], ), controls=[qb[6]]), + Command(engine=None, gate=X, qubits=([qb[6]], ), controls=[qb[7]]), + Command(engine=None, gate=X, qubits=([qb[7]], ), controls=[qb[8]]) + ] + + for cmd_last in [ + Command(engine=None, gate=X, qubits=([qb[3]], ), controls=[qb[0]]), + Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[3]]) + ]: + qubit_manager.clear() + for cmd in cmd_list: + qubit_manager.add_command(cmd) + qubit_manager.add_command(cmd_last) + + subgraphs = qubit_manager.calculate_qubit_interaction_subgraphs( + max_order=2) + + assert len(subgraphs) == 2 + assert len(subgraphs[0]) == 4 + assert all([n in subgraphs[0] for n in [0, 1, 2, 3]]) + assert subgraphs[0][0] == 0 + assert subgraphs[0][-2:] in ([1, 3], [3, 1]) + assert len(subgraphs[1]) == 4 + assert all([n in subgraphs[1] for n in [4, 5, 6, 7]]) + + +def test_qubit_manager_generate_swaps_change_mapping(qubit_manager): + cmd05 = gen_cmd(0, 5) + cmd07 = gen_cmd(0, 7) + cmd58 = gen_cmd(5, 8) + + qubit_manager.add_command(cmd05) + qubit_manager.add_command(cmd07) + qubit_manager.add_command(cmd58) + + mapping = {i: i for i in range(9)} + + swaps, all_qubits = qubit_manager.generate_swaps( + mapping, gatemgr.look_ahead_parallelism_cost_fun, {'W': 0.5}) + + reverse_mapping = {v: k for k, v in mapping.items()} + for bqb0, bqb1 in swaps: + (reverse_mapping[bqb0], + reverse_mapping[bqb1]) = (reverse_mapping[bqb1], + reverse_mapping[bqb0]) + mapping = {v: k for k, v in reverse_mapping.items()} + + cmd_list, _ = qubit_manager.get_executable_commands(mapping) + assert cmd_list == [cmd05, cmd07, cmd58] + assert qubit_manager.size() == 0 + + # ---------------------------------- + + qubit_manager.clear() + + cmd06 = gen_cmd(0, 6) + + qubit_manager.add_command(cmd05) + qubit_manager.add_command(cmd06) + qubit_manager.add_command(cmd58) + + mapping = {i: i for i in range(9)} + + swaps, all_qubits = qubit_manager.generate_swaps( + mapping, gatemgr.look_ahead_parallelism_cost_fun, {'W': 0.5}) + + reverse_mapping = {v: k for k, v in mapping.items()} + for bqb0, bqb1 in swaps: + (reverse_mapping[bqb0], + reverse_mapping[bqb1]) = (reverse_mapping[bqb1], + reverse_mapping[bqb0]) + mapping = {v: k for k, v in reverse_mapping.items()} + + cmd_list, _ = qubit_manager.get_executable_commands(mapping) + assert cmd_list == [cmd05, cmd06] + assert qubit_manager.size() == 1 + + +def test_qubit_manager_str(): + qubit_manager = gatemgr.GateManager(generate_grid_graph(3, 3)) + + qb, allocate_cmds = allocate_all_qubits_cmd(9) + cmd_list = [ + Command(engine=None, gate=H, qubits=([qb[0]], )), + Command(engine=None, gate=X, qubits=([qb[0]], ), controls=[qb[8]]), + Command(engine=None, gate=X, qubits=([qb[2]], ), controls=[qb[6]]), + Command(engine=None, gate=X, qubits=([qb[1]], ), controls=[qb[7]]), + Command(engine=None, gate=X, qubits=([qb[1]], )), + Command(engine=None, gate=X, qubits=([qb[4]], ), controls=[qb[5]]), + Command(engine=None, gate=X, qubits=([qb[5]], ), controls=[qb[4]]), + Command(engine=None, gate=X, qubits=([qb[3]], ), controls=[qb[4]]), + Command(engine=None, gate=X, qubits=([qb[5]], ), controls=[qb[4]]), + Command(engine=None, gate=X, qubits=([qb[3]], ), controls=[qb[4]]), + Command(engine=None, gate=X, qubits=([qb[5]], ), controls=[qb[6]]), + Command(engine=None, gate=X, qubits=([qb[6]], ), controls=[qb[7]]), + Command(engine=None, gate=X, qubits=([qb[7]], ), controls=[qb[8]]), + ] + + for cmd in cmd_list: + qubit_manager.add_command(cmd) + + mapping = {i: i for i in range(16)} + + while qubit_manager.size() > 0: + qubit_manager.get_executable_commands(mapping) + + swaps, all_qubits = qubit_manager.generate_swaps( + mapping, gatemgr.look_ahead_parallelism_cost_fun, {'W': 0.5}) + + reverse_mapping = {v: k for k, v in mapping.items()} + for bqb0, bqb1 in swaps: + (reverse_mapping[bqb0], + reverse_mapping[bqb1]) = (reverse_mapping[bqb1], + reverse_mapping[bqb0]) + mapping = {v: k for k, v in reverse_mapping.items()} + + str_repr = str(qubit_manager) + + num_of_2qubit_gates_ref = 0 + for cmd in cmd_list: + if len({qubit.id for qureg in cmd.all_qubits for qubit in qureg}) == 2: + num_of_2qubit_gates_ref += 1 + + num_of_2qubit_gates = 0 + for line in str_repr.split('\n'): + m = re.match(r'^\s+\[[0-9]+,\s[0-9]+\]:\s*([0-9]+)$', line) + if m: + num_of_2qubit_gates += int(m.group(1)) + + edge34_count = int(re.search(r'\s+\[3, 4\]:\s+([0-9]+)', + str_repr).group(1)) + assert num_of_2qubit_gates == num_of_2qubit_gates_ref + assert edge34_count > 1 + assert str_repr.count("[4, 5]: 3") == 1 diff --git a/projectq/cengines/_graphmapper.py b/projectq/cengines/_graphmapper.py new file mode 100644 index 000000000..883740dbf --- /dev/null +++ b/projectq/cengines/_graphmapper.py @@ -0,0 +1,699 @@ +# Copyright 2018 ProjectQ-Framework (wOAww.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Mapper for a quantum circuit to an arbitrary connected graph. + +Input: Quantum circuit with 1 and 2 qubit gates on n qubits. Gates are assumed +to be applied in parallel if they act on disjoint qubit(s) and any pair of +qubits can perform a 2 qubit gate (all-to-all connectivity) + +Output: Quantum circuit in which qubits are placed in 2-D square grid in which +only nearest neighbour qubits can perform a 2 qubit gate. The mapper uses Swap +gates in order to move qubits next to each other. +""" +from copy import deepcopy + +import random +import sys + +from projectq.cengines import (BasicMapperEngine, return_swap_depth) +from projectq.meta import LogicalQubitIDTag +from projectq.ops import (AllocateQubitGate, Command, DeallocateQubitGate, + FlushGate, Swap) +from projectq.types import WeakQubitRef +from ._gate_manager import GateManager, look_ahead_parallelism_cost_fun + +# ------------------------------------------------------------------------------ + +# https://www.peterbe.com/plog/fastest-way-to-uniquify-a-list-in-python-3.6 +if sys.version_info[0] >= 3 and sys.version_info[1] > 6: # pragma: no cover + + def uniquify_list(seq): + # pylint: disable=missing-function-docstring + return list(dict.fromkeys(seq)) +else: # pragma: no cover + + def uniquify_list(seq): + # pylint: disable=missing-function-docstring + seen = set() + seen_add = seen.add + return [x for x in seq if x not in seen and not seen_add(x)] + + +# ============================================================================== + + +class defaults(object): + """ + Class containing default values for some options + """ + #: Defaults to :py:func:`.look_ahead_parallelism_cost_fun` + cost_fun = staticmethod(look_ahead_parallelism_cost_fun) + max_swap_steps = 30 + + +# ============================================================================== + + +class GraphMapperError(Exception): + """Base class for all exceptions related to the GraphMapper.""" + + +def _add_qubits_to_mapping_fcfs(current_mapping, graph, new_logical_qubit_ids, + commands_dag): + """ + Add active qubits to a mapping. + + This function implements the simple first-come first serve approach; + Qubits that are active but not yet registered in the mapping are added by + mapping them to the next available backend id + + Args: + current_mapping (dict): specify which method should be used to + add the new qubits to the current mapping + graph (networkx.Graph): underlying graph used by the mapper + new_logical_qubit_ids (list): list of logical ids not yet part of the + mapping and that need to be assigned a + backend id + stored_commands (CommandList): list of commands yet to be processed by + the mapper + + Returns: A new mapping + """ + # pylint: disable=unused-argument + + mapping = deepcopy(current_mapping) + currently_used_nodes = sorted([v for _, v in mapping.items()]) + available_nodes = [n for n in graph if n not in currently_used_nodes] + + for i, logical_id in enumerate(new_logical_qubit_ids): + mapping[logical_id] = available_nodes[i] + return mapping + + +def _generate_mapping_minimize_swaps(graph, qubit_interaction_subgraphs): + """ + Generate an initial mapping while maximizing the number of 2-qubit gates + that can be applied without applying any SWAP operations. + + Args: + graph (networkx.Graph): underlying graph used by the mapper + qubit_interaction_subgraph (list): see documentation for CommandList + + Returns: A new mapping + """ + mapping = {} + available_nodes = sorted(list(graph), key=lambda n: len(graph[n])) + + # Initialize the seed node + logical_id = qubit_interaction_subgraphs[0].pop(0) + backend_id = available_nodes.pop() + mapping[logical_id] = backend_id + + for subgraph in qubit_interaction_subgraphs: + anchor_node = backend_id + for logical_id in subgraph: + neighbours = sorted( + [n for n in graph[anchor_node] if n in available_nodes], + key=lambda n: len(graph[n])) + + # If possible, take the neighbour with the highest + # degree. Otherwise, take the next highest order available node + if neighbours: + backend_id = neighbours[-1] + available_nodes.remove(backend_id) + else: + backend_id = available_nodes.pop() + mapping[logical_id] = backend_id + + return mapping + + +def _add_qubits_to_mapping_smart_init(current_mapping, graph, + new_logical_qubit_ids, commands_dag): + """ + Add active qubits to a mapping. + + Similar to the first-come first-serve approach, except the initial mapping + tries to maximize the initial number of gates to be applied without + swaps. Otherwise identical to the first-come first-serve approach. + + Args: + current_mapping (dict): specify which method should be used to + add the new qubits to the current mapping + graph (networkx.Graph): underlying graph used by the mapper + new_logical_qubit_ids (list): list of logical ids not yet part of the + mapping and that need to be assigned a + backend id + stored_commands (CommandList): list of commands yet to be processed by + the mapper + + Returns: A new mapping + """ + if not current_mapping: + qubit_interaction_subgraphs = \ + commands_dag.calculate_qubit_interaction_subgraphs(max_order=2) + + # Interaction subgraph list can be empty if only single qubit gates are + # present + if not qubit_interaction_subgraphs: + qubit_interaction_subgraphs = [list(new_logical_qubit_ids)] + + return _generate_mapping_minimize_swaps(graph, + qubit_interaction_subgraphs) + return _add_qubits_to_mapping_fcfs(current_mapping, graph, + new_logical_qubit_ids, commands_dag) + + +def _add_qubits_to_mapping(current_mapping, graph, new_logical_qubit_ids, + commands_dag): + """ + Add active qubits to a mapping + + Qubits that are active but not yet registered in the mapping are added by + mapping them to an available backend id, as close as possible to other + qubits which they might interact with. + + Args: + current_mapping (dict): specify which method should be used to + add the new qubits to the current mapping + graph (networkx.Graph): underlying graph used by the mapper + new_logical_qubit_ids (list): list of logical ids not yet part of the + mapping and that need to be assigned a + backend id + stored_commands (CommandList): list of commands yet to be processed by + the mapper + + Returns: A new mapping + """ + if not current_mapping: + qubit_interaction_subgraphs = \ + commands_dag.calculate_qubit_interaction_subgraphs(max_order=2) + + # Interaction subgraph list can be empty if only single qubit gates are + # present + if not qubit_interaction_subgraphs: + qubit_interaction_subgraphs = [list(new_logical_qubit_ids)] + + return _generate_mapping_minimize_swaps(graph, + qubit_interaction_subgraphs) + + mapping = deepcopy(current_mapping) + currently_used_nodes = sorted([v for _, v in mapping.items()]) + available_nodes = sorted( + [n for n in graph if n not in currently_used_nodes], + key=lambda n: len(graph[n])) + + for logical_id in uniquify_list(new_logical_qubit_ids): + qubit_interactions = uniquify_list([ + i[0] if i[0] != logical_id else i[1] + for i in commands_dag.calculate_interaction_list() + if logical_id in i + ]) + + backend_id = None + + if len(qubit_interactions) == 1: + # If there's only a single qubit interacting and it is already + # present within the mapping, find the neighbour with the highest + # degree + + qubit = qubit_interactions[0] + + if qubit in mapping: + candidates = sorted( + [n for n in graph[mapping[qubit]] if n in available_nodes], + key=lambda n: len(graph[n])) + if candidates: + backend_id = candidates[-1] + elif qubit_interactions: + # If there are multiple qubits interacting, find out all the + # neighbouring nodes for each interaction. Then within those + # nodes, try to find the one that maximizes the number of + # interactions without swapping + + neighbours = [] + for qubit in qubit_interactions: + if qubit in mapping: + neighbours.append( + set(n for n in graph[mapping[qubit]] + if n in available_nodes)) + else: + break + + # Try to find an intersection that maximizes the number of + # interactions by iteratively reducing the number of considered + # interactions + + intersection = set() + while neighbours and not intersection: + intersection = neighbours[0].intersection(*neighbours[1:]) + neighbours.pop() + + if intersection: + backend_id = intersection.pop() + + if backend_id is None: + backend_id = available_nodes.pop() + else: + available_nodes.remove(backend_id) + + mapping[logical_id] = backend_id + + return mapping + + +class GraphMapper(BasicMapperEngine): + """ + Mapper to an arbitrary connected graph. + + Maps a quantum circuit to an arbitrary connected graph of connected qubits + using Swap gates. + + .. seealso:: + :py:mod:`projectq.cengines._gate_manager` + + Args: + graph (networkx.Graph) : Arbitrary connected graph + storage (int): Approximate number of gates to temporarily store + add_qubits_to_mapping (function or str): Function called when + new qubits are to be added to the current mapping. + Special possible string values: + + - ``"fcfs"``: first-come first serve + - ``"fcfs_init"``: first-come first serve with smarter mapping + initialisation + + Note: + 1) Gates are cached and only mapped from time to time. A + FastForwarding gate doesn't empty the cache, only a FlushGate does. + 2) Only 1 and two qubit gates allowed. + 3) Does not optimize for dirty qubits. + 4) Signature for third argument is + ``add_qubits_to_mapping(current_mapping, graph, + new_logical_qubit_ids, command_dag)`` + + Attributes: + current_mapping: Stores the mapping: key is logical qubit id, value + is mapped qubit id from 0,...,self.num_qubits + storage (int): Approximate number of gate it caches before mapping. + num_qubits(int): number of qubits + num_mappings (int): Number of times the mapper changed the mapping + depth_of_swaps (dict): Key are circuit depth of swaps, value is the + number of such mappings which have beenapplied + num_of_swaps_per_mapping (dict): Key are the number of swaps per + mapping, value is the number of such mappings which have + been applied + """ + def __init__(self, + graph, + storage=1000, + add_qubits_to_mapping=_add_qubits_to_mapping, + opts=None): + """ + Initialize a GraphMapper compiler engine. + + Args: + graph (networkx.Graph): Arbitrary connected graph representing + Qubit connectivity + storage (int): Approximate number of gates to temporarily store + add_qubits_to_mapping (function or str): Function called + when new qubits are to be added to the current + mapping. + Special possible string values: + + - ``"fcfs"``: first-come first serve + - ``"fcfs_init"``: first-come first serve with smarter mapping + initialisation + opts (dict): Extra options (see below) + + Raises: + RuntimeError: if the graph is not a connected graph + + Note: + ``opts`` may contain the following key-values: + + .. list-table:: + :header-rows: 1 + + * - Key + - Type + - Description + * - decay_opts + - ``dict`` + - | Options to pass onto the :py:class:`.DecayManager` + constructor + | (see :py:class:`._gate_manager.defaults`) + * - swap_opts + - ``dict`` + - | Extra options used when generating a list of swap + | operations. + | Acceptable keys: W, cost_fun, near_term_layer_depth, + | max_swap_steps + | (see :py:meth:`.GateManager.generate_swaps`, + | :py:class:`._graphmapper.defaults` and + | :py:class:`._gate_manager.defaults`) + """ + BasicMapperEngine.__init__(self) + + if opts is None: + self._opts = {} + else: + self._opts = opts + + self.qubit_manager = GateManager(graph=graph, + decay_opts=self._opts.get( + 'decay_opts', { + 'delta': 0.001, + 'max_lifetime': 5 + })) + self.num_qubits = graph.number_of_nodes() + self.storage = storage + # Randomness to pick permutations if there are too many. + # This creates an own instance of Random in order to not influence + # the bound methods of the random module which might be used in other + # places. + self._rng = random.Random(11) + # Logical qubit ids for which the Allocate gate has already been + # processed and sent to the next engine but which are not yet + # deallocated: + self._currently_allocated_ids = set() + # Our internal mappings + self._current_mapping = dict() # differs from other mappers + self._reverse_current_mapping = dict() + # Function to add new logical qubits ids to the mapping + self.set_add_qubits_to_mapping(add_qubits_to_mapping) + + # Statistics: + self.num_mappings = 0 + self.depth_of_swaps = dict() + self.num_of_swaps_per_mapping = dict() + + @property + def current_mapping(self): + """Return a copy of the current mapping.""" + return deepcopy(self._current_mapping) + + @current_mapping.setter + def current_mapping(self, current_mapping): + """Set the current mapping to a new value.""" + if not current_mapping: + self._current_mapping = dict() + self._reverse_current_mapping = dict() + else: + self._current_mapping = current_mapping + self._reverse_current_mapping = { + v: k + for k, v in self._current_mapping.items() + } + + def set_add_qubits_to_mapping(self, add_qubits_to_mapping): + """ + Modify the callback function used to add qubits to an existing mapping + + Args: + add_qubits_to_mapping (function): Callback function + + Note: + Signature for callback function is: + ``add_qubits_to_mapping(current_mapping, graph, + new_logical_qubit_ids, command_dag)`` + """ + if isinstance(add_qubits_to_mapping, str): + if add_qubits_to_mapping.lower() == "fcfs": + self._add_qubits_to_mapping = _add_qubits_to_mapping_fcfs + elif add_qubits_to_mapping.lower() == "fcfs_init": + self._add_qubits_to_mapping = _add_qubits_to_mapping_smart_init + else: + raise ValueError( + "Invalid invalid value for add_qubits_to_mapping: {}". + format(add_qubits_to_mapping)) + else: + self._add_qubits_to_mapping = add_qubits_to_mapping + + def is_available(self, cmd): + """Only allows 1 or two qubit gates.""" + num_qubits = 0 + for qureg in cmd.all_qubits: + num_qubits += len(qureg) + return num_qubits <= 2 + + def _send_single_command(self, cmd): + """ + Send a command to the next engine taking care of mapped qubit IDs + + Args: + cmd (Command): A ProjectQ command + """ + + if isinstance(cmd.gate, AllocateQubitGate): + assert cmd.qubits[0][0].id in self._current_mapping + qb0 = WeakQubitRef(engine=self, + idx=self._current_mapping[cmd.qubits[0][0].id]) + self._currently_allocated_ids.add(cmd.qubits[0][0].id) + self.send([ + Command(engine=self, + gate=AllocateQubitGate(), + qubits=([qb0], ), + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + ]) + elif isinstance(cmd.gate, DeallocateQubitGate): + assert cmd.qubits[0][0].id in self._current_mapping + qb0 = WeakQubitRef(engine=self, + idx=self._current_mapping[cmd.qubits[0][0].id]) + self._currently_allocated_ids.remove(cmd.qubits[0][0].id) + self._current_mapping.pop(cmd.qubits[0][0].id) + self.send([ + Command(engine=self, + gate=DeallocateQubitGate(), + qubits=([qb0], ), + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + ]) + else: + self._send_cmd_with_mapped_ids(cmd) + + def _send_possible_commands(self): + """ + Send as many commands as possible without introducing swap operations + + Note: + This function will modify the current mapping when qubit + allocation/deallocation gates are encountered + """ + + (cmds_to_execute, + allocate_cmds) = self.qubit_manager.get_executable_commands( + self._current_mapping) + + # Execute all the commands that can possibly be executed + for cmd in cmds_to_execute: + self._send_single_command(cmd) + + # There are no more commands to + num_available_qubits = self.num_qubits - len(self._current_mapping) + if allocate_cmds and num_available_qubits > 0: + + def rank_allocate_cmds(cmds_list, dag): + # pylint: disable=unused-argument + return cmds_list + + allocate_cmds = rank_allocate_cmds( + allocate_cmds, self.qubit_manager.dag)[:num_available_qubits] + not_in_mapping_qubits = [node.logical_id for node in allocate_cmds] + + new_mapping = self._add_qubits_to_mapping(self._current_mapping, + self.qubit_manager.graph, + not_in_mapping_qubits, + self.qubit_manager.dag) + + self.current_mapping = new_mapping + + for cmd in self.qubit_manager.execute_allocate_cmds( + allocate_cmds, self._current_mapping): + self._send_single_command(cmd) + + cmds_to_execute, _ = self.qubit_manager.get_executable_commands( + self._current_mapping) + for cmd in cmds_to_execute: + self._send_single_command(cmd) + + def _run(self): + """ + Create a new mapping and executes possible gates. + + First execute all possible commands, given the current mapping. Then + find a new mapping, perform the swap operawtions to get to the new + mapping and then execute all possible gates once more. Non-allocated + qubits that are required for the swap operations will be automatically + allocated and deallocated as required. + + Raises: + RuntimeError if the mapper is unable to make progress (possibly + due to an insufficient number of qubits) + """ + + num_of_stored_commands_before = self.qubit_manager.size() + + self._send_possible_commands() + if not self.qubit_manager.size(): + return + + # NB: default values are taken care of at place of access + swap_opts = self._opts.get('swap_opts', {}) + + swaps, all_swapped_qubits = self.qubit_manager.generate_swaps( + self._current_mapping, + cost_fun=swap_opts.get('cost_fun', defaults.cost_fun), + opts=swap_opts, + max_steps=swap_opts.get('max_swap_steps', defaults.max_swap_steps)) + + if swaps: + # Get a list of the qubits we need to allocate just to perform the + # swaps + not_allocated_ids = all_swapped_qubits.difference({ + self._current_mapping[logical_id] + for logical_id in self._currently_allocated_ids + }) + + # Calculate temporary internal reverse mapping + new_internal_mapping = deepcopy(self._reverse_current_mapping) + + # Allocate all mapped qubit ids that are not currently allocated + # but part of some path so that we may perform the swapping + # operations. + for backend_id in not_allocated_ids: + qb0 = WeakQubitRef(engine=self, idx=backend_id) + self.send([ + Command(engine=self, + gate=AllocateQubitGate(), + qubits=([qb0], )) + ]) + + # Those qubits are not part of the current mapping, so add them + # to the temporary internal reverse mapping with invalid ids + new_internal_mapping[backend_id] = -1 + + # Send swap operations to arrive at the new mapping + for bqb0, bqb1 in swaps: + qb0 = WeakQubitRef(engine=self, idx=bqb0) + qb1 = WeakQubitRef(engine=self, idx=bqb1) + self.send( + [Command(engine=self, gate=Swap, qubits=([qb0], [qb1]))]) + + # Update internal mapping based on swap operations + new_internal_mapping[bqb0], \ + new_internal_mapping[bqb1] = \ + new_internal_mapping[bqb1], \ + new_internal_mapping[bqb0] + + # Register statistics: + self.num_mappings += 1 + depth = return_swap_depth(swaps) + self.depth_of_swaps[depth] = self.depth_of_swaps.get(depth, 0) + 1 + self.num_of_swaps_per_mapping[len( + swaps)] = self.num_of_swaps_per_mapping.get(len(swaps), 0) + 1 + + # Calculate the list of "helper" qubits that need to be deallocated + # and remove invalid entries + not_needed_anymore = [] + new_reverse_current_mapping = {} + for backend_id, logical_id in new_internal_mapping.items(): + if logical_id < 0: + not_needed_anymore.append(backend_id) + else: + new_reverse_current_mapping[backend_id] = logical_id + + # Deallocate all previously mapped ids which we only needed for the + # swaps: + for backend_id in not_needed_anymore: + qb0 = WeakQubitRef(engine=self, idx=backend_id) + self.send([ + Command(engine=self, + gate=DeallocateQubitGate(), + qubits=([qb0], )) + ]) + + # Calculate new mapping + self.current_mapping = { + v: k + for k, v in new_reverse_current_mapping.items() + } + + # Send possible gates: + self._send_possible_commands() + + # Check that mapper actually made progress + if self.qubit_manager.size() == num_of_stored_commands_before: + raise RuntimeError("Mapper is potentially in an infinite loop. " + "It is likely that the algorithm requires " + "too many qubits. Increase the number of " + "qubits for this mapper.") + + def receive(self, command_list): + """ + Receive some commands. + + Receive a command list and, for each command, stores it until + we do a mapping (FlushGate or Cache of stored commands is full). + + Args: + command_list (list of Command objects): list of commands to + receive. + """ + for cmd in command_list: + + qubit_ids = [ + qubit.id for qureg in cmd.all_qubits for qubit in qureg + ] + + if len(qubit_ids) > 2 or not qubit_ids: + raise Exception("Invalid command (number of qubits): " + + str(cmd)) + + if isinstance(cmd.gate, FlushGate): + while self.qubit_manager.size() > 0: + self._run() + self.send([cmd]) + else: + self.qubit_manager.add_command(cmd) + + # Storage is full: Create new map and send some gates away: + if self.qubit_manager.size() >= self.storage: + self._run() + + def __str__(self): + """ + Return the string representation of this GraphMapper. + + Returns: + A summary (string) of resources used, including depth of swaps and + statistics about the swaps themselves + """ + + depth_of_swaps_str = "" + for depth_of_swaps, num_mapping in sorted(self.depth_of_swaps.items()): + depth_of_swaps_str += "\n {:3d}: {:3d}".format( + depth_of_swaps, num_mapping) + + num_swaps_per_mapping_str = "" + for num_swaps_per_mapping, num_mapping \ + in sorted(self.num_of_swaps_per_mapping.items(), + key=lambda x: x[1], reverse=True): + num_swaps_per_mapping_str += "\n {:3d}: {:3d}".format( + num_swaps_per_mapping, num_mapping) + + return ("Number of mappings: {}\n" + "Depth of swaps: {}\n\n" + + "Number of swaps per mapping:{}\n\n{}\n\n").format( + self.num_mappings, depth_of_swaps_str, + num_swaps_per_mapping_str, str(self.qubit_manager)) diff --git a/projectq/cengines/_graphmapper_test.py b/projectq/cengines/_graphmapper_test.py new file mode 100644 index 000000000..64f6f0bda --- /dev/null +++ b/projectq/cengines/_graphmapper_test.py @@ -0,0 +1,863 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.cengines._graphmapper.py.""" + +from copy import deepcopy +import itertools + +import pytest +import networkx as nx +from projectq.cengines import DummyEngine, LocalOptimizer, MainEngine +from projectq.meta import LogicalQubitIDTag +from projectq.ops import (Allocate, BasicGate, Command, Deallocate, FlushGate, + X, H, All, Measure, CNOT) +from projectq.types import WeakQubitRef + +from projectq.cengines import _graphmapper as graphm + +import projectq.cengines._gate_manager as gatemgr + + +def decay_to_string(self): + s = '' + for qubit_id, node in self._backend_ids.items(): + s += '{}: {}, {}\n'.format(qubit_id, node['decay'], node['lifetime']) + return s + + +gatemgr.DecayManager.__str__ = decay_to_string +Command.__repr__ = Command.__str__ + + +def allocate_all_qubits_cmd(mapper): + qb = [] + allocate_cmds = [] + for i in range(mapper.num_qubits): + qb.append(WeakQubitRef(engine=None, idx=i)) + allocate_cmds.append( + Command(engine=None, gate=Allocate, qubits=([qb[i]], ))) + return qb, allocate_cmds + + +def generate_grid_graph(nrows, ncols): + graph = nx.Graph() + graph.add_nodes_from(range(nrows * ncols)) + + for row in range(nrows): + for col in range(ncols): + node0 = col + ncols * row + + is_middle = ((0 < row < nrows - 1) and (0 < col < ncols - 1)) + add_horizontal = is_middle or (row in (0, nrows - 1) and + (0 < col < ncols - 1)) + add_vertical = is_middle or (col in (0, ncols - 1) and + (0 < row < nrows - 1)) + if add_horizontal: + graph.add_edge(node0, node0 - 1) + graph.add_edge(node0, node0 + 1) + if add_vertical: + graph.add_edge(node0, node0 - ncols) + graph.add_edge(node0, node0 + ncols) + if nrows == 2: + node0 = col + graph.add_edge(node0, node0 + ncols) + if ncols == 2: + node0 = ncols * row + graph.add_edge(node0, node0 + 1) + + return graph + + +@pytest.fixture(scope="module") +def simple_graph(): + # 2 4 + # / \ / | + # 0 - 1 3 | + # \ / \ | + # 5 6 + graph = nx.Graph() + graph.add_nodes_from(range(7)) + graph.add_edges_from([(0, 1), (1, 2), (1, 5), (2, 3), (5, 3), (3, 4), + (3, 6), (4, 6)]) + return graph + + +@pytest.fixture(scope="module") +def grid22_graph(): + graph = nx.Graph() + graph.add_nodes_from([0, 1, 2, 3]) + graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) + return graph + + +@pytest.fixture(scope="module") +def grid33_graph(): + return generate_grid_graph(3, 3) + + +@pytest.fixture +def grid22_graph_mapper(grid22_graph): + mapper = graphm.GraphMapper(graph=grid22_graph, + add_qubits_to_mapping="fcfs") + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + return mapper, backend + + +@pytest.fixture +def grid33_graph_mapper(grid33_graph): + mapper = graphm.GraphMapper(graph=grid33_graph, + add_qubits_to_mapping="fcfs") + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + return mapper, backend + + +@pytest.fixture +def simple_mapper(simple_graph): + mapper = graphm.GraphMapper(graph=simple_graph, + add_qubits_to_mapping="fcfs") + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + return mapper, backend + + +# ============================================================================== + + +def get_node_list(self): + return list(self.dag._dag.nodes) + + +graphm.GateManager._get_node_list = get_node_list + +# ============================================================================== + + +def test_is_available(simple_graph): + mapper = graphm.GraphMapper(graph=simple_graph) + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + cmd0 = Command(None, BasicGate(), qubits=([qb0], )) + assert mapper.is_available(cmd0) + cmd1 = Command(None, BasicGate(), qubits=([qb0], ), controls=[qb1]) + assert mapper.is_available(cmd1) + cmd2 = Command(None, BasicGate(), qubits=([qb0], [qb1, qb2])) + assert not mapper.is_available(cmd2) + cmd3 = Command(None, BasicGate(), qubits=([qb0], [qb1]), controls=[qb2]) + assert not mapper.is_available(cmd3) + + +def test_invalid_gates(simple_mapper): + mapper, backend = simple_mapper + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], ), controls=[]) + cmd1 = Command(engine=None, gate=Allocate, qubits=([qb1], ), controls=[]) + cmd2 = Command(engine=None, gate=Allocate, qubits=([qb2], ), controls=[]) + cmd3 = Command(engine=None, gate=X, qubits=([qb0], [qb1]), controls=[qb2]) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + + with pytest.raises(Exception): + mapper.receive([cmd0, cmd1, cmd2, cmd3, cmd_flush]) + + +def test_init(simple_graph): + opts = {'decay_opts': {'delta': 0.002}} + + mapper = graphm.GraphMapper(graph=simple_graph, opts=opts) + assert mapper.qubit_manager._decay._delta == 0.002 + assert mapper.qubit_manager._decay._cutoff == 5 + + opts = {'decay_opts': {'delta': 0.002, 'max_lifetime': 10}} + + mapper = graphm.GraphMapper(graph=simple_graph, opts=opts) + assert mapper.qubit_manager._decay._delta == 0.002 + assert mapper.qubit_manager._decay._cutoff == 10 + + +def test_resetting_mapping_to_none(simple_graph): + mapper = graphm.GraphMapper(graph=simple_graph) + mapper.current_mapping = {0: 1} + assert mapper._current_mapping == {0: 1} + assert mapper._reverse_current_mapping == {1: 0} + mapper.current_mapping = {0: 0, 1: 4} + assert mapper._current_mapping == {0: 0, 1: 4} + assert mapper._reverse_current_mapping == {0: 0, 4: 1} + mapper.current_mapping = None + assert mapper._current_mapping == {} + assert mapper._reverse_current_mapping == {} + + +def test_add_qubits_to_mapping_methods_failure(simple_graph): + with pytest.raises(ValueError): + graphm.GraphMapper(graph=simple_graph, add_qubits_to_mapping="as") + + +@pytest.mark.parametrize("add_qubits", ["fcfs", "fcfs_init", "FCFS"]) +def test_add_qubits_to_mapping_methods_only_single(simple_graph, add_qubits): + mapper = graphm.GraphMapper(graph=simple_graph, + add_qubits_to_mapping=add_qubits) + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + gates = [ + Command(None, X, qubits=([qb[1]], )), + Command(None, X, qubits=([qb[2]], )), + ] + + mapper.receive(list(itertools.chain(allocate_cmds, gates, [cmd_flush]))) + assert mapper.num_mappings == 0 + + +@pytest.mark.parametrize("add_qubits", ["fcfs", "fcfs_init", "FCFS"]) +def test_add_qubits_to_mapping_methods(simple_graph, add_qubits): + mapper = graphm.GraphMapper(graph=simple_graph, + add_qubits_to_mapping=add_qubits) + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + gates = [ + Command(None, X, qubits=([qb[1]], ), controls=[qb[0]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[2]]), + ] + + mapper.receive(list(itertools.chain(allocate_cmds, gates, [cmd_flush]))) + assert mapper.num_mappings == 0 + + +def test_qubit_placement_initial_mapping_single_qubit_gates( + grid33_graph_mapper): + grid33_graph_mapper[0].set_add_qubits_to_mapping( + graphm._add_qubits_to_mapping) + mapper, backend = deepcopy(grid33_graph_mapper) + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + + mapper.receive(allocate_cmds + [cmd_flush]) + mapping = mapper.current_mapping + + assert mapper.num_mappings == 0 + assert mapping[0] == 4 + assert sorted([mapping[1], mapping[2], mapping[3], + mapping[4]]) == [1, 3, 5, 7] + assert sorted([mapping[5], mapping[6], mapping[7], + mapping[8]]) == [0, 2, 6, 8] + + +def test_qubit_placement_single_two_qubit_gate(grid33_graph_mapper): + grid33_graph_mapper[0].set_add_qubits_to_mapping( + graphm._add_qubits_to_mapping) + mapper_ref, backend = deepcopy(grid33_graph_mapper) + + mapper_ref.current_mapping = {3: 3, 4: 4, 5: 5} + mapper_ref._currently_allocated_ids = set( + mapper_ref.current_mapping.keys()) + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper_ref) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + + mapper = deepcopy(mapper_ref) + mapper.receive([ + allocate_cmds[0], + Command(None, X, qubits=([qb[0]], ), controls=[qb[3]]), cmd_flush + ]) + mapping = mapper.current_mapping + + assert mapper.num_mappings == 0 + assert mapping[0] in {0, 6} + + mapper = deepcopy(mapper_ref) + mapper.receive([ + allocate_cmds[6], + Command(None, X, qubits=([qb[3]], ), controls=[qb[6]]), cmd_flush + ]) + mapping = mapper.current_mapping + + assert mapper.num_mappings == 0 + assert mapping[6] in {0, 6} + + +def test_qubit_placement_double_two_qubit_gate(grid33_graph_mapper): + grid33_graph_mapper[0].set_add_qubits_to_mapping( + graphm._add_qubits_to_mapping) + mapper_ref, backend_ref = deepcopy(grid33_graph_mapper) + + mapper_ref.current_mapping = {1: 1, 3: 3, 4: 4, 5: 5} + mapper_ref._currently_allocated_ids = set( + mapper_ref.current_mapping.keys()) + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper_ref) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + + mapper = deepcopy(mapper_ref) + backend = deepcopy(backend_ref) + mapper.next_engine = backend + mapper.receive([ + allocate_cmds[0], + Command(None, X, qubits=([qb[0]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[0]], ), controls=[qb[1]]), cmd_flush + ]) + mapping = mapper.current_mapping + + assert mapper.num_mappings == 0 + assert mapping[0] == 0 + + mapper = deepcopy(mapper_ref) + backend = deepcopy(backend_ref) + mapper.next_engine = backend + mapper.receive([ + allocate_cmds[2], + Command(None, X, qubits=([qb[2]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[2]], ), controls=[qb[1]]), + Command(None, X, qubits=([qb[2]], ), controls=[qb[5]]), + cmd_flush, + ]) + mapping = mapper.current_mapping + + assert backend.received_commands[0].gate == Allocate + assert backend.received_commands[0].qubits[0][0].id in [0, 2, 6, 8] + assert backend.received_commands[0].tags == [LogicalQubitIDTag(2)] + + +def test_qubit_placement_multiple_two_qubit_gates(grid33_graph_mapper): + grid33_graph_mapper[0].set_add_qubits_to_mapping( + graphm._add_qubits_to_mapping) + mapper, backend = deepcopy(grid33_graph_mapper) + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + gates = [ + Command(None, X, qubits=([qb[1]], ), controls=[qb[0]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[2]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[4]]), + ] + + all_cmds = list(itertools.chain(allocate_cmds, gates)) + mapper, backend = deepcopy(grid33_graph_mapper) + mapper.receive(all_cmds + [cmd_flush]) + mapping = mapper.current_mapping + + assert mapper.num_mappings == 0 + assert mapping[1] == 4 + assert sorted([mapping[0], mapping[2], mapping[3], + mapping[4]]) == [1, 3, 5, 7] + assert sorted([mapping[5], mapping[6], mapping[7], + mapping[8]]) == [0, 2, 6, 8] + + all_cmds = list(itertools.chain(allocate_cmds[:5], gates)) + mapper, backend = deepcopy(grid33_graph_mapper) + mapper.receive(all_cmds + [cmd_flush]) + + gates = [ + Command(None, X, qubits=([qb[5]], ), controls=[qb[6]]), + Command(None, X, qubits=([qb[5]], ), controls=[qb[7]]), + ] + all_cmds = list(itertools.chain(allocate_cmds[5:], gates)) + mapper.receive(all_cmds + [cmd_flush]) + assert mapper.num_mappings == 2 + + +def test_send_possible_commands(simple_graph, simple_mapper): + mapper, backend = simple_mapper + mapper.current_mapping = dict(enumerate(range(len(simple_graph)))) + + neighbours = set() + for node in simple_graph: + for other in simple_graph[node]: + neighbours.add(frozenset((node, other))) + + neighbours = [tuple(s) for s in neighbours] + + for qb0_id, qb1_id in neighbours: + qb0 = WeakQubitRef(engine=None, idx=qb0_id) + qb1 = WeakQubitRef(engine=None, idx=qb1_id) + cmd1 = Command(None, X, qubits=([qb0], ), controls=[qb1]) + cmd2 = Command(None, X, qubits=([qb1], ), controls=[qb0]) + mapper.qubit_manager.add_command(cmd1) + mapper.qubit_manager.add_command(cmd2) + mapper._send_possible_commands() + assert mapper.qubit_manager.size() == 0 + + for qb0_id, qb1_id in itertools.permutations(range(7), 2): + if ((qb0_id, qb1_id) not in neighbours + and (qb1_id, qb0_id) not in neighbours): + qb0 = WeakQubitRef(engine=None, idx=qb0_id) + qb1 = WeakQubitRef(engine=None, idx=qb1_id) + cmd = Command(None, X, qubits=([qb0], ), controls=[qb1]) + mapper.qubit_manager.clear() + mapper.qubit_manager.add_command(cmd) + mapper._send_possible_commands() + assert mapper.qubit_manager.size() == 1 + + +def test_send_possible_commands_deallocate(simple_mapper): + mapper, backend = simple_mapper + + qb0 = WeakQubitRef(engine=None, idx=0) + cmd0 = Command(engine=None, + gate=Deallocate, + qubits=([qb0], ), + controls=[], + tags=[]) + mapper.qubit_manager.add_command(cmd0) + mapper.current_mapping = dict() + mapper._currently_allocated_ids = set([10]) + # not yet allocated: + mapper._send_possible_commands() + assert len(backend.received_commands) == 0 + assert mapper.qubit_manager.size() == 1 + # allocated: + mapper.current_mapping = {0: 3} + mapper._currently_allocated_ids.add(0) + mapper._send_possible_commands() + assert len(backend.received_commands) == 1 + assert mapper.qubit_manager.size() == 0 + assert mapper.current_mapping == dict() + assert mapper._currently_allocated_ids == set([10]) + + +def test_send_possible_commands_no_initial_mapping(simple_mapper): + mapper, backend = simple_mapper + + assert mapper._current_mapping == {} + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=-1) + + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], ), controls=[]) + cmd1 = Command(engine=None, gate=Allocate, qubits=([qb1], ), controls=[]) + cmd2 = Command(None, X, qubits=([qb0], ), controls=[qb1]) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb2], )) + all_cmds = [cmd0, cmd1, cmd2, cmd_flush] + mapper.receive(all_cmds) + + assert mapper._current_mapping + assert mapper.qubit_manager.size() == 0 + + +def test_send_possible_commands_one_inactive_qubit(simple_mapper): + mapper, backend = simple_mapper + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + cmd0 = Command(engine=None, + gate=Allocate, + qubits=([qb0], ), + controls=[], + tags=[]) + cmd1 = Command(engine=None, gate=X, qubits=([qb0], ), controls=[qb1]) + mapper.qubit_manager.add_command(cmd0) + mapper.qubit_manager.add_command(cmd1) + mapper.current_mapping = {0: 0} + mapper._send_possible_commands() + mapper.qubit_manager._get_node_list()[0].cmd == cmd1 + + +def test_run_and_receive(simple_graph, simple_mapper): + mapper, backend = simple_mapper + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + gates = [ + Command(None, X, qubits=([qb[0]], ), controls=[qb[1]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[2]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[5]]), + Command(None, X, qubits=([qb[2]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[5]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[3]], ), controls=[qb[4]]), + Command(None, X, qubits=([qb[3]], ), controls=[qb[6]]), + Command(None, X, qubits=([qb[4]], ), controls=[qb[6]]), + ] + deallocate_cmds = [ + Command(engine=None, gate=Deallocate, qubits=([qb[1]], )) + ] + + allocated_qubits_ref = set([0, 2, 3, 4, 5, 6]) + + all_cmds = list(itertools.chain(allocate_cmds, gates, deallocate_cmds)) + mapper.receive(all_cmds) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + mapper.receive([cmd_flush]) + assert mapper.qubit_manager.size() == 0 + assert len(backend.received_commands) == len(all_cmds) + 1 + assert mapper._currently_allocated_ids == allocated_qubits_ref + + mapping = dict(enumerate(range(len(simple_graph)))) + del mapping[1] + assert mapper.current_mapping == mapping + + cmd9 = Command(None, X, qubits=([qb[0]], ), controls=[qb[6]]) + mapper.receive([cmd9, cmd_flush]) + assert mapper._currently_allocated_ids == allocated_qubits_ref + for idx in allocated_qubits_ref: + assert idx in mapper.current_mapping + assert mapper.qubit_manager.size() == 0 + assert len(mapper.current_mapping) == 6 + assert mapper.num_mappings == 1 + + +@pytest.mark.parametrize("opts", [{}, { + 'swap_opts': { + } +}, { + 'swap_opts': { + 'W': 0.5, + } +}, { + 'swap_opts': { + 'W': 0.5, + 'near_term_layer_depth': 2 + } +}]) +def test_run_and_receive_with_opts(simple_graph, opts): + mapper = graphm.GraphMapper(graph=simple_graph, + add_qubits_to_mapping="fcfs", + opts=opts) + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = backend + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + gates = [ + Command(None, X, qubits=([qb[0]], ), controls=[qb[1]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[2]]), + Command(None, X, qubits=([qb[1]], ), controls=[qb[5]]), + Command(None, X, qubits=([qb[2]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[5]], ), controls=[qb[3]]), + Command(None, X, qubits=([qb[3]], ), controls=[qb[4]]), + Command(None, X, qubits=([qb[3]], ), controls=[qb[6]]), + Command(None, X, qubits=([qb[4]], ), controls=[qb[6]]), + ] + deallocate_cmds = [ + Command(engine=None, gate=Deallocate, qubits=([qb[1]], )) + ] + + allocated_qubits_ref = set([0, 2, 3, 4, 5, 6]) + + all_cmds = list(itertools.chain(allocate_cmds, gates, deallocate_cmds)) + mapper.receive(all_cmds) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + mapper.receive([cmd_flush]) + assert mapper.qubit_manager.size() == 0 + assert len(backend.received_commands) == len(all_cmds) + 1 + assert mapper._currently_allocated_ids == allocated_qubits_ref + + mapping = dict(enumerate(range(len(simple_graph)))) + del mapping[1] + assert mapper.current_mapping == mapping + + cmd9 = Command(None, X, qubits=([qb[0]], ), controls=[qb[6]]) + mapper.receive([cmd9, cmd_flush]) + assert mapper._currently_allocated_ids == allocated_qubits_ref + for idx in allocated_qubits_ref: + assert idx in mapper.current_mapping + assert mapper.qubit_manager.size() == 0 + assert len(mapper.current_mapping) == 6 + assert mapper.num_mappings == 1 + + +def test_send_two_qubit_gate_before_swap(simple_mapper): + qb, all_cmds = allocate_all_qubits_cmd(simple_mapper[0]) + + all_cmds.insert(3, None) + all_cmds.insert(5, Command(None, X, qubits=([qb[2]], ), controls=[qb[3]])) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + all_cmds.append( + Command(engine=None, gate=FlushGate(), qubits=([qb_flush], ))) + + for cmd in [ + Command(None, X, qubits=([qb[0]], ), controls=[qb[2]]), + Command(None, X, qubits=([qb[2]], ), controls=[qb[0]]) + ]: + mapper, backend = deepcopy(simple_mapper) + + all_cmds[3] = cmd + + mapper.qubit_manager.clear() + mapper.receive(all_cmds) + mapper._run() + assert mapper.num_mappings == 1 + + if mapper.current_mapping[2] == 2: + # qb[2] has not moved, all_cmds[5] and everything + # thereafter is possible + assert mapper.qubit_manager.size() == 0 + assert mapper.current_mapping == { + 0: 1, + 1: 0, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6 + } + else: + # qb[2] moved, all_cmds[5] not possible + assert backend._stored_commands == [all_cmds[5]] + all_cmds[-4:] + assert mapper.current_mapping == { + 0: 0, + 1: 2, + 2: 1, + 3: 3, + } + + +def test_send_two_qubit_gate_before_swap_nonallocated_qubits(simple_mapper): + qb, allocate_cmds = allocate_all_qubits_cmd(simple_mapper[0]) + + all_cmds = [ + allocate_cmds[0], + allocate_cmds[-1], + None, + Command(None, X, qubits=([qb[6]], ), controls=[qb[4]]), + ] + + idx = all_cmds.index(None) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + all_cmds.append( + Command(engine=None, gate=FlushGate(), qubits=([qb_flush], ))) + + for cmd in [ + Command(None, X, qubits=([qb[0]], ), controls=[qb[6]]), + Command(None, X, qubits=([qb[6]], ), controls=[qb[0]]) + ]: + mapper, backend = deepcopy(simple_mapper) + mapper.current_mapping = dict(enumerate(range(len(qb)))) + + all_cmds[idx] = cmd + + mapper.receive(all_cmds) + mapper._run() + assert mapper.num_mappings == 1 + assert mapper.current_mapping[4] == 4 + assert mapper.current_mapping[5] == 5 + assert mapper.current_mapping[6] in [3, 6] + + if mapper.current_mapping[6] == 3: + # qb[6] is on position 3, all commands are possible + assert mapper.qubit_manager.size() == 0 + assert mapper.current_mapping == {0: 2, 4: 4, 5: 5, 6: 3} + else: + assert mapper.qubit_manager.size() == 0 + assert mapper.current_mapping == {0: 3, 4: 4, 5: 5, 6: 6} + + +def test_allocate_too_many_qubits(simple_mapper): + mapper, backend = simple_mapper + + qb, allocate_cmds = allocate_all_qubits_cmd(mapper) + + qb.append(WeakQubitRef(engine=None, idx=len(qb))) + allocate_cmds.append( + Command(engine=None, gate=Allocate, qubits=([qb[-1]], ))) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + + with pytest.raises(RuntimeError): + mapper.receive(allocate_cmds + [cmd_flush]) + + +def test_send_possible_commands_reallocate_backend_id(grid22_graph_mapper): + mapper, backend = grid22_graph_mapper + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + qb3 = WeakQubitRef(engine=None, idx=3) + qb4 = WeakQubitRef(engine=None, idx=4) + all_cmds = [ + Command(engine=None, gate=Allocate, qubits=([qb0], )), + Command(engine=None, gate=Allocate, qubits=([qb1], )), + Command(engine=None, gate=Allocate, qubits=([qb2], )), + Command(engine=None, gate=Allocate, qubits=([qb3], )), + Command(engine=None, gate=X, qubits=([qb1], )), + Command(engine=None, gate=Deallocate, qubits=([qb1], )), + Command(engine=None, gate=Allocate, qubits=([qb4], )), + Command(engine=None, gate=X, qubits=([qb4], )), + ] + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + mapper.receive(all_cmds + [cmd_flush]) + assert mapper.current_mapping == {0: 0, 2: 2, 3: 3, 4: 1} + assert mapper.qubit_manager.size() == 0 + assert len(backend.received_commands) == 9 + + +def test_correct_stats(simple_mapper): + mapper, backend = simple_mapper + + # Should test stats for twice same mapping but depends on heuristic + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], )) + cmd1 = Command(engine=None, gate=Allocate, qubits=([qb1], )) + cmd2 = Command(engine=None, gate=Allocate, qubits=([qb2], )) + + cmd3 = Command(None, X, qubits=([qb0], ), controls=[qb1]) + cmd4 = Command(None, X, qubits=([qb1], ), controls=[qb2]) + cmd5 = Command(None, X, qubits=([qb0], ), controls=[qb2]) + cmd6 = Command(None, X, qubits=([qb2], ), controls=[qb1]) + cmd7 = Command(None, X, qubits=([qb0], ), controls=[qb1]) + cmd8 = Command(None, X, qubits=([qb1], ), controls=[qb2]) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + mapper.receive( + [cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7, cmd8, cmd_flush]) + assert mapper.num_mappings == 2 + + +def test_send_possible_cmds_before_new_mapping(simple_mapper): + mapper, backend = simple_mapper + + def dont_call_mapping(): + raise Exception + + mapper._find_paths = dont_call_mapping + + mapper.current_mapping = {0: 1} + qb0 = WeakQubitRef(engine=None, idx=0) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], )) + qb2 = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb2], )) + mapper.receive([cmd0, cmd_flush]) + + +def test_logical_id_tags_allocate_and_deallocate(simple_mapper): + mapper, backend = simple_mapper + mapper.current_mapping = {0: 1, 1: 6} + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], )) + cmd1 = Command(engine=None, gate=Allocate, qubits=([qb1], )) + cmd2 = Command(None, X, qubits=([qb0], ), controls=[qb1]) + cmd3 = Command(engine=None, gate=Deallocate, qubits=([qb0], )) + cmd4 = Command(engine=None, gate=Deallocate, qubits=([qb1], )) + + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + mapper.receive([cmd0, cmd1, cmd2, cmd_flush]) + assert backend.received_commands[0].gate == Allocate + assert backend.received_commands[0].qubits[0][0].id == 1 + assert backend.received_commands[0].tags == [LogicalQubitIDTag(0)] + assert backend.received_commands[1].gate == Allocate + assert backend.received_commands[1].qubits[0][0].id == 6 + assert backend.received_commands[1].tags == [LogicalQubitIDTag(1)] + for cmd in backend.received_commands[2:]: + if cmd.gate == Allocate: + assert cmd.tags == [] + elif cmd.gate == Deallocate: + assert cmd.tags == [] + mapped_id_for_0 = mapper.current_mapping[0] + mapped_id_for_1 = mapper.current_mapping[1] + mapper.receive([cmd3, cmd4, cmd_flush]) + assert backend.received_commands[-3].gate == Deallocate + assert backend.received_commands[-3].qubits[0][0].id == mapped_id_for_0 + assert backend.received_commands[-3].tags == [LogicalQubitIDTag(0)] + assert backend.received_commands[-2].gate == Deallocate + assert backend.received_commands[-2].qubits[0][0].id == mapped_id_for_1 + assert backend.received_commands[-2].tags == [LogicalQubitIDTag(1)] + + +def test_check_that_local_optimizer_doesnt_merge(simple_graph): + mapper = graphm.GraphMapper(graph=simple_graph) + optimizer = LocalOptimizer(10) + backend = DummyEngine(save_commands=True) + backend.is_last_engine = True + mapper.next_engine = optimizer + mapper.current_mapping = dict(enumerate(range(len(simple_graph)))) + mapper.current_mapping = {0: 0} + mapper.storage = 1 + optimizer.next_engine = backend + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb_flush = WeakQubitRef(engine=None, idx=-1) + cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush], )) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0], )) + cmd1 = Command(None, X, qubits=([qb0], )) + cmd2 = Command(engine=None, gate=Deallocate, qubits=([qb0], )) + mapper.receive([cmd0, cmd1, cmd2]) + assert mapper.qubit_manager.size() == 0 + mapper.current_mapping = {1: 0} + cmd3 = Command(engine=None, gate=Allocate, qubits=([qb1], )) + cmd4 = Command(None, X, qubits=([qb1], )) + cmd5 = Command(engine=None, gate=Deallocate, qubits=([qb1], )) + mapper.receive([cmd3, cmd4, cmd5, cmd_flush]) + assert len(backend.received_commands) == 7 + + +def test_mapper_to_str(simple_graph): + mapper = graphm.GraphMapper(graph=simple_graph, + add_qubits_to_mapping="fcfs") + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend, [mapper]) + qureg = eng.allocate_qureg(len(simple_graph)) + + eng.flush() + assert mapper.current_mapping == dict(enumerate(range(len(simple_graph)))) + + H | qureg[0] + X | qureg[2] + + CNOT | (qureg[6], qureg[4]) + CNOT | (qureg[6], qureg[0]) + CNOT | (qureg[4], qureg[5]) + + All(Measure) | qureg + eng.flush() + + str_repr = str(mapper) + assert str_repr.count("Number of mappings: 1") == 1 + assert str_repr.count("2: 1") == 1 + assert str_repr.count("3: 1") == 1 + + sent_gates = [cmd.gate for cmd in backend.received_commands] + assert sent_gates.count(H) == 1 + assert sent_gates.count(X) == 4 + assert sent_gates.count(Measure) == 7 diff --git a/pytest.ini b/pytest.ini index fab634b12..46f7c05ac 100755 --- a/pytest.ini +++ b/pytest.ini @@ -4,3 +4,5 @@ testpaths = projectq filterwarnings = error ignore:the matrix subclass is not the recommended way:PendingDeprecationWarning + ignore:invalid escape sequence:DeprecationWarning + ignore:Using or importing the ABCs from 'collections' instead:DeprecationWarning