Skip to content

Commit

Permalink
Merge pull request #202 from scipopt/cons-builder
Browse files Browse the repository at this point in the history
Ergonomic constraint builder
  • Loading branch information
mmghannam authored Jan 27, 2025
2 parents 6314f56 + 4a9b685 commit 38b1869
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 54 deletions.
14 changes: 5 additions & 9 deletions examples/create_and_solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@ use russcip::prelude::*;

fn main() {
// Create model
let mut model = Model::new()
.hide_output()
.include_default_plugins()
.create_prob("test")
.set_obj_sense(ObjSense::Maximize);
let mut model = Model::default().maximize();

// Add variables
let x1 = model.add_var(0., f64::INFINITY, 3., "x1", VarType::Integer);
let x2 = model.add_var(0., f64::INFINITY, 4., "x2", VarType::Integer);
let x1 = model.add(var().integer(0, isize::MAX).obj(3.).name("x1"));
let x2 = model.add(var().integer(0, isize::MAX).obj(2.).name("x2"));

// Add constraints
model.add_cons(vec![&x1, &x2], &[2., 1.], -f64::INFINITY, 100., "c1");
model.add_cons(vec![&x1, &x2], &[1., 2.], -f64::INFINITY, 80., "c2");
model.add(cons().name("c1").coef(&x1, 2.).coef(&x2, 1.).le(100.));
model.add(cons().name("c2").coef(&x1, 1.).coef(&x2, 2.).le(80.));

let solved_model = model.solve();

Expand Down
111 changes: 111 additions & 0 deletions src/builder/cons.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use crate::builder::CanBeAddedToModel;
use crate::{Constraint, Model, ModelWithProblem, ProblemCreated, ProblemOrSolving, Variable};

/// A builder for creating constraints.
#[derive(Debug)]
pub struct ConsBuilder<'a> {
lhs: f64,
rhs: f64,
name: Option<&'a str>,
coefs: Vec<(&'a Variable, f64)>,
}

/// Creates a new default `ConsBuilder`.
pub fn cons() -> ConsBuilder<'static> {
ConsBuilder::default()
}

impl Default for ConsBuilder<'_> {
fn default() -> Self {
ConsBuilder {
lhs: f64::NEG_INFINITY,
rhs: f64::INFINITY,
name: None,
coefs: Vec::new(),
}
}
}

impl<'a> ConsBuilder<'a> {
/// Creates a constraint of the form `expr <= val`.
pub fn le(mut self, val: f64) -> Self {
self.rhs = val;
self.lhs = f64::NEG_INFINITY;
self
}

/// Creates a constraint of the form `val <= expr`.
pub fn ge(mut self, val: f64) -> Self {
self.lhs = val;
self.rhs = f64::INFINITY;
self
}

/// Creates a constraint of the form `expr = val`.
pub fn eq(mut self, val: f64) -> Self {
self.lhs = val;
self.rhs = val;
self
}

/// Sets the name of the constraint.
pub fn name(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}

/// Adds a coefficient to the constraint.
pub fn coef(mut self, var: &'a Variable, coef: f64) -> Self {
self.coefs.push((var, coef));
self
}
}

impl CanBeAddedToModel for ConsBuilder<'_> {
type Return = Constraint;
fn add(self, model: &mut Model<ProblemCreated>) -> Self::Return {
let mut vars = Vec::new();
let mut coefs = Vec::new();
for (var, coef) in self.coefs {
vars.push(var);
coefs.push(coef);
}

let name = self.name.map(|s| s.to_string()).unwrap_or_else(|| {
let n_cons = model.n_conss();
format!("cons{}", n_cons)
});
model.add_cons(vars, &coefs, self.lhs, self.rhs, &name)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::builder::var::var;
use crate::minimal_model;

#[test]
fn test_cons_builder() {
let mut model = minimal_model().hide_output();
let var = model.add(var().binary().obj(1.));
let cons = cons().name("c").eq(1.0).coef(&var, 1.0);

assert_eq!(cons.name, Some("c"));
assert_eq!(cons.lhs, 1.0);
assert_eq!(cons.rhs, 1.0);
assert_eq!(cons.coefs.len(), 1);
assert_eq!(cons.coefs[0].1, 1.0);

model.add(cons);

assert_eq!(model.n_conss(), 1);
let cons = &model.conss()[0];
assert_eq!(cons.name(), "c");

let solved = model.solve();

assert_eq!(solved.status(), crate::Status::Optimal);
assert_eq!(solved.obj_val(), 1.0);
}
}
25 changes: 25 additions & 0 deletions src/builder/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// This module contains `ConsBuilder` for easily creating constraints.
pub mod cons;
/// This module contains `VarBuilder` for easily creating variables.
pub mod var;

use crate::{Model, ProblemCreated};

/// A trait for adding two values together.
pub trait CanBeAddedToModel {
/// The return type after adding to the model (e.g. `Variable` / `Constraint` ).
type Return;
/// How to add the value to the model.
fn add(self, model: &mut Model<ProblemCreated>) -> Self::Return;
}

