YouTube Videos

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

Friday, January 19, 2018

Is it Time to Stop Using the Term “Data Science”?

I would not be opposed to dropping the term “data science” and dissolving it into specialized disciplines. Do not misunderstand, I think the global “data science” movement was necessary and had a positive impact on the curmudgeon corporate world. But the campaign has been won and everybody is bought into the idea. Rather than continuing to evangelize, perhaps we should allow the dust to settle so people can adjust to the change.

Data science professionals, consider no longer burdening yourselves with the heavy title of “data scientist”. Most of us do not have PhD’s or encyclopedic knowledge on every new topic. Maybe we should specialize and relieve ourselves the pressure of having to know everything. Data science has become too broad of a buzzword, and it is so ubiquitous and vague it is practically meaningless. Why would anybody want to take ownership of something so nondescript?

In this article, I want to highlight how “data science” has evolved and why it may be time to fragment it.

The Jabberwocky Effect

In 2010, there was a short-lived but memorable U.S. TV series called Better Off Ted. The show is a silly workplace comedy that lampoons corporate culture to a hyperbolic extent. But one episode, Jabberwocky (Season 1 Episode 12), captures the corporate buzzword effect too accurately.

Ted, the lead character, tries to hide budget for a pet project. When his boss Veronica confronts him, he lies and says the funds went to the revolutionary “Jabberwocky” project, which he vaguely makes up on the spot.

Here’s the funny part though. Rather than clarify what “Jabberwocky” is, Veronica pretends to be “in the know” fearing to look incompetent for being out of the loop. She pushes the nonexistant Jabberwocky project as top priority on the rest of the company. With hilarious results, every leader and employee works on Jabberwocky having no idea what it is, but would never dare admit their ignorance to each other.

Blindsided by how far it escalated, Ted comes clean to Veronica right before they do a keynote on “Jabberwocky”. Veronica tells Ted to proceed anyway because “products are for people who don’t have presentations”.

I probably do not have to explain the analogy that is “Jabberwocky”. Replace that word with “Blockchain”, “Big Data”, “Bitcoin”, “Artificial Intelligence”, “Internet of Things”, or “Data Science” and you know exactly what I mean. Corporate culture has long had a history of hyping innovations and people pretending to understand them, only to encounter their limits and chase something else.

Now that I have highlighted the "Jabberwocky Effect", let’s continue.

A Brief History of Data Science

If you want to define “data science” as anything that has to do with “data”, you can go back to the dawn of computing. If you think math and statistics are crucial to data science just as much as data, you could go centuries back and say statisticians were the original “data scientists”.

For the sake of brevity, let’s go to the 1990’s. Things used to be pretty simple. Analysts, statisticians, researchers, and data engineers were all pretty separate roles with occasional overlap. Tooling stacks often consisted of spreadsheets, R, MATLAB, SAS, and/or SQL.

Of course throughout the 2000’s things were changing. Google pushed data collection and analytics to unimaginable heights. In 2009, Google executives insisted statisticians will have the “sexiest job” for the next 10 years. I was in college at the time, and I recall that being a strange sentiment. But lo and behold, in 2011 “Harvard Business Review” mainstreamed this concept called “data science” and declared it the sexiest job of the 21st century.

It was at that moment the craze started in “Jabberwocky” fashion. Harvard created a void called “data science” and everyone raced to fill it. SQL developers, analysts, researchers, quants, statisticians, physicists, biologists, and a myriad of other professionals rebranded themselves as “data science” professionals. Silicon Valley companies, feeling that traditional role titles like “analyst” or “researcher” sounded too limited, renamed the roles to “data scientist” which sounded more empowered and impactful.

Outside Silicon Valley, this added to the confusion as most folks think of “scientists” as PhD’s in white lab coats. Counterintuitively, data scientists actually come from many backgrounds (technical and nontechnical) with varying levels of education (BS, BA, MBA, and sometimes PhDs). Many hiring managers, HR departments, and organizations in general struggled to define what they needed in a “data scientist”. I have heard too many anecdotes about hiring managers being asked “What skills are needed in this data science role?” which was vaguely answered “Well we need to be data-driven. That’s why we are hiring a data scientist”. Rather than defining jobs based on need, they defined jobs on a buzzword.

Throw in scaling advancements in data engineering (think “Big Data”), as well as the rapid rise of “machine learning”, then the “data science” umbrella gets larger and more vague. More buzzwords are thrown around that many people are saying and yet few understand. Before you know it, "Big Data" and "machine learning" have become synonymous, and distinction of disciplines becomes lost.

