Skip to content

Instantly share code, notes, and snippets.

@holgerbrandl
Last active May 29, 2024 07:48
Show Gist options
  • Save holgerbrandl/f3cc4c96477b14d0c165f90933715cef to your computer and use it in GitHub Desktop.
Save holgerbrandl/f3cc4c96477b14d0c165f90933715cef to your computer and use it in GitHub Desktop.
A small self-contained example showcasing the broken explain API in TF v1.10
package com.systema.experiments
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.SolutionManager
import ai.timefold.solver.core.api.solver.SolverFactory
import ai.timefold.solver.core.config.solver.SolverConfig
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"])
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()
}
@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")
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)
}
fun <T, Solution_> ScoreDirector<Solution_>.change(name: String, task: T, block: T.(T) -> Unit) {
beforeVariableChanged(task, name)
task.block(task)
afterVariableChanged(task, name)
}
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 {
// no constraints are needed to reproduce the problem
override fun defineConstraints(constraintFactory: ConstraintFactory): Array<Constraint> = arrayOf()
}
fun main() {
val scheduleStart = LocalDate(2024, 1, 1)
val rooms = listOf(
Room("Room A", scheduleStart.atStartOfDayIn(TimeZone.UTC)),
)
val courses = listOf(
Course("C1", 5, 1.days),
Course("C2", 10, 1.days),
Course("C3", 5, 1.days),
)
val unsolved = CourseSchedule(courses, rooms)
// initialize the problem manually by passing the heuristic
(rooms[0].roomSchedule as MutableList).addAll(courses)
// simply recompute schedule without solving
val solverConfig = SolverConfig()
.withSolutionClass(CourseSchedule::class.java)
.withEntityClasses(Course::class.java, Room::class.java)
.withConstraintProviderClass(CourseConstraints::class.java)
val solverFactory = SolverFactory.create<CourseSchedule>(solverConfig)
val explain = SolutionManager.create(solverFactory).explain(unsolved)
// println(explain) --> does not matter here
// print schedule with updated shadow variables
println(unsolved.rooms[0].roomSchedule)
// problem: first planning entity is not recomputed
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment