Flow Shop

Principles learned

  • Define the actual set of decision variables

  • Add a list decision variable

  • Access the list elements with an “at” operator

  • Access an array with an “at” operator at an index that is an expression

  • Define a sequence of expressions

  • Define an array with a recursive function

Problem

../_images/flowshop.svg

A set of jobs has to be processed on every machine of the shop. Each machine can work in parallel but the sequence of jobs on every machine must be the same. Each machine can only process one job at a time. The workflow is the following: the first job of the sequence goes to the first machine to be processed; meanwhile, other jobs wait; when the first machine has processed the first job, the first job goes to the second machine and the second job of the sequence starts to be processed by the first machine; and so on. If a job has been processed by a machine but the next machine is still busy, it waits until the next machine is empty, but it leaves its last machine empty.

The goal is to find a sequence of jobs that minimize the makespan: the time when all jobs have been processed.

Download the example


Data

The instances provided are from Taillard. The format of the data files is as follows:

  • First line: number of jobs, number of machines, seed used to generate the instance, upper and lower bound previously found.

  • For each machine: the processing time of each job on this machine

Program

The only decision variable of the model is a list variable which describes the sequence of jobs. We constrain all the jobs to be processed thanks to the count operator.

In general, a job starts on a machine when it has been processed by the previous machine and when the machine is empty (when the previous job has been processed by the machine): it is the maximum of these two times. The start time expressions are created as so with the “max” operator. A job ends on a machine after it has been processed. The end time expressions are created by summing the start time and the processing time of the j-th job of the sequence on this machine. This processing time is simply an at operator to retrieve the processing time on the right index.

The definition of ending times for the first machine will be different to the definition for other machines:

  • On the first machine, each job starts right after the previous job has ended, because it was queuing to start processing; and the first job starts at t=0. It can be written as a recursive array. Note that the conventional value (0) of the predecessor of the first item is appropriate here since the first job starts at 0.

  • On every other machine, the end time of each job depends on the end of the previous job in the sequence and the end of the previous job on the same machine. Here again this definition is written with a recursive array.

The makespan to minimize is just the time when the last job of the sequence has been processed by the last machine: end[nbMachines-1][nbJobs-1].

If you are interested in the general case, where the ordering on each machine is free, you can now study our jobshop model.

Execution:
hexaly flowshop.hxm inFileName=instances/tai20_5.txt [hxTimeLimit=] [solFileName=]
use io;

/* Read instance data */
function input() {
    local usage = "Usage: hexaly flowshop.hxm "
            + "inFileName=inputFile [hxTimeLimit=timeLimit]";

    if (inFileName == nil) throw usage;
    local inFile = io.openRead(inFileName);

    nbJobs = inFile.readInt();
    nbMachines = inFile.readInt();
    inFile.readInt();
    inFile.readInt();
    inFile.readInt();
    processingTime[m in 0...nbMachines][j in 0...nbJobs] = inFile.readInt();
}

/* Declare the optimization model */
function model() {
    // Permutation of jobs
    jobs <- list(nbJobs);

    // All jobs have to be assigned
    constraint count(jobs) == nbJobs;

    // On machine 0, the jth job ends on the time it took to be processed after 
    // the end of the previous job
    jobEnd[0] <- array(0...nbJobs, (i, prev) => prev + processingTime[0][jobs[i]], 0);

    // The jth job on machine m starts when it has been processed by machine n-1
    // AND when job j-1 has been processed on machine m.
    // It ends after it has been processed.
    for [m in 1...nbMachines]
        jobEnd[m] <- array(0...nbJobs,
                (i, prev) => max(prev, jobEnd[m-1][i]) + processingTime[m][jobs[i]], 0);

    // Minimize the makespan: end of the last job on the last machine
    makespan <- jobEnd[nbMachines-1][nbJobs-1];
    minimize makespan;
}

/* Parametrize the solver */
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 5;
}

