"""Model validation and pre-solve checks for constraint models.
Provides comprehensive validation with actionable suggestions to help
LLMs and users fix modeling issues before solving.
"""
from dataclasses import dataclass
from enum import Enum
from typing import cast
from chuk_mcp_solver.models import (
AllDifferentParams,
Constraint,
ConstraintKind,
ElementParams,
ImplicationParams,
LinearConstraintParams,
SolveConstraintModelRequest,
SolverMode,
TableParams,
)
class ValidationSeverity(str, Enum):
"""Severity level of validation issue."""
ERROR = "error" # Must fix - will cause solve to fail
WARNING = "warning" # Should fix - may cause poor performance
INFO = "info" # Nice to fix - improves model quality
@dataclass
class ValidationIssue:
"""A validation issue found in the model."""
severity: ValidationSeverity
message: str
suggestion: str
location: str # e.g., "variable 'x'", "constraint 'c1'"
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"severity": self.severity.value,
"message": self.message,
"suggestion": self.suggestion,
"location": self.location,
}
@dataclass
class ValidationResult:
"""Result of model validation."""
is_valid: bool
issues: list[ValidationIssue]
@property
def errors(self) -> list[ValidationIssue]:
"""Get only error-level issues."""
return [i for i in self.issues if i.severity == ValidationSeverity.ERROR]
@property
def warnings(self) -> list[ValidationIssue]:
"""Get only warning-level issues."""
return [i for i in self.issues if i.severity == ValidationSeverity.WARNING]
@property
def infos(self) -> list[ValidationIssue]:
"""Get only info-level issues."""
return [i for i in self.issues if i.severity == ValidationSeverity.INFO]
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"is_valid": self.is_valid,
"error_count": len(self.errors),
"warning_count": len(self.warnings),
"info_count": len(self.infos),
"issues": [issue.to_dict() for issue in self.issues],
}
class ModelValidator:
"""Validates constraint models before solving."""
def __init__(self) -> None:
"""Initialize validator."""
self.issues: list[ValidationIssue] = []
def validate(self, request: SolveConstraintModelRequest) -> ValidationResult:
"""Validate a solve request.
Args:
request: The request to validate
Returns:
ValidationResult with any issues found
"""
self.issues = []
# Run all validation checks
self._validate_variables(request)
self._validate_constraints(request)
self._validate_objective(request)
self._validate_model_structure(request)
# Model is valid only if there are no errors
is_valid = len(self.errors) == 0
return ValidationResult(is_valid=is_valid, issues=self.issues)
@property
def errors(self) -> list[ValidationIssue]:
"""Get current error-level issues."""
return [i for i in self.issues if i.severity == ValidationSeverity.ERROR]
def _validate_variables(self, request: SolveConstraintModelRequest) -> None:
"""Validate variable definitions."""
var_ids = set()
for var in request.variables:
# Check for duplicate IDs
if var.id in var_ids:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Duplicate variable ID '{var.id}'",
suggestion=f"Rename one of the variables with ID '{var.id}' to a unique name",
location=f"variable '{var.id}'",
)
)
var_ids.add(var.id)
# Check for invalid bounds
if var.domain.type.value == "integer":
if var.domain.lower > var.domain.upper:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Variable '{var.id}' has lower bound ({var.domain.lower}) "
f"greater than upper bound ({var.domain.upper})",
suggestion=f"Swap the bounds: set lower={var.domain.upper}, "
f"upper={var.domain.lower}",
location=f"variable '{var.id}'",
)
)
# Check for very large domains (warning)
domain_size = var.domain.upper - var.domain.lower
if domain_size > 1_000_000:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.WARNING,
message=f"Variable '{var.id}' has very large domain "
f"[{var.domain.lower}, {var.domain.upper}] with {domain_size:,} values",
suggestion="Consider tightening the bounds if possible to improve "
"solving performance",
location=f"variable '{var.id}'",
)
)
def _validate_constraints(self, request: SolveConstraintModelRequest) -> None:
"""Validate constraint definitions."""
var_ids = {v.id for v in request.variables}
constraint_ids = set()
for constraint in request.constraints:
# Check for duplicate constraint IDs
if constraint.id in constraint_ids:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Duplicate constraint ID '{constraint.id}'",
suggestion=f"Rename one of the constraints with ID '{constraint.id}' "
"to a unique name",
location=f"constraint '{constraint.id}'",
)
)
constraint_ids.add(constraint.id)
# Validate constraint-specific parameters
self._validate_constraint_params(constraint, var_ids)
def _validate_constraint_params(self, constraint: Constraint, var_ids: set[str]) -> None:
"""Validate constraint parameters."""
params = constraint.params
if constraint.kind == ConstraintKind.LINEAR:
self._validate_linear_constraint(
constraint, cast(LinearConstraintParams, params), var_ids
)
elif constraint.kind == ConstraintKind.ALL_DIFFERENT:
self._validate_all_different_constraint(
constraint, cast(AllDifferentParams, params), var_ids
)
elif constraint.kind == ConstraintKind.ELEMENT:
self._validate_element_constraint(constraint, cast(ElementParams, params), var_ids)
elif constraint.kind == ConstraintKind.TABLE:
self._validate_table_constraint(constraint, cast(TableParams, params), var_ids)
elif constraint.kind == ConstraintKind.IMPLICATION:
self._validate_implication_constraint(
constraint, cast(ImplicationParams, params), var_ids
)
def _validate_linear_constraint(
self, constraint: Constraint, params: LinearConstraintParams, var_ids: set[str]
) -> None:
"""Validate linear constraint parameters."""
# Check for undefined variables
for term in params.terms:
if term.var not in var_ids:
similar = self._find_similar_var(term.var, var_ids)
suggestion = (
f"Define variable '{term.var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{term.var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Variable '{term.var}' referenced in constraint '{constraint.id}' "
"is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
# Check for zero coefficients (warning)
zero_coef_vars = [term.var for term in params.terms if term.coef == 0]
if zero_coef_vars:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.WARNING,
message=f"Constraint '{constraint.id}' has zero coefficients for variables: "
f"{', '.join(zero_coef_vars)}",
suggestion="Remove terms with zero coefficients - they have no effect",
location=f"constraint '{constraint.id}'",
)
)
def _validate_all_different_constraint(
self, constraint: Constraint, params: AllDifferentParams, var_ids: set[str]
) -> None:
"""Validate all_different constraint parameters."""
for var in params.vars:
if var not in var_ids:
similar = self._find_similar_var(var, var_ids)
suggestion = (
f"Define variable '{var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Variable '{var}' in all_different constraint '{constraint.id}' "
"is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
def _validate_element_constraint(
self, constraint: Constraint, params: ElementParams, var_ids: set[str]
) -> None:
"""Validate element constraint parameters."""
if params.index_var not in var_ids:
similar = self._find_similar_var(params.index_var, var_ids)
suggestion = (
f"Define variable '{params.index_var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{params.index_var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Index variable '{params.index_var}' in element constraint "
f"'{constraint.id}' is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
if params.target_var not in var_ids:
similar = self._find_similar_var(params.target_var, var_ids)
suggestion = (
f"Define variable '{params.target_var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{params.target_var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Target variable '{params.target_var}' in element constraint "
f"'{constraint.id}' is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
def _validate_table_constraint(
self, constraint: Constraint, params: TableParams, var_ids: set[str]
) -> None:
"""Validate table constraint parameters."""
for var in params.vars:
if var not in var_ids:
similar = self._find_similar_var(var, var_ids)
suggestion = (
f"Define variable '{var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Variable '{var}' in table constraint '{constraint.id}' "
"is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
def _validate_implication_constraint(
self, constraint: Constraint, params: ImplicationParams, var_ids: set[str]
) -> None:
"""Validate implication constraint parameters."""
if params.if_var not in var_ids:
similar = self._find_similar_var(params.if_var, var_ids)
suggestion = (
f"Define variable '{params.if_var}' in the variables list"
if not similar
else f"Did you mean '{similar}'? Or define variable '{params.if_var}'"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Condition variable '{params.if_var}' in implication constraint "
f"'{constraint.id}' is not defined",
suggestion=suggestion,
location=f"constraint '{constraint.id}'",
)
)
def _validate_objective(self, request: SolveConstraintModelRequest) -> None:
"""Validate objective function."""
if request.mode == SolverMode.OPTIMIZE and not request.objective:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message="Optimization mode requires an objective function",
suggestion="Either add an objective function or change mode to 'satisfy'",
location="request",
)
)
if request.mode == SolverMode.SATISFY and request.objective:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.WARNING,
message="Objective function provided in satisfy mode will be ignored",
suggestion="Change mode to 'optimize' to use the objective, or remove it",
location="request",
)
)
# Validate objective variables if present
if request.objective:
var_ids = {v.id for v in request.variables}
objectives = (
request.objective if isinstance(request.objective, list) else [request.objective]
)
for obj in objectives:
for term in obj.terms:
if term.var not in var_ids:
similar = self._find_similar_var(term.var, var_ids)
suggestion = (
f"Define variable '{term.var}' in the variables list"
if not similar
else f"Did you mean '{similar}'?"
)
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.ERROR,
message=f"Variable '{term.var}' in objective is not defined",
suggestion=suggestion,
location="objective",
)
)
def _validate_model_structure(self, request: SolveConstraintModelRequest) -> None:
"""Validate overall model structure and quality."""
num_vars = len(request.variables)
num_constraints = len(request.constraints)
# Check for under-constrained models
if num_vars > 10 and num_constraints < num_vars / 5:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.INFO,
message=f"Model has {num_vars} variables but only {num_constraints} "
"constraints - it may be under-constrained",
suggestion="Consider adding more constraints to better define the problem",
location="model structure",
)
)
# Check for over-constrained models (many constraints per variable)
if num_vars > 0 and num_constraints / num_vars > 10:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.INFO,
message=f"Model has {num_constraints} constraints for {num_vars} variables "
f"(ratio {num_constraints / num_vars:.1f}:1) - it may be over-constrained",
suggestion="Check if some constraints are redundant or conflicting",
location="model structure",
)
)
# Check for no constraints at all
if num_constraints == 0 and request.mode == SolverMode.SATISFY:
self.issues.append(
ValidationIssue(
severity=ValidationSeverity.WARNING,
message="Model has no constraints in satisfy mode",
suggestion="Any variable assignment will be valid - is this intentional?",
location="model structure",
)
)
def _find_similar_var(self, var_id: str, var_ids: set[str]) -> str | None:
"""Find a similar variable ID using simple string matching.
Args:
var_id: The undefined variable ID
var_ids: Set of defined variable IDs
Returns:
Most similar variable ID, or None if no good match
"""
# Simple heuristic: find vars that differ by one character or have similar prefix
for candidate in var_ids:
# Check edit distance of 1 (simple version)
if len(var_id) == len(candidate):
diff_count = sum(1 for a, b in zip(var_id, candidate, strict=False) if a != b)
if diff_count == 1:
return candidate
# Check common prefix of at least 3 chars
if len(var_id) >= 3 and len(candidate) >= 3:
if var_id[:3] == candidate[:3]:
return candidate
return None
def validate_model(request: SolveConstraintModelRequest) -> ValidationResult:
"""Validate a constraint model.
Args:
request: The solve request to validate
Returns:
ValidationResult with any issues found
"""
validator = ModelValidator()
return validator.validate(request)