Skip to main content

fnx_algorithms/
network_simplex_int.rs

1//! Exact-integer primal network-simplex kernel (first-class Rust API).
2//!
3//! A faithful transcription of NetworkX's `network_simplex`
4//! (`networkx/algorithms/flow/networksimplex.py`, BSD-licensed) pivot logic,
5//! specialised to integer demands/capacities/weights — the common, and the
6//! only byte-exact-integer, case. All arithmetic here is `i64`; there is no
7//! `f64` anywhere on this path, so results are exact and reproducible with no
8//! rounding.
9//!
10//! This kernel was hoisted out of the PyO3 binding crate (`fnx-python`) so that
11//! sibling Rust consumers (e.g. MTDT's integer voice-leading optimal transport)
12//! can use the exact-integer min-cost-flow path directly, without taking a
13//! dependency on `fnx-python`/PyO3. `fnx-python` now calls this same code, so
14//! there is a single source of truth for the pivot logic and both the Rust API
15//! and the Python binding produce identical numeric results.
16//!
17//! Determinism: the entering-edge search (block search / Dantzig + Bland),
18//! the leaving-edge tie-break (first minimiser in reversed cycle order,
19//! matching Python's `min`), and every array update below are deterministic
20//! functions of the input arrays in the order they are supplied. Given the same
21//! `(node_demands, src, tgt, cap, wt)` the solver always returns the same
22//! `(status, cost, flows)` — see the `determinism` unit test.
23//!
24//! Convention (identical to NetworkX): a node's `demand` is positive when the
25//! node is a *sink* (consumes flow) and negative when it is a *source*
26//! (supplies flow); the demands must sum to zero for a feasible problem.
27//!
28//! NetworkX uses a dummy root node and encodes "no parent" / the root via
29//! Python's negative-index alias `-1` (which addresses the dummy root stored at
30//! index `n`). Here the root is the explicit index `n`, and a true `None`
31//! parent is `NONE = usize::MAX`.
32
33#![allow(clippy::needless_range_loop)]
34
35const NONE: usize = usize::MAX;
36
37/// Sentinel used inside [`network_simplex_int`] to mean "unbounded / +infinity"
38/// capacity. Any capacity `>= i64::MAX` is treated as infinite. Rust callers
39/// that want an infinite-capacity edge should pass [`i64::MAX`] for that edge's
40/// capacity.
41pub const CAP_INF: i64 = i64::MAX;
42
43/// Result status of an exact-integer network-simplex solve.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum NetworkSimplexStatus {
46    /// A minimum-cost flow satisfying all demands was found.
47    Optimal,
48    /// No flow satisfies all node demands (e.g. demands do not sum to zero, or
49    /// insufficient capacity connects sources to sinks).
50    Infeasible,
51    /// The problem is unbounded below (a negative-cost cycle of infinite
52    /// capacity exists).
53    Unbounded,
54}
55
56/// Outcome of [`network_simplex_int`].
57///
58/// On [`NetworkSimplexStatus::Optimal`], `cost` is the exact minimum total cost
59/// and `flows[i]` is the (integer) flow assigned to input edge `i` (the edges
60/// in the same order they were supplied to [`network_simplex_int`]). On the
61/// non-optimal statuses, `cost` is `0` and `flows` is empty.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct NetworkSimplexIntSolution {
64    /// Solve status.
65    pub status: NetworkSimplexStatus,
66    /// Exact total cost of the flow (0 when not optimal).
67    pub cost: i64,
68    /// Flow on each input edge, in input order (empty when not optimal).
69    pub flows: Vec<i64>,
70}
71
72/// Benchmark-only selector for the frozen allocating cycle builder and the
73/// solve-local scratch implementation.
74#[cfg(any(test, feature = "bench-internals"))]
75#[doc(hidden)]
76#[derive(Clone, Copy, Debug)]
77pub enum NetworkSimplexCycleArm {
78    /// Preserve the pre-scratch behavior: allocate four path vectors per pivot.
79    Allocating,
80    /// Clear and refill four solve-local path buffers across pivots.
81    ReusedScratch,
82}
83
84#[cfg(test)]
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86struct CycleTrace {
87    pivots: usize,
88    path_calls: usize,
89    path_hops: usize,
90}
91
92#[cfg(test)]
93thread_local! {
94    static CYCLE_TRACE: std::cell::Cell<CycleTrace> =
95        const { std::cell::Cell::new(CycleTrace { pivots: 0, path_calls: 0, path_hops: 0 }) };
96}
97
98#[cfg(test)]
99fn record_path_trace(hops: usize) {
100    CYCLE_TRACE.with(|trace| {
101        let mut current = trace.get();
102        current.path_calls += 1;
103        current.path_hops += hops;
104        trace.set(current);
105    });
106}
107
108struct Simplex {
109    // length edge_count + n (artificial edges appended)
110    edge_sources: Vec<usize>,
111    edge_targets: Vec<usize>,
112    edge_capacities: Vec<i64>,
113    edge_weights: Vec<i64>,
114    edge_flow: Vec<i64>,
115    // length n
116    node_potentials: Vec<i64>,
117    // length n + 1 (index n == root)
118    parent: Vec<usize>,
119    parent_edge: Vec<usize>,
120    subtree_size: Vec<usize>,
121    next_node_dft: Vec<usize>,
122    prev_node_dft: Vec<usize>,
123    last_descendent_dft: Vec<usize>,
124}
125
126#[derive(Default)]
127struct CycleScratch {
128    nodes: Vec<usize>,
129    edges: Vec<usize>,
130    right_nodes: Vec<usize>,
131    right_edges: Vec<usize>,
132}
133
134impl Simplex {
135    #[inline]
136    fn reduced_cost(&self, i: usize) -> i64 {
137        let c = self.edge_weights[i] - self.node_potentials[self.edge_sources[i]]
138            + self.node_potentials[self.edge_targets[i]];
139        if self.edge_flow[i] == 0 { c } else { -c }
140    }
141
142    #[inline]
143    fn residual_capacity(&self, i: usize, p: usize) -> i64 {
144        if self.edge_sources[i] == p {
145            self.edge_capacities[i] - self.edge_flow[i]
146        } else {
147            self.edge_flow[i]
148        }
149    }
150
151    fn find_apex(&self, mut p: usize, mut q: usize) -> usize {
152        let mut size_p = self.subtree_size[p];
153        let mut size_q = self.subtree_size[q];
154        loop {
155            while size_p < size_q {
156                p = self.parent[p];
157                size_p = self.subtree_size[p];
158            }
159            while size_p > size_q {
160                q = self.parent[q];
161                size_q = self.subtree_size[q];
162            }
163            if size_p == size_q {
164                if p != q {
165                    p = self.parent[p];
166                    size_p = self.subtree_size[p];
167                    q = self.parent[q];
168                    size_q = self.subtree_size[q];
169                } else {
170                    return p;
171                }
172            }
173        }
174    }
175
176    /// Frozen allocating baseline for the cycle-scratch A/B test.
177    #[cfg(any(test, feature = "bench-internals"))]
178    fn trace_path(&self, mut p: usize, w: usize) -> (Vec<usize>, Vec<usize>) {
179        let mut wn = vec![p];
180        let mut we = Vec::new();
181        while p != w {
182            we.push(self.parent_edge[p]);
183            p = self.parent[p];
184            wn.push(p);
185        }
186        #[cfg(test)]
187        record_path_trace(we.len());
188        (wn, we)
189    }
190
191    /// Frozen allocating baseline for the cycle-scratch A/B test.
192    #[cfg(any(test, feature = "bench-internals"))]
193    fn find_cycle(&self, i: usize, p: usize, q: usize) -> (Vec<usize>, Vec<usize>) {
194        let w = self.find_apex(p, q);
195        let (mut wn, mut we) = self.trace_path(p, w);
196        wn.reverse();
197        we.reverse();
198        if we.len() != 1 || we[0] != i {
199            we.push(i);
200        }
201        let (mut wnr, wer) = self.trace_path(q, w);
202        wnr.pop(); // del WnR[-1]
203        wn.extend(wnr);
204        we.extend(wer);
205        (wn, we)
206    }
207
208    fn trace_path_into(
209        &self,
210        mut p: usize,
211        w: usize,
212        nodes: &mut Vec<usize>,
213        edges: &mut Vec<usize>,
214    ) {
215        nodes.clear();
216        edges.clear();
217        nodes.push(p);
218        while p != w {
219            edges.push(self.parent_edge[p]);
220            p = self.parent[p];
221            nodes.push(p);
222        }
223        #[cfg(test)]
224        record_path_trace(edges.len());
225    }
226
227    /// Fills solve-local scratch with the fundamental cycle created by adding
228    /// edge `i == (p, q)`, oriented from `p` to `q`.
229    fn find_cycle_into(&self, i: usize, p: usize, q: usize, scratch: &mut CycleScratch) {
230        let w = self.find_apex(p, q);
231        self.trace_path_into(p, w, &mut scratch.nodes, &mut scratch.edges);
232        scratch.nodes.reverse();
233        scratch.edges.reverse();
234        if scratch.edges.len() != 1 || scratch.edges[0] != i {
235            scratch.edges.push(i);
236        }
237        self.trace_path_into(q, w, &mut scratch.right_nodes, &mut scratch.right_edges);
238        scratch.right_nodes.pop(); // del WnR[-1]
239        scratch.nodes.extend_from_slice(&scratch.right_nodes);
240        scratch.edges.extend_from_slice(&scratch.right_edges);
241    }
242
243    fn augment_flow(&mut self, wn: &[usize], we: &[usize], f: i64) {
244        for (idx, &i) in we.iter().enumerate() {
245            let p = wn[idx];
246            if self.edge_sources[i] == p {
247                self.edge_flow[i] += f;
248            } else {
249                self.edge_flow[i] -= f;
250            }
251        }
252    }
253
254    /// Returns (j, s, t): the leaving edge in the cycle (Wn, We).
255    /// nx: `min(zip(reversed(We), reversed(Wn)), key=residual_capacity)` — the
256    /// FIRST minimiser in reversed order (Python min keeps the first on ties).
257    fn find_leaving_edge(&self, wn: &[usize], we: &[usize]) -> (usize, usize, usize) {
258        let mut best_j = we[we.len() - 1];
259        let mut best_s = wn[wn.len() - 1];
260        let mut best_rc = self.residual_capacity(best_j, best_s);
261        // iterate reversed; keep first minimiser (strictly-less replaces)
262        for k in (0..we.len()).rev() {
263            let j = we[k];
264            let s = wn[k];
265            let rc = self.residual_capacity(j, s);
266            if rc < best_rc {
267                best_rc = rc;
268                best_j = j;
269                best_s = s;
270            }
271        }
272        let t = if self.edge_sources[best_j] == best_s {
273            self.edge_targets[best_j]
274        } else {
275            self.edge_sources[best_j]
276        };
277        (best_j, best_s, t)
278    }
279
280    #[inline]
281    fn process_cycle(&mut self, i: usize, mut p: usize, mut q: usize, wn: &[usize], we: &[usize]) {
282        let (j, mut s, mut t) = self.find_leaving_edge(wn, we);
283        let rc = self.residual_capacity(j, s);
284        self.augment_flow(wn, we, rc);
285        if i != j {
286            if self.parent[t] != s {
287                std::mem::swap(&mut s, &mut t);
288            }
289            // We.index(i) > We.index(j): first occurrence positions
290            let pos_i = we
291                .iter()
292                .position(|&edge| edge == i)
293                .expect("fundamental cycle must contain its entering edge");
294            let pos_j = we
295                .iter()
296                .position(|&edge| edge == j)
297                .expect("fundamental cycle must contain its leaving edge");
298            if pos_i > pos_j {
299                std::mem::swap(&mut p, &mut q);
300            }
301            self.remove_edge(s, t);
302            self.make_root(q);
303            self.add_edge(i, p, q);
304            self.update_potentials(i, p, q);
305        }
306    }
307
308    fn remove_edge(&mut self, s: usize, t: usize) {
309        let size_t = self.subtree_size[t];
310        let prev_t = self.prev_node_dft[t];
311        let last_t = self.last_descendent_dft[t];
312        let next_last_t = self.next_node_dft[last_t];
313        self.parent[t] = NONE;
314        self.parent_edge[t] = NONE;
315        self.next_node_dft[prev_t] = next_last_t;
316        self.prev_node_dft[next_last_t] = prev_t;
317        self.next_node_dft[last_t] = t;
318        self.prev_node_dft[t] = last_t;
319        let mut s = s;
320        while s != NONE {
321            self.subtree_size[s] -= size_t;
322            if self.last_descendent_dft[s] == last_t {
323                self.last_descendent_dft[s] = prev_t;
324            }
325            s = self.parent[s];
326        }
327    }
328
329    fn make_root(&mut self, q: usize) {
330        let mut ancestors = Vec::new();
331        let mut cur = q;
332        while cur != NONE {
333            ancestors.push(cur);
334            cur = self.parent[cur];
335        }
336        ancestors.reverse();
337        for w in 0..ancestors.len().saturating_sub(1) {
338            let p = ancestors[w];
339            let q = ancestors[w + 1];
340            let size_p = self.subtree_size[p];
341            let mut last_p = self.last_descendent_dft[p];
342            let prev_q = self.prev_node_dft[q];
343            let last_q = self.last_descendent_dft[q];
344            let next_last_q = self.next_node_dft[last_q];
345            self.parent[p] = q;
346            self.parent[q] = NONE;
347            self.parent_edge[p] = self.parent_edge[q];
348            self.parent_edge[q] = NONE;
349            self.subtree_size[p] = size_p - self.subtree_size[q];
350            self.subtree_size[q] = size_p;
351            self.next_node_dft[prev_q] = next_last_q;
352            self.prev_node_dft[next_last_q] = prev_q;
353            self.next_node_dft[last_q] = q;
354            self.prev_node_dft[q] = last_q;
355            if last_p == last_q {
356                self.last_descendent_dft[p] = prev_q;
357                last_p = prev_q;
358            }
359            self.prev_node_dft[p] = last_q;
360            self.next_node_dft[last_q] = p;
361            self.next_node_dft[last_p] = q;
362            self.prev_node_dft[q] = last_p;
363            self.last_descendent_dft[q] = last_p;
364        }
365    }
366
367    fn add_edge(&mut self, i: usize, p: usize, q: usize) {
368        let last_p = self.last_descendent_dft[p];
369        let next_last_p = self.next_node_dft[last_p];
370        let size_q = self.subtree_size[q];
371        let last_q = self.last_descendent_dft[q];
372        self.parent[q] = p;
373        self.parent_edge[q] = i;
374        self.next_node_dft[last_p] = q;
375        self.prev_node_dft[q] = last_p;
376        self.prev_node_dft[next_last_p] = last_q;
377        self.next_node_dft[last_q] = next_last_p;
378        let mut p = p;
379        while p != NONE {
380            self.subtree_size[p] += size_q;
381            if self.last_descendent_dft[p] == last_p {
382                self.last_descendent_dft[p] = last_q;
383            }
384            p = self.parent[p];
385        }
386    }
387
388    fn update_potentials(&mut self, i: usize, p: usize, q: usize) {
389        let d = if q == self.edge_targets[i] {
390            self.node_potentials[p] - self.edge_weights[i] - self.node_potentials[q]
391        } else {
392            self.node_potentials[p] + self.edge_weights[i] - self.node_potentials[q]
393        };
394        // trace_subtree(q): q, then follow next_node_dft until last_descendent.
395        let l = self.last_descendent_dft[q];
396        let mut cur = q;
397        loop {
398            self.node_potentials[cur] += d;
399            if cur == l {
400                break;
401            }
402            cur = self.next_node_dft[cur];
403        }
404    }
405}
406
407/// Core solve: `cap` uses `i64::MAX` for +infinity. `node_demands` has length n;
408/// the edge arrays have length edge_count (the real, non-self-loop, non-zero-cap
409/// edges already filtered by the caller). Returns the flow on each real edge.
410fn solve_impl<const REUSE_CYCLE_SCRATCH: bool>(
411    node_demands: &[i64],
412    src: &[usize],
413    tgt: &[usize],
414    cap: &[i64], // i64::MAX represents +inf
415    wt: &[i64],
416) -> (NetworkSimplexStatus, i64, Vec<i64>) {
417    let n = node_demands.len();
418    let edge_count = src.len();
419
420    if n == 0 {
421        return (NetworkSimplexStatus::Optimal, 0, Vec::new());
422    }
423
424    let inf = i64::MAX;
425
426    // faux_inf = 3 * max(sum finite caps, sum |weights|, sum |demands|) or 1
427    let mut sum_cap: i64 = 0;
428    for &c in cap {
429        if c < inf {
430            sum_cap += c;
431        }
432    }
433    let mut sum_w: i64 = 0;
434    for &w in wt {
435        sum_w += w.abs();
436    }
437    let mut sum_d: i64 = 0;
438    for &d in node_demands {
439        sum_d += d.abs();
440    }
441    let faux_inf = {
442        let m = sum_cap.max(sum_w).max(sum_d);
443        if m == 0 { 1 } else { 3 * m }
444    };
445
446    // Build full edge arrays: real edges, then n artificial root edges.
447    let total = edge_count + n;
448    let mut edge_sources = Vec::with_capacity(total);
449    let mut edge_targets = Vec::with_capacity(total);
450    let mut edge_capacities = Vec::with_capacity(total);
451    let mut edge_weights = Vec::with_capacity(total);
452    edge_sources.extend_from_slice(src);
453    edge_targets.extend_from_slice(tgt);
454    edge_capacities.extend_from_slice(cap);
455    edge_weights.extend_from_slice(wt);
456    // artificial edges connect node i to the root (index n)
457    for (i, &d) in node_demands.iter().enumerate() {
458        if d > 0 {
459            edge_sources.push(n); // root
460            edge_targets.push(i);
461        } else {
462            edge_sources.push(i);
463            edge_targets.push(n); // root
464        }
465    }
466    edge_weights.extend(std::iter::repeat_n(faux_inf, n));
467    edge_capacities.extend(std::iter::repeat_n(faux_inf, n));
468
469    // initialize_spanning_tree(n, faux_inf)
470    let mut edge_flow = vec![0i64; edge_count];
471    edge_flow.extend(node_demands.iter().map(|d| d.abs()));
472    let node_potentials: Vec<i64> = node_demands
473        .iter()
474        .map(|&d| if d <= 0 { faux_inf } else { -faux_inf })
475        .collect();
476    // parent: nodes 0..n-1 -> root (n); root -> NONE
477    let mut parent = vec![n; n + 1];
478    parent[n] = NONE;
479    // parent_edge: node i -> artificial edge ec+i; root -> NONE
480    let mut parent_edge: Vec<usize> = (edge_count..edge_count + n).collect();
481    parent_edge.push(NONE); // index n (root)
482    // subtree_size: [1]*n + [n+1]
483    let mut subtree_size = vec![1usize; n + 1];
484    subtree_size[n] = n + 1;
485    // next_node_dft: [1,2,...,n-1, n(root), 0]
486    let mut next_node_dft = vec![0usize; n + 1];
487    for i in 0..n.saturating_sub(1) {
488        next_node_dft[i] = i + 1;
489    }
490    if n >= 1 {
491        next_node_dft[n - 1] = n; // was -1 -> root
492        next_node_dft[n] = 0;
493    }
494    // prev_node_dft: [n(root), 0, 1, ..., n-1]
495    let mut prev_node_dft = vec![0usize; n + 1];
496    prev_node_dft[0] = n; // was -1 -> root
497    for i in 1..=n {
498        prev_node_dft[i] = i - 1;
499    }
500    // last_descendent_dft: [0,1,...,n-1, n-1]
501    let mut last_descendent_dft = vec![0usize; n + 1];
502    for i in 0..n {
503        last_descendent_dft[i] = i;
504    }
505    last_descendent_dft[n] = n - 1;
506
507    let mut sx = Simplex {
508        edge_sources,
509        edge_targets,
510        edge_capacities,
511        edge_weights,
512        edge_flow,
513        node_potentials,
514        parent,
515        parent_edge,
516        subtree_size,
517        next_node_dft,
518        prev_node_dft,
519        last_descendent_dft,
520    };
521
522    // Pivot loop with the block-search (Dantzig + Bland) entering-edge finder.
523    if edge_count > 0 {
524        let b = {
525            // B = ceil(sqrt(edge_count))
526            let s = (edge_count as f64).sqrt().ceil() as usize;
527            s.max(1)
528        };
529        let big_m = edge_count.div_ceil(b);
530        let mut m = 0usize; // consecutive blocks without an eligible edge
531        let mut f = 0usize; // first edge in block
532        let mut cycle_scratch = CycleScratch::default();
533        while m < big_m {
534            // next block [f, l) cyclically over 0..edge_count
535            let l = f + b;
536            // find first edge with the lowest reduced cost in the block
537            let mut best_i = usize::MAX;
538            let mut best_c = 0i64;
539            if l <= edge_count {
540                for e in f..l {
541                    let c = sx.reduced_cost(e);
542                    if best_i == usize::MAX || c < best_c {
543                        best_c = c;
544                        best_i = e;
545                    }
546                }
547                f = l;
548            } else {
549                let l2 = l - edge_count;
550                for e in f..edge_count {
551                    let c = sx.reduced_cost(e);
552                    if best_i == usize::MAX || c < best_c {
553                        best_c = c;
554                        best_i = e;
555                    }
556                }
557                for e in 0..l2 {
558                    let c = sx.reduced_cost(e);
559                    if best_i == usize::MAX || c < best_c {
560                        best_c = c;
561                        best_i = e;
562                    }
563                }
564                f = l2;
565            }
566            let i = best_i;
567            let c = sx.reduced_cost(i);
568            if c >= 0 {
569                m += 1;
570                continue;
571            }
572            // entering edge found
573            let (p, q) = if sx.edge_flow[i] == 0 {
574                (sx.edge_sources[i], sx.edge_targets[i])
575            } else {
576                (sx.edge_targets[i], sx.edge_sources[i])
577            };
578            m = 0;
579            // process this entering edge (the loop body of the Python for-loop)
580            #[cfg(test)]
581            CYCLE_TRACE.with(|trace| {
582                let mut current = trace.get();
583                current.pivots += 1;
584                trace.set(current);
585            });
586            if REUSE_CYCLE_SCRATCH {
587                sx.find_cycle_into(i, p, q, &mut cycle_scratch);
588                sx.process_cycle(i, p, q, &cycle_scratch.nodes, &cycle_scratch.edges);
589            } else {
590                #[cfg(any(test, feature = "bench-internals"))]
591                {
592                    let (wn, we) = sx.find_cycle(i, p, q);
593                    sx.process_cycle(i, p, q, &wn, &we);
594                }
595                #[cfg(not(any(test, feature = "bench-internals")))]
596                unreachable!("allocating cycle baseline is benchmark-only");
597            }
598        }
599    }
600
601    // Infeasibility: any artificial edge (indices edge_count..edge_count+n) has
602    // nonzero flow.
603    for i in edge_count..edge_count + n {
604        if sx.edge_flow[i] != 0 {
605            return (NetworkSimplexStatus::Infeasible, 0, Vec::new());
606        }
607    }
608    // Unboundedness: any real edge has flow*2 >= faux_inf.
609    for i in 0..edge_count {
610        if sx.edge_flow[i].saturating_mul(2) >= faux_inf {
611            return (NetworkSimplexStatus::Unbounded, 0, Vec::new());
612        }
613    }
614
615    let mut cost: i64 = 0;
616    for i in 0..edge_count {
617        cost += sx.edge_weights[i] * sx.edge_flow[i];
618    }
619    let flows: Vec<i64> = sx.edge_flow[..edge_count].to_vec();
620    (NetworkSimplexStatus::Optimal, cost, flows)
621}
622
623fn solve(
624    node_demands: &[i64],
625    src: &[usize],
626    tgt: &[usize],
627    cap: &[i64],
628    wt: &[i64],
629) -> (NetworkSimplexStatus, i64, Vec<i64>) {
630    solve_impl::<true>(node_demands, src, tgt, cap, wt)
631}
632
633/// Runs one benchmark arm without changing the public production entry point.
634#[cfg(any(test, feature = "bench-internals"))]
635#[doc(hidden)]
636pub fn network_simplex_int_cycle_arm(
637    node_demands: &[i64],
638    src: &[usize],
639    tgt: &[usize],
640    cap: &[i64],
641    wt: &[i64],
642    arm: NetworkSimplexCycleArm,
643) -> NetworkSimplexIntSolution {
644    let (status, cost, flows) = match arm {
645        NetworkSimplexCycleArm::Allocating => solve_impl::<false>(node_demands, src, tgt, cap, wt),
646        NetworkSimplexCycleArm::ReusedScratch => {
647            solve_impl::<true>(node_demands, src, tgt, cap, wt)
648        }
649    };
650    NetworkSimplexIntSolution {
651        status,
652        cost,
653        flows,
654    }
655}
656
657/// Solve an exact-integer minimum-cost-flow problem via primal network simplex.
658///
659/// This is the first-class Rust entry point for the exact-integer path. It is a
660/// byte-exact-integer solver — every intermediate value is `i64`, there is no
661/// `f64` rounding anywhere, and the result is a deterministic function of the
662/// inputs in the order supplied.
663///
664/// Parameters:
665/// - `node_demands`: one entry per node (node index = position). NetworkX
666///   convention: positive = sink (consumes flow), negative = source (supplies
667///   flow). For a feasible problem the demands must sum to zero.
668/// - `src`, `tgt`: parallel arrays of edge endpoints, given as node indices into
669///   `node_demands`. `src.len() == tgt.len() == cap.len() == wt.len()` is the
670///   number of edges. Callers are expected to have already dropped self-loops
671///   and zero-capacity edges (matching the NetworkX extraction step).
672/// - `cap`: per-edge capacity. Use [`CAP_INF`] ([`i64::MAX`]) for an
673///   infinite-capacity edge.
674/// - `wt`: per-edge cost (weight) incurred per unit of flow.
675///
676/// Returns a [`NetworkSimplexIntSolution`]; on
677/// [`NetworkSimplexStatus::Optimal`], `flows[i]` is the integer flow on edge `i`
678/// in the same order the edges were supplied, and `cost` is the exact minimum
679/// total cost.
680///
681/// # Determinism & exact-integer guarantees
682///
683/// Given identical inputs, this function always returns identical output
684/// (status, cost, and per-edge flows) — the tie-breaking in the entering-edge
685/// block search and the leaving-edge selection are fixed, order-dependent
686/// rules. No floating-point arithmetic participates in the flow or cost values.
687///
688/// # Example
689///
690/// ```ignore
691/// use fnx_algorithms::network_simplex_int::{network_simplex_int, NetworkSimplexStatus, CAP_INF};
692///
693/// // Node 0 supplies 4 (demand -4), node 3 consumes 4 (demand +4).
694/// let demands = [-4, 0, 0, 4];
695/// let src = [0, 0, 1, 2];
696/// let tgt = [1, 2, 3, 3];
697/// let cap = [4, 4, 2, 4];
698/// let wt  = [1, 2, 1, 1];
699/// let sol = network_simplex_int(&demands, &src, &tgt, &cap, &wt);
700/// assert_eq!(sol.status, NetworkSimplexStatus::Optimal);
701/// assert_eq!(sol.cost, 10);
702/// let _ = CAP_INF;
703/// ```
704pub fn network_simplex_int(
705    node_demands: &[i64],
706    src: &[usize],
707    tgt: &[usize],
708    cap: &[i64],
709    wt: &[i64],
710) -> NetworkSimplexIntSolution {
711    let (status, cost, flows) = solve(node_demands, src, tgt, cap, wt);
712    NetworkSimplexIntSolution {
713        status,
714        cost,
715        flows,
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722
723    /// Flat `(node_demands, src, tgt, cap, wt)` arrays describing a fixed-size
724    /// test problem, in the argument order [`network_simplex_int`] expects.
725    type ProblemArrays = ([i64; 4], [usize; 4], [usize; 4], [i64; 4], [i64; 4]);
726
727    /// Small transportation problem with a known exact-integer optimum.
728    ///
729    /// Node 0 supplies 4 units (demand -4); node 3 consumes 4 (demand +4);
730    /// nodes 1 and 2 are transshipment. Edges (src,tgt,cap,wt):
731    ///   0->1 cap4 wt1, 0->2 cap4 wt2, 1->3 cap2 wt1, 2->3 cap4 wt1.
732    /// Cheapest route 0->1->3 (per-unit 2) is bottlenecked at 1->3 cap 2, so
733    /// 2 units flow there and the remaining 2 take 0->2->3 (per-unit 3):
734    ///   cost = 2*(1+1) + 2*(2+1) = 4 + 6 = 10.
735    fn small_problem() -> ProblemArrays {
736        let demands = [-4i64, 0, 0, 4];
737        let src = [0usize, 0, 1, 2];
738        let tgt = [1usize, 2, 3, 3];
739        let cap = [4i64, 4, 2, 4];
740        let wt = [1i64, 2, 1, 1];
741        (demands, src, tgt, cap, wt)
742    }
743
744    type OwnedProblem = (Vec<i64>, Vec<usize>, Vec<usize>, Vec<i64>, Vec<i64>);
745
746    fn assignment_problem(side: usize) -> OwnedProblem {
747        const OFFSETS: [usize; 4] = [0, 1, 7, 19];
748
749        let mut demands = vec![-1; side];
750        demands.extend(std::iter::repeat_n(1, side));
751        let mut src = Vec::with_capacity(side * OFFSETS.len());
752        let mut tgt = Vec::with_capacity(side * OFFSETS.len());
753        let mut cap = Vec::with_capacity(side * OFFSETS.len());
754        let mut wt = Vec::with_capacity(side * OFFSETS.len());
755        for supplier in 0..side {
756            for (lane, offset) in OFFSETS.into_iter().enumerate() {
757                let sink = (supplier + offset) % side;
758                src.push(supplier);
759                tgt.push(side + sink);
760                cap.push(1);
761                wt.push(((supplier * 17 + sink * 13 + lane * 5) % 11) as i64 - 5);
762            }
763        }
764        (demands, src, tgt, cap, wt)
765    }
766
767    fn solve_with_trace<const REUSE: bool>(
768        problem: &OwnedProblem,
769    ) -> ((NetworkSimplexStatus, i64, Vec<i64>), CycleTrace) {
770        let (demands, src, tgt, cap, wt) = problem;
771        CYCLE_TRACE.with(|trace| trace.set(CycleTrace::default()));
772        let result = solve_impl::<REUSE>(demands, src, tgt, cap, wt);
773        let trace = CYCLE_TRACE.with(std::cell::Cell::get);
774        (result, trace)
775    }
776
777    fn assert_cycle_scratch_parity(problem: &OwnedProblem) -> CycleTrace {
778        let (allocating, allocating_trace) = solve_with_trace::<false>(problem);
779        let (reused, reused_trace) = solve_with_trace::<true>(problem);
780        assert_eq!(
781            reused, allocating,
782            "scratch reuse must preserve status, cost, and input-order flows"
783        );
784        assert_eq!(
785            reused_trace, allocating_trace,
786            "scratch reuse must preserve pivot and path traversal counts"
787        );
788        reused_trace
789    }
790
791    #[test]
792    fn cycle_scratch_matches_allocating_baseline() {
793        let (d, s, t, c, w) = small_problem();
794        assert_cycle_scratch_parity(&(d.to_vec(), s.to_vec(), t.to_vec(), c.to_vec(), w.to_vec()));
795        assert_cycle_scratch_parity(&(vec![-4, 4], vec![0], vec![1], vec![2], vec![1]));
796        assert_cycle_scratch_parity(&(
797            vec![-1, 0, 0, 1],
798            vec![0, 1, 0, 2],
799            vec![1, 3, 2, 3],
800            vec![CAP_INF; 4],
801            vec![1, 5, 2, 1],
802        ));
803        assert_cycle_scratch_parity(&(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()));
804        assert_cycle_scratch_parity(&assignment_problem(32));
805        assert_eq!(
806            assert_cycle_scratch_parity(&assignment_problem(64)),
807            CycleTrace {
808                pivots: 140,
809                path_calls: 280,
810                path_hops: 719,
811            },
812            "the resurrected fixture must retain its pre-edit operation profile"
813        );
814    }
815
816    #[test]
817    fn known_integer_optimum() {
818        let (d, s, t, c, w) = small_problem();
819        let sol = network_simplex_int(&d, &s, &t, &c, &w);
820        assert_eq!(sol.status, NetworkSimplexStatus::Optimal);
821        assert_eq!(sol.cost, 10, "exact integer optimum cost");
822        // Flows are returned in input edge order: [0->1, 0->2, 1->3, 2->3].
823        assert_eq!(sol.flows, vec![2, 2, 2, 2]);
824        // Cross-check: cost recomputed from flows equals reported cost, exactly.
825        let recomputed: i64 = w.iter().zip(&sol.flows).map(|(wi, fi)| wi * fi).sum();
826        assert_eq!(recomputed, sol.cost);
827    }
828
829    #[test]
830    fn determinism_across_repeated_runs() {
831        let (d, s, t, c, w) = small_problem();
832        let first = network_simplex_int(&d, &s, &t, &c, &w);
833        for run in 0..64 {
834            let again = network_simplex_int(&d, &s, &t, &c, &w);
835            assert_eq!(again, first, "run {run} diverged from the first solve");
836        }
837    }
838
839    #[test]
840    fn infeasible_when_capacity_insufficient() {
841        // 0 supplies 4, 3 demands 4, but only a single cap-2 edge connects them.
842        let demands = [-4i64, 4];
843        let src = [0usize];
844        let tgt = [1usize];
845        let cap = [2i64];
846        let wt = [1i64];
847        let sol = network_simplex_int(&demands, &src, &tgt, &cap, &wt);
848        assert_eq!(sol.status, NetworkSimplexStatus::Infeasible);
849        assert_eq!(sol.cost, 0);
850        assert!(sol.flows.is_empty());
851    }
852
853    #[test]
854    fn empty_graph_is_trivially_optimal() {
855        let sol = network_simplex_int(&[], &[], &[], &[], &[]);
856        assert_eq!(sol.status, NetworkSimplexStatus::Optimal);
857        assert_eq!(sol.cost, 0);
858        assert!(sol.flows.is_empty());
859    }
860
861    #[test]
862    fn infinite_capacity_shortest_path() {
863        // Shortest-path-as-min-cost-flow: unit demand from 0 to 3, all edges
864        // infinite capacity. Cheapest 0->..->3 path cost is the answer.
865        //   0->1 wt1, 1->3 wt5, 0->2 wt2, 2->3 wt1  => best is 0->2->3 = 3.
866        let demands = [-1i64, 0, 0, 1];
867        let src = [0usize, 1, 0, 2];
868        let tgt = [1usize, 3, 2, 3];
869        let cap = [CAP_INF, CAP_INF, CAP_INF, CAP_INF];
870        let wt = [1i64, 5, 2, 1];
871        let sol = network_simplex_int(&demands, &src, &tgt, &cap, &wt);
872        assert_eq!(sol.status, NetworkSimplexStatus::Optimal);
873        assert_eq!(sol.cost, 3, "0->2->3 exact shortest path cost");
874        // No flow on the expensive 0->1 / 1->3 edges; one unit on 0->2->3.
875        assert_eq!(sol.flows, vec![0, 0, 1, 1]);
876    }
877}