1use scirs2_core::ndarray::Array2;
12
13use crate::error::{GraphError, Result};
14
15use super::spectral::spectral_bisect;
16use super::types::{PartitionConfig, PartitionResult};
17
18#[derive(Debug, Clone)]
20struct CoarseLevel {
21 adj: Array2<f64>,
23 mapping: Vec<usize>,
25 n_nodes: usize,
27}
28
29fn coarsen(adj: &Array2<f64>, threshold: usize) -> Vec<CoarseLevel> {
37 let mut levels = Vec::new();
38 let mut current = adj.clone();
39
40 loop {
41 let n = current.nrows();
42 if n <= threshold {
43 break;
44 }
45
46 let mut matched = vec![false; n];
49 let mut mapping = vec![0usize; n];
50 let mut coarse_id = 0usize;
51
52 let mut node_order: Vec<usize> = (0..n).collect();
54 node_order.sort_by(|&a, &b| {
55 let deg_a: f64 = (0..n).map(|j| current[[a, j]].abs()).sum();
56 let deg_b: f64 = (0..n).map(|j| current[[b, j]].abs()).sum();
57 deg_a
58 .partial_cmp(°_b)
59 .unwrap_or(std::cmp::Ordering::Equal)
60 });
61
62 for &i in &node_order {
63 if matched[i] {
64 continue;
65 }
66
67 let mut best_j: Option<usize> = None;
69 let mut best_w = 0.0f64;
70 for j in 0..n {
71 if j == i || matched[j] {
72 continue;
73 }
74 let w = current[[i, j]].abs();
75 if w > 1e-15 && w > best_w {
76 best_w = w;
77 best_j = Some(j);
78 }
79 }
80
81 if let Some(j) = best_j {
82 mapping[i] = coarse_id;
83 mapping[j] = coarse_id;
84 matched[i] = true;
85 matched[j] = true;
86 coarse_id += 1;
87 } else {
88 mapping[i] = coarse_id;
90 matched[i] = true;
91 coarse_id += 1;
92 }
93 }
94
95 let cn = coarse_id;
96 if cn >= n {
97 break;
99 }
100
101 let mut coarse_adj = Array2::<f64>::zeros((cn, cn));
103 for i in 0..n {
104 for j in (i + 1)..n {
105 let w = current[[i, j]];
106 if w.abs() > 1e-15 {
107 let ci = mapping[i];
108 let cj = mapping[j];
109 if ci != cj {
110 coarse_adj[[ci, cj]] += w;
111 coarse_adj[[cj, ci]] += w;
112 }
113 }
114 }
115 }
116
117 levels.push(CoarseLevel {
118 adj: coarse_adj.clone(),
119 mapping,
120 n_nodes: cn,
121 });
122
123 current = coarse_adj;
124 }
125
126 levels
127}
128
129fn initial_partition(adj: &Array2<f64>, config: &PartitionConfig) -> Result<Vec<usize>> {
133 let n = adj.nrows();
134 if n < 2 {
135 return Ok(vec![0; n]);
136 }
137
138 match spectral_bisect(adj, config) {
140 Ok(result) => Ok(result.assignments),
141 Err(_) => {
142 let mut indices: Vec<usize> = (0..n).collect();
144 indices.sort_by(|&a, &b| {
145 let deg_a: f64 = (0..n).map(|j| adj[[a, j]].abs()).sum();
146 let deg_b: f64 = (0..n).map(|j| adj[[b, j]].abs()).sum();
147 deg_b
148 .partial_cmp(°_a)
149 .unwrap_or(std::cmp::Ordering::Equal)
150 });
151 let mut assignments = vec![0usize; n];
152 let half = n / 2;
153 for &idx in &indices[half..] {
154 assignments[idx] = 1;
155 }
156 Ok(assignments)
157 }
158 }
159}
160
161fn kernighan_lin_pass(adj: &Array2<f64>, partition: &mut [usize], max_swaps: usize) -> usize {
168 let n = adj.nrows();
169 if n < 2 {
170 return 0;
171 }
172
173 let compute_d = |part: &[usize], node: usize| -> f64 {
176 let my_part = part[node];
177 let mut ext = 0.0;
178 let mut int = 0.0;
179 for j in 0..n {
180 if j == node {
181 continue;
182 }
183 let w = adj[[node, j]].abs();
184 if w < 1e-15 {
185 continue;
186 }
187 if part[j] == my_part {
188 int += w;
189 } else {
190 ext += w;
191 }
192 }
193 ext - int
194 };
195
196 let original_cut = count_edge_cut(adj, partition);
197 let mut d_values: Vec<f64> = (0..n).map(|i| compute_d(partition, i)).collect();
198
199 let mut locked = vec![false; n];
200 let mut gains = Vec::new();
201 let mut swap_pairs = Vec::new();
202
203 let effective_swaps = max_swaps.min(n / 2);
204
205 for _ in 0..effective_swaps {
206 let mut best_gain = f64::NEG_INFINITY;
208 let mut best_a: Option<usize> = None;
209 let mut best_b: Option<usize> = None;
210
211 for a in 0..n {
212 if locked[a] || partition[a] != 0 {
213 continue;
214 }
215 for b in 0..n {
216 if locked[b] || partition[b] != 1 {
217 continue;
218 }
219 let gain = d_values[a] + d_values[b] - 2.0 * adj[[a, b]].abs();
220 if gain > best_gain {
221 best_gain = gain;
222 best_a = Some(a);
223 best_b = Some(b);
224 }
225 }
226 }
227
228 let (a, b) = match (best_a, best_b) {
229 (Some(a), Some(b)) => (a, b),
230 _ => break,
231 };
232
233 gains.push(best_gain);
235 swap_pairs.push((a, b));
236 locked[a] = true;
237 locked[b] = true;
238
239 partition[a] = 1;
241 partition[b] = 0;
242 for i in 0..n {
243 if !locked[i] {
244 d_values[i] = compute_d(partition, i);
245 }
246 }
247 }
248
249 let mut cumulative = 0.0f64;
251 let mut best_cumulative = 0.0f64;
252 let mut best_k = 0usize; for (k, &gain) in gains.iter().enumerate() {
255 cumulative += gain;
256 if cumulative > best_cumulative {
257 best_cumulative = cumulative;
258 best_k = k + 1;
259 }
260 }
261
262 for &(a, b) in swap_pairs.iter().rev() {
264 partition[a] = 0;
265 partition[b] = 1;
266 }
267
268 for &(a, b) in swap_pairs.iter().take(best_k) {
270 partition[a] = 1;
271 partition[b] = 0;
272 }
273
274 let new_cut = count_edge_cut(adj, partition);
275 original_cut.saturating_sub(new_cut)
276}
277
278fn count_edge_cut(adj: &Array2<f64>, partition: &[usize]) -> usize {
280 let n = adj.nrows();
281 let mut cut = 0usize;
282 for i in 0..n {
283 for j in (i + 1)..n {
284 if adj[[i, j]].abs() > 1e-15 && partition[i] != partition[j] {
285 cut += 1;
286 }
287 }
288 }
289 cut
290}
291
292fn uncoarsen_with_refinement(
297 levels: &[CoarseLevel],
298 coarse_partition: Vec<usize>,
299 original_adj: &Array2<f64>,
300 config: &PartitionConfig,
301) -> Result<Vec<usize>> {
302 if levels.is_empty() {
303 return Ok(coarse_partition);
304 }
305
306 let mut partition = coarse_partition;
307
308 for level_idx in (0..levels.len()).rev() {
310 let level = &levels[level_idx];
311
312 let fine_n = level.mapping.len();
314 let mut fine_partition = vec![0usize; fine_n];
315 for (fine_node, &coarse_node) in level.mapping.iter().enumerate() {
316 if coarse_node < partition.len() {
317 fine_partition[fine_node] = partition[coarse_node];
318 }
319 }
320
321 let adj_at_level = if level_idx == 0 {
323 original_adj.clone()
324 } else {
325 levels[level_idx - 1].adj.clone()
326 };
327
328 for _ in 0..config.kl_max_passes {
330 let improvement = kernighan_lin_pass(&adj_at_level, &mut fine_partition, fine_n / 2);
331 if improvement == 0 {
332 break;
333 }
334 }
335
336 partition = fine_partition;
337 }
338
339 Ok(partition)
340}
341
342pub fn multilevel_partition(
362 adj: &Array2<f64>,
363 config: &PartitionConfig,
364) -> Result<PartitionResult> {
365 let n = adj.nrows();
366 let k = config.n_partitions;
367
368 if n < 2 {
369 return Err(GraphError::InvalidParameter {
370 param: "adj".to_string(),
371 value: format!("{}x{}", n, n),
372 expected: "at least 2x2 adjacency matrix".to_string(),
373 context: "multilevel_partition".to_string(),
374 });
375 }
376
377 if k < 2 {
378 return Err(GraphError::InvalidParameter {
379 param: "n_partitions".to_string(),
380 value: format!("{}", k),
381 expected: "at least 2".to_string(),
382 context: "multilevel_partition".to_string(),
383 });
384 }
385
386 if k > n {
387 return Err(GraphError::InvalidParameter {
388 param: "n_partitions".to_string(),
389 value: format!("{}", k),
390 expected: format!("at most {} (number of nodes)", n),
391 context: "multilevel_partition".to_string(),
392 });
393 }
394
395 if k == 2 {
396 return multilevel_bisect(adj, config);
397 }
398
399 recursive_multilevel_partition(adj, config)
401}
402
403fn multilevel_bisect(adj: &Array2<f64>, config: &PartitionConfig) -> Result<PartitionResult> {
405 let n = adj.nrows();
406
407 let levels = coarsen(adj, config.coarsening_threshold);
409
410 let coarsest_adj = if levels.is_empty() {
412 adj.clone()
413 } else {
414 levels
415 .last()
416 .map(|l| l.adj.clone())
417 .unwrap_or_else(|| adj.clone())
418 };
419
420 let coarse_partition = initial_partition(&coarsest_adj, config)?;
421
422 let assignments = if levels.is_empty() {
424 let mut part = coarse_partition;
425 for _ in 0..config.kl_max_passes {
427 let improvement = kernighan_lin_pass(adj, &mut part, n / 2);
428 if improvement == 0 {
429 break;
430 }
431 }
432 part
433 } else {
434 uncoarsen_with_refinement(&levels, coarse_partition, adj, config)?
435 };
436
437 Ok(PartitionResult::from_assignments(&assignments, adj, 2))
438}
439
440fn recursive_multilevel_partition(
442 adj: &Array2<f64>,
443 config: &PartitionConfig,
444) -> Result<PartitionResult> {
445 let n = adj.nrows();
446 let k = config.n_partitions;
447
448 let mut assignments = vec![0usize; n];
449 let mut queue: Vec<(usize, Vec<usize>)> = vec![(0, (0..n).collect())];
451 let mut next_id = 1usize;
452
453 while next_id < k {
454 if queue.is_empty() {
455 break;
456 }
457
458 let mut largest_idx = 0;
460 let mut largest_size = 0;
461 for (i, (_, nodes)) in queue.iter().enumerate() {
462 if nodes.len() > largest_size {
463 largest_size = nodes.len();
464 largest_idx = i;
465 }
466 }
467
468 if largest_size < 2 {
469 break;
470 }
471
472 let (pid, nodes) = queue.remove(largest_idx);
473 let sub_n = nodes.len();
474
475 let mut sub_adj = Array2::<f64>::zeros((sub_n, sub_n));
477 for (si, &ni) in nodes.iter().enumerate() {
478 for (sj, &nj) in nodes.iter().enumerate() {
479 sub_adj[[si, sj]] = adj[[ni, nj]];
480 }
481 }
482
483 let bisect_config = PartitionConfig {
485 n_partitions: 2,
486 ..config.clone()
487 };
488 let sub_result = multilevel_bisect(&sub_adj, &bisect_config)?;
489
490 let mut part0 = Vec::new();
491 let mut part1 = Vec::new();
492 for (si, &a) in sub_result.assignments.iter().enumerate() {
493 if a == 0 {
494 assignments[nodes[si]] = pid;
495 part0.push(nodes[si]);
496 } else {
497 assignments[nodes[si]] = next_id;
498 part1.push(nodes[si]);
499 }
500 }
501
502 queue.push((pid, part0));
503 queue.push((next_id, part1));
504 next_id += 1;
505 }
506
507 for (pid, nodes) in &queue {
509 for &ni in nodes {
510 assignments[ni] = *pid;
511 }
512 }
513
514 Ok(PartitionResult::from_assignments(&assignments, adj, k))
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use scirs2_core::ndarray::Array2;
521
522 fn path_graph(n: usize) -> Array2<f64> {
523 let mut adj = Array2::<f64>::zeros((n, n));
524 for i in 0..(n - 1) {
525 adj[[i, i + 1]] = 1.0;
526 adj[[i + 1, i]] = 1.0;
527 }
528 adj
529 }
530
531 fn two_cliques_bridge(n: usize) -> Array2<f64> {
532 let size = 2 * n;
533 let mut adj = Array2::<f64>::zeros((size, size));
534 for i in 0..n {
536 for j in (i + 1)..n {
537 adj[[i, j]] = 1.0;
538 adj[[j, i]] = 1.0;
539 }
540 }
541 for i in n..size {
543 for j in (i + 1)..size {
544 adj[[i, j]] = 1.0;
545 adj[[j, i]] = 1.0;
546 }
547 }
548 adj[[n - 1, n]] = 1.0;
550 adj[[n, n - 1]] = 1.0;
551 adj
552 }
553
554 #[test]
555 fn test_coarsening_reduces_size() {
556 let adj = path_graph(20);
557 let levels = coarsen(&adj, 5);
558 assert!(!levels.is_empty());
560 let mut prev_n = 20;
562 for level in &levels {
563 assert!(
564 level.n_nodes < prev_n,
565 "coarsening should reduce node count"
566 );
567 prev_n = level.n_nodes;
568 }
569 }
570
571 #[test]
572 fn test_kl_never_increases_edge_cut() {
573 let adj = two_cliques_bridge(5);
574 let mut partition: Vec<usize> = (0..10).map(|i| if i < 5 { 0 } else { 1 }).collect();
575 let before = count_edge_cut(&adj, &partition);
576 let improvement = kernighan_lin_pass(&adj, &mut partition, 5);
577 let after = count_edge_cut(&adj, &partition);
578 assert!(after <= before, "KL should not increase edge cut");
579 assert_eq!(improvement, before - after);
581 }
582
583 #[test]
584 fn test_kl_on_already_optimal() {
585 let n = 4;
587 let size = 2 * n;
588 let mut adj = Array2::<f64>::zeros((size, size));
589 for i in 0..n {
590 for j in (i + 1)..n {
591 adj[[i, j]] = 1.0;
592 adj[[j, i]] = 1.0;
593 }
594 }
595 for i in n..size {
596 for j in (i + 1)..size {
597 adj[[i, j]] = 1.0;
598 adj[[j, i]] = 1.0;
599 }
600 }
601 let mut partition: Vec<usize> = (0..size).map(|i| if i < n { 0 } else { 1 }).collect();
602 let improvement = kernighan_lin_pass(&adj, &mut partition, n);
603 assert_eq!(
604 improvement, 0,
605 "already-optimal partition should have 0 improvement"
606 );
607 }
608
609 #[test]
610 fn test_multilevel_bisection_two_cliques() {
611 let clique_size = 5;
612 let adj = two_cliques_bridge(clique_size);
613 let config = PartitionConfig {
614 n_partitions: 2,
615 balance_tolerance: 0.1,
616 coarsening_threshold: 4,
617 kl_max_passes: 10,
618 ..PartitionConfig::default()
619 };
620 let result = multilevel_partition(&adj, &config).expect("should succeed");
621 assert_eq!(result.partition_sizes.len(), 2);
622 assert!(
624 result.edge_cut <= clique_size,
625 "edge cut {} should be bounded for two-clique bridge",
626 result.edge_cut
627 );
628 }
629
630 #[test]
631 fn test_multilevel_kway_all_nonempty() {
632 let n = 20;
633 let adj = path_graph(n);
634 let config = PartitionConfig {
635 n_partitions: 4,
636 balance_tolerance: 0.3,
637 coarsening_threshold: 3,
638 kl_max_passes: 5,
639 ..PartitionConfig::default()
640 };
641 let result = multilevel_partition(&adj, &config).expect("should succeed");
642 assert_eq!(result.partition_sizes.len(), 4);
643 for (i, &s) in result.partition_sizes.iter().enumerate() {
644 assert!(s > 0, "partition {} should be non-empty", i);
645 }
646 }
647
648 #[test]
649 fn test_multilevel_balance_within_tolerance() {
650 let adj = two_cliques_bridge(6);
651 let config = PartitionConfig {
652 n_partitions: 2,
653 balance_tolerance: 0.1,
654 coarsening_threshold: 4,
655 kl_max_passes: 10,
656 ..PartitionConfig::default()
657 };
658 let result = multilevel_partition(&adj, &config).expect("should succeed");
659 assert!(
661 result.imbalance <= 0.5,
662 "imbalance {} should be within a reasonable bound",
663 result.imbalance
664 );
665 }
666
667 #[test]
668 fn test_multilevel_small_graph_matches_spectral() {
669 let adj = path_graph(4);
671 let config = PartitionConfig {
672 n_partitions: 2,
673 balance_tolerance: 0.1,
674 coarsening_threshold: 100, kl_max_passes: 5,
676 ..PartitionConfig::default()
677 };
678 let ml_result = multilevel_partition(&adj, &config).expect("multilevel should succeed");
679 let sp_result = super::super::spectral::spectral_bisect(&adj, &config)
680 .expect("spectral should succeed");
681
682 assert!(
684 (ml_result.edge_cut as i64 - sp_result.edge_cut as i64).unsigned_abs() <= 1,
685 "multilevel ({}) and spectral ({}) should produce similar edge cuts",
686 ml_result.edge_cut,
687 sp_result.edge_cut
688 );
689 }
690
691 #[test]
692 fn test_multilevel_invalid_params() {
693 let adj = path_graph(4);
694 let config = PartitionConfig {
695 n_partitions: 1,
696 ..PartitionConfig::default()
697 };
698 assert!(multilevel_partition(&adj, &config).is_err());
699
700 let config2 = PartitionConfig {
701 n_partitions: 10,
702 ..PartitionConfig::default()
703 };
704 assert!(multilevel_partition(&adj, &config2).is_err());
705 }
706}
707
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714#[non_exhaustive]
715pub enum CoarseningStrategy {
716 HeavyEdgeMatching,
718 RandomMatching,
720 SortedHeavyEdge,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726#[non_exhaustive]
727pub enum RefinementStrategy {
728 FiducciaMattheyses,
730 KernighanLin,
732 GreedyBalance,
734}
735
736#[derive(Debug, Clone)]
738pub struct PartitioningConfig {
739 pub n_parts: usize,
741 pub coarsening: CoarseningStrategy,
743 pub refinement: RefinementStrategy,
745 pub imbalance_factor: f64,
747 pub n_coarsen_levels: usize,
749 pub seed: u64,
751}
752
753impl Default for PartitioningConfig {
754 fn default() -> Self {
755 Self {
756 n_parts: 2,
757 coarsening: CoarseningStrategy::HeavyEdgeMatching,
758 refinement: RefinementStrategy::FiducciaMattheyses,
759 imbalance_factor: 0.03,
760 n_coarsen_levels: 10,
761 seed: 42,
762 }
763 }
764}
765
766#[derive(Debug, Clone)]
768pub struct KwayPartitionResult {
769 pub partition: Vec<usize>,
771 pub edge_cut: usize,
773 pub part_sizes: Vec<usize>,
775 pub imbalance: f64,
777}
778
779#[derive(Debug, Clone)]
781struct AdjCoarseLevel {
782 adj: Vec<Vec<(usize, f64)>>,
784 fine_adj: Vec<Vec<(usize, f64)>>,
786 mapping: Vec<usize>,
788 n_nodes: usize,
790}
791
792fn coarsen_adj_hem(
794 adj: &[Vec<(usize, f64)>],
795 strategy: CoarseningStrategy,
796 seed: u64,
797) -> AdjCoarseLevel {
798 let n = adj.len();
799 let mut matched = vec![false; n];
800 let mut mapping = vec![0usize; n];
801 let mut coarse_id = 0usize;
802
803 let mut node_order: Vec<usize> = (0..n).collect();
805
806 if strategy == CoarseningStrategy::RandomMatching {
808 let mut rng = seed
810 .wrapping_mul(6364136223846793005)
811 .wrapping_add(1442695040888963407);
812 for i in (1..n).rev() {
813 rng = rng
814 .wrapping_mul(6364136223846793005)
815 .wrapping_add(1442695040888963407);
816 let j = (rng >> 33) as usize % (i + 1);
817 node_order.swap(i, j);
818 }
819 }
820
821 for &i in &node_order {
822 if matched[i] {
823 continue;
824 }
825
826 let partner = match strategy {
828 CoarseningStrategy::HeavyEdgeMatching | CoarseningStrategy::SortedHeavyEdge => {
829 adj[i]
831 .iter()
832 .filter(|&&(nb, _)| !matched[nb] && nb != i)
833 .max_by(|&&(_, wa), &&(_, wb)| {
834 wa.partial_cmp(&wb).unwrap_or(std::cmp::Ordering::Equal)
835 })
836 .map(|&(nb, _)| nb)
837 }
838 CoarseningStrategy::RandomMatching => {
839 adj[i]
841 .iter()
842 .find(|&&(nb, _)| !matched[nb] && nb != i)
843 .map(|&(nb, _)| nb)
844 }
845 };
846
847 if let Some(j) = partner {
848 mapping[i] = coarse_id;
849 mapping[j] = coarse_id;
850 matched[i] = true;
851 matched[j] = true;
852 } else {
853 mapping[i] = coarse_id;
854 matched[i] = true;
855 }
856 coarse_id += 1;
857 }
858
859 let cn = coarse_id;
860
861 let mut coarse_adj: Vec<std::collections::HashMap<usize, f64>> =
863 vec![std::collections::HashMap::new(); cn];
864
865 for i in 0..n {
866 let ci = mapping[i];
867 for &(nb, w) in &adj[i] {
868 let cnb = mapping[nb];
869 if ci != cnb {
870 *coarse_adj[ci].entry(cnb).or_insert(0.0) += w;
871 }
872 }
873 }
874
875 let coarse_adj_list: Vec<Vec<(usize, f64)>> = coarse_adj
876 .into_iter()
877 .map(|hm| hm.into_iter().collect())
878 .collect();
879
880 AdjCoarseLevel {
881 adj: coarse_adj_list,
882 fine_adj: adj.to_vec(),
883 mapping,
884 n_nodes: cn,
885 }
886}
887
888fn adj_edge_cut(adj: &[Vec<(usize, f64)>], partition: &[usize]) -> usize {
890 let mut cut = 0usize;
891 for (i, nbrs) in adj.iter().enumerate() {
892 for &(j, _) in nbrs {
893 if j > i && partition[i] != partition[j] {
894 cut += 1;
895 }
896 }
897 }
898 cut
899}
900
901fn adj_partition_stats(partition: &[usize], n_parts: usize) -> (Vec<usize>, f64) {
903 let mut sizes = vec![0usize; n_parts];
904 for &p in partition {
905 if p < n_parts {
906 sizes[p] += 1;
907 }
908 }
909 let n = partition.len();
910 let avg = n as f64 / n_parts as f64;
911 let imbalance = if avg > 0.0 {
912 sizes
913 .iter()
914 .map(|&s| (s as f64 / avg) - 1.0)
915 .fold(f64::NEG_INFINITY, f64::max)
916 .max(0.0)
917 } else {
918 0.0
919 };
920 (sizes, imbalance)
921}
922
923fn initial_bisect_adj(adj: &[Vec<(usize, f64)>]) -> Vec<usize> {
925 let n = adj.len();
926 if n == 0 {
927 return vec![];
928 }
929 if n == 1 {
930 return vec![0];
931 }
932 let mut degrees: Vec<(usize, f64)> = adj
934 .iter()
935 .enumerate()
936 .map(|(i, nbrs)| (i, nbrs.iter().map(|&(_, w)| w).sum::<f64>()))
937 .collect();
938 degrees.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
939
940 let mut partition = vec![0usize; n];
941 let half = n / 2;
942 for (rank, &(node, _)) in degrees.iter().enumerate() {
943 partition[node] = if rank < half { 0 } else { 1 };
944 }
945 partition
946}
947
948fn fm_pass_adj(adj: &[Vec<(usize, f64)>], partition: &mut [usize], n_parts: usize) -> usize {
951 let n = adj.len();
952 if n < 2 || n_parts < 2 {
953 return 0;
954 }
955
956 let compute_gain = |p: &[usize], node: usize| -> f64 {
959 let my_part = p[node];
960 let mut gain = 0.0;
961 for &(nb, w) in &adj[node] {
962 if p[nb] == my_part {
963 gain -= w; } else {
965 gain += w; }
967 }
968 gain
969 };
970
971 let before_cut = adj_edge_cut(adj, partition);
972 let mut gains: Vec<f64> = (0..n).map(|i| compute_gain(partition, i)).collect();
973 let mut locked = vec![false; n];
974 let mut moves: Vec<(usize, usize, usize)> = Vec::new();
976 let mut move_gains: Vec<f64> = Vec::new();
977 let mut curr_sizes = vec![0usize; n_parts];
979 for &p in partition.iter() {
980 if p < n_parts {
981 curr_sizes[p] += 1;
982 }
983 }
984
985 let max_moves = n.min(200);
986
987 for _ in 0..max_moves {
988 let best = (0..n).filter(|&i| !locked[i]).max_by(|&a, &b| {
990 gains[a]
991 .partial_cmp(&gains[b])
992 .unwrap_or(std::cmp::Ordering::Equal)
993 });
994
995 let v = match best {
996 Some(v) => v,
997 None => break,
998 };
999
1000 let old_part = partition[v];
1001 if curr_sizes[old_part] <= 1 {
1003 locked[v] = true;
1004 continue;
1005 }
1006
1007 let mut best_target = (old_part + 1) % n_parts;
1009 let mut best_target_gain = f64::NEG_INFINITY;
1010
1011 for target_p in 0..n_parts {
1012 if target_p == old_part {
1013 continue;
1014 }
1015 let mut g = 0.0;
1016 for &(nb, w) in &adj[v] {
1017 if partition[nb] == old_part {
1018 g -= w;
1019 } else if partition[nb] == target_p {
1020 g += w;
1021 }
1022 }
1023 if g > best_target_gain {
1024 best_target_gain = g;
1025 best_target = target_p;
1026 }
1027 }
1028
1029 moves.push((v, old_part, best_target));
1030 move_gains.push(best_target_gain);
1031 locked[v] = true;
1032 curr_sizes[old_part] -= 1;
1033 curr_sizes[best_target] += 1;
1034 partition[v] = best_target;
1035
1036 for &(nb, _) in &adj[v] {
1038 if !locked[nb] {
1039 gains[nb] = compute_gain(partition, nb);
1040 }
1041 }
1042 }
1043
1044 let mut cumulative = 0.0f64;
1046 let mut best_cum = 0.0f64;
1047 let mut best_k = 0usize;
1048 for (ki, &g) in move_gains.iter().enumerate() {
1049 cumulative += g;
1050 if cumulative > best_cum {
1051 best_cum = cumulative;
1052 best_k = ki + 1;
1053 }
1054 }
1055
1056 for &(node, old_part, _) in moves.iter().rev() {
1058 partition[node] = old_part;
1059 }
1060
1061 for &(node, _, new_part) in moves.iter().take(best_k) {
1063 partition[node] = new_part;
1064 }
1065
1066 let after_cut = adj_edge_cut(adj, partition);
1067 before_cut.saturating_sub(after_cut)
1068}
1069
1070fn greedy_balance_pass(
1072 adj: &[Vec<(usize, f64)>],
1073 partition: &mut [usize],
1074 n_parts: usize,
1075 imbalance_target: f64,
1076) {
1077 let n = adj.len();
1078 let avg = n as f64 / n_parts as f64;
1079 let cap = (avg * (1.0 + imbalance_target)).ceil() as usize;
1080
1081 let mut sizes = vec![0usize; n_parts];
1082 for &p in partition.iter() {
1083 if p < n_parts {
1084 sizes[p] += 1;
1085 }
1086 }
1087
1088 for v in 0..n {
1090 let p = partition[v];
1091 if sizes[p] <= cap {
1092 continue;
1093 }
1094 let is_boundary = adj[v].iter().any(|&(nb, _)| partition[nb] != p);
1096 if !is_boundary {
1097 continue;
1098 }
1099
1100 let mut best_target = p;
1102 let mut best_conn = 0.0f64;
1103 for target in 0..n_parts {
1104 if target == p || sizes[target] >= cap {
1105 continue;
1106 }
1107 let conn: f64 = adj[v]
1108 .iter()
1109 .filter(|&&(nb, _)| partition[nb] == target)
1110 .map(|&(_, w)| w)
1111 .sum();
1112 if conn > best_conn {
1113 best_conn = conn;
1114 best_target = target;
1115 }
1116 }
1117
1118 if best_target != p {
1119 sizes[p] -= 1;
1120 sizes[best_target] += 1;
1121 partition[v] = best_target;
1122 }
1123 }
1124}
1125
1126fn fixup_empty_partitions(partition: &mut [usize], k: usize, n: usize) {
1129 if k == 0 || n == 0 {
1130 return;
1131 }
1132 let mut sizes = vec![0usize; k];
1133 for &p in partition.iter() {
1134 if p < k {
1135 sizes[p] += 1;
1136 }
1137 }
1138
1139 for target in 0..k {
1140 if sizes[target] > 0 {
1141 continue;
1142 }
1143 let donor = sizes
1145 .iter()
1146 .enumerate()
1147 .max_by_key(|(_, &s)| s)
1148 .map(|(i, _)| i)
1149 .unwrap_or(0);
1150
1151 if sizes[donor] < 2 {
1152 break; }
1154
1155 for p in partition.iter_mut() {
1157 if *p == donor {
1158 *p = target;
1159 sizes[donor] -= 1;
1160 sizes[target] += 1;
1161 break;
1162 }
1163 }
1164 }
1165}
1166
1167pub fn multilevel_kway(
1184 adj: &[Vec<(usize, f64)>],
1185 config: &PartitioningConfig,
1186) -> Result<KwayPartitionResult> {
1187 let n = adj.len();
1188 let k = config.n_parts;
1189
1190 if n < 2 {
1191 return Err(GraphError::InvalidParameter {
1192 param: "adj".to_string(),
1193 value: format!("{}", n),
1194 expected: "at least 2 nodes".to_string(),
1195 context: "multilevel_kway".to_string(),
1196 });
1197 }
1198
1199 if k < 2 {
1200 return Err(GraphError::InvalidParameter {
1201 param: "n_parts".to_string(),
1202 value: format!("{}", k),
1203 expected: "at least 2".to_string(),
1204 context: "multilevel_kway".to_string(),
1205 });
1206 }
1207
1208 if k > n {
1209 return Err(GraphError::InvalidParameter {
1210 param: "n_parts".to_string(),
1211 value: format!("{}", k),
1212 expected: format!("at most {} (number of nodes)", n),
1213 context: "multilevel_kway".to_string(),
1214 });
1215 }
1216
1217 let mut levels: Vec<AdjCoarseLevel> = Vec::new();
1219 let mut coarsen_adj = adj.to_vec();
1220
1221 for _ in 0..config.n_coarsen_levels {
1222 if coarsen_adj.len() < k * 2 {
1223 break;
1224 }
1225 let level = coarsen_adj_hem(&coarsen_adj, config.coarsening, config.seed);
1226 if level.n_nodes >= coarsen_adj.len() {
1227 break; }
1229 coarsen_adj = level.adj.clone();
1230 levels.push(level);
1231 if coarsen_adj.len() < k * 2 {
1232 break;
1233 }
1234 }
1235
1236 let coarsest_n = coarsen_adj.len();
1238 let mut partition = if k == 2 {
1239 initial_bisect_adj(&coarsen_adj)
1240 } else {
1241 recursive_bisection_adj(&coarsen_adj, k, config.seed)
1243 };
1244
1245 if partition.len() != coarsest_n {
1247 partition.resize(coarsest_n, 0);
1248 }
1249
1250 for p in &mut partition {
1252 *p = (*p).min(k - 1);
1253 }
1254
1255 match config.refinement {
1257 RefinementStrategy::FiducciaMattheyses => {
1258 for _ in 0..3 {
1259 let imp = fm_pass_adj(&coarsen_adj, &mut partition, k);
1260 if imp == 0 {
1261 break;
1262 }
1263 }
1264 }
1265 RefinementStrategy::KernighanLin => {
1266 for _ in 0..2 {
1268 let imp = fm_pass_adj(&coarsen_adj, &mut partition, k);
1269 if imp == 0 {
1270 break;
1271 }
1272 }
1273 }
1274 RefinementStrategy::GreedyBalance => {
1275 greedy_balance_pass(&coarsen_adj, &mut partition, k, config.imbalance_factor);
1276 }
1277 }
1278
1279 for level in levels.iter().rev() {
1284 let fine_n = level.mapping.len(); let mut fine_partition = vec![0usize; fine_n];
1286 for (fi, &ci) in level.mapping.iter().enumerate() {
1287 if ci < partition.len() {
1288 fine_partition[fi] = partition[ci];
1289 }
1290 }
1291
1292 for p in &mut fine_partition {
1294 *p = (*p).min(k - 1);
1295 }
1296
1297 match config.refinement {
1299 RefinementStrategy::FiducciaMattheyses => {
1300 for _ in 0..5 {
1301 let imp = fm_pass_adj(&level.fine_adj, &mut fine_partition, k);
1302 if imp == 0 {
1303 break;
1304 }
1305 }
1306 }
1307 RefinementStrategy::KernighanLin => {
1308 for _ in 0..3 {
1309 let imp = fm_pass_adj(&level.fine_adj, &mut fine_partition, k);
1310 if imp == 0 {
1311 break;
1312 }
1313 }
1314 }
1315 RefinementStrategy::GreedyBalance => {
1316 greedy_balance_pass(
1317 &level.fine_adj,
1318 &mut fine_partition,
1319 k,
1320 config.imbalance_factor,
1321 );
1322 }
1323 }
1324
1325 partition = fine_partition;
1326 }
1327
1328 if partition.len() != n {
1331 partition.resize(n, 0);
1332 }
1333
1334 for p in &mut partition {
1336 *p = (*p).min(k - 1);
1337 }
1338
1339 fixup_empty_partitions(&mut partition, k, n);
1341
1342 match config.refinement {
1344 RefinementStrategy::FiducciaMattheyses | RefinementStrategy::KernighanLin => {
1345 for _ in 0..5 {
1346 let imp = fm_pass_adj(adj, &mut partition, k);
1347 if imp == 0 {
1348 break;
1349 }
1350 }
1351 }
1352 RefinementStrategy::GreedyBalance => {
1353 greedy_balance_pass(adj, &mut partition, k, config.imbalance_factor);
1354 }
1355 }
1356
1357 fixup_empty_partitions(&mut partition, k, n);
1359
1360 let edge_cut = adj_edge_cut(adj, &partition);
1362 let (part_sizes, imbalance) = adj_partition_stats(&partition, k);
1363
1364 Ok(KwayPartitionResult {
1365 partition,
1366 edge_cut,
1367 part_sizes,
1368 imbalance,
1369 })
1370}
1371
1372pub fn recursive_bisection(
1388 adj: &[Vec<(usize, f64)>],
1389 n_parts: usize,
1390 seed: u64,
1391) -> Result<KwayPartitionResult> {
1392 let n = adj.len();
1393
1394 if n < 2 {
1395 return Err(GraphError::InvalidParameter {
1396 param: "adj".to_string(),
1397 value: format!("{}", n),
1398 expected: "at least 2 nodes".to_string(),
1399 context: "recursive_bisection".to_string(),
1400 });
1401 }
1402
1403 if n_parts < 2 {
1404 return Err(GraphError::InvalidParameter {
1405 param: "n_parts".to_string(),
1406 value: format!("{}", n_parts),
1407 expected: "at least 2".to_string(),
1408 context: "recursive_bisection".to_string(),
1409 });
1410 }
1411
1412 if n_parts > n {
1413 return Err(GraphError::InvalidParameter {
1414 param: "n_parts".to_string(),
1415 value: format!("{}", n_parts),
1416 expected: format!("at most {} (number of nodes)", n),
1417 context: "recursive_bisection".to_string(),
1418 });
1419 }
1420
1421 let mut partition = recursive_bisection_adj(adj, n_parts, seed);
1422 fixup_empty_partitions(&mut partition, n_parts, n);
1423 let edge_cut = adj_edge_cut(adj, &partition);
1424 let (part_sizes, imbalance) = adj_partition_stats(&partition, n_parts);
1425
1426 Ok(KwayPartitionResult {
1427 partition,
1428 edge_cut,
1429 part_sizes,
1430 imbalance,
1431 })
1432}
1433
1434fn recursive_bisection_adj(adj: &[Vec<(usize, f64)>], n_parts: usize, seed: u64) -> Vec<usize> {
1436 let n = adj.len();
1437 let mut partition = vec![0usize; n];
1438
1439 if n_parts <= 1 || n < 2 {
1440 return partition;
1441 }
1442
1443 let mut queue: Vec<(usize, Vec<usize>)> = vec![(0usize, (0..n).collect())];
1445 let mut next_id = 1usize;
1446
1447 while next_id < n_parts {
1448 if queue.is_empty() {
1449 break;
1450 }
1451
1452 let largest_idx = queue
1454 .iter()
1455 .enumerate()
1456 .max_by_key(|(_, (_, nodes))| nodes.len())
1457 .map(|(i, _)| i)
1458 .unwrap_or(0);
1459
1460 if queue[largest_idx].1.len() < 2 {
1461 break;
1462 }
1463
1464 let (pid, nodes) = queue.remove(largest_idx);
1465 let sub_n = nodes.len();
1466
1467 let mut node_to_sub = vec![usize::MAX; n];
1469 for (si, &ni) in nodes.iter().enumerate() {
1470 node_to_sub[ni] = si;
1471 }
1472
1473 let mut sub_adj: Vec<Vec<(usize, f64)>> = vec![vec![]; sub_n];
1474 for (si, &ni) in nodes.iter().enumerate() {
1475 for &(nb, w) in &adj[ni] {
1476 let snb = node_to_sub[nb];
1477 if snb != usize::MAX {
1478 sub_adj[si].push((snb, w));
1479 }
1480 }
1481 }
1482
1483 let sub_partition = initial_bisect_adj_seeded(&sub_adj, seed);
1485
1486 let mut part0 = Vec::new();
1487 let mut part1 = Vec::new();
1488 for (si, &a) in sub_partition.iter().enumerate() {
1489 let global_node = nodes[si];
1490 if a == 0 {
1491 partition[global_node] = pid;
1492 part0.push(global_node);
1493 } else {
1494 partition[global_node] = next_id;
1495 part1.push(global_node);
1496 }
1497 }
1498
1499 queue.push((pid, part0));
1500 queue.push((next_id, part1));
1501 next_id += 1;
1502 }
1503
1504 for (pid, nodes) in &queue {
1506 for &ni in nodes {
1507 partition[ni] = *pid;
1508 }
1509 }
1510
1511 partition
1512}
1513
1514fn initial_bisect_adj_seeded(adj: &[Vec<(usize, f64)>], seed: u64) -> Vec<usize> {
1516 let n = adj.len();
1517 if n == 0 {
1518 return vec![];
1519 }
1520 if n == 1 {
1521 return vec![0];
1522 }
1523
1524 let start = (seed as usize) % n;
1526 let mut partition = vec![usize::MAX; n];
1527 let mut queue = std::collections::VecDeque::new();
1528 queue.push_back(start);
1529 partition[start] = 0;
1530 let mut order = vec![start];
1531
1532 while let Some(v) = queue.pop_front() {
1533 for &(nb, _) in &adj[v] {
1534 if partition[nb] == usize::MAX {
1535 partition[nb] = 0;
1536 order.push(nb);
1537 queue.push_back(nb);
1538 }
1539 }
1540 }
1541
1542 for i in 0..n {
1544 if partition[i] == usize::MAX {
1545 partition[i] = 0;
1546 order.push(i);
1547 }
1548 }
1549
1550 let half = n / 2;
1552 for &node in order.iter().skip(half) {
1553 partition[node] = 1;
1554 }
1555
1556 partition
1557}
1558
1559#[cfg(test)]
1560mod kway_tests {
1561 use super::*;
1562
1563 fn build_path_adj(n: usize) -> Vec<Vec<(usize, f64)>> {
1564 let mut adj = vec![vec![]; n];
1565 for i in 0..(n - 1) {
1566 adj[i].push((i + 1, 1.0));
1567 adj[i + 1].push((i, 1.0));
1568 }
1569 adj
1570 }
1571
1572 fn build_two_cliques_adj(n: usize) -> Vec<Vec<(usize, f64)>> {
1573 let size = 2 * n;
1574 let mut adj = vec![vec![]; size];
1575 for i in 0..n {
1576 for j in (i + 1)..n {
1577 adj[i].push((j, 1.0));
1578 adj[j].push((i, 1.0));
1579 }
1580 }
1581 for i in n..size {
1582 for j in (i + 1)..size {
1583 adj[i].push((j, 1.0));
1584 adj[j].push((i, 1.0));
1585 }
1586 }
1587 adj[n - 1].push((n, 1.0));
1589 adj[n].push((n - 1, 1.0));
1590 adj
1591 }
1592
1593 #[test]
1594 fn test_multilevel_2way() {
1595 let adj = build_path_adj(20);
1596 let config = PartitioningConfig {
1597 n_parts: 2,
1598 ..PartitioningConfig::default()
1599 };
1600 let result = multilevel_kway(&adj, &config).expect("2-way partition should succeed");
1601 assert_eq!(result.part_sizes.len(), 2);
1602 assert_eq!(result.part_sizes.iter().sum::<usize>(), 20);
1603 for &s in &result.part_sizes {
1604 assert!(s > 0, "each part should be non-empty");
1605 }
1606 }
1607
1608 #[test]
1609 fn test_multilevel_4way() {
1610 let adj = build_path_adj(40);
1611 let config = PartitioningConfig {
1612 n_parts: 4,
1613 ..PartitioningConfig::default()
1614 };
1615 let result = multilevel_kway(&adj, &config).expect("4-way partition should succeed");
1616 assert_eq!(result.part_sizes.len(), 4);
1617 assert_eq!(result.part_sizes.iter().sum::<usize>(), 40);
1618 for &s in &result.part_sizes {
1619 assert!(s > 0, "each part should be non-empty");
1620 }
1621 }
1622
1623 #[test]
1624 fn test_recursive_bisection() {
1625 let adj = build_two_cliques_adj(5);
1626 let result = recursive_bisection(&adj, 2, 42).expect("recursive bisection should succeed");
1627 assert_eq!(result.part_sizes.len(), 2);
1628 assert_eq!(result.part_sizes.iter().sum::<usize>(), 10);
1629 }
1630
1631 #[test]
1632 fn test_partition_imbalance_bound() {
1633 let adj = build_two_cliques_adj(6);
1634 let config = PartitioningConfig {
1635 n_parts: 2,
1636 imbalance_factor: 0.3,
1637 ..PartitioningConfig::default()
1638 };
1639 let result = multilevel_kway(&adj, &config).expect("should succeed");
1640 assert!(
1642 result.imbalance < 1.0,
1643 "imbalance {} too high",
1644 result.imbalance
1645 );
1646 }
1647
1648 #[test]
1649 fn test_invalid_params() {
1650 let adj = build_path_adj(4);
1651
1652 let config_k1 = PartitioningConfig {
1653 n_parts: 1,
1654 ..PartitioningConfig::default()
1655 };
1656 assert!(multilevel_kway(&adj, &config_k1).is_err());
1657
1658 let config_big = PartitioningConfig {
1659 n_parts: 100,
1660 ..PartitioningConfig::default()
1661 };
1662 assert!(multilevel_kway(&adj, &config_big).is_err());
1663
1664 assert!(recursive_bisection(&adj, 1, 42).is_err());
1665 assert!(recursive_bisection(&adj, 100, 42).is_err());
1666 }
1667
1668 #[test]
1669 fn test_coarsening_strategies() {
1670 let adj = build_path_adj(20);
1671 for strategy in [
1672 CoarseningStrategy::HeavyEdgeMatching,
1673 CoarseningStrategy::RandomMatching,
1674 CoarseningStrategy::SortedHeavyEdge,
1675 ] {
1676 let config = PartitioningConfig {
1677 n_parts: 2,
1678 coarsening: strategy,
1679 ..PartitioningConfig::default()
1680 };
1681 let result = multilevel_kway(&adj, &config).expect("should succeed");
1682 assert_eq!(result.part_sizes.iter().sum::<usize>(), 20);
1683 }
1684 }
1685
1686 #[test]
1687 fn test_refinement_strategies() {
1688 let adj = build_two_cliques_adj(4);
1689 for strategy in [
1690 RefinementStrategy::FiducciaMattheyses,
1691 RefinementStrategy::KernighanLin,
1692 RefinementStrategy::GreedyBalance,
1693 ] {
1694 let config = PartitioningConfig {
1695 n_parts: 2,
1696 refinement: strategy,
1697 ..PartitioningConfig::default()
1698 };
1699 let result = multilevel_kway(&adj, &config).expect("should succeed");
1700 assert_eq!(result.part_sizes.iter().sum::<usize>(), 8);
1701 }
1702 }
1703}