Skip to content

feat(network): replace union-find stand-in with Leiden consensus - #351

Draft
seonghobae wants to merge 4 commits into
mainfrom
feat/leiden-consensus-gap-009
Draft

feat(network): replace union-find stand-in with Leiden consensus#351
seonghobae wants to merge 4 commits into
mainfrom
feat/leiden-consensus-gap-009

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

GAP-009 remainder on protected main 1bc02f580cf48e1d39da239f0e818453437c31c3. Head is 9945c9147dcc266a1eaf6b984e7a341542c302c1.

network_analysis now runs Traag, Waltman, and van Eck (2019) Leiden modularity (γ = 1) on each co-assignment replicate instead of greedy union-find. Fast local moving, refinement that keeps communities internally connected, and aggregation are the partition. Two triangles joined by a weak bridge stay two communities; union-find glued them. Isolated topics stay unclustered. Aggregation sums parallel supernode edges and keeps distinct neighbours.

Co-assignment follows Monti, Tamayo, Mesirov, and Golub (2003) and Hennig (2007) with an explicit edge_drop_probability. The drop stream is independent of the per-replicate Leiden RNG; admitted edges are sorted by endpoint; out-of-range and self-loop endpoints stay unclustered. The aggregation stop predicate covers the continue-when-moved same-size arm.

Not a graphical lasso, not a causal cluster, and not an export workflow. No Buyer language. Do not merge without two independent APPROVE reviews and exact-head Checks on this SHA. Predecessor-head evidence is non-passing.

Doctoring: docs/research/leiden-consensus.md.

GAP-009 remainder: Traag, Waltman, and van Eck (2019) Leiden modularity
(γ = 1) now partitions each co-assignment replicate. Fast local moving,
refinement that keeps communities well-connected, and aggregation replace
greedy union-find. Two cliques joined by a weak bridge stay two
communities. Isolated topics stay unclustered. The Monti (2003) / Hennig
(2007) consensus wrapper is unchanged. Not a graphical lasso, not a
causal cluster, and not an export workflow.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

network_analysis의 consensus clustering이 greedy union-find에서 Traag et al. (2019) Leiden modularity partitioning으로 전환되었습니다. 그래프 필터링, 커뮤니티 연결성, 결정적 난수 처리, 고립 토픽 보존을 구현하고 관련 문서와 테스트를 추가했습니다.

Changes

Leiden 합의 클러스터링

Layer / File(s) Summary
Leiden 알고리즘과 그래프 처리
crates/network_analysis/src/leiden.rs
유효한 양의 edge만 사용하는 그래프 구성, Newman–Girvan modularity 기반 local moving, refinement, disconnected splitting, aggregation, dense label projection을 추가했습니다. 빈 그래프, 중복 edge, weak bridge, seed 재현성, 고립 토픽 및 각 단계의 테스트를 추가했습니다.
Consensus clustering 통합
crates/network_analysis/src/consensus.rs, crates/network_analysis/src/lib.rs
각 perturbation replicate가 공유 LCG 상태와 함께 leiden_partition을 호출하도록 변경했습니다. 기존 private union-find helper를 제거하고 weak bridge consensus 테스트를 추가했습니다. leiden 모듈을 등록했습니다.
알고리즘 문서와 추적성
CHANGELOG.md, docs/TRACEABILITY.md, docs/research/compositional-cluster-pair-gates.md, docs/research/leiden-consensus.md, docs/research/standards-and-literature.md
Leiden consensus의 입력 필터, modularity γ = 1, 고립 토픽 처리, 제외 범위, 검증 시나리오 및 참고문헌을 문서화했습니다. 추적성 상태를 partial로 갱신했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 8d81e

Replacing the previous clustering behavior changes consensus partitions, but the current implementation can incorrectly classify topics when edges reference invalid endpoints and can produce different consensus results when equivalent edges are reordered. These reproducibility and correctness risks should be fixed or explicitly accepted before merge; citation metadata in the accompanying research documentation also needs correction.

Sequence Diagram(s)

sequenceDiagram
  participant consensus_clusters
  participant leiden_partition
  participant co_assignment
  participant ClusterOutput
  consensus_clusters->>leiden_partition: 각 replicate의 surviving edges 분할
  leiden_partition->>co_assignment: replicate별 community labels 전달
  co_assignment->>ClusterOutput: co-assignment threshold 적용
  ClusterOutput-->>consensus_clusters: consensus clusters 반환
Loading

Suggested reviewers: cursoragent

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 3 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 union-find 대체와 Leiden consensus 도입이라는 주요 변경 사항을 정확하고 간결하게 설명합니다.
Full details: Docstring Coverage

Explanation

Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 3 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/leiden-consensus-gap-009

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +346 to +356
let sub_total = sub_strength.iter().sum::<f64>() / 2.0;
if sub_total <= 0.0 {
continue;
}
let subgraph = Graph {
node_count: members.len(),
adjacency: sub_adj,
strength: sub_strength,
total_weight: sub_total,
};
move_nodes_fast(&subgraph, &mut sub_membership, rng);

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Refinement freezes improvable communities

When a parent has internal edges, refine_partition rescales modularity to its induced subgraph and can keep every node merged. Aggregation then hides profitable node moves, returning a lower-modularity partition.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread crates/network_analysis/src/leiden.rs
Comment on lines +379 to +405
for node in 0..graph.node_count {
strength[refined[node]] += graph.strength[node];
let source = refined[node];
for &(neighbor, weight) in &graph.adjacency[node] {
let target = refined[neighbor];
if source < target {
adjacency[source].push((target, weight));
adjacency[target].push((source, weight));
}
}
}
for neighbors in &mut adjacency {
neighbors.sort_by_key(|left| left.0);
neighbors.dedup_by(|later, kept| {
if later.0 == kept.0 {
kept.1 += later.1;
true
} else {
false
}
});
}
let aggregate = Graph {
node_count: refined_count,
adjacency,
strength,
total_weight: graph.total_weight,

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Aggregate self-loops are intentionally implicit

aggregate_graph omits internal edges but preserves their endpoint strengths and global weight. The current gain formula therefore retains their modularity contribution without explicit self-loops.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

coderabbitai[bot]

This comment was marked as resolved.

cargo fmt --check failed on network_analysis re-export order. The
best_delta <= 0 stay-guard was unreachable once best_community left
the source community, so nightly unique-branch coverage could not
close. Source-out-of-range edges now exercise the other bound check.
…orts

Aggregation never saw two edges between the same supernode pair, so the
dedup merge arm stayed uncovered. Direct aggregate_graph recovery now
sums parallel supernode weights and keeps a distinct neighbour. The
connectedness oracle covers disconnected communities and singletons.
Re-exports match rustfmt type-before-function order.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

Devin Review

best_community = candidate;
}
}
if best_community == from {

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Removed guard was redundant

best_community changes only when delta exceeds the zero-initialized best_delta. Every accepted move therefore still has positive modularity gain.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 new potential issues.

Devin Review

Comment on lines +648 to +680
fn aggregate_sums_parallel_supernode_edges_and_keeps_distinct_neighbors() {
// Two crossing edges between supernodes 0 and 1 plus one edge
// from 1 to 2. Aggregation must merge the parallel pair and keep
// the distinct neighbour.
let owned = vec![
edge(0, 1, 1.0),
edge(2, 3, 1.0),
edge(4, 5, 1.0),
edge(0, 2, 0.3),
edge(1, 3, 0.4),
edge(2, 4, 0.2),
];
let edges = refs(&owned);
let graph = Graph::from_edges(&edges, 6);
let refined = vec![0, 0, 1, 1, 2, 2];
let parent = refined.clone();
let leaf_members: Vec<Vec<usize>> = (0..6).map(|node| vec![node]).collect();
let (aggregate, next_membership, next_leaves) =
aggregate_graph(&graph, &parent, &refined, &leaf_members);
assert_eq!(aggregate.node_count, 3);
assert_eq!(aggregate.adjacency[0].len(), 1);
assert_eq!(aggregate.adjacency[0][0].0, 1);
assert!((aggregate.adjacency[0][0].1 - 0.7).abs() < 1e-15);
assert_eq!(aggregate.adjacency[1].len(), 2);
assert_eq!(aggregate.adjacency[1][0].0, 0);
assert_eq!(aggregate.adjacency[1][1].0, 2);
assert!((aggregate.adjacency[1][0].1 - 0.7).abs() < 1e-15);
assert!((aggregate.adjacency[1][1].1 - 0.2).abs() < 1e-15);
assert_eq!(next_membership, vec![0, 1, 2]);
assert_eq!(next_leaves[0], vec![0, 1]);
assert_eq!(next_leaves[1], vec![2, 3]);
assert_eq!(next_leaves[2], vec![4, 5]);
}

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Aggregation coverage is structurally independent

The fixture directly constructs parallel aggregate edges and a distinct neighbor. Its assertions isolate edge summation and neighbor preservation from partition randomness.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +682 to +689
#[test]
fn connectedness_oracle_rejects_a_disconnected_community() {
let owned = vec![edge(0, 1, 1.0), edge(2, 3, 1.0)];
let edges = refs(&owned);
assert!(!communities_connected(&edges, &[0, 0, 0, 0]));
assert!(communities_connected(&edges, &[0, 0, 1, 1]));
assert!(communities_connected(&edges, &[0, 1, 2, 3]));
}

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Connectedness check has an independent control

The disconnected fixture forces the oracle’s rejection path. Separate connected-pair and singleton assignments prevent a vacuous always-false result.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

The unique-branch gate failed 3949/3950 on the false arm of `!moved` when
aggregation did not shrink. The stop predicate is now a named helper with
both arms recovered (Traag, Waltman, & van Eck, 2019). Consensus drop
sampling no longer shares the Leiden LCG; admitted edges are sorted by
endpoint; out-of-range and self-loop endpoints stay unclustered. APA 7th
Monti, Tamayo, Mesirov, and Golub (2003) coauthors and Hennig (2007)
pages 258–271 are restored.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Devin Review

&& edge.source != edge.target
})
.collect();
admitted.sort_by_key(|edge| (edge.source, edge.target));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Parallel-edge order changes consensus

