From 305ae1126efc5e39dc76136c14dd2197cc7754ab Mon Sep 17 00:00:00 2001 From: radevgit Date: Fri, 26 Sep 2025 19:16:49 +0300 Subject: [PATCH] clean_api --- examples/advanced_runtime_api.rs | 2 +- examples/app_resource_allocation.rs | 8 +- examples/constraint_boolean.rs | 35 +-- examples/constraint_global.rs | 16 +- examples/n_queens.rs | 4 +- src/lib.rs | 50 +++- src/model/core.rs | 361 ++----------------------- src/model/factory.rs | 208 ++++++++++++-- src/model/factory_internal.rs | 228 ++++++++++++++++ src/model/mod.rs | 5 +- src/runtime_api/mod.rs | 2 +- tests/test_core_coverage.rs | 6 +- tests/test_validation_comprehensive.rs | 4 +- tests/test_validation_integration.rs | 2 +- 14 files changed, 522 insertions(+), 409 deletions(-) create mode 100644 src/model/factory_internal.rs diff --git a/examples/advanced_runtime_api.rs b/examples/advanced_runtime_api.rs index ac91f08..138b5ce 100644 --- a/examples/advanced_runtime_api.rs +++ b/examples/advanced_runtime_api.rs @@ -221,7 +221,7 @@ fn example_7_global_constraints() { println!("šŸ“ Example 7: Global Constraint Shortcuts (Phase 2)"); let mut m = Model::default(); - let digits = (0..4).map(|_| m.int(1, 4)).collect::>(); + let digits = m.ints(4, 1, 4); // Ultra-short global constraints m.alldiff(&digits); // All digits must be different diff --git a/examples/app_resource_allocation.rs b/examples/app_resource_allocation.rs index 494c9ca..99ab4b5 100644 --- a/examples/app_resource_allocation.rs +++ b/examples/app_resource_allocation.rs @@ -38,7 +38,7 @@ fn main() { // Assignment variables: worker_assignments[task][worker] = 1 if assigned let mut worker_assignments = Vec::new(); for _ in 0..tasks.len() { - let task_assignments: Vec<_> = m.new_vars_binary(workers.len()).collect(); + let task_assignments = m.bools(workers.len()); worker_assignments.push(task_assignments); } @@ -82,9 +82,9 @@ fn main() { let mut total_time = 0; for (task_idx, task_assignments) in worker_assignments.iter().enumerate() { - let assignments = solution.get_values_binary(task_assignments); - for (worker_idx, &assigned) in assignments.iter().enumerate() { - if assigned { + let assignments = solution.get_values(task_assignments); + for (worker_idx, assigned) in assignments.iter().enumerate() { + if *assigned == Val::ValI(1) { let task_time = match solution[task_completion_times[task_idx]] { Val::ValI(t) => t, _ => 0 diff --git a/examples/constraint_boolean.rs b/examples/constraint_boolean.rs index 21de480..facacfb 100644 --- a/examples/constraint_boolean.rs +++ b/examples/constraint_boolean.rs @@ -8,10 +8,8 @@ fn main() { println!("šŸ“‹ Test 1: Array AND - and([a, b, c, d])"); { let mut m = Model::default(); - let a = m.bool(); - let b = m.bool(); - let c = m.bool(); - let d = m.bool(); + let vars = m.bools(4); + let (a, b, c, d) = (vars[0], vars[1], vars[2], vars[3]); // All must be true (1) for result to be true post!(m, and([a, b, c, d])); @@ -33,10 +31,8 @@ fn main() { println!("\nšŸ“‹ Test 2: Array OR - or([a, b, c, d])"); { let mut m = Model::default(); - let a = m.bool(); - let b = m.bool(); - let c = m.bool(); - let d = m.bool(); + let vars = m.bools(4); + let (a, b, c, d) = (vars[0], vars[1], vars[2], vars[3]); // At least one must be true post!(m, or([a, b, c, d])); @@ -57,10 +53,8 @@ fn main() { println!("\nšŸ“‹ Test 3: Variadic AND - and(a, b, c, d)"); { let mut m = Model::default(); - let a = m.bool(); - let b = m.bool(); - let c = m.bool(); - let d = m.bool(); + let vars = m.bools(4); + let (a, b, c, d) = (vars[0], vars[1], vars[2], vars[3]); post!(m, and(a, b, c, d)); post!(m, a == 1); @@ -81,10 +75,8 @@ fn main() { println!("\nšŸ“‹ Test 4: Variadic OR - or(a, b, c, d)"); { let mut m = Model::default(); - let a = m.bool(); - let b = m.bool(); - let c = m.bool(); - let d = m.bool(); + let vars = m.bools(4); + let (a, b, c, d) = (vars[0], vars[1], vars[2], vars[3]); post!(m, or(a, b, c, d)); post!(m, a == 0); @@ -105,9 +97,8 @@ fn main() { println!("\nšŸ“‹ Test 5: Array NOT - not([a, b, c])"); { let mut m = Model::default(); - let a = m.bool(); - let b = m.bool(); - let c = m.bool(); + let vars = m.bools(3); + let (a, b, c) = (vars[0], vars[1], vars[2]); // This applies not() to each variable individually post!(m, not([a, b, c])); @@ -124,10 +115,8 @@ fn main() { println!("\nšŸ“‹ Test 6: postall! with simple constraints"); { let mut m = Model::default(); - let x = m.bool(); - let y = m.bool(); - let z = m.bool(); - let w = m.bool(); + let vars = m.bools(4); + let (x, y, z, w) = (vars[0], vars[1], vars[2], vars[3]); // Use separate constraints since nested arrays might not work yet post!(m, and([x, y])); // x AND y must be true diff --git a/examples/constraint_global.rs b/examples/constraint_global.rs index 0b0c990..4683390 100644 --- a/examples/constraint_global.rs +++ b/examples/constraint_global.rs @@ -22,7 +22,7 @@ fn main() { println!("šŸ“ Example 1: All Different Constraint"); { let mut model = Model::default(); - let vars: Vec<_> = (0..4).map(|_| model.int(1, 4)).collect(); + let vars = model.ints(4, 1, 4); // All variables must have different values model.alldiff(&vars); @@ -44,7 +44,7 @@ fn main() { println!("šŸ“ Example 2: All Equal Constraint"); { let mut model = Model::default(); - let vars: Vec<_> = (0..3).map(|_| model.int(1, 10)).collect(); + let vars = model.ints(3, 1, 10); // All variables must have the same value model.alleq(&vars); @@ -100,7 +100,7 @@ fn main() { println!("šŸ“ Example 4: Count Constraint"); { let mut model = Model::default(); - let vars: Vec<_> = (0..6).map(|_| model.int(1, 3)).collect(); + let vars = model.ints(6, 1, 3); let count_result = model.int(0, 6); // Count how many variables have value 2 @@ -167,11 +167,11 @@ fn main() { println!("šŸ“ Example 6: Global Cardinality Constraint"); { let mut model = Model::default(); - let vars: Vec<_> = (0..8).map(|_| model.int(1, 4)).collect(); + let vars = model.ints(8, 1, 4); // We want to count 1s, 2s, 3s, and 4s let values = [1, 2, 3, 4]; - let counts: Vec<_> = (0..4).map(|_| model.int(0, 8)).collect(); + let counts = model.ints(4, 0, 8); // Global cardinality constraint model.gcc(&vars, &values, &counts); @@ -223,14 +223,14 @@ fn main() { let mut model = Model::default(); // Create a small scheduling problem with global constraints - let tasks: Vec<_> = (0..4).map(|_| model.int(1, 10)).collect(); // Start times - let resources: Vec<_> = (0..4).map(|_| model.int(1, 3)).collect(); // Resource assignments + let tasks = model.ints(4, 1, 10); // Start times + let resources = model.ints(4, 1, 3); // Resource assignments // All tasks must start at different times (no overlap) model.alldiff(&tasks); // Count resource usage - we have 3 resources, want balanced usage - let resource_counts: Vec<_> = (0..3).map(|_| model.int(0, 4)).collect(); + let resource_counts = model.ints(3, 0, 4); model.gcc(&resources, &[1, 2, 3], &resource_counts); // Each resource should be used at least once diff --git a/examples/n_queens.rs b/examples/n_queens.rs index 855ffdf..f76735e 100644 --- a/examples/n_queens.rs +++ b/examples/n_queens.rs @@ -87,9 +87,7 @@ fn solve_n_queens(n: usize) -> Option<(Vec, SolverStats)> { let mut model = Model::default(); // Variables: queen_row[i] = row position of queen in column i - let queen_rows: Vec<_> = (0..n) - .map(|_| model.int(1, n as i32)) - .collect(); + let queen_rows = model.ints(n, 1, n as i32); // Constraint 1: All queens must be in different rows // This is the most direct AllDifferent constraint diff --git a/src/lib.rs b/src/lib.rs index 662feac..45b8d8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,9 +27,15 @@ //! //! - **Integer variables**: `m.int(min, max)` - continuous range //! - **Float variables**: `m.float(min, max)` - continuous range with precision control -//! - **Custom domains**: `m.ints(vec![values])` - specific integer values only +//! - **Custom domains**: `m.intset(vec![values])` - specific integer values only //! - **Boolean variables**: `m.bool()` - equivalent to `m.int(0, 1)` //! +//! ## Bulk Variable Creation +//! +//! - **Multiple integers**: `m.ints(n, min, max)` - create n integer variables with same bounds +//! - **Multiple floats**: `m.floats(n, min, max)` - create n float variables with same bounds +//! - **Multiple booleans**: `m.bools(n)` - create n boolean variables +//! //! ## Constraint Types //! //! - **Arithmetic**: `+`, `-`, `*`, `/`, `%`, `abs()`, `min()`, `max()`, `sum()` @@ -100,9 +106,9 @@ //! let mut m = Model::default(); //! //! // Variables with custom domains -//! let red = m.ints(vec![1, 3, 5, 7]); // Odd numbers -//! let blue = m.ints(vec![2, 4, 6, 8]); // Even numbers -//! let green = m.ints(vec![2, 3, 5, 7]); // Prime numbers +//! let red = m.intset(vec![1, 3, 5, 7]); // Odd numbers +//! let blue = m.intset(vec![2, 4, 6, 8]); // Even numbers +//! let green = m.intset(vec![2, 3, 5, 7]); // Prime numbers //! //! // All must be different //! post!(m, alldiff([red, blue, green])); @@ -113,7 +119,37 @@ //! } //! ``` //! -//! ## Example 4: Programmatic API - Basic Constraints +//! ## Example 4: Bulk Variable Creation +//! +//! Create multiple variables efficiently with the same domain: +//! +//! ```rust +//! use selen::prelude::*; +//! +//! let mut m = Model::default(); +//! +//! // Create 5 integer variables, each with domain [1, 10] +//! let vars = m.ints(5, 1, 10); +//! +//! // Create 3 boolean variables +//! let flags = m.bools(3); +//! +//! // Create 4 float variables with same bounds +//! let weights = m.floats(4, 0.0, 1.0); +//! +//! // All variables in vars must be different +//! post!(m, alldiff(&vars)); +//! +//! // At least one flag must be true (using slice syntax) +//! post!(m, or([flags[0], flags[1], flags[2]])); +//! +//! if let Ok(solution) = m.solve() { +//! println!("Solution found with {} variables!", +//! vars.len() + flags.len() + weights.len()); +//! } +//! ``` +//! +//! ## Example 5: Programmatic API - Basic Constraints //! //! For developers who prefer explicit, method-based constraint building: //! @@ -134,7 +170,7 @@ //! } //! ``` //! -//! ## Example 5: Programmatic API - Global Constraints +//! ## Example 6: Programmatic API - Global Constraints //! //! ```rust //! use selen::prelude::*; @@ -157,7 +193,7 @@ //! } //! ``` //! -//! ## Example 6: Programmatic API - Complex Operations +//! ## Example 7: Programmatic API - Complex Operations //! //! ```rust //! use selen::prelude::*; diff --git a/src/model/core.rs b/src/model/core.rs index 467bae9..ed2d8b5 100644 --- a/src/model/core.rs +++ b/src/model/core.rs @@ -154,6 +154,26 @@ impl Model { self.memory_limit_exceeded } + /// Set the memory limit exceeded flag (used internally by factory methods) + pub(crate) fn set_memory_limit_exceeded(&mut self) { + self.memory_limit_exceeded = true; + } + + /// Add to estimated memory usage (used internally by factory methods) + pub(crate) fn add_estimated_memory(&mut self, bytes: u64) { + self.estimated_memory_bytes += bytes; + } + + /// Get mutable access to vars (used internally by factory methods) + pub(crate) fn vars_mut(&mut self) -> &mut crate::variables::Vars { + &mut self.vars + } + + /// Get mutable access to props (used internally by factory methods) + pub(crate) fn props_mut(&mut self) -> &mut crate::constraints::props::Propagators { + &mut self.props + } + /// Get the current number of variables in the model /// /// This can be called at any time during model construction to check @@ -198,24 +218,6 @@ impl Model { self.props.count() } - /// Add to estimated memory usage and check limits - fn add_memory_usage(&mut self, bytes: u64) -> Result<(), SolverError> { - self.estimated_memory_bytes += bytes; - - if let Some(limit_mb) = self.config.max_memory_mb { - let limit_bytes = limit_mb * 1024 * 1024; - if self.estimated_memory_bytes > limit_bytes { - self.memory_limit_exceeded = true; - return Err(SolverError::MemoryLimit { - usage_mb: Some(self.estimated_memory_mb() as usize), - limit_mb: Some(limit_mb as usize), - }); - } - } - - Ok(()) - } - /// Get detailed memory breakdown for analysis pub fn memory_breakdown(&self) -> String { format!( @@ -229,51 +231,6 @@ impl Model { self.config.max_memory_mb ) } - - /// Estimate memory usage for a variable with improved accuracy - fn estimate_variable_memory(&self, min: Val, max: Val) -> u64 { - match (min, max) { - (Val::ValI(min_i), Val::ValI(max_i)) => { - // Integer variable: SparseSet structure overhead + domain representation - if min_i > max_i { - // Invalid range - return minimal memory estimate - return 96; // Just base overhead - } - - let domain_size = (max_i - min_i + 1) as u64; - - // Base SparseSet structure overhead (dense/sparse arrays, metadata) - let base_cost = 96; // More realistic estimate including Vec overhead - - let domain_cost = if domain_size > 1000 { - // Large domains use sparse representation - // Two Vec with capacity approximately equal to domain size - let vec_overhead = 24 * 2; // Vec metadata for dense/sparse arrays - let data_cost = domain_size.saturating_mul(4).saturating_mul(2); // Prevent overflow - vec_overhead + data_cost / 8 // Amortized for typical sparsity - } else { - // Small domains use dense representation - let vec_overhead = 24 * 2; - let data_cost = domain_size.saturating_mul(4).saturating_mul(2); // Prevent overflow - vec_overhead + data_cost - }; - - base_cost + domain_cost - } - (Val::ValF(_), Val::ValF(_)) => { - // Float variable: FloatInterval structure - // Contains: min (f64), max (f64), step (f64) = 24 bytes - // Plus wrapper overhead and alignment - let base_cost = 32; // FloatInterval struct - let wrapper_cost = 32; // Var enum wrapper + alignment - base_cost + wrapper_cost - } - _ => { - // Mixed types: treated as float variable - 64 - } - } - } #[doc(hidden)] /// Get access to constraint registry for debugging/analysis @@ -281,289 +238,23 @@ impl Model { self.props.get_constraint_registry() } - #[doc(hidden)] - /// Create a new decision variable, with the provided domain bounds. - /// - /// Both lower and upper bounds are included in the domain. - /// In case `max < min` the bounds will be swapped. - /// We don't want to deal with "unwrap" every time - /// - /// **Note**: This is a low-level method. Use `int()`, `float()`, or `bool()` instead. - /// - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let var = m.new_var(Val::int(1), Val::int(10)); - /// ``` - #[doc(hidden)] - pub fn new_var(&mut self, min: Val, max: Val) -> VarId { - if min < max { - self.new_var_unchecked(min, max) - } else { - self.new_var_unchecked(max, min) - } - } - #[doc(hidden)] - /// Create new decision variables, with the provided domain bounds. - /// - /// All created variables will have the same starting domain bounds. - /// Both lower and upper bounds are included in the domain. - /// In case `max < min` the bounds will be swapped. - /// - /// **Note**: This is a low-level method. Use specific variable creation methods instead. - /// - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let vars: Vec<_> = m.new_vars(3, Val::int(0), Val::int(5)).collect(); - /// ``` - #[doc(hidden)] - pub fn new_vars(&mut self, n: usize, min: Val, max: Val) -> impl Iterator + '_ { - let (actual_min, actual_max) = if min < max { (min, max) } else { (max, min) }; - std::iter::repeat_with(move || self.new_var_unchecked(actual_min, actual_max)).take(n) - } - #[doc(hidden)] - /// Create new integer decision variables, with the provided domain bounds. - /// - /// Both lower and upper bounds are included in the domain. - /// In case `max < min` the bounds will be swapped. - /// - /// # Examples - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let vars: Vec<_> = m.int_vars(5, 0, 9).collect(); - /// ``` - pub fn int_vars( - &mut self, - n: usize, - min: i32, - max: i32, - ) -> impl Iterator + '_ { - self.new_vars(n, Val::ValI(min), Val::ValI(max)) - } - /// Create an integer variable with a custom domain from specific values. - /// - /// Creates a variable that can only take values from the provided list. - /// This is useful for non-contiguous domains, categorical values, or - /// when you need precise control over allowed values. - /// - /// # Arguments - /// * `values` - Vector of integer values that the variable can take - /// - /// # Returns - /// A `VarId` that can only take values from the provided vector - /// - /// # Example - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// - /// // Variable that can only be prime numbers - /// let prime = m.ints(vec![2, 3, 5, 7, 11, 13]); - /// - /// // Variable for days of week (1=Monday, 7=Sunday) - /// let weekday = m.ints(vec![1, 2, 3, 4, 5, 6, 7]); - /// - /// // Non-contiguous range - /// let sparse = m.ints(vec![1, 5, 10, 50, 100]); - /// - /// post!(m, prime != weekday); - /// ``` - pub fn ints(&mut self, values: Vec) -> VarId { - self.props.on_new_var(); - self.vars.new_var_with_values(values) - } - #[doc(hidden)] - /// Create new float decision variables, with the provided domain bounds. - /// - /// Both lower and upper bounds are included in the domain. - /// In case `max < min` the bounds will be swapped. - /// - /// # Examples - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let vars: Vec<_> = m.float_vars(3, 0.0, 1.0).collect(); - /// ``` - pub fn float_vars( - &mut self, - n: usize, - min: f64, - max: f64, - ) -> impl Iterator + '_ { - self.new_vars(n, Val::ValF(min), Val::ValF(max)) - } - /// Create a boolean variable (0 or 1). - /// - /// Creates a variable that can only take values 0 or 1, useful for representing - /// boolean logic, flags, or binary decisions. Equivalent to `m.int(0, 1)` but - /// more semantically clear for boolean use cases. - /// - /// # Returns - /// A `VarId` that can take values 0 (false) or 1 (true) - /// - /// # Example - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let flag = m.bool(); // 0 or 1 - /// let enabled = m.bool(); // 0 or 1 - /// - /// // Use in constraints - /// post!(m, flag != enabled); // Flags must be different - /// - /// // Boolean logic (using model methods) - /// let result = m.bool_and(&[flag, enabled]); // result = flag AND enabled - /// ``` - pub fn bool(&mut self) -> VarId { - self.int(0, 1) - } - #[doc(hidden)] - /// Create a new binary decision variable. - /// - /// - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let var = m.new_var_binary(); - /// ``` - #[doc(hidden)] - pub fn new_var_binary(&mut self) -> VarIdBin { - VarIdBin(self.new_var_unchecked(Val::ValI(0), Val::ValI(1))) - } - #[doc(hidden)] - /// Create new binary decision variables. - /// - /// - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let vars: Vec<_> = m.new_vars_binary(4).collect(); - /// ``` - #[doc(hidden)] - pub fn new_vars_binary(&mut self, n: usize) -> impl Iterator + '_ { - std::iter::repeat_with(|| self.new_var_binary()).take(n) - } - // === SHORT VARIABLE CREATION METHODS === - - /// Create an integer variable with specified bounds. - /// - /// Creates a variable that can take any integer value between `min` and `max` (inclusive). - /// - /// # Arguments - /// * `min` - Minimum value for the variable (inclusive) - /// * `max` - Maximum value for the variable (inclusive) - /// - /// # Note - /// If `min > max`, an invalid variable will be created that will be caught during validation. - /// - /// # Example - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let x = m.int(1, 10); // Variable from 1 to 10 - /// let y = m.int(-5, 15); // Variable from -5 to 15 - /// ``` - pub fn int(&mut self, min: i32, max: i32) -> VarId { - // Create the variable with the bounds as given (don't auto-swap) - // If min > max, this will create an invalid variable that validation will catch - self.new_var_unchecked(Val::ValI(min), Val::ValI(max)) - } - /// Create a floating-point variable with specified bounds. - /// - /// Creates a variable that can take any floating-point value between `min` and `max` (inclusive). - /// The precision is controlled by the model's `float_precision_digits` setting. - /// - /// # Arguments - /// * `min` - Minimum value for the variable (inclusive) - /// * `max` - Maximum value for the variable (inclusive) - /// - /// # Note - /// If `min > max`, an invalid variable will be created that will be caught during validation. - /// - /// # Example - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let x = m.float(0.0, 10.0); // Variable from 0.0 to 10.0 - /// let y = m.float(-1.5, 3.14); // Variable from -1.5 to 3.14 - /// ``` - pub fn float(&mut self, min: f64, max: f64) -> VarId { - // Create the variable with the bounds as given (don't auto-swap) - // If min > max, this will create an invalid variable that validation will catch - self.new_var_unchecked(Val::ValF(min), Val::ValF(max)) - } - /// Create a binary variable (0 or 1). - /// - /// Creates a boolean variable that can only take values 0 or 1. - /// Equivalent to `m.int(0, 1)` but optimized for binary constraints. - /// - /// # Example - /// ``` - /// use selen::prelude::*; - /// let mut m = Model::default(); - /// let flag = m.binary(); // Variable that is 0 or 1 - /// ``` - pub fn binary(&mut self) -> VarIdBin { - self.new_var_binary() - } - #[doc(hidden)] - /// Create a new integer decision variable, with the provided domain bounds. - /// - /// Both lower and upper bounds are included in the domain. - /// - /// This function assumes that `min < max`. - #[doc(hidden)] - pub fn new_var_unchecked(&mut self, min: Val, max: Val) -> VarId { - match self.new_var_checked(min, max) { - Ok(var_id) => var_id, - Err(_error) => { - // Memory limit exceeded during variable creation - // Mark the model as invalid so solve() will return the error gracefully - self.memory_limit_exceeded = true; - - // Return a dummy VarId to keep the API consistent - // The solve() method will detect memory_limit_exceeded and return proper error - VarId::from_index(0) - } - } - } - - /// Create a new variable with memory limit checking - fn new_var_checked(&mut self, min: Val, max: Val) -> Result { - // Check if memory limit was already exceeded - if self.memory_limit_exceeded { - return Err(SolverError::MemoryLimit { - usage_mb: Some(self.estimated_memory_mb() as usize), - limit_mb: self.config.max_memory_mb.map(|x| x as usize), - }); - } - - // Estimate memory needed for this variable - let estimated_memory = self.estimate_variable_memory(min, max); - - // Check if adding this variable would exceed the limit - self.add_memory_usage(estimated_memory)?; - - // Create the variable - self.props.on_new_var(); - let step_size = self.float_step_size(); - let var_id = self.vars.new_var_with_bounds_and_step(min, max, step_size); - - Ok(var_id) - } + + + + + + // ======================================================================== // CONSTRAINT POSTING METHODS diff --git a/src/model/factory.rs b/src/model/factory.rs index 0e9dc6e..4406a5d 100644 --- a/src/model/factory.rs +++ b/src/model/factory.rs @@ -1,25 +1,195 @@ -//! Variable factory methods +//! Public Variable Factory API //! -//! This module contains methods for creating different types of variables. -//! Currently all implementations are in model_core.rs and will be moved here in a future phase. +//! This module provides the clean, user-friendly API for creating variables in constraint models. +//! This is the ONLY API that end users should use for variable creation. +//! +//! ## Single Variable Creation +//! - `int(min, max)` - Create a single integer variable +//! - `float(min, max)` - Create a single floating-point variable +//! - `bool()` - Create a single boolean variable (0 or 1) +//! - `intset(values)` - Create a variable from a set of specific integer values +//! +//! ## Multiple Variable Creation +//! - `ints(n, min, max)` - Create n integer variables with the same bounds +//! - `floats(n, min, max)` - Create n floating-point variables with the same bounds +//! - `bools(n)` - Create n boolean variables use crate::model::core::Model; +use crate::variables::{Val, VarId}; impl Model { - // Note: Variable factory methods are currently implemented in model_core.rs - // They include: - // - new_var(min, max) -> VarId - // - new_vars(n, min, max) -> Iterator - // - int_vars(n, min, max) -> Iterator - // - ints(values) -> VarId - // - float_vars(n, min, max) -> Iterator - // - bool() -> VarId - // - new_var_binary() -> VarIdBin - // - new_vars_binary(n) -> Iterator - // - int(min, max) -> VarId - // - float(min, max) -> VarId - // - binary() -> VarIdBin - // - new_var_unchecked(min, max) -> VarId - // - // These will be moved to this module in a future phase of the modularization. + // ======================================================================== + // SINGLE VARIABLE CREATION - PRIMARY PUBLIC API + // ======================================================================== + + /// Create an integer variable with specified bounds. + /// + /// Creates a variable that can take any integer value between `min` and `max` (inclusive). + /// + /// # Arguments + /// * `min` - Minimum value for the variable (inclusive) + /// * `max` - Maximum value for the variable (inclusive) + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let x = m.int(1, 10); // Variable from 1 to 10 + /// let y = m.int(-5, 15); // Variable from -5 to 15 + /// ``` + pub fn int(&mut self, min: i32, max: i32) -> VarId { + self.new_var_unchecked(Val::ValI(min), Val::ValI(max)) + } + + /// Create a floating-point variable with specified bounds. + /// + /// Creates a variable that can take any floating-point value between `min` and `max` (inclusive). + /// The precision is controlled by the model's `float_precision_digits` setting. + /// + /// # Arguments + /// * `min` - Minimum value for the variable (inclusive) + /// * `max` - Maximum value for the variable (inclusive) + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let x = m.float(0.0, 10.0); // Variable from 0.0 to 10.0 + /// let y = m.float(-1.5, 3.14); // Variable from -1.5 to 3.14 + /// ``` + pub fn float(&mut self, min: f64, max: f64) -> VarId { + self.new_var_unchecked(Val::ValF(min), Val::ValF(max)) + } + + /// Create a boolean variable (0 or 1). + /// + /// Creates a variable that can only take values 0 or 1, useful for representing + /// boolean logic, flags, or binary decisions. + /// + /// # Returns + /// A `VarId` that can take values 0 (false) or 1 (true) + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let flag = m.bool(); // 0 or 1 + /// let enabled = m.bool(); // 0 or 1 + /// + /// // Use in constraints + /// post!(m, flag != enabled); // Flags must be different + /// ``` + pub fn bool(&mut self) -> VarId { + self.int(0, 1) + } + + /// Create an integer variable from a specific set of values. + /// + /// Creates a variable that can only take values from the provided list. + /// This is useful for non-contiguous domains, categorical values, or + /// when you need precise control over allowed values. + /// + /// # Arguments + /// * `values` - Vector of integer values that the variable can take + /// + /// # Returns + /// A `VarId` that can only take values from the provided vector + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// + /// // Variable that can only be prime numbers + /// let prime = m.intset(vec![2, 3, 5, 7, 11, 13]); + /// + /// // Variable for days of week (1=Monday, 7=Sunday) + /// let weekday = m.intset(vec![1, 2, 3, 4, 5, 6, 7]); + /// + /// // Non-contiguous range + /// let sparse = m.intset(vec![1, 5, 10, 50, 100]); + /// + /// post!(m, prime != weekday); + /// ``` + pub fn intset(&mut self, values: Vec) -> VarId { + self.props_mut().on_new_var(); + self.vars_mut().new_var_with_values(values) + } + + // ======================================================================== + // MULTIPLE VARIABLE CREATION - PUBLIC API + // ======================================================================== + + /// Create multiple integer variables with the same bounds. + /// + /// Creates `n` integer variables, each with the same domain bounds. + /// This is more efficient than calling `int()` multiple times. + /// + /// # Arguments + /// * `n` - Number of variables to create + /// * `min` - Minimum value for each variable (inclusive) + /// * `max` - Maximum value for each variable (inclusive) + /// + /// # Returns + /// A `Vec` containing all created variables + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let vars = m.ints(5, 1, 10); // 5 variables, each from 1 to 10 + /// let sudoku_row = m.ints(9, 1, 9); // 9 variables for Sudoku row + /// ``` + pub fn ints(&mut self, n: usize, min: i32, max: i32) -> Vec { + self.int_vars(n, min, max).collect() + } + + /// Create multiple floating-point variables with the same bounds. + /// + /// Creates `n` floating-point variables, each with the same domain bounds. + /// This is more efficient than calling `float()` multiple times. + /// + /// # Arguments + /// * `n` - Number of variables to create + /// * `min` - Minimum value for each variable (inclusive) + /// * `max` - Maximum value for each variable (inclusive) + /// + /// # Returns + /// A `Vec` containing all created variables + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let coords = m.floats(3, 0.0, 1.0); // 3 variables for x, y, z coordinates + /// let weights = m.floats(10, 0.0, 100.0); // 10 weight variables + /// ``` + pub fn floats(&mut self, n: usize, min: f64, max: f64) -> Vec { + self.float_vars(n, min, max).collect() + } + + /// Create multiple boolean variables. + /// + /// Creates `n` boolean variables, each with domain [0, 1]. + /// This is more efficient than calling `bool()` multiple times. + /// + /// # Arguments + /// * `n` - Number of boolean variables to create + /// + /// # Returns + /// A `Vec` containing all created boolean variables + /// + /// # Example + /// ``` + /// use selen::prelude::*; + /// let mut m = Model::default(); + /// let flags = m.bools(8); // 8 boolean flags + /// let choices = m.bools(10); // 10 binary choices + /// + /// // Use in constraints + /// post!(m, flags[0] != flags[1]); // Different flags + /// ``` + pub fn bools(&mut self, n: usize) -> Vec { + self.int_vars(n, 0, 1).collect() + } } \ No newline at end of file diff --git a/src/model/factory_internal.rs b/src/model/factory_internal.rs new file mode 100644 index 0000000..a52bf28 --- /dev/null +++ b/src/model/factory_internal.rs @@ -0,0 +1,228 @@ +//! Internal variable factory methods +//! +//! This module contains low-level variable creation methods that are used internally +//! by the solver and higher-level factory methods. These methods should NOT be used +//! directly by end users - use the methods in `factory.rs` instead. + +use crate::model::core::Model; +use crate::variables::{Val, VarId, VarIdBin}; +use crate::core::error::SolverError; + +impl Model { + // ======================================================================== + // INTERNAL LOW-LEVEL VARIABLE CREATION + // ======================================================================== + + /// Create a new decision variable with the provided domain bounds. + /// + /// Both lower and upper bounds are included in the domain. + /// In case `max < min` the bounds will be swapped. + /// + /// **Note**: This is a low-level internal method. Use `int()`, `float()`, or `bool()` instead. + #[doc(hidden)] + pub fn new_var(&mut self, min: Val, max: Val) -> VarId { + if min < max { + self.new_var_unchecked(min, max) + } else { + self.new_var_unchecked(max, min) + } + } + + /// Create new decision variables, with the provided domain bounds. + /// + /// All created variables will have the same starting domain bounds. + /// Both lower and upper bounds are included in the domain. + /// In case `max < min` the bounds will be swapped. + /// + /// **Note**: This is a low-level internal method. Use specific variable creation methods instead. + #[doc(hidden)] + pub fn new_vars(&mut self, n: usize, min: Val, max: Val) -> impl Iterator + '_ { + let (actual_min, actual_max) = if min < max { (min, max) } else { (max, min) }; + std::iter::repeat_with(move || self.new_var_unchecked(actual_min, actual_max)).take(n) + } + + /// Create a new integer decision variable, with the provided domain bounds. + /// + /// Both lower and upper bounds are included in the domain. + /// This function assumes that `min < max`. + /// + /// **Note**: This is a low-level internal method. + #[doc(hidden)] + pub fn new_var_unchecked(&mut self, min: Val, max: Val) -> VarId { + match self.new_var_checked(min, max) { + Ok(var_id) => var_id, + Err(_error) => { + // Memory limit exceeded during variable creation - set flag via setter + self.set_memory_limit_exceeded(); + + // Return a dummy VarId to keep the API consistent + // The solve() method will detect memory_limit_exceeded and return proper error + VarId::from_index(0) + } + } + } + + /// Create a new variable with memory limit checking + /// + /// **Note**: This is a low-level internal method. + pub(crate) fn new_var_checked(&mut self, min: Val, max: Val) -> Result { + // Check if memory limit was already exceeded + if self.memory_limit_exceeded() { + return Err(SolverError::MemoryLimit { + usage_mb: Some(self.estimated_memory_mb() as usize), + limit_mb: self.config().max_memory_mb.map(|x| x as usize), + }); + } + + // Estimate memory needed for this variable + let estimated_memory = self.estimate_variable_memory(min, max); + + // Check if adding this variable would exceed the limit + self.add_memory_usage(estimated_memory)?; + + // Create the variable + self.props_mut().on_new_var(); + let step_size = self.float_step_size(); + let var_id = self.vars_mut().new_var_with_bounds_and_step(min, max, step_size); + + Ok(var_id) + } + + // ======================================================================== + // INTERNAL MEMORY ESTIMATION HELPERS + // ======================================================================== + + /// Add to estimated memory usage and check limits + /// + /// **Note**: This is an internal helper method. + pub(crate) fn add_memory_usage(&mut self, bytes: u64) -> Result<(), SolverError> { + self.add_estimated_memory(bytes); + + if let Some(limit_mb) = self.config().max_memory_mb { + let limit_bytes = limit_mb * 1024 * 1024; + if self.estimated_memory_bytes() > limit_bytes { + self.set_memory_limit_exceeded(); + return Err(SolverError::MemoryLimit { + usage_mb: Some(self.estimated_memory_mb() as usize), + limit_mb: Some(limit_mb as usize), + }); + } + } + + Ok(()) + } + + /// Estimate memory usage for a variable with improved accuracy + /// + /// **Note**: This is an internal helper method. + pub(crate) fn estimate_variable_memory(&self, min: Val, max: Val) -> u64 { + match (min, max) { + (Val::ValI(min_i), Val::ValI(max_i)) => { + // Integer variable: SparseSet structure overhead + domain representation + if min_i > max_i { + // Invalid range - return minimal memory estimate + return 96; // Just base overhead + } + + let domain_size = (max_i - min_i + 1) as u64; + + // Base SparseSet structure overhead (dense/sparse arrays, metadata) + let base_cost = 96; // More realistic estimate including Vec overhead + + let domain_cost = if domain_size > 1000 { + // Large domains use sparse representation + // Two Vec with capacity approximately equal to domain size + let vec_overhead = 24 * 2; // Vec metadata for dense/sparse arrays + let data_cost = domain_size.saturating_mul(4).saturating_mul(2); // Prevent overflow + vec_overhead + data_cost / 8 // Amortized for typical sparsity + } else { + // Small domains use dense representation + let vec_overhead = 24 * 2; + let data_cost = domain_size.saturating_mul(4).saturating_mul(2); // Prevent overflow + vec_overhead + data_cost + }; + + base_cost + domain_cost + } + (Val::ValF(_), Val::ValF(_)) => { + // Float variable: FloatInterval structure + // Contains: min (f64), max (f64), step (f64) = 24 bytes + // Plus wrapper overhead and alignment + let base_cost = 32; // FloatInterval struct + let wrapper_cost = 32; // Var enum wrapper + alignment + base_cost + wrapper_cost + } + _ => { + // Mixed types: treated as float variable + 64 + } + } + } + + // ======================================================================== + // INTERNAL BATCH VARIABLE CREATION + // ======================================================================== + + /// Create new integer decision variables, with the provided domain bounds. + /// + /// Both lower and upper bounds are included in the domain. + /// In case `max < min` the bounds will be swapped. + /// + /// **Note**: This is an internal method. Use `ints()` for public API. + #[doc(hidden)] + pub fn int_vars( + &mut self, + n: usize, + min: i32, + max: i32, + ) -> impl Iterator + '_ { + self.new_vars(n, Val::ValI(min), Val::ValI(max)) + } + + /// Create new float decision variables, with the provided domain bounds. + /// + /// Both lower and upper bounds are included in the domain. + /// In case `max < min` the bounds will be swapped. + /// + /// **Note**: This is an internal method. Use `floats()` for public API. + #[doc(hidden)] + pub fn float_vars( + &mut self, + n: usize, + min: f64, + max: f64, + ) -> impl Iterator + '_ { + self.new_vars(n, Val::ValF(min), Val::ValF(max)) + } + + // ======================================================================== + // INTERNAL BINARY VARIABLE CREATION + // ======================================================================== + + /// Create a new binary decision variable. + /// + /// **Note**: This is an internal method. Use `bool()` for public API. + #[doc(hidden)] + pub fn new_var_binary(&mut self) -> VarIdBin { + VarIdBin(self.new_var_unchecked(Val::ValI(0), Val::ValI(1))) + } + + /// Create new binary decision variables. + /// + /// **Note**: This is an internal method. Use `bools()` for public API. + #[doc(hidden)] + pub fn new_vars_binary(&mut self, n: usize) -> impl Iterator + '_ { + std::iter::repeat_with(|| self.new_var_binary()).take(n) + } + + /// Create a binary variable (0 or 1) returning VarIdBin. + /// + /// Creates a boolean variable that can only take values 0 or 1. + /// Returns VarIdBin for specialized binary constraints. + /// + /// **Note**: This is an internal method. Use `bool()` for public API. + #[doc(hidden)] + pub fn binary(&mut self) -> VarIdBin { + self.new_var_binary() + } +} \ No newline at end of file diff --git a/src/model/mod.rs b/src/model/mod.rs index b6fd003..b53a06c 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -5,8 +5,9 @@ // Core model functionality (the main Model struct moved from model_core.rs) mod core; -// Organized model functionality -pub mod factory; +// Organized model functionality +pub mod factory; // Public variable factory API +mod factory_internal; // Internal variable creation methods pub mod constraints; pub mod solving; pub mod precision; diff --git a/src/runtime_api/mod.rs b/src/runtime_api/mod.rs index a54133e..155293c 100644 --- a/src/runtime_api/mod.rs +++ b/src/runtime_api/mod.rs @@ -613,7 +613,7 @@ fn post_constraint_kind(model: &mut Model, kind: &ConstraintKind) -> PropId { if let (Val::ValI(left_int), Val::ValI(right_int)) = (left_const, right_const) { // Create a new variable with domain {left_val, right_val} and unify it with the original let domain_vals = vec![*left_int, *right_int]; - let domain_var = model.ints(domain_vals); + let domain_var = model.intset(domain_vals); return model.props.equals(*var_id, domain_var); } } diff --git a/tests/test_core_coverage.rs b/tests/test_core_coverage.rs index f3965c2..16d75b7 100644 --- a/tests/test_core_coverage.rs +++ b/tests/test_core_coverage.rs @@ -406,8 +406,8 @@ mod core_coverage { let mut model = Model::default(); // Test variables with non-contiguous domains - let sparse_var = model.ints(vec![2, 5, 7, 11, 13]); // Prime numbers - let weekday = model.ints(vec![1, 2, 3, 4, 5, 6, 7]); // Days of week + let sparse_var = model.intset(vec![2, 5, 7, 11, 13]); // Prime numbers + let weekday = model.intset(vec![1, 2, 3, 4, 5, 6, 7]); // Days of week post!(model, sparse_var != weekday); @@ -429,7 +429,7 @@ mod core_coverage { let mut model = Model::default(); // Create variable with empty domain - let empty_var = model.ints(vec![]); + let empty_var = model.intset(vec![]); // Add constraint on empty variable post!(model, empty_var >= int(1)); diff --git a/tests/test_validation_comprehensive.rs b/tests/test_validation_comprehensive.rs index b6a3870..ead22dd 100644 --- a/tests/test_validation_comprehensive.rs +++ b/tests/test_validation_comprehensive.rs @@ -11,7 +11,7 @@ use selen::{post}; fn test_validation_empty_domain() { // Create a model with an empty domain let mut model = Model::default(); - let _var = model.ints(vec![]); // Empty domain + let _var = model.intset(vec![]); // Empty domain match model.solve() { Ok(_) => { @@ -109,7 +109,7 @@ fn test_validation_large_domain_handling() { // Create a model with a large domain (but not too extreme to avoid memory issues) let mut model = Model::default(); let large_domain: Vec = (0..50000).collect(); // Large but manageable domain - let _x = model.ints(large_domain); + let _x = model.intset(large_domain); // Large domains should work but might be slow or fail validation match model.solve() { diff --git a/tests/test_validation_integration.rs b/tests/test_validation_integration.rs index 37022df..b49802a 100644 --- a/tests/test_validation_integration.rs +++ b/tests/test_validation_integration.rs @@ -13,7 +13,7 @@ fn main() { // Test 1: Empty domain validation println!("\nTest 1: Empty domain validation"); let mut model1 = Model::default(); - let _empty_var = model1.ints(vec![]); // Empty domain + let _empty_var = model1.intset(vec![]); // Empty domain match model1.solve() { Ok(_) => println!(" āŒ FAILED: Expected validation to catch empty domain"),