The domain of “Data Science” has been exhausted by the “Jabberwocky” effect. If we want it to continue succeeding we need to dissolve it, rather than continuing to stare blankly into the rabbit hole.

Reasons to Dissolve “Data Science”

The “data science” push did some great things. It rejuventated old, grumpy businesses to do something fresh and exciting. IT departments, who were traditionally stingy about giving access to data and allowing non-I.T. staff to write code, were forced to evolve and support such initiatives. Most importantly, it democratized technology to so many non-technology professions. The idea that a lawyer can benefit from learning to code is not so fringe anymore, and the rite is no longer reserved for computer scientists and engineers.

But this is a sign that the “data science” campaign has succeeded and ran its course. Continuing to push it is starting to become detrimental. Here are some reasons why:

It is Too Broad

Not too long ago, if you got a bachelors degree in “Business Management”, you could easily be upwardly mobile. But today, conventional success often requires specializing and focusing in a specific area, simply because our world has gotten complicated. A business student will be much better off studying finance, supply chain management, operations research, accounting, marketing, or some other specific business discipline.

I believe “Data Science” needs to go through a similar transition. Like business itself, there are too many disciplines to expect total mastery. It is unproductive to try learning all of them, especially at once. Of course high-level awareness of what’s out there is beneficial. It is also healthy to change interests over time. However, attempting to be omniscient will never yield value.

It’s always bothered me that “data science” can be creating a chart in Excel or Tableau… as well as building a machine learning algorithm. Seriously, what is up with that? These two tasks are thousands of miles apart in their nature, the technical skill needed, and the salary. Writing a SQL query versus designing a neural network? These are also unrelated skillsets and definitely not interchangeable. Yet there are those that insist we brand each of these skills equally as “data science”, and we generalize people with these diverse skills as “data scientists”.

Some folks reading this may argue “well all these disciplines are interconnected and data science is important to keep them integrated”. That’s arguable to some degree, but marketing, finance, supply chain, accounting, and other business functions are interconnected as well. Despite a common objective, they still are distinct areas and we no longer put emphasis on the whole of “business management”. Fragmentation and specialization is part of a domain maturing, and over time those get more attention than the domain itself.

It is Overwhelming

One of the things that prompted me to write this article is the growing number of articles from data scientists confessing their feelings of “imposter syndrome”. There is this one which I’ve seen circulating. There is also this one. As time progresses, more data science professionals continue to come forward and confess their feelings of fraudulence. My initial reaction was “what took so long?”. Then I felt a bit more empathetic and perhaps even sorry. Professionally, the burden of Imposter Syndrome can fill you with dread and keep you up at night. The question always lingers “How long will it be until I’m discovered for the fraud I am?”

But I believe this a symptom of the larger issue in this article. It took me way too long to figure out that “data science” has become anything and everything related to “data”. Sadly, there are folks that take it upon themselves to own all that. Why anyone would want to is beyond me.

This is all you need to do to become a confident “data scientist”. Totally achievable, right?

The above graphic is a popular “roadmap” to become a “data scientist”. Not only is this impractical for folks with personal lives, but why is it prescribing a “one-size-fits-all” curriculum? Maybe you can get shallow knowledge of every topic on there, but people work in different environments with different problems. At a given point in time, why not learn the tools needed for your particular job? Never mind also that tools come and go. For instance, do you even see Apache Spark, the successor to MapReduce, under "Big Data"? The only part of this roadmap not prone to obsolescence are classic mathematical concepts .

Do not misunderstand, it’s always good to be learning and obtain general ideas of what solutions exist. But in the reality of day-to-day life, effective people know how to discern and prioritize, rather than be driven by FOMO.

It Saturated Everything

Data is like electricity now. It is used everywhere and for different purposes. In the 19th century people would marvel at what electricity enabled.

Today, there is less attention on electricity and more on the devices it is powering. It is not so much we take electricity for granted, but ya know, there just comes a point you stop celebrating it. It is the same thing with data. It has succeeded and became the new normal. Rather than continuing our exhausted celebration, we should focus on the next innovations that it will enable.

Do you think natural language processing can create an opportunity to improve customer complaint handling? Then push “natural language processing”, not “data science” or “machine learning”. Be specific and focused. Are you interested in optimizing profit, cost, revenue, or operational feasibility, then position yourself on linear/integer optimization. “Data science” is just white noise now. Focus on specific and tangible areas where problems are yet to be applied and solved.

