Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions graphify/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def cluster(
final_communities: list[list[str]] = []
for nodes in raw.values():
if len(nodes) > max_size:
final_communities.extend(_split_community(G, nodes))
final_communities.extend(_split_community(G, nodes, resolution))
else:
final_communities.append(nodes)

Expand All @@ -220,7 +220,7 @@ def cluster(
second_pass: list[list[str]] = []
for nodes in final_communities:
if len(nodes) >= _COHESION_SPLIT_MIN_SIZE and cohesion_score(G, nodes) < _COHESION_SPLIT_THRESHOLD:
splits = _split_community(G, nodes)
splits = _split_community(G, nodes, resolution)
second_pass.extend(splits if len(splits) > 1 else [nodes])
else:
second_pass.append(nodes)
Expand All @@ -236,14 +236,19 @@ def cluster(
return {i: sorted(nodes) for i, nodes in enumerate(final_communities)}


def _split_community(G: nx.Graph, nodes: list[str]) -> list[list[str]]:
"""Run a second Leiden pass on a community subgraph to split it further."""
def _split_community(G: nx.Graph, nodes: list[str], resolution: float = 1.0) -> list[list[str]]:
"""Run a second Leiden pass on a community subgraph to split it further.

``resolution`` mirrors the value cluster() partitioned the whole graph with,
so --resolution keeps its meaning on the split passes instead of silently
reverting to 1.0 here.
"""
subgraph = G.subgraph(nodes)
if subgraph.number_of_edges() == 0:
# No edges - split into individual nodes
return [[n] for n in sorted(nodes)]
try:
sub_partition = _partition(subgraph)
sub_partition = _partition(subgraph, resolution=resolution)
sub_communities: dict[int, list[str]] = {}
for node, cid in sub_partition.items():
sub_communities.setdefault(cid, []).append(node)
Expand Down
40 changes: 40 additions & 0 deletions tests/test_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,43 @@ def test_remap_communities_to_previous_assigns_deterministic_new_ids():
assert list(remapped.keys()) == [0, 1]
assert remapped[0] == ["x", "y", "z"]
assert remapped[1] == ["m"]


def test_split_community_forwards_resolution_to_partition(monkeypatch):
from graphify import cluster as cluster_mod

seen: list[float] = []

def fake_partition(G, resolution=1.0):
seen.append(resolution)
return {n: 0 for n in G.nodes}

monkeypatch.setattr(cluster_mod, "_partition", fake_partition)
G = nx.complete_graph(6)
G = nx.relabel_nodes(G, {i: str(i) for i in G.nodes})
cluster_mod._split_community(G, list(G.nodes), resolution=3.0)
assert seen == [3.0]


def test_cluster_forwards_resolution_to_split_passes(monkeypatch):
from graphify import cluster as cluster_mod

seen: list[float] = []

def fake_split(G, nodes, resolution=1.0):
seen.append(resolution)
return [sorted(nodes)]

monkeypatch.setattr(cluster_mod, "_split_community", fake_split)

# Two 20-node cliques joined by a single edge: 40 nodes total, so max_size is
# max(10, 40 * 0.25) = 10 and each clique trips the oversized-split path.
G = nx.Graph()
for offset in (0, 20):
clique = [f"n{offset + i}" for i in range(20)]
G.add_edges_from((a, b) for i, a in enumerate(clique) for b in clique[i + 1:])
G.add_edge("n0", "n20")

cluster_mod.cluster(G, resolution=0.5)
assert seen, "expected the oversized-community split path to run"
assert set(seen) == {0.5}