/* Write the solution in a file */
function output() {
    if (solFileName == nil) return;

    local solFile = io.openWrite(solFileName);
    solFile.println(makespan.value);
    for [j in jobs.value]
        solFile.print(j + " ");
    solFile.println();
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python flowshop.py instances\tai20_5.txt
Execution (Linux)
export PYTHONPATH=/opt/hexaly_13_0/bin/python
python flowshop.py instances/tai20_5.txt
import hexaly.optimizer
import sys

def read_integers(filename):
    with open(filename) as f:
        return [int(elem) for elem in f.read().split()]

#
# Read instance data
#
def read_instance(instance_file):
    file_it = iter(read_integers(instance_file))

    nb_jobs = int(next(file_it))
    nb_machines = int(next(file_it))
    next(file_it)
    next(file_it)
    next(file_it)

    processing_time_data = [[int(next(file_it)) for j in range(nb_jobs)]
                            for j in range(nb_machines)]

    return nb_jobs, nb_machines, processing_time_data

def main(instance_file, output_file, time_limit):
    nb_jobs, nb_machines, processing_time_data = read_instance(instance_file)

    with hexaly.optimizer.HexalyOptimizer() as optimizer:
        #
        # Declare the optimization model
        #
        model = optimizer.model

        # Permutation of jobs
        jobs = model.list(nb_jobs)

        # All jobs have to be assigned
        model.constraint(model.eq(model.count(jobs), nb_jobs))

        # For each machine create proccessingTime[m] as an array to be able
        # to access it with an 'at' operator
        processing_time = [model.array(processing_time_data[m])
                           for m in range(nb_machines)]

        # On machine 0, the jth job ends on the time it took to be processed
        # after the end of the previous job
        job_end = [None] * nb_machines

        first_end_lambda = model.lambda_function(lambda i, prev:
                                                 prev + processing_time[0][jobs[i]])

        job_end[0] = model.array(model.range(0, nb_jobs), first_end_lambda, 0)

        # The jth job on machine m starts when it has been processed by machine n-1
        # AND when job j-1 has been processed on machine m.
        # It ends after it has been processed.
        for m in range(1, nb_machines):
            mL = m
            end_lambda = model.lambda_function(lambda i, prev:
                model.max(prev, job_end[mL - 1][i]) + processing_time[mL][jobs[i]])
            job_end[m] = model.array(model.range(0, nb_jobs), end_lambda, 0)

        # Minimize the makespan: end of the last job on the last machine
        makespan = job_end[nb_machines - 1][nb_jobs - 1]
        model.minimize(makespan)

        model.close()

        # Parameterize the optimizer
        optimizer.param.time_limit = time_limit

        optimizer.solve()

        #
        # Write the solution in a file
        #
        if output_file is not None:
            with open(output_file, 'w') as f:
                f.write("%d\n" % makespan.value)
                for j in jobs.value:
                    f.write("%d " % j)
                f.write("\n")


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python flowshop.py instance_file [output_file] [time_limit]")
        sys.exit(1)

    instance_file = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) >= 3 else None
    time_limit = int(sys.argv[3]) if len(sys.argv) >= 4 else 5
    main(instance_file, output_file, time_limit)

Compilation / Execution (Windows)
cl /EHsc flowshop.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly130.lib
flowshop instances\tai20_5.txt
Compilation / Execution (Linux)
g++ flowshop.cpp -I/opt/hexaly_13_0/include -lhexaly130 -lpthread -o flowshop
./flowshop instances/tai20_5.txt
#include "optimizer/hexalyoptimizer.h"
#include <fstream>
#include <iostream>
#include <vector>

using namespace hexaly;
using namespace std;

class Flowshop {
public:
    // Number of jobs
    int nbJobs;

    // Number of machines
    int nbMachines;

    // Processing time
    vector<vector<int>> processingTimeData;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;

    // Decision variable
    HxExpression jobs;

    // Objective
    HxExpression makespan;

    /* Read instance data */
    void readInstance(const string& fileName) {
        ifstream infile;
        infile.exceptions(ifstream::failbit | ifstream::badbit);
        infile.open(fileName.c_str());

        long tmp;
        infile >> nbJobs;
        infile >> nbMachines;
        infile >> tmp;
        infile >> tmp;
        infile >> tmp;

        processingTimeData.resize(nbMachines);
        for (int m = 0; m < nbMachines; ++m) {
            processingTimeData[m].resize(nbJobs);
            for (int j = 0; j < nbJobs; ++j) {
                infile >> processingTimeData[m][j];
            }
        }
    }

    void solve(int limit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Permutation of jobs
        jobs = model.listVar(nbJobs);

        // All jobs have to be assigned
        model.constraint(model.count(jobs) == nbJobs);

        // For each machine create proccessingTime[m] as an array to be able to access it
        // with an 'at' operator
        vector<HxExpression> processingTime(nbMachines);
        for (int m = 0; m < nbMachines; ++m) {
            processingTime[m] = model.array(processingTimeData[m].begin(), processingTimeData[m].end());
        }

        // On machine 0, the jth job ends on the time it took to be processed after
        // the end of the previous job
        vector<HxExpression> jobEnd(nbMachines);
        HxExpression firstEndLambda = model.createLambdaFunction(
            [&](HxExpression i, HxExpression prev) { return prev + processingTime[0][jobs[i]]; });
        jobEnd[0] = model.array(model.range(0, nbJobs), firstEndLambda, 0);

        // The jth job on machine m starts when it has been processed by machine n-1
        // AND when job j-1 has been processed on machine m. It ends after it has been processed.
        for (int m = 1; m < nbMachines; ++m) {
            int mL = m;
            HxExpression endLambda = model.createLambdaFunction([&](HxExpression i, HxExpression prev) {
                return model.max(prev, jobEnd[mL - 1][i]) + processingTime[mL][jobs[i]];
            });
            jobEnd[m] = model.array(model.range(0, nbJobs), endLambda, 0);
        }

        // Minimize the makespan: end of the last job on the last machine
        makespan = jobEnd[nbMachines - 1][nbJobs - 1];
        model.minimize(makespan);
        model.close();

        // Parametrize the optimizer
        optimizer.getParam().setTimeLimit(limit);

        optimizer.solve();
    }