The Buzzword Dilemma

To wrap up, here are a few final considerations. I made it clear we should stop using the term “data science”. Will that actually happen? Sooner or later, I think it will. Am I going to follow my own suggestion? I am not sure yet. While the term stays in vogue, it may be the only way to get people to show up to my talks. I cannot blame others for doing the same.

Ask yourself this also: do we use buzzwords to spur a positive change? Or to serve our own purposes? Again on a global scale, the “data science” buzzword has had a positive effect. It democratized technology across professions and empowered many people for the digital workforce. But I am sure there are folks calling themselves “data scientists” to exaggerate their capabilities and capitalize on the hype. Others are coping with the pressure to know everything, and I do hope data science fragments and specializes for the sake of their well-being.

In summary, let’s ease off on generalizing, categorizing, and forcing labels on people and what they do. Perhaps we should stop calling roles "Data Scientist" and instead make the role reflective of the tasks it entails. Hire "Data Engineers", "Mathematical Programming Consultants", and "Machine Learning Analysts" rather than "Data Scientists". Give everyone a chance to find their niche and contribute individually in the best way they know how. In time, organizations will shape themselves and align to their people in ways that make sense.

Saturday, January 13, 2018

Kotlin and Linear Programming Part II - Linear Optimization

Untitled Document.md

In my previous article in this series, I introduced using linear programming with Kotlin. Again, linear programming broadly encompasses linear, integer, and binary programming all of which can be helpful for optimization problems. It was probably unconventional I started with binary programming, but it arguably makes linear programming useful beyond simple mixing problems. For instance, binary programming enables tackling complex scheduling problems which I will revisit again in my next post. But for now, let’s get pure linear programming out of the way. Then I will get to the exciting stuff.

Linear Programming and Optimization

Linear programming, in its purest form, optimizes continuous variables. Continuous means a variable can optimize to a decimal precision rather than a whole number. Unlike integer programming where a variable evaluates to a whole integer like 2, linear programming can optimize it to 2.4, 2.4012 or even 2.04000125. Surprisingly, this is much easier for a machine to solve than integer or binary programming. The machine can gracefully solve variables that truly are linear, rather than rely on brute-force approximations that integer programming requires. When you mix linear and integer programming together, it is called mixed programming.

One thing we really did not touch on in the last article is optimization. Our abstract binary problem was not given a goal other than find a solution that was “feasible” and satisfied the constraints. But in optimization, many solutions can exist. For example, an optimizer can produce many possible manufacturing plans (produce this many items of Product X, and this many Product Y), but it is likely we want the one that is the least costly, or most profitable. Therefore we can provide a “goal” function that is used to choose the best solution. This function will mathematically manipulate the variables in a way that expresses cost, revenue, machine/staff utilization, etc and the algorithm will minimize or maximize that function. You can even have multiple goals put into one function, with each one weighted based on their importance.

The Driver Problem

There are a lot of linear problem examples out there that deal with blending, from manufacturing cars to making sausage. But I am interested in scheduling at the moment so let’s put a linear twist on that, even if it conventionally is an integer problem.

At KotlinConf, I presented a driver scheduling example which garnered interest. This is a linear problem with continuous and binary variables, therefore making it a mixed problem. Driver shift start and end variables are allowed to move freely and continuously throughout the day, and the shift must be 4-6 hours in length. This continuous definition of schedule shifts is not likely how schedules are built in real life (they are usually discrete and integer-based), but it is an interesting application nonetheless. Let’s take a look at it:

You have three drivers who charge the following rates:

Driver 1: $10 / hr
Driver 2: $12 / hr
Driver 3: $15 / hr

From 6:00 to 22:00, schedule one driver at a time to provide coverage, and minimize cost.

Each driver must work 4-6 hours a day. Driver 2 cannot work after 11:00.

We could expand the scope of this problem significantly, scheduling an entire week and putting additional constraints on that scope (e.g. a worker cannot be scheduled more than 30 hours/week). But let’s keep the scope of this problem limited to one day for now, and save multi-day scheduling for the next article.

Structuring the Problem

Let’s start by saving the operating day and allowable shift size as Kotlin ranges. If we represent the day as 24 hours, the operating day will be 6:00 to 22:00, or 6..22 as a Kotlin IntRange. The allowableShiftSize can only be 4 to 6 hours, which would be an IntRange of 4..6.

