Skip to main content

oxicuda_solver/sparse/
nested_dissection.rs

1//! Nested dissection ordering for sparse direct solvers.
2//!
3//! Provides graph-based fill-reducing orderings that minimize fill-in during
4//! sparse Cholesky or LU factorization. The main algorithm recursively finds
5//! vertex separators to split the graph, orders each partition recursively,
6//! then places separator vertices last.
7//!
8//! # Algorithm
9//!
10//! 1. Build an [`AdjacencyGraph`] from a symmetric CSR matrix.
11//! 2. Call [`NestedDissectionOrdering::compute`] to obtain a [`Permutation`].
12//! 3. Evaluate quality with [`analyze_ordering`].
13//!
14//! The separator finder uses a multi-level approach: heavy-edge coarsening,
15//! BFS-based bisection on the coarse graph, and Fiduccia-Mattheyses refinement.
16
17use std::collections::VecDeque;
18
19use crate::error::{SolverError, SolverResult};
20
21// ---------------------------------------------------------------------------
22// Permutation
23// ---------------------------------------------------------------------------
24
25/// A permutation with its inverse, for reordering matrix rows/columns.
26#[derive(Debug, Clone)]
27pub struct Permutation {
28    /// Forward permutation: `new[i] = old[perm[i]]`.
29    pub perm: Vec<usize>,
30    /// Inverse permutation: `old[i] = new[iperm[i]]`.
31    pub iperm: Vec<usize>,
32}
33
34impl Permutation {
35    /// Create a permutation from a forward mapping. Returns an error if any
36    /// index is out of range or duplicated.
37    pub fn new(perm: Vec<usize>) -> SolverResult<Self> {
38        let n = perm.len();
39        let mut iperm = vec![0usize; n];
40        let mut seen = vec![false; n];
41        for (i, &p) in perm.iter().enumerate() {
42            if p >= n {
43                return Err(SolverError::InternalError(format!(
44                    "permutation index {p} out of range for size {n}"
45                )));
46            }
47            if seen[p] {
48                return Err(SolverError::InternalError(format!(
49                    "duplicate permutation index {p}"
50                )));
51            }
52            seen[p] = true;
53            iperm[p] = i;
54        }
55        Ok(Self { perm, iperm })
56    }
57
58    /// Identity permutation of size `n`.
59    pub fn identity(n: usize) -> Self {
60        Self {
61            perm: (0..n).collect(),
62            iperm: (0..n).collect(),
63        }
64    }
65
66    /// Apply permutation to a slice: `result[i] = data[perm[i]]`.
67    pub fn apply<T: Copy>(&self, data: &[T]) -> Vec<T> {
68        self.perm.iter().map(|&p| data[p]).collect()
69    }
70
71    /// Compose two permutations: `result[i] = self.perm[other.perm[i]]`.
72    pub fn compose(&self, other: &Permutation) -> SolverResult<Permutation> {
73        let composed: Vec<usize> = other.perm.iter().map(|&i| self.perm[i]).collect();
74        Permutation::new(composed)
75    }
76
77    /// Return the inverse permutation as a slice.
78    pub fn inverse(&self) -> &[usize] {
79        &self.iperm
80    }
81}
82
83// ---------------------------------------------------------------------------
84// AdjacencyGraph
85// ---------------------------------------------------------------------------
86
87/// Undirected graph of a symmetric sparse matrix in CSR-like form.
88///
89/// Self-loops (diagonal entries) are excluded. Each edge `(i,j)` appears in
90/// both `neighbors(i)` and `neighbors(j)`.
91#[derive(Debug, Clone)]
92pub struct AdjacencyGraph {
93    /// Number of vertices.
94    pub num_vertices: usize,
95    /// CSR-style pointers into `adj_list`. Length = `num_vertices + 1`.
96    pub adj_ptr: Vec<usize>,
97    /// Concatenated adjacency lists for all vertices.
98    pub adj_list: Vec<usize>,
99}
100
101impl AdjacencyGraph {
102    /// Build an adjacency graph from a symmetric CSR matrix.
103    ///
104    /// Diagonal entries are excluded. The CSR uses `i32` indices (matching
105    /// cuSPARSE convention). Both upper and lower triangular parts should be
106    /// present for a symmetric matrix.
107    pub fn from_symmetric_csr(row_ptr: &[i32], col_indices: &[i32], n: usize) -> Self {
108        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
109        for row in 0..n {
110            let start = row_ptr[row] as usize;
111            let end = row_ptr[row + 1] as usize;
112            for &col_i32 in &col_indices[start..end] {
113                let col = col_i32 as usize;
114                if col != row && col < n {
115                    adj[row].push(col);
116                }
117            }
118        }
119        // Deduplicate each adjacency list
120        for list in &mut adj {
121            list.sort_unstable();
122            list.dedup();
123        }
124        // Flatten to CSR
125        let mut adj_ptr = Vec::with_capacity(n + 1);
126        let mut adj_list = Vec::new();
127        adj_ptr.push(0);
128        for list in &adj {
129            adj_list.extend_from_slice(list);
130            adj_ptr.push(adj_list.len());
131        }
132        Self {
133            num_vertices: n,
134            adj_ptr,
135            adj_list,
136        }
137    }
138
139    /// Degree of vertex `v`.
140    pub fn degree(&self, v: usize) -> usize {
141        self.adj_ptr[v + 1] - self.adj_ptr[v]
142    }
143
144    /// Neighbors of vertex `v`.
145    pub fn neighbors(&self, v: usize) -> &[usize] {
146        &self.adj_list[self.adj_ptr[v]..self.adj_ptr[v + 1]]
147    }
148}
149
150// ---------------------------------------------------------------------------
151// SeparatorResult
152// ---------------------------------------------------------------------------
153
154/// Result of a graph separator computation.
155#[derive(Debug, Clone)]
156pub struct SeparatorResult {
157    /// Vertices forming the separator.
158    pub separator: Vec<usize>,
159    /// Vertices in partition A.
160    pub part_a: Vec<usize>,
161    /// Vertices in partition B.
162    pub part_b: Vec<usize>,
163    /// Balance ratio (0..1], 1.0 = perfectly balanced.
164    pub balance_ratio: f64,
165}
166
167// ---------------------------------------------------------------------------
168// OrderingQuality
169// ---------------------------------------------------------------------------
170
171/// Quality metrics for an ordering.
172#[derive(Debug, Clone)]
173pub struct OrderingQuality {
174    /// Estimated fill-in count (non-zero entries added during factorization).
175    pub estimated_fill: usize,
176    /// Total separator size across all recursive levels.
177    pub separator_size: usize,
178    /// Balance ratio of the top-level partition.
179    pub balance_ratio: f64,
180}
181
182// ---------------------------------------------------------------------------
183// BFS level-set
184// ---------------------------------------------------------------------------
185
186/// BFS from a source vertex within a subgraph, returning level sets.
187fn bfs_levels(graph: &AdjacencyGraph, source: usize, in_subgraph: &[bool]) -> Vec<Vec<usize>> {
188    let n = graph.num_vertices;
189    let mut visited = vec![false; n];
190    visited[source] = true;
191    let mut levels: Vec<Vec<usize>> = vec![vec![source]];
192    let mut queue = VecDeque::new();
193    queue.push_back(source);
194    while !queue.is_empty() {
195        let mut next_level = Vec::new();
196        let count = queue.len();
197        for _ in 0..count {
198            if let Some(v) = queue.pop_front() {
199                for &nb in graph.neighbors(v) {
200                    if in_subgraph[nb] && !visited[nb] {
201                        visited[nb] = true;
202                        next_level.push(nb);
203                        queue.push_back(nb);
204                    }
205                }
206            }
207        }
208        if !next_level.is_empty() {
209            levels.push(next_level);
210        }
211    }
212    levels
213}
214
215/// Find a pseudo-peripheral vertex in the subgraph (for good BFS root).
216fn pseudo_peripheral(graph: &AdjacencyGraph, subgraph: &[usize]) -> usize {
217    if subgraph.is_empty() {
218        return 0;
219    }
220    let n = graph.num_vertices;
221    let mut in_sub = vec![false; n];
222    for &v in subgraph {
223        in_sub[v] = true;
224    }
225    // Start from vertex with minimum degree in subgraph
226    let mut start = subgraph[0];
227    let mut min_deg = usize::MAX;
228    for &v in subgraph {
229        let d = graph.neighbors(v).iter().filter(|&&nb| in_sub[nb]).count();
230        if d < min_deg {
231            min_deg = d;
232            start = v;
233        }
234    }
235    // Run BFS twice to find pseudo-peripheral vertex
236    for _ in 0..2 {
237        let levels = bfs_levels(graph, start, &in_sub);
238        if let Some(last) = levels.last() {
239            // Pick vertex with minimum degree in last level
240            let mut best = last[0];
241            let mut best_deg = usize::MAX;
242            for &v in last {
243                let d = graph.neighbors(v).iter().filter(|&&nb| in_sub[nb]).count();
244                if d < best_deg {
245                    best_deg = d;
246                    best = v;
247                }
248            }
249            start = best;
250        }
251    }
252    start
253}
254
255// ---------------------------------------------------------------------------
256// Heavy-edge matching (coarsening)
257// ---------------------------------------------------------------------------
258
259/// Coarsen the subgraph via heavy-edge matching.
260/// Returns (coarse_map, coarse_count) where coarse_map[v] = coarse vertex ID.
261fn coarsen(graph: &AdjacencyGraph, subgraph: &[usize]) -> (Vec<i32>, usize) {
262    let n = graph.num_vertices;
263    let mut coarse_map = vec![-1i32; n];
264    let mut matched = vec![false; n];
265    let mut coarse_id = 0usize;
266
267    // Sort by degree (ascending) for better matching
268    let mut sorted = subgraph.to_vec();
269    sorted.sort_unstable_by_key(|&v| graph.degree(v));
270
271    let in_sub: Vec<bool> = {
272        let mut s = vec![false; n];
273        for &v in subgraph {
274            s[v] = true;
275        }
276        s
277    };
278
279    for &v in &sorted {
280        if matched[v] {
281            continue;
282        }
283        // Find heaviest unmatched neighbor (by degree sum as weight proxy)
284        let mut best_nb = None;
285        let mut best_weight = 0usize;
286        for &nb in graph.neighbors(v) {
287            if in_sub[nb] && !matched[nb] && nb != v {
288                let w = graph.degree(v) + graph.degree(nb);
289                if w > best_weight {
290                    best_weight = w;
291                    best_nb = Some(nb);
292                }
293            }
294        }
295        let cid = coarse_id as i32;
296        coarse_map[v] = cid;
297        matched[v] = true;
298        if let Some(nb) = best_nb {
299            coarse_map[nb] = cid;
300            matched[nb] = true;
301        }
302        coarse_id += 1;
303    }
304    (coarse_map, coarse_id)
305}
306
307// ---------------------------------------------------------------------------
308// BFS-based bisection
309// ---------------------------------------------------------------------------
310
311/// Bisect a subgraph using BFS level sets, returning (part_a, part_b, separator).
312fn bfs_bisect(graph: &AdjacencyGraph, subgraph: &[usize]) -> SeparatorResult {
313    if subgraph.len() <= 1 {
314        return SeparatorResult {
315            separator: subgraph.to_vec(),
316            part_a: Vec::new(),
317            part_b: Vec::new(),
318            balance_ratio: 1.0,
319        };
320    }
321    let n = graph.num_vertices;
322    let mut in_sub = vec![false; n];
323    for &v in subgraph {
324        in_sub[v] = true;
325    }
326    let start = pseudo_peripheral(graph, subgraph);
327    let levels = bfs_levels(graph, start, &in_sub);
328
329    if levels.len() <= 2 {
330        // Too few levels — use simple split
331        let half = subgraph.len() / 2;
332        let part_a = subgraph[..half].to_vec();
333        let separator = vec![subgraph[half]];
334        let part_b = if half + 1 < subgraph.len() {
335            subgraph[half + 1..].to_vec()
336        } else {
337            Vec::new()
338        };
339        let balance = compute_balance(part_a.len(), part_b.len());
340        return SeparatorResult {
341            separator,
342            part_a,
343            part_b,
344            balance_ratio: balance,
345        };
346    }
347
348    // Find the middle level as separator
349    let mid = levels.len() / 2;
350    let separator = levels[mid].clone();
351    let mut part_a = Vec::new();
352    let mut part_b = Vec::new();
353    for (i, level) in levels.iter().enumerate() {
354        if i < mid {
355            part_a.extend_from_slice(level);
356        } else if i > mid {
357            part_b.extend_from_slice(level);
358        }
359    }
360    let balance = compute_balance(part_a.len(), part_b.len());
361    SeparatorResult {
362        separator,
363        part_a,
364        part_b,
365        balance_ratio: balance,
366    }
367}
368
369/// Compute balance ratio: min(a,b)/max(a,b), or 1.0 if both zero.
370fn compute_balance(a: usize, b: usize) -> f64 {
371    if a == 0 && b == 0 {
372        return 1.0;
373    }
374    let min_v = a.min(b) as f64;
375    let max_v = a.max(b) as f64;
376    if max_v == 0.0 { 1.0 } else { min_v / max_v }
377}
378
379// ---------------------------------------------------------------------------
380// FM refinement
381// ---------------------------------------------------------------------------
382
383/// Single-pass Fiduccia-Mattheyses refinement to improve separator quality.
384fn fm_refine(graph: &AdjacencyGraph, result: &mut SeparatorResult) {
385    let n = graph.num_vertices;
386    // partition[v]: 0=A, 1=B, 2=separator
387    let mut partition = vec![0u8; n];
388    for &v in &result.part_a {
389        partition[v] = 0;
390    }
391    for &v in &result.part_b {
392        partition[v] = 1;
393    }
394    for &v in &result.separator {
395        partition[v] = 2;
396    }
397
398    let total = result.part_a.len() + result.part_b.len() + result.separator.len();
399    if total <= 3 {
400        return;
401    }
402
403    // Try moving separator vertices to A or B if it doesn't break connectivity
404    let mut improved = true;
405    let mut iterations = 0;
406    while improved && iterations < 10 {
407        improved = false;
408        iterations += 1;
409        let sep_copy = result.separator.clone();
410        for &sv in &sep_copy {
411            // Count neighbors in A, B, separator
412            let mut na = 0usize;
413            let mut nb = 0usize;
414            for &nb_v in graph.neighbors(sv) {
415                match partition[nb_v] {
416                    0 => na += 1,
417                    1 => nb += 1,
418                    _ => {}
419                }
420            }
421            // Move to side with more connections if safe
422            if na > 0 && nb == 0 {
423                partition[sv] = 0;
424                result.part_a.push(sv);
425                result.separator.retain(|&x| x != sv);
426                improved = true;
427            } else if nb > 0 && na == 0 {
428                partition[sv] = 1;
429                result.part_b.push(sv);
430                result.separator.retain(|&x| x != sv);
431                improved = true;
432            }
433        }
434    }
435
436    // Try moving boundary vertices of A/B into separator if it improves balance
437    let a_len = result.part_a.len();
438    let b_len = result.part_b.len();
439    if a_len > 2 * b_len + 2 {
440        // A is too large, move some A-boundary vertices to separator
441        let mut candidates: Vec<usize> = result
442            .part_a
443            .iter()
444            .copied()
445            .filter(|&v| {
446                graph
447                    .neighbors(v)
448                    .iter()
449                    .any(|&nb| partition[nb] == 1 || partition[nb] == 2)
450            })
451            .collect();
452        candidates.sort_unstable_by_key(|&v| graph.degree(v));
453        let to_move = (a_len - b_len) / 4;
454        for &v in candidates.iter().take(to_move) {
455            partition[v] = 2;
456            result.separator.push(v);
457            result.part_a.retain(|&x| x != v);
458        }
459    } else if b_len > 2 * a_len + 2 {
460        let mut candidates: Vec<usize> = result
461            .part_b
462            .iter()
463            .copied()
464            .filter(|&v| {
465                graph
466                    .neighbors(v)
467                    .iter()
468                    .any(|&nb| partition[nb] == 0 || partition[nb] == 2)
469            })
470            .collect();
471        candidates.sort_unstable_by_key(|&v| graph.degree(v));
472        let to_move = (b_len - a_len) / 4;
473        for &v in candidates.iter().take(to_move) {
474            partition[v] = 2;
475            result.separator.push(v);
476            result.part_b.retain(|&x| x != v);
477        }
478    }
479
480    result.balance_ratio = compute_balance(result.part_a.len(), result.part_b.len());
481}
482
483// ---------------------------------------------------------------------------
484// VertexSeparator
485// ---------------------------------------------------------------------------
486
487/// Finds graph vertex separators using a multi-level approach.
488pub struct VertexSeparator;
489
490impl VertexSeparator {
491    /// Find a vertex separator for the given subgraph vertices.
492    ///
493    /// Uses multi-level coarsening + BFS bisection + FM refinement.
494    pub fn find_separator(graph: &AdjacencyGraph, subgraph: &[usize]) -> SeparatorResult {
495        if subgraph.len() <= 3 {
496            return Self::trivial_separator(subgraph);
497        }
498        // Coarsen
499        let (coarse_map, coarse_count) = coarsen(graph, subgraph);
500        if coarse_count >= subgraph.len() || coarse_count <= 1 {
501            // Coarsening didn't help, bisect directly
502            let mut result = bfs_bisect(graph, subgraph);
503            fm_refine(graph, &mut result);
504            return result;
505        }
506
507        // Build coarse subgraph vertex list (representative vertices)
508        let mut coarse_repr: Vec<Option<usize>> = vec![None; coarse_count];
509        for &v in subgraph {
510            let cid = coarse_map[v];
511            if cid >= 0 {
512                let cid = cid as usize;
513                if coarse_repr[cid].is_none() {
514                    coarse_repr[cid] = Some(v);
515                }
516            }
517        }
518        let coarse_verts: Vec<usize> = coarse_repr.iter().filter_map(|&x| x).collect();
519
520        // Bisect coarse graph
521        let coarse_result = bfs_bisect(graph, &coarse_verts);
522
523        // Uncoarsen: map coarse partition back to fine vertices
524        let n = graph.num_vertices;
525        let mut fine_part = vec![0u8; n]; // 0=A, 1=B, 2=sep
526        for &v in &coarse_result.part_a {
527            let cid = coarse_map[v];
528            for &fv in subgraph {
529                if coarse_map[fv] == cid {
530                    fine_part[fv] = 0;
531                }
532            }
533        }
534        for &v in &coarse_result.part_b {
535            let cid = coarse_map[v];
536            for &fv in subgraph {
537                if coarse_map[fv] == cid {
538                    fine_part[fv] = 1;
539                }
540            }
541        }
542        for &v in &coarse_result.separator {
543            let cid = coarse_map[v];
544            for &fv in subgraph {
545                if coarse_map[fv] == cid {
546                    fine_part[fv] = 2;
547                }
548            }
549        }
550
551        // Build boundary separator: vertices in A adjacent to B become separator
552        let mut separator = Vec::new();
553        let mut part_a = Vec::new();
554        let mut part_b = Vec::new();
555        for &v in subgraph {
556            if fine_part[v] == 2 {
557                separator.push(v);
558            } else {
559                let is_boundary = graph
560                    .neighbors(v)
561                    .iter()
562                    .any(|&nb| fine_part[nb] != fine_part[v] && fine_part[nb] != 2);
563                if is_boundary {
564                    separator.push(v);
565                } else if fine_part[v] == 0 {
566                    part_a.push(v);
567                } else {
568                    part_b.push(v);
569                }
570            }
571        }
572
573        let balance = compute_balance(part_a.len(), part_b.len());
574        let mut result = SeparatorResult {
575            separator,
576            part_a,
577            part_b,
578            balance_ratio: balance,
579        };
580        fm_refine(graph, &mut result);
581        result
582    }
583
584    /// Trivial separator for tiny subgraphs.
585    fn trivial_separator(subgraph: &[usize]) -> SeparatorResult {
586        match subgraph.len() {
587            0 => SeparatorResult {
588                separator: Vec::new(),
589                part_a: Vec::new(),
590                part_b: Vec::new(),
591                balance_ratio: 1.0,
592            },
593            1 => SeparatorResult {
594                separator: subgraph.to_vec(),
595                part_a: Vec::new(),
596                part_b: Vec::new(),
597                balance_ratio: 1.0,
598            },
599            2 => SeparatorResult {
600                separator: vec![subgraph[0]],
601                part_a: vec![subgraph[1]],
602                part_b: Vec::new(),
603                balance_ratio: 0.0,
604            },
605            _ => SeparatorResult {
606                separator: vec![subgraph[1]],
607                part_a: vec![subgraph[0]],
608                part_b: vec![subgraph[2]],
609                balance_ratio: 1.0,
610            },
611        }
612    }
613}
614
615// ---------------------------------------------------------------------------
616// MinimumDegreeOrdering
617// ---------------------------------------------------------------------------
618
619/// Approximate minimum degree ordering for small subgraphs.
620pub struct MinimumDegreeOrdering;
621
622impl MinimumDegreeOrdering {
623    /// Compute an approximate minimum degree ordering for the given vertices.
624    ///
625    /// Greedily eliminates the vertex with minimum degree at each step.
626    pub fn compute(graph: &AdjacencyGraph, vertices: &[usize]) -> Vec<usize> {
627        if vertices.is_empty() {
628            return Vec::new();
629        }
630        let n = graph.num_vertices;
631        let mut in_sub = vec![false; n];
632        for &v in vertices {
633            in_sub[v] = true;
634        }
635        let mut eliminated = vec![false; n];
636        let mut order = Vec::with_capacity(vertices.len());
637
638        for _ in 0..vertices.len() {
639            // Find vertex with minimum current degree
640            let mut best = None;
641            let mut best_deg = usize::MAX;
642            for &v in vertices {
643                if eliminated[v] {
644                    continue;
645                }
646                let deg = graph
647                    .neighbors(v)
648                    .iter()
649                    .filter(|&&nb| in_sub[nb] && !eliminated[nb])
650                    .count();
651                if deg < best_deg {
652                    best_deg = deg;
653                    best = Some(v);
654                }
655            }
656            if let Some(v) = best {
657                order.push(v);
658                eliminated[v] = true;
659            }
660        }
661        order
662    }
663}
664
665// ---------------------------------------------------------------------------
666// NestedDissectionOrdering
667// ---------------------------------------------------------------------------
668
669/// Threshold below which we switch to minimum degree ordering.
670const ND_THRESHOLD: usize = 32;
671
672/// Maximum recursion depth to prevent stack overflow.
673const MAX_DEPTH: usize = 64;
674
675/// Nested dissection ordering algorithm.
676///
677/// Recursively finds vertex separators to split the graph, orders each
678/// partition recursively, then places separator vertices last. This produces
679/// a fill-reducing ordering for sparse Cholesky/LU factorization.
680pub struct NestedDissectionOrdering;
681
682impl NestedDissectionOrdering {
683    /// Compute a fill-reducing permutation via nested dissection.
684    pub fn compute(graph: &AdjacencyGraph) -> SolverResult<Permutation> {
685        let n = graph.num_vertices;
686        if n == 0 {
687            return Permutation::new(Vec::new());
688        }
689        let vertices: Vec<usize> = (0..n).collect();
690        let mut order = Vec::with_capacity(n);
691        Self::recurse(graph, &vertices, &mut order, 0);
692        // order now contains vertices in elimination order
693        // perm[i] = which original vertex goes to position i
694        Permutation::new(order)
695    }
696
697    /// Recursive nested dissection.
698    fn recurse(graph: &AdjacencyGraph, subgraph: &[usize], order: &mut Vec<usize>, depth: usize) {
699        if subgraph.len() <= ND_THRESHOLD || depth >= MAX_DEPTH {
700            let md_order = MinimumDegreeOrdering::compute(graph, subgraph);
701            order.extend(md_order);
702            return;
703        }
704
705        let sep = VertexSeparator::find_separator(graph, subgraph);
706
707        // Recurse on A, then B
708        if !sep.part_a.is_empty() {
709            Self::recurse(graph, &sep.part_a, order, depth + 1);
710        }
711        if !sep.part_b.is_empty() {
712            Self::recurse(graph, &sep.part_b, order, depth + 1);
713        }
714        // Separator last
715        order.extend(&sep.separator);
716    }
717}
718
719// ---------------------------------------------------------------------------
720// analyze_ordering
721// ---------------------------------------------------------------------------
722
723/// Analyze the quality of an ordering by estimating fill-in via symbolic
724/// factorization.
725pub fn analyze_ordering(graph: &AdjacencyGraph, perm: &Permutation) -> OrderingQuality {
726    let n = graph.num_vertices;
727    if n == 0 {
728        return OrderingQuality {
729            estimated_fill: 0,
730            separator_size: 0,
731            balance_ratio: 1.0,
732        };
733    }
734
735    // Symbolic Cholesky: estimate fill using elimination tree
736    let iperm = perm.inverse();
737    let mut fill = 0usize;
738    let mut parent = vec![n; n]; // elimination tree parent (n = root)
739
740    for i in 0..n {
741        let orig = perm.perm[i]; // original vertex eliminated at step i
742        let mut visited = vec![false; n];
743        visited[i] = true;
744        for &nb in graph.neighbors(orig) {
745            let mut j = iperm[nb]; // position of neighbor in elimination order
746            // Walk up elimination tree
747            while j < n && !visited[j] && j > i {
748                visited[j] = true;
749                fill += 1;
750                if parent[j] == n || parent[j] > i {
751                    // j has no parent yet or we found a closer one
752                    // Don't overwrite if already set to something smaller
753                    if parent[j] == n {
754                        parent[j] = i;
755                    }
756                }
757                j = parent[j];
758            }
759        }
760    }
761
762    // Get separator info from top-level partition
763    let vertices: Vec<usize> = (0..n).collect();
764    let sep = if n > 3 {
765        VertexSeparator::find_separator(graph, &vertices)
766    } else {
767        SeparatorResult {
768            separator: Vec::new(),
769            part_a: Vec::new(),
770            part_b: Vec::new(),
771            balance_ratio: 1.0,
772        }
773    };
774
775    OrderingQuality {
776        estimated_fill: fill,
777        separator_size: sep.separator.len(),
778        balance_ratio: sep.balance_ratio,
779    }
780}
781
782// ---------------------------------------------------------------------------
783// Tests
784// ---------------------------------------------------------------------------
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    /// Build a simple path graph: 0-1-2-..-(n-1)
791    fn path_graph(n: usize) -> AdjacencyGraph {
792        let mut row_ptr = vec![0i32];
793        let mut col_indices = Vec::new();
794        for i in 0..n {
795            if i > 0 {
796                col_indices.push((i - 1) as i32);
797            }
798            if i + 1 < n {
799                col_indices.push((i + 1) as i32);
800            }
801            row_ptr.push(col_indices.len() as i32);
802        }
803        AdjacencyGraph::from_symmetric_csr(&row_ptr, &col_indices, n)
804    }
805
806    /// Build a grid graph of size rows x cols.
807    fn grid_graph(rows: usize, cols: usize) -> AdjacencyGraph {
808        let n = rows * cols;
809        let mut row_ptr = vec![0i32];
810        let mut col_indices = Vec::new();
811        for r in 0..rows {
812            for c in 0..cols {
813                let v = r * cols + c;
814                if r > 0 {
815                    col_indices.push((v - cols) as i32);
816                }
817                if c > 0 {
818                    col_indices.push((v - 1) as i32);
819                }
820                if c + 1 < cols {
821                    col_indices.push((v + 1) as i32);
822                }
823                if r + 1 < rows {
824                    col_indices.push((v + cols) as i32);
825                }
826                row_ptr.push(col_indices.len() as i32);
827            }
828        }
829        AdjacencyGraph::from_symmetric_csr(&row_ptr, &col_indices, n)
830    }
831
832    #[test]
833    fn permutation_identity() {
834        let p = Permutation::identity(5);
835        assert_eq!(p.perm, vec![0, 1, 2, 3, 4]);
836        assert_eq!(p.inverse(), &[0, 1, 2, 3, 4]);
837    }
838
839    #[test]
840    fn permutation_valid() {
841        let p = Permutation::new(vec![2, 0, 1]).expect("valid perm");
842        assert_eq!(p.perm, vec![2, 0, 1]);
843        assert_eq!(p.iperm, vec![1, 2, 0]);
844    }
845
846    #[test]
847    fn permutation_invalid_duplicate() {
848        let result = Permutation::new(vec![0, 0, 1]);
849        assert!(result.is_err());
850    }
851
852    #[test]
853    fn permutation_invalid_out_of_range() {
854        let result = Permutation::new(vec![0, 5, 2]);
855        assert!(result.is_err());
856    }
857
858    #[test]
859    fn permutation_apply() {
860        let p = Permutation::new(vec![2, 0, 1]).expect("valid perm");
861        let data = vec![10, 20, 30];
862        let result = p.apply(&data);
863        assert_eq!(result, vec![30, 10, 20]);
864    }
865
866    #[test]
867    fn permutation_compose() {
868        let p1 = Permutation::new(vec![1, 2, 0]).expect("valid");
869        let p2 = Permutation::new(vec![2, 0, 1]).expect("valid");
870        let composed = p1.compose(&p2).expect("compose");
871        // composed[i] = p1.perm[p2.perm[i]]
872        // p2.perm = [2,0,1], p1.perm = [1,2,0]
873        // composed[0] = p1[2] = 0, composed[1] = p1[0] = 1, composed[2] = p1[1] = 2
874        assert_eq!(composed.perm, vec![0, 1, 2]);
875    }
876
877    #[test]
878    fn adjacency_graph_path() {
879        let g = path_graph(5);
880        assert_eq!(g.num_vertices, 5);
881        assert_eq!(g.degree(0), 1);
882        assert_eq!(g.degree(2), 2);
883        assert_eq!(g.degree(4), 1);
884        assert_eq!(g.neighbors(2), &[1, 3]);
885    }
886
887    #[test]
888    fn adjacency_graph_grid() {
889        let g = grid_graph(3, 3);
890        assert_eq!(g.num_vertices, 9);
891        // Corner has 2 neighbors
892        assert_eq!(g.degree(0), 2);
893        // Center has 4 neighbors
894        assert_eq!(g.degree(4), 4);
895    }
896
897    #[test]
898    fn separator_splits_graph() {
899        let g = grid_graph(4, 4);
900        let verts: Vec<usize> = (0..16).collect();
901        let sep = VertexSeparator::find_separator(&g, &verts);
902        // All vertices accounted for
903        let total = sep.separator.len() + sep.part_a.len() + sep.part_b.len();
904        assert_eq!(total, 16);
905        // Separator is non-empty
906        assert!(!sep.separator.is_empty());
907        // No edge between part_a and part_b (they are separated)
908        let n = g.num_vertices;
909        let mut part = vec![0u8; n];
910        for &v in &sep.part_a {
911            part[v] = 1;
912        }
913        for &v in &sep.part_b {
914            part[v] = 2;
915        }
916        for &v in &sep.part_a {
917            for &nb in g.neighbors(v) {
918                assert_ne!(part[nb], 2, "edge {v}-{nb} crosses separator");
919            }
920        }
921    }
922
923    #[test]
924    fn nd_ordering_valid_permutation() {
925        let g = grid_graph(4, 4);
926        let perm = NestedDissectionOrdering::compute(&g).expect("nd ordering");
927        assert_eq!(perm.perm.len(), 16);
928        // Check it's a valid permutation
929        let mut sorted = perm.perm.clone();
930        sorted.sort_unstable();
931        assert_eq!(sorted, (0..16).collect::<Vec<_>>());
932    }
933
934    #[test]
935    fn nd_ordering_empty_graph() {
936        let g = AdjacencyGraph::from_symmetric_csr(&[0], &[], 0);
937        let perm = NestedDissectionOrdering::compute(&g).expect("empty");
938        assert!(perm.perm.is_empty());
939    }
940
941    #[test]
942    fn nd_ordering_single_vertex() {
943        let g = AdjacencyGraph::from_symmetric_csr(&[0, 0], &[], 1);
944        let perm = NestedDissectionOrdering::compute(&g).expect("single");
945        assert_eq!(perm.perm, vec![0]);
946    }
947
948    #[test]
949    fn nd_reduces_fill_vs_natural() {
950        let g = grid_graph(8, 8);
951        let nd_perm = NestedDissectionOrdering::compute(&g).expect("nd");
952        let natural_perm = Permutation::identity(64);
953        let nd_quality = analyze_ordering(&g, &nd_perm);
954        let nat_quality = analyze_ordering(&g, &natural_perm);
955        // ND should produce less fill than natural ordering for grid
956        assert!(
957            nd_quality.estimated_fill <= nat_quality.estimated_fill,
958            "ND fill {} should be <= natural fill {}",
959            nd_quality.estimated_fill,
960            nat_quality.estimated_fill
961        );
962    }
963
964    #[test]
965    fn minimum_degree_ordering_valid() {
966        let g = path_graph(10);
967        let verts: Vec<usize> = (0..10).collect();
968        let order = MinimumDegreeOrdering::compute(&g, &verts);
969        assert_eq!(order.len(), 10);
970        let mut sorted = order.clone();
971        sorted.sort_unstable();
972        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
973    }
974
975    #[test]
976    fn ordering_quality_metrics() {
977        let g = grid_graph(4, 4);
978        let perm = NestedDissectionOrdering::compute(&g).expect("nd");
979        let quality = analyze_ordering(&g, &perm);
980        assert!(quality.estimated_fill < 200, "fill should be reasonable");
981        assert!(quality.balance_ratio >= 0.0 && quality.balance_ratio <= 1.0);
982    }
983
984    #[test]
985    fn larger_grid_nd() {
986        let g = grid_graph(16, 16);
987        let perm = NestedDissectionOrdering::compute(&g).expect("nd 16x16");
988        assert_eq!(perm.perm.len(), 256);
989        let mut sorted = perm.perm.clone();
990        sorted.sort_unstable();
991        assert_eq!(sorted, (0..256).collect::<Vec<_>>());
992    }
993}