From 2baf9c15ef9d856c44065a7d648bbc895c458e24 Mon Sep 17 00:00:00 2001 From: Nathy-bajo Date: Tue, 22 Jul 2025 23:13:35 +0100 Subject: [PATCH 1/2] add expression parsing and non-linear constraint support --- src/model.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/scip.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/model.rs b/src/model.rs index 0db748c..7502681 100644 --- a/src/model.rs +++ b/src/model.rs @@ -6,7 +6,7 @@ use crate::node::Node; use crate::param::ScipParameter; use crate::probing::Prober; use crate::retcode::Retcode; -use crate::scip::ScipPtr; +use crate::scip::{Expr, ScipPtr}; use crate::solution::{SolError, Solution}; use crate::status::Status; use crate::variable::{VarId, VarType, Variable}; @@ -380,6 +380,40 @@ impl Model { state: Solved {}, } } + + /// Parses an expression from a string and returns an Expr object + pub fn parse_expr(&self, expr_str: &str) -> Result { + self.scip.parse_expr(expr_str) + } + + /// Creates a constraint from a parsed expression + pub fn add_cons_expr( + &mut self, + expr: Expr, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result { + let cons = self + .scip + .create_cons_basic_nonlinear(expr.raw, lhs, rhs, name)?; + Ok(Constraint { + raw: cons, + scip: self.scip.clone(), + }) + } + + /// Helper method to parse and add a constraint from an expression string + pub fn add_cons_from_expr_str( + &mut self, + expr_str: &str, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result { + let expr = self.parse_expr(expr_str)?; + self.add_cons_expr(expr, lhs, rhs, name) + } } impl Model { @@ -662,6 +696,40 @@ impl Model { lb )); } + + /// Parses an expression from a string and returns an Expr object + pub fn parse_expr(&self, expr_str: &str) -> Result { + self.scip.parse_expr(expr_str) + } + + /// Creates a constraint from a parsed expression + pub fn add_cons_expr( + &mut self, + expr: Expr, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result { + let cons = self + .scip + .create_cons_basic_nonlinear(expr.raw, lhs, rhs, name)?; + Ok(Constraint { + raw: cons, + scip: self.scip.clone(), + }) + } + + /// Helper method to parse and add a constraint from an expression string + pub fn add_cons_from_expr_str( + &mut self, + expr_str: &str, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result { + let expr = self.parse_expr(expr_str)?; + self.add_cons_expr(expr, lhs, rhs, name) + } } impl Model { diff --git a/src/scip.rs b/src/scip.rs index fdb7f98..dfd71a6 100644 --- a/src/scip.rs +++ b/src/scip.rs @@ -18,10 +18,24 @@ use scip_sys::{ use std::collections::BTreeMap; use std::ffi::{CStr, CString, c_int}; use std::mem::MaybeUninit; +use std::ptr; use std::rc::Rc; use crate::builder::row::{RowBuilder, RowSource}; +/// Represents a parsed expression +#[derive(Debug)] +pub struct Expr { + pub(crate) raw: *mut ffi::SCIP_EXPR, +} + +impl Expr { + /// Creates a new expression from a raw pointer + pub fn from_raw(raw: *mut ffi::SCIP_EXPR) -> Self { + Self { raw } + } +} + #[non_exhaustive] #[derive(Debug)] pub struct ScipPtr { @@ -1699,6 +1713,49 @@ impl ScipPtr { map.insert(thing); Ok(()) } + + /// Parses an expression from a string + pub(crate) fn parse_expr(&self, expr_str: &str) -> Result { + let c_expr = CString::new(expr_str).expect("Failed to convert string to CString"); + let mut scip_expr = MaybeUninit::uninit(); + let mut final_pos = ptr::null(); + + scip_call! { ffi::SCIPparseExpr( + self.raw, + scip_expr.as_mut_ptr(), + c_expr.as_ptr(), + &mut final_pos, + None, + std::ptr::null_mut(), + ) }; + + let scip_expr = unsafe { scip_expr.assume_init() }; + Ok(Expr { raw: scip_expr }) + } + + /// Creates a basic nonlinear constraint from an expression + pub(crate) fn create_cons_basic_nonlinear( + &self, + expr: *mut ffi::SCIP_EXPR, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result<*mut SCIP_Cons, Retcode> { + let c_name = CString::new(name).unwrap(); + let mut scip_cons = MaybeUninit::uninit(); + + scip_call! { ffi::SCIPcreateConsBasicNonlinear( + self.raw, + scip_cons.as_mut_ptr(), + c_name.as_ptr(), + expr, + lhs, + rhs, + ) }; + + let scip_cons = unsafe { scip_cons.assume_init() }; + Ok(scip_cons) + } } impl Drop for ScipPtr { From 26d75445cf996d65026c4f06bd66aabcf87115e4 Mon Sep 17 00:00:00 2001 From: Nathy-bajo Date: Mon, 28 Jul 2025 15:17:50 +0100 Subject: [PATCH 2/2] add tests --- src/model.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/scip.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 240 insertions(+), 3 deletions(-) diff --git a/src/model.rs b/src/model.rs index 7502681..e59c416 100644 --- a/src/model.rs +++ b/src/model.rs @@ -414,6 +414,30 @@ impl Model { let expr = self.parse_expr(expr_str)?; self.add_cons_expr(expr, lhs, rhs, name) } + + /// Creates a variable expression + pub fn create_var_expr(&self, var: &Variable) -> Result { + self.scip.create_expr_var(var.raw) + } + + /// Creates a power expression from a variable (var^exponent) + pub fn create_pow_expr(&self, var: &Variable, exponent: f64) -> Result { + let var_expr = self.create_var_expr(var)?; + var_expr.pow(exponent, &self.scip) + } + + /// Adds a power expression constraint (var^exponent) + pub fn add_pow_cons( + &mut self, + var: &Variable, + exponent: f64, + lhs: f64, + rhs: f64, + name: &str, + ) -> Result { + let expr = self.create_pow_expr(var, exponent)?; + self.add_cons_expr(expr, lhs, rhs, name) + } } impl Model { @@ -730,6 +754,17 @@ impl Model { let expr = self.parse_expr(expr_str)?; self.add_cons_expr(expr, lhs, rhs, name) } + + /// Creates a variable expression + pub fn create_var_expr(&self, var: &Variable) -> Result { + self.scip.create_expr_var(var.raw) + } + + /// Creates a power expression from a variable (var^exponent) + pub fn create_pow_expr(&self, var: &Variable, exponent: f64) -> Result { + let var_expr = self.create_var_expr(var)?; + var_expr.pow(exponent, &self.scip) + } } impl Model { @@ -2462,4 +2497,91 @@ mod tests { assert_eq!(solved_model.n_sols(), sols.len()); assert!(1 >= sols.len()); } + + #[test] + fn test_pow_expr() { + let mut model = Model::new() + .hide_output() + .include_default_plugins() + .create_prob("test_pow") + .set_obj_sense(ObjSense::Maximize); + + // Add variables + let x = model.add_var(0.0, 10.0, 1.0, "x", VarType::Continuous); + + // Create x^2 expression and constraint x^2 <= 25 + let _cons = model + .add_pow_cons(&x, 2.0, -f64::INFINITY, 25.0, "x_squared") + .expect("Failed to create power constraint"); + + let solved = model.solve(); + assert_eq!(solved.status(), Status::Optimal); + + let sol = solved.best_sol().unwrap(); + assert!((sol.val(&x) - 5.0).abs() < 1e-6); // x should be 5 (maximizing x with x^2 <= 25) + } + + #[test] + fn test_parsed_expr() { + let mut model = Model::new() + .hide_output() + .include_default_plugins() + .create_prob("test_parsed_expr") + .set_obj_sense(ObjSense::Maximize); + + // Add variables + let x = model.add_var(0.0, 10.0, 1.0, "x", VarType::Continuous); + let y = model.add_var(0.0, 10.0, 0.0, "y", VarType::Continuous); + + // Parse and add constraint "x^2 + y^2 <= 25" + let expr = model.parse_expr("x^2 + y^2").unwrap(); + model + .add_cons_expr(expr, -f64::INFINITY, 25.0, "circle") + .expect("Failed to add parsed expression constraint"); + + let solved = model.solve(); + assert_eq!(solved.status(), Status::Optimal); + + let sol = solved.best_sol().unwrap(); + // Should maximize x with x^2 + y^2 <= 25 + assert!((sol.val(&x) - 5.0).abs() < 1e-6); + assert!(sol.val(&y).abs() < 1e-6); + } + + #[test] + fn test_complex_expr() { + let mut model = Model::new() + .hide_output() + .include_default_plugins() + .create_prob("test_complex_expr") + .set_obj_sense(ObjSense::Maximize); + + // Add variables + let x = model.add_var(0.0, 10.0, 1.0, "x", VarType::Continuous); + let y = model.add_var(0.0, 10.0, 0.0, "y", VarType::Continuous); + + // Create expressions + let x_expr = model.create_var_expr(&x).unwrap(); + let y_expr = model.create_var_expr(&y).unwrap(); + + // Create x^2 and y^2 + let x_sq = x_expr.pow(2.0, &model.scip).unwrap(); + let y_sq = y_expr.pow(2.0, &model.scip).unwrap(); + + // Create sum x^2 + y^2 + let sum = Expr::sum(&[x_sq, y_sq], &mut [1.0, 1.0], &model.scip).unwrap(); + + // Add constraint x^2 + y^2 <= 25 + model + .add_cons_expr(sum, -f64::INFINITY, 25.0, "circle") + .expect("Failed to add complex expression constraint"); + + let solved = model.solve(); + assert_eq!(solved.status(), Status::Optimal); + + let sol = solved.best_sol().unwrap(); + // Should maximize x with x^2 + y^2 <= 25 + assert!((sol.val(&x) - 5.0).abs() < 1e-6); + assert!(sol.val(&y).abs() < 1e-6); + } } diff --git a/src/scip.rs b/src/scip.rs index dfd71a6..51f15c4 100644 --- a/src/scip.rs +++ b/src/scip.rs @@ -34,6 +34,33 @@ impl Expr { pub fn from_raw(raw: *mut ffi::SCIP_EXPR) -> Self { Self { raw } } + + /// Creates a power expression (base^exponent) + pub fn pow(self, exponent: f64, scip: &ScipPtr) -> Result { + scip.create_expr_pow(self.raw, exponent) + } + + /// Creates a sum expression + pub fn sum(exprs: &[Expr], coefficients: &mut [f64], scip: &ScipPtr) -> Result { + let raw_exprs: Vec<_> = exprs.iter().map(|e| e.raw).collect(); + scip.create_expr_sum(&raw_exprs, coefficients) + } + + /// Creates a product expression + pub fn product(exprs: &[Expr], scip: &ScipPtr) -> Result { + let raw_exprs: Vec<_> = exprs.iter().map(|e| e.raw).collect(); + scip.create_expr_product(&raw_exprs) + } +} + +impl Drop for Expr { + fn drop(&mut self) { + unsafe { + if !self.raw.is_null() { + ffi::SCIPreleaseExpr(std::ptr::null_mut(), &mut self.raw); + } + } + } } #[non_exhaustive] @@ -1054,7 +1081,9 @@ impl ScipPtr { result: *mut ffi::SCIP_RESULT, ) -> ffi::SCIP_RETCODE { let data_ptr = unsafe { ffi::SCIPheurGetData(heur) }; - assert!(!data_ptr.is_null()); + if data_ptr.is_null() { + return Retcode::Error.into(); + } let rule_ptr = data_ptr as *mut Box; let current_n_sols = unsafe { ffi::SCIPgetNSols(scip) }; @@ -1087,8 +1116,9 @@ impl ScipPtr { heur: *mut ffi::SCIP_HEUR, ) -> ffi::SCIP_Retcode { let data_ptr = unsafe { ffi::SCIPheurGetData(heur) }; - assert!(!data_ptr.is_null()); - drop(unsafe { Box::from_raw(data_ptr as *mut Box) }); + if !data_ptr.is_null() { + drop(unsafe { Box::from_raw(data_ptr as *mut Box) }); + } Retcode::Okay.into() } @@ -1756,6 +1786,91 @@ impl ScipPtr { let scip_cons = unsafe { scip_cons.assume_init() }; Ok(scip_cons) } + + /// Creates a power expression (base^exponent) + pub(crate) fn create_expr_pow( + &self, + base: *mut ffi::SCIP_EXPR, + exponent: f64, + ) -> Result { + let mut expr_ptr = MaybeUninit::uninit(); + scip_call! { ffi::SCIPcreateExprPow( + self.raw, + expr_ptr.as_mut_ptr(), + base, + exponent, + None, + std::ptr::null_mut(), + ) }; + + let expr_ptr = unsafe { expr_ptr.assume_init() }; + Ok(Expr { raw: expr_ptr }) + } + + /// Creates a variable expression + pub(crate) fn create_expr_var(&self, var: *mut ffi::SCIP_VAR) -> Result { + let mut expr_ptr = MaybeUninit::uninit(); + scip_call! { ffi::SCIPcreateExprVar( + self.raw, + expr_ptr.as_mut_ptr(), + var, + None, + std::ptr::null_mut(), + ) }; + + let expr_ptr = unsafe { expr_ptr.assume_init() }; + Ok(Expr { raw: expr_ptr }) + } + + /// Creates a sum expression + pub(crate) fn create_expr_sum( + &self, + exprs: &[*mut ffi::SCIP_EXPR], + coefficients: &mut [f64], + ) -> Result { + assert_eq!(exprs.len(), coefficients.len()); + let mut expr_ptr = MaybeUninit::uninit(); + + // Convert exprs to a mutable pointer array + let mut expr_ptrs: Vec<_> = exprs.iter().map(|&e| e).collect(); + + scip_call! { ffi::SCIPcreateExprSum( + self.raw, + expr_ptr.as_mut_ptr(), + exprs.len() as i32, + expr_ptrs.as_mut_ptr(), + coefficients.as_mut_ptr(), + 0.0, + None, + std::ptr::null_mut(), + ) }; + + let expr_ptr = unsafe { expr_ptr.assume_init() }; + Ok(Expr { raw: expr_ptr }) + } + + pub(crate) fn create_expr_product( + &self, + exprs: &[*mut ffi::SCIP_EXPR], + ) -> Result { + let mut expr_ptr = MaybeUninit::uninit(); + + // Convert exprs to a mutable pointer array + let mut expr_ptrs: Vec<_> = exprs.iter().map(|&e| e).collect(); + + scip_call! { ffi::SCIPcreateExprProduct( + self.raw, + expr_ptr.as_mut_ptr(), + exprs.len() as i32, + expr_ptrs.as_mut_ptr(), + 1.0, + None, + std::ptr::null_mut(), + ) }; + + let expr_ptr = unsafe { expr_ptr.assume_init() }; + Ok(Expr { raw: expr_ptr }) + } } impl Drop for ScipPtr {