Слишком переусложненная структура - но оставим для будущего
This commit is contained in:
94
constraints/__init__.py
Normal file
94
constraints/__init__.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Constraints package."""
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
|
||||
from constraints.base_constraint import Constraint, ConstraintViolation, ConstraintType
|
||||
from constraints.hard_constraints import (
|
||||
SkillRequirementConstraint,
|
||||
ShiftCoverageConstraint,
|
||||
MaxWorkTimeConstraint,
|
||||
DepartmentAssignmentConstraint,
|
||||
MinCoverageConstraint,
|
||||
ConflictPreventionConstraint,
|
||||
create_hard_constraints
|
||||
)
|
||||
from constraints.soft_constraints import (
|
||||
OperatorPreferenceConstraint,
|
||||
DayAvailabilityConstraint,
|
||||
WorkBalanceConstraint,
|
||||
ContinuityPreference,
|
||||
SeniorityPriorityConstraint,
|
||||
create_soft_constraints
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstraintSet:
|
||||
"""Container for all constraints (hard and soft)."""
|
||||
hard_constraints: List[Constraint]
|
||||
soft_constraints: List[Constraint]
|
||||
scenario: Dict[str, Any]
|
||||
|
||||
def validate(self, context: Dict[str, Any]) -> List[ConstraintViolation]:
|
||||
"""Validate all constraints and return violations."""
|
||||
violations = []
|
||||
|
||||
# Check hard constraints
|
||||
for constraint in self.hard_constraints:
|
||||
if constraint.enabled:
|
||||
violation = constraint.get_violation(context)
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
|
||||
# Check soft constraints (only warn, don't fail)
|
||||
for constraint in self.soft_constraints:
|
||||
if constraint.enabled:
|
||||
violation = constraint.get_violation(context)
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
|
||||
return violations
|
||||
|
||||
def get_enabled_hard_constraints(self) -> List[Constraint]:
|
||||
"""Get all enabled hard constraints."""
|
||||
return [c for c in self.hard_constraints if c.enabled]
|
||||
|
||||
def get_enabled_soft_constraints(self) -> List[Constraint]:
|
||||
"""Get all enabled soft constraints."""
|
||||
return [c for c in self.soft_constraints if c.enabled]
|
||||
|
||||
def get_disabled_constraints(self) -> List[Constraint]:
|
||||
"""Get all disabled constraints."""
|
||||
return [c for c in self.hard_constraints + self.soft_constraints if not c.enabled]
|
||||
|
||||
def enable_constraint(self, constraint_id: str) -> bool:
|
||||
"""Enable a constraint by ID."""
|
||||
for constraint in self.hard_constraints + self.soft_constraints:
|
||||
if constraint.constraint_id == constraint_id:
|
||||
constraint.enabled = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def disable_constraint(self, constraint_id: str) -> bool:
|
||||
"""Disable a constraint by ID."""
|
||||
for constraint in self.hard_constraints + self.soft_constraints:
|
||||
if constraint.constraint_id == constraint_id:
|
||||
constraint.enabled = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear_violations(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if all violations can be cleared in context."""
|
||||
violations = self.validate(context)
|
||||
return len(violations) == 0
|
||||
|
||||
|
||||
def create_constraint_set(scenario: Dict[str, Any]) -> ConstraintSet:
|
||||
"""Factory function to create a complete constraint set."""
|
||||
hard = create_hard_constraints(scenario)
|
||||
soft = create_soft_constraints(scenario)
|
||||
return ConstraintSet(
|
||||
hard_constraints=hard,
|
||||
soft_constraints=soft,
|
||||
scenario=scenario
|
||||
)
|
||||
64
constraints/base_constraint.py
Normal file
64
constraints/base_constraint.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Base constraint classes."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Set, Dict, Optional, Any
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ConstraintType(Enum):
|
||||
"""Types of constraints in the scheduling system."""
|
||||
HARD = "hard" # Must be satisfied
|
||||
SOFT = "soft" # Should be satisfied if possible
|
||||
PREFERENCE = "preference" # Operator preferences
|
||||
|
||||
|
||||
@dataclass
|
||||
class Constraint:
|
||||
"""Base constraint class with common functionality."""
|
||||
constraint_id: str
|
||||
constraint_type: ConstraintType
|
||||
description: str
|
||||
severity: int = 0 # Higher = more important
|
||||
enabled: bool = True
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if constraint is satisfied in given context."""
|
||||
# Base implementation - subclasses should override this
|
||||
return True
|
||||
|
||||
def get_violation(self, context: Dict[str, Any]) -> Optional['ConstraintViolation']:
|
||||
"""Get violation if constraint is not satisfied."""
|
||||
if self.is_satisfied(context):
|
||||
return None
|
||||
return ConstraintViolation(
|
||||
constraint_id=self.constraint_id,
|
||||
constraint_type=self.constraint_type,
|
||||
description=self.description,
|
||||
severity=self.severity,
|
||||
context=context
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstraintViolation:
|
||||
"""Represents a constraint violation."""
|
||||
constraint_id: str
|
||||
constraint_type: ConstraintType
|
||||
description: str
|
||||
severity: int
|
||||
context: Dict[str, Any]
|
||||
violation_message: str = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.violation_message = f"{self.constraint_id}: {self.description}"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert violation to dictionary."""
|
||||
return {
|
||||
'constraint_id': self.constraint_id,
|
||||
'constraint_type': self.constraint_type.value,
|
||||
'description': self.description,
|
||||
'severity': self.severity,
|
||||
'context': self.context,
|
||||
'violation_message': self.violation_message
|
||||
}
|
||||
1
constraints/global_constraints.py
Normal file
1
constraints/global_constraints.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Global constraints."""
|
||||
120
constraints/hard_constraints.py
Normal file
120
constraints/hard_constraints.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Hard constraints - must always be satisfied."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from constraints.base_constraint import Constraint, ConstraintViolation, ConstraintType
|
||||
|
||||
|
||||
@dataclass
|
||||
class SkillRequirementConstraint(Constraint):
|
||||
"""Constraint ensuring operator has required skills for shift."""
|
||||
operator_id: str
|
||||
required_skills: List[str]
|
||||
shift_id: str
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if operator has all required skills."""
|
||||
operator_skills = context.get('operators', {}).get(self.operator_id, {}).get('skills', [])
|
||||
return all(skill in operator_skills for skill in self.required_skills)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShiftCoverageConstraint(Constraint):
|
||||
"""Constraint ensuring shift is covered by at least one operator."""
|
||||
shift_id: str
|
||||
minimum_coverage: int = 1
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if shift has minimum coverage."""
|
||||
assigned_operators = context.get('assignments', {}).get(self.shift_id, [])
|
||||
return len(assigned_operators) >= self.minimum_coverage
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaxWorkTimeConstraint(Constraint):
|
||||
"""Constraint limiting maximum working hours per day/week."""
|
||||
operator_id: str
|
||||
max_daily_hours: int = 8
|
||||
max_weekly_hours: int = 40
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if operator hasn't exceeded max hours."""
|
||||
operator_shifts = context.get('operators', {}).get(self.operator_id, {}).get('shifts', [])
|
||||
total_hours = sum(len(s) for s in operator_shifts)
|
||||
return total_hours <= self.max_daily_hours
|
||||
|
||||
|
||||
@dataclass
|
||||
class DepartmentAssignmentConstraint(Constraint):
|
||||
"""Constraint ensuring operator is assigned to correct department."""
|
||||
operator_id: str
|
||||
allowed_departments: List[str]
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if operator is in allowed department."""
|
||||
assigned_dept = context.get('assignments', {}).get(self.operator_id, {}).get('department')
|
||||
return assigned_dept in self.allowed_departments
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinCoverageConstraint(Constraint):
|
||||
"""Constraint ensuring minimum operators per shift."""
|
||||
shift_id: str
|
||||
min_operators: int
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if minimum operators are assigned."""
|
||||
assigned = context.get('assignments', {}).get(self.shift_id, [])
|
||||
return len(assigned) >= self.min_operators
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConflictPreventionConstraint(Constraint):
|
||||
"""Constraint preventing conflicting assignments."""
|
||||
operator_id: str
|
||||
conflicts: List[str]
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check for conflicts."""
|
||||
current_conflicts = [c for c in self.conflicts if c in context.get('conflicts', {})]
|
||||
return len(current_conflicts) == 0
|
||||
|
||||
|
||||
def create_hard_constraints(scenario: Dict[str, Any]) -> List[Constraint]:
|
||||
"""Factory function to create all hard constraints from scenario."""
|
||||
constraints = []
|
||||
|
||||
# Create skill requirement constraints
|
||||
for shift in scenario.get('events', []):
|
||||
for agent in scenario.get('agents', []):
|
||||
if agent.get('type') == 'operator':
|
||||
constraints.append(SkillRequirementConstraint(
|
||||
constraint_id=f"skill_req_{shift['id']}_{agent['id']}",
|
||||
constraint_type=ConstraintType.HARD,
|
||||
description=f"Operator {agent['id']} must have skills for shift {shift['id']}",
|
||||
operator_id=agent['id'],
|
||||
required_skills=agent.get('skills', []),
|
||||
shift_id=shift['id']
|
||||
))
|
||||
|
||||
# Create coverage constraints
|
||||
for shift in scenario.get('events', []):
|
||||
constraints.append(ShiftCoverageConstraint(
|
||||
constraint_id=f"coverage_{shift['id']}",
|
||||
constraint_type=ConstraintType.HARD,
|
||||
description=f"Shift {shift['id']} must be covered",
|
||||
shift_id=shift['id'],
|
||||
minimum_coverage=1
|
||||
))
|
||||
|
||||
# Create department constraints
|
||||
for agent in scenario.get('agents', []):
|
||||
if agent.get('type') == 'operator':
|
||||
constraints.append(DepartmentAssignmentConstraint(
|
||||
constraint_id=f"dept_{agent['id']}",
|
||||
constraint_type=ConstraintType.HARD,
|
||||
description=f"Operator {agent['id']} assigned to correct department",
|
||||
operator_id=agent['id'],
|
||||
allowed_departments=[agent.get('department', '')]
|
||||
))
|
||||
|
||||
return constraints
|
||||
123
constraints/soft_constraints.py
Normal file
123
constraints/soft_constraints.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Soft constraints - should be satisfied when possible."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from constraints.base_constraint import Constraint, ConstraintViolation, ConstraintType
|
||||
|
||||
|
||||
@dataclass
|
||||
class OperatorPreferenceConstraint(Constraint):
|
||||
"""Soft constraint for operator shift preferences."""
|
||||
operator_id: str
|
||||
preferred_shifts: List[str] # Shift types operator prefers
|
||||
priority: int = 1 # Higher = more important preference
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if operator is assigned preferred shifts."""
|
||||
assigned_shifts = context.get('operators', {}).get(self.operator_id, {}).get('shifts', [])
|
||||
assigned_shift_types = [s.get('shift_type') for s in assigned_shifts]
|
||||
return all(pref in assigned_shift_types for pref in self.preferred_shifts)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DayAvailabilityConstraint(Constraint):
|
||||
"""Soft constraint for operator day availability."""
|
||||
operator_id: str
|
||||
preferred_days: List[str]
|
||||
priority: int = 1
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if operator works on preferred days."""
|
||||
assigned_shifts = context.get('operators', {}).get(self.operator_id, {}).get('shifts', [])
|
||||
assigned_days = set()
|
||||
for shift in assigned_shifts:
|
||||
day_name = shift.get('date', {}).get('strftime', '%Y-%m-%d')
|
||||
assigned_days.add(day_name)
|
||||
return all(day in assigned_days for day in self.preferred_days)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkBalanceConstraint(Constraint):
|
||||
"""Soft constraint for balanced workload distribution."""
|
||||
shift_id: str
|
||||
max_assignments: int = 3 # Don't assign more than X people to same shift
|
||||
priority: int = 2
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if shift isn't overloaded."""
|
||||
assigned = context.get('assignments', {}).get(self.shift_id, [])
|
||||
return len(assigned) <= self.max_assignments
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityPreference(Constraint):
|
||||
"""Soft constraint for consecutive shift preferences."""
|
||||
operator_id: str
|
||||
preferred_pattern: str # e.g., "morning,afternoon" for consecutive shifts
|
||||
priority: int = 1
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if consecutive shifts match preference."""
|
||||
assigned_shifts = context.get('operators', {}).get(self.operator_id, {}).get('shifts', [])
|
||||
if len(assigned_shifts) < 2:
|
||||
return True
|
||||
# Check if shifts are consecutive and match pattern
|
||||
return True # Implementation depends on shift ordering
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeniorityPriorityConstraint(Constraint):
|
||||
"""Soft constraint to prioritize senior operators."""
|
||||
shift_id: str
|
||||
max_junior_assignments: int = 2
|
||||
|
||||
def is_satisfied(self, context: Dict[str, Any]) -> bool:
|
||||
"""Check if enough senior operators are assigned."""
|
||||
assigned = context.get('assignments', {}).get(self.shift_id, [])
|
||||
junior_count = sum(1 for op in assigned if op.get('seniority', 0) < 3)
|
||||
return junior_count <= self.max_junior_assignments
|
||||
|
||||
|
||||
def create_soft_constraints(scenario: Dict[str, Any]) -> List[Constraint]:
|
||||
"""Factory function to create all soft constraints from scenario."""
|
||||
constraints = []
|
||||
|
||||
# Create preference constraints
|
||||
for agent in scenario.get('agents', []):
|
||||
if agent.get('type') == 'operator':
|
||||
prefs = agent.get('preferences', {})
|
||||
if prefs:
|
||||
constraints.append(OperatorPreferenceConstraint(
|
||||
constraint_id=f"pref_{agent['id']}",
|
||||
constraint_type=ConstraintType.SOFT,
|
||||
description=f"Operator {agent['id']} prefers shifts: {prefs}",
|
||||
operator_id=agent['id'],
|
||||
preferred_shifts=list(prefs.keys()) if isinstance(prefs, dict) else prefs,
|
||||
priority=1
|
||||
))
|
||||
|
||||
# Create day availability constraints
|
||||
for agent in scenario.get('agents', []):
|
||||
if agent.get('type') == 'operator':
|
||||
preferred_days = agent.get('preferred_days', [])
|
||||
if preferred_days:
|
||||
constraints.append(DayAvailabilityConstraint(
|
||||
constraint_id=f"day_pref_{agent['id']}",
|
||||
constraint_type=ConstraintType.SOFT,
|
||||
description=f"Operator {agent['id']} prefers days: {preferred_days}",
|
||||
operator_id=agent['id'],
|
||||
preferred_days=preferred_days,
|
||||
priority=1
|
||||
))
|
||||
|
||||
# Create work balance constraints
|
||||
for shift in scenario.get('events', []):
|
||||
constraints.append(WorkBalanceConstraint(
|
||||
constraint_id=f"balance_{shift['id']}",
|
||||
constraint_type=ConstraintType.SOFT,
|
||||
description=f"Limit assignments for shift {shift['id']}",
|
||||
shift_id=shift['id'],
|
||||
max_assignments=scenario.get('constraints', {}).get('max_per_shift', 3),
|
||||
priority=2
|
||||
))
|
||||
|
||||
return constraints
|
||||
Reference in New Issue
Block a user