When duplicate endpoint pairs carry different effects, sort_by_key preserves their input order and assigns fixed drop draws to different weights. Reordering equivalent edge lists can therefore change consensus clusters.

Suggested change
admitted.sort_by_key(|edge| (edge.source, edge.target));
admitted.sort_by(|left, right| {
left.source
.cmp(&right.source)
.then(left.target.cmp(&right.target))
.then_with(|| left.effect.total_cmp(&right.effect))
});
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +699 to +709
#[test]
fn aggregation_continues_when_nodes_moved_on_a_same_size_level() {
// Unique-branch coverage of the `!moved` false arm: refinement
// did not shrink the graph, but local moving still changed the
// partition, so Leiden must not stop (Traag et al., 2019).
assert!(aggregation_level_is_stable(4, 4, false));
assert!(!aggregation_level_is_stable(4, 4, true));
assert!(!aggregation_level_is_stable(2, 4, false));
assert!(!aggregation_level_is_stable(1, 4, true));
assert!(aggregation_level_is_stable(1, 1, false));
assert!(!aggregation_level_is_stable(1, 1, true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Continuation path lacks integration coverage

The test invokes only aggregation_level_is_stable with synthetic inputs. It cannot detect failures reaching this branch or carrying its updated partition forward.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Hour-20 exact-head review request.

Current head 9945c9147dcc266a1eaf6b984e7a341542c302c1. Devin/CodeRabbit/author COMMENTED is not independent APPROVE. OpenCode DISMISSED/REQUEST_CHANGES on predecessor heads does not transfer. Ruleset 18156473 needs two independent APPROVE reviews on this exact SHA.

Draft GAP-009 Leiden. Do not convert a failing head into a second Leiden PR.

Do not self-approve. Do not --admin merge. Checks/reviews are not a reason to weaken fail-closed gates.

This was referenced Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Hour-35 exact-head review request on draft Leiden.

Current head 9945c9147dcc266a1eaf6b984e7a341542c302c1. Do not duplicate Leiden or Driver p.16 std-family. Do not merge without two independent APPROVE reviews and exact-head Checks on this SHA.

Author/bot COMMENTED is not independent APPROVE. Copilot review requests are not APPROVE. Predecessor-head evidence is non-passing. Do not self-approve.

@opencode-agent review

Copy link
Copy Markdown
Contributor Author

Hour-36 re-verify: draft Leiden head 9945c91. Zero independent APPROVEs. Do not duplicate Leiden. Exact-head Checks on 9945c91 only. Do not self-approve. @opencode-agent review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant