tide-maxflow 0.1.0

Tide max flow algorithm — a push-pull-relabel variant with O(1) array-based data structures
Documentation
//! Read DIMACS max-flow files and solve with Tide.
//! Usage: solve_dimacs <file.max> [file2.max ...]
//! Or:    solve_dimacs --dir `directory`
//!
//! Outputs CSV with per-phase timing breakdown.

use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::time::Instant;
use tide_maxflow::{max_flow_adaptive_timed, max_flow_hybrid_timed, max_flow_timed};

/// Parse a DIMACS max-flow file. Returns (n, source, sink, edges).
/// DIMACS is 1-indexed; we convert to 0-indexed for Tide.
fn parse_dimacs(path: &Path) -> (usize, usize, usize, Vec<(usize, usize, i64)>) {
    let file = fs::File::open(path).unwrap();
    let reader = BufReader::new(file);

    let mut n = 0usize;
    let mut source = 0usize;
    let mut sink = 0usize;
    let mut edges = Vec::new();

    for line in reader.lines() {
        let line = line.unwrap();
        let line = line.trim();
        if line.is_empty() || line.starts_with('c') {
            continue;
        }
        let parts: Vec<&str> = line.split_whitespace().collect();
        match parts[0] {
            "p" => {
                // p max NODES ARCS
                n = parts[2].parse().unwrap();
            }
            "n" => {
                // n ID s|t
                let id: usize = parts[1].parse().unwrap();
                match parts[2] {
                    "s" => source = id - 1, // convert to 0-indexed
                    "t" => sink = id - 1,
                    _ => {}
                }
            }
            "a" => {
                // a FROM TO CAPACITY
                let from: usize = parts[1].parse().unwrap();
                let to: usize = parts[2].parse().unwrap();
                let cap: i64 = parts[3].parse().unwrap();
                edges.push((from - 1, to - 1, cap)); // 0-indexed
            }
            _ => {}
        }
    }
    (n, source, sink, edges)
}

fn solve_file(path: &Path, mode: &str) {
    let filename = path.file_stem().unwrap().to_str().unwrap();

    let t_parse_start = Instant::now();
    let (n, source, sink, edges) = parse_dimacs(path);
    let parse_us = t_parse_start.elapsed().as_micros() as u64;

    let m = edges.len();

    let t_solve_start = Instant::now();
    let timed = match mode {
        "hybrid" => max_flow_hybrid_timed(n, source, sink, &edges),
        "adaptive" => max_flow_adaptive_timed(n, source, sink, &edges),
        _ => max_flow_timed(n, source, sink, &edges),
    };
    let solve_us = t_solve_start.elapsed().as_micros() as u64;

    let r = &timed.result;
    let t = &timed.timing;
    println!(
        "{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
        filename,
        n,
        m,
        source,
        sink,
        r.flow,
        r.steps,
        parse_us,
        solve_us,
        t.relabel_us,
        t.pull_us,
        t.push_us,
        t.convergence_us,
        t.relabel_skipped,
    );
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let mode = if args.iter().any(|a| a == "--adaptive") {
        "adaptive"
    } else if args.iter().any(|a| a == "--hybrid") {
        "hybrid"
    } else {
        "standard"
    };

    // Parse --max-size <bytes> flag
    let max_size: Option<u64> = args
        .windows(2)
        .find(|w| w[0] == "--max-size")
        .and_then(|w| w[1].parse().ok());

    // Filter out flags and their values, keeping positional args
    let filtered_args: Vec<&String> = {
        let mut result = Vec::new();
        let mut skip_next = false;
        for a in &args {
            if skip_next {
                skip_next = false;
                continue;
            }
            if a == "--max-size" {
                skip_next = true;
                continue;
            }
            if a == "--hybrid" || a == "--adaptive" || a == "--cascade" {
                continue;
            }
            result.push(a);
        }
        result
    };

    println!("name,n,m,source,sink,max_flow,steps,parse_us,solve_us,relabel_us,pull_us,push_us,convergence_us,relabel_skipped");

    if filtered_args.len() >= 3 && filtered_args[1] == "--dir" {
        let dir = Path::new(filtered_args[2].as_str());
        let mut entries: Vec<_> = fs::read_dir(dir)
            .unwrap()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().map_or(false, |ext| ext == "max"))
            .filter(|e| {
                if let Some(max) = max_size {
                    e.metadata().map_or(false, |m| m.len() <= max)
                } else {
                    true
                }
            })
            .collect();
        entries.sort_by_key(|e| e.file_name());
        for entry in entries {
            solve_file(&entry.path(), mode);
        }
    } else {
        for path_str in &filtered_args[1..] {
            solve_file(Path::new(path_str.as_str()), mode);
        }
    }
}