Source code for utool.util_graph

# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

try:
    import numpy as np
except ImportError:
    pass
try:
    import networkx as nx
except ImportError:
    pass
import collections
import functools
from utool import util_inject
from utool import util_const
from six.moves import reduce, zip, range
import itertools as it

(print, rrr, profile) = util_inject.inject2(__name__)


[docs]def nx_topsort_nodes(graph, nodes): import utool as ut node_rank = ut.nx_topsort_rank(graph, nodes) node_idx = ut.rebase_labels(node_rank) sorted_nodes = ut.take(nodes, node_idx) return sorted_nodes
[docs]def nx_topsort_rank(graph, nodes=None): """ graph = inputs.exi_graph.reverse() nodes = flat_node_order_ """ import utool as ut if False: # Determenistic version # Ok, this doesn't work. dag_ranks = nx_dag_node_rank(graph, nodes) toprank = ut.argsort(dag_ranks, list(map(str, nodes))) else: # Non-determenistic version dag_ranks = nx_dag_node_rank(graph, nodes) topsort = list(nx.topological_sort(graph)) # print('topsort = %r' % (topsort,)) node_to_top_rank = ut.make_index_lookup(topsort) toprank = ut.dict_take(node_to_top_rank, nodes) return toprank
[docs]def nx_common_descendants(graph, node1, node2): descendants1 = nx.descendants(graph, node1) descendants2 = nx.descendants(graph, node2) common_descendants = set.intersection(descendants1, descendants2) return common_descendants
[docs]def nx_common_ancestors(graph, node1, node2): ancestors1 = nx.ancestors(graph, node1) ancestors2 = nx.ancestors(graph, node2) common_ancestors = set.intersection(ancestors1, ancestors2) return common_ancestors
[docs]def nx_make_adj_matrix(G): import utool as ut nodes = list(G.nodes()) node2_idx = ut.make_index_lookup(nodes) edges = list(G.edges()) edge2_idx = ut.partial(ut.dict_take, node2_idx) uv_list = ut.lmap(edge2_idx, edges) A = np.zeros((len(nodes), len(nodes))) A[tuple(np.array(uv_list).T)] = 1 return A
[docs]def nx_transitive_reduction(G, mode=1): """ References: https://en.wikipedia.org/wiki/Transitive_reduction#Computing_the_reduction_using_the_closure http://dept-info.labri.fr/~thibault/tmp/0201008.pdf http://stackoverflow.com/questions/17078696/transitive-reduction-of-directed-graph-in-python CommandLine: python -m utool.util_graph nx_transitive_reduction --show Example: >>> # DISABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.DiGraph([('a', 'b'), ('a', 'c'), ('a', 'e'), >>> ('a', 'd'), ('b', 'd'), ('c', 'e'), >>> ('d', 'e'), ('c', 'e'), ('c', 'd')]) >>> G = testdata_graph()[1] >>> G_tr = nx_transitive_reduction(G, mode=1) >>> G_tr2 = nx_transitive_reduction(G, mode=1) >>> ut.quit_if_noshow() >>> try: >>> import wbia.plottool as pt >>> except ImportError: >>> import wbia.plottool as pt >>> G_ = nx.dag.transitive_closure(G) >>> pt.show_nx(G , pnum=(1, 5, 1), fnum=1) >>> pt.show_nx(G_tr , pnum=(1, 5, 2), fnum=1) >>> pt.show_nx(G_tr2 , pnum=(1, 5, 3), fnum=1) >>> pt.show_nx(G_ , pnum=(1, 5, 4), fnum=1) >>> pt.show_nx(nx.dag.transitive_closure(G_tr), pnum=(1, 5, 5), fnum=1) >>> ut.show_if_requested() """ import utool as ut has_cycles = not nx.is_directed_acyclic_graph(G) if has_cycles: # FIXME: this does not work for cycle graphs. # Need to do algorithm on SCCs G_orig = G G = nx.condensation(G_orig) nodes = list(G.nodes()) node2_idx = ut.make_index_lookup(nodes) # For each node u, perform DFS consider its set of (non-self) children C. # For each descendant v, of a node in C, remove any edge from u to v. if mode == 1: G_tr = G.copy() for parent in G_tr.nodes(): # Remove self loops if G_tr.has_edge(parent, parent): G_tr.remove_edge(parent, parent) # For each child of the parent for child in list(G_tr.successors(parent)): # Preorder nodes includes its argument (no added complexity) for gchild in list(G_tr.successors(child)): # Remove all edges from parent to non-child descendants for descendant in nx.dfs_preorder_nodes(G_tr, gchild): if G_tr.has_edge(parent, descendant): G_tr.remove_edge(parent, descendant) if has_cycles: # Uncondense graph uncondensed_G_tr = G.__class__() mapping = G.graph['mapping'] uncondensed_G_tr.add_nodes_from(mapping.keys()) inv_mapping = ut.invert_dict(mapping, unique_vals=False) for u, v in G_tr.edges(): u_ = inv_mapping[u][0] v_ = inv_mapping[v][0] uncondensed_G_tr.add_edge(u_, v_) for key, path in inv_mapping.items(): if len(path) > 1: directed_cycle = list(ut.itertwo(path, wrap=True)) uncondensed_G_tr.add_edges_from(directed_cycle) G_tr = uncondensed_G_tr else: def make_adj_matrix(G): edges = list(G.edges()) edge2_idx = ut.partial(ut.dict_take, node2_idx) uv_list = ut.lmap(edge2_idx, edges) A = np.zeros((len(nodes), len(nodes))) A[tuple(np.array(uv_list).T)] = 1 return A G_ = nx.dag.transitive_closure(G) A = make_adj_matrix(G) B = make_adj_matrix(G_) # AB = A * B # AB = A.T.dot(B) AB = A.dot(B) # AB = A.dot(B.T) A_and_notAB = np.logical_and(A, np.logical_not(AB)) tr_uvs = np.where(A_and_notAB) # nodes = G.nodes() edges = list(zip(*ut.unflat_take(nodes, tr_uvs))) G_tr = G.__class__() G_tr.add_nodes_from(nodes) G_tr.add_edges_from(edges) if has_cycles: # Uncondense graph uncondensed_G_tr = G.__class__() mapping = G.graph['mapping'] uncondensed_G_tr.add_nodes_from(mapping.keys()) inv_mapping = ut.invert_dict(mapping, unique_vals=False) for u, v in G_tr.edges(): u_ = inv_mapping[u][0] v_ = inv_mapping[v][0] uncondensed_G_tr.add_edge(u_, v_) for key, path in inv_mapping.items(): if len(path) > 1: directed_cycle = list(ut.itertwo(path, wrap=True)) uncondensed_G_tr.add_edges_from(directed_cycle) G_tr = uncondensed_G_tr return G_tr
[docs]def nx_source_nodes(graph): # for node in nx.dag.topological_sort(graph): for node in graph.nodes(): if graph.in_degree(node) == 0: yield node
[docs]def nx_sink_nodes(graph): # for node in nx.dag.topological_sort(graph): for node in graph.nodes(): if graph.out_degree(node) == 0: yield node
# def nx_sink_nodes(graph): # topsort_iter = nx.dag.topological_sort(graph) # sink_iter = (node for node in topsort_iter # if graph.out_degree(node) == 0) # return sink_iter
[docs]def nx_to_adj_dict(graph): import utool as ut adj_dict = ut.ddict(list) for u, edges in graph.adjacency(): adj_dict[u].extend(list(edges.keys())) adj_dict = dict(adj_dict) return adj_dict
[docs]def nx_from_adj_dict(adj_dict, cls=None): if cls is None: cls = nx.DiGraph nodes = list(adj_dict.keys()) edges = [(u, v) for u, adj in adj_dict.items() for v in adj] graph = cls() graph.add_nodes_from(nodes) graph.add_edges_from(edges) return graph
[docs]def nx_dag_node_rank(graph, nodes=None): """ Returns rank of nodes that define the "level" each node is on in a topological sort. This is the same as the Graphviz dot rank. Ignore: simple_graph = ut.simplify_graph(exi_graph) adj_dict = ut.nx_to_adj_dict(simple_graph) import wbia.plottool as pt pt.qt4ensure() pt.show_nx(graph) Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> adj_dict = {0: [5], 1: [5], 2: [1], 3: [4], 4: [0], 5: [], 6: [4], 7: [9], 8: [6], 9: [1]} >>> nodes = [2, 1, 5] >>> f_graph = ut.nx_from_adj_dict(adj_dict, nx.DiGraph) >>> graph = f_graph.reverse() >>> #ranks = ut.nx_dag_node_rank(graph, nodes) >>> ranks = ut.nx_dag_node_rank(graph, nodes) >>> result = ('ranks = %r' % (ranks,)) >>> print(result) ranks = [3, 2, 1] """ import utool as ut source = list(ut.nx_source_nodes(graph))[0] longest_paths = dict( [(target, dag_longest_path(graph, source, target)) for target in graph.nodes()] ) node_to_rank = ut.map_dict_vals(len, longest_paths) if nodes is None: return node_to_rank else: ranks = ut.dict_take(node_to_rank, nodes) return ranks
[docs]def nx_all_nodes_between(graph, source, target, data=False): """ Find all nodes with on paths between source and target. """ import utool as ut if source is None: # assume there is a single source sources = list(ut.nx_source_nodes(graph)) assert len(sources) == 1, 'specify source if there is not only one' source = sources[0] if target is None: # assume there is a single source sinks = list(ut.nx_sink_nodes(graph)) assert len(sinks) == 1, 'specify sink if there is not only one' target = sinks[0] all_simple_paths = list(nx.all_simple_paths(graph, source, target)) nodes = sorted(set.union(*map(set, all_simple_paths))) return nodes
[docs]def nx_all_simple_edge_paths(G, source, target, cutoff=None, keys=False, data=False): """ Returns each path from source to target as a list of edges. This function is meant to be used with MultiGraphs or MultiDiGraphs. When ``keys`` is True each edge in the path is returned with its unique key identifier. In this case it is possible to distinguish between different paths along different edges between the same two nodes. Derived from simple_paths.py in networkx """ if cutoff is None: cutoff = len(G) - 1 if cutoff < 1: return import utool as ut import six visited_nodes = [source] visited_edges = [] if G.is_multigraph(): get_neighbs = ut.partial(G.edges, keys=keys, data=data) else: get_neighbs = ut.partial(G.edges, data=data) edge_stack = [iter(get_neighbs(source))] while edge_stack: children_edges = edge_stack[-1] child_edge = six.next(children_edges, None) if child_edge is None: edge_stack.pop() visited_nodes.pop() if len(visited_edges) > 0: visited_edges.pop() elif len(visited_nodes) < cutoff: child_node = child_edge[1] if child_node == target: yield visited_edges + [child_edge] elif child_node not in visited_nodes: visited_nodes.append(child_node) visited_edges.append(child_edge) edge_stack.append(iter(get_neighbs(child_node))) else: for edge in [child_edge] + list(children_edges): if edge[1] == target: yield visited_edges + [edge] edge_stack.pop() visited_nodes.pop() if len(visited_edges) > 0: visited_edges.pop()
[docs]def nx_edges_between( graph, nodes1, nodes2=None, assume_disjoint=False, assume_sparse=True ): r""" Get edges between two components or within a single component Args: graph (nx.Graph): the graph nodes1 (set): list of nodes nodes2 (set): (default=None) if None it is equivlanet to nodes2=nodes1 assume_disjoint (bool): skips expensive check to ensure edges arnt returned twice (default=False) CommandLine: python -m utool.util_graph --test-nx_edges_between Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> edges = [ >>> (1, 2), (2, 3), (3, 4), (4, 1), (4, 3), # cc 1234 >>> (1, 5), (7, 2), (5, 1), # cc 567 / 5678 >>> (7, 5), (5, 6), (8, 7), >>> ] >>> digraph = nx.DiGraph(edges) >>> graph = nx.Graph(edges) >>> nodes1 = [1, 2, 3, 4] >>> nodes2 = [5, 6, 7] >>> n2 = sorted(nx_edges_between(graph, nodes1, nodes2)) >>> n4 = sorted(nx_edges_between(graph, nodes1)) >>> n5 = sorted(nx_edges_between(graph, nodes1, nodes1)) >>> n1 = sorted(nx_edges_between(digraph, nodes1, nodes2)) >>> n3 = sorted(nx_edges_between(digraph, nodes1)) >>> print('n2 == %r' % (n2,)) >>> print('n4 == %r' % (n4,)) >>> print('n5 == %r' % (n5,)) >>> print('n1 == %r' % (n1,)) >>> print('n3 == %r' % (n3,)) >>> assert n2 == ([(1, 5), (2, 7)]), '2' >>> assert n4 == ([(1, 2), (1, 4), (2, 3), (3, 4)]), '4' >>> assert n5 == ([(1, 2), (1, 4), (2, 3), (3, 4)]), '5' >>> assert n1 == ([(1, 5), (5, 1), (7, 2)]), '1' >>> assert n3 == ([(1, 2), (2, 3), (3, 4), (4, 1), (4, 3)]), '3' >>> n6 = sorted(nx_edges_between(digraph, nodes1 + [6], nodes2 + [1, 2], assume_sparse=True)) >>> print('n6 = %r' % (n6,)) >>> n6 = sorted(nx_edges_between(digraph, nodes1 + [6], nodes2 + [1, 2], assume_sparse=False)) >>> print('n6 = %r' % (n6,)) >>> assert n6 == ([(1, 2), (1, 5), (2, 3), (4, 1), (5, 1), (5, 6), (7, 2)]), '6' Timeit: from utool.util_graph import * # NOQA # ut.timeit_compare() import networkx as nx import utool as ut graph = nx.fast_gnp_random_graph(1000, .001) list(nx.connected_components(graph)) rng = np.random.RandomState(0) nodes1 = set(rng.choice(list(graph.nodes()), 500, replace=False)) nodes2 = set(graph.nodes()) - nodes1 edges_between = ut.nx_edges_between %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=False, assume_disjoint=True)) %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=False, assume_disjoint=False)) %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=True, assume_disjoint=False)) %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=True, assume_disjoint=True)) graph = nx.fast_gnp_random_graph(1000, .1) rng = np.random.RandomState(0) print(graph.number_of_edges()) nodes1 = set(rng.choice(list(graph.nodes()), 500, replace=False)) nodes2 = set(graph.nodes()) - nodes1 edges_between = ut.nx_edges_between %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=True, assume_disjoint=True)) %timeit list(edges_between(graph, nodes1, nodes2, assume_sparse=False, assume_disjoint=True)) Ignore: graph = nx.DiGraph(edges) graph = nx.Graph(edges) nodes1 = [1, 2, 3, 4] nodes2 = nodes1 """ if assume_sparse: # Method 1 is where we check the intersection of existing edges # and the edges in the second set (faster for sparse graphs) # helpers nx_edges between def _node_combo_lower(graph, both): both_lower = set([]) for u in both: neighbs = set(graph.adj[u]) neighbsBB_lower = neighbs.intersection(both_lower) for v in neighbsBB_lower: yield (u, v) both_lower.add(u) def _node_combo_upper(graph, both): both_upper = both.copy() for u in both: neighbs = set(graph.adj[u]) neighbsBB_upper = neighbs.intersection(both_upper) for v in neighbsBB_upper: yield (u, v) both_upper.remove(u) def _node_product(graph, only1, only2): for u in only1: neighbs = set(graph.adj[u]) neighbs12 = neighbs.intersection(only2) for v in neighbs12: yield (u, v) # Test for special cases if nodes2 is None or nodes2 is nodes1: # Case where we just are finding internal edges both = set(nodes1) if graph.is_directed(): edge_sets = ( _node_combo_upper(graph, both), # B-to-B (upper) _node_combo_lower(graph, both), # B-to-B (lower) ) else: edge_sets = (_node_combo_upper(graph, both),) # B-to-B (upper) elif assume_disjoint: # Case where we find edges between disjoint sets only1 = set(nodes1) only2 = set(nodes2) if graph.is_directed(): edge_sets = ( _node_product(graph, only1, only2), # 1-to-2 _node_product(graph, only2, only1), # 2-to-1 ) else: edge_sets = (_node_product(graph, only1, only2),) # 1-to-2 else: # Full general case nodes1_ = set(nodes1) if nodes2 is None: nodes2_ = nodes1_ else: nodes2_ = set(nodes2) both = nodes1_.intersection(nodes2_) only1 = nodes1_ - both only2 = nodes2_ - both # This could be made faster by avoiding duplicate # calls to set(graph.adj[u]) in the helper functions if graph.is_directed(): edge_sets = ( _node_product(graph, only1, only2), # 1-to-2 _node_product(graph, only1, both), # 1-to-B _node_combo_upper(graph, both), # B-to-B (u) _node_combo_lower(graph, both), # B-to-B (l) _node_product(graph, both, only1), # B-to-1 _node_product(graph, both, only2), # B-to-2 _node_product(graph, only2, both), # 2-to-B _node_product(graph, only2, only1), # 2-to-1 ) else: edge_sets = ( _node_product(graph, only1, only2), # 1-to-2 _node_product(graph, only1, both), # 1-to-B _node_combo_upper(graph, both), # B-to-B (u) _node_product(graph, only2, both), # 2-to-B ) for u, v in it.chain.from_iterable(edge_sets): yield u, v else: # Method 2 is where we enumerate all possible edges and just take the # ones that exist (faster for very dense graphs) if nodes2 is None or nodes2 is nodes1: edge_iter = it.combinations(nodes1, 2) else: if assume_disjoint: # We assume len(isect(nodes1, nodes2)) == 0 edge_iter = it.product(nodes1, nodes2) else: # make sure a single edge is not returned twice # in the case where len(isect(nodes1, nodes2)) > 0 nodes1_ = set(nodes1) nodes2_ = set(nodes2) nodes_isect = nodes1_.intersection(nodes2_) nodes_only1 = nodes1_ - nodes_isect nodes_only2 = nodes2_ - nodes_isect edge_sets = [ it.product(nodes_only1, nodes_only2), it.product(nodes_only1, nodes_isect), it.product(nodes_only2, nodes_isect), it.combinations(nodes_isect, 2), ] edge_iter = it.chain.from_iterable(edge_sets) if graph.is_directed(): for n1, n2 in edge_iter: if graph.has_edge(n1, n2): yield n1, n2 if graph.has_edge(n2, n1): yield n2, n1 else: for n1, n2 in edge_iter: if graph.has_edge(n1, n2): yield n1, n2
[docs]def nx_delete_node_attr(graph, name, nodes=None): """ Removes node attributes Doctest: >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.karate_club_graph() >>> nx.set_node_attributes(G, name='foo', values='bar') >>> datas = nx.get_node_attributes(G, 'club') >>> assert len(nx.get_node_attributes(G, 'club')) == 34 >>> assert len(nx.get_node_attributes(G, 'foo')) == 34 >>> ut.nx_delete_node_attr(G, ['club', 'foo'], nodes=[1, 2]) >>> assert len(nx.get_node_attributes(G, 'club')) == 32 >>> assert len(nx.get_node_attributes(G, 'foo')) == 32 >>> ut.nx_delete_node_attr(G, ['club']) >>> assert len(nx.get_node_attributes(G, 'club')) == 0 >>> assert len(nx.get_node_attributes(G, 'foo')) == 32 """ if nodes is None: nodes = list(graph.nodes()) removed = 0 # names = [name] if not isinstance(name, list) else name node_dict = nx_node_dict(graph) if isinstance(name, list): for node in nodes: for name_ in name: try: del node_dict[node][name_] removed += 1 except KeyError: pass else: for node in nodes: try: del node_dict[node][name] removed += 1 except KeyError: pass return removed
[docs]@profile def nx_delete_edge_attr(graph, name, edges=None): """ Removes an attributes from specific edges in the graph Doctest: >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.karate_club_graph() >>> nx.set_edge_attributes(G, name='spam', values='eggs') >>> nx.set_edge_attributes(G, name='foo', values='bar') >>> assert len(nx.get_edge_attributes(G, 'spam')) == 78 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 78 >>> ut.nx_delete_edge_attr(G, ['spam', 'foo'], edges=[(1, 2)]) >>> assert len(nx.get_edge_attributes(G, 'spam')) == 77 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 77 >>> ut.nx_delete_edge_attr(G, ['spam']) >>> assert len(nx.get_edge_attributes(G, 'spam')) == 0 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 77 Doctest: >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.MultiGraph() >>> G.add_edges_from([(1, 2), (2, 3), (3, 4), (4, 5), (4, 5), (1, 2)]) >>> nx.set_edge_attributes(G, name='spam', values='eggs') >>> nx.set_edge_attributes(G, name='foo', values='bar') >>> assert len(nx.get_edge_attributes(G, 'spam')) == 6 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 6 >>> ut.nx_delete_edge_attr(G, ['spam', 'foo'], edges=[(1, 2, 0)]) >>> assert len(nx.get_edge_attributes(G, 'spam')) == 5 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 5 >>> ut.nx_delete_edge_attr(G, ['spam']) >>> assert len(nx.get_edge_attributes(G, 'spam')) == 0 >>> assert len(nx.get_edge_attributes(G, 'foo')) == 5 """ removed = 0 keys = [name] if not isinstance(name, (list, tuple)) else name if edges is None: if graph.is_multigraph(): edges = graph.edges(keys=True) else: edges = graph.edges() if graph.is_multigraph(): for u, v, k in edges: for key_ in keys: try: del graph[u][v][k][key_] removed += 1 except KeyError: pass else: for u, v in edges: for key_ in keys: try: del graph[u][v][key_] removed += 1 except KeyError: pass return removed
[docs]def nx_delete_None_edge_attr(graph, edges=None): removed = 0 if graph.is_multigraph(): if edges is None: edges = list(graph.edges(keys=graph.is_multigraph())) for edge in edges: u, v, k = edge data = graph[u][v][k] for key in list(data.keys()): try: if data[key] is None: del data[key] removed += 1 except KeyError: pass else: if edges is None: edges = list(graph.edges()) for edge in graph.edges(): u, v = edge data = graph[u][v] for key in list(data.keys()): try: if data[key] is None: del data[key] removed += 1 except KeyError: pass return removed
[docs]def nx_delete_None_node_attr(graph, nodes=None): removed = 0 if nodes is None: nodes = list(graph.nodes()) for node in graph.nodes(): node_dict = nx_node_dict(graph) data = node_dict[node] for key in list(data.keys()): try: if data[key] is None: del data[key] removed += 1 except KeyError: pass return removed
[docs]def nx_set_default_node_attributes(graph, key, val): unset_nodes = [n for n, d in graph.nodes(data=True) if key not in d] if isinstance(val, dict): values = {n: val[n] for n in unset_nodes if n in val} else: values = {n: val for n in unset_nodes} nx.set_node_attributes(graph, name=key, values=values)
[docs]def nx_set_default_edge_attributes(graph, key, val): unset_edges = [(u, v) for u, v, d in graph.edges(data=True) if key not in d] if isinstance(val, dict): values = {e: val[e] for e in unset_edges if e in val} else: values = {e: val for e in unset_edges} nx.set_edge_attributes(graph, name=key, values=values)
[docs]def nx_get_default_edge_attributes(graph, key, default=None): import utool as ut edge_list = list(graph.edges()) partial_attr_dict = nx.get_edge_attributes(graph, key) attr_dict = ut.dict_subset(partial_attr_dict, edge_list, default=default) return attr_dict
[docs]def nx_get_default_node_attributes(graph, key, default=None): import utool as ut node_list = list(graph.nodes()) partial_attr_dict = nx.get_node_attributes(graph, key) attr_dict = ut.dict_subset(partial_attr_dict, node_list, default=default) return attr_dict
[docs]def nx_gen_node_values(G, key, nodes, default=util_const.NoParam): """ Generates attributes values of specific nodes """ node_dict = nx_node_dict(G) if default is util_const.NoParam: return (node_dict[n][key] for n in nodes) else: return (node_dict[n].get(key, default) for n in nodes)
[docs]def nx_gen_node_attrs( G, key, nodes=None, default=util_const.NoParam, on_missing='error', on_keyerr='default', ): """ Improved generator version of nx.get_node_attributes Args: on_missing (str): Strategy for handling nodes missing from G. Can be {'error', 'default', 'filter'}. defaults to 'error'. on_keyerr (str): Strategy for handling keys missing from node dicts. Can be {'error', 'default', 'filter'}. defaults to 'default' if default is specified, otherwise defaults to 'error'. Notes: strategies are: error - raises an error if key or node does not exist default - returns node, but uses value specified by default filter - skips the node Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.Graph([(1, 2), (2, 3)]) >>> nx.set_node_attributes(G, name='part', values={1: 'bar', 3: 'baz'}) >>> nodes = [1, 2, 3, 4] >>> # >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', default=None, on_missing='error', on_keyerr='default'))) == 3 >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', default=None, on_missing='error', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, ut.nx_gen_node_attrs(G, 'part', on_missing='error', on_keyerr='error')) >>> # >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', nodes, default=None, on_missing='filter', on_keyerr='default'))) == 3 >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', nodes, default=None, on_missing='filter', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, ut.nx_gen_node_attrs(G, 'part', nodes, on_missing='filter', on_keyerr='error')) >>> # >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', nodes, default=None, on_missing='default', on_keyerr='default'))) == 4 >>> assert len(list(ut.nx_gen_node_attrs(G, 'part', nodes, default=None, on_missing='default', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, ut.nx_gen_node_attrs(G, 'part', nodes, on_missing='default', on_keyerr='error')) Example: >>> # DISABLE_DOCTEST >>> # ALL CASES >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.Graph([(1, 2), (2, 3)]) >>> nx.set_node_attributes(G, name='full', values={1: 'A', 2: 'B', 3: 'C'}) >>> nx.set_node_attributes(G, name='part', values={1: 'bar', 3: 'baz'}) >>> nodes = [1, 2, 3, 4] >>> attrs = dict(ut.nx_gen_node_attrs(G, 'full')) >>> input_grid = { >>> 'nodes': [None, (1, 2, 3, 4)], >>> 'key': ['part', 'full'], >>> 'default': [util_const.NoParam, None], >>> } >>> inputs = ut.all_dict_combinations(input_grid) >>> kw_grid = { >>> 'on_missing': ['error', 'default', 'filter'], >>> 'on_keyerr': ['error', 'default', 'filter'], >>> } >>> kws = ut.all_dict_combinations(kw_grid) >>> for in_ in inputs: >>> for kw in kws: >>> kw2 = ut.dict_union(kw, in_) >>> #print(kw2) >>> on_missing = kw['on_missing'] >>> on_keyerr = kw['on_keyerr'] >>> if on_keyerr == 'default' and in_['default'] is util_const.NoParam: >>> on_keyerr = 'error' >>> will_miss = False >>> will_keyerr = False >>> if on_missing == 'error': >>> if in_['key'] == 'part' and in_['nodes'] is not None: >>> will_miss = True >>> if in_['key'] == 'full' and in_['nodes'] is not None: >>> will_miss = True >>> if on_keyerr == 'error': >>> if in_['key'] == 'part': >>> will_keyerr = True >>> if on_missing == 'default': >>> if in_['key'] == 'full' and in_['nodes'] is not None: >>> will_keyerr = True >>> want_error = will_miss or will_keyerr >>> gen = ut.nx_gen_node_attrs(G, **kw2) >>> try: >>> attrs = list(gen) >>> except KeyError: >>> if not want_error: >>> raise AssertionError('should not have errored') >>> else: >>> if want_error: >>> raise AssertionError('should have errored') """ if on_missing is None: on_missing = 'error' if default is util_const.NoParam and on_keyerr == 'default': on_keyerr = 'error' if nodes is None: nodes = G.nodes() # Generate `node_data` nodes and data dictionary node_dict = nx_node_dict(G) if on_missing == 'error': node_data = ((n, node_dict[n]) for n in nodes) elif on_missing == 'filter': node_data = ((n, node_dict[n]) for n in nodes if n in G) elif on_missing == 'default': node_data = ((n, node_dict.get(n, {})) for n in nodes) else: raise KeyError( 'on_missing={} must be error, filter or default'.format(on_missing) ) # Get `node_attrs` desired value out of dictionary if on_keyerr == 'error': node_attrs = ((n, d[key]) for n, d in node_data) elif on_keyerr == 'filter': node_attrs = ((n, d[key]) for n, d in node_data if key in d) elif on_keyerr == 'default': node_attrs = ((n, d.get(key, default)) for n, d in node_data) else: raise KeyError('on_keyerr={} must be error filter or default'.format(on_keyerr)) return node_attrs
[docs]def nx_gen_edge_values( G, key, edges=None, default=util_const.NoParam, on_missing='error', on_keyerr='default', ): """ Generates attributes values of specific edges Args: on_missing (str): Strategy for handling nodes missing from G. Can be {'error', 'default'}. defaults to 'error'. on_keyerr (str): Strategy for handling keys missing from node dicts. Can be {'error', 'default'}. defaults to 'default' if default is specified, otherwise defaults to 'error'. """ if edges is None: edges = G.edges() if on_missing is None: on_missing = 'error' if on_keyerr is None: on_keyerr = 'default' if default is util_const.NoParam and on_keyerr == 'default': on_keyerr = 'error' # Generate `data_iter` edges and data dictionary if on_missing == 'error': data_iter = (G.adj[u][v] for u, v in edges) elif on_missing == 'default': data_iter = (G.adj[u][v] if G.has_edge(u, v) else {} for u, v in edges) else: raise KeyError( 'on_missing={} must be error, filter or default'.format(on_missing) ) # Get `value_iter` desired value out of dictionary if on_keyerr == 'error': value_iter = (d[key] for d in data_iter) elif on_keyerr == 'default': value_iter = (d.get(key, default) for d in data_iter) else: raise KeyError('on_keyerr={} must be error or default'.format(on_keyerr)) return value_iter
# if default is util_const.NoParam: # return (G.adj[u][v][key] for u, v in edges) # else: # return (G.adj[u][v].get(key, default) for u, v in edges)
[docs]def nx_gen_edge_attrs( G, key, edges=None, default=util_const.NoParam, on_missing='error', on_keyerr='default', ): """ Improved generator version of nx.get_edge_attributes Args: on_missing (str): Strategy for handling nodes missing from G. Can be {'error', 'default', 'filter'}. defaults to 'error'. is on_missing is not error, then we allow any edge even if the endpoints are not in the graph. on_keyerr (str): Strategy for handling keys missing from node dicts. Can be {'error', 'default', 'filter'}. defaults to 'default' if default is specified, otherwise defaults to 'error'. Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> G = nx.Graph([(1, 2), (2, 3), (3, 4)]) >>> nx.set_edge_attributes(G, name='part', values={(1, 2): 'bar', (2, 3): 'baz'}) >>> edges = [(1, 2), (2, 3), (3, 4), (4, 5)] >>> func = ut.partial(ut.nx_gen_edge_attrs, G, 'part', default=None) >>> # >>> assert len(list(func(on_missing='error', on_keyerr='default'))) == 3 >>> assert len(list(func(on_missing='error', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, func(on_missing='error', on_keyerr='error')) >>> # >>> assert len(list(func(edges, on_missing='filter', on_keyerr='default'))) == 3 >>> assert len(list(func(edges, on_missing='filter', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, func(edges, on_missing='filter', on_keyerr='error')) >>> # >>> assert len(list(func(edges, on_missing='default', on_keyerr='default'))) == 4 >>> assert len(list(func(edges, on_missing='default', on_keyerr='filter'))) == 2 >>> ut.assert_raises(KeyError, list, func(edges, on_missing='default', on_keyerr='error')) """ if on_missing is None: on_missing = 'error' if default is util_const.NoParam and on_keyerr == 'default': on_keyerr = 'error' if edges is None: if G.is_multigraph(): raise NotImplementedError('') # uvk_iter = G.edges(keys=True) else: edges = G.edges() # Generate `edge_data` edges and data dictionary if on_missing == 'error': edge_data = (((u, v), G.adj[u][v]) for u, v in edges) elif on_missing == 'filter': edge_data = (((u, v), G.adj[u][v]) for u, v in edges if G.has_edge(u, v)) elif on_missing == 'default': edge_data = ( ((u, v), G.adj[u][v]) if G.has_edge(u, v) else ((u, v), {}) for u, v in edges ) else: raise KeyError('on_missing={}'.format(on_missing)) # Get `edge_attrs` desired value out of dictionary if on_keyerr == 'error': edge_attrs = ((e, d[key]) for e, d in edge_data) elif on_keyerr == 'filter': edge_attrs = ((e, d[key]) for e, d in edge_data if key in d) elif on_keyerr == 'default': edge_attrs = ((e, d.get(key, default)) for e, d in edge_data) else: raise KeyError('on_keyerr={}'.format(on_keyerr)) return edge_attrs
# if edges is None: # if G.is_multigraph(): # edges_ = G.edges(keys=True, data=True) # else: # edges_ = G.edges(data=True) # if default is util_const.NoParam: # return ((x[:-1], x[-1][key]) for x in edges_ if key in x[-1]) # else: # return ((x[:-1], x[-1].get(key, default)) for x in edges_) # else: # if on_missing == 'error': # uv_iter = edges # uvd_iter = ((u, v, G.adj[u][v]) for u, v in uv_iter) # elif on_missing == 'filter': # # filter edges that don't exist # uv_iter = (e for e in edges if G.has_edge(*e)) # uvd_iter = ((u, v, G.adj[u][v]) for u, v in uv_iter) # elif on_missing == 'default': # # Return default data as if it existed # uvd_iter = ( # (u, v, G.adj[u][v]) # if G.has_edge(u, v) else # (u, v, {}) # for u, v in uv_iter # ) # else: # raise KeyError('on_missing={}'.format(on_missing)) # if default is util_const.NoParam: # # return (((u, v), d[key]) for u, v, d in uvd_iter if key in d) # return (((u, v), d[key]) for u, v, d in uvd_iter) # else: # uvd_iter = ((u, v, G.adj[u][v]) for u, v in uv_iter) # return (((u, v), d.get(key, default)) for u, v, d in uvd_iter)
[docs]def nx_from_node_edge(nodes=None, edges=None): graph = nx.Graph() if nodes: graph.add_nodes_from(nodes) if edges: graph.add_edges_from(edges) return graph
[docs]def nx_minimum_weight_component(graph, weight='weight'): """ A minimum weight component is an MST + all negative edges """ mwc = nx.minimum_spanning_tree(graph, weight=weight) # negative edges only reduce the total weight neg_edges = (e for e, w in nx_gen_edge_attrs(graph, weight) if w < 0) mwc.add_edges_from(neg_edges) return mwc
[docs]def nx_from_matrix(weight_matrix, nodes=None, remove_self=True): import utool as ut import numpy as np if nodes is None: nodes = list(range(len(weight_matrix))) weight_list = weight_matrix.ravel() flat_idxs_ = np.arange(weight_matrix.size) multi_idxs_ = np.unravel_index(flat_idxs_, weight_matrix.shape) # Remove 0 weight edges flags = np.logical_not(np.isclose(weight_list, 0)) weight_list = ut.compress(weight_list, flags) multi_idxs = ut.compress(list(zip(*multi_idxs_)), flags) edge_list = ut.lmap(tuple, ut.unflat_take(nodes, multi_idxs)) if remove_self: flags = [e1 != e2 for e1, e2 in edge_list] edge_list = ut.compress(edge_list, flags) weight_list = ut.compress(weight_list, flags) graph = nx.Graph() graph.add_nodes_from(nodes) graph.add_edges_from(edge_list) label_list = ['%.2f' % w for w in weight_list] nx.set_edge_attributes(graph, name='weight', values=dict(zip(edge_list, weight_list))) nx.set_edge_attributes(graph, name='label', values=dict(zip(edge_list, label_list))) return graph
[docs]def nx_ensure_agraph_color(graph): """ changes colors to hex strings on graph attrs """ try: from wbia.plottool import color_funcs import wbia.plottool as pt except ImportError: from plottool import color_funcs import wbia.plottool as pt # import six def _fix_agraph_color(data): try: orig_color = data.get('color', None) alpha = data.get('alpha', None) color = orig_color if color is None and alpha is not None: color = [0, 0, 0] if color is not None: color = pt.ensure_nonhex_color(color) # if isinstance(color, np.ndarray): # color = color.tolist() color = list(color_funcs.ensure_base255(color)) if alpha is not None: if len(color) == 3: color += [int(alpha * 255)] else: color[3] = int(alpha * 255) color = tuple(color) if len(color) == 3: data['color'] = '#%02x%02x%02x' % color else: data['color'] = '#%02x%02x%02x%02x' % color except Exception as ex: import utool as ut ut.printex(ex, keys=['color', 'orig_color', 'data']) raise for node, node_data in graph.nodes(data=True): data = node_data _fix_agraph_color(data) for u, v, edge_data in graph.edges(data=True): data = edge_data _fix_agraph_color(data)
[docs]def nx_edges(graph, keys=False, data=False): if graph.is_multigraph(): edges = graph.edges(keys=keys, data=data) else: edges = graph.edges(data=data) # if keys: # edges = [e[0:2] + (0,) + e[:2] for e in edges] return edges
[docs]def dag_longest_path(graph, source, target): """ Finds the longest path in a dag between two nodes """ if source == target: return [source] allpaths = nx.all_simple_paths(graph, source, target) longest_path = [] for l in allpaths: if len(l) > len(longest_path): longest_path = l return longest_path
[docs]def testdata_graph(): r""" Returns: tuple: (graph, G) CommandLine: python -m utool.util_graph --exec-testdata_graph --show Example: >>> # DISABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> (graph, G) = testdata_graph() >>> import wbia.plottool as pt >>> ut.ensureqt() >>> pt.show_nx(G, layout='agraph') >>> ut.show_if_requested() """ import utool as ut # Define adjacency list graph = { 'a': ['b'], 'b': ['c', 'f', 'e'], 'c': ['g', 'd'], 'd': ['c', 'h'], 'e': ['a', 'f'], 'f': ['g'], 'g': ['f'], 'h': ['g', 'd'], 'i': ['j'], 'j': [], } graph = { 'a': ['b'], 'b': ['c'], 'c': ['d'], 'd': ['a', 'e'], 'e': ['c'], } # graph = {'a': ['b'], 'b': ['c'], 'c': ['d'], 'd': ['a']} # graph = {'a': ['b'], 'b': ['c'], 'c': ['d'], 'd': ['e'], 'e': ['a']} graph = {'a': ['b'], 'b': ['c'], 'c': ['d'], 'd': ['e'], 'e': ['a'], 'f': ['c']} # graph = {'a': ['b'], 'b': ['c'], 'c': ['d'], 'd': ['e'], 'e': ['b']} graph = { 'a': ['b', 'c', 'd'], 'e': ['d'], 'f': ['d', 'e'], 'b': [], 'c': [], 'd': [], } # double pair in non-scc graph = { 'a': ['b', 'c', 'd'], 'e': ['d'], 'f': ['d', 'e'], 'b': [], 'c': [], 'd': ['e'], } # double pair in non-scc # graph = {'a': ['b', 'c', 'd'], 'e': ['d', 'f'], 'f': ['d', 'e'], 'b': [], 'c': [], 'd': ['e']} # double pair in non-scc # graph = {'a': ['b', 'c', 'd'], 'e': ['d', 'c'], 'f': ['d', 'e'], 'b': ['e'], 'c': ['e'], 'd': ['e']} # double pair in non-scc graph = { 'a': ['b', 'c', 'd'], 'e': ['d', 'c'], 'f': ['d', 'e'], 'b': ['e'], 'c': ['e', 'b'], 'd': ['e'], } # double pair in non-scc # Extract G = (V, E) nodes = list(graph.keys()) edges = ut.flatten([[(v1, v2) for v2 in v2s] for v1, v2s in graph.items()]) G = nx.DiGraph() G.add_nodes_from(nodes) G.add_edges_from(edges) if False: G.remove_node('e') del graph['e'] for val in graph.values(): try: val.remove('e') except ValueError: pass return graph, G
[docs]def dict_depth(dict_, accum=0): if not isinstance(dict_, dict): return accum return max([dict_depth(val, accum + 1) for key, val in dict_.items()])
[docs]def edges_to_adjacency_list(edges): import utool as ut children_, parents_ = list(zip(*edges)) parent_to_children = ut.group_items(parents_, children_) # to_leafs = {tablename: path_to_leafs(tablename, parent_to_children)} return parent_to_children
[docs]def paths_to_root(tablename, root, child_to_parents): """ CommandLine: python -m utool.util_graph --exec-paths_to_root:0 python -m utool.util_graph --exec-paths_to_root:1 Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> child_to_parents = { >>> 'chip': ['dummy_annot'], >>> 'chipmask': ['dummy_annot'], >>> 'descriptor': ['keypoint'], >>> 'fgweight': ['keypoint', 'probchip'], >>> 'keypoint': ['chip'], >>> 'notch': ['dummy_annot'], >>> 'probchip': ['dummy_annot'], >>> 'spam': ['fgweight', 'chip', 'keypoint'] >>> } >>> root = 'dummy_annot' >>> tablename = 'fgweight' >>> to_root = paths_to_root(tablename, root, child_to_parents) >>> result = ut.repr3(to_root) >>> print(result) { 'keypoint': { 'chip': { 'dummy_annot': None, }, }, 'probchip': { 'dummy_annot': None, }, } Example: >>> # DISABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> root = u'annotations' >>> tablename = u'Notch_Tips' >>> child_to_parents = { >>> 'Block_Curvature': [ >>> 'Trailing_Edge', >>> ], >>> 'Has_Notch': [ >>> 'annotations', >>> ], >>> 'Notch_Tips': [ >>> 'annotations', >>> ], >>> 'Trailing_Edge': [ >>> 'Notch_Tips', >>> ], >>> } >>> to_root = paths_to_root(tablename, root, child_to_parents) >>> result = ut.repr3(to_root) >>> print(result) """ if tablename == root: return None parents = child_to_parents[tablename] return {parent: paths_to_root(parent, root, child_to_parents) for parent in parents}
[docs]def get_allkeys(dict_): import utool as ut if not isinstance(dict_, dict): return [] subkeys = [[key] + get_allkeys(val) for key, val in dict_.items()] return ut.unique_ordered(ut.flatten(subkeys))
[docs]def traverse_path(start, end, seen_, allkeys, mat): import utool as ut if seen_ is None: seen_ = set([]) index = allkeys.index(start) sub_indexes = np.where(mat[index])[0] if len(sub_indexes) > 0: subkeys = ut.take(allkeys, sub_indexes) # subkeys_ = ut.take(allkeys, sub_indexes) # subkeys = [subkey for subkey in subkeys_ # if subkey not in seen_] # for sk in subkeys: # seen_.add(sk) if len(subkeys) > 0: return { subkey: traverse_path(subkey, end, seen_, allkeys, mat) for subkey in subkeys } return None
[docs]def reverse_path(dict_, root, child_to_parents): """ CommandLine: python -m utool.util_graph --exec-reverse_path --show Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> child_to_parents = { >>> 'chip': ['dummy_annot'], >>> 'chipmask': ['dummy_annot'], >>> 'descriptor': ['keypoint'], >>> 'fgweight': ['keypoint', 'probchip'], >>> 'keypoint': ['chip'], >>> 'notch': ['dummy_annot'], >>> 'probchip': ['dummy_annot'], >>> 'spam': ['fgweight', 'chip', 'keypoint'] >>> } >>> to_root = { >>> 'fgweight': { >>> 'keypoint': { >>> 'chip': { >>> 'dummy_annot': None, >>> }, >>> }, >>> 'probchip': { >>> 'dummy_annot': None, >>> }, >>> }, >>> } >>> reversed_ = reverse_path(to_root, 'dummy_annot', child_to_parents) >>> result = ut.repr3(reversed_) >>> print(result) { 'dummy_annot': { 'chip': { 'keypoint': { 'fgweight': None, }, }, 'probchip': { 'fgweight': None, }, }, } """ # Hacky but illustrative # TODO; implement non-hacky version allkeys = get_allkeys(dict_) mat = np.zeros((len(allkeys), len(allkeys))) for key in allkeys: if key != root: for parent in child_to_parents[key]: rx = allkeys.index(parent) cx = allkeys.index(key) mat[rx][cx] = 1 end = None seen_ = set([]) reversed_ = {root: traverse_path(root, end, seen_, allkeys, mat)} return reversed_
[docs]def get_levels(dict_, n=0, levels=None): r""" DEPCIRATE Args: dict_ (dict_): a dictionary n (int): (default = 0) levels (None): (default = None) CommandLine: python -m utool.util_graph --test-get_levels --show python3 -m utool.util_graph --test-get_levels --show Example: >>> # DISABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> from_root = { >>> 'dummy_annot': { >>> 'chip': { >>> 'keypoint': { >>> 'fgweight': None, >>> }, >>> }, >>> 'probchip': { >>> 'fgweight': None, >>> }, >>> }, >>> } >>> dict_ = from_root >>> n = 0 >>> levels = None >>> levels_ = get_levels(dict_, n, levels) >>> result = ut.repr2(levels_, nl=1) >>> print(result) [ ['dummy_annot'], ['chip', 'probchip'], ['keypoint', 'fgweight'], ['fgweight'], ] """ if levels is None: levels_ = [[] for _ in range(dict_depth(dict_))] else: levels_ = levels if dict_ is None: return [] for key in dict_.keys(): levels_[n].append(key) for val in dict_.values(): get_levels(val, n + 1, levels_) return levels_
[docs]def longest_levels(levels_): r""" Args: levels_ (list): CommandLine: python -m utool.util_graph --exec-longest_levels --show Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> levels_ = [ >>> ['dummy_annot'], >>> ['chip', 'probchip'], >>> ['keypoint', 'fgweight'], >>> ['fgweight'], >>> ] >>> new_levels = longest_levels(levels_) >>> result = ('new_levels = %s' % (ut.repr2(new_levels, nl=1),)) >>> print(result) new_levels = [ ['dummy_annot'], ['chip', 'probchip'], ['keypoint'], ['fgweight'], ] """ return shortest_levels(levels_[::-1])[::-1]
# seen_ = set([]) # new_levels = [] # for level in levels_[::-1]: # new_level = [item for item in level if item not in seen_] # seen_ = seen_.union(set(new_level)) # new_levels.append(new_level) # new_levels = new_levels[::-1] # return new_levels
[docs]def shortest_levels(levels_): r""" Args: levels_ (list): CommandLine: python -m utool.util_graph --exec-shortest_levels --show Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> levels_ = [ >>> ['dummy_annot'], >>> ['chip', 'probchip'], >>> ['keypoint', 'fgweight'], >>> ['fgweight'], >>> ] >>> new_levels = shortest_levels(levels_) >>> result = ('new_levels = %s' % (ut.repr2(new_levels, nl=1),)) >>> print(result) new_levels = [ ['dummy_annot'], ['chip', 'probchip'], ['keypoint', 'fgweight'], ] """ seen_ = set([]) new_levels = [] for level in levels_: new_level = [item for item in level if item not in seen_] seen_ = seen_.union(set(new_level)) if len(new_level) > 0: new_levels.append(new_level) new_levels = new_levels return new_levels
[docs]def simplify_graph(graph): """ strips out everything but connectivity Args: graph (nx.Graph): Returns: nx.Graph: new_graph CommandLine: python3 -m utool.util_graph simplify_graph --show python2 -m utool.util_graph simplify_graph --show python2 -c "import networkx as nx; print(nx.__version__)" python3 -c "import networkx as nx; print(nx.__version__)" Ignore: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> graph = nx.DiGraph([('a', 'b'), ('a', 'c'), ('a', 'e'), >>> ('a', 'd'), ('b', 'd'), ('c', 'e'), >>> ('d', 'e'), ('c', 'e'), ('c', 'd')]) >>> new_graph = simplify_graph(graph) >>> result = ut.repr2(list(new_graph.edges())) >>> #adj_list = sorted(list(nx.generate_adjlist(new_graph))) >>> #result = ut.repr2(adj_list) >>> print(result) [(0, 1), (0, 2), (0, 3), (0, 4), (1, 3), (2, 3), (2, 4), (3, 4)] ['0 1 2 3 4', '1 3 4', '2 4', '3', '4 3'] """ import utool as ut nodes = sorted(list(graph.nodes())) node_lookup = ut.make_index_lookup(nodes) if graph.is_multigraph(): edges = list(graph.edges(keys=True)) else: edges = list(graph.edges()) new_nodes = ut.take(node_lookup, nodes) if graph.is_multigraph(): new_edges = [(node_lookup[e[0]], node_lookup[e[1]], e[2], {}) for e in edges] else: new_edges = [(node_lookup[e[0]], node_lookup[e[1]]) for e in edges] cls = graph.__class__ new_graph = cls() new_graph.add_nodes_from(new_nodes) new_graph.add_edges_from(new_edges) return new_graph
[docs]def subgraph_from_edges(G, edge_list, ref_back=True): """ Creates a networkx graph that is a subgraph of G defined by the list of edges in edge_list. Requires G to be a networkx MultiGraph or MultiDiGraph edge_list is a list of edges in either (u,v) or (u,v,d) form where u and v are nodes comprising an edge, and d would be a dictionary of edge attributes ref_back determines whether the created subgraph refers to back to the original graph and therefore changes to the subgraph's attributes also affect the original graph, or if it is to create a new copy of the original graph. References: http://stackoverflow.com/questions/16150557/nx-subgraph-from-edges """ # TODO: support multi-di-graph sub_nodes = list({y for x in edge_list for y in x[0:2]}) # edge_list_no_data = [edge[0:2] for edge in edge_list] multi_edge_list = [edge[0:3] for edge in edge_list] if ref_back: G_sub = G.subgraph(sub_nodes) for edge in G_sub.edges(keys=True): if edge not in multi_edge_list: G_sub.remove_edge(*edge) else: G_sub = G.subgraph(sub_nodes).copy() for edge in G_sub.edges(keys=True): if edge not in multi_edge_list: G_sub.remove_edge(*edge) return G_sub
[docs]def nx_node_dict(G): if nx.__version__.startswith('1'): return getattr(G, 'node') else: return G.nodes
[docs]def all_multi_paths(graph, source, target, data=False): r""" Returns specific paths along multi-edges from the source to this table. Multipaths are identified by edge keys. Returns all paths from source to target. This function treats multi-edges as distinct and returns the key value in each edge tuple that defines a path. Example: >>> # DISABLE_DOCTEST >>> from dtool.depcache_control import * # NOQA >>> from utool.util_graph import * # NOQA >>> from dtool.example_depcache import testdata_depc >>> depc = testdata_depc() >>> graph = depc.graph >>> source = depc.root >>> target = 'notchpair' >>> path_list1 = ut.all_multi_paths(graph, depc.root, 'notchpair') >>> path_list2 = ut.all_multi_paths(graph, depc.root, 'spam') >>> result1 = ('path_list1 = %s' % ut.repr3(path_list1, nl=1)) >>> result2 = ('path_list2 = %s' % ut.repr3(path_list2, nl=2)) >>> result = '\n'.join([result1, result2]) >>> print(result) path_list1 = [ [('dummy_annot', 'notch', 0), ('notch', 'notchpair', 0)], [('dummy_annot', 'notch', 0), ('notch', 'notchpair', 1)], ] path_list2 = [ [ ('dummy_annot', 'chip', 0), ('chip', 'keypoint', 0), ('keypoint', 'fgweight', 0), ('fgweight', 'spam', 0), ], [ ('dummy_annot', 'chip', 0), ('chip', 'keypoint', 0), ('keypoint', 'spam', 0), ], [ ('dummy_annot', 'chip', 0), ('chip', 'spam', 0), ], [ ('dummy_annot', 'probchip', 0), ('probchip', 'fgweight', 0), ('fgweight', 'spam', 0), ], ] """ path_multiedges = list( nx_all_simple_edge_paths(graph, source, target, keys=True, data=data) ) return path_multiedges
[docs]def reverse_path_edges(edge_list): return [(edge[1], edge[0],) + tuple(edge[2:]) for edge in edge_list][::-1]
[docs]def bfs_multi_edges(G, source, reverse=False, keys=True, data=False): """Produce edges in a breadth-first-search starting at source. ----- Based on http://www.ics.uci.edu/~eppstein/PADS/BFS.py by D. Eppstein, July 2004. """ from collections import deque from functools import partial if reverse: G = G.reverse() edges_iter = partial(G.edges_iter, keys=keys, data=data) list(G.edges_iter('multitest', keys=True, data=True)) visited_nodes = set([source]) # visited_edges = set([]) queue = deque([(source, edges_iter(source))]) while queue: parent, edges = queue[0] try: edge = next(edges) edge_nodata = edge[0:3] # if edge_nodata not in visited_edges: yield edge # visited_edges.add(edge_nodata) child = edge_nodata[1] if child not in visited_nodes: visited_nodes.add(child) queue.append((child, edges_iter(child))) except StopIteration: queue.popleft()
[docs]def dfs_conditional(G, source, state, can_cross): """ Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * >>> G = nx.Graph() >>> G.add_edges_from([(1, 2), (2, 3), (3, 4), (4, 5)]) >>> G.adj[2][3]['lava'] = True >>> G.adj[3][4]['lava'] = True >>> def can_cross(G, edge, state): >>> # can only cross lava once, then your lava protection wears off >>> data = G.get_edge_data(*edge) >>> lava = int(data.get('lava', False)) >>> if not lava or state == 0: >>> return True, state + lava >>> return False, lava >>> assert 5 not in dfs_conditional(G, 1, state=0, can_cross=can_cross) >>> G.adj[3][4]['lava'] = False >>> assert 5 in dfs_conditional(G, 1, state=0, can_cross=can_cross) """ # stack based version visited = {source} stack = [(source, iter(G[source]), state)] while stack: parent, children, state = stack[-1] try: child = next(children) if child not in visited: edge = (parent, child) flag, new_state = can_cross(G, edge, state) if flag: yield child visited.add(child) stack.append((child, iter(G[child]), new_state)) except StopIteration: stack.pop()
[docs]def bfs_conditional( G, source, reverse=False, keys=True, data=False, yield_nodes=True, yield_if=None, continue_if=None, visited_nodes=None, yield_source=False, ): """ Produce edges in a breadth-first-search starting at source, but only return nodes that satisfiy a condition, and only iterate past a node if it satisfies a different condition. conditions are callables that take (G, child, edge) and return true or false CommandLine: python -m utool.util_graph bfs_conditional Example: >>> # DISABLE_DOCTEST >>> import networkx as nx >>> import utool as ut >>> G = nx.Graph() >>> G.add_edges_from([(1, 2), (1, 3), (2, 3), (2, 4)]) >>> continue_if = lambda G, child, edge: True >>> result = list(ut.bfs_conditional(G, 1, yield_nodes=False)) >>> print(result) [(1, 2), (1, 3), (2, 1), (2, 3), (2, 4), (3, 1), (3, 2), (4, 2)] Example: >>> # ENABLE_DOCTEST >>> import networkx as nx >>> import utool as ut >>> G = nx.Graph() >>> continue_if = lambda G, child, edge: (child % 2 == 0) >>> yield_if = lambda G, child, edge: (child % 2 == 1) >>> G.add_edges_from([(0, 1), (1, 3), (3, 5), (5, 10), >>> (4, 3), (3, 6), >>> (0, 2), (2, 4), (4, 6), (6, 10)]) >>> result = list(ut.bfs_conditional(G, 0, continue_if=continue_if, >>> yield_if=yield_if)) >>> print(result) [1, 3, 5] """ if reverse and hasattr(G, 'reverse'): G = G.reverse() if isinstance(G, nx.Graph): neighbors = functools.partial(G.edges, data=data) else: neighbors = functools.partial(G.edges, keys=keys, data=data) queue = collections.deque([]) if visited_nodes is None: visited_nodes = set([]) else: visited_nodes = set(visited_nodes) if source not in visited_nodes: if yield_nodes and yield_source: yield source visited_nodes.add(source) new_edges = neighbors(source) if isinstance(new_edges, list): new_edges = iter(new_edges) queue.append((source, new_edges)) while queue: parent, edges = queue[0] for edge in edges: child = edge[1] if yield_nodes: if child not in visited_nodes: if yield_if is None or yield_if(G, child, edge): yield child else: if yield_if is None or yield_if(G, child, edge): yield edge if child not in visited_nodes: visited_nodes.add(child) # Add new children to queue if the condition is satisfied if continue_if is None or continue_if(G, child, edge): new_edges = neighbors(child) if isinstance(new_edges, list): new_edges = iter(new_edges) queue.append((child, new_edges)) queue.popleft()
[docs]def color_nodes(graph, labelattr='label', brightness=0.878, outof=None, sat_adjust=None): """ Colors edges and nodes by nid """ try: import wbia.plottool as pt except ImportError: import wbia.plottool as pt import utool as ut node_to_lbl = nx.get_node_attributes(graph, labelattr) unique_lbls = sorted(set(node_to_lbl.values())) ncolors = len(unique_lbls) if outof is None: if (ncolors) == 1: unique_colors = [pt.LIGHT_BLUE] elif (ncolors) == 2: # https://matplotlib.org/examples/color/named_colors.html unique_colors = ['royalblue', 'orange'] unique_colors = list(map(pt.color_funcs.ensure_base01, unique_colors)) else: unique_colors = pt.distinct_colors(ncolors, brightness=brightness) else: unique_colors = pt.distinct_colors(outof, brightness=brightness) if sat_adjust: unique_colors = [ pt.color_funcs.adjust_hsv_of_rgb(c, sat_adjust=sat_adjust) for c in unique_colors ] # Find edges and aids strictly between two nids if outof is None: lbl_to_color = ut.dzip(unique_lbls, unique_colors) else: gray = pt.color_funcs.ensure_base01('lightgray') unique_colors = [gray] + unique_colors offset = max(1, min(unique_lbls)) - 1 node_to_lbl = ut.map_vals(lambda nid: max(0, nid - offset), node_to_lbl) lbl_to_color = ut.dzip(range(outof + 1), unique_colors) node_to_color = ut.map_vals(lbl_to_color, node_to_lbl) nx.set_node_attributes(graph, name='color', values=node_to_color) ut.nx_ensure_agraph_color(graph)
[docs]def graph_info(graph, ignore=None, stats=False, verbose=False): import utool as ut node_dict = nx_node_dict(graph) node_attrs = list(node_dict.values()) edge_attrs = list(ut.take_column(graph.edges(data=True), 2)) if stats: import utool with utool.embed_on_exception_context: import pandas as pd node_df = pd.DataFrame(node_attrs) edge_df = pd.DataFrame(edge_attrs) if ignore is not None: ut.delete_dict_keys(node_df, ignore) ut.delete_dict_keys(edge_df, ignore) # Not really histograms anymore try: node_attr_hist = node_df.describe().to_dict() except ValueError: node_attr_hist try: edge_attr_hist = edge_df.describe().to_dict() except ValueError: edge_attr_hist = {} key_order = ['count', 'mean', 'std', 'min', '25%', '50%', '75%', 'max'] node_attr_hist = ut.map_dict_vals( lambda x: ut.order_dict_by(x, key_order), node_attr_hist ) edge_attr_hist = ut.map_dict_vals( lambda x: ut.order_dict_by(x, key_order), edge_attr_hist ) else: node_attr_hist = ut.dict_hist(ut.flatten([attr.keys() for attr in node_attrs])) edge_attr_hist = ut.dict_hist(ut.flatten([attr.keys() for attr in edge_attrs])) if ignore is not None: ut.delete_dict_keys(edge_attr_hist, ignore) ut.delete_dict_keys(node_attr_hist, ignore) node_type_hist = ut.dict_hist(list(map(type, graph.nodes()))) info_dict = ut.odict( [ ('directed', graph.is_directed()), ('multi', graph.is_multigraph()), ('num_nodes', len(graph)), ('num_edges', len(list(graph.edges()))), ('edge_attr_hist', ut.sort_dict(edge_attr_hist)), ('node_attr_hist', ut.sort_dict(node_attr_hist)), ('node_type_hist', ut.sort_dict(node_type_hist)), ('graph_attrs', graph.graph), ('graph_name', graph.name), ] ) # unique_attrs = ut.map_dict_vals(ut.unique, ut.dict_accum(*node_attrs)) # ut.dict_isect_combine(*node_attrs)) # [list(attrs.keys())] if verbose: print(ut.repr3(info_dict)) return info_dict
[docs]def get_graph_bounding_box(graph): # import utool as ut try: import vtool as vt except ImportError: import vtool as vt # nx.get_node_attrs = nx.get_node_attributes nodes = list(graph.nodes()) # pos_list = nx_gen_node_values(graph, 'pos', nodes, default=(0, 0)) # shape_list = nx_gen_node_values(graph, 'size', nodes, default=(1, 1)) shape_list = nx_gen_node_values(graph, 'size', nodes) pos_list = nx_gen_node_values(graph, 'pos', nodes) node_extents = np.array( [ vt.extent_from_bbox(vt.bbox_from_center_wh(xy, wh)) for xy, wh in zip(pos_list, shape_list) ] ) tl_x, br_x, tl_y, br_y = node_extents.T extent = tl_x.min(), br_x.max(), tl_y.min(), br_y.max() bbox = vt.bbox_from_extent(extent) return bbox
[docs]def translate_graph(graph, t_xy): # import utool as ut import utool as ut node_pos_attrs = ['pos'] for attr in node_pos_attrs: attrdict = nx.get_node_attributes(graph, attr) attrdict = {node: pos + t_xy for node, pos in attrdict.items()} nx.set_node_attributes(graph, name=attr, values=attrdict) edge_pos_attrs = ['ctrl_pts', 'end_pt', 'head_lp', 'lp', 'start_pt', 'tail_lp'] ut.nx_delete_None_edge_attr(graph) for attr in edge_pos_attrs: attrdict = nx.get_edge_attributes(graph, attr) attrdict = { node: pos + t_xy if pos is not None else pos for node, pos in attrdict.items() } nx.set_edge_attributes(graph, name=attr, values=attrdict)
[docs]def translate_graph_to_origin(graph): x, y, w, h = get_graph_bounding_box(graph) translate_graph(graph, (-x, -y))
[docs]def stack_graphs(graph_list, vert=False, pad=None): import utool as ut graph_list_ = [g.copy() for g in graph_list] for g in graph_list_: translate_graph_to_origin(g) bbox_list = [get_graph_bounding_box(g) for g in graph_list_] if vert: dim1 = 3 dim2 = 2 else: dim1 = 2 dim2 = 3 dim1_list = np.array([bbox[dim1] for bbox in bbox_list]) dim2_list = np.array([bbox[dim2] for bbox in bbox_list]) if pad is None: pad = np.mean(dim1_list) / 2 offset1_list = ut.cumsum([0] + [d + pad for d in dim1_list[:-1]]) max_dim2 = max(dim2_list) offset2_list = [(max_dim2 - d2) / 2 for d2 in dim2_list] if vert: t_xy_list = [(d2, d1) for d1, d2 in zip(offset1_list, offset2_list)] else: t_xy_list = [(d1, d2) for d1, d2 in zip(offset1_list, offset2_list)] for g, t_xy in zip(graph_list_, t_xy_list): translate_graph(g, t_xy) nx.set_node_attributes(g, name='pin', values='true') new_graph = nx.compose_all(graph_list_) # pt.show_nx(new_graph, layout='custom', node_labels=False, as_directed=False) # NOQA return new_graph
[docs]def nx_contracted_nodes(G, u, v, self_loops=True, inplace=False): """ copy of networkx function with inplace modification TODO: commit to networkx """ import itertools as it if G.is_directed(): in_edges = ( (w, u, d) for w, x, d in G.in_edges(v, data=True) if self_loops or w != u ) out_edges = ( (u, w, d) for x, w, d in G.out_edges(v, data=True) if self_loops or w != u ) new_edges = it.chain(in_edges, out_edges) else: new_edges = ( (u, w, d) for x, w, d in G.edges(v, data=True) if self_loops or w != u ) if inplace: H = G new_edges = list(new_edges) else: H = G.copy() node_dict = nx_node_dict(H) v_data = node_dict[v] H.remove_node(v) H.add_edges_from(new_edges) if 'contraction' in node_dict[u]: node_dict[u]['contraction'][v] = v_data else: node_dict[u]['contraction'] = {v: v_data} return H
[docs]def approx_min_num_components(nodes, negative_edges): """ Find approximate minimum number of connected components possible Each edge represents that two nodes must be separated This code doesn't solve the problem. The problem is NP-complete and reduces to minimum clique cover (MCC). This is only an approximate solution. Not sure what the approximation ratio is. CommandLine: python -m utool.util_graph approx_min_num_components Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> nodes = [1, 2, 3, 4, 5, 6, 7, 8, 9] >>> edges = [(1, 2), (2, 3), (3, 1), >>> (4, 5), (5, 6), (6, 4), >>> (7, 8), (8, 9), (9, 7), >>> (1, 4), (4, 7), (7, 1), >>> ] >>> g_pos = nx.Graph() >>> g_pos.add_edges_from(edges) >>> g_neg = nx.complement(g_pos) >>> #import wbia.plottool as pt >>> #pt.qt4ensure() >>> #pt.show_nx(g_pos) >>> #pt.show_nx(g_neg) >>> negative_edges = g_neg.edges() >>> nodes = [1, 2, 3, 4, 5, 6, 7] >>> negative_edges = [(1, 2), (2, 3), (4, 5)] >>> result = approx_min_num_components(nodes, negative_edges) >>> print(result) 2 """ import utool as ut num = 0 g_neg = nx.Graph() g_neg.add_nodes_from(nodes) g_neg.add_edges_from(negative_edges) # Collapse all nodes with degree 0 if nx.__version__.startswith('2'): deg0_nodes = [n for n, d in g_neg.degree() if d == 0] else: deg0_nodes = [n for n, d in g_neg.degree_iter() if d == 0] for u, v in ut.itertwo(deg0_nodes): nx_contracted_nodes(g_neg, v, u, inplace=True) # g_neg = nx.contracted_nodes(g_neg, v, u, self_loops=False) # Initialize unused nodes to be everything unused = list(g_neg.nodes()) # complement of the graph contains all possible positive edges g_pos = nx.complement(g_neg) if False: from networkx.algorithms.approximation import clique maxiset, cliques = clique.clique_removal(g_pos) num = len(cliques) return num # Iterate until we have used all nodes while len(unused) > 0: # Seed a new "minimum component" num += 1 # Grab a random unused node n1 # idx1 = np.random.randint(0, len(unused)) idx1 = 0 n1 = unused[idx1] unused.remove(n1) neigbs = list(g_pos.neighbors(n1)) neigbs = ut.isect(neigbs, unused) while len(neigbs) > 0: # Find node n2, that n1 could be connected to # idx2 = np.random.randint(0, len(neigbs)) idx2 = 0 n2 = neigbs[idx2] unused.remove(n2) # Collapse negative information of n1 and n2 g_neg = nx.contracted_nodes(g_neg, n1, n2) # Compute new possible positive edges g_pos = nx.complement(g_neg) # Iterate until n1 has no more possible connections neigbs = list(g_pos.neighbors(n1)) neigbs = ut.isect(neigbs, unused) print('num = %r' % (num,)) return num
[docs]def nx_mincut_edges_weighted(G, s, t, capacity='weight'): # http://stackoverflow.com/questions/33332462/minimum-s-t-edge-cut-which-takes-edge-weight-into-consideration cut_weight, partitions = nx.minimum_cut(G, s, t, capacity=capacity) edge_cut_list = [] for p1_node in partitions[0]: for p2_node in partitions[1]: if G.has_edge(p1_node, p2_node): edge_cut_list.append((p1_node, p2_node)) # assert edge_cut_list == nx_edges_between(G, partitions[0], partitions[1]) return edge_cut_list
[docs]def weighted_diamter(graph, weight=None): if weight is None: distances = nx.all_pairs_shortest_path_length(graph) else: distances = nx.all_pairs_dijkstra_path_length(graph, weight=weight) if isinstance(distances, dict): eccentricities = (max(list(dists.values())) for node, dists in distances.items()) else: eccentricities = (max(list(dists.values())) for node, dists in distances) diameter = max(list(eccentricities)) return diameter
[docs]def mincost_diameter_augment(graph, max_cost, candidates=None, weight=None, cost=None): """ PROBLEM: Bounded Cost Minimum Diameter Edge Addition (BCMD) Args: graph (nx.Graph): input graph max_cost (float): maximum weighted diamter of the graph weight (str): key of the edge weight attribute cost (str): key of the edge cost attribute candidates (list): set of non-edges, optional, defaults to the complement of the graph Returns: None: if no solution exists list: minimum cost edges if solution exists Notes: We are given a graph G = (V, E) with an edge weight function w, an edge cost function c, an a maximum cost B. The goal is to find a set of candidate non-edges F. Let x[e] in {0, 1} denote if a non-edge e is excluded or included. minimize sum(c(e) * x[e] for e in F) such that weighted_diamter(graph.union({e for e in F if x[e]})) <= B References: https://www.cse.unsw.edu.au/~sergeg/papers/FratiGGM13isaac.pdf http://www.cis.upenn.edu/~sanjeev/papers/diameter.pdf http://dl.acm.org/citation.cfm?id=2953882 Notes: There is a 4-Approximation of the BCMD problem Running time is O((3 ** B * B ** 3 + n + log(B * n)) * B * n ** 2) This algorithm usexs a clustering approach to find a set C, of B + 1 cluster centers. Then we create a minimum height rooted tree, T = (U \subseteq V, D) so that C \subseteq U. This tree T approximates an optimal B-augmentation. Example: >>> # ENABLE_DOCTEST >>> from utool.util_graph import * # NOQA >>> import utool as ut >>> graph = nx.Graph() >>> if nx.__version__.startswith('1'): >>> nx.add_path = nx.Graph.add_path >>> nx.add_path(graph, range(6)) >>> #cost_func = lambda e: e[0] + e[1] >>> cost_func = lambda e: 1 >>> weight_func = lambda e: (e[0]) / e[1] >>> comp_graph = nx.complement(graph) >>> nx.set_edge_attributes(graph, name='cost', values={e: cost_func(e) for e in graph.edges()}) >>> nx.set_edge_attributes(graph, name='weight', values={e: weight_func(e) for e in graph.edges()}) >>> nx.set_edge_attributes(comp_graph, name='cost', values={e: cost_func(e) for e in comp_graph.edges()}) >>> nx.set_edge_attributes(comp_graph, name='weight', values={e: weight_func(e) for e in comp_graph.edges()}) >>> candidates = list(comp_graph.edges(data=True)) >>> max_cost = 2 >>> cost = 'cost' >>> weight = 'weight' >>> best_edges = mincost_diameter_augment(graph, max_cost, candidates, weight, cost) >>> print('best_edges = %r' % (best_edges,)) >>> soln_edges = greedy_mincost_diameter_augment(graph, max_cost, candidates, weight, cost) >>> print('soln_edges = %r' % (soln_edges,)) """ import utool as ut import operator as op if candidates is None: candidates = list(graph.complement().edges(data=True)) def augment_add(graph, edges): aug_graph = graph.copy() aug_graph.add_edges_from(edges) return aug_graph def solution_energy(chosen_edges): if weight is None: return len(chosen_edges) else: return sum(d[weight] for (u, v, d) in chosen_edges) variable_basis = [(0, 1) for _ in candidates] best_energy = np.inf best_soln = None soln_generator = ut.product(*variable_basis) length = reduce(op.mul, map(len, variable_basis), 1) if length > 3000: # Let the user know that it might take some time to find a solution soln_generator = ut.ProgIter( soln_generator, label='BruteForce BCMD', length=length ) # Brute force solution for x in soln_generator: chosen_edges = ut.compress(candidates, x) aug_graph = augment_add(graph, chosen_edges) total_cost = weighted_diamter(aug_graph, weight=cost) energy = solution_energy(chosen_edges) if total_cost <= max_cost: if energy < best_energy: best_energy = energy best_soln = x best_edges = ut.compress(candidates, best_soln) return best_edges
[docs]def greedy_mincost_diameter_augment( graph, max_cost, candidates=None, weight=None, cost=None ): # import utool as ut def solution_cost(graph): return weighted_diamter(graph, weight=cost) def solution_energy(chosen_edges): if weight is None: return len(chosen_edges) else: return sum(d[weight] for (u, v, d) in chosen_edges) def augment_add(graph, edges): aug_graph = graph.copy() aug_graph.add_edges_from(edges) return aug_graph def augment_remove(graph, edges): aug_graph = graph.copy() aug_graph.remove_edges_from(edges) return aug_graph base_cost = solution_cost(graph) # base_energy = 0 full_graph = augment_add(graph, candidates) full_cost = solution_cost(full_graph) # full_energy = solution_energy(candidates) def greedy_improvement(soln_graph, available_candidates, base_cost=None): """ Choose edge that results in the best improvement """ best_loss = None best_cost = None best_energy = None best_e = None best_graph = None for e in available_candidates: aug_graph = augment_add(soln_graph, [e]) aug_cost = solution_cost(aug_graph) aug_energy = solution_energy([e]) # We don't want to go over if possible aug_loss = max(aug_cost - max_cost, 0) if best_loss is None or aug_loss <= best_loss: if best_energy is None or aug_energy < best_energy: best_loss = aug_loss best_e = e best_graph = aug_graph best_cost = aug_cost best_energy = aug_energy if best_e is None: return None else: return best_cost, best_graph, best_energy, best_e import warnings if full_cost > max_cost: warnings.warn('no feasible solution') else: soln_graph = graph.copy() available_candidates = candidates[:] soln_edges = [] soln_energy = 0 soln_cost = base_cost # Add edges to the solution until the cost is feasible while soln_cost > max_cost and len(available_candidates): tup = greedy_improvement(soln_graph, available_candidates, soln_cost) if tup is None: warnings.warn('no improvement found') break soln_cost, soln_graph, best_energy, best_e = tup soln_energy += best_energy soln_edges.append(best_e) available_candidates.remove(best_e) # Check to see we can remove edges while maintaining feasibility for e in soln_edges[:]: aug_graph = augment_remove(soln_graph, [e]) aug_cost = solution_cost(aug_graph) if aug_cost <= soln_cost: soln_cost = aug_cost soln_graph = aug_graph soln_edges.remove(e) return soln_edges
if __name__ == '__main__': r""" CommandLine: python -m utool.util_graph --allexamples """ import multiprocessing multiprocessing.freeze_support() # for win32 import utool as ut # NOQA ut.doctest_funcs()