val operatingDay = 6..22
val allowableShiftSize = 4..6

Declare an instance of an ExpressionBasedModel. This is the entity that we will input constraint functions into. Let’s also define an improvised Kotlin DSL to streamline ojAlgo. These variable() and addExpression() functions will automatically assign a name to each variable and function, which personally I find tedious to do. Let’s also calculate the operatingDayLength and save it to a constant for convenience.

import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.util.concurrent.atomic.AtomicInteger

// declare ojAlgo Model
val model = ExpressionsBasedModel()

// custom DSL for  expression inputs, eliminate naming and adding
val funcId = AtomicInteger(0)
val variableId = AtomicInteger(0)
fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)
fun addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { model.addExpression(it) }


// constants
val operatingDayLength = operatingDay.endInclusive - operatingDay.start

Now let’s get to the core of the problem.

data class Driver(val driverNumber: Int,
                  val rate: Double,
                  val availability: IntRange? = null) {

    val shiftStart = variable().lower(6).upper(22)
    val shiftEnd = variable().lower(6).upper(22)

    fun addToModel() {
        // expression inputs go here
    }
}

As shown above, create a Driver class with two ojAlgo variables, which will contain the shiftStart and shiftEnd values after the model is optimized. Note these two variables are bound to a lower() of 6 and an upper() of 22 which is our operating day. Also declare a driverNumber which will simply act as an ID, the rate which will be type Double, and a nullable IntRange called availability for drivers that can work only certain hours. Finally, put an empty addToModel() function which is where we will write code that “puts” the driver mathematically into the ojAlgo model. We will get to this in a moment.

We can then hold a list of Drivers. As a placeholder, we will have a main() function that calls a buildModel() function, then minimize() the cost, and print the results.


// declare drivers
val drivers = listOf(
        Driver(driverNumber = 1, rate = 10.0),
        Driver(driverNumber = 2, rate = 12.0, availability = 6..11),
        Driver(driverNumber = 3, rate = 14.0)
)


// parameters
val operatingDay = 6..22
val allowableShiftSize = 4..6

fun main(args: Array<String>) {

    buildModel()

    model.minimise().run(::println)

    // see variables for each driver
    drivers.forEach {
        println("Driver ${it.driverNumber}: ${it.shiftStart.value.toInt()}-${it.shiftEnd.value.toInt()}")
    }
}

fun buildModel() {

}

Implementing the Math

Next we need to populate the buildModel() function with our mathematical constraints. Some of those constraints will be specific to each Driver, in which case those will be in the Driver’s addToModel() function. Other constraints will apply to all the drivers.

Day Coverage

We have already constrained the shiftStart and shiftEnd variables to the operating day, so we are good there (using the lower() and upper() functions). We still need a constraint that ensures our entire 16-hour operating day has been covered. This is pretty easy to express mathematically. The sum of differences between each shiftEnd and shiftStart for each driver must sum up to the operating day’s length:

Therefore in our buildModel() function we can call addExpression(), level() it to the operatingDayLength (which should be 16), and use a handy Kotlin apply() closure to loop through each driver. We add each driver’s shiftStart and shiftEnd variables to the expression with the appropriate 1 or -1 multiplier to add or subtract them.


fun buildModel() {

    //ensure coverage of entire day
    model.addExpression()
            .level(operatingDayLength)
            .apply {
                drivers.forEach {
                    set(it.shiftEnd, 1)
                    set(it.shiftStart, -1)
                }
            }
}

Cost Objective

Before we create expressions in context with each individal Driver, let’s define our objective(). We want to minimize cost, but for now we just need an expression that calculates the cost which we call the minimise() function against. This expression will be similar to the day coverage one, but we multiply each difference by the driver’s rate. This will yield the total cost.

Then we express this in the buildModel() function in a similar way as the day coverage, but when we call addExpression() and give it a weight(1) of 1. This will make the expression a goal driver rather than a constraint. Note we can actually provide multiple objectives and give each one a weight based on their importance, but we will stick with 1 for now.

fun buildModel() {

    //ensure coverage of entire day
    model.addExpression()
            .level(operatingDayLength)
            .apply {
                drivers.forEach {
                    set(it.shiftEnd, 1)
                    set(it.shiftStart, -1)
                }
            }

    // set objective
    model.expression().apply {
        weight(1)
        drivers.forEach {
            set(it.shiftEnd, it.rate)
            set(it.shiftStart, -1 * it.rate)
        }
    }

    // driver-specific expressions
    drivers.forEach { it.addToModel() }
}

