Skip to content

Commit 7f1357a

Browse files
authored
Merge pull request #2 from SingleRust/dev-leiden-optimization
Dev leiden optimization
2 parents 4912762 + 81eb521 commit 7f1357a

File tree

8 files changed

+807
-224
lines changed

8 files changed

+807
-224
lines changed

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.4.0"
3+
version = "0.5.0"
44
edition = "2024"
55
authors = ["Ian F. Diks"]
66
homepage = "https://singlerust.com"

src/community_search/leiden/optimizer.rs

Lines changed: 116 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ use anyhow::Ok;
44
use num_traits::Float;
55
use rand::{Rng, SeedableRng, seq::SliceRandom};
66
use rand_chacha::ChaCha8Rng;
7-
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
87
use single_utilities::traits::FloatOpsTS;
98

109
use crate::{
1110
community_search::leiden::{ConsiderComms, LeidenConfig, partition::VertexPartition},
12-
network::{Network, grouping::NetworkGrouping},
11+
network::{CSRNetwork, grouping::NetworkGrouping},
1312
};
1413

1514
#[derive(Debug, Clone)]
@@ -168,53 +167,111 @@ impl LeidenOptimizer {
168167
}
169168

170169
fn find_best_community_move<N, G, P>(
171-
&self,
172-
v: usize,
173-
v_comm: usize,
174-
comms: &[usize],
175-
partitions: &[P],
176-
layer_weights: &[N],
177-
max_comm_size: Option<usize>,
178-
) -> anyhow::Result<(usize, N)>
179-
where
180-
N: FloatOpsTS + 'static,
181-
G: NetworkGrouping,
182-
P: VertexPartition<N, G>,
183-
{
184-
let mut max_comm = v_comm;
185-
let mut max_improv = if let Some(max_size) = max_comm_size {
186-
if max_size < partitions[0].csize(v_comm) {
187-
<N as Float>::neg_infinity()
188-
} else {
189-
N::from(10.0).unwrap() * <N as Float>::epsilon()
190-
}
170+
&self,
171+
v: usize,
172+
v_comm: usize,
173+
comms: &[usize],
174+
partitions: &mut [P], // Changed to mutable slice
175+
layer_weights: &[N],
176+
max_comm_size: Option<usize>,
177+
) -> anyhow::Result<(usize, N)>
178+
where
179+
N: FloatOpsTS + 'static,
180+
G: NetworkGrouping,
181+
P: VertexPartition<N, G>,
182+
{
183+
let mut max_comm = v_comm;
184+
let time = Instant::now();
185+
// println!("Finding best community move: {:?}", time.elapsed());
186+
187+
// Pre-compute these values once instead of in the loop
188+
let v_comm_size = partitions[0].csize(v_comm);
189+
let epsilon_threshold = N::from(10.0).unwrap() * <N as Float>::epsilon();
190+
191+
let mut max_improv = if let Some(max_size) = max_comm_size {
192+
if max_size < v_comm_size {
193+
<N as Float>::neg_infinity()
191194
} else {
192-
N::from(10.0).unwrap() * <N as Float>::epsilon()
193-
};
195+
epsilon_threshold
196+
}
197+
} else {
198+
epsilon_threshold
199+
};
194200

195-
let v_size = 1; // assuming unsit size
201+
const V_SIZE: usize = 1; // Made it a const for better optimization
196202

197-
for &comm in comms {
198-
if let Some(max_size) = max_comm_size {
199-
if max_size < partitions[0].csize(comm) + v_size {
200-
continue;
201-
}
202-
}
203+
// Early exit if no communities to check
204+
if comms.is_empty() {
205+
return Ok((max_comm, max_improv));
206+
}
207+
208+
// println!("Prefiltering valid comms {:?}", time.elapsed());
209+
// Pre-filter communities by size constraint to avoid repeated checks
210+
let valid_comms: Vec<usize> = if let Some(max_size) = max_comm_size {
211+
comms
212+
.iter()
213+
.copied()
214+
.filter(|&comm| partitions[0].csize(comm) + V_SIZE <= max_size)
215+
.collect()
216+
} else {
217+
comms.to_vec()
218+
};
219+
// println!("Filtered valid comms: {:?}", time.elapsed());
220+
221+
// Early exit if no valid communities
222+
if valid_comms.is_empty() {
223+
return Ok((max_comm, max_improv));
224+
}
203225

226+
// Optimized single-layer case
227+
if partitions.len() == 1 && layer_weights[0] == N::one() {
228+
// println!("checking valid comms: {:?}", time.elapsed());
229+
230+
// Get mutable reference to the single partition
231+
let partition = &mut partitions[0];
232+
233+
for &comm in &valid_comms {
234+
let t = Instant::now();
235+
let possible_improv = partition.diff_move(v, comm);
236+
// println!("Executed diff move, took: {:?}", t.elapsed());
237+
238+
if possible_improv > max_improv {
239+
max_comm = comm;
240+
max_improv = possible_improv;
241+
}
242+
}
243+
} else {
244+
// Multi-layer case
245+
for &comm in &valid_comms {
204246
let mut possible_improv = N::zero();
205-
for (layer, partition) in partitions.iter().enumerate() {
206-
let layer_improv = partition.diff_move(v, comm);
207-
possible_improv += layer_weights[layer] * layer_improv;
247+
248+
for layer_idx in 0..partitions.len() {
249+
// Get mutable reference to current partition
250+
let layer_improv = partitions[layer_idx].diff_move(v, comm);
251+
possible_improv += layer_weights[layer_idx] * layer_improv;
252+
253+
// Early termination optimization
254+
if possible_improv + epsilon_threshold < max_improv {
255+
let remaining_positive = layer_weights[layer_idx + 1..]
256+
.iter()
257+
.all(|&w| w >= N::zero());
258+
259+
if remaining_positive && layer_improv <= N::zero() {
260+
break;
261+
}
262+
}
208263
}
209264

210265
if possible_improv > max_improv {
211266
max_comm = comm;
212267
max_improv = possible_improv;
213268
}
214269
}
215-
Ok((max_comm, max_improv))
216270
}
217271

272+
Ok((max_comm, max_improv))
273+
}
274+
218275
fn collect_candidate_communities<N, G, P>(
219276
&mut self,
220277
v: usize,
@@ -330,7 +387,7 @@ impl LeidenOptimizer {
330387
P: VertexPartition<N, G>,
331388
{
332389
let time = Instant::now();
333-
println!("MOVE_NODES | Starting | time: {:?}", time.elapsed());
390+
// println!("MOVE_NODES | Starting | time: {:?}", time.elapsed());
334391
if partitions.is_empty() {
335392
return Ok(N::from(-1.0).unwrap());
336393
}
@@ -366,10 +423,10 @@ impl LeidenOptimizer {
366423
let mut comm_added = vec![false; partitions[0].community_count()];
367424
let mut comms = Vec::new();
368425

369-
println!(
370-
"MOVE_NODES | Basic setup finished... | time: {:?}",
371-
time.elapsed()
372-
);
426+
// println!(
427+
// "MOVE_NODES | Basic setup finished... | time: {:?}",
428+
// time.elapsed()
429+
// );
373430
let mut i: i32 = 0;
374431
while let Some(v) = vertex_order.pop_front() {
375432
// println!(
@@ -394,24 +451,24 @@ impl LeidenOptimizer {
394451

395452
self.collect_candidate_communities(
396453
v,
397-
&partitions,
454+
partitions,
398455
consider_comms,
399456
&mut comms,
400457
&mut comm_added,
401458
);
402459

403-
//println!(
404-
// "MOVE_NODES | Found all candidates | time: {:?} | iteration: {:?}",
405-
// time.elapsed(),
406-
// i
407-
//);
460+
// println!(
461+
// "MOVE_NODES | Found all candidates | time: {:?} | iteration: {:?}",
462+
// time.elapsed(),
463+
// i
464+
// );
408465

409466
if consider_empty_community && partitions[0].cnodes(v_comm) > 1 {
410-
//println!(
411-
// "MOVE_NODES | Considering empty move | time: {:?} | iteration: {:?}",
412-
// time.elapsed(),
413-
// i
414-
//);
467+
// println!(
468+
// "MOVE_NODES | Considering empty move | time: {:?} | iteration: {:?}",
469+
// time.elapsed(),
470+
// i
471+
// );
415472
let n_comms_before = partitions[0].community_count();
416473
let empty_comm = partitions[0].get_empty_community();
417474
comms.push(empty_comm);
@@ -668,7 +725,7 @@ impl LeidenOptimizer {
668725
v: usize,
669726
v_comm: usize,
670727
comms: &[usize],
671-
partitions: &[P],
728+
partitions: &mut [P],
672729
layer_weights: &[N],
673730
max_comm_size: Option<usize>,
674731
) -> anyhow::Result<(usize, N)>
@@ -698,7 +755,7 @@ impl LeidenOptimizer {
698755
}
699756

700757
let mut possible_improvement = N::zero();
701-
for (layer, partition) in partitions.iter().enumerate() {
758+
for (layer, partition) in partitions.iter_mut().enumerate() {
702759
let layer_improv = partition.diff_move(v, comm);
703760
possible_improvement += layer_weights[layer] * layer_improv;
704761
}
@@ -823,7 +880,7 @@ impl LeidenOptimizer {
823880

824881
let mut possible_improv = N::zero();
825882

826-
for (layer, partition) in partitions.iter().enumerate() {
883+
for (layer, partition) in partitions.iter_mut().enumerate() {
827884
let layer_imrpov = partition.diff_move(v, comm);
828885
possible_improv += layer_weights[layer] * layer_imrpov;
829886
}
@@ -910,9 +967,9 @@ impl LeidenOptimizer {
910967
for layer in 0..nb_layers {
911968
let collapsed_network = collapsed_partitions[layer]
912969
.network()
913-
.create_reduced_network(sub_collapsed_partitions[layer].grouping());
970+
.aggregate(sub_collapsed_partitions[layer].grouping());
914971
let refined_membership = sub_collapsed_partitions[layer].membership_vector();
915-
let mut new_membership = vec![0; collapsed_network.nodes()];
972+
let mut new_membership = vec![0; collapsed_network.node_count()];
916973

917974
for v in 0..collapsed_partitions[layer].node_count() {
918975
let refined_comm = refined_membership[v];
@@ -939,9 +996,7 @@ impl LeidenOptimizer {
939996
let mut new_collapsed_partitions = Vec::new();
940997

941998
for partition in collapsed_partitions {
942-
let collapsed_network = partition
943-
.network()
944-
.create_reduced_network(partition.grouping());
999+
let collapsed_network = partition.network().aggregate(partition.grouping());
9451000
let new_partition = partition.create_like(collapsed_network);
9461001
new_collapsed_partitions.push(new_partition);
9471002
}
@@ -1042,7 +1097,7 @@ impl LeidenOptimizer {
10421097
}
10431098

10441099
// Initialize collapsed structures - start with original partitions
1045-
let mut collapsed_partitions: Vec<P> = partitions.iter().cloned().collect();
1100+
let mut collapsed_partitions: Vec<P> = partitions.to_vec();
10461101
let mut is_collapsed_membership_fixed = is_membership_fixed.to_vec();
10471102
let mut aggregate_node_per_individual_node: Vec<usize> = (0..n).collect();
10481103
let mut is_first_iteration = true;
@@ -1172,7 +1227,7 @@ impl LeidenOptimizer {
11721227
Ok(improvement)
11731228
}
11741229

1175-
pub fn find_partition<N, G, P>(&mut self, network: Network<N, N>) -> anyhow::Result<P>
1230+
pub fn find_partition<N, G, P>(&mut self, network: CSRNetwork<N, N>) -> anyhow::Result<P>
11761231
where
11771232
N: FloatOpsTS + 'static,
11781233
G: NetworkGrouping + Clone + Default,

src/community_search/leiden/partition/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashSet;
22

33
use single_utilities::traits::FloatOpsTS;
44

5-
use crate::network::{grouping::NetworkGrouping, Network};
5+
use crate::network::{grouping::NetworkGrouping, CSRNetwork};
66

77
mod modularity;
88
pub use modularity::ModularityPartition;
@@ -14,19 +14,19 @@ where
1414
N: FloatOpsTS + 'static,
1515
G: NetworkGrouping,
1616
{
17-
fn create_partition(network: Network<N, N>) -> Self;
17+
fn create_partition(network: CSRNetwork<N, N>) -> Self;
1818

19-
fn create_with_membership(network: Network<N, N>, membership: &[usize]) -> Self;
19+
fn create_with_membership(network: CSRNetwork<N, N>, membership: &[usize]) -> Self;
2020

21-
fn create_like(&self, network: Network<N, N>) -> Self;
21+
fn create_like(&self, network: CSRNetwork<N, N>) -> Self;
2222

23-
fn create_like_with_membership(&self, network: Network<N, N>, membership: &[usize]) -> Self;
23+
fn create_like_with_membership(&self, network: CSRNetwork<N, N>, membership: &[usize]) -> Self;
2424

2525
fn quality(&self) -> N;
2626

27-
fn diff_move(&self, node: usize, new_community: usize) -> N;
27+
fn diff_move(&mut self, node: usize, new_community: usize) -> N;
2828

29-
fn network(&self) -> &Network<N, N>;
29+
fn network(&self) -> &CSRNetwork<N, N>;
3030

3131
fn grouping(&self) -> &G;
3232

@@ -51,7 +51,7 @@ where
5151
}
5252

5353
fn node_count(&self) -> usize {
54-
self.network().nodes()
54+
self.network().node_count()
5555
}
5656

5757
fn community_count(&self) -> usize {

0 commit comments

Comments
 (0)