Skip to main content

ferrox_ortools_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_char, c_double, c_int};
5
6// ── Status ────────────────────────────────────────────────────────────────────
7
8#[repr(i32)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum OrtoolsStatus {
11    Unknown = 0,
12    Optimal = 1,
13    Feasible = 2,
14    Infeasible = 3,
15    Unbounded = 4,
16    ModelInvalid = 5,
17    Error = 6,
18}
19
20impl OrtoolsStatus {
21    pub fn is_success(self) -> bool {
22        matches!(self, Self::Optimal | Self::Feasible)
23    }
24}
25
26// ── Opaque C types ────────────────────────────────────────────────────────────
27
28#[repr(C)]
29pub struct CpModelBuilder {
30    _p: [u8; 0],
31}
32#[repr(C)]
33pub struct CpSolverResponse {
34    _p: [u8; 0],
35}
36
37#[repr(i32)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum LpSolverType {
40    Glop = 0,
41}
42
43#[repr(C)]
44pub struct MpSolver {
45    _p: [u8; 0],
46}
47
48// ── FFI declarations ──────────────────────────────────────────────────────────
49
50#[cfg(feature = "link")]
51unsafe extern "C" {
52    pub fn cpmodel_new() -> *mut CpModelBuilder;
53    pub fn cpmodel_free(m: *mut CpModelBuilder);
54    pub fn cpmodel_new_int_var(
55        m: *mut CpModelBuilder,
56        lb: i64,
57        ub: i64,
58        name: *const c_char,
59    ) -> i32;
60    pub fn cpmodel_new_bool_var(m: *mut CpModelBuilder, name: *const c_char) -> i32;
61    pub fn cpmodel_add_linear_le(
62        m: *mut CpModelBuilder,
63        idx: *const i32,
64        c: *const i64,
65        n: usize,
66        rhs: i64,
67    );
68    pub fn cpmodel_add_linear_ge(
69        m: *mut CpModelBuilder,
70        idx: *const i32,
71        c: *const i64,
72        n: usize,
73        rhs: i64,
74    );
75    pub fn cpmodel_add_linear_eq(
76        m: *mut CpModelBuilder,
77        idx: *const i32,
78        c: *const i64,
79        n: usize,
80        rhs: i64,
81    );
82    pub fn cpmodel_add_all_different(m: *mut CpModelBuilder, idx: *const i32, n: usize);
83    pub fn cpmodel_minimize(m: *mut CpModelBuilder, idx: *const i32, c: *const i64, n: usize);
84    pub fn cpmodel_maximize(m: *mut CpModelBuilder, idx: *const i32, c: *const i64, n: usize);
85    pub fn cpmodel_solve(m: *mut CpModelBuilder, time_limit: c_double) -> *mut CpSolverResponse;
86    pub fn cpresponse_status(r: *const CpSolverResponse) -> OrtoolsStatus;
87    pub fn cpresponse_objective_value(r: *const CpSolverResponse) -> i64;
88    pub fn cpresponse_value(r: *const CpSolverResponse, var_index: i32) -> i64;
89    pub fn cpresponse_wall_time(r: *const CpSolverResponse) -> c_double;
90    pub fn cpresponse_free(r: *mut CpSolverResponse);
91
92    pub fn cpmodel_new_interval_var(
93        m: *mut CpModelBuilder,
94        start: c_int,
95        size: i64,
96        end: c_int,
97        name: *const c_char,
98    ) -> c_int;
99    pub fn cpmodel_new_optional_interval_var(
100        m: *mut CpModelBuilder,
101        start: c_int,
102        size: i64,
103        end: c_int,
104        lit: c_int,
105        name: *const c_char,
106    ) -> c_int;
107    pub fn cpmodel_add_circuit(
108        m: *mut CpModelBuilder,
109        tails: *const c_int,
110        heads: *const c_int,
111        lits: *const c_int,
112        n: usize,
113    );
114    pub fn cpmodel_add_no_overlap(m: *mut CpModelBuilder, idx: *const c_int, n: usize);
115
116    pub fn mpsolver_new(name: *const c_char, t: LpSolverType) -> *mut MpSolver;
117    pub fn mpsolver_free(s: *mut MpSolver);
118    pub fn mpsolver_num_var(
119        s: *mut MpSolver,
120        lb: c_double,
121        ub: c_double,
122        name: *const c_char,
123    ) -> i32;
124    pub fn mpsolver_int_var(
125        s: *mut MpSolver,
126        lb: c_double,
127        ub: c_double,
128        name: *const c_char,
129    ) -> i32;
130    pub fn mpsolver_bool_var(s: *mut MpSolver, name: *const c_char) -> i32;
131    pub fn mpsolver_add_constraint(
132        s: *mut MpSolver,
133        lb: c_double,
134        ub: c_double,
135        name: *const c_char,
136    ) -> i32;
137    pub fn mpsolver_set_constraint_coeff(s: *mut MpSolver, ci: c_int, vi: c_int, coeff: c_double);
138    pub fn mpsolver_set_objective_coeff(s: *mut MpSolver, vi: c_int, coeff: c_double);
139    pub fn mpsolver_minimize(s: *mut MpSolver);
140    pub fn mpsolver_maximize(s: *mut MpSolver);
141    pub fn mpsolver_solve(s: *mut MpSolver) -> OrtoolsStatus;
142    pub fn mpsolver_objective_value(s: *const MpSolver) -> c_double;
143    pub fn mpsolver_var_value(s: *const MpSolver, vi: c_int) -> c_double;
144}
145
146// ── Safe wrappers ─────────────────────────────────────────────────────────────
147
148#[cfg(feature = "link")]
149pub mod safe {
150    #[allow(clippy::wildcard_imports)]
151    use super::*;
152    use std::{ffi::CString, ptr::NonNull};
153
154    pub struct CpModel {
155        ptr: NonNull<CpModelBuilder>,
156    }
157
158    impl CpModel {
159        pub fn new() -> Self {
160            unsafe {
161                Self {
162                    ptr: NonNull::new(cpmodel_new()).expect("cpmodel_new returned null"),
163                }
164            }
165        }
166
167        pub fn new_int_var(&mut self, lb: i64, ub: i64, name: &str) -> i32 {
168            let c = CString::new(name).unwrap();
169            unsafe { cpmodel_new_int_var(self.ptr.as_ptr(), lb, ub, c.as_ptr()) }
170        }
171
172        pub fn new_bool_var(&mut self, name: &str) -> i32 {
173            let c = CString::new(name).unwrap();
174            unsafe { cpmodel_new_bool_var(self.ptr.as_ptr(), c.as_ptr()) }
175        }
176
177        pub fn add_linear_le(&mut self, vars: &[i32], coeffs: &[i64], rhs: i64) {
178            assert_eq!(vars.len(), coeffs.len());
179            unsafe {
180                cpmodel_add_linear_le(
181                    self.ptr.as_ptr(),
182                    vars.as_ptr(),
183                    coeffs.as_ptr(),
184                    vars.len(),
185                    rhs,
186                );
187            }
188        }
189
190        pub fn add_linear_ge(&mut self, vars: &[i32], coeffs: &[i64], rhs: i64) {
191            assert_eq!(vars.len(), coeffs.len());
192            unsafe {
193                cpmodel_add_linear_ge(
194                    self.ptr.as_ptr(),
195                    vars.as_ptr(),
196                    coeffs.as_ptr(),
197                    vars.len(),
198                    rhs,
199                );
200            }
201        }
202
203        pub fn add_linear_eq(&mut self, vars: &[i32], coeffs: &[i64], rhs: i64) {
204            assert_eq!(vars.len(), coeffs.len());
205            unsafe {
206                cpmodel_add_linear_eq(
207                    self.ptr.as_ptr(),
208                    vars.as_ptr(),
209                    coeffs.as_ptr(),
210                    vars.len(),
211                    rhs,
212                );
213            }
214        }
215
216        pub fn add_all_different(&mut self, vars: &[i32]) {
217            unsafe {
218                cpmodel_add_all_different(self.ptr.as_ptr(), vars.as_ptr(), vars.len());
219            }
220        }
221
222        pub fn minimize(&mut self, vars: &[i32], coeffs: &[i64]) {
223            assert_eq!(vars.len(), coeffs.len());
224            unsafe {
225                cpmodel_minimize(
226                    self.ptr.as_ptr(),
227                    vars.as_ptr(),
228                    coeffs.as_ptr(),
229                    vars.len(),
230                );
231            }
232        }
233
234        pub fn maximize(&mut self, vars: &[i32], coeffs: &[i64]) {
235            assert_eq!(vars.len(), coeffs.len());
236            unsafe {
237                cpmodel_maximize(
238                    self.ptr.as_ptr(),
239                    vars.as_ptr(),
240                    coeffs.as_ptr(),
241                    vars.len(),
242                );
243            }
244        }
245
246        pub fn new_fixed_interval_var(&mut self, start: i32, size: i64, end: i32, name: &str) -> i32 {
247            let c = CString::new(name).unwrap();
248            unsafe { cpmodel_new_interval_var(self.ptr.as_ptr(), start, size, end, c.as_ptr()) }
249        }
250
251        pub fn new_optional_interval_var(
252            &mut self,
253            start: i32,
254            size: i64,
255            end: i32,
256            lit: i32,
257            name: &str,
258        ) -> i32 {
259            let c = CString::new(name).unwrap();
260            unsafe {
261                cpmodel_new_optional_interval_var(
262                    self.ptr.as_ptr(),
263                    start,
264                    size,
265                    end,
266                    lit,
267                    c.as_ptr(),
268                )
269            }
270        }
271
272        /// Add a Hamiltonian circuit constraint.
273        ///
274        /// Each arc is `(tail, head, literal)`.  The arcs whose literal equals 1
275        /// must form a single circuit covering all nodes that have no self-loop
276        /// literal set to 1.  Use a self-loop `(i, i, lit)` to make node `i`
277        /// optional — if `lit = 1` the node is skipped.
278        pub fn add_circuit(&mut self, tails: &[i32], heads: &[i32], lits: &[i32]) {
279            assert_eq!(tails.len(), heads.len());
280            assert_eq!(tails.len(), lits.len());
281            unsafe {
282                cpmodel_add_circuit(
283                    self.ptr.as_ptr(),
284                    tails.as_ptr(),
285                    heads.as_ptr(),
286                    lits.as_ptr(),
287                    tails.len(),
288                );
289            }
290        }
291
292        pub fn add_no_overlap(&mut self, intervals: &[i32]) {
293            unsafe {
294                cpmodel_add_no_overlap(self.ptr.as_ptr(), intervals.as_ptr(), intervals.len());
295            }
296        }
297
298        pub fn solve(&self, time_limit_seconds: f64) -> CpSolution {
299            unsafe {
300                CpSolution {
301                    ptr: NonNull::new(cpmodel_solve(self.ptr.as_ptr(), time_limit_seconds))
302                        .expect("cpmodel_solve returned null"),
303                }
304            }
305        }
306    }
307
308    impl Default for CpModel {
309        fn default() -> Self {
310            Self::new()
311        }
312    }
313
314    impl Drop for CpModel {
315        fn drop(&mut self) {
316            unsafe { cpmodel_free(self.ptr.as_ptr()) }
317        }
318    }
319
320    pub struct CpSolution {
321        ptr: NonNull<CpSolverResponse>,
322    }
323
324    impl CpSolution {
325        pub fn status(&self) -> OrtoolsStatus {
326            unsafe { cpresponse_status(self.ptr.as_ptr()) }
327        }
328        pub fn objective_value(&self) -> i64 {
329            unsafe { cpresponse_objective_value(self.ptr.as_ptr()) }
330        }
331        pub fn value(&self, var_index: i32) -> i64 {
332            unsafe { cpresponse_value(self.ptr.as_ptr(), var_index) }
333        }
334        pub fn wall_time(&self) -> f64 {
335            unsafe { cpresponse_wall_time(self.ptr.as_ptr()) }
336        }
337    }
338
339    impl Drop for CpSolution {
340        fn drop(&mut self) {
341            unsafe { cpresponse_free(self.ptr.as_ptr()) }
342        }
343    }
344
345    pub struct LinearSolver {
346        ptr: NonNull<MpSolver>,
347    }
348
349    impl LinearSolver {
350        pub fn new_glop(name: &str) -> Self {
351            let c = CString::new(name).unwrap();
352            unsafe {
353                Self {
354                    ptr: NonNull::new(mpsolver_new(c.as_ptr(), LpSolverType::Glop))
355                        .expect("mpsolver_new returned null"),
356                }
357            }
358        }
359
360        pub fn num_var(&mut self, lb: f64, ub: f64, name: &str) -> i32 {
361            let c = CString::new(name).unwrap();
362            unsafe { mpsolver_num_var(self.ptr.as_ptr(), lb, ub, c.as_ptr()) }
363        }
364
365        pub fn add_constraint(&mut self, lb: f64, ub: f64, name: &str) -> i32 {
366            let c = CString::new(name).unwrap();
367            unsafe { mpsolver_add_constraint(self.ptr.as_ptr(), lb, ub, c.as_ptr()) }
368        }
369
370        pub fn set_constraint_coeff(&mut self, ci: i32, vi: i32, coeff: f64) {
371            unsafe { mpsolver_set_constraint_coeff(self.ptr.as_ptr(), ci, vi, coeff) }
372        }
373
374        pub fn set_objective_coeff(&mut self, vi: i32, coeff: f64) {
375            unsafe { mpsolver_set_objective_coeff(self.ptr.as_ptr(), vi, coeff) }
376        }
377
378        pub fn maximize(&mut self) {
379            unsafe { mpsolver_maximize(self.ptr.as_ptr()) }
380        }
381        pub fn minimize(&mut self) {
382            unsafe { mpsolver_minimize(self.ptr.as_ptr()) }
383        }
384
385        pub fn solve(&mut self) -> OrtoolsStatus {
386            unsafe { mpsolver_solve(self.ptr.as_ptr()) }
387        }
388
389        pub fn objective_value(&self) -> f64 {
390            unsafe { mpsolver_objective_value(self.ptr.as_ptr()) }
391        }
392
393        pub fn var_value(&self, vi: i32) -> f64 {
394            unsafe { mpsolver_var_value(self.ptr.as_ptr(), vi) }
395        }
396    }
397
398    impl Drop for LinearSolver {
399        fn drop(&mut self) {
400            unsafe { mpsolver_free(self.ptr.as_ptr()) }
401        }
402    }
403}
404
405// ── Tests ─────────────────────────────────────────────────────────────────────
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn status_values() {
413        assert_eq!(OrtoolsStatus::Optimal as i32, 1);
414        assert!(OrtoolsStatus::Optimal.is_success());
415        assert!(!OrtoolsStatus::Infeasible.is_success());
416    }
417
418    #[cfg(feature = "link")]
419    mod integration {
420        use super::super::safe::*;
421
422        #[test]
423        fn cpsat_minimize() {
424            let mut m = CpModel::new();
425            let x = m.new_int_var(0, 10, "x");
426            let y = m.new_int_var(0, 10, "y");
427            m.add_linear_eq(&[x, y], &[1, 1], 10);
428            m.minimize(&[x], &[1]);
429            let s = m.solve(60.0);
430            assert!(s.status().is_success());
431            assert_eq!(s.value(x), 0);
432            assert_eq!(s.value(y), 10);
433        }
434
435        #[test]
436        fn cpsat_all_different() {
437            let mut model = CpModel::new();
438            let va = model.new_int_var(1, 3, "a");
439            let vb = model.new_int_var(1, 3, "b");
440            let vc = model.new_int_var(1, 3, "c");
441            model.add_all_different(&[va, vb, vc]);
442            let sol = model.solve(60.0);
443            assert!(sol.status().is_success());
444            let vals = [sol.value(va), sol.value(vb), sol.value(vc)];
445            assert!(vals.contains(&1) && vals.contains(&2) && vals.contains(&3));
446        }
447
448        #[test]
449        fn glop_maximize() {
450            let mut s = LinearSolver::new_glop("test");
451            let x = s.num_var(0.0, f64::INFINITY, "x");
452            let y = s.num_var(0.0, f64::INFINITY, "y");
453            let c = s.add_constraint(f64::NEG_INFINITY, 10.0, "c1");
454            s.set_constraint_coeff(c, x, 1.0);
455            s.set_constraint_coeff(c, y, 1.0);
456            s.set_objective_coeff(x, 1.0);
457            s.set_objective_coeff(y, 1.0);
458            s.maximize();
459            assert!(s.solve().is_success());
460            assert!((s.objective_value() - 10.0).abs() < 1e-6);
461        }
462    }
463}