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