Flexible Job Shop Problem (FJSP)

Problem

In the Flexible Job Shop Problem (FJSP), a set of jobs has to be processed on the machines in the shop. Each job consists of an ordered sequence of tasks (called operations): each operation can only start when the previous one has ended. Each operation has a set of compatible machines, and must be processed by one of them. The processing time of an operation depends on the machine processing it. The machines are disjunctive: they can only process one task at a time. The objective is to minimize the makespan, which is the time when all jobs have ended.

Principles learned

Data

The instances we provide come from the Brandimarte [1] dataset. Their format is as follows:

  • First line: number of jobs, number of machines, average number of compatible machines per operation (not needed)
  • From the second line, for each job:
    • Number of operations in that job
    • For each operation:
      • Number of machines compatible with this operation
      • For each compatible machine: machine index and processing time on this machine

Program

The Hexaly model for the Flexible Job Shop Problem (FJSP) uses two kinds of decision variables: intervals and lists. On the one hand, we use interval variables to model the time ranges of the operations. On the other hand, we use list variables to represent the order of the tasks scheduled on each machine.

Using the ‘partition‘ operator, we ensure that each task is assigned to exactly one machine. Since there are compatibility constraints between the tasks and machines, not every assignment is feasible. For each operation, we filter out incompatible machines thanks to the ‘contains’ operator. Using the ‘find‘ operator, we can then retrieve the index of the machine that was chosen to process each task. This allows us to deduce the processing time of each operation, which depends on the chosen machine, and to constrain the length of each interval accordingly.

The precedence constraints are easily written: for each job, each operation of this job must start after the end of the previous operation. The disjunctive resource constraints can be formulated as follows: for all i, the task processed in position i+1 must start after the end of the task processed in position i. To model this constraint, we define a lambda function expressing the relationship between two consecutive activities. This function is then used within a variadic ‘and’ operator over all tasks processed processed by each machine. Note that the number of terms inside these ‘and’ expressions varies during the search, along with the size of the lists (the number of tasks assigned to each machine).

The objective consists in minimizing the makespan, which is the time when all the tasks have ended.

Execution
hexaly flexible_jobshop.hxm inFileName=instances/Mk01.fjs [outFileName=] [hxTimeLimit=]
use io;

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

    if (inFileName == nil) throw usage;

    // Constant for incompatible machines
    INFINITE = 1000000;

    inFile = io.openRead(inFileName);
    // Number of jobs
    nbJobs = inFile.readInt();
    // Number of machines
    nbMachines = inFile.readInt();
    inFile.readln(); // skip last number

    // Number of tasks
    nbTasks = 0;
    processingTime = {};
    // Processing time for each task, for each machine
    taskProcessingTime = {};
    // For each job, for each operation, the corresponding task id
    jobOperationTask = {};
    
    for [j in 0...nbJobs] {
        // Number of operations for each job
        nbOperations[j] = inFile.readInt();
        for [o in 0...nbOperations[j]] {
            local nbMachinesOperation = inFile.readInt();
            for [i in 0...nbMachinesOperation] {
                local machine = inFile.readInt() - 1;
                local time = inFile.readInt();
                processingTime[j][o][machine] = time;
                taskProcessingTime[nbTasks][machine] = time;
            }
            jobOperationTask[j][o] = nbTasks;
            nbTasks += 1;
        }
    }
    inFile.close();

    // Trivial upper bound for the start times of the tasks
    maxStart = 0;
    for [j in 0...nbJobs][o in 0...nbOperations[j]] {
        local maxProcessingTime = 0;
        for [m in 0...nbMachines] {
            if (processingTime[j][o][m] == nil) {
                local task = jobOperationTask[j][o];
                taskProcessingTime[task][m] = INFINITE;
            } else if (processingTime[j][o][m] >= maxProcessingTime) {
                maxProcessingTime = processingTime[j][o][m];
            }
        }
        maxStart += maxProcessingTime;
    }
}