Let’s also add a call to each driver’s addToModel() function which we will implment next.

Shift Length

A driver must work a minimum of 4 hours but a maximum of 6 hours. This one is pretty simple. We just express the difference of the shiftEnd and shiftStart as an inequality.

In the Driver’s addToModel() function, we can call addExpression(), constrain its lower() and upper() bounds to the allowable shift size range. Then we set the shiftStart and shiftEnd variables, multiplying the shiftStart by -1 so it is subtracted rather than added.

data class Driver(val driverNumber: Int,
                  val rate: Double,
                  val availability: IntRange? = null) {

    val shiftStart = variable().lower(6).upper(22)
    val shiftEnd = variable().lower(6).upper(22)

    fun addToModel() {

        //constrain shift length
        model.addExpression()
                .lower(allowableShiftSize.start)
                .upper(allowableShiftSize.endInclusive)
                .set(shiftEnd, 1)
                .set(shiftStart, -1)

    }
}

Shift Restrictions

Driver 2 is the only driver that restricted their shift to something less than the operating day. He wants to only work within the 6 to 11 range. This constraint is expressed as two very simple inequalities:

If a Driver indeed has provided a specific availability that is not null, we can take that range and use it to build those constraints as shown below:

data class Driver ... {

    fun addToModel() {

        //constrain shift length
        model.addExpression()
                .lower(allowableShiftSize.start)
                .upper(allowableShiftSize.endInclusive)
                .set(shiftEnd, 1)
                .set(shiftStart, -1)

        //add specific driver availability
        availability?.let {
            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftStart, 1)

            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftEnd, 1)
        }
    }
}

Preventing Shift Overlap

Okay, everything else up to this point was fairly easy, right? Now we come to the hard part and introduce some binary variables to prevent shift overlap. If you run with just these constraints we put in so far, we are going to have drivers overlapping their shifts! We need to prevent this and unfortunately, we have to think very abstractly to do it.

To prevent overlaps, each driver needs to have a mathematical relationship to the other drivers. Let’s focus on Driver 1 and Driver 2. The shiftStart of Driver 1 may be after the shiftEnd of Driver 2. But the shiftStart of Driver 2 could also be after the shiftEnd of Driver 1. Both cases are visually shown below:

  S2________E2          S1__________E1

  S1________E1          S2__________E2

Both are valid cases, and we need either to be true as a constraint to prevent overlap. But how do we support both cases in our mathematical model, which expects all constraints to be satisfied in order to yield a solution? You logically cannot declare both cases must be true, right? It seems like a paradox. We need to an “OR” rather than an “AND”.

So how do we mathematically express that “OR”? There is a way! For a given relationship between a driver i and another driver j, you can share a boolean variable between them that must be 1 or 0. Here is a modification to our constraints that effectively achieves the “OR”:

Well, that jumped up a notch. The M is going to be the length of the planning window, which is 16 hours. We just need M to be a very large number, so we could make it a 1000 but the window size should be fine.

The binary variable is represented by a delta, and it can only be optimized to 1 or 0. Notice the subtle math this achieves and allows both cases of Si coming before Ej, or Sj coming before Ei, but demands at least one of them must be true. M is a large enough number (the planning window) to throw the inequality far out enough that it allows the “false” case to now be true. It effectively nullifies. Thus, this achieves our much-needed “OR” operation for our constraint.

If your head is spinning pull out a pencil and paper, throw a few values at the expression, and you’ll see what I mean.

Here is how we can implement this for our Driver instance. It must establish this constraint with each other Driver. Note that ojAlgo needs all variables on one side of the equation and distributed, so we may have to do a little bit of basic algebra to move our variables around like this:

data class Driver ... {

    fun addToModel() {

        //constrain shift length
        model.addExpression()
                .lower(allowableShiftSize.start)
                .upper(allowableShiftSize.endInclusive)
                .set(shiftEnd, 1)
                .set(shiftStart, -1)

        //add specific driver availability
        availability?.let {
            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftStart, 1)

            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftEnd, 1)
        }

        //prevent shift overlap
       drivers.asSequence()
               .filter { it != this }
               .forEach { otherDriver ->

                   val occupied = variable().binary()

                   model.addExpression()
                           .upper(0)
                           .set(otherDriver.shiftEnd, 1)
                           .set(occupied, operatingDayLength * - 1)
                           .set(shiftStart, -1)

                   model.addExpression()
                           .upper(operatingDayLength)
                           .set(shiftEnd, 1)
                           .set(occupied, operatingDayLength)
                           .set(otherDriver.shiftStart, -1)
               }
    }
}

