selen 0.15.5

Constraint Satisfaction Problem (CSP) solver
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
//! Direct analytical optimization for pure float problems
//!
//! This module implements O(1) analytical solutions for floating-point optimization
//! problems with bounds constraints. Instead of using binary search enumeration,
//! we compute optimal solutions directly using mathematical analysis.
//!
//! # Key Insight
//! 
//! For pure float optimization problems like:
//! - maximize x subject to x ∈ [1.0, 10.0]
//! - minimize y subject to y ≥ 2.0, y ≤ 8.0
//!
//! The optimal solution is simply the appropriate boundary value:
//! - maximize x ∈ [a, b] → x* = b  
//! - minimize x ∈ [a, b] → x* = a
//!
//! This avoids the exponential step enumeration that causes hanging in high precision.

use crate::variables::{Vars, VarId};
use crate::variables::domain::FloatInterval;

/// Types of optimization outcomes for float bounds optimization
#[derive(Debug, Clone, PartialEq)]
pub enum OptimizationOutcome {
    /// Successfully found optimal value
    Success { 
        /// The operation performed
        operation: OptimizationOperation,
        /// The variable that was optimized
        variable_id: VarId,
    },
    /// Failed due to variable-related issues
    VariableError(VariableError),
    /// Failed due to domain issues
    DomainError(DomainError),
}

/// Types of optimization operations
#[derive(Debug, Clone, PartialEq)]
pub enum OptimizationOperation {
    /// Maximization operation
    Maximization,
    /// Minimization operation
    Minimization,
}

/// Variable-related optimization errors
#[derive(Debug, Clone, PartialEq)]
pub enum VariableError {
    /// Variable is not a float type
    NotFloatVariable,
    /// Variable ID is invalid
    InvalidVariable,
}

/// Domain-related optimization errors
#[derive(Debug, Clone, PartialEq)]
pub enum DomainError {
    /// Variable has empty domain
    EmptyDomain,
    /// Domain bounds are invalid
    InvalidBounds,
}

/// Core analytical optimizer for pure float problems
#[derive(Debug)]
pub struct FloatBoundsOptimizer;

/// Result of direct float optimization
#[derive(Debug, Clone, PartialEq)]
pub struct OptimizationResult {
    /// The optimal value found (NaN if failed)
    pub optimal_value: f64,
    /// Whether the optimization was successful
    pub success: bool,
    /// Structured outcome information
    pub outcome: OptimizationOutcome,
}

impl OptimizationResult {
    /// Create a successful optimization result
    pub fn success(value: f64, operation: OptimizationOperation, variable_id: VarId) -> Self {
        Self {
            optimal_value: value,
            success: true,
            outcome: OptimizationOutcome::Success { operation, variable_id },
        }
    }

    /// Create a failed optimization result due to variable error
    pub fn variable_error(error: VariableError) -> Self {
        Self {
            optimal_value: f64::NAN,
            success: false,
            outcome: OptimizationOutcome::VariableError(error),
        }
    }

    /// Create a failed optimization result due to domain error
    pub fn domain_error(error: DomainError) -> Self {
        Self {
            optimal_value: f64::NAN,
            success: false,
            outcome: OptimizationOutcome::DomainError(error),
        }
    }

    /// Get human-readable description (for debugging/logging only)
    pub fn description(&self) -> String {
        match &self.outcome {
            OptimizationOutcome::Success { operation, variable_id } => {
                let op_str = match operation {
                    OptimizationOperation::Maximization => "Maximized",
                    OptimizationOperation::Minimization => "Minimized",
                };
                format!("{} {} to {}", op_str, var_id_to_string(*variable_id), self.optimal_value)
            },
            OptimizationOutcome::VariableError(error) => {
                match error {
                    VariableError::NotFloatVariable => "Variable is not a float variable".to_string(),
                    VariableError::InvalidVariable => "Invalid variable ID".to_string(),
                }
            },
            OptimizationOutcome::DomainError(error) => {
                match error {
                    DomainError::EmptyDomain => "Cannot optimize: variable has empty domain".to_string(),
                    DomainError::InvalidBounds => "Cannot optimize: invalid domain bounds".to_string(),
                }
            },
        }
    }
}

