use crate::core::error::SolverError;
use crate::variables::{Vars, Var, VarId};
use crate::constraints::props::Propagators;
use crate::optimization::constraint_metadata::ConstraintType;
use std::collections::{HashMap, HashSet};
#[doc(hidden)]
pub struct ModelValidator<'a> {
vars: &'a Vars,
props: &'a Propagators,
}
#[derive(Debug, Clone)]
pub enum ValidationError {
InvalidDomain {
variable_id: VarId,
issue: DomainIssue,
},
InvalidVariableReference {
constraint_id: usize,
variable_id: VarId,
constraint_type: String,
},
ConflictingConstraints {
conflict_type: ConflictType,
variables: Vec<VarId>,
constraint_details: String,
},
InvalidConstraintParameters {
constraint_id: usize,
constraint_type: String,
issue: String,
},
}
#[derive(Debug, Clone)]
pub enum DomainIssue {
EmptyDomain,
InvalidBounds { min: i32, max: i32 },
FloatPrecisionIssue { interval: String },
}
#[derive(Debug, Clone)]
pub enum ConflictType {
DirectValueConflict,
AllDifferentDomainTooSmall,
EmptyIntersection,
}
impl<'a> ModelValidator<'a> {
pub fn new(vars: &'a Vars, props: &'a Propagators) -> Self {
Self { vars, props }
}
pub fn validate(&self) -> Result<(), SolverError> {
self.validate_variable_domains()?;
self.validate_constraint_references()?;
self.validate_constraint_conflicts()?;
self.validate_constraint_parameters()?;
Ok(())
}
fn validate_variable_domains(&self) -> Result<(), SolverError> {
for (var_id, var) in self.vars.iter_with_indices() {
match var {
Var::VarI(sparse_set) => {
if sparse_set.is_empty() {
let min_val = sparse_set.min_universe_value();
let max_val = sparse_set.max_universe_value();
if max_val < min_val {
return Err(SolverError::InvalidDomain {
message: format!("Variable created with invalid bounds: min ({}) > max ({})", min_val, max_val),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some("integer variable bounds are reversed".to_string()),
});
} else {
return Err(SolverError::InvalidDomain {
message: "Variable domain is empty".to_string(),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some("integer domain with no valid values".to_string()),
});
}
}
let min_val = sparse_set.min_universe_value();
let max_val = sparse_set.max_universe_value();
if min_val > max_val {
return Err(SolverError::InvalidDomain {
message: "Variable domain bounds are invalid".to_string(),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some(format!("min ({}) > max ({})", min_val, max_val)),
});
}
let domain_size = sparse_set.universe_size();
if domain_size > crate::variables::domain::MAX_SPARSE_SET_DOMAIN_SIZE as usize {
return Err(SolverError::InvalidDomain {
message: "Variable domain is too large and may cause performance issues".to_string(),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some(format!("domain size: {} (max: {})", domain_size, crate::variables::domain::MAX_SPARSE_SET_DOMAIN_SIZE)),
});
}
},
Var::VarF(interval) => {
if interval.min > interval.max {
return Err(SolverError::InvalidDomain {
message: format!("Float variable created with invalid bounds: min ({}) > max ({})", interval.min, interval.max),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some("float variable bounds are reversed".to_string()),
});
}
if interval.min.is_infinite() || interval.max.is_infinite() {
return Err(SolverError::InvalidDomain {
message: "Float variable has infinite bounds".to_string(),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some(format!("bounds: [{}, {}]", interval.min, interval.max)),
});
}
if interval.min.is_nan() || interval.max.is_nan() {
return Err(SolverError::InvalidDomain {
message: "Float variable has NaN bounds".to_string(),
variable_name: Some(format!("var_{:?}", var_id)),
domain_info: Some("NaN values are not allowed in variable bounds".to_string()),
});
}
},
}
}
Ok(())
}
fn validate_constraint_references(&self) -> Result<(), SolverError> {
let constraint_registry = self.props.get_constraint_registry();
for constraint_id in constraint_registry.get_all_constraint_ids() {
if let Some(metadata) = constraint_registry.get_constraint(constraint_id) {
for &var_id in &metadata.variables {
if var_id.to_index() >= self.vars.count() {
return Err(SolverError::InvalidVariable {
message: "Constraint references non-existent variable".to_string(),
variable_id: Some(format!("var_{:?}", var_id)),
expected: Some(format!("0 to {}", self.vars.count() - 1)),
});
}
}
}
}
Ok(())
}
fn validate_constraint_conflicts(&self) -> Result<(), SolverError> {
let constraint_registry = self.props.get_constraint_registry();
let mut variable_constraints: HashMap<VarId, Vec<(usize, &ConstraintType)>> = HashMap::with_capacity(64);
for constraint_id in constraint_registry.get_all_constraint_ids() {
if let Some(metadata) = constraint_registry.get_constraint(constraint_id) {
for &var_id in &metadata.variables {
variable_constraints
.entry(var_id)
.or_insert_with(Vec::new)
.push((constraint_id.0, &metadata.constraint_type));
}
}
}
for (_var_id, constraints) in variable_constraints.iter() {
let mut equality_constraints = Vec::new();
for &(constraint_id, constraint_type) in constraints {
if matches!(constraint_type, ConstraintType::Equals) {
equality_constraints.push(constraint_id);
}
}
if equality_constraints.len() > 1 {
}
}
self.validate_alldiff_constraints()?;
Ok(())
}
fn validate_alldiff_constraints(&self) -> Result<(), SolverError> {
let constraint_registry = self.props.get_constraint_registry();
let alldiff_constraints = constraint_registry.get_constraints_by_type(&ConstraintType::AllDifferent);
for constraint_id in alldiff_constraints {
if let Some(metadata) = constraint_registry.get_constraint(constraint_id) {
let variables = &metadata.variables;
let num_variables = variables.len();
if num_variables <= 1 {
continue; }
let mut fixed_values = std::collections::HashSet::new();
let mut all_possible_values = std::collections::HashSet::new();
for &var_id in variables {
let var = &self.vars[var_id];
match var {
Var::VarI(sparse_set) => {
for val in sparse_set.iter() {
all_possible_values.insert(val);
}
if sparse_set.size() == 1 {
let fixed_val = sparse_set.iter().next().unwrap();
if fixed_values.contains(&fixed_val) {
return Err(SolverError::ConflictingConstraints {
constraint_names: Some(vec![format!("alldiff_constraint_{}", constraint_id.0)]),
variables: Some(variables.iter().map(|id| format!("var_{:?}", id)).collect()),
context: Some(format!(
"AllDifferent constraint has two variables fixed to the same value: {}",
fixed_val
)),
});
}
fixed_values.insert(fixed_val);
}
}
Var::VarF(_) => {
continue;
}
};
}
if all_possible_values.len() < num_variables {
return Err(SolverError::ConflictingConstraints {
constraint_names: Some(vec![format!("alldiff_constraint_{}", constraint_id.0)]),
variables: Some(variables.iter().map(|id| format!("var_{:?}", id)).collect()),
context: Some(format!(
"AllDifferent constraint requires {} distinct values, but only {} distinct values are available in the domains",
num_variables, all_possible_values.len()
)),
});
}
}
}
Ok(())
}
fn validate_constraint_parameters(&self) -> Result<(), SolverError> {
let constraint_registry = self.props.get_constraint_registry();
for constraint_id in constraint_registry.get_all_constraint_ids() {
if let Some(metadata) = constraint_registry.get_constraint(constraint_id) {
match &metadata.constraint_type {
ConstraintType::AllDifferent => {
let variables = &metadata.variables;
let mut seen_vars = HashSet::new();
for &var_id in variables {
if !seen_vars.insert(var_id) {
return Err(SolverError::InvalidConstraint {
message: "AllDifferent constraint contains duplicate variables".to_string(),
constraint_name: Some(format!("alldiff_constraint_{}", constraint_id.0)),
variables: Some(vec![format!("var_{:?} (duplicate)", var_id)]),
});
}
}
},
ConstraintType::Addition | ConstraintType::Multiplication => {
let operand_count = if let crate::optimization::constraint_metadata::ConstraintData::NAry { operands } = &metadata.data {
operands.len()
} else {
metadata.variables.len()
};
if operand_count != 3 {
return Err(SolverError::InvalidConstraint {
message: format!(
"{:?} constraint requires exactly 3 operands (x, y, result), got {}",
metadata.constraint_type, operand_count
),
constraint_name: Some(format!("constraint_{}", constraint_id.0)),
variables: Some(metadata.variables.iter().map(|id| format!("var_{:?}", id)).collect()),
});
}
if metadata.variables.len() < 2 || metadata.variables.len() > 3 {
return Err(SolverError::InvalidConstraint {
message: format!(
"{:?} constraint requires 2-3 variables, got {}",
metadata.constraint_type, metadata.variables.len()
),
constraint_name: Some(format!("constraint_{}", constraint_id.0)),
variables: Some(metadata.variables.iter().map(|id| format!("var_{:?}", id)).collect()),
});
}
},
ConstraintType::Division | ConstraintType::Modulo => {
if metadata.variables.len() != 3 {
return Err(SolverError::InvalidConstraint {
message: format!(
"{:?} constraint requires exactly 3 variables (dividend, divisor, result), got {}",
metadata.constraint_type, metadata.variables.len()
),
constraint_name: Some(format!("constraint_{}", constraint_id.0)),
variables: Some(metadata.variables.iter().map(|id| format!("var_{:?}", id)).collect()),
});
}
if metadata.variables.len() >= 2 {
let divisor_var_id = metadata.variables[1];
let divisor_var = &self.vars[divisor_var_id];
if let Var::VarI(sparse_set) = divisor_var {
if sparse_set.contains(0) {
return Err(SolverError::InvalidConstraint {
message: "Division/Modulo constraint has divisor that can be zero".to_string(),
constraint_name: Some(format!("constraint_{}", constraint_id.0)),
variables: Some(vec![format!("var_{:?} (divisor)", divisor_var_id)]),
});
}
}
}
},
ConstraintType::Minimum | ConstraintType::Maximum => {
if metadata.variables.len() < 2 {
return Err(SolverError::InvalidInput {
message: format!(
"{:?} constraint requires at least 1 input variable, got {}",
metadata.constraint_type,
metadata.variables.len().saturating_sub(1) ),
function_name: Some(match metadata.constraint_type {
ConstraintType::Minimum => "min".to_string(),
ConstraintType::Maximum => "max".to_string(),
_ => unreachable!(),
}),
expected: Some("non-empty slice of variable IDs".to_string()),
});
}
let input_var_count = metadata.variables.len().saturating_sub(1);
if input_var_count == 0 {
return Err(SolverError::InvalidInput {
message: format!(
"Cannot compute {} of empty variable list",
match metadata.constraint_type {
ConstraintType::Minimum => "minimum",
ConstraintType::Maximum => "maximum",
_ => unreachable!(),
}
),
function_name: Some(match metadata.constraint_type {
ConstraintType::Minimum => "min".to_string(),
ConstraintType::Maximum => "max".to_string(),
_ => unreachable!(),
}),
expected: Some("non-empty slice of variable IDs".to_string()),
});
}
},
_ => {
}
}
}
}
Ok(())
}
}
impl From<ValidationError> for SolverError {
fn from(validation_error: ValidationError) -> Self {
match validation_error {
ValidationError::InvalidDomain { variable_id, issue } => {
let (message, domain_info) = match issue {
DomainIssue::EmptyDomain => (
"Variable domain is empty".to_string(),
Some("no valid values in domain".to_string())
),
DomainIssue::InvalidBounds { min, max } => (
"Variable domain bounds are invalid".to_string(),
Some(format!("min ({}) > max ({})", min, max))
),
DomainIssue::FloatPrecisionIssue { interval } => (
"Float variable precision issue".to_string(),
Some(interval)
),
};
SolverError::InvalidDomain {
message,
variable_name: Some(format!("var_{:?}", variable_id)),
domain_info,
}
},
ValidationError::InvalidVariableReference { constraint_id, variable_id, constraint_type } => {
SolverError::InvalidVariable {
message: format!("{} constraint references invalid variable", constraint_type),
variable_id: Some(format!("var_{:?}", variable_id)),
expected: Some(format!("constraint_{}", constraint_id)),
}
},
ValidationError::ConflictingConstraints { conflict_type, variables, constraint_details } => {
let context = match conflict_type {
ConflictType::DirectValueConflict => "Direct value conflict detected".to_string(),
ConflictType::AllDifferentDomainTooSmall => "AllDifferent domain too small".to_string(),
ConflictType::EmptyIntersection => "Constraints create empty solution space".to_string(),
};
SolverError::ConflictingConstraints {
constraint_names: None,
variables: Some(variables.iter().map(|id| format!("var_{:?}", id)).collect()),
context: Some(format!("{}: {}", context, constraint_details)),
}
},
ValidationError::InvalidConstraintParameters { constraint_id, constraint_type, issue } => {
SolverError::InvalidConstraint {
message: issue,
constraint_name: Some(format!("{}_constraint_{}", constraint_type, constraint_id)),
variables: None,
}
},
}
}
}