Слишком переусложненная структура - но оставим для будущего

This commit is contained in:
ki.sagidullin
2026-04-05 10:05:03 +05:00
parent 018e2a860e
commit f916d764c0
48 changed files with 2222 additions and 5 deletions

94
constraints/__init__.py Normal file
View 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
)

View 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
}

View File

@@ -0,0 +1 @@
"""Global constraints."""

View 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

View 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