Solving your first business problem in Python¶
The car sequencing problem is a well-known combinatorial optimization problem related to the organization of the production line of a car manufacturer. It involves scheduling a set of cars which are divided into classes by different sets of options. The assembly line has different stations where the various options (air-conditioning, sun-roof, etc.) are installed by teams. Each such station has a certain workload capacity given by the parameters P and Q: the station is able to handle P cars with the given option in each sequence of Q cars. The goal of the problem is to produce of a sequence of cars respecting the given demand for each class and such that overcapacities are minimized. In other words, for each option P/Q and for each window of length Q, a penalty max(0,N-P) is counted where N is the number of such options actually scheduled in this window.
In this section, we present a simple model for this problem. We illustrate how to read the input data from a file, we show how to model the Car Sequencing Problem by clearly defining the decisions, constraints, and objective of the problem, and we introduce some of the parameters that can be used to control the search. The full program for this problem is available in our example tour, together with its Hexaly Modeler, C++, Java, and C# versions, plus a certain number of instance files.
Reading input data¶
Data files for this problem are available in folder
hexaly/examples/carsequencing
and more files can be downloaded from
http://www.csplib.org (problem number 001). The format of these files is:
1st line: number of cars; number of options; number of classes.
2nd line: for each option, the maximum number of cars with this option in a block (parameter P).
3rd line: for each option, the block size to which the maximum number refers (parameter Q).
Then for each class: index of the class; number of cars in this class; for each option, whether or not this class requires it (0 or 1).
Below is the code to read the input data:
def read_integers(filename):
with open(filename) as f:
return [int(elem) for elem in f.read().split()]
file_it = iter(read_integers(sys.argv[1]))
nb_positions = next(file_it)
nb_options = next(file_it)
nb_classes = next(file_it)
P = [next(file_it) for i in range(nb_options)]
Q = [next(file_it) for i in range(nb_options)]
nb_cars = []
options = []
for c in range(nb_classes):
next(file_it)
nb_cars.append(next(file_it))
options.append([next(file_it) == 1 for i in range(nb_options)])
Modeling the problem¶
One of the most important steps when modeling an optimization problem with
Hexaly is to define the decision variables. These decisions are such that
instantiating them allows evaluating all other expressions of your model
(in particular, the constraints and objectives). Hexaly Optimizer will try
to find the best possible decisions with respect to the given objectives and
constraints. Here, we want to decide the ordering of cars in the production line.
In terms of 0-1 modeling, it amounts to defining nb_classes * nb_positions
variables cp[c][p]
such that cp[c][p]
is equal to 1 when the car at
position p
belongs to class c
and 0 otherwise:
cp = [[model.bool() for j in range(nb_positions)] for i in range(nb_classes)]
Defining the constraints of your model is another important modeling task. Generally speaking, constraints should be limited to strictly necessary requirements. In the car sequencing problem, it is physically impossible to have two cars at the same position. On the contrary, satisfying all P/Q ratios is not always possible, which is why we define it as a first-priority objective instead. Limiting the use of constraints is generally a good practice, as Hexaly Optimizer tends to benefit from denser search spaces. Therefore, symmetry-breaking constraints and other types of non-necessary constraints should also be avoided.
Here, we have two families of constraints expressing the assignment requirements: we must have exactly one car per position and the demand in cars for each class should be satisfied:
for c in range(nb_classes):
model.constraint(model.sum(cp[c][p] for p in range(nb_positions)) == nb_cars[c])
for p in range(nb_positions):
model.constraint(model.sum(cp[c][p] for c in range(nb_classes)) == 1)
Before writing the objective function, we introduce some intermediate
expressions. First, we declare expressions op[o][p]
, which are equal to 1
when the car at position p
has option o
and 0 otherwise:
for o in range(nb_options):
for p in range(nb_positions):
op[o][p] = model.or_(cp[c][p] for c in range(nb_classes) if options[c][o])
Indeed, option o
appears at position p
if this position hosts one of
the classes featuring this option. This ability to define intermediate
expressions makes the model more readable and more efficient (if these
intermediate expressions are used in several other expressions). Similarly,
the number of times option o
appears between position p
and position
p+Q[o]-1
is defined as:
for o in range(nb_options):
for p in range(nb_positions - Q[o] + 1):
nb_cars_in_window[o][p] = model.sum(op[o][p + k] for k in range(Q[o]))
Finally, the penalty function on overcapacities can be written exactly as it was defined above in the specifications of the problem:
for o in range(nb_options):
for p in range(nb_positions - Q[o] + 1):
nb_violations_in_window[o][p] = model.max(nb_cars_in_window[o][p] - P[o], 0);
Note that we are using here a different way to express a sum. Indeed, you can
define a sum with the sum
operator, but you can also use arithmetic
operators +
and -
. Ultimately, the last step consists in defining the
objective function, which is the sum of all violations:
obj = model.sum(nb_violations_in_window[o][p] for p in range(nb_positions - Q[o] + 1) for o in range(nb_options));
model.minimize(obj)
Here is the resulting code of the Hexaly model for the Car Sequencing Problem written using the Python API:
with hexaly.optimizer.HexalyOptimizer() as optimizer:
model = optimizer.model
# 0-1 decisions:
# cp[c][p] = 1 if class c is at position p, and 0 otherwise
cp = [[model.bool() for j in range(nb_positions)] for i in range(nb_classes)]
# Constraints:
# For each class c, nbCars[c] are assigned to the different positions
for c in range(nb_classes):
model.constraint(model.sum(cp[c][p] for p in range(nb_positions)) == nb_cars[c])
# Constraints: one car assigned to each position p
for p in range(nb_positions):
model.constraint(model.sum(cp[c][p] for c in range(nb_classes)) == 1)
# Intermediate expressions:
# op[o][p] = 1 if option o appears at position p, and 0 otherwise
op = [None]*nb_options
for o in range(nb_options):
op[o] = [None]*nb_positions
for p in range(nb_positions):
op[o][p] = model.or_(cp[c][p] for c in range(nb_classes) if options[c][o])
# Intermediate expressions: compute the number of cars in each window
nb_cars_in_window = [None]*nb_options
for o in range(nb_options):
nb_cars_in_window[o] = [None]*nb_positions
for p in range(nb_positions - Q[o] + 1):
nb_cars_in_window[o][p] = model.sum(op[o][p + k] for k in range(Q[o]))
# Intermediate expressions: compute the number of violations in each window
nb_violations_in_window = [None]*nb_options
for o in range(nb_options):
nb_violations_in_window[o] = [None]*nb_positions
for p in range(nb_positions - Q[o] + 1):
nb_violations_in_window[o][p] = model.max(nb_cars_in_window[o][p] + - P[o], 0);
# Objective function: minimize the sum of violations for all options and all windows
obj = model.sum(nb_violations_in_window[o][p] for p in range(nb_positions - Q[o] + 1) for o in range(nb_options));
model.minimize(obj)
model.close()
Parameterizing the optimizer¶
Some parameters can be defined after closing the model in order to control the search:
if len(sys.argv) >= 4: optimizer.param.time_limit = int(sys.argv[3])
else: optimizer.param.time_limit = 30
optimizer.param.time_between_displays = 5
All control parameters can be found in the HxParam class.
Having launched Hexaly Optimizer, you should observe the following output:
Hexaly 12.5.20240604-Win64. All rights reserved.
Load car_sequencing.hxm...
Run input...
Run model...
Preprocess model 100% ...
Run param...
Run solver...
Initialize solver 100% ...
Push initial solutions 100% ...
Model: expressions = 27001, decisions = 10000 constraints = 520, objectives = 1,
Param: time limit = 30 sec, no iteration limit
[ optimization direction ] minimize
[ 0 sec, 0 itr]: No feasible solution found (infeas = 1040)
[ 5 sec, 217424 itr]: obj = 33
[ 10 sec, 384536 itr]: obj = 24
[ 15 sec, 584611 itr]: obj = 19
[ 20 sec, 783454 itr]: obj = 15
[ 25 sec, 1043836 itr]: obj = 12
[ 30 sec, 1310253 itr]: obj = 10
[ 30 sec, 1310253 itr]: obj = 10
1310253 iterations performed in 31 seconds
Feasible solution:
obj = 10
gap = 100.00%
bounds = 0
Run output...
Every five seconds (in accordance with parameter time_between_displays
),
a brief report of the current state of the search is displayed: the number of
elapsed seconds and the number of iterations performed, and the values of the
objective functions given in lexicographic order (separated with |
).
You can disable all displays by setting parameter verbosity
to 0.
Writing solutions¶
Here, we define a function writing the solution of the problem in a file. It writes the solution in the following format:
First line = the objective value
Second line = the classes at positions 1 to nbPositions
You can retrieve the value of any expression (decisions, intermediate
expressions) thanks to the value
attribute:
with open(sys.argv[2], 'w') as f:
f.write("%d\n" % obj.value)
for p in range(nb_positions):
for c in range(nb_classes):
if cp[c][p].value == 1:
f.write("%d " % c)
break
f.write("\n")