impl FloatBoundsOptimizer {
    /// Create a new float bounds optimizer
    pub fn new() -> Self {
        Self
    }

    /// Directly maximize a float variable subject to its bounds
    /// 
    /// This is the core O(1) optimization operation. For a variable with
    /// bounds [min, max], the maximum value is simply max (or max - ε
    /// if the bound is exclusive).
    ///
    /// # Arguments
    /// * `vars` - Variable collection containing the target variable
    /// * `var_id` - ID of the variable to maximize
    ///
    /// # Returns
    /// An `OptimizationResult` with the optimal value or failure reason
    pub fn maximize_variable(
        &self, 
        vars: &Vars, 
        var_id: VarId
    ) -> OptimizationResult {
        match self.get_float_bounds(vars, var_id) {
            Some(interval) => {
                if interval.is_empty() {
                    return OptimizationResult::domain_error(DomainError::EmptyDomain);
                }

                // For maximization, we want the largest possible value
                let optimal_value = interval.max;
                
                OptimizationResult::success(
                    optimal_value,
                    OptimizationOperation::Maximization,
                    var_id
                )
            },
            None => OptimizationResult::variable_error(VariableError::NotFloatVariable),
        }
    }

    /// Directly minimize a float variable subject to its bounds
    /// 
    /// For a variable with bounds [min, max], the minimum value is simply min.
    ///
    /// # Arguments
    /// * `vars` - Variable collection containing the target variable
    /// * `var_id` - ID of the variable to minimize
    ///
    /// # Returns
    /// An `OptimizationResult` with the optimal value or failure reason
    pub fn minimize_variable(
        &self,
        vars: &Vars,
        var_id: VarId
    ) -> OptimizationResult {
        match self.get_float_bounds(vars, var_id) {
            Some(interval) => {
                if interval.is_empty() {
                    return OptimizationResult::domain_error(DomainError::EmptyDomain);
                }

                // For minimization, we want the smallest possible value
                let optimal_value = interval.min;
                
                OptimizationResult::success(
                    optimal_value,
                    OptimizationOperation::Minimization,
                    var_id
                )
            },
            None => OptimizationResult::variable_error(VariableError::NotFloatVariable),
        }
    }

    /// Check if a variable can be optimized using direct float optimization
    ///
    /// This returns true if:
    /// 1. The variable is a float variable (not integer)
    /// 2. The variable has a non-empty domain
    pub fn can_optimize(&self, vars: &Vars, var_id: VarId) -> bool {
        match self.get_float_bounds(vars, var_id) {
            Some(interval) => !interval.is_empty(),
            None => false,
        }
    }

