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())