Integer (or Mixed Integer) Programming

The only “problem” with the Linear Program is that it’s linear, meaning it is not limited to integer values. This means it could tell you that the optimal solution was 1.5 pears and 3.5 apples. Nice. But what if the store won’t sell half an apple?

The conversion from real number to integer is non-trivial. Solutions use branch-bound algorithms to try and find an integer solution that gets as close to the linear solution as possible. The solver gets this by first running the system as a Linear Program ignoring the n, med to have integer results.

The branch-bound adds greatly to the difficulty of the solution resulting in much greater run times. In fact, one of the best options for run time is if the problem can be converted from Integer to Linear. This is analogous to changing a polynomial problem to linear in terms of algorithm complexity.

Everything You Need to Know About Developing Linear and Mixed Integer Programs

Basic Idea

First, let us start with Linear Programming by talking about a system of equations…

X + Y = 20

2x + Y = 30

If we think back to Middle School/High School (ages 12-16), we remember several techniques for solving the system of equations (X=10, Y=10). Then you recall the system of inequalities:

X < 20

Y < 20

2x + Y < 1000

Then you realize that you could maximize an equation such as…

Maximize X+Y

So we could finally see the maximum number of things that satisfy X is less than 20, y is less than 20, and 2x + Y is less than 20. Obviously, X is 19 and Y is 19.

Solvers

Solvers such as Gurobi, CPlex, and LP Solver find the values of the variables such that all the constraints hold true and the objective function is either minimized or maximized.

IIn the late 1940s, George Bernard Dantzig devised the Simplex Method that could solve ALL (feasible) linear programs. Not only solve it but provide a mathematically proven optimal solution. This was like a hallmark in the computer science revolution similar to E=MC^2 in physics. Huge. Any problem that could be described as a system of equations can be solved and optimized by a computer.

https://en.wikipedia.org/wiki/Simplex_algorithm

