optapy / optapy-quickstarts

OptaPy quick starts for AI optimization: showcases many different use cases.
Apache License 2.0
19 stars 13 forks source link

Employee Scheduling - Adding a new constraint about employee's minimum working day #26

Closed AybukeAk closed 1 year ago

AybukeAk commented 1 year ago

Hello!

I am using constraints no_overlapping_shifts, at_least_10_hours_between_two_shifts, one_shift_per_day, tag_constraint_for_employee in your constraint.py file. In addition to these, I wanted to add the max_working _day constraint that the employee should not exceed the maximum number of days a week can work. The code is like this:

def constraint_for_employee_max_working_day_in_week(constraint_factory: ConstraintFactory):
    return constraint_factory \
        .for_each(Shift) \
             .group_by(lambda shift: shift.employee, ConstraintCollectors.count()) \
             .filter(lambda employee ,shift_count : ((shift_count > employee.max_working_day_in_week)))\
             .penalize('Constraint for employee max working day in week', HardSoftScore.ONE_HARD,
             lambda employee, shift_count: shift_count - employee.max_working_day_in_week)  

This code is working correctly and I wanted to add min working day constraint too, and updated domain.py , constraint.py according to the function. It didn't work when the two were together. Although it produces results that do not meet the min_working_day constraint, it did not show in the error score. Do you have any idea why it is not working? Any ideas on how I can add these constraints?

Thank you in advance

def constraint_for_employee_min_working_day_in_week(constraint_factory: ConstraintFactory):
    return constraint_factory \
        .for_each(Shift) \
             .group_by(lambda shift: shift.employee, ConstraintCollectors.count()) \
             .filter(lambda employee ,shift_count : ((shift_count < employee.min_working_day_in_week)))\
             .penalize('Constraint for employee min working day in week', HardSoftScore.ONE_HARD,
             lambda employee, shift_count: employee.min_working_day_in_week - shift_count) 
Christopher-Chianelli commented 1 year ago

min is an annoying one to implement, because unlike max, you also need to handle the 0 case. However, if an employee have 0 shifts, it will not appear as a group key in the group_by. This means to correctly implement min, you need two separate constraints:

AybukeAk commented 1 year ago

Hello! Thank you so much for your reply. When I implemented the two functions as you specified, I could not understand the error score of the constraint_for_employee_min_working_day_in_week_no_shifts constraint. Normally I expect non-assignables (0 case ) to produce as errors as min_working_days . But regardless of the number of non-assigned employees, it produces a score like (number of agents * min_working_day of the employee) . For example, when I want to assign 18 agents to 27 shifts, when min_working_day is 1 and max_working_day is 2, if all other constraints are met without error, 4 agents are not assigned and it generates -18 scores. So I thought constraint_for_employee_min_working_day_in_week constraint also generates error for unassigned workers. Do you think is it possible? Could you help me to fix this error?

When I tried to use “if_exist” function in constraint_for_employee_min_working_day_in_week, I got following error :

TypeError: Unable to cast 'org.optaplanner.constraint.streams.common.bi.DefaultBiJoiner' to java type 'org.optaplanner.core.api.score.stream.tri.TriJoiner'

Other cases I have observed are as follows:

of employee | #of shifts | min_working_day | max_working_day | #of employees has no shift | error score

-- | -- | -- | -- | -- | -- 18 | 27 | 2 | 2 | 4 | -36 18 | 27 | 5 | 7 | 13 | -90 18 | 27 | 6 | 7 | 13 | -108

Thank you in advance

Christopher-Chianelli commented 1 year ago

@AybukeAk the TypeError is because you used a joiner the joins on a UniStream (BiJoiner) at a place that joins on a BiStream (as the groupby transformed it from a uni to a bi).

You can verify constraints using constraint verifier; see https://github.com/optapy/optapy-quickstarts/blob/stable/employee-scheduling/tests.py for examples.

When tested using constraint verifier, constraint_for_employee_min_working_day_in_week_no_shifts works as expected (only penalizes employees with no shifts).

You can use ScoreManager to explain the score of a given solution; see https://www.optapy.org/docs/latest/score-calculation/score-calculation.html#explainingTheScore .

AybukeAk commented 1 year ago

Thank you so much for your reply. If_not_exists did not work correctly because we were using the old version of optapy. After the update, both constraints worked correctly. But now we got an error in a previously working constrait. I opened another issue regarding this: https://github.com/optapy/optapy-quickstarts/issues/28