def test_tree_from_peo(filename='inst_2x2_7_0.txt'): import qtree.operators as ops nq, c = ops.read_circuit_file(filename) graph, *_ = circ2graph(nq, c, omit_terminals=False) peo, _ = get_peo(graph) tree = get_tree_from_peo(get_simple_graph(graph), peo) elements = list(range(1, graph.number_of_nodes() + 1)) for element in elements: nodes_containing_element = [] for node in tree.nodes(): if element in node: nodes_containing_element.append(node) subtree = nx.subgraph(tree, nodes_containing_element) if subtree.number_of_nodes() > 0: connected = nx.connected.is_connected(subtree) else: connected = True # Take empty graph as connected if not connected: # draw_graph(subtree, f"st_{element}") pass draw_graph(tree, f"tree")
def get_upper_bound_peo_pace2017(old_graph, method="tamaki", wait_time=60, print_stats=False): """ Calculates a PEO and treewidth using one of the external solvers Parameters ---------- graph : networkx.Graph graph to estimate method : str one of {"tamaki"} wait_time : float allowed running time (in seconds) Returns ------- peo : list treewidth : int treewidth """ 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-heuristic', 'cwd': defs.TAMAKI_SOLVER_PATH, 'wait_time': wait_time } } assert (method in method_args.keys()) # ensure graph is labelad 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_heuristic_solver(data, **method_args[method]) try: stats = get_stats_from_td_file(out_data) if print_stats: print('stats', stats) tree, treewidth = read_td_file(out_data, as_data=True) except ValueError: print(out_data) raise peo = get_peo_from_tree(tree) # return to the original labelling peo = [inv_dict[pp] for pp in peo] return peo, treewidth
def get_upper_bound_peo_builtin(old_graph, method="min_fill"): """ Calculates an upper bound on treewidth using one of the heuristics. Best among implemented here is min-fill, as described in V. Gogate and R. Dechter :url:`http://arxiv.org/abs/1207.4109` Parameters ---------- graph : networkx.Graph graph to estimate method : str one of {"min_fill", "min_degree", "cardinality"} Returns ------- peo : list list of nodes in perfect elimination order treewidth : int treewidth corresponding to peo """ methods = { "min_fill": get_node_min_fill_heuristic, "min_degree": get_node_min_degree_heuristic, "cardinality": get_node_max_cardinality_heuristic } assert method in methods.keys() node_heuristic_fn = methods[method] # copy graph as we will destroy it here # and relabel to consequtive ints 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) node, max_degree = node_heuristic_fn(graph) peo = [node] eliminate_node(graph, node, self_loops=False) for ii in range(graph.number_of_nodes()): node, degree = node_heuristic_fn(graph) peo.append(node) max_degree = max(max_degree, degree) eliminate_node(graph, node, self_loops=False) # relabel peo back peo = [inv_dict[pp] for pp in peo] return peo, max_degree # this is clique size - 1
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 get_equivalent_peo(old_graph, peo, clique_vertices): """ This function returns an equivalent peo with the clique_indices in the rest of the new order """ # Ensure that the graph is simple graph = get_simple_graph(old_graph) # Complete the graph graph_chordal = get_fillin_graph2(graph, peo) # MCS will produce alternative PEO with this clique at the end new_peo = maximum_cardinality_search(graph_chordal, list(clique_vertices)) return new_peo
def get_treewidth_from_peo(old_graph, peo): """ This function checks the treewidth of a given peo. The graph is simplified: all selfloops and parallel edges are removed. Parameters ---------- old_graph : networkx.Graph or networkx.MultiGraph graph to use peo : list list of nodes in the perfect elimination order Returns ------- treewidth : int treewidth corresponding to peo """ # Ensure PEO is a list of ints peo = list(map(int, peo)) # Copy graph and make it simple graph = get_simple_graph(old_graph) treewidth = 0 for node in peo: # Get the size of the next clique - 1 neighbors = list(graph[node]) n_neighbors = len(neighbors) if len(neighbors) > 1: edges = itertools.combinations(neighbors, 2) else: edges = None # Treewidth is the size of the maximal clique - 1 treewidth = max(n_neighbors, treewidth) graph.remove_node(node) # Make the next clique if edges is not None: graph.add_edges_from(edges) return treewidth
def get_upper_bound_peo_quickbb(old_graph, wait_time=60, quickbb_extra_args=" --min-fill-ordering ", input_suffix=None, keep_input=False): """ Calculates the elimination order for an undirected graphical model of the circuit. Parameters ---------- graph : networkx.Graph graph of the undirected graphical model to decompose wait_time : int, default 60 waiting time in seconds quickbb_extra_args : str, default '--min-fill-ordering --time 60' Optional commands to QuickBB. input_suffix : str, default None Optional suffix to allow parallel execution. If None is provided a random suffix is generated keep_input : bool, default False Whether to keep input files for debugging Returns ------- peo : list containing indices in optimal order of elimination treewidth : int treewidth of the decomposition """ import qtree.graph_model.quickbb_api as api # save initial indices to ensure nothing is missed initial_indices = old_graph.nodes() # Remove selfloops and parallel edges. Critical graph = get_simple_graph(old_graph) # Relabel graph nodes to consequtive ints graph, initial_to_conseq = relabel_graph_nodes( graph, dict(zip(graph.nodes, range(1, graph.number_of_nodes() + 1)))) # prepare environment if input_suffix is None: input_suffix = ''.join(str(random.randint(0, 9)) for n in range(8)) cnffile_abs_path = os.path.join(defs.QTREE_PATH, '..', 'output', 'quickbb.' + input_suffix + '.cnf') cnffile_dirname = os.path.dirname(cnffile_abs_path) cnffile = os.path.basename(cnffile_abs_path) if graph.number_of_edges() > 0: generate_cnf_file(graph, cnffile_abs_path) quickbb_rel_path = os.path.relpath(defs.QUICKBB_COMMAND, cnffile_dirname) out_bytes = api.run_quickbb(cnffile, wait_time=wait_time, command=quickbb_rel_path, cwd=cnffile_dirname) # Extract order m = re.search(b'(?P<peo>(\d+ )+).*Treewidth=(?P<treewidth>\s\d+)', out_bytes, flags=re.MULTILINE | re.DOTALL) peo = [int(ii) for ii in m['peo'].split()] # Map peo back to original indices. PEO in QuickBB is 1-based # but we need it 0-based peo = [initial_to_conseq[pp] for pp in peo] treewidth = int(m['treewidth']) else: peo = [] treewidth = 0 # find the rest of indices which quickBB did not spit out. # Those include isolated nodes (don't affect # scaling and may be added to the end of the variables list) # and something else isolated_nodes = nx.isolates(old_graph) peo = peo + sorted(isolated_nodes, key=int) # assert(set(initial_indices) - set(peo) == set()) missing_indices = set(initial_indices) - set(peo) # The next line needs review. Why quickBB misses some indices? # It is here to make program work, but is it an optimal order? peo = peo + sorted(list(missing_indices), key=int) # Ensure no indices were missed assert (sorted(peo, key=int) == sorted(initial_indices, key=int)) # log.info('Final peo from quickBB:\n{}'.format(peo)) # remove input file to honor EPA if not keep_input: try: os.remove(cnffile_abs_path) except FileNotFoundError: pass # transform PEO to a list of Var objects as expected by # other parts of code return peo, treewidth
def get_upper_bound_peo_pace2017_interactive(old_graph, method="tamaki", max_time=60, max_width=None, print_stats=False): """ Calculates a PEO and treewidth using one of the external solvers Parameters ---------- graph : networkx.Graph graph to estimate method : str one of {"tamaki", "tamaki_exact"} max_time : float Run until not reached time max_width : int Run until not reached width Returns ------- peo : list treewidth : int treewidth """ from qtree.graph_model.clique_trees import get_peo_from_tree import qtree.graph_model.pace2017_solver_api as api # ensure graph is labelad 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) start = time.time() def callback(line_info): ts, width = line_info print(f'Time={ts}, width={width}', file=sys.stderr) elapsed = time.time() - start if max_time: if elapsed > max_time: raise StopIteration('Timeout') if max_width and width: if width <= max_width: raise StopIteration('Solution is good enough') method_args = { 'tamaki': { 'command': './tw-heuristic', 'cwd': defs.TAMAKI_SOLVER_PATH, 'callback': callback, }, 'tamaki_exact': { 'command': './tw-exact', 'cwd': defs.TAMAKI_SOLVER_PATH, 'callback': lambda x: print(f'Time={time.time()-start}', file=sys.stderr), 'callback_delay': 2 } } assert (method in method_args.keys()) out_data = api.run_heuristic_solver_interactive(data, **method_args[method]) try: stats = get_stats_from_td_file(out_data) if print_stats: print('stats', stats) tree, treewidth = read_td_file(out_data, as_data=True) except ValueError: print(out_data) raise peo = get_peo_from_tree(tree) # return to the original labelling peo = [inv_dict[pp] for pp in peo] return peo, treewidth
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