0

I'm working on a schedule optimization solution with Optapy.

I'm trying like to make a chained planned variable, which should include multiple types of actual entities in the chain. Something like this:

@planning_entity
class Slot:
  """ Schedule slot """

  def __init__(self):
      self.prev_slot = None

@planning_variable(Slot, value_range_provider_refs=['slot_range'],
                          graph_type=PlanningVariableGraphType.CHAINED)
def get_previous_slot(self):
    return self.prev_slot

def set_previous_slot(self, previous_slot: 'Slot'):
    self.prev_slot = previous_slot

@planning_entity
class Task(Slot):
    """ Allocation to task """
    ...

@planning_entity
class LunchBreak(Slot):
    """ Lunch break """
    ...

This doesn't work by various reasons. If I don't add any planning variable to derived classes, it fails saying I have to, but if I add them - I'm getting java exceptions like 'assignment error' or similar.

Is it actually possible to inherit one @planning_entity class from another in Optapy?

kol
  • 126
  • 1
  • 5
  • Sounds like a bug in the current version of OptaPy (which OptaPlanner doesn't suffer from). Would you mind creating an issue for it on the optapy github? – Geoffrey De Smet Sep 05 '22 at 10:30

1 Answers1

2

The issue is in this code:

@planning_entity
class Slot:
    """ Schedule slot """
    def __init__(self):
        self.prev_slot = None

    @planning_variable(Slot, value_range_provider_refs=['slot_range'],
                       graph_type=PlanningVariableGraphType.CHAINED)
    def get_previous_slot(self):
        return self.prev_slot

    def set_previous_slot(self, previous_slot: 'Slot'):
        self.prev_slot = previous_slot

Due to Python semantics, the class Slot is not available during it own definition. However, chained models / subclasses are possible in optapy. There is a bug in the current version of optapy (https://github.com/optapy/optapy/issues/101) that throws a ClassCastException if the @value_range_provider type does not EXACTLY match the @planning_variable type. This can be work around by using the most derived super class (which in this case, is Slot) in the @planning_solution. Additionally, there seem to be a problem with multiple @planning_entity classes in chained models that I'll investigate. To accomplish having multiple subclasses of planning entities (without new @planning_variables), the following code will work:

import optapy
from optapy.score import HardSoftScore
from optapy.types import PlanningVariableGraphType


@optapy.problem_fact
class Base:
    pass

@optapy.planning_entity
class Slot(Base):
    def __init__(self, value=None):
        self.value = value
        
    @optapy.planning_variable(Base, value_range_provider_refs=['employee_range', 'task_range', 'lunch_break_range'],
                              graph_type=PlanningVariableGraphType.CHAINED)
    def get_value(self):
        return self.value

    def set_value(self, value):
        self.value = value


@optapy.problem_fact
class Employee(Base): # chained models need an anchor
    def __init__(self, code):
        self.code = code


@optapy.planning_entity
class Task(Slot):
    def __init__(self, code, value=None):
        self.code = code
        self.value = value


@optapy.planning_entity
class LunchBreak(Slot):
    def __init__(self, code, value=None):
        self.code = code
        self.value = value


@optapy.planning_solution
class Solution:
    def __init__(self, employees, tasks, lunch_breaks, score=None):
        self.employees = employees
        self.tasks = tasks
        self.lunch_breaks = lunch_breaks
        self.score = score
    
    @optapy.problem_fact_collection_property(Slot)
    @optapy.value_range_provider('employee_range')
    def get_employees(self):
        return self.employees
    
    @optapy.planning_entity_collection_property(Slot)
    @optapy.value_range_provider('task_range')
    def get_tasks(self):
        return self.tasks
    
    @optapy.planning_entity_collection_property(Slot)
    @optapy.value_range_provider('lunch_break_range')
    def get_lunch_breaks(self):
        return self.lunch_breaks

    @optapy.planning_score(HardSoftScore)
    def get_score(self):
        return self.score

    def set_score(self, score):
        self.score = score



def build_problem():
    employees = [
        Employee('Amy'),
        Employee('Beth')
    ]
    tasks = [
        Task('T1'),
        Task('T2'),
        Task('T3')
    ]
    lunch_breaks = [
        LunchBreak('L1'),
        LunchBreak('L2')
    ]
    return Solution(employees, tasks, lunch_breaks)

And to create the solver:

import optapy
import optapy.config
from optapy.types import Duration
from domain import Solution, Slot, build_problem
from constraints import define_constraints

solver_config = (optapy.config.solver.SolverConfig()
    .withSolutionClass(Solution)
    .withEntityClasses(Slot)
    .withConstraintProviderClass(define_constraints) 
    .withTerminationSpentLimit(Duration.ofSeconds(10))
    )

solver_factory = optapy.solver_factory_create(solver_config)
solver = solver_factory.buildSolver()
solver.solve(build_problem())
Christopher Chianelli
  • 1,163
  • 1
  • 8
  • 8
  • Hi, thanks for your response, but I'm getting `java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: java.lang.ClassCastException`. This is how I set up `slot_range` provider, is it correct (sorry for formatting)? `slots: list[Slot] @planning_entity_collection_property(Slot) @value_range_provider('slot_range') def get_slot_range(self): return self.slots ` – kol Sep 06 '22 at 06:15
  • I was able to replicate it; seem to be a bug in `optapy` where if the `@value_range_provider` does not have the same type as the `planning_variable` (i.e. if `@planning_variable` is of type Slot, then `@value_range_provider` must also be of type Slot, not Task (which is a subclass of Slot). This is a bug that I'll look into; I'll update the answer with a workaround. – Christopher Chianelli Sep 06 '22 at 15:38