A Simple Neural Network
KotlinConf 2018 - Mathematical Modeling
Creating a Sudoku Solver from Scratch
Traveling Salesman Problem
Text Categorization w/ Naive Bayes
Monty Hall Problem
Solving World's Hardest Sudoku

## Saturday, October 6, 2018

### Animating the Traveling Salesman Problem with JavaFX

Untitled Document.md

Animation can be a powerful tool. It is one thing to explain a complex topic in words or even in pictures, but visuals in motion have an amazing quality to bring abstract ideas to life. This can be especially helpful in complex areas of computer science like optimization and machine learning.

I recently gave a talk at KotlinConf on optimization and machine learning. Of the several examples, one was the Traveling Salesman Problem (a.k.a. “TSP”). This is such a fun and fascinating problem and it often serves as a benchmark for optimization and even machine learning algorithms. However, explaining some of the algorithms (like k-opt and simulated annealing) is less intuitive without a visual aid. Therefore, I made this an open-source project using JavaFX via TornadoFX.

A lot of folks at the conference and online were surprised by the animated visuals, remarking how “integrated” and slick it looked. The truth is I hacked this application together and JavaFX was instrumental in the heavy lifting. It took care of the animation for me so I could focus on the algorithm itself. This is what I want to blog about in this article.

You can watch the video walkthrough of this application (with a thorough explanation of the TSP) here. I recommend watching this before reading on!

The focus of this blog post will be on the animation and how it was achieved. For an in-depth explanation of the TSP and how it can be solved, please watch the above video.

## The Structure

To build this, let’s first lay out the structure of our visual framework. I am going to express this in the Kotlin language, and leverage JavaFX via TornadoFX. Thankfully, TornadoFX does not hide or supresss any of JavaFX’s functionality, but rather augments it with expressive Kotlin DSL’s. So you can do this in Java, Kotlin, Scala, or any JVM language that can use JavaFX.

The first thing I’m going to do in my application is declare a Pane, and place in it an ImageView with a simple map image of Europe. Then from my domain model, I’ll import my City objects and place a red Circle on the x and y screen coordinates relative to the Europe map. Finally, I’ll import the Edge objects from my domain where each one is tied to a City, and bind each one to a Line. Each Edge represents a connection between two cities, and it is initialized with the start point and end point being the same city. Therefore, the Line will initialize by resting inside the Circle as a little dot. The Line will also be bound to the startX, endX, startY, and endY properties of its owning Edge.

pane {
imageview(Image("europe.png")) {
fitHeight = 1000.0
fitWidth = 1000.0

CitiesAndDistances.cities.forEach { city ->
circle(city.x,city.y,10.0) {
fill = Color.RED
}
}

Model.edges.forEach { edge ->
line {
startXProperty().bind(edge.edgeStartX)
startYProperty().bind(edge.edgeStartY)
endXProperty().bind(edge.edgeEndX)
endYProperty().bind(edge.edgeEndY)
strokeWidth = 3.0
stroke = Color.RED
}
}
}
}


At this point, I should have something that renders like this:

When we animate, we will change each Edge’s startX, endX, startY, and endY properties. When we want to connect two cities, for instance, I can change the endX and endY properties to make that line extend to that other city’s coordinates.

## Planning the Animation

With this structure in place, I did have to make a few considerations next. Should I animate the algorithm in live time or queue up the animations and make them replayable? Did I want to animate every single thing the algorithm did or filter out the noise and animate only the key events?

These decisions may seem unimportant at first glance, and I even told myself “why not animate everything?”. Of course, this backfired quickly because the animations already slow down the algorithm… and animating unproductive events in the algorithm only added noise. This also made the animation incredingly long and boring.

So what are unproductive events, you ask? Well, the algorithm works by doing thousands of random Edge swaps as explained in the video. When the swap did not improve the solution (or it failed the coin flip in the Simulated Annealing approach), I would undo the swap and put everything back. I learned it was best to not animate these events because most iterations were failed swaps, and it was better to animate successes to show progress rather than every iteration including failures.

