Skip to content

Commit 0760e20

Browse files
author
Ian
committed
corrected louvain clustering approach, now louvain clustering in beta
1 parent c2dc496 commit 0760e20

3 files changed

Lines changed: 103 additions & 78 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "single-clustering"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2024"
55
authors = ["Ian F. Diks"]
66
homepage = "https://singlerust.com"

src/moving/standard.rs

Lines changed: 101 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ where
4343
self.neighboring_clusters.resize(size, 0);
4444
}
4545

46+
fn calculate_modularity_gain(
47+
&self,
48+
node_weight: T,
49+
k_i_in: T,
50+
cluster_tot: T,
51+
total_edge_weight_2m: T,
52+
) -> T {
53+
// ΔQ = [Σin + ki,in]/2m - [(Σtot + ki)/2m]² - [Σin/2m - (Σtot/2m)² - (ki/2m)²]
54+
// Simplifying: ΔQ = ki,in/2m - ki*Σtot/2m² - ki²/2m²
55+
// Further: ΔQ = (ki,in - ki*Σtot/2m - ki²/2m) / 2m
56+
57+
let two_m = total_edge_weight_2m;
58+
let term1 = k_i_in / two_m;
59+
let term2 = (node_weight * cluster_tot * self.resolution) / (two_m * two_m);
60+
61+
term1 - term2
62+
}
63+
4664
pub fn iterate<C, R>(
4765
&mut self,
4866
network: &Network<T, T>,
@@ -53,119 +71,126 @@ where
5371
C: NetworkGrouping,
5472
R: RngCore,
5573
{
56-
let mut update = false;
5774
let node_count = network.nodes();
58-
self.ensure_capacity(node_count);
75+
if node_count == 0 {
76+
return false;
77+
}
5978

60-
self.cluster_weights.fill(T::zero());
61-
self.nodes_per_cluster.fill(0);
62-
self.edge_weight_per_cluster.fill(T::zero());
79+
self.ensure_capacity(node_count);
80+
81+
// Initialize cluster weights and counts
82+
self.cluster_weights[..node_count].fill(T::zero());
83+
self.nodes_per_cluster[..node_count].fill(0);
84+
self.edge_weight_per_cluster[..node_count].fill(T::zero());
6385

86+
// Calculate initial cluster weights
6487
for i in 0..node_count {
6588
let cluster = clustering.get_group(i);
6689
self.cluster_weights[cluster] += network.weight(i);
6790
self.nodes_per_cluster[cluster] += 1;
6891
}
6992

93+
// Find unused clusters
7094
let mut num_unused_clusters = 0;
7195
for i in (0..node_count).rev() {
7296
if self.nodes_per_cluster[i] == 0 {
7397
self.unused_clusters[num_unused_clusters] = i;
7498
num_unused_clusters += 1;
7599
}
76100
}
77-
//println!("Number of unused clusters: {:?}", num_unused_clusters);
78101

102+
// Shuffle node order once per iteration
79103
self.node_order.clear();
80104
self.node_order.extend(0..node_count);
81105
self.node_order.shuffle(rng);
82106

83-
let total_edge_weight =
84-
T::from_f64(network.get_total_edge_weight().to_f64().unwrap()).unwrap();
85-
let mut num_unstable_nodes = node_count;
86-
let mut i = 0;
87-
88-
//println!("Total edge weight: {:?}, num unstable nodes: {:?}, i = {:?}", total_edge_weight.to_f64().unwrap(), num_unstable_nodes, i);
107+
let total_edge_weight_2m = network.get_total_edge_weight() * T::from(2).unwrap();
108+
let mut global_update = false;
89109

90-
while num_unstable_nodes > 0 {
91-
//println!("ITERATION | Total edge weight: {:?}, num unstable nodes: {:?}, i = {:?}", total_edge_weight.to_f64().unwrap(), num_unstable_nodes, i);
92-
let node = self.node_order[i];
93-
let current_cluster = clustering.get_group(node);
94-
//println!("ITERATION | Node: {:?}, Current Cluster: {:?}", node, current_cluster);
110+
// Keep iterating until no improvements are found
111+
let mut local_improvement = true;
112+
while local_improvement {
113+
local_improvement = false;
95114

96-
// Remove node from current cluster
97-
let node_weight = T::from_f64(network.weight(node).to_f64().unwrap()).unwrap();
98-
self.cluster_weights[current_cluster] -= node_weight;
99-
self.nodes_per_cluster[current_cluster] -= 1;
115+
for &node in &self.node_order {
116+
let current_cluster = clustering.get_group(node);
117+
let node_weight = network.weight(node);
100118

101-
if self.nodes_per_cluster[current_cluster] == 0 {
102-
self.unused_clusters[num_unused_clusters] = current_cluster;
103-
num_unused_clusters += 1;
104-
//println!("ITERATION | Nodes per cluster == 0, num unused clusters {:?}", num_unused_clusters);
105-
}
119+
// Remove node from current cluster
120+
self.cluster_weights[current_cluster] -= node_weight;
121+
self.nodes_per_cluster[current_cluster] -= 1;
106122

107-
// Find neighboring clusters
108-
self.neighboring_clusters[0] = self.unused_clusters[num_unused_clusters - 1];
109-
let mut num_neighboring_clusters = 1;
110-
111-
for (target, weight) in network.neighbors(node) {
112-
let neighbor_cluster = clustering.get_group(target);
113-
let edge_weight = T::from_f64(weight.to_f64().unwrap()).unwrap();
114-
//println!("ITERATION | FORLOOP | target: {:?}, neighbor cluster: {:?}, edge_weight: {:?}", target, neighbor_cluster, edge_weight.to_f64().unwrap());
115-
if self.edge_weight_per_cluster[neighbor_cluster] == T::zero() {
116-
//println!("ITERATION | FORLOOP | Is T::zero, edge_weight_per_cluster");
117-
self.neighboring_clusters[num_neighboring_clusters] = neighbor_cluster;
118-
num_neighboring_clusters += 1;
123+
if self.nodes_per_cluster[current_cluster] == 0 {
124+
self.unused_clusters[num_unused_clusters] = current_cluster;
125+
num_unused_clusters += 1;
119126
}
120-
self.edge_weight_per_cluster[neighbor_cluster] += edge_weight;
121-
}
122127

123-
// Find best cluster
124-
let mut best_cluster = current_cluster;
125-
let mut max_quality_increment = self.edge_weight_per_cluster[current_cluster]
126-
- (node_weight * self.cluster_weights[current_cluster] * self.resolution)
127-
/ (T::from_f64(2.0).unwrap() * total_edge_weight);
128-
129-
//println!("ITERATION | Best Cluster {:?} Max Quality Increment {:?}", best_cluster, max_quality_increment.to_f64().unwrap());
130-
131-
for &cluster in &self.neighboring_clusters[..num_neighboring_clusters] {
132-
let quality_increment = self.edge_weight_per_cluster[cluster]
133-
- (node_weight * self.cluster_weights[cluster] * self.resolution)
134-
/ num_traits::Float::powi(T::from_f64(2.0).unwrap() * total_edge_weight, 2);
135-
//println!("ITERATION | Cluster {:?} Quality Increment {:?}", cluster, quality_increment.to_f64().unwrap());
136-
if quality_increment > max_quality_increment
137-
|| (quality_increment == max_quality_increment && cluster < best_cluster)
138-
{
139-
best_cluster = cluster;
140-
max_quality_increment = quality_increment;
141-
//println!("ITERATION | Passes if for quality improvement")
128+
// Clear edge weights from previous iteration
129+
self.edge_weight_per_cluster[..node_count].fill(T::zero());
130+
131+
// Find neighboring clusters and calculate edge weights
132+
self.neighboring_clusters[0] = if num_unused_clusters > 0 {
133+
self.unused_clusters[num_unused_clusters - 1]
134+
} else {
135+
current_cluster // fallback
136+
};
137+
let mut num_neighboring_clusters = 1;
138+
139+
for (target, weight) in network.neighbors(node) {
140+
let neighbor_cluster = clustering.get_group(target);
141+
142+
if self.edge_weight_per_cluster[neighbor_cluster] == T::zero() &&
143+
neighbor_cluster != self.neighboring_clusters[0] {
144+
self.neighboring_clusters[num_neighboring_clusters] = neighbor_cluster;
145+
num_neighboring_clusters += 1;
146+
}
147+
self.edge_weight_per_cluster[neighbor_cluster] += weight;
142148
}
143-
self.edge_weight_per_cluster[cluster] = T::zero();
144-
}
145149

146-
// Update cluster assignment
147-
self.cluster_weights[best_cluster] += node_weight;
148-
self.nodes_per_cluster[best_cluster] += 1;
150+
// Find best cluster
151+
let mut best_cluster = current_cluster;
152+
let mut max_quality_increment = self.calculate_modularity_gain(
153+
node_weight,
154+
self.edge_weight_per_cluster[current_cluster],
155+
self.cluster_weights[current_cluster],
156+
total_edge_weight_2m,
157+
);
158+
159+
for &cluster in &self.neighboring_clusters[..num_neighboring_clusters] {
160+
let quality_increment = self.calculate_modularity_gain(
161+
node_weight,
162+
self.edge_weight_per_cluster[cluster],
163+
self.cluster_weights[cluster],
164+
total_edge_weight_2m,
165+
);
166+
167+
if quality_increment > max_quality_increment ||
168+
(quality_increment == max_quality_increment && cluster < best_cluster) {
169+
best_cluster = cluster;
170+
max_quality_increment = quality_increment;
171+
}
172+
}
149173

150-
if best_cluster == self.unused_clusters[num_unused_clusters - 1] {
151-
num_unused_clusters -= 1;
152-
}
174+
// Update cluster assignment
175+
self.cluster_weights[best_cluster] += node_weight;
176+
self.nodes_per_cluster[best_cluster] += 1;
153177

154-
num_unstable_nodes -= 1;
178+
if best_cluster == self.unused_clusters[num_unused_clusters - 1] {
179+
num_unused_clusters -= 1;
180+
}
155181

156-
if best_cluster != current_cluster {
157-
clustering.set_group(node, best_cluster);
158-
update = true;
182+
if best_cluster != current_cluster {
183+
clustering.set_group(node, best_cluster);
184+
local_improvement = true;
185+
global_update = true;
186+
}
159187
}
160-
161-
i = (i + 1) % node_count;
162-
//println!("END: {:?} best cluster {:?} num unstable nodes {:?} num unused clusters {:?}", i, best_cluster, num_unstable_nodes, num_unused_clusters)
163188
}
164189

165-
if update {
190+
if global_update {
166191
clustering.normalize_groups();
167192
}
168193

169-
update
194+
global_update
170195
}
171196
}

0 commit comments

Comments
 (0)