def get_layered_layout( binary_tree: nx.DiGraph, min_allowed_margin: float = 1 ) -> tp.Dict[tp.Hashable, tp.Tuple[float, float]]: if not nx.is_tree(binary_tree): raise nx.NotATree() assert all( binary_tree.out_degree(node) <= 2 for node in binary_tree.nodes()), "Binary tree expected!" roots = _find_roots(binary_tree) assert len(roots) == 1, "Rooted tree expected!" root = roots[0] layout = _get_basic_layered_layout(binary_tree, root) layout = _ensure_margins_between_vertices_of_the_same_depth( binary_tree, root, layout, min_allowed_margin) layout = _center_layered_layout(binary_tree, root, layout) return layout
def to_nested_tuple(T, root, canonical_form=False): """Returns a nested tuple representation of the given tree. The nested tuple representation of a tree is defined recursively. The tree with one node and no edges is represented by the empty tuple, ``()``. A tree with ``k`` subtrees is represented by a tuple of length ``k`` in which each element is the nested tuple representation of a subtree. Parameters ---------- T : NetworkX graph An undirected graph object representing a tree. root : node The node in ``T`` to interpret as the root of the tree. canonical_form : bool If ``True``, each tuple is sorted so that the function returns a canonical form for rooted trees. This means "lighter" subtrees will appear as nested tuples before "heavier" subtrees. In this way, each isomorphic rooted tree has the same nested tuple representation. Returns ------- tuple A nested tuple representation of the tree. Notes ----- This function is *not* the inverse of :func:`from_nested_tuple`; the only guarantee is that the rooted trees are isomorphic. See also -------- from_nested_tuple to_prufer_sequence Examples -------- The tree need not be a balanced binary tree:: >>> T = nx.Graph() >>> T.add_edges_from([(0, 1), (0, 2), (0, 3)]) >>> T.add_edges_from([(1, 4), (1, 5)]) >>> T.add_edges_from([(3, 6), (3, 7)]) >>> root = 0 >>> nx.to_nested_tuple(T, root) (((), ()), (), ((), ())) Continuing the above example, if ``canonical_form`` is ``True``, the nested tuples will be sorted:: >>> nx.to_nested_tuple(T, root, canonical_form=True) ((), ((), ()), ((), ())) Even the path graph can be interpreted as a tree:: >>> T = nx.path_graph(4) >>> root = 0 >>> nx.to_nested_tuple(T, root) ((((),),),) """ def _make_tuple(T, root, _parent): """Recursively compute the nested tuple representation of the given rooted tree. ``_parent`` is the parent node of ``root`` in the supertree in which ``T`` is a subtree, or ``None`` if ``root`` is the root of the supertree. This argument is used to determine which neighbors of ``root`` are children and which is the parent. """ # Get the neighbors of `root` that are not the parent node. We # are guaranteed that `root` is always in `T` by construction. children = set(T[root]) - {_parent} if len(children) == 0: return () nested = (_make_tuple(T, v, root) for v in children) if canonical_form: nested = sorted(nested) return tuple(nested) # Do some sanity checks on the input. if not nx.is_tree(T): raise nx.NotATree('provided graph is not a tree') if root not in T: raise nx.NodeNotFound('Graph {} contains no node {}'.format(T, root)) return _make_tuple(T, root, None)
def to_prufer_sequence(T): r"""Returns the Prüfer sequence of the given tree. A *Prüfer sequence* is a list of *n* - 2 numbers between 0 and *n* - 1, inclusive. The tree corresponding to a given Prüfer sequence can be recovered by repeatedly joining a node in the sequence with a node with the smallest potential degree according to the sequence. Parameters ---------- T : NetworkX graph An undirected graph object representing a tree. Returns ------- list The Prüfer sequence of the given tree. Raises ------ NetworkXPointlessConcept If the number of nodes in `T` is less than two. NotATree If `T` is not a tree. KeyError If the set of nodes in `T` is not {0, …, *n* - 1}. Notes ----- There is a bijection from labeled trees to Prüfer sequences. This function is the inverse of the :func:`from_prufer_sequence` function. Sometimes Prüfer sequences use nodes labeled from 1 to *n* instead of from 0 to *n* - 1. This function requires nodes to be labeled in the latter form. You can use :func:`~networkx.relabel_nodes` to relabel the nodes of your tree to the appropriate format. This implementation is from [1]_ and has a running time of :math:`O(n \log n)`. See also -------- to_nested_tuple from_prufer_sequence References ---------- .. [1] Wang, Xiaodong, Lei Wang, and Yingjie Wu. "An optimal algorithm for Prufer codes." *Journal of Software Engineering and Applications* 2.02 (2009): 111. <http://dx.doi.org/10.4236/jsea.2009.22016> Examples -------- There is a bijection between Prüfer sequences and labeled trees, so this function is the inverse of the :func:`from_prufer_sequence` function:: >>> edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)] >>> tree = nx.Graph(edges) >>> sequence = nx.to_prufer_sequence(tree) >>> sequence [3, 3, 3, 4] >>> tree2 = nx.from_prufer_sequence(sequence) >>> list(tree2.edges()) == edges True """ # Perform some sanity checks on the input. n = len(T) if n < 2: msg = 'Prüfer sequence undefined for trees with fewer than two nodes' raise nx.NetworkXPointlessConcept(msg) if not nx.is_tree(T): raise nx.NotATree('provided graph is not a tree') if set(T) != set(range(n)): raise KeyError('tree must have node labels {0, ..., n - 1}') degree = dict(T.degree()) def parents(u): return next(v for v in T[u] if degree[v] > 1) index = u = min(k for k in range(n) if degree[k] == 1) result = [] for i in range(n - 2): v = parents(u) result.append(v) degree[v] -= 1 if v < index and degree[v] == 1: u = v else: index = u = min(k for k in range(index + 1, n) if degree[k] == 1) return result
def lukes_partitioning(G, max_size: int, node_weight=None, edge_weight=None) -> list: """Optimal partitioning of a weighted tree using the Lukes algorithm. This algorithm partitions a connected, acyclic graph featuring integer node weights and float edge weights. The resulting clusters are such that the total weight of the nodes in each cluster does not exceed max_size and that the weight of the edges that are cut by the partition is minimum. The algorithm is based on LUKES[1]. Parameters ---------- G : graph max_size : int Maximum weight a partition can have in terms of sum of node_weight for all nodes in the partition edge_weight : key Edge data key to use as weight. If None, the weights are all set to one. node_weight : key Node data key to use as weight. If None, the weights are all set to one. The data must be int. Returns ------- partition : list A list of sets of nodes representing the clusters of the partition. Raises ------ NotATree If G is not a tree. TypeError If any of the values of node_weight is not int. References ---------- .. Lukes, J. A. (1974). "Efficient Algorithm for the Partitioning of Trees." IBM Journal of Research and Development, 18(3), 217–224. """ # First sanity check and tree preparation if not nx.is_tree(G): raise nx.NotATree("lukes_partitioning works only on trees") else: if nx.is_directed(G): root = [n for n, d in G.in_degree() if d == 0] assert len(root) == 1 root = root[0] t_G = deepcopy(G) else: root = choice(list(G.nodes)) # this has the desirable side effect of not inheriting attributes t_G = nx.dfs_tree(G, root) # Since we do not want to screw up the original graph, # if we have a blank attribute, we make a deepcopy if edge_weight is None or node_weight is None: safe_G = deepcopy(G) if edge_weight is None: nx.set_edge_attributes(safe_G, D_EDGE_VALUE, D_EDGE_W) edge_weight = D_EDGE_W if node_weight is None: nx.set_node_attributes(safe_G, D_NODE_VALUE, D_NODE_W) node_weight = D_NODE_W else: safe_G = G # Second sanity check # The values of node_weight MUST BE int. # I cannot see any room for duck typing without incurring serious # danger of subtle bugs. all_n_attr = nx.get_node_attributes(safe_G, node_weight).values() for x in all_n_attr: if not isinstance(x, int): raise TypeError("lukes_partitioning needs integer " f"values for node_weight ({node_weight})") # SUBROUTINES ----------------------- # these functions are defined here for two reasons: # - brevity: we can leverage global "safe_G" # - caching: signatures are hashable @not_implemented_for("undirected") # this is intended to be called only on t_G def _leaves(gr): for x in gr.nodes: if not nx.descendants(gr, x): yield x @not_implemented_for("undirected") def _a_parent_of_leaves_only(gr): tleaves = set(_leaves(gr)) for n in set(gr.nodes) - tleaves: if all([x in tleaves for x in nx.descendants(gr, n)]): return n @lru_cache(CLUSTER_EVAL_CACHE_SIZE) def _value_of_cluster(cluster: frozenset): valid_edges = [ e for e in safe_G.edges if e[0] in cluster and e[1] in cluster ] return sum([safe_G.edges[e][edge_weight] for e in valid_edges]) def _value_of_partition(partition: list): return sum([_value_of_cluster(frozenset(c)) for c in partition]) @lru_cache(CLUSTER_EVAL_CACHE_SIZE) def _weight_of_cluster(cluster: frozenset): return sum([safe_G.nodes[n][node_weight] for n in cluster]) def _pivot(partition: list, node): ccx = [c for c in partition if node in c] assert len(ccx) == 1 return ccx[0] def _concatenate_or_merge(partition_1: list, partition_2: list, x, i, ref_weigth): ccx = _pivot(partition_1, x) cci = _pivot(partition_2, i) merged_xi = ccx.union(cci) # We first check if we can do the merge. # If so, we do the actual calculations, otherwise we concatenate if _weight_of_cluster(frozenset(merged_xi)) <= ref_weigth: cp1 = list(filter(lambda x: x != ccx, partition_1)) cp2 = list(filter(lambda x: x != cci, partition_2)) option_2 = [merged_xi] + cp1 + cp2 return option_2, _value_of_partition(option_2) else: option_1 = partition_1 + partition_2 return option_1, _value_of_partition(option_1) # INITIALIZATION ----------------------- leaves = set(_leaves(t_G)) for lv in leaves: t_G.nodes[lv][PKEY] = dict() slot = safe_G.nodes[lv][node_weight] t_G.nodes[lv][PKEY][slot] = [{lv}] t_G.nodes[lv][PKEY][0] = [{lv}] for inner in [x for x in t_G.nodes if x not in leaves]: t_G.nodes[inner][PKEY] = dict() slot = safe_G.nodes[inner][node_weight] t_G.nodes[inner][PKEY][slot] = [{inner}] # CORE ALGORITHM ----------------------- while True: x_node = _a_parent_of_leaves_only(t_G) weight_of_x = safe_G.nodes[x_node][node_weight] best_value = 0 best_partition = None bp_buffer = dict() x_descendants = nx.descendants(t_G, x_node) for i_node in x_descendants: for j in range(weight_of_x, max_size + 1): for a, b in _split_n_from(j, weight_of_x): if (a not in t_G.nodes[x_node][PKEY].keys() or b not in t_G.nodes[i_node][PKEY].keys()): # it's not possible to form this particular weight sum continue part1 = t_G.nodes[x_node][PKEY][a] part2 = t_G.nodes[i_node][PKEY][b] part, value = _concatenate_or_merge( part1, part2, x_node, i_node, j) if j not in bp_buffer.keys() or bp_buffer[j][1] < value: # we annotate in the buffer the best partition for j bp_buffer[j] = part, value # we also keep track of the overall best partition if best_value <= value: best_value = value best_partition = part # as illustrated in Lukes, once we finished a child, we can # discharge the partitions we found into the graph # (the key phrase is make all x == x') # so that they are used by the subsequent children for w, (best_part_for_vl, vl) in bp_buffer.items(): t_G.nodes[x_node][PKEY][w] = best_part_for_vl bp_buffer.clear() # the absolute best partition for this node # across all weights has to be stored at 0 t_G.nodes[x_node][PKEY][0] = best_partition t_G.remove_nodes_from(x_descendants) if x_node == root: # the 0-labeled partition of root # is the optimal one for the whole tree return t_G.nodes[root][PKEY][0]
def to_s_expression(T, root, canonical_form=False): """Modified version of networkx.to_nested_tuple(). Returns a nested tuple representation of the given tree. The nested tuple representation of a tree is defined recursively. The tree with one node and no edges is represented by the empty tuple, ``()``. A tree with ``k`` subtrees is represented by a tuple of length ``k`` in which each element is the nested tuple representation of a subtree. Parameters ---------- T : NetworkX graph An undirected graph object representing a tree. root : node The node in ``T`` to interpret as the root of the tree. canonical_form : bool If ``True``, each tuple is sorted so that the function returns a canonical form for rooted trees. This means "lighter" subtrees will appear as nested tuples before "heavier" subtrees. In this way, each isomorphic rooted tree has the same nested tuple representation. Returns ------- tuple A nested list representation of the tree. """ def _make_tuple(T, root, _parent, nested): """Recursively compute the nested tuple representation of the given rooted tree. ``_parent`` is the parent node of ``root`` in the supertree in which ``T`` is a subtree, or ``None`` if ``root`` is the root of the supertree. This argument is used to determine which neighbors of ``root`` are children and which is the parent. """ # Get the neighbors of `root` that are not the parent node. We # are guaranteed that `root` is always in `T` by construction. # T[root] are the adjacent nodes of root # children are a subset of this children = [n for n in T[root] if n != _parent] if len(children) == 0: return [T.node[root]['label']] nested = [T.node[root]['label'] ] + [_make_tuple(T, v, root, nested) for v in children] # if canonical_form: # nested = sorted(nested) return nested # Do some sanity checks on the input. if not nx.is_tree(T): raise nx.NotATree('provided graph is not a tree') if root not in T: raise nx.NodeNotFound('Graph {} contains no node {}'.format(T, root)) return _make_tuple(T, root, (), None)