/* Declare the optimization model */
function model() {
    // Sequence of tasks on each machine
    jobsOrder[m in 0...nbMachines] <- list(nbTasks);

    // Each task is scheduled on a machine
    constraint partition[m in 0...nbMachines](jobsOrder[m]);

    // Only compatible machines can be selected for a task
    for [i in 0...nbTasks][m in 0...nbMachines : taskProcessingTime[i][m] == INFINITE]
        constraint !contains(jobsOrder[m], i);

    // For each task, the selected machine
    taskMachine[i in 0...nbTasks] <- find(jobsOrder, i);

    // Interval decisions: time range of each task
    tasks[i in 0...nbTasks] <- interval(0, maxStart);

    // The task duration depends on the selected machine
    duration[i in 0...nbTasks] <- taskProcessingTime[i][taskMachine[i]];
    for [i in 0...nbTasks]
        constraint length(tasks[i]) == duration[i];

    // Precedence constraints between the operations of a job
    for [j in 0...nbJobs][o in 0...nbOperations[j]-1] {
        local i1 = jobOperationTask[j][o];
        local i2 = jobOperationTask[j][o + 1];
        constraint tasks[i1] < tasks[i2];
    }

    // Disjunctive resource constraints between the tasks on a machine
    for [m in 0...nbMachines] {
        local sequence <- jobsOrder[m];
        constraint and(0...count(sequence)-1,
                i => tasks[sequence[i]] < tasks[sequence[i + 1]]);
    }

    // Minimize the makespan: end of the last task
    makespan <- max[i in 0...nbTasks](end(tasks[i]));
    minimize makespan;
}

/* Parameterize the solver */
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 60;
}

/* Write the solution in a file with the following format:
 *  - for each operation of each job, the selected machine, the start and end dates */
function output() {
    if (outFileName != nil) {
        outFile = io.openWrite(outFileName);
        println("Solution written in file ", outFileName);
        for [j in 0...nbJobs][o in 0...nbOperations[j]] {
            local taskIndex = jobOperationTask[j][o];
            outFile.println(j + 1, "\t", o + 1, "\t", taskMachine[taskIndex].value + 1, 
                    "\t", tasks[taskIndex].value.start, "\t", tasks[taskIndex].value.end);
        }
    }
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python flexible_jobshop.py instances\Mk01.fjs
Execution (Linux)
export PYTHONPATH=/opt/hexaly_13_0/bin/python
python flexible_jobshop.py instances/Mk01.fjs
import hexaly.optimizer
import sys

# Constant for incompatible machines
INFINITE = 1000000


def read_instance(filename):
    with open(filename) as f:
        lines = f.readlines()

    first_line = lines[0].split()
    # Number of jobs
    nb_jobs = int(first_line[0])
    # Number of machines
    nb_machines = int(first_line[1])

    # Number of operations for each job
    nb_operations = [int(lines[j + 1].split()[0]) for j in range(nb_jobs)]

    # Number of tasks
    nb_tasks = sum(nb_operations[j] for j in range(nb_jobs))

    # Processing time for each task, for each machine
    task_processing_time = [[INFINITE for m in range(nb_machines)] for i in range(nb_tasks)]

    # For each job, for each operation, the corresponding task id
    job_operation_task = [[0 for o in range(nb_operations[j])] for j in range(nb_jobs)]

    id = 0
    for j in range(nb_jobs):
        line = lines[j + 1].split()
        tmp = 0
        for o in range(nb_operations[j]):
            nb_machines_operation = int(line[tmp + o + 1])
            for i in range(nb_machines_operation):
                machine = int(line[tmp + o + 2 * i + 2]) - 1
                time = int(line[tmp + o + 2 * i + 3])
                task_processing_time[id][machine] = time
            job_operation_task[j][o] = id
            id = id + 1
            tmp = tmp + 2 * nb_machines_operation

    # Trivial upper bound for the start times of the tasks
    max_start = sum(
        max(task_processing_time[i][m] for m in range(nb_machines) if task_processing_time[i][m] != INFINITE)
        for i in range(nb_tasks))

    return nb_jobs, nb_machines, nb_tasks, task_processing_time, job_operation_task, nb_operations, max_start


def main(instance_file, output_file, time_limit):
    nb_jobs, nb_machines, nb_tasks, task_processing_time_data, job_operation_task, \
        nb_operations, max_start = read_instance(instance_file)

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

        # Sequence of tasks on each machine
        jobs_order = [model.list(nb_tasks) for _ in range(nb_machines)]
        machines = model.array(jobs_order)

        # Each task is scheduled on a machine
        model.constraint(model.partition(machines))

        # Only compatible machines can be selected for a task
        for i in range(nb_tasks):
            for m in range(nb_machines):
                if task_processing_time_data[i][m] == INFINITE:
                    model.constraint(model.not_(model.contains(jobs_order[m], i)))

        # For each task, the selected machine
        task_machine = [model.find(machines, i) for i in range(nb_tasks)]

        task_processing_time = model.array(task_processing_time_data)

        # Interval decisions: time range of each task
        tasks = [model.interval(0, max_start) for _ in range(nb_tasks)]

        # The task duration depends on the selected machine
        duration = [model.at(task_processing_time, i, task_machine[i]) for i in range(nb_tasks)]
        for i in range(nb_tasks):
            model.constraint(model.length(tasks[i]) == duration[i])

        task_array = model.array(tasks)

        # Precedence constraints between the operations of a job
        for j in range(nb_jobs):
            for o in range(nb_operations[j] - 1):
                i1 = job_operation_task[j][o]
                i2 = job_operation_task[j][o + 1]
                model.constraint(tasks[i1] < tasks[i2])

        # Disjunctive resource constraints between the tasks on a machine
        for m in range(nb_machines):
            sequence = jobs_order[m]
            sequence_lambda = model.lambda_function(
                lambda i: task_array[sequence[i]] < task_array[sequence[i + 1]])
            model.constraint(model.and_(model.range(0, model.count(sequence) - 1), sequence_lambda))

        # Minimize the makespan: end of the last task
        makespan = model.max([model.end(tasks[i]) for i in range(nb_tasks)])
        model.minimize(makespan)

        model.close()

        # Parameterize the optimizer
        optimizer.param.time_limit = time_limit

        optimizer.solve()

        # Write the solution in a file with the following format:
        # - for each operation of each job, the selected machine, the start and end dates
        if output_file != None:
            with open(output_file, "w") as f:
                print("Solution written in file", output_file)
                for j in range(nb_jobs):
                    for o in range(0, nb_operations[j]):
                        taskIndex = job_operation_task[j][o]
                        f.write(str(j + 1) + "\t" + str(o + 1)
                                + "\t" + str(task_machine[taskIndex].value + 1)
                                + "\t" + str(tasks[taskIndex].value.start())
                                + "\t" + str(tasks[taskIndex].value.end()) + "\n")


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python flexible_jobshop.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 60
    main(instance_file, output_file, time_limit)
Compilation / Execution (Windows)
cl /EHsc flexible_jobshop.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly130.lib
flexible_jobshop instances\Mk01.fjs
Compilation / Execution (Linux)
g++ flexible_jobshop.cpp -I/opt/hexaly_13_0/include -lhexaly130 -lpthread -o flexible_jobshop
./flexible_jobshop instances/Mk01.fjs
#include "optimizer/hexalyoptimizer.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <numeric>
#include <vector>

using namespace hexaly;

class FlexibleJobshop {
private:
    // Number of jobs
    int nbJobs;
    // Number of machines
    int nbMachines;
    // Number of tasks
    int nbTasks;
    // Processing time for each task, for each machine
    std::vector<std::vector<int>> taskProcessingTimeData;
    // For each job, for each operation, the corresponding task id
    std::vector<std::vector<int>> jobOperationTask;
    // Number of operations for each job
    std::vector<int> nbOperations;
    // Trivial upper bound for the start times of the tasks
    int maxStart;
    // Constant for incompatible machines
    const int INFINITE = 1000000;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;
    // Decision variables: time range of each task
    std::vector<HxExpression> tasks;
    // Decision variables: sequence of tasks on each machine
    std::vector<HxExpression> jobsOrder;
    // For each task, the selected machine
    std::vector<HxExpression> taskMachine;
    // Objective = minimize the makespan: end of the last task
    HxExpression makespan;

public:
    FlexibleJobshop() : optimizer() {}

    void readInstance(const std::string& fileName) {
        std::ifstream infile;
        infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        infile.open(fileName.c_str());

        infile >> nbJobs;
        infile >> nbMachines;
        infile.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // skip last number

        nbTasks = 0;
        std::vector<std::vector<std::vector<int>>> processingTime = std::vector<std::vector<std::vector<int>>>(nbJobs);
        jobOperationTask.resize(nbJobs);
        nbOperations.resize(nbJobs);
        for (unsigned int j = 0; j < nbJobs; ++j) {
            infile >> nbOperations[j];
            jobOperationTask[j].resize(nbOperations[j]);
            processingTime[j].resize(nbOperations[j]);
            for (unsigned int o = 0; o < nbOperations[j]; ++o) {
                int nbMachinesOperation;
                infile >> nbMachinesOperation;
                taskProcessingTimeData.push_back(std::vector<int>(nbMachines, INFINITE));
                processingTime[j][o].resize(nbMachines, INFINITE);
                for (int m = 0; m < nbMachinesOperation; ++m) {
                    int machine;
                    int time;
                    infile >> machine;
                    infile >> time;
                    processingTime[j][o][machine - 1] = time;
                    taskProcessingTimeData[nbTasks][machine - 1] = time;
                }
                jobOperationTask[j][o] = nbTasks;
                nbTasks += 1;
            }
        }
        infile.close();

        // Trivial upper bound for the start times of the tasks
        maxStart = 0;
        for (unsigned int j = 0; j < nbJobs; ++j) {
            for (unsigned int o = 0; o < nbOperations[j]; ++o) {
                int maxProcessingTime = 0;
                for (unsigned int m = 0; m < nbMachines; ++m) {
                    if (processingTime[j][o][m] != INFINITE && processingTime[j][o][m] > maxProcessingTime)
                        maxProcessingTime = processingTime[j][o][m];
                }
                maxStart += maxProcessingTime;
            }
        }
    }

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

        // Sequence of tasks on each machine
        jobsOrder.resize(nbMachines);
        HxExpression machines = model.array();
        for (unsigned int m = 0; m < nbMachines; ++m) {
            jobsOrder[m] = model.listVar(nbTasks);
            machines.addOperand(jobsOrder[m]);
        }

        // Each task is scheduled on a machine
        model.constraint(model.partition(machines));

        // Only compatible machines can be selected for a task
        for (int i = 0; i < nbTasks; ++i) {
            for (unsigned int m = 0; m < nbMachines; ++m) {
                if (taskProcessingTimeData[i][m] == INFINITE) {
                    model.constraint(!model.contains(jobsOrder[m], i));
                }
            }
        }

        taskMachine.resize(nbTasks);
        HxExpression taskProcessingTime = model.array();
        for (int i = 0; i < nbTasks; ++i) {
            // For each task, the selected machine
            taskMachine[i] = model.find(machines, i);
            taskProcessingTime.addOperand(
                model.array(taskProcessingTimeData[i].begin(), taskProcessingTimeData[i].end()));
        }

        tasks.resize(nbTasks);
        std::vector<HxExpression> duration(nbTasks);
        for (int i = 0; i < nbTasks; ++i) {
            // Interval decisions: time range of each task
            tasks[i] = model.intervalVar(0, maxStart);

            // The task duration depends on the selected machine
            duration[i] = model.at(taskProcessingTime, i, taskMachine[i]);
            model.constraint(model.length(tasks[i]) == duration[i]);
        }
        HxExpression taskArray = model.array(tasks.begin(), tasks.end());

        // Precedence constraints between the operations of a job
        for (unsigned int j = 0; j < nbJobs; ++j) {
            for (unsigned int o = 0; o < nbOperations[j] - 1; ++o) {
                int i1 = jobOperationTask[j][o];
                int i2 = jobOperationTask[j][o + 1];
                model.constraint(tasks[i1] < tasks[i2]);
            }
        }

        // Disjunctive resource constraints between the tasks on a machine
        for (int m = 0; m < nbMachines; ++m) {
            HxExpression sequence = jobsOrder[m];
            HxExpression sequenceLambda = model.createLambdaFunction(
                [&](HxExpression i) { return taskArray[sequence[i]] < taskArray[sequence[i + 1]]; });
            model.constraint(model.and_(model.range(0, model.count(sequence) - 1), sequenceLambda));
        }

        // Minimize the makespan: end of the last task
        makespan = model.max();
        for (int i = 0; i < nbTasks; ++i) {
            makespan.addOperand(model.end(tasks[i]));
        }
        model.minimize(makespan);

        model.close();

        // Parameterize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);

        optimizer.solve();
    }

    /* Write the solution in a file with the following format:
     *  - for each operation of each job, the selected machine, the start and end dates */
    void writeSolution(const std::string& fileName) {
        std::ofstream outfile(fileName.c_str());
        if (!outfile.is_open()) {
            std::cerr << "File " << fileName << " cannot be opened." << std::endl;
            exit(1);
        }
        std::cout << "Solution written in file " << fileName << std::endl;

        for (unsigned int j = 0; j < nbJobs; ++j) {
            for (unsigned int o = 0; o < nbOperations[j]; ++o) {
                int taskIndex = jobOperationTask[j][o];
                outfile << j + 1 << "\t" << o + 1 << "\t" << taskMachine[taskIndex].getValue() + 1 << "\t"
                        << tasks[taskIndex].getIntervalValue().getStart() << "\t"
                        << tasks[taskIndex].getIntervalValue().getEnd() << std::endl;
            }
        }
        outfile.close();
    }
};

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

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

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

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

    // Number of machines
    private int nbMachines;

    // Number of tasks
    private int nbTasks;

    // Processing time for each task, for each machine
    private long[][] taskProcessingTimeData;

    // For each job, for each operation, the corresponding task id
    private int[][] jobOperationTask;

    // Number of operations for each job;
    private int[] nbOperations;

    // Trivial upper bound for the start times of the tasks
    private long maxStart;

    // Constant for incompatible machines
    private const long INFINITE = 1000000;

    // Hexaly Optimizer
    private HexalyOptimizer optimizer;

    // Decision variables: time range of each task
    private HxExpression[] tasks;

    // Decision variables: sequence of tasks on each machine
    private HxExpression[] jobsOrder;

    // For each task, the selected machine
    private HxExpression[] taskMachine;

    // Objective = minimize the makespan: end of the last task
    private HxExpression makespan;

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

    public void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName))
        {
            char[] separators = new char[] { '\t', ' ' };
            string[] splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            nbJobs = int.Parse(splitted[0]);
            nbMachines = int.Parse(splitted[1]);

            nbTasks = 0;
            long[][][] processingTime = new long[nbJobs][][];
            jobOperationTask = new int[nbJobs][];
            nbOperations = new int[nbJobs];
            for (int j = 0; j < nbJobs; ++j)
            {
                splitted = input
                    .ReadLine()
                    .Split(separators, StringSplitOptions.RemoveEmptyEntries);
                nbOperations[j] = int.Parse(splitted[0]);
                jobOperationTask[j] = new int[nbOperations[j]];
                processingTime[j] = new long[nbOperations[j]][];
                int k = 1;
                for (int o = 0; o < nbOperations[j]; ++o)
                {
                    int nbMachinesOperation = int.Parse(splitted[k]);
                    k++;
                    processingTime[j][o] = Enumerable.Repeat((long)INFINITE, nbMachines).ToArray();
                    for (int m = 0; m < nbMachinesOperation; ++m)
                    {
                        int machine = int.Parse(splitted[k]) - 1;
                        long time = long.Parse(splitted[k + 1]);
                        processingTime[j][o][machine] = time;
                        k += 2;
                    }
                    jobOperationTask[j][o] = nbTasks;
                    nbTasks++;
                }
            }

            // Trivial upper bound for the start times of the tasks
            maxStart = 0;
            taskProcessingTimeData = new long[nbTasks][];
            for (int j = 0; j < nbJobs; ++j)
            {
                long maxProcessingTime = 0;
                for (int o = 0; o < nbOperations[j]; ++o)
                {
                    int task = jobOperationTask[j][o];
                    taskProcessingTimeData[task] = new long[nbMachines];
                    for (int m = 0; m < nbMachines; ++m)
                    {
                        taskProcessingTimeData[task][m] = processingTime[j][o][m];
                        if (
                            processingTime[j][o][m] != INFINITE
                            && processingTime[j][o][m] > maxProcessingTime
                        )
                        {
                            maxProcessingTime = processingTime[j][o][m];
                        }
                    }
                    maxStart += maxProcessingTime;
                }
            }
        }
    }

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

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

        // Sequence of tasks on each machine
        jobsOrder = new HxExpression[nbMachines];
        HxExpression machines = model.Array();
        for (int m = 0; m < nbMachines; ++m)
        {
            jobsOrder[m] = model.List(nbTasks);
            machines.AddOperand(jobsOrder[m]);
        }

        // Each task is scheduled on a machine
        model.Constraint(model.Partition(machines));

        // Only compatible machines can be selected for a task
        for (int i = 0; i < nbTasks; ++i)
        {
            for (int m = 0; m < nbMachines; ++m)
            {
                if (taskProcessingTimeData[i][m] == INFINITE)
                    model.Constraint(!model.Contains(jobsOrder[m], i));
            }
        }

        // For each task, the selected machine
        taskMachine = new HxExpression[nbTasks];
        for (int i = 0; i < nbTasks; ++i)
        {
            taskMachine[i] = model.Find(machines, i);
        }

        tasks = new HxExpression[nbTasks];
        HxExpression[] duration = new HxExpression[nbTasks];
        HxExpression taskProcessingTime = model.Array(taskProcessingTimeData);
        for (int i = 0; i < nbTasks; ++i)
        {
            // Interval decisions: time range of each task
            tasks[i] = model.Interval(0, maxStart);

            // The task duration depends on the selected machine
            HxExpression iExpr = model.CreateConstant(i);
            duration[i] = model.At(taskProcessingTime, iExpr, taskMachine[i]);
            model.Constraint(model.Length(tasks[i]) == duration[i]);
        }
        HxExpression taskArray = model.Array(tasks);

        // Precedence constraints between the operations of a job
        for (int j = 0; j < nbJobs; ++j)
        {
            for (int o = 0; o < nbOperations[j] - 1; ++o)
            {
                int i1 = jobOperationTask[j][o];
                int i2 = jobOperationTask[j][o + 1];
                model.Constraint(tasks[i1] < tasks[i2]);
            }
        }

        // Disjunctive resource constraints between the tasks on a machine
        for (int m = 0; m < nbMachines; ++m)
        {
            HxExpression sequence = jobsOrder[m];
            HxExpression sequenceLambda = model.LambdaFunction(
                i => taskArray[sequence[i]] < taskArray[sequence[i + 1]]
            );
            model.Constraint(model.And(model.Range(0, model.Count(sequence) - 1), sequenceLambda));
        }

        // Minimize the makespan: end of the last task
        makespan = model.Max();
        for (int i = 0; i < nbTasks; ++i)
        {
            makespan.AddOperand(model.End(tasks[i]));
        }
        model.Minimize(makespan);

        model.Close();

        // Parameterize the optimizer
        optimizer.GetParam().SetTimeLimit(timeLimit);

        optimizer.Solve();
    }

    /* Write the solution in a file with the following format:
     *  - for each operation of each job, the selected machine, the start and end dates */
    public void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            Console.WriteLine("Solution written in file " + fileName);
            for (int j = 1; j <= nbJobs; ++j)
            {
                for (int o = 1; o <= nbOperations[j - 1]; ++o)
                {
                    int taskIndex = jobOperationTask[j - 1][o - 1];
                    output.WriteLine(
                        j
                            + "\t"
                            + o
                            + "\t"
                            + taskMachine[taskIndex].GetValue()
                            + "\t"
                            + tasks[taskIndex].GetIntervalValue().Start()
                            + "\t"
                            + tasks[taskIndex].GetIntervalValue().End()
                    );
                }
            }
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Usage: FlexibleJobshop instanceFile [outputFile] [timeLimit]");
            System.Environment.Exit(1);
        }

        string instanceFile = args[0];
        string outputFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "60";

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

