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 Tag Constraint - about employee's working state #28

Open AybukeAk opened 1 year ago

AybukeAk commented 1 year ago

Hello,

We want to add another specific constraint about working status. Some employees cannot work on weekends or on specific days. In addition, there are restrictions such as pregnant, disabled and those with babies shouldn't be assigned at the weekend. We created the "Tag" class to indicate this. In our constraint list, we are using the True-False boolean values in the "working_state" variable, which indicates whether the employee can work according to employee's tag list from Employee class. (For example: "tag_list": [ "parttime","pregnant"])

class Tag:
    name: str 
    min_work_hours: int
    max_work_hours: int

    def __init__(self, name: str=None, min_work_hours: int=None, max_work_hours: int=None):
        self.name = name
        self.min_work_hours = min_work_hours
        self.max_work_hours = max_work_hours

    def __str__(self):
        return f'Tag(name={self.name}, min_work_hours = {self.min_work_hours}, max_work_hours = {self.max_work_hours})'

    def to_dict(self):
        return {
            'name': self.name,
            'min_work_hours': self.min_work_hours,
            'max_work_hours': self.max_work_hours
    }

@optapy.problem_fact
class ConstraintForTag:
    shift_id: int 
    tag: Tag
    working_state: bool 

    def __init__(self, shift_id: int=None, tag: Tag=None, working_state: bool=None):
        self.shift_id = shift_id
        self.tag = tag
        self.working_state = working_state

    def __str__(self):
        return f'ConstraintForEmployee(shift_id={self.shift_id}, tag = {self.tag}, working_state = {self.working_state})'

    def to_dict(self):
        return {
            'shift_id': self.shift_id,
            'tag': self.tag.to_dict(),
            'working_state': self.working_state
    }

And we added the "tag_constraint_for_employee" constraint as follows :

def tag_constraint_for_employee(constraint_factory: ConstraintFactory):
    return constraint_factory \
        .for_each(Shift) \
        .join(ConstraintForTag,
              Joiners.equal(lambda shift: shift.shift_id,
                            lambda constraint: constraint.shift_id)    
              ) \
             .filter(lambda shift, constraint: (constraint.tag.name in  shift.employee.tag_list) & (constraint.working_state==False)) \
        .penalize('Tag constraint for employee', HardSoftScore.ONE_HARD)`

We were using the old version of optapy before and this constraint was working without any errors, assigning employees without error or it was showing score if it brokes the constraint, now after upgrading to 8.31.1b0 we got the following error:

RuntimeError: An error occurred during solving. This can occur when functions take the wrong number of parameters (ex: a setter that does not take exactly one parameter) or by a function returning an incompatible return type (ex: returning a str in a filter, which expects a bool). This can also occur when an exception is raised when evaluating constraints/getters/setters.

Do you have any idea about how can I fix it? Thank you in advance.

Christopher-Chianelli commented 1 year ago

Reading from the traceback after running locally, it is because you returned an int in the filter, which expects a bool. This is because & is bitwise-and (i.e. given two ints, return the and of all their bits). If you changed the & to and, it will work.

Admittedly, it is bad form to return something that is not a bool in a filter, but I don't see a reason to prohibit it; created https://github.com/optapy/optapy/issues/159 to track the root issue.

AybukeAk commented 1 year ago

Hello, We're having trouble with our tag constraint. Optaplanner returned incorrect assignment, although a correct solution exists. While assigning, I am sure that it assigns taking into account the tag constraint. But when assigning, it gives priority the first employees in the input employee list. That causes the problem. I mean, even if employees at the end of the list are more suitable, those at the top of the list are prioritized. Increasing the duration.of seconds didn't make change (the optimum result was exist), I got the same erroneous output (ex : -7 hard score/0 soft score) . What would you recommend about it? Thank you in advance. Best regards

Christopher-Chianelli commented 1 year ago

@AybukeAk if you use ConstraintVerifier (see https://github.com/optapy/optapy-quickstarts/blob/stable/employee-scheduling/tests.py) on the tag constraint with the "correct" solution, does it still get triggered (i.e. did you verify for good inputs, it does NOT penalize, and for bad input, it does penalize)? What is your score calculation speed? Does the score in the UI match the score in the log?

AybukeAk commented 1 year ago

Hello, Thank you for your reply, sorry for late response. We are trying to use ConstraintVerifier. But our structure is different, we are using algorithm for n-n shift-employee schedule. We wrote a simple test as follows . But we got an error as "java.lang.IllegalArgumentException". We checked many times, our attributes of classes and ensured that parameters are correct. Do you have any idea how to handle this problem? Can we use ConstraintVerifier for our case?

Thank you in advance. Best regards,

from datetime import date, time, datetime, timedelta

DAY_1 = date(2021, 2, 1)

DAY_START_TIME = datetime.combine(DAY_1, time(9, 0))
DAY_END_TIME = datetime.combine(DAY_1, time(17, 0))
AFTERNOON_START_TIME = datetime.combine(DAY_1, time(13, 0))
AFTERNOON_END_TIME = datetime.combine(DAY_1, time(21, 0))

constraint_verifier: ConstraintVerifier = constraint_verifier_build(employee_scheduling_constraints,
                                                                    EmployeeSchedule, Shift)

def test_one_shift_per_day():
    employee1 = Employee("Amy", "email1",["tag1"],["skill"],5,7)
    # employee2 = Employee("Beth","email2",["tag2"],["skill2"],5,7)

    params1 = {
            "id": 1,
            "shift_id": 1,
            "employee": employee1,
            "end": DAY_END_TIME,
            "start": DAY_START_TIME,
            "weekday": "monday"
        }
    shift1 =  Shift(**params1)

    params2 = {
            "id": 2,
            "shift_id": 1,
            "employee": employee1,
            "end": DAY_END_TIME,
            "start": DAY_START_TIME,
            "weekday": "monday"
        }
    shift2 =  Shift(**params1)

    constraint_verifier.verify_that(no_overlapping_shifts) \
        .given(employee1, shift1, shift2).penalizes(1)