Search code examples
pythonoptapy

Shadow variable in optapy is not updated as expected


This clarification makes a lot of sense to me, but if I try and apply the reasoning to the code below (which is based on the employee scheduling example available in optapy) I would have expected set_timeslot_list to be called when set_task is called but it does not look like it is.

The optimisation runs OK and finds a suitable set of tasks to assign to the list of time slots that I have available, but each task.timeslot_list remains empty, and looks like the set_timeslot_list method is never called.

I believe I am missing something..Can you please help me understand what is wrong with how I modified the example in the code below or with I am interpreting how shadow vars work? I can provide longer snippets, or the @planning_solution class if this is not sufficient.

@planning_entity(pinning_filter=timeslot_pinning_filter)
class Timeslot:
    def __init__(self, start: datetime.datetime = None, end: datetime.datetime = None,
             location: str = None, required_skill: str = None, task: object = None):
        self.start = start
        self.end = end
        self.location = location
        self.required_skill = required_skill
        self.task = task

        @planning_id
        def get_id(self):
            return self.id

        # The type of the planning variable is Task, but we cannot use it because task refers to Timeslot below.
        @planning_variable(object, value_range_provider_refs=['task_range'], nullable=False)
        def get_task(self):
            return self.task

        def set_task(self, task):
            self.task = task
    

@planning_entity
class Task:
    def __init__(self, name: str = None, duration: int = None, skill_set: list = None):
        self.name = name
        self.duration = duration
        self.skill_set = skill_set
        self.timeslot_list = [] #The shadow property, which is a list, can never be None. If no genuine variable references that shadow entity, then it is an empty list

    @inverse_relation_shadow_variable(Timeslot, source_variable_name = "task")
    def get_timeslot_list(self):
        return self.timeslot_list

    def set_timeslot_list(self, ts):
        self.timeslot_list = ts

Solution

  • Inverse Relation Shadow variables work differently than other variables: in particular, they directly modify the list returned by get_timeslot_list, so set_timeslot_list is never called. Your code look correct, which leaves me to believe you are checking the original planning entities and not the solution planning entities. In OptaPy (and OptaPlanner), the working solution/planning solution is cloned whenever we find a new best solution. As a result, the original problem (and the original planning entities) are never touched. So if your code look similar to this:

    solver = optapy.solver_factory_create(...).buildSolver()
    timeslot_list = [...]
    task_1 = Task(...)
    task_2 = Task(...)
    task_list = [task_1, task_2]
    problem = EmployeeSchedulingProblem(timeslot_list, task_list, ...)
    solution = solver.solve(problem)
    # this is incorrect; it prints the timeslot_list of the original problem
    print(task_1.timeslot_list)
    

    It should be changed to this instead:

    solver = optapy.solver_factory_create(...).buildSolver()
    timeslot_list = [...]
    task_1 = Task(...)
    task_2 = Task(...)
    task_list = [task_1, task_2]
    problem = EmployeeSchedulingProblem(timeslot_list, task_list, ...)
    solution = solver.solve(problem)
    # this is correct; it prints the timeslot_list of the solution
    print(solution.task_list[0].timeslot_list)