Skip to main content

ferrox_highs_sys/
lib.rs

1#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
2
3#[cfg(feature = "link")]
4use std::os::raw::c_int;
5
6// ── Status ────────────────────────────────────────────────────────────────────
7
8#[repr(i32)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum HighsReturnStatus {
11    Ok = 0,
12    Warning = 1,
13    Error = 2,
14}
15
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum HighsModelStatus {
19    NotSet = 0,
20    LoadError = 1,
21    ModelError = 2,
22    Infeasible = 8,
23    Optimal = 7,
24    Unbounded = 9,
25    SolutionLimit = 11,
26    TimeLimit = 12,
27}
28
29impl HighsModelStatus {
30    pub fn is_success(self) -> bool {
31        matches!(self, Self::Optimal | Self::SolutionLimit | Self::TimeLimit)
32    }
33    pub fn is_optimal(self) -> bool {
34        self == Self::Optimal
35    }
36}
37
38// ── Opaque C type ─────────────────────────────────────────────────────────────
39
40#[repr(C)]
41pub struct HighsHandle {
42    _p: [u8; 0],
43}
44
45// ── FFI declarations ──────────────────────────────────────────────────────────
46
47#[cfg(feature = "link")]
48unsafe extern "C" {
49    pub fn highs_create() -> *mut HighsHandle;
50    pub fn highs_destroy(h: *mut HighsHandle);
51    pub fn highs_add_col(h: *mut HighsHandle, cost: f64, lb: f64, ub: f64) -> HighsReturnStatus;
52    pub fn highs_add_row(
53        h: *mut HighsHandle,
54        lb: f64,
55        ub: f64,
56        num_nz: c_int,
57        idx: *const c_int,
58        val: *const f64,
59    ) -> HighsReturnStatus;
60    pub fn highs_change_col_integer_type(
61        h: *mut HighsHandle,
62        col: c_int,
63        is_integer: c_int,
64    ) -> HighsReturnStatus;
65    pub fn highs_set_time_limit(h: *mut HighsHandle, seconds: f64) -> HighsReturnStatus;
66    pub fn highs_set_mip_rel_gap(h: *mut HighsHandle, gap: f64) -> HighsReturnStatus;
67    pub fn highs_run(h: *mut HighsHandle) -> HighsReturnStatus;
68    pub fn highs_get_model_status(h: *mut HighsHandle) -> HighsModelStatus;
69    pub fn highs_get_objective_value(h: *mut HighsHandle) -> f64;
70    pub fn highs_get_col_value(h: *mut HighsHandle, col: c_int) -> f64;
71    pub fn highs_get_mip_gap(h: *mut HighsHandle) -> f64;
72}
73
74// ── Safe wrapper ──────────────────────────────────────────────────────────────
75
76#[cfg(feature = "link")]
77pub mod safe {
78    #[allow(clippy::wildcard_imports)]
79    use super::*;
80    use std::ptr::NonNull;
81
82    pub struct HighsSolver {
83        ptr: NonNull<HighsHandle>,
84        num_cols: i32,
85    }
86
87    impl HighsSolver {
88        pub fn new() -> Self {
89            unsafe {
90                Self {
91                    ptr: NonNull::new(highs_create()).expect("highs_create returned null"),
92                    num_cols: 0,
93                }
94            }
95        }
96
97        /// Add a continuous variable. Returns the column index.
98        pub fn add_col(&mut self, cost: f64, lb: f64, ub: f64) -> i32 {
99            unsafe { highs_add_col(self.ptr.as_ptr(), cost, lb, ub) };
100            let idx = self.num_cols;
101            self.num_cols += 1;
102            idx
103        }
104
105        /// Add an integer variable. Returns the column index.
106        pub fn add_int_col(&mut self, cost: f64, lb: f64, ub: f64) -> i32 {
107            let col = self.add_col(cost, lb, ub);
108            unsafe { highs_change_col_integer_type(self.ptr.as_ptr(), col, 1) };
109            col
110        }
111
112        /// Add a binary (0/1) variable. Returns the column index.
113        pub fn add_bin_col(&mut self, cost: f64) -> i32 {
114            self.add_int_col(cost, 0.0, 1.0)
115        }
116
117        /// Add a row constraint: lb <= sum(idx[i] * val[i]) <= ub
118        pub fn add_row(&mut self, lb: f64, ub: f64, indices: &[i32], coeffs: &[f64]) {
119            assert_eq!(indices.len(), coeffs.len());
120            unsafe {
121                highs_add_row(
122                    self.ptr.as_ptr(),
123                    lb,
124                    ub,
125                    i32::try_from(indices.len()).expect("row nnz fits i32"),
126                    indices.as_ptr(),
127                    coeffs.as_ptr(),
128                );
129            }
130        }
131
132        pub fn set_time_limit(&mut self, secs: f64) {
133            unsafe {
134                highs_set_time_limit(self.ptr.as_ptr(), secs);
135            }
136        }
137
138        pub fn set_mip_rel_gap(&mut self, gap: f64) {
139            unsafe {
140                highs_set_mip_rel_gap(self.ptr.as_ptr(), gap);
141            }
142        }
143
144        pub fn run(&mut self) -> HighsModelStatus {
145            unsafe {
146                highs_run(self.ptr.as_ptr());
147                highs_get_model_status(self.ptr.as_ptr())
148            }
149        }
150
151        pub fn objective_value(&self) -> f64 {
152            unsafe { highs_get_objective_value(self.ptr.as_ptr()) }
153        }
154
155        pub fn col_value(&self, col: i32) -> f64 {
156            unsafe { highs_get_col_value(self.ptr.as_ptr(), col) }
157        }
158
159        pub fn mip_gap(&self) -> f64 {
160            unsafe { highs_get_mip_gap(self.ptr.as_ptr()) }
161        }
162    }
163
164    impl Default for HighsSolver {
165        fn default() -> Self {
166            Self::new()
167        }
168    }
169
170    impl Drop for HighsSolver {
171        fn drop(&mut self) {
172            unsafe { highs_destroy(self.ptr.as_ptr()) }
173        }
174    }
175}
176
177// ── Tests ─────────────────────────────────────────────────────────────────────
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn status_values() {
185        assert_eq!(HighsModelStatus::Optimal as i32, 7);
186        assert!(HighsModelStatus::Optimal.is_success());
187        assert!(!HighsModelStatus::Infeasible.is_success());
188    }
189
190    #[cfg(feature = "link")]
191    mod integration {
192        use super::super::safe::*;
193
194        #[test]
195        fn mip_binary_knapsack() {
196            // Maximize 5x + 4y + 3z  subject to  2x + 3y + 2z <= 5,  x,y,z in {0,1}
197            let mut s = HighsSolver::new();
198            let x = s.add_bin_col(-5.0); // minimize negated costs
199            let y = s.add_bin_col(-4.0);
200            let z = s.add_bin_col(-3.0);
201            s.add_row(f64::NEG_INFINITY, 5.0, &[x, y, z], &[2.0, 3.0, 2.0]);
202            let status = s.run();
203            assert!(status.is_success());
204            // Optimal: x=1,y=1 → capacity=5, value=9 (beats x=1,z=1 → value=8)
205            assert!((-s.objective_value() - 9.0).abs() < 0.5);
206        }
207    }
208}