Beispiel #1
0
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")
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
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