cplex_rs/
lib.rs

1//! Safe rust bindings to the CPLEX solver API.
2//!
3//! # Example
4//! ```
5//! use cplex_rs::*;
6//!
7//! let env = Environment::new().unwrap();
8//! let mut problem = Problem::new(env, "my_prob").unwrap();
9//!
10//! let v0 = problem.add_variable(Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x0")).unwrap();
11//! let v1 = problem.add_variable(Variable::new(VariableType::Continuous, 10.0, 0.0, 1.0, "x1")).unwrap();
12//! let c0 = problem.add_constraint(Constraint::new(ConstraintType::GreaterThanEq, 0.3, None, vec![(v0, 1.0)])).unwrap();
13//! let c1 = problem.add_constraint(Constraint::new(ConstraintType::Eq, 1.0, None, vec![(v0, 1.0), (v1, 1.0)])).unwrap();
14//!
15//! let solution = problem.set_objective_type(ObjectiveType::Maximize)
16//!    .unwrap()
17//!    .solve_as(ProblemType::Linear)
18//!    .unwrap();
19//!
20//! assert_eq!(solution.variable_value(v0), 0.3);
21//! assert_eq!(solution.variable_value(v1), 0.7);
22//! ```
23
24pub mod constants;
25mod constraints;
26mod environment;
27pub mod errors;
28pub mod logging;
29pub mod parameters;
30mod solution;
31mod variables;
32
33pub use constraints::*;
34pub use environment::*;
35pub use errors::{Error, Result};
36pub use ffi;
37use ffi::{
38    cpxlp, CPX_STAT_INForUNBD, CPXaddmipstarts, CPXaddrows, CPXchgobj, CPXchgobjsen,
39    CPXchgprobtype, CPXcreateprob, CPXfreeprob, CPXgetobjval, CPXgetstat, CPXgetx, CPXlpopt,
40    CPXmipopt, CPXnewcols, CPXwriteprob, CPXMIP_UNBOUNDED, CPXPROB_LP, CPXPROB_MILP, CPX_MAX,
41    CPX_MIN, CPX_STAT_INFEASIBLE, CPX_STAT_UNBOUNDED,
42};
43use log::debug;
44pub use solution::*;
45pub use variables::*;
46
47use std::{
48    ffi::{c_int, CString},
49    time::Instant,
50};
51
52mod macros {
53    macro_rules! cpx_lp_result {
54    ( unsafe { $func:ident ( $env:expr, $lp:expr $(, $b:expr)* $(,)? ) } ) => {
55        {
56            let status = unsafe { $func($env, $lp $(,$b)* ) };
57            if status != 0 {
58                Err(errors::Error::from(errors::Cplex::from_code($env, $lp, status)))
59            } else {
60                Ok(())
61            }
62        }
63    };
64}
65
66    pub(super) use cpx_lp_result;
67}
68
69/// A variable identifier, unique with respect to a given problem instance
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct VariableId(usize);
72
73impl VariableId {
74    pub fn into_inner(self) -> usize {
75        self.0
76    }
77}
78
79/// A constraint identifier, unique with respect to a given problem instance
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct ConstraintId(usize);
82
83impl ConstraintId {
84    pub fn into_inner(self) -> usize {
85        self.0
86    }
87}
88
89/// A CPLEX problem instance
90pub struct Problem {
91    inner: *mut cpxlp,
92    env: Environment,
93    variables: Vec<Variable>,
94    constraints: Vec<Constraint>,
95}
96
97unsafe impl Send for Problem {}
98
99#[derive(Copy, Clone, Debug)]
100pub enum ObjectiveType {
101    Maximize,
102    Minimize,
103}
104
105impl ObjectiveType {
106    fn into_raw(self) -> c_int {
107        match self {
108            ObjectiveType::Minimize => CPX_MIN as c_int,
109            ObjectiveType::Maximize => CPX_MAX as c_int,
110        }
111    }
112}
113
114#[derive(Copy, Clone, Debug, PartialEq, Eq)]
115pub enum ProblemType {
116    Linear,
117    MixedInteger,
118}
119
120impl ProblemType {
121    fn into_raw(self) -> c_int {
122        match self {
123            ProblemType::Linear => CPXPROB_LP as c_int,
124            ProblemType::MixedInteger => CPXPROB_MILP as c_int,
125        }
126    }
127}
128
129impl Problem {
130    /// Create a new CPLEX problem from a CPLEX environmant
131    pub fn new<S>(env: Environment, name: S) -> Result<Self>
132    where
133        S: AsRef<str>,
134    {
135        let mut status = 0;
136        let name =
137            CString::new(name.as_ref()).map_err(|e| errors::Input::from_message(e.to_string()))?;
138        let inner = unsafe { CPXcreateprob(env.inner, &mut status, name.as_ptr()) };
139        if inner.is_null() {
140            Err(errors::Cplex::from_code(env.inner, std::ptr::null(), status).into())
141        } else {
142            Ok(Problem {
143                inner,
144                env,
145                variables: vec![],
146                constraints: vec![],
147            })
148        }
149    }
150
151    /// Add a variable to the problem.
152    ///
153    /// The id for the Variable is returned.
154    pub fn add_variable(&mut self, var: Variable) -> Result<VariableId> {
155        let name = CString::new(var.name().as_bytes())
156            .map_err(|e| errors::Input::from_message(e.to_string()))?;
157
158        macros::cpx_lp_result!(unsafe {
159            CPXnewcols(
160                self.env.inner,
161                self.inner,
162                1,
163                &var.weight(),
164                &var.lower_bound(),
165                &var.upper_bound(),
166                &var.type_().into_raw() as *const u8 as *const i8,
167                &mut (name.as_ptr() as *mut _),
168            )
169        })?;
170
171        let index = self.variables.len();
172        self.variables.push(var);
173        Ok(VariableId(index))
174    }
175
176    /// Add an array of variables to the problem.
177    ///
178    /// The id for the variables are returned, in the same order they have been given in the input.
179    pub fn add_variables(&mut self, vars: Vec<Variable>) -> Result<Vec<VariableId>> {
180        let names = vars
181            .iter()
182            .map(|v| {
183                CString::new(v.name().as_bytes())
184                    .map_err(|e| errors::Input::from_message(e.to_string()).into())
185            })
186            .collect::<Result<Vec<_>>>()?;
187
188        let mut name_ptrs = names
189            .iter()
190            .map(|n| n.as_ptr() as *mut _)
191            .collect::<Vec<_>>();
192
193        let objs = vars.iter().map(|v| v.weight()).collect::<Vec<_>>();
194        let lbs = vars.iter().map(|v| v.lower_bound()).collect::<Vec<_>>();
195        let ubs = vars.iter().map(|v| v.upper_bound()).collect::<Vec<_>>();
196        let types = vars
197            .iter()
198            .map(|v| v.type_().into_raw() as i8)
199            .collect::<Vec<_>>();
200
201        macros::cpx_lp_result!(unsafe {
202            CPXnewcols(
203                self.env.inner,
204                self.inner,
205                vars.len() as i32,
206                objs.as_ptr(),
207                lbs.as_ptr(),
208                ubs.as_ptr(),
209                types.as_ptr(),
210                name_ptrs.as_mut_ptr(),
211            )
212        })?;
213
214        let indices: Vec<VariableId> = vars
215            .iter()
216            .enumerate()
217            .map(|(idx, _)| VariableId(idx + self.variables.len()))
218            .collect();
219        self.variables.extend(vars);
220        Ok(indices)
221    }
222
223    /// Add a constraint to the problem.
224    ///
225    /// The id for the constraint is returned.
226    pub fn add_constraint(&mut self, constraint: Constraint) -> Result<ConstraintId> {
227        let (ind, val): (Vec<c_int>, Vec<f64>) = constraint
228            .weights()
229            .iter()
230            .filter(|(_, weight)| *weight != 0.0)
231            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
232            .unzip();
233        let nz = val.len() as c_int;
234        let name = constraint
235            .name()
236            .map(|n| {
237                CString::new(n.as_bytes()).map_err(|e| errors::Input::from_message(e.to_string()))
238            })
239            .transpose()?;
240        macros::cpx_lp_result!(unsafe {
241            CPXaddrows(
242                self.env.inner,
243                self.inner,
244                0,
245                1,
246                nz,
247                &constraint.rhs(),
248                &constraint.type_().into_raw(),
249                &0,
250                ind.as_ptr(),
251                val.as_ptr(),
252                std::ptr::null_mut(),
253                &mut (name
254                    .as_ref()
255                    .map(|n| n.as_ptr())
256                    .unwrap_or(std::ptr::null()) as *mut _),
257            )
258        })?;
259
260        let index = self.constraints.len();
261        self.constraints.push(constraint);
262        Ok(ConstraintId(index))
263    }
264
265    /// Add an array of constraints to the problem.
266    ///
267    /// The id for the constraints are returned, in the same order they have been given in the input.
268    pub fn add_constraints(&mut self, con: Vec<Constraint>) -> Result<Vec<ConstraintId>> {
269        if con.is_empty() {
270            return Err(errors::Input::from_message(
271                "Called add_constraints with 0 constaints".to_owned(),
272            )
273            .into());
274        }
275        let beg = std::iter::once(0)
276            .chain(con[..con.len() - 1].iter().map(|c| c.weights().len()))
277            .scan(0, |state, x| {
278                *state += x;
279                Some(*state as i32)
280            })
281            .collect::<Vec<_>>();
282
283        let (ind, val): (Vec<c_int>, Vec<f64>) = con
284            .iter()
285            .flat_map(|c| c.weights().iter())
286            .filter(|(_, weight)| *weight != 0.0)
287            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
288            .unzip();
289
290        let nz = val.len() as c_int;
291        let names = con
292            .iter()
293            .map(|c| {
294                c.name()
295                    .map(|n| {
296                        CString::new(n.as_bytes())
297                            .map_err(|e| errors::Input::from_message(e.to_string()).into())
298                    })
299                    .transpose()
300            })
301            .collect::<Result<Vec<_>>>()?;
302
303        let mut name_ptrs = names
304            .iter()
305            .map(|n| {
306                n.as_ref()
307                    .map(|n| n.as_ptr())
308                    .unwrap_or(std::ptr::null_mut()) as *mut _
309            })
310            .collect::<Vec<_>>();
311
312        let rhss = con.iter().map(|c| c.rhs()).collect::<Vec<_>>();
313        let senses = con.iter().map(|c| c.type_().into_raw()).collect::<Vec<_>>();
314
315        macros::cpx_lp_result!(unsafe {
316            CPXaddrows(
317                self.env.inner,
318                self.inner,
319                0,
320                con.len() as i32,
321                nz,
322                rhss.as_ptr(),
323                senses.as_ptr(),
324                beg.as_ptr(),
325                ind.as_ptr(),
326                val.as_ptr(),
327                std::ptr::null_mut(),
328                name_ptrs.as_mut_ptr(),
329            )
330        })?;
331
332        let indices = con
333            .iter()
334            .enumerate()
335            .map(|(idx, _)| ConstraintId(idx + self.constraints.len()))
336            .collect();
337        self.constraints.extend(con);
338        Ok(indices)
339    }
340
341    /// Set the objective coefficients.
342    pub fn set_objective(self, ty: ObjectiveType, obj: Vec<(VariableId, f64)>) -> Result<Self> {
343        let (ind, val): (Vec<c_int>, Vec<f64>) = obj
344            .into_iter()
345            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
346            .unzip();
347
348        macros::cpx_lp_result!(unsafe {
349            CPXchgobj(
350                self.env.inner,
351                self.inner,
352                ind.len() as c_int,
353                ind.as_ptr(),
354                val.as_ptr(),
355            )
356        })?;
357
358        self.set_objective_type(ty)
359    }
360
361    /// Change the objective type. Default: `ObjectiveType::Minimize`.
362    pub fn set_objective_type(self, ty: ObjectiveType) -> Result<Self> {
363        macros::cpx_lp_result!(unsafe { CPXchgobjsen(self.env.inner, self.inner, ty.into_raw()) })?;
364        Ok(self)
365    }
366
367    /// Write the problem to a file named `name`.
368    pub fn write<S>(&self, name: S) -> Result<()>
369    where
370        S: AsRef<str>,
371    {
372        let name =
373            CString::new(name.as_ref()).map_err(|e| errors::Input::from_message(e.to_string()))?;
374
375        macros::cpx_lp_result!(unsafe {
376            CPXwriteprob(self.env.inner, self.inner, name.as_ptr(), std::ptr::null())
377        })
378    }
379
380    /// Add an initial solution to the problem.
381    ///
382    /// `vars` is an array of indices (i.e. the result of `prob.add_variable`) and `values` are
383    /// their values.
384    pub fn add_initial_soln(&mut self, vars: &[VariableId], values: &[f64]) -> Result<()> {
385        if values.len() != vars.len() {
386            return Err(errors::Input::from_message(
387                "number of solution variables and values does not match".to_string(),
388            )
389            .into());
390        }
391        let vars = vars.iter().map(|&u| u.0 as c_int).collect::<Vec<_>>();
392
393        macros::cpx_lp_result!(unsafe {
394            CPXaddmipstarts(
395                self.env.inner,
396                self.inner,
397                1,
398                vars.len() as c_int,
399                &0,
400                vars.as_ptr(),
401                values.as_ptr(),
402                &0,
403                &mut std::ptr::null_mut(),
404            )
405        })
406    }
407
408    /// Solve the Problem, returning a `Solution` object with the
409    /// result.
410    pub fn solve_as(self, pt: ProblemType) -> Result<Solution> {
411        macros::cpx_lp_result!(unsafe {
412            CPXchgprobtype(self.env.inner, self.inner, pt.into_raw())
413        })?;
414
415        let start_optim = Instant::now();
416        match pt {
417            ProblemType::MixedInteger => {
418                macros::cpx_lp_result!(unsafe { CPXmipopt(self.env.inner, self.inner) })?
419            }
420            ProblemType::Linear => {
421                macros::cpx_lp_result!(unsafe { CPXlpopt(self.env.inner, self.inner) })?
422            }
423        };
424        let elapsed = start_optim.elapsed();
425        debug!("CPLEX model solution took: {:?}", elapsed);
426
427        let code = unsafe { CPXgetstat(self.env.inner, self.inner) };
428        if code as u32 == CPX_STAT_INFEASIBLE || code as u32 == CPX_STAT_INForUNBD {
429            return Err(crate::errors::Cplex::Unfeasible {
430                code,
431                message: "Unfeasible problem".to_string(),
432            }
433            .into());
434        }
435
436        if code as u32 == CPX_STAT_UNBOUNDED || code as u32 == CPXMIP_UNBOUNDED {
437            return Err(crate::errors::Cplex::Unbounded {
438                code,
439                message: "Unbounded problem".to_string(),
440            }
441            .into());
442        }
443
444        let mut objective_value: f64 = 0.0;
445        macros::cpx_lp_result!(unsafe {
446            CPXgetobjval(self.env.inner, self.inner, &mut objective_value)
447        })?;
448
449        let mut variable_values = vec![0f64; self.variables.len()];
450        macros::cpx_lp_result!(unsafe {
451            CPXgetx(
452                self.env.inner,
453                self.inner,
454                variable_values.as_mut_ptr(),
455                0,
456                self.variables.len() as c_int - 1,
457            )
458        })?;
459
460        Ok(Solution::new(variable_values, objective_value))
461    }
462}
463
464impl Drop for Problem {
465    fn drop(&mut self) {
466        unsafe {
467            assert_eq!(CPXfreeprob(self.env.inner, &mut self.inner), 0);
468        }
469    }
470}
471
472#[cfg(test)]
473mod test {
474    use constants::INFINITY;
475    use constraints::ConstraintType;
476
477    use super::*;
478    use variables::{Variable, VariableType};
479
480    #[test]
481    fn mipex1() {
482        let env = Environment::new().unwrap();
483        let mut problem = Problem::new(env, "mipex1").unwrap();
484
485        let x0 = problem
486            .add_variable(Variable::new(
487                VariableType::Continuous,
488                1.0,
489                0.0,
490                40.0,
491                "x0",
492            ))
493            .unwrap();
494
495        let x1 = problem
496            .add_variable(Variable::new(
497                VariableType::Continuous,
498                2.0,
499                0.0,
500                INFINITY,
501                "x1",
502            ))
503            .unwrap();
504
505        let x2 = problem
506            .add_variable(Variable::new(
507                VariableType::Continuous,
508                3.0,
509                0.0,
510                INFINITY,
511                "x2",
512            ))
513            .unwrap();
514
515        let x3 = problem
516            .add_variable(Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"))
517            .unwrap();
518
519        assert_eq!(x0, VariableId(0));
520        assert_eq!(x1, VariableId(1));
521        assert_eq!(x2, VariableId(2));
522        assert_eq!(x3, VariableId(3));
523
524        let c0 = problem
525            .add_constraint(Constraint::new(
526                ConstraintType::LessThanEq,
527                20.0,
528                None,
529                vec![(x0, -1.0), (x1, 1.0), (x2, 1.0), (x3, 10.0)],
530            ))
531            .unwrap();
532
533        let c1 = problem
534            .add_constraint(Constraint::new(
535                ConstraintType::LessThanEq,
536                30.0,
537                None,
538                vec![(x0, 1.0), (x1, -3.0), (x2, 1.0)],
539            ))
540            .unwrap();
541
542        let c2 = problem
543            .add_constraint(Constraint::new(
544                ConstraintType::Eq,
545                0.0,
546                None,
547                vec![(x1, 1.0), (x3, -3.5)],
548            ))
549            .unwrap();
550
551        assert_eq!(c0, ConstraintId(0));
552        assert_eq!(c1, ConstraintId(1));
553        assert_eq!(c2, ConstraintId(2));
554
555        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
556
557        let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
558
559        assert_eq!(solution.objective_value(), 122.5);
560    }
561
562    #[test]
563    fn mipex1_batch() {
564        let env = Environment::new().unwrap();
565        let mut problem = Problem::new(env, "mipex1").unwrap();
566
567        let vars = problem
568            .add_variables(vec![
569                Variable::new(VariableType::Continuous, 1.0, 0.0, 40.0, "x0"),
570                Variable::new(VariableType::Continuous, 2.0, 0.0, INFINITY, "x1"),
571                Variable::new(VariableType::Continuous, 3.0, 0.0, INFINITY, "x2"),
572                Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"),
573            ])
574            .unwrap();
575
576        assert_eq!(
577            vars,
578            vec![VariableId(0), VariableId(1), VariableId(2), VariableId(3)]
579        );
580
581        let cons = problem
582            .add_constraints(vec![
583                Constraint::new(
584                    ConstraintType::LessThanEq,
585                    20.0,
586                    None,
587                    vec![
588                        (vars[0], -1.0),
589                        (vars[1], 1.0),
590                        (vars[2], 1.0),
591                        (vars[3], 10.0),
592                    ],
593                ),
594                Constraint::new(
595                    ConstraintType::LessThanEq,
596                    30.0,
597                    None,
598                    vec![(vars[0], 1.0), (vars[1], -3.0), (vars[2], 1.0)],
599                ),
600                Constraint::new(
601                    ConstraintType::Eq,
602                    0.0,
603                    None,
604                    vec![(vars[1], 1.0), (vars[3], -3.5)],
605                ),
606            ])
607            .unwrap();
608
609        assert_eq!(
610            cons,
611            vec![ConstraintId(0), ConstraintId(1), ConstraintId(2)]
612        );
613
614        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
615
616        let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
617
618        assert_eq!(solution.objective_value(), 122.5);
619    }
620
621    #[test]
622    fn unfeasible() {
623        let env = Environment::new().unwrap();
624        let mut problem = Problem::new(env, "unfeasible").unwrap();
625
626        let vars = problem
627            .add_variables(vec![
628                Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x0"),
629                Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x1"),
630            ])
631            .unwrap();
632
633        assert_eq!(vars, vec![VariableId(0), VariableId(1)]);
634
635        let cons = problem
636            .add_constraints(vec![
637                Constraint::new(
638                    ConstraintType::Eq,
639                    0.0,
640                    None,
641                    vec![(vars[0], 1.0), (vars[1], 1.0)],
642                ),
643                Constraint::new(
644                    ConstraintType::Eq,
645                    1.0,
646                    None,
647                    vec![(vars[0], 1.0), (vars[1], 1.0)],
648                ),
649            ])
650            .unwrap();
651
652        assert_eq!(cons, vec![ConstraintId(0), ConstraintId(1)]);
653
654        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
655        assert!(matches!(
656            problem.solve_as(ProblemType::Linear),
657            Err(errors::Error::Cplex(errors::Cplex::Unfeasible { .. }))
658        ));
659    }
660
661    #[test]
662    fn unbounded() {
663        let env = Environment::new().unwrap();
664        let mut problem = Problem::new(env, "unbounded").unwrap();
665
666        problem
667            .add_variable(Variable::new(
668                VariableType::Integer,
669                1.0,
670                0.0,
671                INFINITY,
672                "x0",
673            ))
674            .unwrap();
675
676        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
677
678        assert!(matches!(
679            problem.solve_as(ProblemType::MixedInteger),
680            Err(errors::Error::Cplex(errors::Cplex::Unbounded { .. }))
681        ));
682    }
683}