    /// Get the current bounds of a float variable
    ///
    /// Returns None if the variable is not a float variable.
    fn get_float_bounds<'a>(&self, vars: &'a Vars, var_id: VarId) -> Option<&'a FloatInterval> {
        // Access the variable through the Vars indexing
        match &vars[var_id] {
            crate::variables::Var::VarF(interval) => Some(interval),
            crate::variables::Var::VarI(_) => None,
        }
    }

    /// Apply the optimization result by updating the variable domain
    ///
    /// This sets the variable to the single optimal value, effectively
    /// "assigning" it to the solution.
    pub fn apply_result(
        &self,
        vars: &mut Vars,
        var_id: VarId,
        result: &OptimizationResult
    ) -> Result<(), DomainError> {
        if !result.success {
            return Err(DomainError::InvalidBounds);
        }

        // Update the variable domain to the single optimal value
        match &mut vars[var_id] {
            crate::variables::Var::VarF(interval) => {
                // Create a new interval containing only the optimal value
                // We use a small epsilon to create a tiny interval around the optimal point
                let optimal = result.optimal_value;
                let step = interval.step;
                
                // Set the interval to contain just the optimal value
                // This is mathematically equivalent to "assigning" the variable
                *interval = FloatInterval::with_step(optimal, optimal, step);
                
                Ok(())
            },
            crate::variables::Var::VarI(_) => {
                Err(DomainError::InvalidBounds)
            }
        }
    }

    /// Optimize and apply in one operation (convenience method)
    ///
    /// This is equivalent to calling maximize_variable() followed by apply_result(),
    /// but handles the error cases more cleanly.
    pub fn maximize_and_apply(
        &self,
        vars: &mut Vars,
        var_id: VarId
    ) -> OptimizationResult {
        let result = self.maximize_variable(vars, var_id);
        
        if result.success {
            match self.apply_result(vars, var_id, &result) {
                Ok(()) => result,
                Err(error) => {
                    OptimizationResult::domain_error(error)
                },
            }
        } else {
            result
        }
    }

    /// Minimize and apply in one operation (convenience method)
    pub fn minimize_and_apply(
        &self,
        vars: &mut Vars,
        var_id: VarId
    ) -> OptimizationResult {
        let result = self.minimize_variable(vars, var_id);
        
        if result.success {
            match self.apply_result(vars, var_id, &result) {
                Ok(()) => result,
                Err(error) => {
                    OptimizationResult::domain_error(error)
                },
            }
        } else {
            result
        }
    }
}

impl Default for FloatBoundsOptimizer {
    fn default() -> Self {
        Self::new()
    }
}

