How to warm-start a solver
Many solvers permit the possibility of giving a valid (or parcially valid in some cases) solution so the solver can start from that solution. This can lead to performance gains.
Supported solver APIs
The present solver APIs that work with PuLP warm-start are the following: CPLEX_CMD
, GUROBI_CMD
, PULP_CBC_CMD
, CBC_CMD
, CPLEX_PY
, GUROBI
, XPRESS
, XPRESS_PY
.
Example problem
We will use as example the model in A Set Partitioning Problem. At the end is the complete modified code.
Filling a variable with a value
If a model has been previously solved, each variable has already a value. To check the value of a variable we can do it via the value
method of the variable.
In our example, if we solve the problem, we could just do the following afterwards:
x[('O', 'P', 'Q', 'R')].value() # 1.0
x[('K', 'N', 'O', 'R')].value() # 0.0
If we have not yet solved the model, we can use the setInitialValue
method to assign a value to the variable.
In our example, if we want to get those two same values, we would do the following:
x[('O', 'P', 'Q', 'R')].setInitialValue(1)
x[('K', 'N', 'O', 'R')].setInitialValue(0)
Activating MIP start
Once we have assigned values to all variables and we want to run a model while reusing those values, we just need to pass the warmStart=True
argument to the solver when initiating it.
For example, using the default PuLP solver we would do:
seating_model.solve(pulp.PULP_CBC_CMD(msg=True, warmStart=True))
I usually turn msg=True
so I can see the messages from the solver confirming it loaded the solution correctly.
Fixing a variable
Assigning values to variables also permits fixing those variables to that value. In order to do that, we use the fixValue
method of the variable.
For our example, if we know some variable needs to be 1, we can do:
_variable = x[('O', 'P', 'Q', 'R')]
_variable.setInitialValue(1)
_variable.fixValue()
This implies setting the lower bound and the upperbound to the value of the variable.
Whole Example
If you want to see the complete code of the warm start version of the example, click here
or see below.
"""
A set partitioning model of a wedding seating problem
Adaptation where an initial solution is given to solvers: CPLEX_CMD, GUROBI_CMD, PULP_CBC_CMD
Authors: Stuart Mitchell 2009, Franco Peschiera 2019
"""
import pulp
max_tables = 5
max_table_size = 4
guests = "A B C D E F G I J K L M N O P Q R".split()
def happiness(table):
"""
Find the happiness of the table
- by calculating the maximum distance between the letters
"""
return abs(ord(table[0]) - ord(table[-1]))
# create list of all possible tables
possible_tables = [tuple(c) for c in pulp.allcombinations(guests, max_table_size)]
# create a binary variable to state that a table setting is used
x = pulp.LpVariable.dicts(
"table", possible_tables, lowBound=0, upBound=1, cat=pulp.LpInteger
)
seating_model = pulp.LpProblem("Wedding Seating Model", pulp.LpMinimize)
seating_model += pulp.lpSum([happiness(table) * x[table] for table in possible_tables])
# specify the maximum number of tables
seating_model += (
pulp.lpSum([x[table] for table in possible_tables]) <= max_tables,
"Maximum_number_of_tables",
)
# A guest must seated at one and only one table
for guest in guests:
seating_model += (
pulp.lpSum([x[table] for table in possible_tables if guest in table]) == 1,
f"Must_seat_{guest}",
)
# I've taken the optimal solution from a previous solving. x is the variable dictionary.
solution = {
("M", "N"): 1.0,
("E", "F", "G"): 1.0,
("A", "B", "C", "D"): 1.0,
("I", "J", "K", "L"): 1.0,
("O", "P", "Q", "R"): 1.0,
}
for k, v in solution.items():
x[k].setInitialValue(v)
solver = pulp.PULP_CBC_CMD(msg=True, warmStart=True)
# solver = pulp.CPLEX_CMD(msg=True, warmStart=True)
# solver = pulp.GUROBI_CMD(msg=True, warmStart=True)
# solver = pulp.CPLEX_PY(msg=True, warmStart=True)
# solver = pulp.GUROBI(msg=True, warmStart=True)
seating_model.solve(solver)
print(f"The chosen tables are out of a total of {len(possible_tables)}:")
for table in possible_tables:
if x[table].value() == 1.0:
print(table)