public class FlexibleJobshop {
    // Number of jobs
    private int nbJobs;
    // Number of machines
    private int nbMachines;
    // Number of tasks
    private int nbTasks;
    // Processing time for each task, for each machine
    private long[][] taskProcessingTimeData;
    // For each job, for each operation, the corresponding task id
    private int[][] jobOperationTask;
    // Number of operations for each job;
    private int[] nbOperations;
    // Trivial upper bound for the start times of the tasks
    private long maxStart;
    // Constant for incompatible machines
    private final int INFINITE = 1000000;

    // Hexaly Optimizer
    final HexalyOptimizer optimizer;
    // Decision variables: time range of each task
    private HxExpression[] tasks;
    // Decision variables: sequence of tasks on each machine
    private HxExpression[] jobsOrder;
    // For each task, the selected machine
    private HxExpression[] taskMachine;
    // Objective = minimize the makespan: end of the last task
    private HxExpression makespan;

    public FlexibleJobshop(HexalyOptimizer optimizer) throws IOException {
        this.optimizer = optimizer;
    }

    public void readInstance(String fileName) throws IOException {
        try (Scanner input = new Scanner(new File(fileName))) {
            nbJobs = input.nextInt();
            nbMachines = input.nextInt();
            input.next(); // skip last number

            nbTasks = 0;
            long[][][] processingTime = new long[nbJobs][][];
            jobOperationTask = new int[nbJobs][];
            nbOperations = new int[nbJobs];
            for (int j = 0; j < nbJobs; ++j) {
                nbOperations[j] = input.nextInt();
                jobOperationTask[j] = new int[nbOperations[j]];
                processingTime[j] = new long[nbOperations[j]][nbMachines];
                for (int o = 0; o < nbOperations[j]; ++o) {
                    int nbMachinesOperation = input.nextInt();
                    Arrays.fill(processingTime[j][o], INFINITE);
                    for (int m = 0; m < nbMachinesOperation; ++m) {
                        int machine = input.nextInt() - 1;
                        long time = input.nextLong();
                        processingTime[j][o][machine] = time;
                    }
                    jobOperationTask[j][o] = nbTasks;
                    nbTasks++;
                }
            }

            // Trivial upper bound for the start times of the tasks
            maxStart = 0;
            taskProcessingTimeData = new long[nbTasks][];
            for (int j = 0; j < nbJobs; ++j) {
                long maxProcessingTime = 0;
                for (int o = 0; o < nbOperations[j]; ++o) {
                    int task = jobOperationTask[j][o];
                    taskProcessingTimeData[task] = new long[nbMachines];
                    for (int m = 0; m < nbMachines; ++m) {
                        taskProcessingTimeData[task][m] = processingTime[j][o][m];
                        if (processingTime[j][o][m] != INFINITE && processingTime[j][o][m] > maxProcessingTime) {
                            maxProcessingTime = processingTime[j][o][m];
                        }
                    }
                    maxStart += maxProcessingTime;
                }
            }
        }
    }

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

