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(
247            &mut self,
248            start: i32,
249            size: i64,
250            end: i32,
251            name: &str,
252        ) -> i32 {
253            let c = CString::new(name).unwrap();
254            unsafe { cpmodel_new_interval_var(self.ptr.as_ptr(), start, size, end, c.as_ptr()) }
255        }
256
257        pub fn new_optional_interval_var(
258            &mut self,
259            start: i32,
260            size: i64,
261            end: i32,
262            lit: i32,
263            name: &str,
264        ) -> i32 {
265            let c = CString::new(name).unwrap();
266            unsafe {
267                cpmodel_new_optional_interval_var(
268                    self.ptr.as_ptr(),
269                    start,
270                    size,
271                    end,
272                    lit,
273                    c.as_ptr(),
274                )
275            }
276        }
277
278        /// Add a Hamiltonian circuit constraint.
279        ///
280        /// Each arc is `(tail, head, literal)`.  The arcs whose literal equals 1
281        /// must form a single circuit covering all nodes that have no self-loop
282        /// literal set to 1.  Use a self-loop `(i, i, lit)` to make node `i`
283        /// optional — if `lit = 1` the node is skipped.
284        pub fn add_circuit(&mut self, tails: &[i32], heads: &[i32], lits: &[i32]) {
285            assert_eq!(tails.len(), heads.len());
286            assert_eq!(tails.len(), lits.len());
287            unsafe {
288                cpmodel_add_circuit(
289                    self.ptr.as_ptr(),
290                    tails.as_ptr(),
291                    heads.as_ptr(),
292                    lits.as_ptr(),
293                    tails.len(),
294                );
295            }
296        }
297
298        pub fn add_no_overlap(&mut self, intervals: &[i32]) {
299            unsafe {
300                cpmodel_add_no_overlap(self.ptr.as_ptr(), intervals.as_ptr(), intervals.len());
301            }
302        }
303
304        pub fn solve(&self, time_limit_seconds: f64) -> CpSolution {
305            unsafe {
306                CpSolution {
307                    ptr: NonNull::new(cpmodel_solve(self.ptr.as_ptr(), time_limit_seconds))
308                        .expect("cpmodel_solve returned null"),
309                }
310            }
311        }
312    }
313
314    impl Default for CpModel {
315        fn default() -> Self {
316            Self::new()
317        }
318    }
319
320    impl Drop for CpModel {
321        fn drop(&mut self) {
322            unsafe { cpmodel_free(self.ptr.as_ptr()) }
323        }
324    }
325
326    pub struct CpSolution {
327        ptr: NonNull<CpSolverResponse>,
328    }
329
330    impl CpSolution {
331        pub fn status(&self) -> OrtoolsStatus {
332            unsafe { cpresponse_status(self.ptr.as_ptr()) }
333        }
334        pub fn objective_value(&self) -> i64 {
335            unsafe { cpresponse_objective_value(self.ptr.as_ptr()) }
336        }
337        pub fn value(&self, var_index: i32) -> i64 {
338            unsafe { cpresponse_value(self.ptr.as_ptr(), var_index) }
339        }
340        pub fn wall_time(&self) -> f64 {
341            unsafe { cpresponse_wall_time(self.ptr.as_ptr()) }
342        }
343    }
344
345    impl Drop for CpSolution {
346        fn drop(&mut self) {
347            unsafe { cpresponse_free(self.ptr.as_ptr()) }
348        }
349    }
350
351    pub struct LinearSolver {
352        ptr: NonNull<MpSolver>,
353    }
354
355    impl LinearSolver {
356        pub fn new_glop(name: &str) -> Self {
357            let c = CString::new(name).unwrap();
358            unsafe {
359                Self {
360                    ptr: NonNull::new(mpsolver_new(c.as_ptr(), LpSolverType::Glop))
361                        .expect("mpsolver_new returned null"),
362                }
363            }
364        }
365
366        pub fn num_var(&mut self, lb: f64, ub: f64, name: &str) -> i32 {
367            let c = CString::new(name).unwrap();
368            unsafe { mpsolver_num_var(self.ptr.as_ptr(), lb, ub, c.as_ptr()) }
369        }
370
371        pub fn add_constraint(&mut self, lb: f64, ub: f64, name: &str) -> i32 {
372            let c = CString::new(name).unwrap();
373            unsafe { mpsolver_add_constraint(self.ptr.as_ptr(), lb, ub, c.as_ptr()) }
374        }
375
376        pub fn set_constraint_coeff(&mut self, ci: i32, vi: i32, coeff: f64) {
377            unsafe { mpsolver_set_constraint_coeff(self.ptr.as_ptr(), ci, vi, coeff) }
378        }
379
380        pub fn set_objective_coeff(&mut self, vi: i32, coeff: f64) {
381            unsafe { mpsolver_set_objective_coeff(self.ptr.as_ptr(), vi, coeff) }
382        }
383
384        pub fn maximize(&mut self) {
385            unsafe { mpsolver_maximize(self.ptr.as_ptr()) }
386        }
387        pub fn minimize(&mut self) {
388            unsafe { mpsolver_minimize(self.ptr.as_ptr()) }
389        }
390
391        pub fn solve(&mut self) -> OrtoolsStatus {
392            unsafe { mpsolver_solve(self.ptr.as_ptr()) }
393        }
394
395        pub fn objective_value(&self) -> f64 {
396            unsafe { mpsolver_objective_value(self.ptr.as_ptr()) }
397        }
398
399        pub fn var_value(&self, vi: i32) -> f64 {
400            unsafe { mpsolver_var_value(self.ptr.as_ptr(), vi) }
401        }
402    }
403
404    impl Drop for LinearSolver {
405        fn drop(&mut self) {
406            unsafe { mpsolver_free(self.ptr.as_ptr()) }
407        }
408    }
409}
410
411// ── Tests ─────────────────────────────────────────────────────────────────────
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn status_values() {
419        assert_eq!(OrtoolsStatus::Optimal as i32, 1);
420        assert!(OrtoolsStatus::Optimal.is_success());
421        assert!(!OrtoolsStatus::Infeasible.is_success());
422    }
423
424    #[cfg(feature = "link")]
425    mod integration {
426        use super::super::safe::*;
427
428        #[test]
429        fn cpsat_minimize() {
430            let mut m = CpModel::new();
431            let x = m.new_int_var(0, 10, "x");
432            let y = m.new_int_var(0, 10, "y");
433            m.add_linear_eq(&[x, y], &[1, 1], 10);
434            m.minimize(&[x], &[1]);
435            let s = m.solve(60.0);
436            assert!(s.status().is_success());
437            assert_eq!(s.value(x), 0);
438            assert_eq!(s.value(y), 10);
439        }
440
441        #[test]
442        fn cpsat_all_different() {
443            let mut model = CpModel::new();
444            let va = model.new_int_var(1, 3, "a");
445            let vb = model.new_int_var(1, 3, "b");
446            let vc = model.new_int_var(1, 3, "c");
447            model.add_all_different(&[va, vb, vc]);
448            let sol = model.solve(60.0);
449            assert!(sol.status().is_success());
450            let vals = [sol.value(va), sol.value(vb), sol.value(vc)];
451            assert!(vals.contains(&1) && vals.contains(&2) && vals.contains(&3));
452        }
453
454        #[test]
455        fn glop_maximize() {
456            let mut s = LinearSolver::new_glop("test");
457            let x = s.num_var(0.0, f64::INFINITY, "x");
458            let y = s.num_var(0.0, f64::INFINITY, "y");
459            let c = s.add_constraint(f64::NEG_INFINITY, 10.0, "c1");
460            s.set_constraint_coeff(c, x, 1.0);
461            s.set_constraint_coeff(c, y, 1.0);
462            s.set_objective_coeff(x, 1.0);
463            s.set_objective_coeff(y, 1.0);
464            s.maximize();
465            assert!(s.solve().is_success());
466            assert!((s.objective_value() - 10.0).abs() < 1e-6);
467        }
468    }
469}