def get_upper_bound_peo(graph, method='tamaki', **kwargs): """ Run one of the heuristics to get PEO and treewidth Parameters: ----------- graph: networkx.Graph graph to calculate PEO method: str, default 'tamaki' solver to use **kwargs: default {} optional keyword arguments to pass to the solver """ builtin_heuristics = {"min_fill", "min_degree", "cardinality"} pace_heuristics = {"tamaki"} if method in pace_heuristics: peo, tw = get_upper_bound_peo_pace2017(graph, method, **kwargs) elif method in builtin_heuristics: peo, tw = get_upper_bound_peo_builtin(graph, method) elif method == "quickbb": peo, tw = get_upper_bound_peo_quickbb(graph, **kwargs) else: raise ValueError(f'Unknown method: {method}') peo_vars = [ Var(var, size=graph.nodes[var]['size'], name=graph.nodes[var]['name']) for var in peo ] return peo_vars, tw
def nodes_to_vars(old_graph, peo): peo_vars = [ Var(v, size=old_graph.nodes[v]['size'], name=old_graph.nodes[v]['name']) for v in peo ] return peo_vars
def get_peo(old_graph, method="tamaki"): """ Calculates a perfect elimination order using one of the external methods. Parameters ---------- graph : networkx.Graph graph to estimate method : str one of {"tamaki"} Returns ------- peo : list list of nodes in perfect elimination order treewidth : int treewidth corresponding to peo """ from qtree.graph_model.clique_trees import get_peo_from_tree import qtree.graph_model.pace2017_solver_api as api method_args = { 'tamaki': { 'command': './tw-exact', 'cwd': defs.TAMAKI_SOLVER_PATH } } assert (method in method_args.keys()) # ensure graph is labeled starting from 1 with integers graph, inv_dict = relabel_graph_nodes( old_graph, dict(zip(old_graph.nodes, range(1, old_graph.number_of_nodes() + 1)))) # Remove selfloops and parallel edges. Critical graph = get_simple_graph(graph) data = generate_gr_file(graph) out_data = api.run_exact_solver(data, **method_args[method]) tree, treewidth = read_td_file(out_data, as_data=True) peo = get_peo_from_tree(tree) peo = [inv_dict[pp] for pp in peo] try: peo_vars = [ Var(var, size=old_graph.nodes[var]['size'], name=old_graph.nodes[var]['name']) for var in peo ] except: peo_vars = peo return peo_vars, treewidth
def split_graph_random(old_graph, n_var_parallel=0): """ Splits a graphical model with randomly chosen nodes to parallelize over. Parameters ---------- old_graph : networkx.Graph graph to contract (after eliminating variables which are parallelized over) n_var_parallel : int number of variables to eliminate by parallelization Returns ------- idx_parallel : list of Idx variables removed by parallelization graph : networkx.Graph new graph without parallelized variables """ graph = copy.deepcopy(old_graph) indices = [var for var in graph.nodes(data=False)] idx_parallel = np.random.choice( indices, size=n_var_parallel, replace=False) idx_parallel_var = [Var(var, size=graph.nodes[var]) for var in idx_parallel] for idx in idx_parallel: remove_node(graph, idx) log.info("Removed indices by parallelization:\n{}".format(idx_parallel)) log.info("Removed {} variables".format(len(idx_parallel))) peo, treewidth = get_peo(graph) return sorted(idx_parallel_var, key=int), graph
def maximum_cardinality_search(old_graph, last_clique_vertices=[]): """ This function builds elimination order of a chordal graph using maximum cardinality search algorithm. If last_clique_vertices is provided the algorithm will place these indices at the end of the elimination list in the same order as provided. Parameters ---------- graph : nx.Graph or nx.MultiGraph chordal graph to build the elimination order last_clique_vertices : list, default [] list of vertices to be placed at the end of the elimination order Returns ------- list Perfect elimination order """ # convert input to int last_clique_vertices = [int(var) for var in last_clique_vertices] # Check is last_clique_vertices is a clique graph = copy.deepcopy(old_graph) n_nodes = graph.number_of_nodes() nodes_number_of_ord_neighbors = {node: 0 for node in graph.nodes} # range(0, n_nodes + 1) is important here as we need n+1 lists # to ensure proper indexing in the case of a clique nodes_by_ordered_neighbors = [[] for ii in range(0, n_nodes + 1)] for node in graph.nodes: nodes_by_ordered_neighbors[0].append(node) last_nonempty = 0 peo = [] for ii in range(n_nodes, 0, -1): # Take any unordered node with highest cardinality # or the ones in the last_clique_vertices if it was provided if len(last_clique_vertices) > 0: # Forcibly select the node from the clique node = last_clique_vertices.pop() # The following should always be possible if # last_clique_vertices induces a clique and I understood # the theorem correctly. If it raises something is wrong # with the algorithm/input is not a clique try: nodes_by_ordered_neighbors[last_nonempty].remove(node) except ValueError: if not is_clique(graph, last_clique_vertices): raise ValueError('last_clique_vertices are not a clique') else: raise AssertionError('Algorithmic error. Investigate') else: node = nodes_by_ordered_neighbors[last_nonempty].pop() peo = [node] + peo nodes_number_of_ord_neighbors[node] = -1 unordered_neighbors = [(neighbor, nodes_number_of_ord_neighbors[neighbor]) for neighbor in graph[node] if nodes_number_of_ord_neighbors[neighbor] >= 0] # Increase number of ordered neighbors for all adjacent # unordered nodes for neighbor, n_ordered_neighbors in unordered_neighbors: nodes_by_ordered_neighbors[n_ordered_neighbors].remove(neighbor) nodes_number_of_ord_neighbors[neighbor] = (n_ordered_neighbors + 1) nodes_by_ordered_neighbors[n_ordered_neighbors + 1].append(neighbor) last_nonempty += 1 while last_nonempty >= 0: if len(nodes_by_ordered_neighbors[last_nonempty]) == 0: last_nonempty -= 1 else: break # Create Var objects peo_vars = [ Var(var, size=graph.nodes[var]['size'], name=graph.nodes[var]['name']) for var in peo ] return peo_vars
def circ2graph(qubit_count, circuit, pdict={}, max_depth=None, omit_terminals=True): """ Constructs a graph from a circuit in the form of a list of lists. Parameters ---------- qubit_count : int number of qubits in the circuit circuit : list of lists quantum circuit as returned by :py:meth:`operators.read_circuit_file` pdict : dict Dictionary with placeholders if any parameteric gates were unresolved max_depth : int, default None Maximal depth of the circuit which should be used omit_terminals : bool, default True If terminal nodes should be excluded from the final graph. Returns ------- graph : networkx.MultiGraph Graph which corresponds to the circuit data_dict : dict Dictionary with all tensor data """ import functools import qtree.operators as ops if max_depth is None: max_depth = len(circuit) data_dict = {} # Let's build the graph. # The circuit is built from left to right, as it operates # on the ket ( |0> ) from the left. We thus first place # the bra ( <x| ) and then put gates in the reverse order # Fill the variable `frame` layer_variables = list(range(qubit_count)) current_var_idx = qubit_count # Initialize the graph graph = nx.MultiGraph() # Populate nodes and save variables of the bra bra_variables = [] for var in layer_variables: graph.add_node(var, name=f'o_{var}', size=2) bra_variables.append(Var(var, name=f"o_{var}")) # Place safeguard measurement circuits before and after # the circuit measurement_circ = [[ops.M(qubit) for qubit in range(qubit_count)]] combined_circ = functools.reduce( lambda x, y: itertools.chain(x, y), [measurement_circ, reversed(circuit[:max_depth])]) # Start building the graph in reverse order for layer in combined_circ: for op in layer: # build the indices of the gate. If gate # changes the basis of a qubit, a new variable # has to be introduced and current_var_idx is increased. # The order of indices # is always (a_new, a, b_new, b, ...), as # this is how gate tensors are chosen to be stored variables = [] current_var_idx_copy = current_var_idx for qubit in op.qubits: if qubit in op.changed_qubits: variables.extend( [layer_variables[qubit], current_var_idx_copy]) graph.add_node(current_var_idx_copy, name='v_{}'.format(current_var_idx_copy), size=2) current_var_idx_copy += 1 else: variables.extend([layer_variables[qubit]]) # Form a tensor and add a clique to the graph # fill placeholders in gate's parameters if any for par, value in op.parameters.items(): if isinstance(value, ops.placeholder): op._parameters[par] = pdict[value] data_key = hash((op.name, tuple(op.parameters.items()))) tensor = { 'name': op.name, 'indices': tuple(variables), 'data_key': data_key } # Insert tensor data into data dict data_dict[data_key] = op.gen_tensor() if len(variables) > 1: edges = itertools.combinations(variables, 2) else: edges = [(variables[0], variables[0])] graph.add_edges_from(edges, tensor=tensor) # Update current variable frame for qubit in op.changed_qubits: layer_variables[qubit] = current_var_idx current_var_idx += 1 # Finally go over the qubits, append measurement gates # and collect ket variables ket_variables = [] op = ops.M(0) # create a single measurement gate object for qubit in range(qubit_count): var = layer_variables[qubit] new_var = current_var_idx ket_variables.append(Var(new_var, name=f'i_{qubit}', size=2)) # update graph and variable `frame` graph.add_node(new_var, name=f'i_{qubit}', size=2) data_key = hash((op.name, tuple(op.parameters.items()))) tensor = { 'name': op.name, 'indices': (var, new_var), 'data_key': data_key } graph.add_edge(var, new_var, tensor=tensor) layer_variables[qubit] = new_var current_var_idx += 1 if omit_terminals: graph.remove_nodes_from(tuple(map(int, bra_variables + ket_variables))) v = graph.number_of_nodes() e = graph.number_of_edges() log.info(f"Generated graph with {v} nodes and {e} edges") # log.info(f"last index contains from {layer_variables}") return graph, data_dict, bra_variables, ket_variables
def split_graph_by_metric_greedy( old_graph, n_var_parallel=0, metric_fn=get_node_by_treewidth_reduction, greedy_step_by=1, forbidden_nodes=(), peo_function=get_peo): """ This function splits graph by greedily selecting next nodes up to the n_var_parallel using the metric function and recomputing PEO after each node elimination Parameters ---------- old_graph : networkx.Graph or networkx.MultiGraph graph to split by parallelizing over variables and to contract Parallel edges and self-loops in the graph are removed (if any) before the calculation of metric n_var_parallel : int number of variables to eliminate by parallelization metric_fn : function, optional function to evaluate node metric. Default get_node_by_mem_reduction greedy_step_by : int, default 1 Step size for the greedy algorithm forbidden_nodes : iterable, optional nodes in this list will not be considered for deletion. Default (). peo_function: function function to calculate PEO. Should have signature lambda (graph): return peo, treewidth Returns ------- idx_parallel : list variables removed by parallelization graph : networkx.Graph new graph without parallelized variables """ # import pdb # pdb.set_trace() # convert everything to int forbidden_nodes = [int(var) for var in forbidden_nodes] # Simplify graph graph = get_simple_graph(old_graph) idx_parallel = [] idx_parallel_var = [] steps = [greedy_step_by] * (n_var_parallel // greedy_step_by) # append last batch to steps steps.append(n_var_parallel - (n_var_parallel // greedy_step_by) * greedy_step_by) for n_parallel in steps: # Get optimal order - recalculate treewidth peo, tw = peo_function(graph) graph_optimal, inverse_order = relabel_graph_nodes( graph, dict(zip(peo, sorted(graph.nodes)))) # get nodes by metric in descending order nodes_by_metric_optimal = metric_fn(graph_optimal) nodes_by_metric_optimal.sort( key=lambda pair: pair[1], reverse=True) # filter out forbidden nodes and get nodes in original order nodes_by_metric_allowed = [] for node, metric in nodes_by_metric_optimal: if inverse_order[node] not in forbidden_nodes: nodes_by_metric_allowed.append( (inverse_order[node], metric)) # Take first nodes by cost and map them back to original # order nodes_with_cost = nodes_by_metric_allowed[:n_parallel] if len(nodes_with_cost) > 0: nodes, costs = zip(*nodes_with_cost) else: nodes = [] # Update list and update graph idx_parallel += nodes # create var objects from nodes idx_parallel_var += [Var(var, size=graph.nodes[var]['size']) for var in nodes] for node in nodes: remove_node(graph, node) return idx_parallel_var, graph
def split_graph_with_mem_constraint_greedy( old_graph, n_var_parallel_min=0, mem_constraint=defs.MAXIMAL_MEMORY, step_by=5, n_var_parallel_max=None, metric_fn=get_node_by_mem_reduction, forbidden_nodes=(), peo_function=get_peo): """ This function splits graph by greedily selecting next nodes up to the n_var_parallel using the metric function and recomputing PEO after each node elimination. The graph is **ASSUMED** to be in the perfect elimination order Parameters ---------- old_graph : networkx.Graph() initial contraction graph n_var_parallel_min : int minimal number of variables to split the task to mem_constraint : int Upper limit on memory per task metric_function : function, optional function to rank nodes for elimination step_by : int, optional scan the metric function with this step n_var_parallel_max : int, optional constraint on the maximal number of parallelized variables. Default None forbidden_nodes: iterable, default () nodes forbidden for parallelization peo_function: function function to calculate PEO. Should have signature lambda (graph): return peo, treewidth Returns ------- idx_parallel : list list of removed variables graph : networkx.Graph reduced contraction graph """ # convert everything to int forbidden_nodes = [int(var) for var in forbidden_nodes] graph = copy.deepcopy(old_graph) n_var_total = old_graph.number_of_nodes() if n_var_parallel_max is None: n_var_parallel_max = n_var_total mem_cost, flop_cost = get_contraction_costs(graph) max_mem = sum(mem_cost) idx_parallel = [] idx_parallel_var = [] steps = list(range(0, n_var_parallel_max, step_by)) if len(steps) == 0 or (n_var_parallel_max % step_by != 0): steps.append(n_var_parallel_max) steps = [step_by] * (n_var_parallel_max // step_by) # append last batch to steps steps.append(n_var_parallel_max - (n_var_parallel_max // step_by) * step_by) for n_parallel in steps: # Get optimal order peo, tw = peo_function(graph) graph_optimal, inverse_order = relabel_graph_nodes( graph, dict(zip(peo, range(len(peo))))) # get nodes by metric in descending order nodes_by_metric_optimal = metric_fn(graph_optimal) nodes_by_metric_optimal.sort( key=lambda pair: pair[1], reverse=True) nodes_by_metric_allowed = [] for node, metric in nodes_by_metric_optimal: if inverse_order[node] not in forbidden_nodes: nodes_by_metric_allowed.append( (inverse_order[node], metric)) # Take first nodes by cost and map them back to original # order nodes_with_cost = nodes_by_metric_allowed[:n_parallel] if len(nodes_with_cost) > 0: nodes, costs = zip(*nodes_with_cost) else: nodes = [] # Update list and update graph idx_parallel += nodes # create var objects from nodes idx_parallel_var += [Var(var, size=graph.nodes[var]['size']) for var in nodes] for node in nodes: remove_node(graph, node) # Renumerate graph nodes to be consequtive ints (may be redundant) label_dict = dict(zip(sorted(graph.nodes), range(len(graph.nodes())))) graph_relabelled, _ = relabel_graph_nodes(graph, label_dict) mem_cost, flop_cost = get_contraction_costs(graph_relabelled) max_mem = sum(mem_cost) if (max_mem <= mem_constraint and len(idx_parallel) >= n_var_parallel_min): break if max_mem > mem_constraint: raise ValueError('Maximal memory constraint is not met') return idx_parallel_var, graph
def split_graph_by_metric( old_graph, n_var_parallel=0, metric_fn=get_node_by_degree, forbidden_nodes=()): """ Parallel-splitted version of :py:meth:`get_peo` with nodes to split chosen according to the metric function. Metric function should take a graph and return a list of pairs (node : metric_value) Parameters ---------- old_graph : networkx.Graph or networkx.MultiGraph graph to split by parallelizing over variables and to contract Parallel edges and self-loops in the graph are removed (if any) before the calculation of metric n_var_parallel : int number of variables to eliminate by parallelization metric_fn : function, optional function to evaluate node metric. Default get_node_by_degree forbidden_nodes : iterable, optional nodes in this list will not be considered for deletion. Default (). Returns ------- idx_parallel : list variables removed by parallelization graph : networkx.Graph new graph without parallelized variables """ # graph = get_simple_graph(old_graph) # import pdb # pdb.set_trace() graph = copy.deepcopy(old_graph) # convert everything to int forbidden_nodes = [int(var) for var in forbidden_nodes] # get nodes by metric in descending order nodes_by_metric = metric_fn(graph) nodes_by_metric.sort(key=lambda pair: int(pair[1]), reverse=True) nodes_by_metric_allowed = [] for node, metric in nodes_by_metric: if node not in forbidden_nodes: nodes_by_metric_allowed.append((node, metric)) idx_parallel = [] for ii in range(n_var_parallel): node, metric = nodes_by_metric_allowed[ii] idx_parallel.append(node) # create var objects from nodes idx_parallel_var = [Var(var, size=graph.nodes[var]['size']) for var in idx_parallel] for idx in idx_parallel: remove_node(graph, idx) log.info("Removed indices by parallelization:\n{}".format(idx_parallel)) log.info("Removed {} variables".format(len(idx_parallel))) return idx_parallel_var, graph