use crate::variables::{Vars, VarId};
use crate::constraints::props::Propagators;
use crate::optimization::float_direct::{FloatBoundsOptimizer, OptimizationResult, OptimizationOperation, VariableError, DomainError};
use crate::variables::domain::FloatInterval;
#[derive(Debug)]
pub struct ConstraintAwareOptimizer {
base_optimizer: FloatBoundsOptimizer,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BoundsDerivation {
OriginalDomain,
LinearEquality { target_value: f64 },
LinearInequality {
lower_constraint: Option<f64>,
upper_constraint: Option<f64>
},
CombinedConstraints {
constraint_count: usize
},
Infeasible {
conflict_type: ConflictType
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConflictType {
EmptyDomain,
NonFloatVariable,
ConflictingEqualities { value1: f64, value2: f64 },
ConflictingInequalities { upper_bound: f64, lower_bound: f64 },
}
impl BoundsDerivation {
pub fn to_description(&self) -> String {
match self {
BoundsDerivation::OriginalDomain => "original variable bounds".to_string(),
BoundsDerivation::LinearEquality { target_value } =>
format!("equality constraint x = {}", target_value),
BoundsDerivation::LinearInequality { lower_constraint, upper_constraint } => {
match (lower_constraint, upper_constraint) {
(Some(min), Some(max)) => format!("inequality constraints {} <= x <= {}", min, max),
(Some(min), None) => format!("lower bound constraint x >= {}", min),
(None, Some(max)) => format!("upper bound constraint x <= {}", max),
(None, None) => "no active inequality constraints".to_string(),
}
},
BoundsDerivation::CombinedConstraints { constraint_count } =>
format!("combination of {} constraints", constraint_count),
BoundsDerivation::Infeasible { conflict_type } => {
match conflict_type {
ConflictType::EmptyDomain => "Infeasible: Variable has empty domain".to_string(),
ConflictType::NonFloatVariable => "Infeasible: Variable is not a float variable".to_string(),
ConflictType::ConflictingEqualities { value1, value2 } =>
format!("Infeasible: Conflicting equalities x = {} and x = {}", value1, value2),
ConflictType::ConflictingInequalities { upper_bound, lower_bound } =>
format!("Infeasible: Conflicting inequalities x <= {} and x >= {}", upper_bound, lower_bound),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConstrainedBounds {
pub min: f64,
pub max: f64,
pub is_feasible: bool,
pub derivation: BoundsDerivation,
}
impl ConstrainedBounds {
pub fn new(min: f64, max: f64, derivation: BoundsDerivation) -> Self {
Self {
min,
max,
is_feasible: min <= max,
derivation,
}
}
pub fn infeasible(conflict_type: ConflictType) -> Self {
Self {
min: f64::INFINITY,
max: f64::NEG_INFINITY,
is_feasible: false,
derivation: BoundsDerivation::Infeasible { conflict_type },
}
}
}
impl ConstraintAwareOptimizer {
pub fn new() -> Self {
Self {
base_optimizer: FloatBoundsOptimizer::new(),
}
}
pub fn maximize_with_constraints(
&self,
vars: &Vars,
props: &Propagators,
var_id: VarId,
) -> OptimizationResult {
if !self.base_optimizer.can_optimize(vars, var_id) {
return OptimizationResult::variable_error(VariableError::NotFloatVariable);
}
let constrained_bounds = self.analyze_constraints(vars, props, var_id);
if !constrained_bounds.is_feasible {
return OptimizationResult::domain_error(DomainError::EmptyDomain);
}
let mut temp_vars = vars.clone();
if let Err(_error) = self.apply_constrained_bounds(&mut temp_vars, var_id, &constrained_bounds) {
return OptimizationResult::domain_error(DomainError::InvalidBounds);
}
let result = self.base_optimizer.maximize_variable(&temp_vars, var_id);
if result.success {
OptimizationResult::success(
result.optimal_value,
OptimizationOperation::Maximization,
var_id
)
} else {
result
}
}
pub fn minimize_with_constraints(
&self,
vars: &Vars,
props: &Propagators,
var_id: VarId,
) -> OptimizationResult {
if !self.base_optimizer.can_optimize(vars, var_id) {
return OptimizationResult::variable_error(VariableError::NotFloatVariable);
}
let constrained_bounds = self.analyze_constraints(vars, props, var_id);
if !constrained_bounds.is_feasible {
return OptimizationResult::domain_error(DomainError::EmptyDomain);
}
let mut temp_vars = vars.clone();
if let Err(_error) = self.apply_constrained_bounds(&mut temp_vars, var_id, &constrained_bounds) {
return OptimizationResult::domain_error(DomainError::InvalidBounds);
}
let result = self.base_optimizer.minimize_variable(&temp_vars, var_id);
if result.success {
OptimizationResult::success(
result.optimal_value,
OptimizationOperation::Minimization,
var_id
)
} else {
result
}
}
fn analyze_constraints(
&self,
vars: &Vars,
props: &Propagators,
var_id: VarId,
) -> ConstrainedBounds {
let original_interval = match &vars[var_id] {
crate::variables::Var::VarF(interval) => {
if interval.is_empty() {
return ConstrainedBounds::infeasible(ConflictType::EmptyDomain);
}
interval
},
crate::variables::Var::VarI(_) => {
return ConstrainedBounds::infeasible(ConflictType::NonFloatVariable);
}
};
use crate::search::{Space, propagate};
use crate::search::agenda::Agenda;
let mut space = Space {
vars: vars.clone(),
props: props.clone(),
trail: crate::search::trail::Trail::new(),
lp_solver_used: false,
lp_constraint_count: 0,
lp_variable_count: 0,
lp_stats: None,
};
let agenda = Agenda::with_props(props.get_prop_ids_iter());
match propagate(space, agenda) {
Some((_has_unassigned, result_space)) => {
space = result_space;
}
None => {
return ConstrainedBounds::infeasible(ConflictType::ConflictingInequalities {
upper_bound: original_interval.max,
lower_bound: original_interval.min,
});
}
}
let effective_min;
let effective_max;
let constraint_count = props.count();
let found_constraints = constraint_count > 0;
match &space.vars[var_id] {
crate::variables::Var::VarF(new_interval) => {
effective_min = new_interval.min;
effective_max = new_interval.max;
if effective_min > effective_max {
return ConstrainedBounds::infeasible(ConflictType::ConflictingInequalities {
upper_bound: effective_max,
lower_bound: effective_min,
});
}
}
crate::variables::Var::VarI(_) => {
return ConstrainedBounds::infeasible(ConflictType::NonFloatVariable);
}
}
let derivation = if !found_constraints {
BoundsDerivation::OriginalDomain
} else if constraint_count == 1 {
let effective_interval = FloatInterval::with_step(effective_min, effective_max, original_interval.step);
if effective_interval.is_fixed() {
BoundsDerivation::LinearEquality { target_value: effective_min }
} else {
BoundsDerivation::LinearInequality {
lower_constraint: if effective_min > original_interval.min { Some(effective_min) } else { None },
upper_constraint: if effective_max < original_interval.max { Some(effective_max) } else { None },
}
}
} else {
BoundsDerivation::CombinedConstraints { constraint_count }
};
ConstrainedBounds::new(effective_min, effective_max, derivation)
}
fn apply_constrained_bounds(
&self,
vars: &mut Vars,
var_id: VarId,
bounds: &ConstrainedBounds,
) -> Result<(), String> {
match &mut vars[var_id] {
crate::variables::Var::VarF(interval) => {
let step = interval.step;
*interval = FloatInterval::with_step(bounds.min, bounds.max, step);
Ok(())
},
crate::variables::Var::VarI(_) => {
Err("Cannot apply float bounds to integer variable".to_string())
}
}
}
pub fn maximize_and_apply_with_constraints(
&self,
vars: &mut Vars,
props: &Propagators,
var_id: VarId,
) -> OptimizationResult {
let result = self.maximize_with_constraints(vars, props, var_id);
if result.success {
match self.base_optimizer.apply_result(vars, var_id, &result) {
Ok(()) => result,
Err(_error) => OptimizationResult::domain_error(DomainError::InvalidBounds),
}
} else {
result
}
}
pub fn minimize_and_apply_with_constraints(
&self,
vars: &mut Vars,
props: &Propagators,
var_id: VarId,
) -> OptimizationResult {
let result = self.minimize_with_constraints(vars, props, var_id);
if result.success {
match self.base_optimizer.apply_result(vars, var_id, &result) {
Ok(()) => result,
Err(_error) => OptimizationResult::domain_error(DomainError::InvalidBounds),
}
} else {
result
}
}
}
impl Default for ConstraintAwareOptimizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::variables::Vars;
use crate::constraints::props::Propagators;
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)
}
fn create_test_props() -> Propagators {
Propagators::default()
}
#[test]
fn test_maximize_without_constraints() {
let optimizer = ConstraintAwareOptimizer::new();
let (vars, var_id) = create_test_vars_with_float(2.0, 8.0);
let props = create_test_props();
let result = optimizer.maximize_with_constraints(&vars, &props, var_id);
assert!(result.success, "Optimization should succeed");
assert_eq!(result.optimal_value, 8.0, "Should maximize to upper bound");
match result.outcome {
crate::optimization::float_direct::OptimizationOutcome::Success { operation, .. } => {
assert_eq!(operation, crate::optimization::float_direct::OptimizationOperation::Maximization);
},
_ => panic!("Expected successful maximization outcome"),
}
}
#[test]
fn test_minimize_without_constraints() {
let optimizer = ConstraintAwareOptimizer::new();
let (vars, var_id) = create_test_vars_with_float(1.5, 9.5);
let props = create_test_props();
let result = optimizer.minimize_with_constraints(&vars, &props, var_id);
assert!(result.success, "Optimization should succeed");
assert_eq!(result.optimal_value, 1.5, "Should minimize to lower bound");
match result.outcome {
crate::optimization::float_direct::OptimizationOutcome::Success { operation, .. } => {
assert_eq!(operation, crate::optimization::float_direct::OptimizationOperation::Minimization);
},
_ => panic!("Expected successful minimization outcome"),
}
}
#[test]
fn test_constrained_bounds_creation() {
let bounds = ConstrainedBounds::new(1.0, 5.0, BoundsDerivation::OriginalDomain);
assert_eq!(bounds.min, 1.0);
assert_eq!(bounds.max, 5.0);
assert!(bounds.is_feasible);
assert_eq!(bounds.derivation, BoundsDerivation::OriginalDomain);
}
#[test]
fn test_infeasible_bounds() {
let bounds = ConstrainedBounds::infeasible(ConflictType::EmptyDomain);
assert!(!bounds.is_feasible);
assert!(matches!(bounds.derivation, BoundsDerivation::Infeasible { .. }));
if let BoundsDerivation::Infeasible { conflict_type } = bounds.derivation {
assert_eq!(conflict_type, ConflictType::EmptyDomain);
}
}
#[test]
fn test_infeasible_optimization() {
let optimizer = ConstraintAwareOptimizer::new();
let mut vars = Vars::new();
let props = create_test_props();
let var_id = vars.new_var_with_bounds(
crate::variables::Val::float(1.0),
crate::variables::Val::float(5.0)
);
if let crate::variables::Var::VarF(interval) = &mut vars[var_id] {
interval.min = 5.0;
interval.max = 1.0; }
let result = optimizer.maximize_with_constraints(&vars, &props, var_id);
assert!(!result.success, "Should fail on infeasible domain");
println!("Actual outcome: {:?}", result.outcome);
match result.outcome {
crate::optimization::float_direct::OptimizationOutcome::DomainError(crate::optimization::float_direct::DomainError::EmptyDomain) => {
},
crate::optimization::float_direct::OptimizationOutcome::VariableError(crate::optimization::float_direct::VariableError::NotFloatVariable) => {
println!("Got NotFloatVariable, which might be expected for empty intervals");
},
_ => panic!("Expected EmptyDomain error for infeasible case, got: {:?}", result.outcome),
}
}
#[test]
fn test_integer_variable_rejection() {
let optimizer = ConstraintAwareOptimizer::new();
let mut vars = Vars::new();
let props = create_test_props();
let int_var_id = vars.new_var_with_bounds(
crate::variables::Val::int(1),
crate::variables::Val::int(10)
);
let result = optimizer.maximize_with_constraints(&vars, &props, int_var_id);
assert!(!result.success, "Should fail on integer variable");
match result.outcome {
crate::optimization::float_direct::OptimizationOutcome::VariableError(crate::optimization::float_direct::VariableError::NotFloatVariable) => {
},
_ => panic!("Expected NotFloatVariable error for integer variable"),
}
}
#[test]
fn test_maximize_and_apply_with_constraints() {
let optimizer = ConstraintAwareOptimizer::new();
let (mut vars, var_id) = create_test_vars_with_float(3.0, 7.0);
let props = create_test_props();
let result = optimizer.maximize_and_apply_with_constraints(&mut vars, &props, var_id);
assert!(result.success, "Maximize and apply should succeed");
assert_eq!(result.optimal_value, 7.0, "Should find correct maximum");
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_with_constraints() {
let optimizer = ConstraintAwareOptimizer::new();
let (mut vars, var_id) = create_test_vars_with_float(2.5, 6.5);
let props = create_test_props();
let result = optimizer.minimize_and_apply_with_constraints(&mut vars, &props, var_id);
assert!(result.success, "Minimize and apply should succeed");
assert_eq!(result.optimal_value, 2.5, "Should find correct minimum");
if let crate::variables::Var::VarF(interval) = &vars[var_id] {
assert_eq!(interval.min, 2.5);
assert_eq!(interval.max, 2.5);
} else {
assert!(false, "Variable should still be float");
}
}
#[test]
fn test_constraint_analysis_placeholder() {
let optimizer = ConstraintAwareOptimizer::new();
let (vars, var_id) = create_test_vars_with_float(1.0, 10.0);
let props = create_test_props();
let bounds = optimizer.analyze_constraints(&vars, &props, var_id);
assert!(bounds.is_feasible);
assert_eq!(bounds.min, 1.0);
assert_eq!(bounds.max, 10.0);
assert_eq!(bounds.derivation, BoundsDerivation::OriginalDomain);
}
}