    /* Write the solution in a file */
    void writeSolution(const string& fileName) {
        ofstream outfile;
        outfile.exceptions(ofstream::failbit | ofstream::badbit);
        outfile.open(fileName.c_str());

        outfile << makespan.getValue() << endl;
        HxCollection jobsCollection = jobs.getCollectionValue();
        for (int j = 0; j < nbJobs; ++j) {
            outfile << jobsCollection[j] << " ";
        }
        outfile << endl;
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "Usage: flowshop inputFile [outputFile] [timeLimit]" << endl;
        return 1;
    }

    const char* instanceFile = argv[1];
    const char* solFile = argc > 2 ? argv[2] : NULL;
    const char* strTimeLimit = argc > 3 ? argv[3] : "5";

    try {
        Flowshop model;
        model.readInstance(instanceFile);
        model.solve(atoi(strTimeLimit));
        if (solFile != NULL)
            model.writeSolution(solFile);
        return 0;
    } catch (const exception& e) {
        cerr << "An error occurred: " << e.what() << endl;
        return 1;
    }
}
Compilation / Execution (Windows)
copy %HX_HOME%\bin\Hexaly.NET.dll .
csc Flowshop.cs /reference:Hexaly.NET.dll
Flowshop instances\tai20_5.txt
using System;
using System.IO;
using Hexaly.Optimizer;

public class Flowshop : IDisposable
{
    // Number of jobs
    int nbJobs;

    // Number of machines
    int nbMachines;

    // Processing time
    long[][] processingTimeData;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;

    // Decision variable
    HxExpression jobs;

    // Objective
    HxExpression makespan;

    public Flowshop()
    {
        optimizer = new HexalyOptimizer();
    }

    /* Read instance data */
    void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName))
        {
            string[] firstLineSplit = input
                .ReadLine()
                .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
            nbJobs = int.Parse(firstLineSplit[0]);
            nbMachines = int.Parse(firstLineSplit[1]);

            string[] matrixText = input
                .ReadToEnd()
                .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
            processingTimeData = new long[nbMachines][];
            for (int m = 0; m < nbMachines; ++m)
            {
                processingTimeData[m] = new long[nbJobs];
                for (int j = 0; j < nbJobs; ++j)
                    processingTimeData[m][j] = long.Parse(matrixText[m * nbJobs + j]);
            }
        }
    }

    public void Dispose()
    {
        if (optimizer != null)
            optimizer.Dispose();
    }

    void Solve(int limit)
    {
        // Declare the optimization model
        HxModel model = optimizer.GetModel();

        // Permutation of jobs
        jobs = model.List(nbJobs);

        // All jobs have to be assigned
        model.Constraint(model.Count(jobs) == nbJobs);

        // For each machine create proccessingTime[m] as an array to be able to access it
        // with an 'at' operator
        HxExpression[] processingTime = new HxExpression[nbMachines];
        for (int m = 0; m < nbMachines; ++m)
            processingTime[m] = model.Array(processingTimeData[m]);

        // On machine 0, the jth job ends on the time it took to be processed after
        // the end of the previous job
        HxExpression[] jobEnd = new HxExpression[nbJobs];
        HxExpression firstEndLambda = model.LambdaFunction(
            (i, prev) => prev + processingTime[0][jobs[i]]
        );
        jobEnd[0] = model.Array(model.Range(0, nbJobs), firstEndLambda, 0);

        // The jth job on machine m starts when it has been processed by machine n-1
        // AND when job j-1 has been processed on machine m. It ends after it has been processed.
        for (int m = 1; m < nbMachines; ++m)
        {
            HxExpression endLambda = model.LambdaFunction(
                (i, prev) => model.Max(prev, jobEnd[m - 1][i]) + processingTime[m][jobs[i]]
            );
            jobEnd[m] = model.Array(model.Range(0, nbJobs), endLambda, 0);
        }

        // Minimize the makespan: end of the last job on the last machine
        makespan = jobEnd[nbMachines - 1][nbJobs - 1];
        model.Minimize(makespan);

        model.Close();

        // Parametrize the optimizer
        optimizer.GetParam().SetTimeLimit(limit);

        optimizer.Solve();
    }

    /* Write the solution in a file */
    void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            output.WriteLine(makespan.GetValue());
            HxCollection jobsCollection = jobs.GetCollectionValue();
            for (int j = 0; j < nbJobs; ++j)
                output.Write(jobsCollection[j] + " ");
            output.WriteLine();
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Usage: Flowshop inputFile [solFile] [timeLimit]");
            Environment.Exit(1);
        }
        string instanceFile = args[0];
        string outputFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "5";

        using (Flowshop model = new Flowshop())
        {
            model.ReadInstance(instanceFile);
            model.Solve(int.Parse(strTimeLimit));
            if (outputFile != null)
                model.WriteSolution(outputFile);
        }
    }
}
Compilation / Execution (Windows)
javac Flowshop.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. Flowshop instances\tai20_5.txt
Compilation / Execution (Linux)
javac Flowshop.java -cp /opt/hexaly_13_0/bin/hexaly.jar
java -cp /opt/hexaly_13_0/bin/hexaly.jar:. Flowshop instances/tai20_5.txt
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;

