Last active
May 7, 2024 11:39
-
-
Save holgerbrandl/df80f0f13b93afec5cfa8d3518da8db4 to your computer and use it in GitHub Desktop.
CourseListSchedule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ai.timefold.solver.core.api.domain.entity.PlanningEntity | |
import ai.timefold.solver.core.api.domain.lookup.PlanningId | |
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty | |
import ai.timefold.solver.core.api.domain.solution.PlanningScore | |
import ai.timefold.solver.core.api.domain.solution.PlanningSolution | |
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider | |
import ai.timefold.solver.core.api.domain.variable.* | |
import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore | |
import ai.timefold.solver.core.api.score.director.ScoreDirector | |
import ai.timefold.solver.core.api.score.stream.Constraint | |
import ai.timefold.solver.core.api.score.stream.ConstraintFactory | |
import ai.timefold.solver.core.api.score.stream.ConstraintProvider | |
import ai.timefold.solver.core.api.solver.SolverFactory | |
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig | |
import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig | |
import ai.timefold.solver.core.config.solver.SolverConfig | |
import ai.timefold.solver.core.config.solver.termination.TerminationCompositionStyle | |
import ai.timefold.solver.core.config.solver.termination.TerminationConfig | |
import kotlinx.datetime.Instant | |
import kotlinx.datetime.LocalDate | |
import kotlinx.datetime.TimeZone | |
import kotlinx.datetime.atStartOfDayIn | |
import kotlin.properties.Delegates | |
import kotlin.time.Duration | |
import kotlin.time.Duration.Companion.days | |
@PlanningEntity | |
class Room { | |
@get:PlanningId | |
var id: String? = null | |
protected set | |
// @PlanningListVariable(valueRangeProviderRefs = ["courses"]) // workardound from trieco | |
@PlanningListVariable | |
val roomSchedule: List<Course> = mutableListOf() | |
constructor(name: String, availableFrom: Instant) { | |
this.id = name | |
this.availableFrom = availableFrom | |
} | |
lateinit var availableFrom: Instant | |
@Suppress("unused") | |
constructor() | |
} | |
@PlanningEntity | |
open class Course { | |
@get:PlanningId | |
var id: String? = null | |
protected set | |
@InverseRelationShadowVariable(sourceVariableName = "roomSchedule") | |
var room: Room? = null | |
@PreviousElementShadowVariable(sourceVariableName = "roomSchedule") | |
var prevCourse: Course? = null | |
@NextElementShadowVariable(sourceVariableName = "roomSchedule") | |
var nextCourse: Course? = null | |
constructor(title: String, numParticipants: Int, duration: Duration) { | |
id = title | |
this.numParticipants = numParticipants | |
this.duration = duration | |
} | |
var numParticipants: Int = 0 | |
var duration by Delegates.notNull<Duration>() | |
@ShadowVariable( | |
variableListenerClass = CourseVariablesListener::class, | |
sourceVariableName = "prevCourse" | |
) | |
var start: Instant? = null | |
@PiggybackShadowVariable(shadowVariableName = "start") | |
var end: Instant? = null | |
override fun toString() = "$id::$room($start-$end)" | |
@Suppress("unused") | |
constructor() | |
} | |
fun createCourseSolveConfig(): SolverConfig = | |
SolverConfig() | |
.withSolutionClass(CourseSchedule::class.java) | |
.withEntityClasses(Course::class.java, Room::class.java) | |
.withConstraintProviderClass(CourseConstraints::class.java) | |
.withPhases( | |
ConstructionHeuristicPhaseConfig() | |
.apply { | |
// constructionHeuristicType = ConstructionHeuristicType.FIRST_FIT | |
}, | |
LocalSearchPhaseConfig().apply { | |
terminationConfig = TerminationConfig().apply { | |
withStepCountLimit(100) | |
withTerminationCompositionStyle(TerminationCompositionStyle.OR) | |
withUnimprovedStepCountLimit(100) | |
} | |
} | |
) | |
@PlanningSolution | |
class CourseSchedule { | |
constructor(courses: List<Course>, rooms: List<Room>) { | |
this.courses = courses | |
this.rooms = rooms | |
} | |
@PlanningEntityCollectionProperty | |
lateinit var rooms: List<Room> | |
@PlanningEntityCollectionProperty | |
// @ValueRangeProvider(id="courses") // workardound from trieco | |
@ValueRangeProvider | |
lateinit var courses: List<Course> | |
@PlanningScore | |
var score: HardMediumSoftScore? = null | |
@Suppress("unused") | |
constructor() // needed by solver engine | |
} | |
class CourseVariablesListener : VariableListener<CourseSchedule, Course> { | |
override fun afterEntityAdded(scoreDirector: ScoreDirector<CourseSchedule>, entity: Course) { | |
afterVariableChanged(scoreDirector, entity) | |
} | |
override fun afterVariableChanged(scoreDirector: ScoreDirector<CourseSchedule>, rootCourse: Course) { | |
var course: Course? = rootCourse | |
while(course != null) { | |
scoreDirector.change(Course::start.name, course) { | |
start = prevCourse?.end ?: room?.availableFrom | |
} | |
scoreDirector.change(Course::end.name, course) { | |
end = start?.let { it + duration } | |
} | |
course = course.nextCourse | |
} | |
} | |
override fun beforeEntityAdded(scoreDirector: ScoreDirector<CourseSchedule>?, entity: Course?) { | |
} | |
override fun beforeEntityRemoved(scoreDirector: ScoreDirector<CourseSchedule>?, entity: Course?) { | |
} | |
override fun afterEntityRemoved(scoreDirector: ScoreDirector<CourseSchedule>?, entity: Course?) { | |
} | |
override fun beforeVariableChanged(scoreDirector: ScoreDirector<CourseSchedule>?, entity: Course?) { | |
} | |
} | |
open class CourseConstraints : ConstraintProvider { | |
override fun defineConstraints(constraintFactory: ConstraintFactory): Array<Constraint> = buildList { | |
add(minimizeMakespan(constraintFactory)) | |
}.toTypedArray() | |
// makespan minimization | |
fun minimizeMakespan(constraintFactory: ConstraintFactory) = constraintFactory | |
.forEach(Course::class.java) | |
.filter { it.end != null } // course is scheduled | |
.penalize(HardMediumSoftScore.ONE_SOFT) { course -> | |
(course.end!! - course.room?.availableFrom!!).inWholeDays.toInt() | |
} | |
.asConstraint("minimize-makespan") | |
} | |
fun main() { | |
val scheduleStart = LocalDate(2024, 1, 1) | |
val rooms = listOf( | |
Room("Room A", scheduleStart.asInstant()), | |
Room("Room B", scheduleStart.asInstant()), | |
Room("Room C", scheduleStart.asInstant()), | |
) | |
// val partDist = UniformIntegerDistribution(JDKRandomGenerator(42), 10, 10) | |
// val durationDist = UniformIntegerDistribution(JDKRandomGenerator(42), 10, 10) | |
val courses = listOf( | |
Course("C1", 5, 1.days), | |
Course("C2", 10, 1.days), | |
Course("C3", 5, 1.days), | |
Course("C4", 5, 1.days), | |
Course("C5", 5, 1.days), | |
Course("C6", 5, 2.days), | |
) | |
val unsolved = CourseSchedule(courses, rooms) | |
val solverFactory = SolverFactory.create<CourseSchedule>(createCourseSolveConfig()) | |
val solved = solverFactory.buildSolver().solve(unsolved) | |
// solved.analyzeSchedule(solverFactory) | |
// solved.show() | |
// solved.printSchedule() | |
} | |
fun <T, Solution_> ScoreDirector<Solution_>.change(name: String, task: T, block: T.(T) -> Unit) { | |
beforeVariableChanged(task, name) | |
task.block(task) | |
afterVariableChanged(task, name) | |
} | |
fun LocalDate.asInstant(): Instant = atStartOfDayIn(TimeZone.UTC) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment