LocalSolver logo
is now
Hexaly logo

We're excited to share that we are moving forward. We're leaving behind the LocalSolver brand and transitioning to our new identity: Hexaly. This represents a leap forward in our mission to enable every organization to make better decisions faster when faced with operational and strategic challenges.

This page is for an old version of Hexaly Optimizer. We recommend that you update your version and read the documentation for the latest stable release.

Flowshop¶

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

Problem¶

../_images/flowshop.png

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 jth job of the sequence on this machine. This processing time is simply an “at” operator to retrieve the processing time on the right index.

We have to initialize the end times of the first job on every machine, and of every job on the first machine in order to use the previous formula:

  • On the first machine, the first job ends after its processing time because it starts at 0.
  • On every other machine, the first job can start right after the previous machine because all machines are empty as it is the first job. Its end time is simply the end time of the previous machine plus the processing time which is got through an “at” operator.
  • On the first machine, each job starts right after the previous jobs has ended, because it was queuing to start processing. Its end time is simply the end time of the previous job plus the processing time which is got through an “at” operator.

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].

Execution:
localsolver flowshop.lsp inFileName=instances/tai20_5.txt [lsTimeLimit=] [solFileName=]
/********** flowshop.lsp **********/

use io;

/* Reads instance data */
function input() {
    local usage = "Usage: localsolver flowshop.lsp "
        + "inFileName=inputFile [lsTimeLimit=timeLimit]";

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

    nbJobs = inFile.readInt();
    nbMachines = inFile.readInt();
    initialSeed = inFile.readInt();
    upperBound = inFile.readInt();
    lowerBound = inFile.readInt();
    processingTime[m in 0..nbMachines-1][j in 0..nbJobs-1] = inFile.readInt();
}

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

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

    // end[m][j] is the end of the jth job on machine m

    // On machine 0, the first job ends after it has been processed.
    end[0][0] <- processingTime[0][jobs[0]];
    // On machine 0, the jth job ends on the time it took to be processed after 
    // the end of the previous job
    end[0][j in 1..nbJobs-1] <- end[0][j-1] + processingTime[0][jobs[j]];
    // On machine m, the first job ends on the time it took to be processed after 
    // the end on the previous machine
    end[m in 1..nbMachines-1][0] <- end[m-1][0] + processingTime[m][jobs[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-1][j in 1..nbJobs-1] {
        start <- max(end[m][j-1], end[m-1][j]);
        end[m][j] <- start + processingTime[m][jobs[j]];
    }

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

/* Parameterizes the solver. */
function param() {
    if (lsTimeLimit == nil) lsTimeLimit = 5;  
}

/* Writes 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=%LS_HOME%\bin\python27\
python flowshop.py instances\tai20_5.txt
Execution (Linux)
export PYTHONPATH=/opt/localsolver_XXX/bin/python27/
python flowshop.py instances/tai20_5.txt
########## flowshop.py ##########

import localsolver
import sys

if len(sys.argv) < 2:
    print ("Usage: python flowshop.py inputFile [outputFile] [timeLimit]")
    sys.exit(1)


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


with localsolver.LocalSolver() as ls:

    #
    # Reads instance data
    #
    file_it = iter(read_integers(sys.argv[1]))

    nb_jobs = int(file_it.next())
    nb_machines = int(file_it.next())
    initial_seed = int(file_it.next())
    upper_bound = int(file_it.next())
    lower_bound = int(file_it.next())

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

    #
    # Declares the optimization model
    #
    model = ls.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_array = [model.array(processing_time[m]) for m in range(nb_machines)]

    # end[m][j] is the end of the jth job on machine m
    end = [[None for j in range(nb_jobs)] for m in range(nb_machines)]

    # On machine 0, the first job ends after it has been processed.
    end[0][0] = processing_time_array[0][jobs[0]]
    # On machine 0, the jth job ends on the time it took to be processed after 
    # the end of the previous job
    for j in range(1, nb_jobs):
        end[0][j] = end[0][j-1] + processing_time_array[0][jobs[j]]
    # On machine m, the first job ends on the time it took to be processed after 
    # the end on the previous machine
    for m in range(1, nb_machines):
        end[m][0] = end[m-1][0] + processing_time_array[m][jobs[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):
        for j in range(1, nb_jobs):
            start = model.max(end[m][j-1], end[m-1][j])
            end[m][j] = start + processing_time_array[m][jobs[j]]

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

    model.close()

    #
    # Parameterizes the solver
    #
    if len(sys.argv) >= 4: ls.create_phase().time_limit = int(sys.argv[3])
    else: ls.create_phase().time_limit = 5

    ls.solve()

    #
    # Writes the solution in a file
    #
    if len(sys.argv) >= 3:
        with open(sys.argv[2], 'w') as f:
            f.write("%d\n" % makespan.value)
            for j in jobs.value:
                f.write("%d " % j)
            f.write("\n")
Compilation / Execution (Windows)
cl /EHsc flowshop.cpp -I%LS_HOME%\include /link %LS_HOME%\bin\localsolver.dll.lib
flowshop instances\tai20_5.txt
Compilation / Execution (Linux)
g++ flowshop.cpp -I/opt/localsolver_XXX/include -llocalsolver -lpthread -o flowshop
./flowshop instances/tai20_5.txt
/********** flowshop.cpp **********/

#include <iostream>
#include <fstream>
#include <vector>
#include "localsolver.h"

using namespace localsolver;
using namespace std;

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

    /* Number of machines */
	int nbMachines;

    /* Initial seed used to generate the instance */
	long initialSeed;

    /* Upper bound */
	int upperBound;

    /* Lower bound*/
	int lowerBound;

    /* Processing time */
    vector<vector<lsint> > processingTime;

    /* LocalSolver */
    LocalSolver localsolver;

    /* Decision variable */
    LSExpression jobs;

    /* Objective */
    LSExpression makespan;

    /* Reads instance data. */
    void readInstance(const string& fileName) {
        ifstream infile(fileName.c_str());
        if (!infile.is_open()) {
            cerr << "File " << fileName << " cannot be opened." << endl;
            exit(1);
        }
        infile >> nbJobs;
        infile >> nbMachines;
        infile >> initialSeed;
        infile >> upperBound;
        infile >> lowerBound;

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

    void solve(int limit) {
        try {
            /* Declares the optimization model. */
            LSModel model = localsolver.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<LSExpression> processingTimeArray(nbMachines);
            for (int m = 0; m < nbMachines; m++) {
                processingTimeArray[m] = model.array(processingTime[m].begin(), processingTime[m].end());
            }            

            // end[m][j] is the end of the jth job on machine m
            vector<vector<LSExpression> > end(nbMachines);
            for (int m = 0; m < nbMachines; m++) {
                end[m].resize(nbJobs);
            }

            // On machine 0, the first job ends after it has been processed.
            end[0][0] = processingTimeArray[0][jobs[0]];
            // On machine 0, the jth job ends on the time it took to be processed after 
            // the end of the previous job
            for (int j = 1; j < nbJobs; j++) {
                end[0][j] = end[0][j-1] + processingTimeArray[0][jobs[j]];;
            }
            // On machine m, the first job ends on the time it took to be processed after 
            // the end on the previous machine
            for (int m = 1; m < nbMachines; m++) {
            	end[m][0] = end[m-1][0] + processingTimeArray[m][jobs[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++) {
                for (int j = 1; j < nbJobs; j++) {
                    LSExpression start = model.max(end[m][j-1], end[m-1][j]);
                    end[m][j] = start + processingTimeArray[m][jobs[j]];
                }
            }

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

            model.close();

            /* Parameterizes the solver. */
            LSPhase phase = localsolver.createPhase();
            phase.setTimeLimit(limit);
            localsolver.solve();
            
        } catch (const LSException& e) {
            cout << "LSException:" << e.getMessage() << endl;
            exit(1);
        }
    }

    /* Writes the solution in a file */
    void writeSolution(const string& fileName) {
        ofstream outfile(fileName.c_str());
        if (!outfile.is_open()) {
            cerr << "File " << fileName << " cannot be opened." << endl;
            exit(1);
        }
        outfile << makespan.getValue() << endl;
        LSCollection jobsCollection = jobs.getCollectionValue();
        for (int j = 0; j < nbJobs; j++) {
            outfile << jobsCollection[j] << " ";
        }
        outfile << endl;
        outfile.close();
    }
};

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

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

    Flowshop model;
    model.readInstance(instanceFile);
    model.solve(atoi(strTimeLimit));
    if(solFile != NULL)
        model.writeSolution(solFile);
    return 0;
}
Compilation / Execution (Windows)
javac Flowshop.java -cp %LS_HOME%\bin\localsolver.jar
java -cp %LS_HOME%\bin\localsolver.jar;. Flowshop instances\tai20_5.txt
Compilation/Execution (Linux)
javac Flowshop.java -cp /opt/localsolver_XXX/bin/localsolver.jar
java -cp /opt/localsolver_XXX/bin/localsolver.jar:. Flowshop instances/tai20_5.txt
/********** Flowshop.java **********/

import java.util.*;
import java.io.*;
import localsolver.*;

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

    /* Number of machines */
	int nbMachines;

    /* Initial seed used to generate the instance */
	long initialSeed;

    /* Upper bound */
	int upperBound;

    /* Lower bound*/
	int lowerBound;

    /* Processing time */
    long[][] processingTime;

    /* LocalSolver */
    LocalSolver localsolver;

    /* Decision variable */
    LSExpression jobs;

    /* Objective */
    LSExpression makespan;

    /* Reads instance data. */
    void readInstance(String fileName) {
        try {
            Scanner input = new Scanner(new File(fileName));

            nbJobs = input.nextInt();
            nbMachines = input.nextInt();
            initialSeed = input.nextInt();
            upperBound = input.nextInt();
            lowerBound = input.nextInt();

            processingTime = new long[nbMachines][nbJobs];
            for (int m = 0; m < nbMachines; m++) {
                for(int j = 0; j < nbJobs; j++) {
                    processingTime[m][j] = input.nextInt();
                }
            }
            input.close();
            
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    void solve(int limit) {
        try {
            localsolver = new LocalSolver();

            /* Declares the optimization model. */
            LSModel model = localsolver.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
            LSExpression[] processingTimeArray = new LSExpression[nbMachines];
            for (int m = 0; m < nbMachines; m++) {
                processingTimeArray[m] = model.array(processingTime[m]);
            }
   
			// As the operator [] is not overloaded, create the explicit list of jobs for easy access
            LSExpression[] explicitJobs = new LSExpression[nbJobs];
            for (int j = 0; j < nbJobs; j++) {
                explicitJobs[j] = model.at(jobs, j);
            }

            // end[m][j] is the end of the jth job on machine m
            LSExpression[][] end = new LSExpression[nbMachines][nbJobs];

            // On machine 0, the first job ends after it has been processed.
            end[0][0] = model.at(processingTimeArray[0], explicitJobs[0]);
            // On machine 0, the jth job ends on the time it took to be processed after 
            // the end of the previous job
            for (int j = 1; j < nbJobs; j++) {
                end[0][j] = model.sum(end[0][j-1], model.at(processingTimeArray[0], explicitJobs[j]));
            }
            // On machine m, the first job ends on the time it took to be processed after 
            // the end on the previous machine
            for (int m = 1; m < nbMachines; m++) {
            	end[m][0] = model.sum(end[m-1][0], model.at(processingTimeArray[m], explicitJobs[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++) {
                for (int j = 1; j < nbJobs; j++) {
                    LSExpression start = model.max(end[m][j-1], end[m-1][j]);
                    end[m][j] = model.sum(start, model.at(processingTimeArray[m], explicitJobs[j]));
                }
            }

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

            model.close();

            /* Parameterizes the solver. */
            LSPhase phase = localsolver.createPhase();
            phase.setTimeLimit(limit);

            localsolver.solve();

        } catch (LSException e) {
            System.out.println("LSException:" + e.getMessage());
            System.exit(1);
        }
    }

    /* Writes the solution in a file */
    void writeSolution(String fileName) {
        try {
            BufferedWriter output = new BufferedWriter(new FileWriter(fileName));
            output.write(makespan.getValue() + "\n");
            LSCollection jobsCollection = jobs.getCollectionValue();
            for (int j = 0; j < nbJobs; j++) {
                output.write(jobsCollection.get(j) + " ");
            }
            output.write("\n");
            output.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
     public static void main(String[] args) {
        if (args.length < 1) {
            System.out.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] : "20";

        Flowshop model = new Flowshop();
        model.readInstance(instanceFile);
        model.solve(Integer.parseInt(strTimeLimit));
        if(outputFile != null) {
            model.writeSolution(outputFile);
        }
    }
}
Compilation/Execution (Windows)
copy %LS_HOME%\bin\*net.dll .
csc Flowshop.cs /reference:localsolvernet.dll
Flowshop instances\tai20_5.txt
/********** Flowshop.cs **********/

using System;
using System.IO;
using System.Collections.Generic;
using localsolver;


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

    /* Number of machines */
	int nbMachines;

    /* Initial seed used to generate the instance */
	long initialSeed;

    /* Upper bound */
	int upperBound;

    /* Lower bound*/
	int lowerBound;

    /* Processing time */
    long[][] processingTime;

    /* LocalSolver */
    LocalSolver localsolver;

    /* Decision variable */
    LSExpression jobs;

    /* Objective */
    LSExpression makespan;

    public Flowshop ()
    {
        localsolver = new LocalSolver();
    }

    /* Reads 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]);
            initialSeed = int.Parse(firstLineSplit[2]);
            upperBound = int.Parse(firstLineSplit[3]);
            lowerBound = int.Parse(firstLineSplit[4]);

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

    public void Dispose ()
    {
        localsolver.Dispose();
    }

    void Solve(int limit)
    {
        /* Declares the optimization model. */
        LSModel model = localsolver.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
        LSExpression[] processingTimeArray = new LSExpression[nbMachines];
        for (int m = 0; m < nbMachines; m++)
        {
            processingTimeArray[m] = model.Array(processingTime[m]);
        }            

        // end[m][j] is the end of the jth job on machine m
        LSExpression[,] end = new LSExpression[nbMachines, nbJobs];

        // On machine 0, the first job ends after it has been processed.
        end[0,0] = processingTimeArray[0][jobs[0]];
        // On machine 0, the jth job ends on the time it took to be processed after 
        // the end of the previous job
        for (int j = 1; j < nbJobs; j++)
        {
            end[0,j] = end[0,j-1] + processingTimeArray[0][jobs[j]];
        }
        // On machine m, the first job ends on the time it took to be processed after 
        // the end on the previous machine
        for (int m = 1; m < nbMachines; m++)
        {
        	end[m,0] = end[m-1,0] + processingTimeArray[m][jobs[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++)
        {
            for (int j = 1; j < nbJobs; j++)
            {
                LSExpression start = model.Max(end[m,j-1], end[m-1,j]);
                end[m,j] = start + processingTimeArray[m][jobs[j]];
            }
        }

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

        model.Close();
        
        /* Parameterizes the solver. */
        LSPhase phase = localsolver.CreatePhase();
        phase.SetTimeLimit(limit);

        localsolver.Solve();
    }

    /* Writes the solution in a file */
    void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            output.WriteLine(makespan.GetValue());
            LSCollection 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]");
            System.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);
        }
    }   
}