/// Helper function to convert VarId to string for error messages
fn var_id_to_string(var_id: VarId) -> String {
    format!("VarId({:?})", var_id)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::variables::Vars;

    fn create_test_vars_with_float(min: f64, max: f64) -> (Vars, VarId) {
        let mut vars = Vars::new();
        let var_id = vars.new_var_with_bounds(
            crate::variables::Val::float(min), 
            crate::variables::Val::float(max)
        );
        (vars, var_id)
    }

    #[test]
    fn test_maximize_simple_bounds() {
        let optimizer = FloatBoundsOptimizer::new();
        let (vars, var_id) = create_test_vars_with_float(1.0, 10.0);

        let result = optimizer.maximize_variable(&vars, var_id);

        assert!(result.success, "Optimization should succeed");
        assert_eq!(result.optimal_value, 10.0, "Should maximize to upper bound");
        
        // Check that the outcome is a successful maximization
        match result.outcome {
            OptimizationOutcome::Success { operation, .. } => {
                assert_eq!(operation, OptimizationOperation::Maximization);
            },
            _ => assert!(false, "Expected successful maximization outcome"),
        }
    }

    #[test]
    fn test_minimize_simple_bounds() {
        let optimizer = FloatBoundsOptimizer::new();
        let (vars, var_id) = create_test_vars_with_float(2.5, 8.7);

        let result = optimizer.minimize_variable(&vars, var_id);

        assert!(result.success, "Optimization should succeed");
        assert_eq!(result.optimal_value, 2.5, "Should minimize to lower bound");
        
        // Check that the outcome is a successful minimization
        match result.outcome {
            OptimizationOutcome::Success { operation, .. } => {
                assert_eq!(operation, OptimizationOperation::Minimization);
            },
            _ => assert!(false, "Expected successful minimization outcome"),
        }
    }

    #[test]
    fn test_single_point_domain() {
        let optimizer = FloatBoundsOptimizer::new();
        let (vars, var_id) = create_test_vars_with_float(5.0, 5.0);

        let max_result = optimizer.maximize_variable(&vars, var_id);
        let min_result = optimizer.minimize_variable(&vars, var_id);

        assert!(max_result.success);
        assert!(min_result.success);
        assert_eq!(max_result.optimal_value, 5.0);
        assert_eq!(min_result.optimal_value, 5.0);
    }

    #[test]
    fn test_can_optimize_float_variable() {
        let optimizer = FloatBoundsOptimizer::new();
        let (vars, var_id) = create_test_vars_with_float(1.0, 10.0);

        assert!(optimizer.can_optimize(&vars, var_id), "Should be able to optimize float variable");
    }

    #[test]
    fn test_cannot_optimize_integer_variable() {
        let optimizer = FloatBoundsOptimizer::new();
        let mut vars = Vars::new();
        let int_var_id = vars.new_var_with_bounds(
            crate::variables::Val::int(1), 
            crate::variables::Val::int(10)
        );

        assert!(!optimizer.can_optimize(&vars, int_var_id), "Should not be able to optimize integer variable");
        
        let result = optimizer.maximize_variable(&vars, int_var_id);
        assert!(!result.success, "Should fail on integer variable");
        
        // Check that the outcome is a variable error
        match result.outcome {
            OptimizationOutcome::VariableError(VariableError::NotFloatVariable) => {
                // This is expected
            },
            _ => assert!(false, "Expected NotFloatVariable error"),
        }
    }

    #[test]
    fn test_apply_optimization_result() {
        let optimizer = FloatBoundsOptimizer::new();
        let (mut vars, var_id) = create_test_vars_with_float(1.0, 10.0);

        let result = optimizer.maximize_variable(&vars, var_id);
        assert!(result.success);

        let apply_result = optimizer.apply_result(&mut vars, var_id, &result);
        assert!(apply_result.is_ok(), "Should successfully apply optimization result");

        // Verify the variable is now set to the optimal value
        let final_result = optimizer.maximize_variable(&vars, var_id);
        assert!(final_result.success);
        assert_eq!(final_result.optimal_value, 10.0);
    }

    #[test]
    fn test_maximize_and_apply_convenience() {
        let optimizer = FloatBoundsOptimizer::new();
        let (mut vars, var_id) = create_test_vars_with_float(3.0, 7.0);

        let result = optimizer.maximize_and_apply(&mut vars, var_id);

        assert!(result.success, "Maximize and apply should succeed");
        assert_eq!(result.optimal_value, 7.0, "Should find correct maximum");

        // Verify the variable domain was updated
        if let crate::variables::Var::VarF(interval) = &vars[var_id] {
            assert_eq!(interval.min, 7.0);
            assert_eq!(interval.max, 7.0);
        } else {
            assert!(false, "Variable should still be float");
        }
    }

    #[test]
    fn test_minimize_and_apply_convenience() {
        let optimizer = FloatBoundsOptimizer::new();
        let (mut vars, var_id) = create_test_vars_with_float(1.5, 9.5);

        let result = optimizer.minimize_and_apply(&mut vars, var_id);

        assert!(result.success, "Minimize and apply should succeed");
        assert_eq!(result.optimal_value, 1.5, "Should find correct minimum");

        // Verify the variable domain was updated
        if let crate::variables::Var::VarF(interval) = &vars[var_id] {
            assert_eq!(interval.min, 1.5);
            assert_eq!(interval.max, 1.5);
        } else {
            assert!(false, "Variable should still be float");
        }
    }

    #[test]
    fn test_precision_handling() {
        // Test that the optimizer works with different precision levels
        let optimizer = FloatBoundsOptimizer::new();
        
        // High precision case that would cause hanging with binary search
        let (vars, var_id) = create_test_vars_with_float(0.000001, 1.000000);

        let result = optimizer.maximize_variable(&vars, var_id);

        assert!(result.success, "Should handle high precision without hanging");
        assert_eq!(result.optimal_value, 1.000000, "Should find correct maximum");
        
        // This should complete instantly, not hang like the binary search approach
        let start = std::time::Instant::now();
        let _repeated_result = optimizer.maximize_variable(&vars, var_id);
        let duration = start.elapsed();
        
        assert!(duration.as_millis() < 10, "Should complete in well under 10ms");
    }
}