about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py227
1 files changed, 227 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py
new file mode 100644
index 00000000..08dd7cd5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py
@@ -0,0 +1,227 @@
+"""Lukes Algorithm for exact optimal weighted tree partitioning."""
+
+from copy import deepcopy
+from functools import lru_cache
+from random import choice
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["lukes_partitioning"]
+
+D_EDGE_W = "weight"
+D_EDGE_VALUE = 1.0
+D_NODE_W = "weight"
+D_NODE_VALUE = 1
+PKEY = "partitions"
+CLUSTER_EVAL_CACHE_SIZE = 2048
+
+
+def _split_n_from(n, min_size_of_first_part):
+    # splits j in two parts of which the first is at least
+    # the second argument
+    assert n >= min_size_of_first_part
+    for p1 in range(min_size_of_first_part, n + 1):
+        yield p1, n - p1
+
+
+@nx._dispatchable(node_attrs="node_weight", edge_attrs="edge_weight")
+def lukes_partitioning(G, max_size, node_weight=None, edge_weight=None):
+    """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 [1]_.
+
+    Parameters
+    ----------
+    G : NetworkX 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
+    ----------
+    .. [1] 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):
+        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):
+        return sum(_value_of_cluster(frozenset(c)) for c in partition)
+
+    @lru_cache(CLUSTER_EVAL_CACHE_SIZE)
+    def _weight_of_cluster(cluster):
+        return sum(safe_G.nodes[n][node_weight] for n in cluster)
+
+    def _pivot(partition, node):
+        ccx = [c for c in partition if node in c]
+        assert len(ccx) == 1
+        return ccx[0]
+
+    def _concatenate_or_merge(partition_1, partition_2, x, i, ref_weight):
+        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_weight:
+            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] = {}
+        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] = {}
+        slot = safe_G.nodes[inner][node_weight]
+        t_G.nodes[inner][PKEY][slot] = [{inner}]
+    nx._clear_cache(t_G)
+
+    # 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 = {}
+        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]
+                        or b not in t_G.nodes[i_node][PKEY]
+                    ):
+                        # 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 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]