ripopt 0.7.1

A memory-safe interior point optimizer in Rust
Documentation
// Benchmark comparing ripopt vs ipopt on AC Optimal Power Flow (grid) problems.
//
// Run with: cargo run --release --features ipopt-native --example grid_benchmark

use ripopt::{NlpProblem, SolverOptions};
use std::time::Instant;

#[path = "../benchmarks/grid/problems.rs"]
mod problems;

// =========================================================================
// Ipopt C API FFI (same pattern as benchmark_solvers.rs)
// =========================================================================

#[cfg(feature = "ipopt-native")]
mod ipopt_ffi {
    use std::ffi::CString;
    use std::os::raw::c_void;
    use std::time::Instant;
    use ripopt::NlpProblem;

    type IpoptProblemPtr = *mut c_void;
    extern "C" {
        fn CreateIpoptProblem(n: i32, x_l: *mut f64, x_u: *mut f64, m: i32, g_l: *mut f64, g_u: *mut f64, nele_jac: i32, nele_hess: i32, index_style: i32, eval_f: EvalFCB, eval_g: EvalGCB, eval_grad_f: EvalGradFCB, eval_jac_g: EvalJacGCB, eval_h: EvalHCB) -> IpoptProblemPtr;
        fn FreeIpoptProblem(problem: IpoptProblemPtr);
        fn AddIpoptStrOption(problem: IpoptProblemPtr, keyword: *const i8, val: *const i8) -> bool;
        fn AddIpoptNumOption(problem: IpoptProblemPtr, keyword: *const i8, val: f64) -> bool;
        fn AddIpoptIntOption(problem: IpoptProblemPtr, keyword: *const i8, val: i32) -> bool;
        fn SetIntermediateCallback(problem: IpoptProblemPtr, cb: IntermediateCB) -> bool;
        fn IpoptSolve(problem: IpoptProblemPtr, x: *mut f64, g: *mut f64, obj_val: *mut f64, mult_g: *mut f64, mult_x_l: *mut f64, mult_x_u: *mut f64, user_data: *mut c_void) -> i32;
    }
    type EvalFCB = extern "C" fn(i32, *const f64, bool, *mut f64, *mut c_void) -> bool;
    type EvalGradFCB = extern "C" fn(i32, *const f64, bool, *mut f64, *mut c_void) -> bool;
    type EvalGCB = extern "C" fn(i32, *const f64, bool, i32, *mut f64, *mut c_void) -> bool;
    type EvalJacGCB = extern "C" fn(i32, *const f64, bool, i32, i32, *mut i32, *mut i32, *mut f64, *mut c_void) -> bool;
    type EvalHCB = extern "C" fn(i32, *const f64, bool, f64, i32, *const f64, bool, i32, *mut i32, *mut i32, *mut f64, *mut c_void) -> bool;
    type IntermediateCB = extern "C" fn(i32, i32, f64, f64, f64, f64, f64, f64, f64, f64, i32, *mut c_void) -> bool;