Another adapatation I ultimtely made is running the algorithm first, and then animating the results. This had the benefit of being able to replay the results without having to run the entire process again. The key utility I needed in the JavaFX library is the SequentialTransition, which allows me to queue up animations and have them played in order (rather than all at once). I can then have my algorithm add animations to the SequentialTransition and it can be played later when it is done.

I stored each algorithm (“GREEDY”, “TWO-OPT”, “SIMULATED_ANNEALING”, etc) as an enumerable so I gave each one its own SequentialTransition. I also created some convenient extension functions so I could use += operators to add animations.

enum class SearchStrategy {

RANDOM {
...
},

GREEDY {
...
},

REMOVE_OVERLAPS {
...
},
TWO_OPT {
...
},

SIMULATED_ANNEALING {
...
}

val animationQueue = SequentialTransition()

abstract fun execute()
}

// extension functions for SequentialTransition
operator fun SequentialTransition.plusAssign(timeline: Timeline) { children += timeline }
fun SequentialTransition.clear() = children.clear()
operator fun SequentialTransition.plusAssign(other: SequentialTransition) { children.addAll(other) }



And of course, I set a speed as a constant that defines how long each animation frame takes.

// animation parameters
var speed = 200.millis



## Executing a Path Traversal

On the domain model side, I have Edgeitems that initially belong to one City. However, the startCity and endCity can be mutated and on each mutation, the Edge has an animateChange() function returning a deferred Timeline that will play that change.

But here is the interesting design decision I ended up doing. I created the edgeStartX, edgeStartY, edgeEndX, and edgeEndY to not be synchronized to their respective startCity and endCity. Rather, these are used purely for animation execution. When I decide to animate a change in the startCity or endCity, I call animateChange() to create a Timeline that animates the coordinate changes. It will take the current value in each JavaFX property holding the coordinate values, and animate it by gradually increasing/decreasing to the specified value in that amount of time (which is the speed of the KeyFrame).

Note though this Timeline does not execute, that is up to the function caller on how to use that animation.

class Edge(city: City) {

val startCityProperty = SimpleObjectProperty(city)
var startCity by startCityProperty

val endCityProperty = SimpleObjectProperty(city)
var endCity by endCityProperty

val distance get() = CitiesAndDistances.distances[CityPair(startCity.id, endCity.id)]?:0.0

// animated properties
val edgeStartX = SimpleDoubleProperty(startCity.x)
val edgeStartY = SimpleDoubleProperty(startCity.y)
val edgeEndX = SimpleDoubleProperty(startCity.x)
val edgeEndY = SimpleDoubleProperty(startCity.y)

fun animateChange() = timeline(play = false) {
keyframe(speed) {
keyvalue(edgeStartX, startCity?.x ?: 0.0)
keyvalue(edgeStartY, startCity?.y ?: 0.0)
keyvalue(edgeEndX, endCity?.x ?: 0.0)
keyvalue(edgeEndY, endCity?.y ?: 0.0)
keyvalue(Model.distanceProperty, Model.totalDistance)
}
}
}


This particular function is used to expand an Edge for the first time to another city, which happens in the GREEDY and RANDOM algorithms. Stiching these together in a sequence results in a path traversing slickly to create a round-trip. Here is how the animateChange() function is leveraged in the RANDOM algorithm. Note how when I traverse to each random City, I connect each consecutive Edge pairs by their startcity and endCity respectively. Then I call animateChange() to return a Timeline and add it to the animationQueue.

RANDOM {
override fun execute() {
animationQueue.clear()

val capturedCities = mutableSetOf<Int>()

val startingEdge = Model.edges.sample()
var edge = startingEdge

while(capturedCities.size < CitiesAndDistances.cities.size) {
capturedCities += edge.startCity.id

val nextRandom = Model.edges.asSequence()
.filter { it.startCity.id !in capturedCities }
.sampleOrNull()?:startingEdge

edge.endCity = nextRandom.startCity
animationQueue += edge.animateChange()
edge = nextRandom
}

Model.bestDistanceProperty.set(Model.totalDistance)
}
}


My UI can then call animationQueue.play() to execute that change when the green play button is pressed.

## Executing a Swap