public class Flowshop {
    // Number of jobs
    private int nbJobs;

    // Number of machines
    private int nbMachines;

    // Processing time
    private long[][] processingTimeData;

    // Hexaly Optimizer
    private final HexalyOptimizer optimizer;

    // Decision variable
    private HxExpression jobs;

    // Objective
    private HxExpression makespan;

    private Flowshop(HexalyOptimizer optimizer) {
        this.optimizer = optimizer;
    }

    /* Read instance data */
    private void readInstance(String fileName) throws IOException {
        try (Scanner input = new Scanner(new File(fileName))) {
            nbJobs = input.nextInt();
            nbMachines = input.nextInt();
            input.nextInt();
            input.nextInt();
            input.nextInt();

            processingTimeData = new long[nbMachines][nbJobs];
            for (int m = 0; m < nbMachines; ++m) {
                for (int j = 0; j < nbJobs; ++j) {
                    processingTimeData[m][j] = input.nextInt();
                }
            }
        }
    }

    private void solve(int limit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Permutation of jobs
        jobs = model.listVar(nbJobs);

        // All jobs have to be assigned
        model.constraint(model.eq(model.count(jobs), nbJobs));

        // For each machine create proccessingTime[m] as an array to be able to access it
        // with an 'at' operator
        HxExpression[] processingTime = new HxExpression[nbMachines];
        for (int m = 0; m < nbMachines; ++m) {
            processingTime[m] = model.array(processingTimeData[m]);
        }

        // On machine 0, the jth job ends on the time it took to be processed after
        // the end of the previous job
        HxExpression[] jobEnd = new HxExpression[nbJobs];
        HxExpression firstEndLambda = model
            .lambdaFunction((i, prev) -> model.sum(prev, model.at(processingTime[0], model.at(jobs, i))));
        jobEnd[0] = model.array(model.range(0, nbJobs), firstEndLambda, 0);

        // The jth job on machine m starts when it has been processed by machine n-1
        // AND when job j-1 has been processed on machine m. It ends after it has been processed.
        for (int m = 1; m < nbMachines; ++m) {
            final int mL = m;
            HxExpression endLambda = model.lambdaFunction((i, prev) -> model
                .sum(model.max(prev, model.at(jobEnd[mL - 1], i)), model.at(processingTime[mL], model.at(jobs, i))));
            jobEnd[m] = model.array(model.range(0, nbJobs), endLambda, 0);
        }

        // Minimize the makespan: end of the last job on the last machine
        makespan = model.at(jobEnd[nbMachines - 1], nbJobs - 1);
        model.minimize(makespan);
        model.close();

        // Parametrize the optimizer
        optimizer.getParam().setTimeLimit(limit);

        optimizer.solve();
    }

    /* Write the solution in a file */
    private void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            output.println(makespan.getValue());
            HxCollection jobsCollection = jobs.getCollectionValue();
            for (int j = 0; j < nbJobs; ++j) {
                output.print(jobsCollection.get(j) + " ");
            }
            output.println();
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java Flowshop inputFile [outputFile] [timeLimit]");
            System.exit(1);
        }

        String instanceFile = args[0];
        String outputFile = args.length > 1 ? args[1] : null;
        String strTimeLimit = args.length > 2 ? args[2] : "5";

        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            Flowshop model = new Flowshop(optimizer);
            model.readInstance(instanceFile);
            model.solve(Integer.parseInt(strTimeLimit));
            if (outputFile != null) {
                model.writeSolution(outputFile);
            }
        } catch (Exception ex) {
            System.err.println(ex);
            ex.printStackTrace();
            System.exit(1);
        }
    }
}