I’m working on a scheduling problem where I need to assign technicians to service devices. However, in some cases, it is preferable to leave a device unassigned if no suitable technician (lack of demanded skills) is available (i.e., the device.pesel should be None). I’ve structured my domain model as follows:
@dataclass
class Technician:
id: Annotated[int, PlanningId]
pesel: str
name: str
rbh_per_week: int
rbh_per_year: float
rbh_week_plan: float
selected_rbh: float
free_rbh: float
iums: Set[str]
def has_ium(self, ium: str) -> bool:
return ium in this.iums
def __str__(self) -> str:
return (f"Technician(id={self.id}, name={self.name}, pesel={self.pesel}, "
f"rbh_per_week={self.rbh_per_week}, rbh_per_year={self.rbh_per_year}, "
f"rbh_week_plan={self.rbh_week_plan}, selected_rbh={self.selected_rbh}, "
f"free_rbh={self.free_rbh}, iums={self.iums})")
@planning_entity
@dataclass
class Device:
index: Annotated[int, PlanningId]
ind_rek: str
ium: str
nazwa: str
typ: str
nr_fab: str
norma_rbh: float
data_dostawy: str
uzytkownik: str
pesel: Annotated[Technician | None, PlanningVariable(value_range_provider_refs=['technicianRange'], nullable=True)] = field(default=None)
def __str__(self) -> str:
technician_str = str(self.pesel) if self.pesel else "None"
return (f"Device(index={self.index}, ind_rek={self.ind_rek}, ium={self.ium}, "
f"nazwa={self.nazwa}, typ={self.typ}, nr_fab={self.nr_fab}, "
f"norma_rbh={self.norma_rbh}, data_dostawy={self.data_dostawy}, "
f"uzytkownik={self.uzytkownik}, assigned_technician={technician_str})")
@planning_solution
@dataclass
class DeviceSchedule:
id: str
technician_list: Annotated[List[Technician], ProblemFactCollectionProperty, ValueRangeProvider]
device_list: Annotated[List[Device], PlanningEntityCollectionProperty]
score: Annotated[HardSoftScore, PlanningScore] = field(default=None)
def __str__(self):
return (
f"DeviceSchedule("
f"id={self.id},n"
f"technician_list={self.technician_list},n"
f"device_list={self.device_list},n"
f"score={self.score}"
f")"
)
My constraints are:
@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
return [
# HARD constraint to avoid assigning a technician without the required skill
technician_skill_conflict(constraint_factory),
]
def technician_skill_conflict(constraint_factory: ConstraintFactory):
return (constraint_factory
.for_each(Device)
.filter(lambda device: device.pesel is not None and not device.pesel.has_ium(device.ium))
.penalize(HardSoftScore.ONE_HARD)
.as_constraint("Technician skill conflict"))
The Problem:
Even though I have implemented the nullable=True option in the PlanningVariable, and I have defined a hard constraint that penalizes assigning a technician without the required skill, the solver still tends to assign a technician to every device, even when it would be better to leave the device unassigned.
My Question:
How should I structure my classes and constraints to ensure that the solver leaves the device.pesel as None when no suitable technician is available, without incurring hard penalties for leaving the device unassigned?
I will be appreciated for any insights or suggestions how to handle this situation.