        // Sequence of tasks on each machine
        jobsOrder = new HxExpression[nbMachines];
        HxExpression machines = model.array();
        for (int m = 0; m < nbMachines; ++m) {
            jobsOrder[m] = model.listVar(nbTasks);
            machines.addOperand(jobsOrder[m]);
        }

        // Each task is scheduled on a machine
        model.constraint(model.partition(machines));

        // Only compatible machines can be selected for a task
        for (int i = 0; i < nbTasks; ++i) {
            for (int m = 0; m < nbMachines; ++m) {
                if (taskProcessingTimeData[i][m] == INFINITE) {
                    model.constraint(model.not(model.contains(jobsOrder[m], i)));
                }
            }
        }

        // For each task, the selected machine
        taskMachine = new HxExpression[nbTasks];
        for (int i = 0; i < nbTasks; ++i) {
            taskMachine[i] = model.find(machines, i);
        }

        HxExpression taskProcessingTime = model.array(taskProcessingTimeData);

        tasks = new HxExpression[nbTasks];
        HxExpression[] duration = new HxExpression[nbTasks];
        for (int i = 0; i < nbTasks; ++i) {
            // Interval decisions: time range of each task
            tasks[i] = model.intervalVar(0, maxStart);

            // The task duration depends on the selected machine
            HxExpression iExpr = model.createConstant(i);
            duration[i] = model.at(taskProcessingTime, iExpr, taskMachine[i]);
            model.constraint(model.eq(model.length(tasks[i]), duration[i]));
        }
        HxExpression taskArray = model.array(tasks);

