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    /// Get a mutable reference to the environment of the problem.
152    pub fn env_mut(&mut self) -> &mut Environment {
153        &mut self.env
154    }
155
156    /// Get a reference to the environment of the problem.
157    pub fn env(&self) -> &Environment {
158        &self.env
159    }
160
161    /// Add a variable to the problem.
162    ///
163    /// The id for the Variable is returned.
164    pub fn add_variable(&mut self, var: Variable) -> Result<VariableId> {
165        let name = CString::new(var.name().as_bytes())
166            .map_err(|e| errors::Input::from_message(e.to_string()))?;
167
168        macros::cpx_lp_result!(unsafe {
169            CPXnewcols(
170                self.env.inner,
171                self.inner,
172                1,
173                &var.weight(),
174                &var.lower_bound(),
175                &var.upper_bound(),
176                &var.type_().into_raw() as *const u8 as *const i8,
177                &mut (name.as_ptr() as *mut _),
178            )
179        })?;
180
181        let index = self.variables.len();
182        self.variables.push(var);
183        Ok(VariableId(index))
184    }
185
186    /// Add an array of variables to the problem.
187    ///
188    /// The id for the variables are returned, in the same order they have been given in the input.
189    pub fn add_variables(&mut self, vars: Vec<Variable>) -> Result<Vec<VariableId>> {
190        let names = vars
191            .iter()
192            .map(|v| {
193                CString::new(v.name().as_bytes())
194                    .map_err(|e| errors::Input::from_message(e.to_string()).into())
195            })
196            .collect::<Result<Vec<_>>>()?;
197
198        let mut name_ptrs = names
199            .iter()
200            .map(|n| n.as_ptr() as *mut _)
201            .collect::<Vec<_>>();
202
203        let objs = vars.iter().map(|v| v.weight()).collect::<Vec<_>>();
204        let lbs = vars.iter().map(|v| v.lower_bound()).collect::<Vec<_>>();
205        let ubs = vars.iter().map(|v| v.upper_bound()).collect::<Vec<_>>();
206        let types = vars
207            .iter()
208            .map(|v| v.type_().into_raw() as i8)
209            .collect::<Vec<_>>();
210
211        macros::cpx_lp_result!(unsafe {
212            CPXnewcols(
213                self.env.inner,
214                self.inner,
215                vars.len() as i32,
216                objs.as_ptr(),
217                lbs.as_ptr(),
218                ubs.as_ptr(),
219                types.as_ptr(),
220                name_ptrs.as_mut_ptr(),
221            )
222        })?;
223
224        let indices: Vec<VariableId> = vars
225            .iter()
226            .enumerate()
227            .map(|(idx, _)| VariableId(idx + self.variables.len()))
228            .collect();
229        self.variables.extend(vars);
230        Ok(indices)
231    }
232
233    /// Add a constraint to the problem.
234    ///
235    /// The id for the constraint is returned.
236    pub fn add_constraint(&mut self, constraint: Constraint) -> Result<ConstraintId> {
237        let (ind, val): (Vec<c_int>, Vec<f64>) = constraint
238            .weights()
239            .iter()
240            .filter(|(_, weight)| *weight != 0.0)
241            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
242            .unzip();
243        let nz = val.len() as c_int;
244        let name = constraint
245            .name()
246            .map(|n| {
247                CString::new(n.as_bytes()).map_err(|e| errors::Input::from_message(e.to_string()))
248            })
249            .transpose()?;
250        macros::cpx_lp_result!(unsafe {
251            CPXaddrows(
252                self.env.inner,
253                self.inner,
254                0,
255                1,
256                nz,
257                &constraint.rhs(),
258                &constraint.type_().into_raw(),
259                &0,
260                ind.as_ptr(),
261                val.as_ptr(),
262                std::ptr::null_mut(),
263                &mut (name
264                    .as_ref()
265                    .map(|n| n.as_ptr())
266                    .unwrap_or(std::ptr::null()) as *mut _),
267            )
268        })?;
269
270        let index = self.constraints.len();
271        self.constraints.push(constraint);
272        Ok(ConstraintId(index))
273    }
274
275    /// Add an array of constraints to the problem.
276    ///
277    /// The id for the constraints are returned, in the same order they have been given in the input.
278    pub fn add_constraints(&mut self, con: Vec<Constraint>) -> Result<Vec<ConstraintId>> {
279        if con.is_empty() {
280            return Err(errors::Input::from_message(
281                "Called add_constraints with 0 constaints".to_owned(),
282            )
283            .into());
284        }
285        let beg = std::iter::once(0)
286            .chain(con[..con.len() - 1].iter().map(|c| c.weights().len()))
287            .scan(0, |state, x| {
288                *state += x;
289                Some(*state as i32)
290            })
291            .collect::<Vec<_>>();
292
293        let (ind, val): (Vec<c_int>, Vec<f64>) = con
294            .iter()
295            .flat_map(|c| c.weights().iter())
296            .filter(|(_, weight)| *weight != 0.0)
297            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
298            .unzip();
299
300        let nz = val.len() as c_int;
301        let names = con
302            .iter()
303            .map(|c| {
304                c.name()
305                    .map(|n| {
306                        CString::new(n.as_bytes())
307                            .map_err(|e| errors::Input::from_message(e.to_string()).into())
308                    })
309                    .transpose()
310            })
311            .collect::<Result<Vec<_>>>()?;
312
313        let mut name_ptrs = names
314            .iter()
315            .map(|n| {
316                n.as_ref()
317                    .map(|n| n.as_ptr())
318                    .unwrap_or(std::ptr::null_mut()) as *mut _
319            })
320            .collect::<Vec<_>>();
321
322        let rhss = con.iter().map(|c| c.rhs()).collect::<Vec<_>>();
323        let senses = con.iter().map(|c| c.type_().into_raw()).collect::<Vec<_>>();
324
325        macros::cpx_lp_result!(unsafe {
326            CPXaddrows(
327                self.env.inner,
328                self.inner,
329                0,
330                con.len() as i32,
331                nz,
332                rhss.as_ptr(),
333                senses.as_ptr(),
334                beg.as_ptr(),
335                ind.as_ptr(),
336                val.as_ptr(),
337                std::ptr::null_mut(),
338                name_ptrs.as_mut_ptr(),
339            )
340        })?;
341
342        let indices = con
343            .iter()
344            .enumerate()
345            .map(|(idx, _)| ConstraintId(idx + self.constraints.len()))
346            .collect();
347        self.constraints.extend(con);
348        Ok(indices)
349    }
350
351    /// Set the objective coefficients.
352    pub fn set_objective(self, ty: ObjectiveType, obj: Vec<(VariableId, f64)>) -> Result<Self> {
353        let (ind, val): (Vec<c_int>, Vec<f64>) = obj
354            .into_iter()
355            .map(|(var_id, weight)| (var_id.0 as c_int, weight))
356            .unzip();
357
358        macros::cpx_lp_result!(unsafe {
359            CPXchgobj(
360                self.env.inner,
361                self.inner,
362                ind.len() as c_int,
363                ind.as_ptr(),
364                val.as_ptr(),
365            )
366        })?;
367
368        self.set_objective_type(ty)
369    }
370
371    /// Change the objective type. Default: `ObjectiveType::Minimize`.
372    pub fn set_objective_type(self, ty: ObjectiveType) -> Result<Self> {
373        macros::cpx_lp_result!(unsafe { CPXchgobjsen(self.env.inner, self.inner, ty.into_raw()) })?;
374        Ok(self)
375    }
376
377    /// Write the problem to a file named `name`.
378    pub fn write<S>(&self, name: S) -> Result<()>
379    where
380        S: AsRef<str>,
381    {
382        let name =
383            CString::new(name.as_ref()).map_err(|e| errors::Input::from_message(e.to_string()))?;
384
385        macros::cpx_lp_result!(unsafe {
386            CPXwriteprob(self.env.inner, self.inner, name.as_ptr(), std::ptr::null())
387        })
388    }
389
390    /// Add an initial solution to the problem.
391    ///
392    /// `vars` is an array of indices (i.e. the result of `prob.add_variable`) and `values` are
393    /// their values.
394    pub fn add_initial_soln(&mut self, vars: &[VariableId], values: &[f64]) -> Result<()> {
395        if values.len() != vars.len() {
396            return Err(errors::Input::from_message(
397                "number of solution variables and values does not match".to_string(),
398            )
399            .into());
400        }
401        let vars = vars.iter().map(|&u| u.0 as c_int).collect::<Vec<_>>();
402
403        macros::cpx_lp_result!(unsafe {
404            CPXaddmipstarts(
405                self.env.inner,
406                self.inner,
407                1,
408                vars.len() as c_int,
409                &0,
410                vars.as_ptr(),
411                values.as_ptr(),
412                &0,
413                &mut std::ptr::null_mut(),
414            )
415        })
416    }
417
418    /// Solve the Problem, returning a `Solution` object with the
419    /// result.
420    pub fn solve_as(self, pt: ProblemType) -> Result<Solution> {
421        macros::cpx_lp_result!(unsafe {
422            CPXchgprobtype(self.env.inner, self.inner, pt.into_raw())
423        })?;
424
425        let start_optim = Instant::now();
426        match pt {
427            ProblemType::MixedInteger => {
428                macros::cpx_lp_result!(unsafe { CPXmipopt(self.env.inner, self.inner) })?
429            }
430            ProblemType::Linear => {
431                macros::cpx_lp_result!(unsafe { CPXlpopt(self.env.inner, self.inner) })?
432            }
433        };
434        let elapsed = start_optim.elapsed();
435        debug!("CPLEX model solution took: {:?}", elapsed);
436
437        let code = unsafe { CPXgetstat(self.env.inner, self.inner) };
438        if code as u32 == CPX_STAT_INFEASIBLE || code as u32 == CPX_STAT_INForUNBD {
439            return Err(crate::errors::Cplex::Unfeasible {
440                code,
441                message: "Unfeasible problem".to_string(),
442            }
443            .into());
444        }
445
446        if code as u32 == CPX_STAT_UNBOUNDED || code as u32 == CPXMIP_UNBOUNDED {
447            return Err(crate::errors::Cplex::Unbounded {
448                code,
449                message: "Unbounded problem".to_string(),
450            }
451            .into());
452        }
453
454        let mut objective_value: f64 = 0.0;
455        macros::cpx_lp_result!(unsafe {
456            CPXgetobjval(self.env.inner, self.inner, &mut objective_value)
457        })?;
458
459        let mut variable_values = vec![0f64; self.variables.len()];
460        macros::cpx_lp_result!(unsafe {
461            CPXgetx(
462                self.env.inner,
463                self.inner,
464                variable_values.as_mut_ptr(),
465                0,
466                self.variables.len() as c_int - 1,
467            )
468        })?;
469
470        Ok(Solution::new(variable_values, objective_value))
471    }
472}
473
474impl Drop for Problem {
475    fn drop(&mut self) {
476        unsafe {
477            assert_eq!(CPXfreeprob(self.env.inner, &mut self.inner), 0);
478        }
479    }
480}
481
482#[cfg(test)]
483mod test {
484    use constants::INFINITY;
485    use constraints::ConstraintType;
486
487    use super::*;
488    use variables::{Variable, VariableType};
489
490    #[test]
491    fn mipex1() {
492        let env = Environment::new().unwrap();
493        let mut problem = Problem::new(env, "mipex1").unwrap();
494
495        let x0 = problem
496            .add_variable(Variable::new(
497                VariableType::Continuous,
498                1.0,
499                0.0,
500                40.0,
501                "x0",
502            ))
503            .unwrap();
504
505        let x1 = problem
506            .add_variable(Variable::new(
507                VariableType::Continuous,
508                2.0,
509                0.0,
510                INFINITY,
511                "x1",
512            ))
513            .unwrap();
514
515        let x2 = problem
516            .add_variable(Variable::new(
517                VariableType::Continuous,
518                3.0,
519                0.0,
520                INFINITY,
521                "x2",
522            ))
523            .unwrap();
524
525        let x3 = problem
526            .add_variable(Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"))
527            .unwrap();
528
529        assert_eq!(x0, VariableId(0));
530        assert_eq!(x1, VariableId(1));
531        assert_eq!(x2, VariableId(2));
532        assert_eq!(x3, VariableId(3));
533
534        let c0 = problem
535            .add_constraint(Constraint::new(
536                ConstraintType::LessThanEq,
537                20.0,
538                None,
539                vec![(x0, -1.0), (x1, 1.0), (x2, 1.0), (x3, 10.0)],
540            ))
541            .unwrap();
542
543        let c1 = problem
544            .add_constraint(Constraint::new(
545                ConstraintType::LessThanEq,
546                30.0,
547                None,
548                vec![(x0, 1.0), (x1, -3.0), (x2, 1.0)],
549            ))
550            .unwrap();
551
552        let c2 = problem
553            .add_constraint(Constraint::new(
554                ConstraintType::Eq,
555                0.0,
556                None,
557                vec![(x1, 1.0), (x3, -3.5)],
558            ))
559            .unwrap();
560
561        assert_eq!(c0, ConstraintId(0));
562        assert_eq!(c1, ConstraintId(1));
563        assert_eq!(c2, ConstraintId(2));
564
565        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
566
567        let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
568
569        assert_eq!(solution.objective_value(), 122.5);
570    }
571
572    #[test]
573    fn mipex1_batch() {
574        let env = Environment::new().unwrap();
575        let mut problem = Problem::new(env, "mipex1").unwrap();
576
577        let vars = problem
578            .add_variables(vec![
579                Variable::new(VariableType::Continuous, 1.0, 0.0, 40.0, "x0"),
580                Variable::new(VariableType::Continuous, 2.0, 0.0, INFINITY, "x1"),
581                Variable::new(VariableType::Continuous, 3.0, 0.0, INFINITY, "x2"),
582                Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"),
583            ])
584            .unwrap();
585
586        assert_eq!(
587            vars,
588            vec![VariableId(0), VariableId(1), VariableId(2), VariableId(3)]
589        );
590
591        let cons = problem
592            .add_constraints(vec![
593                Constraint::new(
594                    ConstraintType::LessThanEq,
595                    20.0,
596                    None,
597                    vec![
598                        (vars[0], -1.0),
599                        (vars[1], 1.0),
600                        (vars[2], 1.0),
601                        (vars[3], 10.0),
602                    ],
603                ),
604                Constraint::new(
605                    ConstraintType::LessThanEq,
606                    30.0,
607                    None,
608                    vec![(vars[0], 1.0), (vars[1], -3.0), (vars[2], 1.0)],
609                ),
610                Constraint::new(
611                    ConstraintType::Eq,
612                    0.0,
613                    None,
614                    vec![(vars[1], 1.0), (vars[3], -3.5)],
615                ),
616            ])
617            .unwrap();
618
619        assert_eq!(
620            cons,
621            vec![ConstraintId(0), ConstraintId(1), ConstraintId(2)]
622        );
623
624        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
625
626        let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
627
628        assert_eq!(solution.objective_value(), 122.5);
629    }
630
631    #[test]
632    fn unfeasible() {
633        let env = Environment::new().unwrap();
634        let mut problem = Problem::new(env, "unfeasible").unwrap();
635
636        let vars = problem
637            .add_variables(vec![
638                Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x0"),
639                Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x1"),
640            ])
641            .unwrap();
642
643        assert_eq!(vars, vec![VariableId(0), VariableId(1)]);
644
645        let cons = problem
646            .add_constraints(vec![
647                Constraint::new(
648                    ConstraintType::Eq,
649                    0.0,
650                    None,
651                    vec![(vars[0], 1.0), (vars[1], 1.0)],
652                ),
653                Constraint::new(
654                    ConstraintType::Eq,
655                    1.0,
656                    None,
657                    vec![(vars[0], 1.0), (vars[1], 1.0)],
658                ),
659            ])
660            .unwrap();
661
662        assert_eq!(cons, vec![ConstraintId(0), ConstraintId(1)]);
663
664        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
665        assert!(matches!(
666            problem.solve_as(ProblemType::Linear),
667            Err(errors::Error::Cplex(errors::Cplex::Unfeasible { .. }))
668        ));
669    }
670
671    #[test]
672    fn unbounded() {
673        let env = Environment::new().unwrap();
674        let mut problem = Problem::new(env, "unbounded").unwrap();
675
676        problem
677            .add_variable(Variable::new(
678                VariableType::Integer,
679                1.0,
680                0.0,
681                INFINITY,
682                "x0",
683            ))
684            .unwrap();
685
686        let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
687
688        assert!(matches!(
689            problem.solve_as(ProblemType::MixedInteger),
690            Err(errors::Error::Cplex(errors::Cplex::Unbounded { .. }))
691        ));
692    }
693}