impl<T, I> CanBeAddedToModel for I
where
T: CanBeAddedToModel,
I: IntoIterator<Item = T>,
{
type Return = Vec<T::Return>;
fn add(self, model: &mut Model<ProblemCreated>) -> Self::Return {
self.into_iter().map(|x| x.add(model)).collect()
}
}
63 changes: 19 additions & 44 deletions src/builder.rs → src/builder/var.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
use crate::builder::CanBeAddedToModel;
use crate::{Model, ModelWithProblem, ProblemCreated, VarType, Variable};

/// A trait for adding two values together.
pub trait CanBeAddedToModel {
/// The return type after adding to the model (e.g. `Variable` / `Constraint` ).
type Return;
/// How to add the value to the model.
fn add(self, model: &mut Model<ProblemCreated>) -> Self::Return;
}

impl<T, I> CanBeAddedToModel for I
where
T: CanBeAddedToModel,
I: IntoIterator<Item = T>,
{
type Return = Vec<T::Return>;
fn add(self, model: &mut Model<ProblemCreated>) -> Self::Return {
self.into_iter().map(|x| x.add(model)).collect()
}
}

/// A builder for variables.
pub struct VarBuilder {
name: Option<String>,
pub struct VarBuilder<'a> {
name: Option<&'a str>,
obj: f64,
lb: f64,
ub: f64,
var_type: VarType,
}

impl Default for VarBuilder {
/// Creates a new default `VarBuilder`.
pub fn var<'a>() -> VarBuilder<'a> {
VarBuilder::default()
}

impl Default for VarBuilder<'_> {
fn default() -> Self {
VarBuilder {
name: None,
Expand All @@ -40,7 +27,7 @@ impl Default for VarBuilder {
}
}

impl VarBuilder {
impl<'a> VarBuilder<'a> {
/// Sets the variable to be an integer variable.
pub fn integer(mut self, lb: isize, ub: isize) -> Self {
self.lb = lb as f64;
Expand Down Expand Up @@ -74,7 +61,7 @@ impl VarBuilder {
}

/// Sets the name of the variable.
pub fn name(mut self, name: String) -> Self {
pub fn name(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}
Expand All @@ -86,10 +73,10 @@ impl VarBuilder {
}
}

impl CanBeAddedToModel for VarBuilder {
impl CanBeAddedToModel for VarBuilder<'_> {
type Return = Variable;
fn add(self, model: &mut Model<ProblemCreated>) -> Variable {
let name = self.name.clone().unwrap_or_else(|| {
let name = self.name.map(|s| s.to_string()).unwrap_or_else(|| {
let n_vars = model.n_vars();
format!("x{}", n_vars)
});
Expand All @@ -105,11 +92,11 @@ mod tests {
#[test]
fn test_var_builder() {
let var = VarBuilder::default()
.name("x".to_string())
.name("x")
.obj(1.0)
.continuous(0.0, 1.0);

assert_eq!(var.name, Some("x".to_string()));
assert_eq!(var.name, Some("x"));
assert_eq!(var.obj, 1.0);
assert_eq!(var.lb, 0.0);
assert_eq!(var.ub, 1.0);
Expand All @@ -118,10 +105,7 @@ mod tests {
#[test]
fn test_var_builder_add() {
let mut model = Model::default().set_obj_sense(crate::ObjSense::Maximize);
let var = VarBuilder::default()
.name("x".to_string())
.obj(1.0)
.continuous(0.0, 1.0);
let var = var().name("x").obj(1.0).continuous(0.0, 1.0);

let var = model.add(var);

Expand All @@ -140,18 +124,9 @@ mod tests {
fn test_var_add_all() {
let mut model = Model::default().set_obj_sense(crate::ObjSense::Maximize);
let vars = vec![
VarBuilder::default()
.name("1".to_string())
.obj(1.0)
.continuous(0.0, 1.0),
VarBuilder::default()
.name("2".to_string())
.obj(1.0)
.continuous(0.0, 1.0),
VarBuilder::default()
.name("3".to_string())
.obj(1.0)
.continuous(0.0, 1.0),
var().name("1").obj(1.0).continuous(0.0, 1.0),
var().name("2").obj(1.0).continuous(0.0, 1.0),
var().name("3").obj(1.0).continuous(0.0, 1.0),
];

let vars = model.add(vars);
Expand Down
16 changes: 15 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
//! # russcip
//! Safe Rust interface for [SCIP](https://scipopt.org/) optimization suite.
//!
//! For examples and usage, please refer to the [repository](https://github.com/scipopt/russcip).
//! For usage, please refer to the [README](https://github.com/scipopt/russcip).
//!
//! # Example
//! ```rust
//! use russcip::prelude::*;
//!
//! let mut model = Model::default().minimize();
//! let x = model.add(var().binary().obj(1.0));
//! let y = model.add(var().binary().obj(2.0));
//! model.add(cons().coef(&x, 1.0).coef(&y, 1.0).eq(1.0));
//!
//! let solve = model.solve();
//! assert_eq!(solve.status(), Status::Optimal);
//! assert_eq!(solve.obj_val(), 1.0);
//! ```
#![deny(missing_docs)]
#![allow(clippy::macro_metavars_in_unsafe)]
Expand Down
2 changes: 2 additions & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub use crate::builder::cons::cons;
pub use crate::builder::var::var;
pub use crate::model::Model;
pub use crate::model::ModelWithProblem;
pub use crate::model::ObjSense;
Expand Down

0 comments on commit 38b1869

Please sign in to comment.