Last active
May 29, 2024 07:48
-
-
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
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
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