Skip to content

Instantly share code, notes, and snippets.

@holgerbrandl
Last active May 7, 2024 11:39
Show Gist options
  • Save holgerbrandl/df80f0f13b93afec5cfa8d3518da8db4 to your computer and use it in GitHub Desktop.
Save holgerbrandl/df80f0f13b93afec5cfa8d3518da8db4 to your computer and use it in GitHub Desktop.
CourseListSchedule.kt
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