I'm kind of new to linear optimization, and I want to apply it to classic scheduling problems. For staffing problems I'm not quite clear how to declare functions that capture the concept of a "shift" being taken.
I'm using ojAlgo which has been pretty awesome so far. Here is my contrived little problem I came up with:
SCENARIO:
You have three drivers to make deliveries.
Driver 1 costs $10 / hr
Driver 2 costs $12 / hr
Driver 3 costs $14 / hr
Each driver can only work 3-6 hours a day.
Only one shift can be worked by a worker a day.
Operating day is 6:00 to 22:00, which must be fully covered.
Driver 2 cannot work after 11:00.
Create a schedule that minimizes the cost.
Solve Variables:
Tsx = shift start for Driver X
Tex = shift end for Driver X
Minimize:
10(Te1 - Ts1) + 12(Te2 - Ts2) + 14(Te3 - Ts3)
10Te1 - 10Te2 + 12Te2 - 12Ts2 + 14Te3 - 14Ts3
Constraints:
4.0 <= Te - Ts <= 6.0
6.0 <= Ts, Te <= 22.0
(Te1 - Ts1) + (Te2 - Ts2) + (Te3 - Ts3) = (22.0 - 6.0)
Te2 <= 11
Here is the Kotlin code I put together. I found it easier for each Driver
instance to take care of as many of its function inputs as possible (which yielded some interesting patterns with OOP).
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
fun main(args: Array<String>) {
val model = ExpressionsBasedModel()
val drivers = sequenceOf(
Driver(1, 10.0, model),
Driver(2, 12.0, model),
Driver(3, 14.0, model)
).map { it.driverNumber to it }
.toMap()
model.addExpression("EnsureCoverage")
.level(16.0)
.apply {
drivers.values.forEach {
set(it.shiftEnd, 1)
set(it.shiftStart, -1)
}
}
model.addExpression("Driver2OffAt11")
.upper(11)
.set(drivers[1]!!.shiftEnd, 1)
val result = model.minimise()
println(result)
}
data class Driver(val driverNumber: Int,
val rate: Double,
val model: ExpressionsBasedModel) {
val shiftStart = Variable.make("${driverNumber}shiftStart").weight(rate).lower(6).upper(22).apply(model::addVariable)
val shiftEnd = Variable.make("${driverNumber}shiftEnd").weight(rate).lower(6).upper(22).apply(model::addVariable)
init {
model.addExpression("${driverNumber}shiftLength")
.lower(4.0)
.upper(6.0)
.set(shiftEnd, 1)
.set(shiftStart, -1)
}
}
But I'm getting this output indicating all three drivers were assigned at 6:00AM and to work simultaneously. Driver 1 from 6:00-11:00, Driver 2 from 6:00-12:00, and Driver 3 from 6:00-11:00.
OPTIMAL 624.0 @ [6.0, 11.0, 6.0, 12.0, 6.0, 11.0]
I don't want them to overlap. I want only one driver at a time to be assigned, and I want the entire working day to be covered. How do I express the binary state of time being occupied already?