Skip to main content

scirs2_sparse/reorder/
nested_dissection.rs

1//! Nested Dissection ordering for fill-reducing reordering
2//!
3//! Nested dissection is a divide-and-conquer algorithm that recursively
4//! bisects a graph using vertex separators. Separator nodes are ordered
5//! last, and the algorithm recurses on the two subgraphs. This produces
6//! orderings with near-optimal fill-in for many classes of sparse matrices,
7//! particularly those arising from 2D and 3D finite element meshes.
8//!
9//! # Algorithm
10//!
11//! 1. Find a vertex separator S that splits graph G into subgraphs A and B.
12//! 2. Recursively order A and B.
13//! 3. Order separator S last.
14//!
15//! When the subgraph is small enough, switch to AMD for the base case.
16//!
17//! # Multi-level variant
18//!
19//! The multi-level variant coarsens the graph by heavy-edge matching,
20//! partitions the coarsened graph, then uncoarsens and refines.
21//!
22//! # References
23//!
24//! - A. George, "Nested Dissection of a Regular Finite Element Mesh",
25//!   SIAM J. Numer. Anal., 10(2), 1973.
26//! - G. Karypis, V. Kumar, "A Fast and High Quality Multilevel Scheme for
27//!   Partitioning Irregular Graphs", SIAM J. Sci. Comput., 20(1), 1998.
28
29use std::collections::VecDeque;
30
31use super::adjacency::AdjacencyGraph;
32use super::amd;
33use crate::error::SparseResult;
34
35/// Configuration for nested dissection.
36#[derive(Debug, Clone)]
37pub struct NestedDissectionConfig {
38    /// Minimum subgraph size before switching to AMD.
39    /// Default: 64.
40    pub min_subgraph_size: usize,
41    /// Whether to use multi-level coarsening for separator finding.
42    /// Default: true.
43    pub multi_level: bool,
44    /// Maximum number of coarsening levels in multi-level mode.
45    /// Default: 10.
46    pub max_coarsening_levels: usize,
47    /// Number of Kernighan-Lin refinement passes per level.
48    /// Default: 5.
49    pub kl_passes: usize,
50}
51
52impl Default for NestedDissectionConfig {
53    fn default() -> Self {
54        Self {
55            min_subgraph_size: 64,
56            multi_level: true,
57            max_coarsening_levels: 10,
58            kl_passes: 5,
59        }
60    }
61}
62
63/// Result of nested dissection ordering.
64#[derive(Debug, Clone)]
65pub struct NestedDissectionResult {
66    /// Permutation vector: `perm[new_index] = old_index`.
67    pub perm: Vec<usize>,
68    /// Inverse permutation.
69    pub inv_perm: Vec<usize>,
70    /// Number of separator nodes at the top level.
71    pub top_separator_size: usize,
72}
73
74/// Compute a nested dissection ordering for a symmetric adjacency graph.
75///
76/// Uses default configuration (min subgraph size 64, multi-level enabled).
77pub fn nested_dissection(graph: &AdjacencyGraph) -> SparseResult<Vec<usize>> {
78    let config = NestedDissectionConfig::default();
79    nested_dissection_with_config(graph, &config)
80}
81
82/// Compute a nested dissection ordering with custom configuration.
83pub fn nested_dissection_with_config(
84    graph: &AdjacencyGraph,
85    config: &NestedDissectionConfig,
86) -> SparseResult<Vec<usize>> {
87    let result = nested_dissection_full(graph, config)?;
88    Ok(result.perm)
89}
90
91/// Compute a nested dissection ordering with full result.
92pub fn nested_dissection_full(
93    graph: &AdjacencyGraph,
94    config: &NestedDissectionConfig,
95) -> SparseResult<NestedDissectionResult> {
96    let n = graph.num_nodes();
97    if n == 0 {
98        return Ok(NestedDissectionResult {
99            perm: Vec::new(),
100            inv_perm: Vec::new(),
101            top_separator_size: 0,
102        });
103    }
104
105    let nodes: Vec<usize> = (0..n).collect();
106    let mut perm = Vec::with_capacity(n);
107    let mut top_sep_size = 0;
108
109    nd_recurse(graph, &nodes, config, &mut perm, 0, &mut top_sep_size)?;
110
111    // Build inverse permutation
112    let mut inv_perm = vec![0usize; n];
113    for (new_i, &old_i) in perm.iter().enumerate() {
114        inv_perm[old_i] = new_i;
115    }
116
117    Ok(NestedDissectionResult {
118        perm,
119        inv_perm,
120        top_separator_size: top_sep_size,
121    })
122}
123
124/// Recursive nested dissection on a subset of nodes.
125fn nd_recurse(
126    graph: &AdjacencyGraph,
127    nodes: &[usize],
128    config: &NestedDissectionConfig,
129    perm: &mut Vec<usize>,
130    depth: usize,
131    top_sep_size: &mut usize,
132) -> SparseResult<()> {
133    let n = nodes.len();
134
135    if n == 0 {
136        return Ok(());
137    }
138
139    // Base case: switch to AMD for small subgraphs
140    if n <= config.min_subgraph_size {
141        let (sub_graph, mapping) = graph.subgraph(nodes);
142        let amd_result = amd::amd(&sub_graph)?;
143        for &p in &amd_result.perm {
144            perm.push(mapping[p]);
145        }
146        return Ok(());
147    }
148
149    // Find a vertex separator
150    let (part_a, separator, part_b) = if config.multi_level {
151        find_separator_multilevel(graph, nodes, config)
152    } else {
153        find_separator_bfs(graph, nodes)
154    };
155
156    // Recurse on part A
157    nd_recurse(graph, &part_a, config, perm, depth + 1, top_sep_size)?;
158
159    // Recurse on part B
160    nd_recurse(graph, &part_b, config, perm, depth + 1, top_sep_size)?;
161
162    // Order separator last
163    for &s in &separator {
164        perm.push(s);
165    }
166
167    if depth == 0 {
168        *top_sep_size = separator.len();
169    }
170
171    Ok(())
172}
173
174/// Find a vertex separator using BFS-based level-set approach.
175///
176/// 1. BFS from a peripheral node to get level sets.
177/// 2. Pick the middle level as separator.
178/// 3. Nodes in levels before the middle form part A, after form part B.
179fn find_separator_bfs(
180    graph: &AdjacencyGraph,
181    nodes: &[usize],
182) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
183    if nodes.is_empty() {
184        return (Vec::new(), Vec::new(), Vec::new());
185    }
186    if nodes.len() <= 2 {
187        // Too small to split meaningfully
188        return (nodes.to_vec(), Vec::new(), Vec::new());
189    }
190
191    let node_set: std::collections::HashSet<usize> = nodes.iter().copied().collect();
192
193    // Find peripheral node in subgraph (minimum degree)
194    let start = nodes
195        .iter()
196        .copied()
197        .min_by_key(|&u| {
198            graph
199                .neighbors(u)
200                .iter()
201                .filter(|&&v| node_set.contains(&v))
202                .count()
203        })
204        .unwrap_or(nodes[0]);
205
206    // BFS in subgraph
207    let mut level = std::collections::HashMap::new();
208    let mut queue = VecDeque::new();
209    let mut order = Vec::new();
210    level.insert(start, 0usize);
211    queue.push_back(start);
212
213    while let Some(u) = queue.pop_front() {
214        order.push(u);
215        let l = level[&u];
216        for &v in graph.neighbors(u) {
217            if node_set.contains(&v) && !level.contains_key(&v) {
218                level.insert(v, l + 1);
219                queue.push_back(v);
220            }
221        }
222    }
223
224    // Add any unvisited nodes (disconnected)
225    for &u in nodes {
226        level.entry(u).or_insert_with(|| {
227            order.push(u);
228            0
229        });
230    }
231
232    let max_level = level.values().copied().max().unwrap_or(0);
233    if max_level == 0 {
234        // All nodes at same level (complete graph or single node)
235        let mid = nodes.len() / 2;
236        return (
237            nodes[..mid].to_vec(),
238            vec![nodes[mid]],
239            nodes[mid + 1..].to_vec(),
240        );
241    }
242
243    // Pick middle level as separator
244    let sep_level = max_level / 2;
245
246    let mut part_a = Vec::new();
247    let mut separator = Vec::new();
248    let mut part_b = Vec::new();
249
250    for &u in nodes {
251        let l = level.get(&u).copied().unwrap_or(0);
252        if l < sep_level {
253            part_a.push(u);
254        } else if l == sep_level {
255            separator.push(u);
256        } else {
257            part_b.push(u);
258        }
259    }
260
261    // Ensure separator actually separates: if part_a or part_b is empty,
262    // fall back to simple bisection
263    if part_a.is_empty() || part_b.is_empty() {
264        let mid = order.len() / 2;
265        let sep_idx = mid.min(order.len().saturating_sub(1));
266        part_a = order[..sep_idx].to_vec();
267        separator = vec![order[sep_idx]];
268        part_b = if sep_idx + 1 < order.len() {
269            order[sep_idx + 1..].to_vec()
270        } else {
271            Vec::new()
272        };
273    }
274
275    (part_a, separator, part_b)
276}
277
278/// Find a vertex separator using multi-level coarsening.
279///
280/// 1. Coarsen the subgraph by heavy-edge matching.
281/// 2. Bisect the coarsest graph.
282/// 3. Uncoarsen and refine with Kernighan-Lin.
283/// 4. Extract separator from the partition boundary.
284fn find_separator_multilevel(
285    graph: &AdjacencyGraph,
286    nodes: &[usize],
287    config: &NestedDissectionConfig,
288) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
289    if nodes.len() <= 3 {
290        return find_separator_bfs(graph, nodes);
291    }
292
293    // Build subgraph
294    let (sub_graph, mapping) = graph.subgraph(nodes);
295    let sub_n = sub_graph.num_nodes();
296
297    // Coarsen
298    let coarsening_levels = coarsen_graph(&sub_graph, config.max_coarsening_levels);
299
300    // Bisect the coarsest graph
301    let coarsest = if coarsening_levels.is_empty() {
302        &sub_graph
303    } else {
304        coarsening_levels
305            .last()
306            .map(|l| &l.coarse_graph)
307            .unwrap_or(&sub_graph)
308    };
309
310    let mut partition = initial_bisection(coarsest);
311
312    // Uncoarsen and refine
313    for level in coarsening_levels.iter().rev() {
314        partition = uncoarsen_partition(&partition, &level.fine_to_coarse, level.fine_n);
315        kl_refine_partition(&level.coarse_graph, &mut partition, config.kl_passes);
316    }
317
318    // If we didn't coarsen at all, refine on the subgraph directly
319    if coarsening_levels.is_empty() {
320        kl_refine_partition(&sub_graph, &mut partition, config.kl_passes);
321    }
322
323    // Extract separator: boundary nodes between the two partitions
324    let mut part_a = Vec::new();
325    let mut separator = Vec::new();
326    let mut part_b = Vec::new();
327
328    // Find boundary nodes (nodes adjacent to the other partition)
329    let mut is_boundary = vec![false; sub_n];
330    for u in 0..sub_n {
331        for &v in sub_graph.neighbors(u) {
332            if partition[u] != partition[v] {
333                is_boundary[u] = true;
334                break;
335            }
336        }
337    }
338
339    // Use boundary nodes from the smaller partition as separator
340    let count_0: usize = partition.iter().filter(|&&p| p == 0).count();
341    let count_1 = sub_n - count_0;
342    let sep_side = if count_0 <= count_1 { 0 } else { 1 };
343
344    for u in 0..sub_n {
345        if is_boundary[u] && partition[u] == sep_side {
346            separator.push(mapping[u]);
347        } else if partition[u] == 0 && (sep_side != 0 || !is_boundary[u]) {
348            part_a.push(mapping[u]);
349        } else if partition[u] == 1 && (sep_side != 1 || !is_boundary[u]) {
350            part_b.push(mapping[u]);
351        }
352    }
353
354    // Ensure we have a valid split
355    if separator.is_empty() {
356        // Fallback: use BFS-based separator
357        return find_separator_bfs(graph, nodes);
358    }
359
360    (part_a, separator, part_b)
361}
362
363/// A coarsening level in the multi-level hierarchy.
364struct CoarseningLevel {
365    /// The coarsened graph.
366    coarse_graph: AdjacencyGraph,
367    /// Mapping from fine node to coarse node.
368    fine_to_coarse: Vec<usize>,
369    /// Number of fine nodes.
370    fine_n: usize,
371}
372
373/// Coarsen a graph by heavy-edge matching.
374fn coarsen_graph(graph: &AdjacencyGraph, max_levels: usize) -> Vec<CoarseningLevel> {
375    let mut levels = Vec::new();
376    let mut current = graph.clone();
377
378    for _ in 0..max_levels {
379        let n = current.num_nodes();
380        if n <= 16 {
381            break; // Small enough
382        }
383
384        // Heavy-edge matching: match each unmatched node with its
385        // unmatched neighbor of highest degree
386        let mut matched = vec![false; n];
387        let mut coarse_id = vec![usize::MAX; n];
388        let mut next_id = 0usize;
389
390        // Process nodes in order of increasing degree
391        let mut node_order: Vec<usize> = (0..n).collect();
392        node_order.sort_unstable_by_key(|&u| current.degree(u));
393
394        for &u in &node_order {
395            if matched[u] {
396                continue;
397            }
398
399            // Find best unmatched neighbor (highest degree = heaviest edge heuristic)
400            let best_neighbor = current
401                .neighbors(u)
402                .iter()
403                .copied()
404                .filter(|&v| !matched[v])
405                .max_by_key(|&v| current.degree(v));
406
407            matched[u] = true;
408            coarse_id[u] = next_id;
409
410            if let Some(v) = best_neighbor {
411                matched[v] = true;
412                coarse_id[v] = next_id;
413            }
414
415            next_id += 1;
416        }
417
418        // Handle any unmatched nodes (shouldn't happen, but be safe)
419        for u in 0..n {
420            if coarse_id[u] == usize::MAX {
421                coarse_id[u] = next_id;
422                next_id += 1;
423            }
424        }
425
426        let coarse_n = next_id;
427        if coarse_n >= n * 3 / 4 {
428            break; // Not coarsening enough, stop
429        }
430
431        // Build coarse graph
432        let mut coarse_adj: Vec<Vec<usize>> = vec![Vec::new(); coarse_n];
433        for u in 0..n {
434            let cu = coarse_id[u];
435            for &v in current.neighbors(u) {
436                let cv = coarse_id[v];
437                if cu != cv {
438                    coarse_adj[cu].push(cv);
439                }
440            }
441        }
442
443        let coarse_graph = AdjacencyGraph::from_adjacency_list(coarse_adj);
444
445        levels.push(CoarseningLevel {
446            coarse_graph: coarse_graph.clone(),
447            fine_to_coarse: coarse_id,
448            fine_n: n,
449        });
450
451        current = coarse_graph;
452    }
453
454    levels
455}
456
457/// Initial bisection of a small graph using BFS.
458fn initial_bisection(graph: &AdjacencyGraph) -> Vec<usize> {
459    let n = graph.num_nodes();
460    if n == 0 {
461        return Vec::new();
462    }
463
464    // BFS from minimum degree node
465    let start = (0..n).min_by_key(|&u| graph.degree(u)).unwrap_or(0);
466
467    let mut visited = vec![false; n];
468    let mut order = Vec::new();
469    let mut queue = VecDeque::new();
470    visited[start] = true;
471    queue.push_back(start);
472
473    while let Some(u) = queue.pop_front() {
474        order.push(u);
475        for &v in graph.neighbors(u) {
476            if !visited[v] {
477                visited[v] = true;
478                queue.push_back(v);
479            }
480        }
481    }
482
483    // Add unvisited nodes
484    for u in 0..n {
485        if !visited[u] {
486            order.push(u);
487        }
488    }
489
490    // First half -> partition 0, second half -> partition 1
491    let mid = order.len() / 2;
492    let mut partition = vec![0usize; n];
493    for &u in &order[mid..] {
494        partition[u] = 1;
495    }
496
497    partition
498}
499
500/// Project a coarse partition back to the fine level.
501fn uncoarsen_partition(
502    coarse_partition: &[usize],
503    fine_to_coarse: &[usize],
504    fine_n: usize,
505) -> Vec<usize> {
506    let mut fine_partition = vec![0usize; fine_n];
507    for u in 0..fine_n {
508        let cu = fine_to_coarse[u];
509        if cu < coarse_partition.len() {
510            fine_partition[u] = coarse_partition[cu];
511        }
512    }
513    fine_partition
514}
515
516/// Kernighan-Lin refinement of a bisection.
517fn kl_refine_partition(graph: &AdjacencyGraph, partition: &mut [usize], max_passes: usize) {
518    let n = graph.num_nodes();
519    if n < 2 {
520        return;
521    }
522
523    for _pass in 0..max_passes {
524        let mut improved = false;
525
526        // Compute gain for moving each node
527        let mut best_gain = 0i64;
528        let mut best_node = None;
529
530        for u in 0..n {
531            let ext: i64 = graph
532                .neighbors(u)
533                .iter()
534                .filter(|&&v| partition[v] != partition[u])
535                .count() as i64;
536            let int: i64 = graph
537                .neighbors(u)
538                .iter()
539                .filter(|&&v| partition[v] == partition[u])
540                .count() as i64;
541            let gain = ext - int;
542
543            if gain > best_gain {
544                // Check balance: don't move if it makes partition too unbalanced
545                let count_same: usize = partition.iter().filter(|&&p| p == partition[u]).count();
546                if count_same > n / 4 {
547                    best_gain = gain;
548                    best_node = Some(u);
549                }
550            }
551        }
552
553        if let Some(u) = best_node {
554            partition[u] = 1 - partition[u];
555            improved = true;
556        }
557
558        if !improved {
559            break;
560        }
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    fn path_graph(n: usize) -> AdjacencyGraph {
569        let mut adj = vec![Vec::new(); n];
570        for i in 0..n.saturating_sub(1) {
571            adj[i].push(i + 1);
572            adj[i + 1].push(i);
573        }
574        AdjacencyGraph::from_adjacency_list(adj)
575    }
576
577    fn grid_graph(rows: usize, cols: usize) -> AdjacencyGraph {
578        let n = rows * cols;
579        let mut adj = vec![Vec::new(); n];
580        for r in 0..rows {
581            for c in 0..cols {
582                let u = r * cols + c;
583                if c + 1 < cols {
584                    let v = r * cols + c + 1;
585                    adj[u].push(v);
586                    adj[v].push(u);
587                }
588                if r + 1 < rows {
589                    let v = (r + 1) * cols + c;
590                    adj[u].push(v);
591                    adj[v].push(u);
592                }
593            }
594        }
595        AdjacencyGraph::from_adjacency_list(adj)
596    }
597
598    #[test]
599    fn test_nd_valid_permutation() {
600        let graph = path_graph(20);
601        let perm = nested_dissection(&graph).expect("ND");
602        assert_eq!(perm.len(), 20);
603        let mut sorted = perm.clone();
604        sorted.sort_unstable();
605        assert_eq!(sorted, (0..20).collect::<Vec<_>>());
606    }
607
608    #[test]
609    fn test_nd_grid_valid_permutation() {
610        let graph = grid_graph(4, 4);
611        let perm = nested_dissection(&graph).expect("ND grid");
612        assert_eq!(perm.len(), 16);
613        let mut sorted = perm.clone();
614        sorted.sort_unstable();
615        assert_eq!(sorted, (0..16).collect::<Vec<_>>());
616    }
617
618    #[test]
619    fn test_nd_small_graph_uses_amd() {
620        // With min_subgraph_size = 64, a 10-node graph goes straight to AMD
621        let graph = path_graph(10);
622        let config = NestedDissectionConfig {
623            min_subgraph_size: 64,
624            ..Default::default()
625        };
626        let perm = nested_dissection_with_config(&graph, &config).expect("ND small");
627        assert_eq!(perm.len(), 10);
628        let mut sorted = perm.clone();
629        sorted.sort_unstable();
630        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
631    }
632
633    #[test]
634    fn test_nd_large_graph() {
635        // Large enough to trigger actual nested dissection
636        let graph = grid_graph(10, 10);
637        let config = NestedDissectionConfig {
638            min_subgraph_size: 16,
639            multi_level: true,
640            ..Default::default()
641        };
642        let result = nested_dissection_full(&graph, &config).expect("ND large");
643        assert_eq!(result.perm.len(), 100);
644        let mut sorted = result.perm.clone();
645        sorted.sort_unstable();
646        assert_eq!(sorted, (0..100).collect::<Vec<_>>());
647    }
648
649    #[test]
650    fn test_nd_empty_graph() {
651        let graph = AdjacencyGraph::from_adjacency_list(Vec::new());
652        let perm = nested_dissection(&graph).expect("ND empty");
653        assert!(perm.is_empty());
654    }
655
656    #[test]
657    fn test_nd_single_node() {
658        let graph = AdjacencyGraph::from_adjacency_list(vec![Vec::new()]);
659        let config = NestedDissectionConfig {
660            min_subgraph_size: 1,
661            multi_level: false,
662            ..Default::default()
663        };
664        let perm = nested_dissection_with_config(&graph, &config).expect("ND single");
665        assert_eq!(perm, vec![0]);
666    }
667
668    #[test]
669    fn test_nd_bfs_separator() {
670        // Test the BFS separator directly
671        let graph = path_graph(10);
672        let nodes: Vec<usize> = (0..10).collect();
673        let (a, sep, b) = find_separator_bfs(&graph, &nodes);
674        // All nodes should be accounted for
675        let total = a.len() + sep.len() + b.len();
676        assert_eq!(total, 10, "all nodes must be in some partition");
677        // Separator should be non-empty for a path graph of size 10
678        assert!(!sep.is_empty(), "separator should be non-empty");
679    }
680
681    #[test]
682    fn test_nd_disconnected() {
683        // Two disconnected components
684        let mut adj = vec![Vec::new(); 8];
685        for i in 0..3 {
686            adj[i].push(i + 1);
687            adj[i + 1].push(i);
688        }
689        for i in 4..7 {
690            adj[i].push(i + 1);
691            adj[i + 1].push(i);
692        }
693        let graph = AdjacencyGraph::from_adjacency_list(adj);
694        let config = NestedDissectionConfig {
695            min_subgraph_size: 2,
696            multi_level: false,
697            ..Default::default()
698        };
699        let perm = nested_dissection_with_config(&graph, &config).expect("ND disconnected");
700        assert_eq!(perm.len(), 8);
701        let mut sorted = perm.clone();
702        sorted.sort_unstable();
703        assert_eq!(sorted, (0..8).collect::<Vec<_>>());
704    }
705}