    struct IpoptWrapper<'a> { problem: &'a dyn NlpProblem, jac_rows: Vec<i32>, jac_cols: Vec<i32>, hess_rows: Vec<i32>, hess_cols: Vec<i32>, iterations: i32 }

    extern "C" fn eval_f_cb(n: i32, x: *const f64, _: bool, obj: *mut f64, ud: *mut c_void) -> bool { unsafe { let w = &*(ud as *const IpoptWrapper); w.problem.objective(std::slice::from_raw_parts(x, n as usize), true, &mut *obj); true } }
    extern "C" fn eval_grad_f_cb(n: i32, x: *const f64, _: bool, g: *mut f64, ud: *mut c_void) -> bool { unsafe { let w = &*(ud as *const IpoptWrapper); w.problem.gradient(std::slice::from_raw_parts(x, n as usize), true, std::slice::from_raw_parts_mut(g, n as usize)); true } }
    extern "C" fn eval_g_cb(n: i32, x: *const f64, _: bool, _m: i32, g: *mut f64, ud: *mut c_void) -> bool { unsafe { let w = &*(ud as *const IpoptWrapper); let m = w.problem.num_constraints(); if m > 0 { w.problem.constraints(std::slice::from_raw_parts(x, n as usize), true, std::slice::from_raw_parts_mut(g, m)); } true } }
    extern "C" fn eval_jac_g_cb(n: i32, x: *const f64, _: bool, _m: i32, _nj: i32, ir: *mut i32, jc: *mut i32, vals: *mut f64, ud: *mut c_void) -> bool { unsafe { let w = &*(ud as *const IpoptWrapper); if vals.is_null() { let nele = w.jac_rows.len(); std::slice::from_raw_parts_mut(ir, nele).copy_from_slice(&w.jac_rows); std::slice::from_raw_parts_mut(jc, nele).copy_from_slice(&w.jac_cols); } else { w.problem.jacobian_values(std::slice::from_raw_parts(x, n as usize), true, std::slice::from_raw_parts_mut(vals, w.jac_rows.len())); } true } }
    extern "C" fn eval_h_cb(n: i32, x: *const f64, _: bool, obj_factor: f64, _m: i32, lambda: *const f64, _: bool, _nh: i32, ir: *mut i32, jc: *mut i32, vals: *mut f64, ud: *mut c_void) -> bool { unsafe { let w = &*(ud as *const IpoptWrapper); if vals.is_null() { let nele = w.hess_rows.len(); std::slice::from_raw_parts_mut(ir, nele).copy_from_slice(&w.hess_rows); std::slice::from_raw_parts_mut(jc, nele).copy_from_slice(&w.hess_cols); } else { let xs = std::slice::from_raw_parts(x, n as usize); let m = w.problem.num_constraints(); let ls = if m > 0 { std::slice::from_raw_parts(lambda, m) } else { &[] }; w.problem.hessian_values(xs, true, obj_factor, ls, std::slice::from_raw_parts_mut(vals, w.hess_rows.len())); } true } }
    extern "C" fn intermediate_cb(_: i32, iter: i32, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64, _: i32, ud: *mut c_void) -> bool { unsafe { (*(ud as *mut IpoptWrapper)).iterations = iter; } true }

    fn set_str(p: IpoptProblemPtr, k: &str, v: &str) { let ks = CString::new(k).unwrap(); let vs = CString::new(v).unwrap(); unsafe { AddIpoptStrOption(p, ks.as_ptr(), vs.as_ptr()); } }
    fn set_num(p: IpoptProblemPtr, k: &str, v: f64) { let ks = CString::new(k).unwrap(); unsafe { AddIpoptNumOption(p, ks.as_ptr(), v); } }
    fn set_int(p: IpoptProblemPtr, k: &str, v: i32) { let ks = CString::new(k).unwrap(); unsafe { AddIpoptIntOption(p, ks.as_ptr(), v); } }

    pub fn ipopt_status_str(s: i32) -> String { match s { 0 => "Optimal".into(), 1 => "Acceptable".into(), 2 => "Infeasible".into(), -1 => "MaxIter".into(), -2 => "RestorationFailed".into(), other => format!("Status({})", other) } }

    pub fn solve_ipopt(problem: &dyn NlpProblem, tol: f64, max_iter: i32) -> super::SolveResult {
        let n = problem.num_variables(); let m = problem.num_constraints();
        let mut x_l = vec![0.0; n]; let mut x_u = vec![0.0; n]; problem.bounds(&mut x_l, &mut x_u);
        let mut g_l = vec![0.0; m.max(1)]; let mut g_u = vec![0.0; m.max(1)]; if m > 0 { problem.constraint_bounds(&mut g_l, &mut g_u); }
        let (jr, jc) = problem.jacobian_structure(); let (hr, hc) = problem.hessian_structure();
        let mut wrapper = IpoptWrapper { problem, jac_rows: jr.iter().map(|&r| r as i32).collect(), jac_cols: jc.iter().map(|&c| c as i32).collect(), hess_rows: hr.iter().map(|&r| r as i32).collect(), hess_cols: hc.iter().map(|&c| c as i32).collect(), iterations: 0 };
        unsafe {
            let ip = CreateIpoptProblem(n as i32, x_l.as_mut_ptr(), x_u.as_mut_ptr(), m as i32, g_l.as_mut_ptr(), g_u.as_mut_ptr(), wrapper.jac_rows.len() as i32, wrapper.hess_rows.len() as i32, 0, eval_f_cb, eval_g_cb, eval_grad_f_cb, eval_jac_g_cb, eval_h_cb);
            set_str(ip, "sb", "yes"); set_str(ip, "mu_strategy", "adaptive"); set_num(ip, "tol", tol); set_int(ip, "max_iter", max_iter); set_int(ip, "print_level", 0);
            SetIntermediateCallback(ip, intermediate_cb);
            let mut x = vec![0.0; n]; problem.initial_point(&mut x);
            let mut g = vec![0.0; m.max(1)]; let mut obj = 0.0; let mut mg = vec![0.0; m.max(1)]; let mut ml = vec![0.0; n]; let mut mu = vec![0.0; n];
            let ud = &mut wrapper as *mut IpoptWrapper as *mut c_void;
            let t0 = Instant::now();
            let status = IpoptSolve(ip, x.as_mut_ptr(), g.as_mut_ptr(), &mut obj, mg.as_mut_ptr(), ml.as_mut_ptr(), mu.as_mut_ptr(), ud);
            let elapsed = t0.elapsed().as_secs_f64(); let iters = wrapper.iterations; FreeIpoptProblem(ip);
            super::SolveResult { status: ipopt_status_str(status), objective: obj, iterations: iters, time_s: elapsed }
        }
    }
}