Swaps are a bit more tricky than animating a path traversal. When TWO_OPT or SIMULATED_ANNEALING algorithms select random edges and try to swap their cities (vertices) somehow, sometimes it will fail and sometimes it will succeed. A failure can happen if a swap breaks the tour, and the reverse() function will be called. If it is successful, an animate() function can be called and return a Timeline that waits to be queued or executed.


class TwoSwap(val city1: City,
val city2: City,
val edge1: Edge,
val edge2: Edge
) {

fun execute() {
edge1.let { sequenceOf(it.startCityProperty, it.endCityProperty) }.first { it.get() == city1 }.set(city2)
edge2.let { sequenceOf(it.startCityProperty, it.endCityProperty) }.first { it.get() == city2 }.set(city1)
}
fun reverse() {
edge1.let { sequenceOf(it.startCityProperty, it.endCityProperty) }.first { it.get() == city2 }.set(city1)
edge2.let { sequenceOf(it.startCityProperty, it.endCityProperty) }.first { it.get() == city1 }.set(city2)
}

fun animate() = timeline(play = false) {
keyframe(speed) {
sequenceOf(edge1,edge2).forEach {
keyvalue(it.edgeStartX, it.startCity?.x ?: 0.0)
keyvalue(it.edgeStartY, it.startCity?.y ?: 0.0)
keyvalue(it.edgeEndX, it.endCity?.x ?: 0.0)
keyvalue(it.edgeEndY, it.endCity?.y ?: 0.0)
}
}
keyframe(1.millis) {
sequenceOf(edge1,edge2).forEach {
keyvalue(Model.distanceProperty, Model.totalDistance)
}
}
}

}

fun attemptTwoSwap(otherEdge: Edge): TwoSwap? {

val e1 = this
val e2 = otherEdge

val startCity1 = startCity
val endCity1 = endCity
val startCity2 = otherEdge.startCity
val endCity2 = otherEdge.endCity

return sequenceOf(
TwoSwap(startCity1, startCity2, e1, e2),
TwoSwap(endCity1, endCity2, e1, e2),

TwoSwap(startCity1, endCity2, e1, e2),
TwoSwap(endCity1, startCity2, e1, e2)

).filter {
it.edge1.startCity !in it.edge2.let { setOf(it.startCity, it.endCity) } &&
it.edge1.endCity !in it.edge2.let { setOf(it.startCity, it.endCity) }
}
.firstOrNull { swap ->
swap.execute()
val result = Model.tourMaintained
if (!result) {
swap.reverse()
}
result
}
}



This can then be used for the TWO_OPT and SIMULATED_ANNEALING algorithms. Note that for both these algorithms I start by cleaning the animationQueue, execute the RANDOM algorithm and take all of its animations, and add them to this algorithm’s animations. For the TWO_OPT, I then attempt 2000 random swaps and only add animations that improve the distance of the tour. Otehrwise I call reverse() and do not animate the swap (as if it never happened).

TWO_OPT {
override fun execute() {
animationQueue.clear()
SearchStrategy.RANDOM.execute()
animationQueue += SearchStrategy.RANDOM.animationQueue

(1..2000).forEach { iteration ->
Model.edges.sampleDistinct(2).toList()
.let { it.first() to it.last() }
.also { (e1,e2) ->

val oldDistance = Model.totalDistance
e1.attemptTwoSwap(e2)?.also {
when {
oldDistance <= Model.totalDistance -> it.reverse()
oldDistance > Model.totalDistance -> animationQueue += it.animate()
}
}
}
}
Model.distanceProperty.set(Model.totalDistance)
Model.bestDistanceProperty.set(Model.totalDistance)

fun addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { model.addExpression(it) }  We are going to take advantage of Java 8’s great LocalDate/LocalTime API to make calendar work easier. Let’s set up our core parameters like so: import java.time.LocalDate import java.time.LocalTime // Any Monday through Friday date range will work val operatingDates = LocalDate.of(2017,10,16)..LocalDate.of(2017,10,20) val operatingDay = LocalTime.of(8,0)..LocalTime.of(17,0) val breaks = listOf<ClosedRange<LocalTime>>( LocalTime.of(11,30)..LocalTime.of(13,0) ) // classes val scheduledClasses = listOf( ScheduledClass(id=1, name="Psych 101",hoursLength=1.0, repetitions=2), ScheduledClass(id=2, name="English 101", hoursLength=1.5, repetitions=3), ScheduledClass(id=3, name="Math 300", hoursLength=1.5, repetitions=2), ScheduledClass(id=4, name="Psych 300", hoursLength=3.0, repetitions=1), ScheduledClass(id=5, name="Calculus I", hoursLength=2.0, repetitions=2), ScheduledClass(id=6, name="Linear Algebra I", hoursLength=2.0, repetitions=3), ScheduledClass(id=7, name="Sociology 101", hoursLength=1.0, repetitions=2), ScheduledClass(id=8, name="Biology 101", hoursLength=1.0, repetitions=2) ) data class ScheduledClass(val id: Int, val name: String, val hoursLength: Double, val repetitions: Int, val repetitionGapDays: Int = 2)  The repetitionGapDays is the minimum number of days needed between each recurrence’s start time. For instance, since Psych 100 requires 2 repetitions and defaults to a 2-day gap, if the first class was on MONDAY at 8AM then the second repetition must be scheduled at least 2 days (48 hours) later, which is WEDNESDAY at 8AM. All classes will default to a 2-day gap. The Block class will represent each discrete 15-minute time period. We will use a Kotlin Sequence in combination with Java 8’s LocalDate/LocalTime API to generate all of them for the entire planning window. We will also create a few helper properties to extract the timeRange as well as whether it is withinOperatingDay. The withinOperatingDay property will determine if this Block is within an operating day. data class Block(val dateTimeRange: ClosedRange<LocalDateTime>) { val timeRange = dateTimeRange.let { it.start.toLocalTime()..it.endInclusive.toLocalTime() } /** indicates if this block is zeroed due to operating day/break constraints */ val withinOperatingDay get() = breaks.all { timeRange.start !in it } && timeRange.start in operatingDay && timeRange.endInclusive in operatingDay companion object { // Operating blocks val all by lazy { generateSequence(operatingDates.start.atStartOfDay()) { it.plusMinutes(15).takeIf { it.plusMinutes(15) <= operatingDates.endInclusive.atTime(23,59) } }.map { Block(it..it.plusMinutes(15)) } .toList() } } }  Note I am going to initialize items for each domain object using a lazy { } delegate. This is to prevent circular construction issues. Finally, the Slot class will represent an intersection between a ScheduledClass and a Block. We will generate all of them by pairing every ScheduledClass with every Block. We will also create a binary() ojAlgo variable which will be fixed to 0 if the Block is not within the operating day. data class Slot(val block: Block, val scheduledClass: ScheduledClass) { val occupied = variable().apply { if (block.withinOperatingDay) binary() else level(0) } companion object { val all by lazy { Block.all.asSequence().flatMap { b -> ScheduledClass.all.asSequence().map { Slot(b,it) } }.toList() } } }  ## Coming Up with a Model In the first article in this series, I showed an approach to capture the necessary contiguous blocks for a given session. I found this approach to scale poorly with ojAlgo, although there are changes in the upcoming release (support for ordered sets) that might work with this approach. I could also drop in a$10K CPLEX license which also might execute a solve quickly.

But I like things to remain free and open-source where possible, so I concentrated hard and came up with a better mathematical model. It is highly abstract but powerful and effective for this particular problem.

Again, we are going to label each Slot as 1 or 0 to indicate the start of the first class repetition. Here is one possible iteration the solver may come up with, where the first Psych 101 class starts on MON 9:00AM and Sociology 101 starts on MON 9:45AM. Here it is on our grid:

Study this scenario closely. Do you see a pattern for an invalid case? In the MON 9:45AM block, Psych 101 (which requires four blocks) and Sociology 101 (which also requires four blocks) are in conflict with each other. Visually, you might be able to see the conflict. But how do you describe it?

The sum of scheduled class blocks that “affect” the 9:45AM block must be less than or equal to 1. A sum of 1 effectively means only one class is taking up that block, and 0 means no classes are occupying that block at all (also valid). This particular case fails because the sum of “affecting” blocks is 2.

If we shifted Sociology 101 to 10:00AM, the sum would then be 1 and all is good.

We need to apply this logic to every block across the entire timeline, querying for earlier slots for each class that occupy this block, and dictate their sum must be no greater than 1. This abstract but powerful idea achieves everything we need. Here is what this looks like in practice below, where all slots affecting the 9:45AM block are highlighted in blue. All of these blue blocks must sum to no more than 1.

This can even account for the recurrences too. After all, we put a 1 in a slot to indicate the candidate start time of the first class. If we were looking at the 9:45AM block on Friday, we would query for time slots earlier in the week that would result in this 9:45AM Friday block being occupied (all they way to Monday). Here is a wide visual below. The sum of these blue slots must be no greater than 1.

Okay is your head spinning yet? The power of this model is not so much the math, but the ability for each block to query the slots that impact it and mandate they must sum to no more than 1. That is where the hard work will happen, and Kotlin’s stdlib can nail this effectively. The benefit is we do not have create any new variables, and can constrain the existing slot binary variables with a series of simple sum constraints.

## Extracting Recurrences and Affected Slots

Wrangling and transforming data is tedious, and it is the unglamorous part of data science where 90% of the work occurs. It is for this reason Python has rapidly overtook R, but I think Kotlin can serve us type safety-minded folks who also appreciate extensibility and higher-order functions.

What we need to do first is identify the “groups” of slots for each class, and by “group” I mean an entire set of recurrences across the week. The star of this codebase is is going to be this Kotlin extension function, which will accomplish just that:

fun <T> List<T>.rollingRecurrences(slotsNeeded: Int, gap: Int, recurrences: Int) =
(0..size).asSequence().map { i ->
(1..recurrences).asSequence().map { (it - 1) * gap }
.filter { it + i < size}
.map { r ->
subList(i + r, (i + r + slotsNeeded).let { if (it > size) size else it })
}.filter { it.size == slotsNeeded }
.toList()
}.filter { it.size == recurrences }


I will let you dive deep into the implementation on your own later. For now it is more productive to cover what it accomplishes, which is take any List<T> and perform a specialized windowed() operation that injects a gap between each grouping. Note the gap is the number of items between each start of the window. For instance, we can take the numbers 1…20 and break them up in groups of 4, with a gap of 6 between each recurrence start, and have 3 recurrences.

fun main(args: Array<String>) {

(1..20).toList().rollingRecurrences(slotsNeeded = 4, gap = 6, recurrences = 3)
.forEach { println(it) }
}


OUTPUT:

[[1, 2, 3, 4], [7, 8, 9, 10], [13, 14, 15, 16]]
[[2, 3, 4, 5], [8, 9, 10, 11], [14, 15, 16, 17]]
[[3, 4, 5, 6], [9, 10, 11, 12], [15, 16, 17, 18]]
[[4, 5, 6, 7], [10, 11, 12, 13], [16, 17, 18, 19]]
[[5, 6, 7, 8], [11, 12, 13, 14], [17, 18, 19, 20]]


We can use this extension function to handle the class repetitions, and generate all possible permutations within our time planning window of one week. We can then use that to find slots for a particular class that affect a particular block, as implemented with our affectingSlotsFor() function shown below. We will also set our constraints dictating

data class ScheduledClass(val id: Int,
val name: String,
val hoursLength: Double,
val repetitions: Int,
val repetitionGapDays: Int = 2) {

/** the # of slots between each recurrence */
val gapLengthInSlots = repetitionGapDays * 24 * 4

/** the # of slots needed for a given recurrence */
val slotsNeeded = (hoursLength * 4).toInt()

/** yields slots for this given scheduled class */
val slots by lazy {
Slot.all.asSequence().filter { it.scheduledClass == this }.toList()
}

/** yields slot groups for this scheduled class */
val slotGroups by lazy {
slots.rollingRecurrences(slotsNeeded = slotsNeeded, gap = gapLengthInSlots, recurrences = repetitions)
}

/** yields slots that affect the given block for this scheduled class */
fun affectingSlotsFor(block: Block) = slotGroups.asSequence()
.filter { it.flatMap { it }.any { it.block == block } }
.map { it.first().first() }

companion object {
val all by lazy { scheduledClasses }
}
}


To finish this off, let’s implement the needed constraints in the ScheduledClass with a addConstraints() function. We will set the sum of slots for each given class must be 1, so that at least one instance is scheduled. We will also limit the model exploring solutions for classes that have 3 repetitions, and say the start of the first class must be on MONDAY for those cases. For 2 repetitions, we will specify the first class must start on MONDAY, WEDNESDAY, or FRIDAY. We will achieve these by saying the sum in these regions must be 1.

We will also create start and end properties that will translate the model’s optimized slots (where one slot is 1), and translate it back to a LocalDateTime.

data class ScheduledClass(val id: Int,
val name: String,
val hoursLength: Double,
val repetitions: Int,
val repetitionGapDays: Int = 2) {

/** the # of slots between each recurrence */
val gapLengthInSlots = repetitionGapDays * 24 * 4

/** the # of slots needed for a given occurrence */
val slotsNeeded = (hoursLength * 4).toInt()

/** yields slots for this given scheduled class */
val slots by lazy {
Slot.all.asSequence().filter { it.scheduledClass == this }.toList()
}

/** yields slot groups for this scheduled class */
val slotGroups by lazy {
slots.rollingRecurrences(slotsNeeded = slotsNeeded, gap = gapLengthInSlots, recurrences = repetitions)
}

/** yields slots that affect the given block for this scheduled class */
fun affectingSlotsFor(block: Block) = slotGroups.asSequence()
.filter { it.flatMap { it }.any { it.block == block } }
.map { it.first().first() }

/** translates and returns the optimized start time of the class */
val start get() = slots.asSequence().filter { it.occupied.value.toInt() == 1 }.map { it.block.dateTimeRange.start }.min()!!

/** translates and returns the optimized end time of the class */
val end get() = start.plusMinutes((hoursLength * 60.0).toLong())

/** returns the DayOfWeeks where recurrences take place */
val daysOfWeek get() = (0..(repetitions-1)).asSequence().map { start.dayOfWeek.plus(it.toLong() * repetitionGapDays) }.sorted()

//sum of all slots for this scheduledClass must be 1
// s1 + s2 + s3 .. + sn = 1
slots.forEach {
set(it.occupied, 1)
}
}

// Guide Mon/Wed/Fri for three repetitions
// If 3 repetitions are needed, the sum of slots on Monday must be 1
if (repetitions == 3) {
slots.filter { it.block.dateTimeRange.start.dayOfWeek == DayOfWeek.MONDAY }
.forEach {
set(it.occupied, 1)
}
}
}

// Guide two repetitions to start on Mon, Tues, or Wed
// If 2 repetitions are needed, the sum of slots on Monday, Tuesday, and Wednesday must be 1

if (repetitions == 2) {
slots.filter { it.block.dateTimeRange.start.dayOfWeek in DayOfWeek.MONDAY..DayOfWeek.WEDNESDAY }.forEach {
set(it.occupied, 1)
}
}
}
}

companion object {
val all by lazy { scheduledClasses }
}
}


lNow going back to the Block class, I will add an addConstraints() function. It will query all the affecting blocks for each ScheduledClass and say they must all sum to no more than 1. This ensures no overlap between classes will occur. But if a block is not within an operating day, not only should its slots be fixed to 0, but all of its affecting slots should be fixed to 0.

/** A discrete, 15-minute chunk of time a class can be scheduled on */
data class Block(val dateTimeRange: ClosedRange<LocalDateTime>) {

val timeRange = dateTimeRange.let { it.start.toLocalTime()..it.endInclusive.toLocalTime() }

/** indicates if this block is zeroed due to operating day/break constraints */
val withinOperatingDay get() =  breaks.all { timeRange.start !in it } &&
timeRange.start in operatingDay &&
timeRange.endInclusive in operatingDay

if (withinOperatingDay) {
ScheduledClass.all.asSequence().flatMap { it.affectingSlotsFor([email protected]) }
.forEach {
set(it.occupied, 1)
}
}
} else {
ScheduledClass.all.asSequence().flatMap { it.affectingSlotsFor([email protected]) }
.forEach {
it.occupied.level(0)
}
}
}

companion object {

/* All operating blocks for the entire week, broken up in 15 minute increments */
val all by lazy {
generateSequence(operatingDates.start.atStartOfDay()) {
it.plusMinutes(15).takeIf { it.plusMinutes(15) <= operatingDates.endInclusive.atTime(23,59) }
}.map { Block(it..it.plusMinutes(15)) }
.toList()
}

fun applyConstraints() {
}
}
}


Here is the Kotlin code in its entirety, where the respective addConstraints() functions are invoked and the results are iterated. You can also get this code here on GitHub.

import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.concurrent.atomic.AtomicInteger

// Any Monday through Friday date range will work
val operatingDates = LocalDate.of(2017,10,16)..LocalDate.of(2017,10,20)
val operatingDay = LocalTime.of(8,0)..LocalTime.of(17,0)

val breaks = listOf<ClosedRange<LocalTime>>(
LocalTime.of(11,30)..LocalTime.of(13,0)
)

// classes
val scheduledClasses = listOf(
ScheduledClass(id=1, name="Psych 101",hoursLength=1.0, repetitions=2),
ScheduledClass(id=2, name="English 101", hoursLength=1.5, repetitions=3),
ScheduledClass(id=3, name="Math 300", hoursLength=1.5, repetitions=2),
ScheduledClass(id=4, name="Psych 300", hoursLength=3.0, repetitions=1),
ScheduledClass(id=5, name="Calculus I", hoursLength=2.0, repetitions=2),
ScheduledClass(id=6, name="Linear Algebra I", hoursLength=2.0, repetitions=3),
ScheduledClass(id=7, name="Sociology 101", hoursLength=1.0, repetitions=2),
ScheduledClass(id=8, name="Biology 101", hoursLength=1.0, repetitions=2)
)

fun main(args: Array<String>) {

println("Job started at ${LocalTime.now()}") applyConstraints() model.countVariables().run { println("$this variables") }

model.options.apply {
iterations_suffice = 0
}

println(model.minimise())

ScheduledClass.all.forEach {
println("${it.name}-${it.daysOfWeek.joinToString("/")} ${it.start.toLocalTime()}-${it.end.toLocalTime()}")
}

println("Job ended at ${LocalTime.now()}") } // declare model val model = ExpressionsBasedModel() // improvised DSL val funcId = AtomicInteger(0) val variableId = AtomicInteger(0) fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)

/** A discrete, 15-minute chunk of time a class can be scheduled on */
data class Block(val dateTimeRange: ClosedRange<LocalDateTime>) {

val timeRange = dateTimeRange.let { it.start.toLocalTime()..it.endInclusive.toLocalTime() }

/** indicates if this block is zeroed due to operating day/break constraints */
val withinOperatingDay get() =  breaks.all { timeRange.start !in it } &&
timeRange.start in operatingDay &&
timeRange.endInclusive in operatingDay

if (withinOperatingDay) {
ScheduledClass.all.asSequence().flatMap { it.affectingSlotsFor([email protected]) }
.filter { it.block.withinOperatingDay }
.forEach {
set(it.occupied, 1)
}
}
} else {
ScheduledClass.all.asSequence().flatMap { it.affectingSlotsFor([email protected]) }
.forEach {
it.occupied.level(0)
}
}
}

companion object {

/* All operating blocks for the entire week, broken up in 15 minute increments */
val all by lazy {
generateSequence(operatingDates.start.atStartOfDay()) {
it.plusMinutes(15).takeIf { it.plusMinutes(15) <= operatingDates.endInclusive.atTime(23,59) }
}.map { Block(it..it.plusMinutes(15)) }
.toList()
}

fun applyConstraints() {
}
}
}

data class ScheduledClass(val id: Int,
val name: String,
val hoursLength: Double,
val repetitions: Int,
val repetitionGapDays: Int = 2) {

/** the # of slots between each recurrence */
val gapLengthInSlots = repetitionGapDays * 24 * 4

/** the # of slots needed for a given occurrence */
val slotsNeeded = (hoursLength * 4).toInt()

/** yields slots for this given scheduled class */
val slots by lazy {
Slot.all.asSequence().filter { it.scheduledClass == this }.toList()
}

/** yields slot groups for this scheduled class */
val slotGroups by lazy {
slots.rollingRecurrences(slotsNeeded = slotsNeeded, gap = gapLengthInSlots, recurrences = repetitions)
}

/** yields slots that affect the given block for this scheduled class */
fun affectingSlotsFor(block: Block) = slotGroups.asSequence()
.filter { it.flatMap { it }.any { it.block == block } }
.map { it.first().first() }

/** translates and returns the optimized start time of the class */
val start get() = slots.asSequence().filter { it.occupied.value.toInt() == 1 }.map { it.block.dateTimeRange.start }.min()!!

/** translates and returns the optimized end time of the class */
val end get() = start.plusMinutes((hoursLength * 60.0).toLong())

/** returns the DayOfWeeks where recurrences take place */
val daysOfWeek get() = (0..(repetitions-1)).asSequence().map { start.dayOfWeek.plus(it.toLong() * repetitionGapDays) }.sorted()

//sum of all slots for this scheduledClass must be 1
// s1 + s2 + s3 .. + sn = 1
slots.forEach {
set(it.occupied, 1)
}
}

// Guide Mon/Wed/Fri for three repetitions
// If 3 repetitions are needed, the sum of slots on Monday must be 1
if (repetitions == 3) {
slots.filter { it.block.dateTimeRange.start.dayOfWeek == DayOfWeek.MONDAY }
.forEach {
set(it.occupied, 1)
}
}
}

// Guide two repetitions to start on Mon, Tues, or Wed
// If 2 repetitions are needed, the sum of slots on Monday, Tuesday, and Wednesday must be 1

if (repetitions == 2) {
slots.filter { it.block.dateTimeRange.start.dayOfWeek in DayOfWeek.MONDAY..DayOfWeek.WEDNESDAY }.forEach {
set(it.occupied, 1)
}
}
}
}

companion object {
val all by lazy { scheduledClasses }
}
}

data class Slot(val block: Block, val scheduledClass: ScheduledClass) {
val occupied = variable().apply { if (block.withinOperatingDay) binary() else level(0) }

companion object {

val all by lazy {
Block.all.asSequence().flatMap { b ->
ScheduledClass.all.asSequence().map { Slot(b,it) }
}.toList()
}
}
}

fun applyConstraints() {
Block.applyConstraints()
}

fun <T> List<T>.rollingBatches(batchSize: Int) = (0..size).asSequence().map { i ->
subList(i, (i + batchSize).let { if (it > size) size else it })
}.filter { it.size == batchSize }

fun <T> List<T>.rollingRecurrences(slotsNeeded: Int, gap: Int, recurrences: Int) =
(0..size).asSequence().map { i ->
(1..recurrences).asSequence().map { (it - 1) * gap }
.filter { it + i < size}
.map { r ->
subList(i + r, (i + r + slotsNeeded).let { if (it > size) size else it })
}.filter { it.size == slotsNeeded }
.toList()
}.filter { it.size == recurrences }


When I run this entire application, here are the scheduled classes!

Psych 101- WEDNESDAY/FRIDAY 10:30-11:30
English 101- MONDAY/WEDNESDAY/FRIDAY 13:15-14:45
Math 300- TUESDAY/THURSDAY 15:15-16:45
Psych 300- THURSDAY 08:15-11:15
Calculus I- TUESDAY/THURSDAY 13:15-15:15
Linear Algebra I- MONDAY/WEDNESDAY/FRIDAY 08:15-10:15
Sociology 101- WEDNESDAY/FRIDAY 16:00-17:00
Biology 101- WEDNESDAY/FRIDAY 15:00-16:00


If we were to plot this out visually, here is what the schedule looks like:

Hopefully you guys find this fascinating and useful. I will definitely post a few more articles on Kotlin for linear programming when I find some interesting use cases.