Developers must understand some basic concepts to utilize the simplex algorithm. They have to define variables (X and Y), define the constants (the 2 in the 2x + y < 1000 from the previous example, define the constraints as a combination of variables on constants (X < 20, Y < 20, etc…), and then they have to define an objective (Maximize X + Y).

Variables, Constraints, and the Objective are passed to a solver such as Gurobi, and it returns us the result!

Obviously, it is more complex than this.

Linear vs. Integer Programs

Solvers will not return X is 19, and Y is 19 for the previous example.

X < 20

Y < 20

2x + Y < 1000

The solver will actually return numbers like X = 19.99999999 and Y = 19.999999. The Simplex algorithm optimizes along with Real Numbers. That’s why Linear Programming is called Linear – a constant line from negative infinity to positive infinity to positive infinity. Integers are a dashed line – all those real numbers between 1 and 2 are skipped.

This is a problem because a production line cannot build 19.9999 engines, a company cannot schedule 19.9999 technicians. Real numbers aren’t always a realistic choice for many applications. Instead, we need Integers. We need to know if it is 19 or 20 engines.

When a variable such as X or Y is defined, it must be declared if it is an Integer or Real. Once at least one variable is declared as an Integer then the problem moves from Linear to Integer. It is common that not all variables are declared as integers, creating a mixed set of Linear and Integer variables which forms a Mixed Integer Program.

Moving from a Linear to Integer or Mixed Integer has significant ramifications. Optimizing the solution for a linear program was “easy” because the simplex method produces this solution. But if the real numbers are not valid then the solver must find an integer that approximates those values, produces the optimal solution, and still satisfies the constraints.

In the previous example, the solver could round up the 19.999 or round it down. But which variable rounds up or which one rounds down? But if a variable is rounded up or down does it change something about another constraint which forces us to change another variable?

This decision-making process is solved by an algorithm called Branch and Bound. If you want to be an expert on MIP, then you must understand this in great detail. But if you just want to get an answer – then you just need to know the basics.

Branch and Bound

The solver will first solve the problem as if all variables are Real Numbers with Linear Program via the Simplex Method. This generates the value for the variables from the previous example; X = 19.9999 and Y = 19.9999.

The solver then starts the branch and bound. It will try to play around with X and Y and find the maximum result for the Objective, so it might try 19 for X and 19 for Y or some other combinations. It will do this in such a way as to try and close the gap between the best MIP solution and the LP solution. The LP being 19.9999 + 19.9999 which is like 39.9999 and the LP is getting 19 + 19 = 38 and it tries a bunch of combinations of X and Y until it decides that 38 is as close to 39.9999 as it can get. The difference in the LP (Optimal Solution) and the best MIP solution is the MIP Gap.

One of the key differences between Solvers such as Gurobi, CPlex, and LP Solve is the quality of the Branch and Bound algorithm in terms of how quickly they produce the optimal result or, and might really be more important, is to decide that it’s infeasible to find on Integer solution within the MIP Gap from the optimal Real number solution.

NOTE: MIP Gap is an important term. This is a key way we control run time. The Solver can be configured to give up after the MIP Gap drops to a certain percentage.

Boolean and Selection Variables

A special type of Integer variable is a Boolean variable. This is exactly the same as defining a variable as an integer variable that must be >= 0 and <= 1. Boolean variables are so common that most solvers allow you to define the variable as Boolean, and those constraints are understood. Most engines also appreciate knowing a variable is Boolean and can help it speed up its Branch and Bound algorithm.

Selection variables are a use of Boolean variables. Assume we have a problem that requires a selection of a card from a standard 52 card playing deck. Then we could have a Boolean variable for each card. And if at the end of the optimization we expect that one and only one variable has the value one, that variable with the one represents the selected card. Hence the Boolean variable is being used to select one of the cards - a selection variable.

The selection variables can be used in many different ways. If there is a cost associated with a selection, then a monomial could be created with a cost derived during a preprocess step (before the solver is called). Remember, a monomial is a combination of constant and variable. Imagine we have a constant c and a variable X then the monomial is cX. In that same program, a constraint could add up all the selection variables and ensure it does not exceed 1. This would result in only 1 card being selected from the deck.

Decision Variables and Working Variables

Selection variables are a type of Decision Variables. Decision Variables are the expected output of the solver. Decision variables can be Boolean such as the Selection Variables in the previous section, but they may also be Integer values, such as a number of engines to produce, or even Real, such as the optimal interest rate to charge.

Working variables are additional variables defined to assist the creation of various constraints. The user doesn’t really care, or perhaps doesn’t even know about, the value of the variables. For example, several logical techniques can be used by having a variable that represents a gap between the maximum. The value of the gap is irrelevant but the fact that that value is 0 or negative or positive can be used in other constraints. These types of variables are used in techniques like Big M.

Defining Variables and Building Constraints

The number of variables required for an average problem is actually quite large. From a developer’s perspective, working with the large number of variables proves to be the most difficult task in converting the design into a product.

Creating string names for the variables would result in an extreme amount of memory just to hold the names as well as an outrageous amount of time spent parsing those strings. Let’s take a look at our previous example.

X < 20

Y < 20

2x + Y < 1000

Max (X + Y)

The equations in the example are the constraints. The Max is the objective. The X and the Y are decision variables. And there are no working variables.

If we think of the variables as just one of a million variables, our only solution is to number them…

X1 < 20

X2< 20

2X1 + X2 < 1000

Max (X1 + X2)

Next, we need to understand that every variable in a constraint is really a monomial. A monomial is a combination of a constant and the variable. We must also understand that every variable is in every constraint and the objective. If we do not explicitly add the variable, then the solver assumes the variable in a monomial with a 0 constant. So, in reality, it looks more like…

1*X1 + 0*X2 < 20

0*X1 + 1*X2< 20

2*X1 + 1*X2 < 1000

Max (1*X1 + 1*X2)

Let me choose another example where the objective doesn’t contain one of the variables…

X1 < 20

X2 < 20

X3 < 20

X1+X2+X3>10

2X1 + X2 < 1000

Max (X1 + X2)

So if we expand it so that every variable is in every constraint and objective…

1*X1 + 0*X2 < 20

0*X1 + 1*X2< 20

1*X1 + 1*X2+ 1*X3>10

2*X1 + 1*X2 0*X3 < 1000

Max (1*X1 + 1*X2 + 0*X3)

The trick for the developer is to find the values of the decision variables from all the variables and properly map them back to meaningful variables within the application. X1 might be the number of coffee cups, while X2 might be the number of beer steins. If we lose track of that relationship, then it does us very little good to know X1 is 19 and X2 is 20.

Normal Form for Constraints

Some solvers require the constraints to be defined in a standard form. The following constraint, for example, is a perfectly reasonable inequality.

X1+X2 > 10 – 3x

But a solver such as Gurobi requires all the variables to be on the left-hand side of the inequality.

X1+X2 + 3x > 10

And moving the constant of the left-hand side so that the right-hand side is always 0.

X1+X2 + 3x – 10 > 0

This allows all the variables and constants to be described in one series of monomials. Then an inequality. And the right-hand side is always 0.

Defining Variables and Building Constraints in Pseudo Code

Let’s say I have 3 workers (Dwarkesh, Andrew, Pramod). Let’s say I have 3 tasks (T1,T2, and T3). Let’s say these can be done in 1 of 5 days in a week (D1, D2, .., D5). Let’s say that we have a selection variable (w – worker, t – task, d – day).

We build these variables with a nested loop…

Loop Workers

Loop Tasks

Loop Days

Create variable for the Worker, Task, Day

End

End

End

Now you want to build a constraint… Let’s say every Task can only be assigned once.

So the sum of all the selection variables for a task must be 1 or 0 (some task may be unassigned).

Loop Tasks

Now we want to create a constraint

So we need all the variables

How do we get these variables???

End Loop

We could have created a map keyed by T and value is a list of all the variables. Or we could have a structure such as MIP Wrapper that allows us to ask for all variables that contain T. I think if you look at Sara’s code – she creates a map keyed by the various data elements she will loop through.

After the solution is done, we want to loop through all the selection variables and find out which ones are 1 -- i.e., the ones the Solver selected. We see the 514th X is 1. Which w, t, d was that? So we need a way to pass the 514 into a structure to find out that it was Dwarkesh, T2, on D3. That’s what we update in the database.

Gurobi, the solver we use at the time of this writing, has an alternate way of getting the result back (this selection variable technique is a common design pattern for MIP). They can return you all the selected variables. You still have to accurately map those variables back to variables in your code. But it makes it a little easier to work with only the selected (non-zero) variables. In the earlier example with a standard deck of 52 cards and only 1 card is selected, having the solver return you only the selected variable saves you from looping through 52 cards. Imagine you have a problem with a million variables; that feature can save some real time.

Mathematicians Express Variables, Constraints, and Objectives Differently

Mathematicians might write an objective such as...

What the hell is this?

r – worker

t – task

d – day

X – Selection variable

rtd – a specific worker, task, day combination

This is objective (either minimize or maximize). here are no =, <, or >. It’s the sum of every selection variable. Loop through all r (workers), Loop through all tasks, loop through all days.

Loop Worker

Loop Task

Loop Day

Constraint add Monomial (a constant and variable par) for the Worker, Task, Day

End Loop

End Loop

End Loop


Results in an objective something like…

Maximize (PramodT1D1 + PramodT1D2 + … PramodT3D5 + DwarkeshT1D1 …)

So you can see how the is really a clue that you will have a loop and the variable you will be looping on. And combinations of those summations are nested loops. Basically, the mathematicians are coding the solution for you.

Tips and Tricks

Limit Variable Creation to Viable Options

Let’s say that Andrew is not available on D4 and D5 because of Thanksgiving. If the worker is not available, then the engine should not select them. If the engine cannot select them, then WHY create a variable? By avoiding creating the variable, you eliminate a selection variable, and you save the need for a constraint. This preprocess enforces a constraint before the solver is executed.

Distance is a great example. If the Worker is too far away from the Task, then the Worker cannot be assigned the task. Where is the constraint in the engine that describes this??? It doesn’t exist? Did Sara fail to write code correctly? No. She simply did not create a variable for a combination of worker, and task that was not valid.

Obvious right?

Well no. Remember how the mathematicians are writing the loops…

This implies EVERY worker on EVERY task for EVERY day, but this is NOT what they mean. What they mean is everyone for which they created a variable. If they didn’t create a Variable for R=3, T=2, and D=4, then that variable is NOT part of the objective.

Multiple Objectives

The design may call for multiple objectives. Perhaps one objective is to maximize the number of selections. But there may be multiple ways to solve the problem and achieve that number of selections. The second objective could be to minimize cost.

This objective maximizes the number of selections X. This could be followed by…

The idea would be to solve the problem with the first objective and turn around and solve it again but with the second objective.

If this is done, the only result will be for the minimization of cost. It likely results in no selections. This isn’t very useful. It is because a step is missing. The result from the first objective must be converted to a constraint and added to the problem before solver for the second objective. This would result in minimizing the cost for a solution that results in now fewer than the maximum number.

Logical Operations

Constraints can be created to produce logical operations. These can apply to selection problems with selection variables. As a developer, you will likely not be called upon to write these yourself. The science team will have likely designed the MIP for you, but it’s enlightening to see some combinations to really gain a depth of understanding of the mathematicians’ work.

Logical AND: Use the linear constraints y1≥x1+x2−1, y1≤x1, y1≤x2, 0≤y1≤1, where y1

is constrained to be an integer.


X1

X2

Y1

0

0

0 (y1 can’t be larger than X1 or X2)

0

1

0 (y1 can’t be larger than X1)

1

0

0 (y1 can’t be larger than X2)

1

1

1 (y1 can’t be less than X1 + X2 – 1)

Notice that Y1 is only true if both X1 “and” X2 are true.

Logical OR: Use the linear constraints y2≤x1+x2, y2≥x1, y2≥x2, 0≤y2≤1, where y2 is constrained to be an integer.

X1

X2

Y2

0

0

0 (y2 must be less than or equal to X1 + X2)

0

1

1 (y2 must be equal to or larger than X2)

1

0

1 (y2 must be equal to or larger than X1)

1

1

1 (y2 is Boolean and cannot exceed 1)

Notice that Y2 is only true if either X1 “or” X2 is true.

Logical NOT: Use y3=1−x1.

X1

Y3

0

1 (1 – 0 = 1)

1

0 (1 – 1 = 0)

Logical Implication: To express y4=(x1⇒x2) (i.e., y4=¬x1∨x2), we can adapt the construction for logical OR. In particular, use the linear constraints y4≤1−x1+x2, y4≥1−x1, y4≥x2, 0≤y4≤1, where y4 is constrained to be an integer.

X1

X2

Y4

0

0

1 (y4 must be greater 1 – X1)

0

1

0 (y4 must be greater X2)

1

0

0 (y4 must be less than 1 - X1 + X2)

1

1

1 (y2 must be greater X2)

Notice that Y4 is only true if X1 and X2 are equal. The value is the ability to use Y4 in other constraints.

Forced logical implication: To express that x1⇒x2 must hold, simply use the linear constraint x1≤x2 (assuming that x1 and x2are already constrained to boolean values).

X1

X2

Y4

0

0

1

0

1

1

1

0

0

1

1

1

Notice that Y4 is true if X2 is true or both X1 and X2 are false.

XOR: To express y5=x1⊕x2 (the exclusive-or of x1 and x2), use linear inequalities y5≤x1+x2, y5≥x1−x2, y5≥x2−x1, y5≤2−x1−x2, 0≤y5≤1, where y5 is constrained to be an integer.

X1

X2

Y5

0

0

0 (Y5 must be less than X1 + X2)

0

1

1 (Y5 must be greater than X2 – X1)

1

0

1 (Y5 must be greater than X1 – X2)

1

1

0 (Y5 must be less than 2 – X1 – X2)

Notice that Y5 is true if and only if X1 or X2 is true but not if both X1 and X2 are true.

Cast to boolean (version 1): Suppose you have an integer variable x, and you want to define y so that y=1 if x≠0 and y=0 if x=0. If you additionally know that 0≤x≤U, then you can use the linear inequalities 0≤y≤1, y≤x, x≤Uy; however, this only works if you know an upper and lower bound on x. Alternatively, if you know that |x|≤U(that is, −U≤x≤U) for some constant U, then you can use the method described here. This is only applicable if you know an upper bound on |x|.

X

U

Y6

0

512

0 (Y6 must be less than X)

300

512

1 (U * Y6 must be greater than or equal to 512)

Decision variables may be Boolean for selection variables. The logic may be based on some real or integer number. A conversion from that to a Boolean is helpful in getting useful results from the program. Notice the requirement to know the maximum; this is not always trivial.

Cast to boolean (version 2): Let's consider the same goal, but now we don't know an upper bound on x. However, assume we do know that x≥0. Here's how you might be able to express that constraint in a linear system. First, introduce a new integer variable t. Add inequalities 0≤y≤1, y≤x, t=x−y. Then, choose the objective function so that you minimize t. This only works if you didn't already have an objective function. If you have n non-negative integer variables x1,…,xn and you want to cast all of them to booleans, so that yi=1 if xi≥1 and yi=0 if xi=0, then you can introduce n variables t1,…,tn with inequalities 0≤yi≤1, yi≤xi, ti=xi−yi and define the objective function to minimize t1+⋯+tn. Again, this only works nothing else needs to define an objective function (if, apart from the casts to boolean, you were planning to just check the feasibility of the resulting ILP, not trying to minimize/maximize some function of the variables).

BIG M (IF THEN): If a tech is assigned to this task in this district on this day, then it cannot be assigned to any of these tasks in the other districts on that day.

if z = 0, then x = 0

where z binary, and x >= 0

We could use the big-M formulation:

x - M * z <= 0

z

x

M

x – M * z

x – M * z <= 0

1

0

1000

-1000

1

1

13

1000

-987

1

0

0

1000

0

1

0

13

1000

13

0

If Z is zero, then X is 0. If Z is not zero, then we don’t care. Truth table reflects not caring as 1. That is driven by the M being so large that it outweighs the X. Hence Big M.

Notice that this is very similar as Cast to Boolean. It’s just a different presentation of the same idea. M is U. The problem must be set up with these to be sufficiently large. The result is 1 if a set of conditions is true else it is 1 – a cast to Boolean.

Developing Linear and Integer Programs in Studio

Configuring Solver

Platform supplies a MIPConfig class. This class is extended by a GurobiMIPConfig. These classes wrap a JSON and provide common config values. This structure is loose to allow specific engines to add in their own custom config information that they need to pass to the engine.

  • MIP_ENGINE – which solver to use: Gurobi, Cplex, etc.

  • TIMIE_LIMIT_IN_SECONDS – maximum time described in seconds

  • TIME_LIMIT – maximum time described in milliseconds

  • RELATIVE_MIP_GAP – stop solver as soon as it gets close enough to optimal answer

  • ABSOLUTE_MIP_GAP – stop solver as soon as it gets close enough to optimal answer

  • EXPORT_MIP – instruct the solver to export the program in LP format

MIP Wrapper

Platform supplies a MIPWrapper that separates ONE Network engines from the specific solver such as Gurobi, CPlex, etc…

MipWrapper has functions that help you build the program, define variables, constraints, exporting problems, and solving. The MIPWrapper also contains MipMap which assists in the difficult task of mapping variables within the MIP and variables in the engine code.

The following code snippet shows how to instantiate the wrapper. Notice that it passes a MIPConfig (basically a JSON string of engine parameters). The engine name defaults to GUROBI but the MIPConfig can override this.

String engineName = MIPEngineFactory.GUROBI;

mipWrapper = new MIPWrapper(engineName, mipEngine);

mipWrapper.setConfig(dataModel.getMIPConfig());

MIP Map

Platform provides a MIPMap class. This class matches a key in the form of a byte array to an index. The index is an index for a variable within the solver. The byte array is a combination of attributes that describe the variable.

In a previous section, we had a variable Xrtd. Assume that there were other variables such as Y and Z. The first attribute is the fact that it is X. The next set of attributes is the r, t, and d. There are four attributes in all.

In order to form the byte array, we must know the maximum value for each attribute. The number of variable types is 3 (X, Y, and Z). The number of r might be 16, the number of tasks might be 24, and the number of d might be 48. It takes 2 bits for the 3, 5 bits for 24, and 6 for 48. The total number of bits is 13. Two bytes for 13 bits. The key is a 2-byte array.

The following code segment is key to setting up the MIPMap within the MIPWrapper.


private void defineVariableKeyFormat(DataModel dataModel, MIPProcessData mipProcessData) {

ArrayList<ByteArrayEncoder.Entry> keyFormat = new ArrayList<ByteArrayEncoder.Entry>();

int noOfResourceVariables = dataModel.getResourceGeographies().size();

//task variables

int noOfTaskVariables = dataModel.getTasks().size();

// Goal variables

int noOfGoalVariables = dataModel.getObjective().getObjectiveSequence().size();

ByteArrayEncoder.Entry varEntry = new ByteArrayEncoder.Entry(RESOURCES, noOfResourceVariables);

ByteArrayEncoder.Entry varEntryTasks = new ByteArrayEncoder.Entry(TASKS, noOfTaskVariables);

Optional<Entry<Integer, Integer>> maxEntry = mipProcessData.getResIndexToMaxDayCount().entrySet().stream().max((e1, e2) -> e1.getValue()

.compareTo(e2.getValue())

);

int noOfDayVariables= maxEntry.get().getValue();

ByteArrayEncoder.Entry varEntryDays = new ByteArrayEncoder.Entry(DAYS, noOfDayVariables);

ByteArrayEncoder.Entry varEntryGoals = new ByteArrayEncoder.Entry(GOALS, noOfGoalVariables+1);

keyFormat.add(varEntry);

keyFormat.add(varEntryTasks);

keyFormat.add(varEntryDays);

keyFormat.add(varEntryGoals);

mipWrapper.setVariableFormat(keyFormat);

}



The key here is that four attributes are used to describe all of the variables in this program. The first attribute is a number of resources, next is a number of tasks, third is a number of days, and the last is a number of goals. Notice that for each of these it inspects the data model (contain of all data being used by engine) to calculate these maximums.

The real takeaway is that the developer needs to know that a mapping must take place. This mapping is done by a set of attributes to an index. The developer needs the index when working with the solver and the set of attributes when working with their code. The set of attributes is an array of values that must be meaningful to the engine code (i.e. the 3rd worker, 17th task, and the 2nd day).

Variable Declaration

MIPWrapper provides method createVariable(). There are two basic uses of this function – create a variable with a name and without a name. Named variables are easy to use – no need for MIPWrapper. You can refer to the variable by know and the solver understands the correct variable. But this is terrible for performance in that you build up millions of string names. The second use is for variables without names. This is where it becomes important to track the index within the solver to variable in your code.

The following is a sample of a call. It is a variable with no name. The minimum is 0 and the maximum is the maximum value of a real number. The type of Float (i.e. real). The goalKeyArray requires a little more understanding.

mipWrapper.createVariable(NO_NAME, BOOLEAN_MIN, Double.MAX_VALUE, MipVariableTypeEnum.FLOAT, goalKeyArray);

goalKeyArray is an array of attributes that define the variable. In the previous section that discussed the MIPMap, the map is keyed by a byte array. To avoid having all of the engine code work with byte arrays, Platform exposes this as an array of integers. One element in the array for each attribute. The value of each attribute should NOT exceed the maximum as defined when the MIPMap was first created. If this is not adhered to, then the mapping will break down as keys will overlap.

MIPWrapper is smart enough to know if the variable has been added before. The engine code simply tries and creates variables as required, safe in the knowledge that it will not create duplicates. But it should be noted that this processing is not free. If a process can be developed such that the order will generate no duplicates, then this is optimal. If the engine code must keep its own map of variables, then it is wasting its time. It's no more difficult for MIPWrapper to compare against its map than for the engine code to compare against its own.

The following sample code puts the variable creation inside a couple of loops. In this case, it is days and the available days a worker can be assigned. assignmentKeyArray is the integer array of attributes that describe the variable. You can imagine that in here there could be logic – do not create this variable if the worker is not available for example. Placing the logic here removes the option from the solver. Rather than creating a variable and a constraint, avoid creating the variable in the first place.


for (int day = 0; day<= noOfDays; day++) {

long taskDate = dateIncrementer(task.getEarliestDate(), day);

int dayIndex = -1;

for (Long key : mapDateToAvlHours.keySet()) {

++dayIndex;

if(key.equals(taskDate)) {

List<Double> estDriveAndTotalTime = new ArrayList<Double>();

estDriveAndTotalTime.add(xEstDriveTime);

estDriveAndTotalTime.add(xTotalTime);


int[] assignmentKeyArray = {resIndex, taskIndex, dayIndex, NO_INDEX};

estDriveAndTotalTime.add((double)mapDateToAvlHours.get(taskDate));

if (resGeo.getWorkerType().equals(WorkerTypeEnum.CONTRACTOR.toString())) {

estDriveAndTotalTime.add(1d);

}

else {

estDriveAndTotalTime.add(0d);

}

estDriveAndTotalTime.add(eDist);

taskIndexes.add(taskIndex);

mapAssignmentArrayToDriveTimeToTotalTime.put(assignmentKeyArray, estDriveAndTotalTime);

if (LOG.isDebugEnabled()) {

mipWrapper.createVariable(AssignmentSelection.getVariableName(assignmentKeyArray), BOOLEAN_MIN, Double.MAX_VALUE, MipVariableTypeEnum.BOOLEAN, assignmentKeyArray);

}

else {

mipWrapper.createVariable(NO_NAME,BOOLEAN_MIN, Double.MAX_VALUE, MipVariableTypeEnum.BOOLEAN, assignmentKeyArray);

}

break;

}

}

}

Constraint Creation

MIPWrapper provides a function to add Constraints. The addConstraints function is very flexible and thus a little confusing. addConstraints can add multiple constraints in one call! This can be useful if the logic for building constraints is done PRIOR to adding constraints. If this logic for determining a single constraint is executed and a call for adding a constraint is done, then only one constraint is added at a time.

Let’s look a little more closely.

/**

*

* @param constraintVariables: variables that are in constraint equation

* int[constraint Number][variable][varriableAttributes]

* @param coefficients : variable coefficients

* double[constraint Number][coefficient]

* @param constraintType : constraint type as LESS_THAN_OR_EQUAL, GREATER_THAN_OR_EQUAL or EQUAL

* @param constraintRightHandSide: right hand side value for constraints

*/

mipWrapper.addConstraints(constraintVariables, constraintVarcoefficient, constraintType, constraintRightHandSide);

ConstraintVariables and constraingVarCoefficient describe the monomials that make up the constraints. In order to match values from one structure to another, we have to be sure we are talking about the same constraint (if creating more than one at a time), the position the variable is in within the constraint (is it the first variable or second?), then finally the integer array that describes the variable itself.

Let’s look at the flowing constraint.

2x1 + x2 <= 1000

Because we are passing only 1 constraint, the constraint number is always 1. The variable x1 is in position 1. The variable x2 is in position 2. There would be a set of attributes that describe X1 and X2. The constraintVariable would have an entry for x1 that would be 1 (constraint), 1 (position), and attribute array describing x1. The constraintVarCoefficient would also have the constraint and position but then it would just be a value. The coefficient for x1 would be 1 (constraint), 1 (position), 2 value.

constraintVariable

1

1

Attribute array describing x1

1

2

Attribute array describing x2

constraintVarCoefficient

1

1

2

1

2

1

Constrainttype and constraintRightHand side work the same way in regards to an index for the constraint. But there is no position; there is only one type (equality/inequality) and only one right hand side.

constraintType

1

<=

constraintRightHandSide

1

1000

Because there can be hundreds of thousands of constraints, constraint creation often finds itself in a loop. Let’s take a look at a sample…

for (int taskIndex: taskIndexes) {

int taskId = taskIndex;


List<int[]> eachResWithSameTaskList = mapAssignmentArrayToDriveTimeToTotalTime.keySet().stream()

.filter(k -> k[1] == taskId)

.collect(Collectors.toList());

if (eachResWithSameTaskList.size()>0) {

int resSize = (int) eachResWithSameTaskList.size();

int[][][] constraintVariables = new int[tasks.size()][][];

double[][] constraintVarcoefficient = new double[tasks.size()][];

ConstraintTypeEnum constraintType[] = new ConstraintTypeEnum[tasks.size()];

double[] constraintRightHandSide = new double[tasks.size()];


constraintVariables[taskIndex] = new int[resSize][];

constraintVarcoefficient[taskIndex] = new double[resSize];


int locationIndex=0;

for(int[] assignmentKeyArray : eachResWithSameTaskList){

constraintVariables[taskIndex][locationIndex] = assignmentKeyArray;

constraintVarcoefficient[taskIndex][locationIndex] = 1.0;

locationIndex++;

}


constraintType[taskIndex] = ConstraintTypeEnum.LESS_THAN_OR_EQUAL;

constraintRightHandSide[taskIndex] = 1.0;


mipWrapper.addConstraints(constraintVariables, constraintVarcoefficient, constraintType, constraintRightHandSide);

}

}

From this you can get an idea of what’s going on. At the time of this writing, it appears there may be a mistake in this code – in that the first index is being sized to the number of tasks but there is only one. But this sample should be good enough to see how the MipWrapper can be used to automate constraint construction through reference of variables via their attributes.

Adding Objective

MIPWrapper provides ability to define an objective. This is similar to the Constraint but more straightforward in that there is only one for any given problem. Note that in previous sections we discussed multiple objectives. That is referring to creation of a problem, adding objective, solving and then adding another constraint, next objective, and solving. There are two programs being solved – not just one.

mipWrapper.addObjective(objectiveGoalVariable, goalcoefficient, ObjectiveTypeEnum.MAXIMIZE);

ObjectiveGoalVaraible and goalCoefficeint work like their compatriots in addConstraint. They have one fewer dimension in that there is only one objective.

Consider the following objective: Max (X1 + X2)

objectiveVariable

1

Attribute array describing x1

2

Attribute array describing x2

objectiveCoefficient

1

1

2

1

Objective type is Maximize. And that’s it. Same logic with the Attributes mapping the variable.

Solving

MIPWrapper provides a solve method. The variables, constraints, and objectives loaded prior to this step along with the MIPConfig are all that is needed.


if (mipWrapper.solve()) { // solve MIP model

if (isInfo) {

LOG.info("MIP solved");

}

mipSuccess = true;


double objectiveValue = mipWrapper.getObjectiveValue();

if (isInfo) {

LOG.info("objectiveValue: " + objectiveValue);

}


}

else {

LOG.info("Failed to solve MIP model for goal: " + goal);

break; // Failed to solve MIP model no need to execute for any other goal

}

The only real question is what to do in the event of an exception. Solve returns a Boolean – if it is false then this code is simply writing a log statement. Clearly, reading about GUROBI and the possible exit conditions and how you want your application to behave is critical. For example, we had one engine that we set a short time out. Then detect if it timed out and then simplified the problem and reran. This ensured we always got a result – but tried, as much as we were willing to wait, to get the most optimal response.

Parsing the Result

MIPWrapper provides several methods to get variable values. The value of the variables is the value that was required to get the optimal result. MipWrapper also provides getObjectvieValue that gets the maximized or minimized result. Often users don’t particularly care for these values, but it is interesting for logging or debugging purpose.

double objectiveValue = mipWrapper.getObjectiveValue();

In the following example, the developer decided they wanted all of the selection variables. They are trying to decide which combination the solver chose. In this case, they saved the set of attributes describing all of these variables in an external object (mapAssignmentArrayToIndex). This is a bit excessive. It shows that perhaps we need to extend the MipMap to track something like Selection Variable so that we can simply ask MipWrapper to return the variables used as selection criteria.


Map<int[], List<Double>> mapAssignmentArrayToIndex = mipProcessData.getMapAssignmentArrayToDriveTimeToTotalTime();

for (int[] assignmentKeyArray : mapAssignmentArrayToIndex.keySet()) {

double variableValue = mipWrapper.getVariableValue(assignmentKeyArray);

if (variableValue > 0) {

selectedAssignmentArrays.add(assignmentKeyArray);

}

}

LP Files

There are many standard file formats solvers use to describe the Linear and Integer Programs that they can solve. The tools at One Network take advantage of the LP (Linear Program). The solvers support a feature to export their problems in this format. Most engines created at One Network have an engine option that request the solver to export this format and save it the shared folder.

This LP File is a valuable debugging tool that can be provided to the OR guys in the AI team or even be passed to Gurobi technical support. Gurobi technical support is actually quite impressive in their ability to help in creating programs, debugging programs, and configuring Gurobi to best solve programs. This LP file can be used to help leverage their support.

Questions

  1. Identify the Constraint. (a)

    1. 2x + Y < 1000

    2. Max (X + Y)

    3. 2x

    4. 1000

  2. Identify the Monomial. (c)

    1. 2x + Y < 1000

    2. Max (X + Y)

    3. 2x

    4. 1000

  3. Identify the Objective. (b)

    1. 2x + Y < 1000

    2. Max (X + Y)

    3. 2x

    4. 1000

  4. Identify the valid types of Objectives. (d)

    1. Linear and Integer

    2. Real and Integer

    3. Low and High

    4. Min and Max

  5. When variables are declared, what data types may they be? (c)

    1. String, Object, Char

    2. Blob, Clob, and Date

    3. Real, Integer, and Boolean (some solvers)

    4. Solvers do not use data types

  6. The user wants to optimize. Which type of variables should be used? (a)

    1. Decision

    2. Working

    3. Imaginary

    4. Real

  7. Variables that are not being optimized but used to express the problem are: (b)

    1. Decision

    2. Working

    3. Imaginary

    4. Real

  8. Minimum number of Integer Variables to make an Integer Program: (d)

    1. None

    2. One

    3. A few

    4. All

  9. Minimum number of Integer Variables to make a Mixed Integer Program: (b)

    1. None

    2. One

    3. A few

    4. All

  10. Maximum number of Integer Variables to make a Linear Program: (a)

    1. None

    2. One

    3. A few

    4. All

  11. What is the most reasonable explanation for this objective function? (c)

    1. Minimize sum of X across the dimensions R, T and D

    2. Maximize product of X across the dimension R, T and D

    3. Maximize sum of X across the dimension R, T and D

    4. Maximize sum of X across the dimension R and D

  12. What is the most logical explanation for these linear constraints: y1≥x1+x2−1, y1≤x1, y1≤x2, where y1, x1, and x2 are boolean variables? (d)

    1. y1 is only 1 if X1 is 1 and X2 is 0

    2. y1 is only 1 if X1 is 0 and X2 is 0

    3. y1 is only 1 if X1 is 0 and X2 is 1

    4. y1 is only 1 if X1 is 1 and X2 is 1

  13. The following constraints are all equal. Which form works best with Gurobi solver? (b)

    1. X1+X2 > 10 – X3

    2. X1+X2 + X3 - 10 > 0

    3. X1 > 10 – X3 – X2

    4. 0 > 10 – X3 – X2 – X1

  14. Platform provides no support for Linear Programming. (b)

    1. True

    2. False

  15. What is the name of the Platform class that provides ability to create variables, constraints, objectives, and solver linear or integer programs? (c)

    1. MIPMaster

    2. MIPConfig

    3. MIPMap

    4. MIPWrapper

  16. What is the name of the Platform class that assists in the configuration of Platform class for linear and integer programs? (b)

    1. MIPMaster

    2. MIPConfig

    3. MIPMap

    4. MIPWrapper

  17. What is the name of the Platform class that assists in linking variables in the application program space and the solver space? (c)

    1. MIPMaster

    2. MIPConfig

    3. MIPMap

    4. MIPWrapper