        // Precedence constraints between the operations of a job
        for (int j = 0; j < nbJobs; ++j) {
            for (int o = 0; o < nbOperations[j] - 1; ++o) {
                int i1 = jobOperationTask[j][o];
                int i2 = jobOperationTask[j][o + 1];
                model.constraint(model.lt(tasks[i1], tasks[i2]));
            }
        }

        // Disjunctive resource constraints between the tasks on a machine
        for (int m = 0; m < nbMachines; ++m) {
            HxExpression sequence = jobsOrder[m];
            HxExpression sequenceLambda = model.lambdaFunction(i -> model
                    .lt(model.at(taskArray, model.at(sequence, i)),
                            model.at(taskArray, model.at(sequence, model.sum(i, 1)))));
            model.constraint(model.and(model.range(0, model.sub(model.count(sequence), 1)), sequenceLambda));
        }

        // Minimize the makespan: end of the last task
        makespan = model.max();
        for (int i = 0; i < nbTasks; ++i) {
            makespan.addOperand(model.end(tasks[i]));
        }
        model.minimize(makespan);

        model.close();

        // Parameterize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);

        optimizer.solve();
    }

    /*
     * Write the solution in a file with the following format:
     * - for each operation of each job, the selected machine, the start and end
     * dates
     */
    public void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            System.out.println("Solution written in file " + fileName);

            for (int j = 1; j <= nbJobs; ++j) {
                for (int o = 1; o <= nbOperations[j - 1]; ++o) {
                    int taskIndex = jobOperationTask[j - 1][o - 1];
                    output.write(j + "\t" + o
                            + "\t" + taskMachine[taskIndex].getValue()
                            + "\t" + tasks[taskIndex].getIntervalValue().getStart()
                            + "\t" + tasks[taskIndex].getIntervalValue().getEnd() + "\n");
                }
            }
        }
    }

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

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

        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            FlexibleJobshop model = new FlexibleJobshop(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);
        }
    }
}

Results

Hexaly Optimizer reaches an average gap of 1.47% on the Flexible Job Shop Problem (FJSP) in 1 minute of running time, on a large benchmark from the literature composed of instances with up to 500 tasks and 60 machines. Our Flexible Job Shop (FJSP) benchmark page illustrates how Hexaly Optimizer outperforms traditional general-purpose optimization solvers like Gurobi on this challenging problem.


[1] P. Brandimarte (1993). Routing and scheduling in a flexible job shop by tabu search. Annals of Operations Research 22, 157-183.