// =========================================================================
// Result type and ripopt wrapper
// =========================================================================

struct SolveResult { status: String, objective: f64, iterations: i32, time_s: f64 }

#[derive(serde::Serialize)]
struct BenchEntry {
    solver: String,
    name: String,
    n: usize,
    m: usize,
    status: String,
    objective: f64,
    iterations: i32,
    solve_time: f64,
}

fn solve_ripopt<P: NlpProblem>(problem: &P, tol: f64, max_iter: usize) -> SolveResult {
    let plvl = std::env::var("RIPOPT_PRINT_LEVEL").ok().and_then(|s| s.parse().ok()).unwrap_or(0);
    let options = SolverOptions { tol, max_iter, max_wall_time: 60.0, print_level: plvl, ..SolverOptions::default() };
    let t0 = Instant::now();
    let result = ripopt::solve(problem, &options);
    let elapsed = t0.elapsed().as_secs_f64();
    SolveResult { status: format!("{:?}", result.status), objective: result.objective, iterations: result.iterations as i32, time_s: elapsed }
}

// =========================================================================
// Main
// =========================================================================

fn main() {
    let tol = 1e-6;
    let max_iter: usize = 3000;
    let have_ipopt = cfg!(feature = "ipopt-native");

    println!();
    println!("AC Optimal Power Flow Benchmark: ripopt vs ipopt");
    println!("================================================");
    println!();

    let mut header = format!(
        "{:<20} {:>4} {:>4} {:>4} | {:>12} {:>5} {:>8}",
        "Problem", "n", "m", "nnz", "ripopt obj", "iter", "time(s)"
    );
    if have_ipopt {
        header += &format!(" | {:>12} {:>5} {:>8}", "ipopt obj", "iter", "time(s)");
    }
    println!("{}", header);
    let width = header.len();
    println!("{}", "-".repeat(width));

    let mut results: Vec<BenchEntry> = Vec::new();

    macro_rules! bench {
        ($name:expr, $problem:expr, $known_opt:expr) => {{
            let p = $problem;
            let n = p.num_variables();
            let m = p.num_constraints();
            let (jr, _) = p.jacobian_structure();
            let nnz = jr.len();
            let rp = solve_ripopt(&p, tol, max_iter);
            #[allow(unused_mut)]
            let mut line = format!(
                "{:<20} {:>4} {:>4} {:>4} | {:>12.2} {:>5} {:>8.4}",
                $name, n, m, nnz, rp.objective, rp.iterations, rp.time_s
            );
            #[allow(unused_mut)]
            let mut notes = Vec::new();
            if rp.status != "Optimal" {
                notes.push(format!("ripopt={}", rp.status));
            }
            results.push(BenchEntry {
                solver: "ripopt".into(), name: $name.into(), n, m,
                status: rp.status.clone(), objective: rp.objective,
                iterations: rp.iterations, solve_time: rp.time_s,
            });
            let rp_gap = ((rp.objective - $known_opt) / $known_opt * 100.0).abs();

            #[cfg(feature = "ipopt-native")]
            {
                let ip = ipopt_ffi::solve_ipopt(&p, tol, max_iter as i32);
                line += &format!(" | {:>12.2} {:>5} {:>8.4}", ip.objective, ip.iterations, ip.time_s);
                if ip.status != "Optimal" { notes.push(format!("ipopt={}", ip.status)); }
                results.push(BenchEntry {
                    solver: "ipopt".into(), name: $name.into(), n, m,
                    status: ip.status.clone(), objective: ip.objective,
                    iterations: ip.iterations, solve_time: ip.time_s,
                });
            }

            println!("{}", line);
            if !notes.is_empty() {
                println!("  status: {}", notes.join(", "));
            }
            if rp_gap > 1.0 {
                println!("  gap from known optimal: {:.2}%", rp_gap);
            }
        }};
    }

    use problems::*;

    bench!("case3_lmbd", case3_lmbd(), 5812.64);
    bench!("case5_pjm", case5_pjm(), 17551.89);
    bench!("case14_ieee", case14_ieee(), 2178.08);
    bench!("case30_ieee", case30_ieee(), 8081.52);

    println!("{}", "-".repeat(width));

    // Write JSON results
    let results_path = std::env::var("RESULTS_FILE")
        .unwrap_or_else(|_| "grid_results.json".to_string());
    let json = serde_json::to_string_pretty(&results).unwrap();
    std::fs::write(&results_path, json).unwrap();
    eprintln!("Results written to {}", results_path);
}