Skip to main content

feral_kahip/
lib.rs

1//! KaHIP-style flow-based nested-dissection fill-reducing ordering.
2//!
3//! **Status: phases K1-K6 complete.**
4//! [`kahip_order`] produces a contract-conforming permutation via the
5//! full pipeline: K1 data reduction (degree-1 / degree-2 / twin /
6//! subset), then K2-K6 multilevel flow-based nested dissection on the
7//! reduced graph (coarsen → initial bisect → uncoarsen with K3 flow
8//! refinement → K4 boundary-bipartite node separator → recurse), then
9//! K1 expansion to lift the reduced-graph permutation back to original
10//! indices.
11//!
12//! **Plan.** `dev/plans/ordering-kahip.md` tracks the six
13//! implementation phases:
14//!   - K1: Data reduction (degree-1 / degree-2 / twin / neighborhood-
15//!     subset rules, fixed-point loop, expansion permutation stack).
16//!   - K2: Push-relabel max-flow (with gap relabeling).
17//!   - K3: Flow-based edge refinement (band extraction, super-source/
18//!     sink construction, Most Balanced Min Cut).
19//!   - K4: Flow-based node separator (vertex-capacitated max-flow).
20//!   - K5: V-cycle / F-cycle controller (cut-edge-preserving
21//!     re-coarsening for monotone quality improvement).
22//!   - K6: Driver and Fast / Eco / Strong modes.
23//!
24//! **Reference papers** (published, public-domain algorithms — the
25//! implementation must be clean-room from these sources, not from
26//! KaHIP's C++ codebase):
27//!   - Sanders & Schulz, "Engineering Multilevel Graph Partitioning
28//!     Algorithms" (2011) — the kaffpa framework.
29//!   - Ost, Schulz & Strash, "Engineering Data Reduction for Nested
30//!     Dissection" (2021) — the K1 reduction rules.
31//!
32//! The public surface conforms to the FERAL ordering-crate contract
33//! (`dev/plans/ordering-crate-contract.md`): `CscPattern`,
34//! `OrderingStats`, `OrderingError`, and `CONTRACT_VERSION` are
35//! re-exported from `feral-ordering-core`.
36
37#![forbid(unsafe_code)]
38#![deny(missing_docs)]
39
40pub use feral_ordering_core::{CscPattern, OrderingError, OrderingStats, CONTRACT_VERSION};
41
42// Phase K1: data reduction (Ost-Schulz-Strash 2021). Wired into the
43// K6 driver (see `node_nd::kahip_nd_order`) as a fixed-point pre-pass
44// that shrinks the graph via degree-1 / degree-2 / twin / subset rules
45// before multilevel partitioning. Eliminated vertices are expanded
46// back into the final permutation via `expand_permutation`. See
47// `dev/plans/ordering-kahip.md` and `dev/research/ordering-kahip-k1.md`.
48mod data_reduction;
49
50// Phase K2: push-relabel max-flow / min-cut (Goldberg-Tarjan 1988 +
51// Cherkassky-Goldberg 1995 gap relabeling). Internal until K3 (flow-
52// based edge refinement) consumes it; see
53// `dev/plans/ordering-kahip.md` and `dev/research/ordering-kahip-k2.md`.
54#[allow(dead_code)]
55mod flow;
56
57// Phase K3 scaffolding: shared undirected-graph type (CSR) used by
58// K3/K4/K5/K6, and flow-based edge refinement of a bisection.
59// Internal until K5/K6 consume them; see
60// `dev/plans/ordering-kahip.md` and `dev/research/ordering-kahip-k3.md`.
61#[allow(dead_code)]
62mod flow_refine;
63#[allow(dead_code)]
64mod graph;
65
66// Phase K4: flow-based node separator via boundary-bipartite vertex
67// cover (König's theorem reduction). Internal until K5/K6 consume
68// it; see `dev/plans/ordering-kahip.md` and
69// `dev/research/ordering-kahip-k4.md`.
70#[allow(dead_code)]
71mod node_separator;
72
73// Phase K5 (multilevel bisection controller) and K6 (recursive ND
74// driver). K5 reuses feral-metis's coarsening / initial-partition / FM
75// plumbing and plugs in K3 flow refinement at each uncoarsening level.
76// K6 walks connected components, recurses on each, and layers K4 on top
77// of K5 to produce a node separator at every internal level.
78mod cycle;
79mod node_nd;
80
81/// Crate-specific diagnostic statistics.
82///
83/// Populated by [`kahip_order_full`] once the implementation lands;
84/// zeroed while the crate is in its scaffold state.
85#[derive(Debug, Default, Clone)]
86pub struct KahipStats {
87    /// Number of vertices after data-reduction preprocessing.
88    /// `reduced_n == 0` indicates the reduction phase has not run
89    /// (scaffold state).
90    pub reduced_n: usize,
91    /// Largest max-flow subproblem size, in vertices, encountered
92    /// during flow-based refinement. `0` while scaffolded.
93    pub max_flow_vertices: usize,
94    /// Number of multilevel bisections performed — one per node-separator
95    /// computation across the nested-dissection tree. Each is a single
96    /// V-cycle (one coarsen followed by one uncoarsen). `0` while
97    /// scaffolded.
98    pub cycles: usize,
99    /// Number of top-level connected components encountered by the
100    /// nested-dissection driver. `0` while scaffolded. Matches the
101    /// `n_components` field on `MetisStats` / `ScotchStats`.
102    pub n_components: u32,
103}
104
105/// Quality / speed tradeoff modes for the KaHIP driver.
106///
107/// The exact tuning of each mode is fixed once phase K6 lands. Until
108/// then the enum is reserved so that callers can encode intent
109/// without the crate compiling the mapping.
110#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
111pub enum KahipMode {
112    /// METIS-comparable wall-clock; single multilevel pass.
113    #[default]
114    Fast,
115    /// 2-3× Fast; one V-cycle with flow refinement at the finest
116    /// level.
117    Eco,
118    /// 5-10× Fast; F-cycle with flow refinement at every level.
119    Strong,
120}
121
122/// Tunable parameters for KaHIP nested-dissection ordering.
123///
124/// Kept intentionally narrow while the crate is a scaffold —
125/// defaults will match KaHIP's library defaults (seed=0, mode=Fast)
126/// once phase K6 is implemented.
127#[derive(Debug, Clone)]
128pub struct KahipOptions {
129    /// Deterministic RNG seed. Two runs with the same seed on the
130    /// same input must produce the same permutation.
131    pub seed: u64,
132    /// Quality / speed tradeoff. See [`KahipMode`].
133    pub mode: KahipMode,
134}
135
136impl Default for KahipOptions {
137    fn default() -> Self {
138        Self {
139            seed: 1,
140            mode: KahipMode::default(),
141        }
142    }
143}
144
145/// Compute a fill-reducing KaHIP nested-dissection ordering.
146///
147/// Thin wrapper over [`kahip_order_full`] that discards the
148/// diagnostic stats. Returns a permutation `perm` (new-to-old).
149///
150/// Runs the K2-K6 pipeline with default options; see
151/// [`kahip_order_full`] for the tunable entry point.
152pub fn kahip_order(pattern: &CscPattern<'_>) -> Result<Vec<i32>, OrderingError> {
153    kahip_order_full(pattern, &KahipOptions::default()).map(|(perm, _, _)| perm)
154}
155
156/// Contract-conforming ordering producer.
157///
158/// Signature matches the shape every FERAL ordering crate must expose
159/// per `dev/plans/ordering-crate-contract.md`: input is a
160/// full-symmetric [`CscPattern`] and options; output is a three-tuple
161/// of `(perm, OrderingStats, crate-stats)`, with errors in
162/// [`OrderingError`].
163///
164/// Runs the K2-K6 pipeline: K5 multilevel edge bisection (coarsen,
165/// initial bisect, uncoarsen with K3 flow refinement at each level),
166/// K4 boundary-bipartite vertex cover to lift the bisection to a node
167/// separator, and recursive nested dissection with an AMD leaf
168/// fallback for subgraphs below the mode-dependent switch.
169///
170/// `OrderingStats.time_us` is the wall-clock time of this call.
171/// `fill_estimate` and `flop_estimate` stay `None` — KaHIP does not
172/// produce them at the ordering boundary; they belong to a downstream
173/// symbolic analysis.
174pub fn kahip_order_full(
175    pattern: &CscPattern<'_>,
176    opts: &KahipOptions,
177) -> Result<(Vec<i32>, OrderingStats, KahipStats), OrderingError> {
178    if pattern.col_ptr.len() != pattern.n + 1 {
179        return Err(OrderingError::MalformedInput);
180    }
181    let t0 = std::time::Instant::now();
182    let mut stats = KahipStats::default();
183    let perm = node_nd::kahip_nd_order(pattern, opts, &mut stats)?;
184    let ordering_stats = OrderingStats {
185        time_us: t0.elapsed().as_micros() as u64,
186        fill_estimate: None,
187        flop_estimate: None,
188    };
189    Ok((perm, ordering_stats, stats))
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn scaffold_rejects_malformed_input() {
198        let col_ptr = [0i32, 0];
199        let row_idx: [i32; 0] = [];
200        let pattern = CscPattern::new(5, &col_ptr, &row_idx);
201        assert!(pattern.is_none(), "malformed pattern must fail validation");
202    }
203
204    #[test]
205    fn diagonal_pattern_yields_valid_permutation() {
206        let col_ptr = [0i32, 1, 2, 3];
207        let row_idx = [0i32, 1, 2];
208        let pattern = CscPattern::new(3, &col_ptr, &row_idx).expect("valid pattern");
209        let perm = kahip_order(&pattern).expect("ordering ok");
210        assert_eq!(perm.len(), 3);
211        let mut seen = [false; 3];
212        for &p in &perm {
213            assert!((0..3).contains(&p));
214            seen[p as usize] = true;
215        }
216        assert!(seen.iter().all(|&s| s));
217    }
218
219    #[test]
220    fn scaffold_propagates_malformed_input_to_caller() {
221        // Caller-side malformed check: col_ptr len mismatch.
222        // We have to construct this manually since CscPattern::new
223        // refuses it — so build the struct through a sibling-crate
224        // pattern then corrupt through direct field access is not
225        // possible. Instead, test the same invariant via public API.
226        let col_ptr = [0i32, 2];
227        let row_idx = [0i32, 1];
228        let pattern = CscPattern::new(1, &col_ptr, &row_idx);
229        assert!(
230            pattern.is_none(),
231            "n=1 but col_ptr suggests 1 column with 2 rows"
232        );
233    }
234}