And there you have it. If you find this is dizzying to grasp, spend some time with pencil and paper, visualizing how this equation works and throw different scenarios and variable values at it. I have found visualizing the problem is the most effective way to make it intuitive.

Running the Program

Finally, go back to your main() function and call the minimise() function, and print the results of the variables like this:

fun main(args: Array<String>) {

    buildModel()

    model.minimise().run(::println)

    // see variables for each driver
    drivers.forEach {
        println("Driver ${it.driverNumber}: ${it.shiftStart.value.toInt()}-${it.shiftEnd.value.toInt()}")
    }
}

You should get the following output, with these shifts that will minimize the cost according to your constraints.

OPTIMAL 190.0 @ [11.0, 17.0, 6.0, 11.0, 17.0, 22.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0]
Driver 1: 11-17
Driver 2: 6-11
Driver 3: 17-22

Hopefully you found this somewhat interesting and exciting, and this topic of linear/integer programming is giving you new ways to look at optimization problems. In the next article, I will cover an ambitious scheduling example that is more real-life, and schedules classrooms and classes across an entire week.

Here is the entire code example. You can also view it on GitHub here.

import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.util.concurrent.atomic.AtomicInteger

// declare ojAlgo Model
val model = ExpressionsBasedModel()

// custom DSL for model expression inputs, eliminate naming and adding
val funcId = AtomicInteger(0)
val variableId = AtomicInteger(0)
fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)
fun ExpressionsBasedModel.addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { addExpression(it) }

// parameters
val operatingDay = 6..22
val allowableShiftSize = 4..6

// constants
val operatingDayLength = operatingDay.endInclusive - operatingDay.start


// declare drivers
val drivers = listOf(
        Driver(driverNumber = 1, rate = 10.0),
        Driver(driverNumber = 2, rate = 12.0, availability = 6..11),
        Driver(driverNumber = 3, rate = 14.0)
)


fun main(args: Array<String>) {

    buildModel()

    model.minimise().run(::println)

    // see variables for each driver
    drivers.forEach {
        println("Driver ${it.driverNumber}: ${it.shiftStart.value.toInt()}-${it.shiftEnd.value.toInt()}")
    }
}

fun buildModel() {

    //ensure coverage of entire day
    model.addExpression()
            .level(operatingDayLength)
            .apply {
                drivers.forEach {
                    set(it.shiftEnd, 1)
                    set(it.shiftStart, -1)
                }
            }

    // set objective
    model.objective().apply {
        drivers.forEach {
            set(it.shiftEnd, it.rate)
            set(it.shiftStart, -1 * it.rate)
        }
    }

    // driver-specific expressions
    drivers.forEach { it.addToModel() }
}

// Driver class will put itself into the Model when addToModel() is called
data class Driver(val driverNumber: Int,
                  val rate: Double,
                  val availability: IntRange? = null) {

    val shiftStart = variable().lower(6).upper(22)
    val shiftEnd = variable().lower(6).upper(22)

    fun addToModel() {

        //constrain shift length
        model.addExpression()
                .lower(allowableShiftSize.start)
                .upper(allowableShiftSize.endInclusive)
                .set(shiftEnd, 1)
                .set(shiftStart, -1)

        //add specific driver availability
        availability?.let {
            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftStart, 1)

            model.addExpression()
                    .lower(it.start)
                    .upper(it.endInclusive)
                    .set(shiftEnd, 1)
        }

        //prevent shift overlap
        drivers.asSequence()
                .filter { it != this }
                .forEach { otherDriver ->

                    val occupied = variable().binary()

                    model.addExpression()
                            .upper(0)
                            .set(otherDriver.shiftEnd, 1)
                            .set(occupied, operatingDayLength * - 1)
                            .set(shiftStart, -1)

                    model.addExpression()
                            .upper(operatingDayLength)
                            .set(shiftEnd, 1)
                            .set(occupied, operatingDayLength)
                            .set(otherDriver.shiftStart, -1)
                }
    }
}