Parallel Machine Scheduling Problem (PMS)

Scheduling

Problem

The Parallel Machine Scheduling Problem (PMS) is a classic problem in the scheduling literature. It consists in the scheduling of tasks on a set of parallel machines. In its most basic formulation, all the machines are identical (i.e. tasks can be scheduled on any machine) and every task can be processed by exactly one machine.

The goal is to find a schedule that minimizes the makespan: the maximum completion time of all the tasks. This variant of the Parallel Machine Scheduling Problem (PMS) is also referred to as P || Cmax , with the P indicating that there are identical parallel machines, and Cmax indicating that the optimization criterion is the makespan.

Principles learned

Data

To illustrate this example, we took a set of instances from the literature. The format of the Parallel Machine Scheduling (PMS) instances is as follows:

  • First line:
    • Number of tasks;
    • Number of machines;
  • Second line: the lengths associated with each task.

Model

The Hexaly model for the Parallel Machine Scheduling (PMS) uses set decision variables. For each machine, we define a set variable representing the set of tasks assigned to this machine. We constrain the set variables to form a partition, ensuring that each task is scheduled on exactly one machine.

We compute the makespan of a machine using a variadic sum operator on the set and a lambda function returning the length associated with any task index. Note that the number of terms in this sum varies during the search, along with the size of the set.

We use the max operator to extract the global makespan from each machine’s individual makespan. This expression defines the objective function that is minimized during the search.

Execution
hexaly parallel_scheduling.hxm inFileName=instances/p_cmax-n120-m3.txt [hxTimeLimit=] [solFileName=]
use io;

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

    if (inFileName == nil) throw usage;
    local inFile = io.openRead(inFileName);
    local line = inFile.readln();
    line = line.trim().split();

    nbTasks = line[2].toInt();
    nbMachines = line[3].toInt();
    taskLengths[0...nbTasks] = inFile.readInt();
    totalTaskLengths = sum[i in 0...nbTasks](taskLengths[i]);
    makespanLB = floor(totalTaskLengths / nbMachines);
}

/* Declare the optimization model */
function model() {
    // Set decisions: machineTasks[k] represents the tasks assigned to machine k
    machineTasks[0...nbMachines] <- set(nbTasks);

    // Each task must be scheduled on exactly one machine
    constraint partition[m in 0...nbMachines](machineTasks[m]);

    // Minimize the makespan
    machineMakespan[m in 0...nbMachines] <- sum(machineTasks[m], i => taskLengths[i]);
    makespan <- max[m in 0...nbMachines](machineMakespan[m]);
    minimize makespan;
}

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

    // Stop the search if the lower threshold is reached
    hxObjectiveThreshold = makespanLB;
}

/* Write the solution in a file */
function output() {
    if (solFileName == nil) return;
    local solFile = io.openWrite(solFileName);
    for [k in 0...nbMachines] {
        solFile.print("Makespan machine ", k, ": ", machineMakespan[k].value, " | Items: ");
        if (machineTasks[k].value.count() == 0) continue;
        for [e in machineTasks[k].value]
            solFile.print(e + " ");
        solFile.println();
    }
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python parallel_scheduling.py instances\p_cmax-n120-m3.txt
Execution (Linux)
export PYTHONPATH=/opt/hexaly_14_0/bin/python
python parallel_scheduling.py instances/p_cmax-n120-m3.txt
import hexaly.optimizer
import sys
import math

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

with hexaly.optimizer.HexalyOptimizer() as optimizer:
    # Read instance data
    filename = sys.argv[1]

    with open(filename) as f:
        line = f.readline()
        _, _, n_tasks, n_machines = [elem for elem in line.split()]

        # Number of tasks
        n_tasks = int(n_tasks)

        # Number of machines
        n_machines = int(n_machines)

        # Lengths of the tasks
        task_lengths = [int(elem) for elem in f.readline().split() if elem != '0']

        # Lowerbound on the makespan
        makespan_lb = int(sum(task_lengths) / n_machines) 

    # Declare the optimization model
    model = optimizer.model

    # Set decisions: machineTasks[k] represents the tasks assigned to machine k
    machine_tasks = [model.set(n_tasks) for _ in range(n_machines)]

    # Each task must be scheduled on exactly one machine
    model.constraint(model.partition(machine_tasks))

    # Create an array and a lambda function to retrieve the tasks' lengths
    lengths = model.array(task_lengths)
    lengths_lambda = model.lambda_function(lambda i: lengths[i])

    # Minimize the makespan
    machine_makespan = [model.sum(i, lengths_lambda) for i in machine_tasks]
    makespan = model.max(machine_makespan)
    model.minimize(makespan)

    model.close()

    # Parametrize the optimizer
    if len(sys.argv) >= 4:
        optimizer.param.time_limit = int(sys.argv[3])
    else:
        optimizer.param.time_limit = 5

    # Stop the search if the lower threshold is reached
    optimizer.param.set_objective_threshold(0, makespan_lb)

    optimizer.solve()

    # Write the solution in a file
    if len(sys.argv) >= 3:
        with open(sys.argv[2], 'w') as f:
            for k in range(n_machines):
                f.write("Makespan machine {}: {} | Items: ".format(k, machine_makespan[k].value))
                if len(machine_tasks[k].value) == 0:
                    continue
                for e in machine_tasks[k].value:
                    f.write("%d " % e)
                f.write("\n")
Compilation / Execution (Windows)
cl /EHsc parallel_scheduling.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly140.lib
parallel_scheduling instances\p_cmax-n120-m3.txt
Compilation / Execution (Linux)
g++ parallel_scheduling.cpp -I/opt/hexaly_14_0/include -lhexaly140 -lpthread -o frcpsps
./parallel_scheduling instances/p_cmax-n120-m3.txt
#include "optimizer/hexalyoptimizer.h"
#include <cmath>
#include <fstream>
#include <iostream>
#include <numeric>
#include <vector>

using namespace hexaly; 
using namespace std;

class ParallelScheduling {
private:
    // Number of tasks
    int nbTasks;

    // Number of machines
    int nbMachines;

    // Lengths of the tasks
    vector<int> taskLengths;

    // Lowerbound on the makespan
    int makespanLB;

    // Set of tasks assigned to machine
    vector<HxExpression> machineTasks;

    // Makespan of each machine
    vector<HxExpression> machineMakespan;

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

        string dump;
        infile >> dump;
        infile >> dump;
        infile >> nbTasks;
        infile >> nbMachines;

        taskLengths.resize(nbTasks);
        int totalLength = 0;
        for (int i = 0; i < nbTasks; ++i) {
            infile >> taskLengths[i];
            totalLength += taskLengths[i];
        }
        makespanLB = totalLength / nbMachines;
    }

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

        machineTasks.resize(nbMachines);
        machineMakespan.resize(nbMachines);
        
        // Set decisions: machineTasks[k] represents the tasks assigned to machine k
        for (int i = 0; i < nbMachines; i++){
            machineTasks[i] = model.setVar(nbTasks);
        }

        // Each task must be scheduled on exactly one machine
        model.addConstraint(model.partition(machineTasks.begin(), machineTasks.end()));

        // Create an array and a lambda function to retrieve the tasks' lengths
        HxExpression lengths = model.array(taskLengths.begin(), taskLengths.end());
        HxExpression lengthLambda = model.createLambdaFunction(
                [&](HxExpression i){ return lengths[i]; });

        for (int i = 0; i < nbMachines; i++){
            machineMakespan[i] = model.sum(machineTasks[i], lengthLambda);
        }
        // Minimize the makespan
        HxExpression makespan = model.max(machineMakespan.begin(),
                machineMakespan.end());
        model.minimize(makespan);

        model.close();

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

        // Stop the search if the lower threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, (hxint) makespanLB);

        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());
        for (int k = 0; k < nbMachines; ++k) {
            outfile << "Makespan machine " << k << ": "
                    << machineMakespan[k].getValue() << " | Items: ";
            HxCollection machineCollection = machineTasks[k].getCollectionValue();
            if (machineCollection.count() == 0)
                continue;
            for (int i = 0; i < machineCollection.count(); ++i) {
                outfile << machineCollection[i] << " ";
            }
            outfile << endl;
        }
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "Usage: parallel_scheduling 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 {
        ParallelScheduling 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 ParallelScheduling.cs /reference:Hexaly.NET.dll
ParallelScheduling instances\p_cmax-n120-m3.txt

using System;
using System.IO;
using System.Linq;
using Hexaly.Optimizer;

public class ParallelScheduling : IDisposable
{
    // Number of tasks
    private int nbTasks;

    // Number of machines
    private int nbMachines;

    // Lengths of the tasks
    private long[] taskLengths;

    // Lowerbound on the makespan
    private long makespanLB;

    // Set of tasks assigned to each machine
    private HxExpression[] machineTasks;

    // Makespan of each machine 
    private HxExpression[] machineMakespan;

    // Hexaly Optimizer
    private HexalyOptimizer optimizer;

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

    /* Read instance data */
    public void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName)){
            string[] line = input.ReadLine().Split(" ");
            nbTasks = int.Parse(line[2]);
            nbMachines = int.Parse(line[3]);
            taskLengths = new long[nbTasks];
            line = input.ReadLine().Split(" ");

            long totalLength = 0;
            for (int i = 0; i < nbTasks; i++){
                taskLengths[i] = int.Parse(line[i]);
                totalLength += taskLengths[i];
            }
            makespanLB = totalLength / nbMachines;
        }
    }

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

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

        machineTasks = new HxExpression[nbMachines];
        machineMakespan = new HxExpression[nbMachines];

        //Set decisions: machineTasks[k] represents the tasks assigned to machine k
        for (int i = 0; i < nbMachines; i++)
            machineTasks[i] = model.Set(nbTasks);
        
        // Each task must be scheduled on exactly one machine
        model.AddConstraint(model.Partition(machineTasks));

        // Create an array and a lambda function to retrieve the tasks' lengths
        HxExpression lengths = model.Array(taskLengths);
        var lengthLambda = model.CreateLambdaFunction(iExpr => lengths[iExpr]);

        for (int i = 0; i < nbMachines; i++)
            machineMakespan[i] = model.Sum(machineTasks[i], lengthLambda);
        
        // Minimize the makespan
        HxExpression makespan = model.Max(machineMakespan);
        model.Minimize(makespan);
        
        model.Close();

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

        // Stop the search if the lower threshold is reached
        optimizer.GetParam().SetObjectiveThreshold(0, makespanLB);

        optimizer.Solve();
    }

    /* Write the solution in a file */
    void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            for (int k = 0; k < nbMachines; ++k)
            {
                output.Write("Makespan machine " + k + ": "
                        + machineMakespan[k].GetValue() + " | Items: ");
                HxCollection machineCollection = machineTasks[k].GetCollectionValue();
                if (machineCollection.Count() == 0)
                    continue;
                for (int i = 0; i < machineCollection.Count(); ++i)
                    output.Write(machineCollection[i] + " ");
                output.WriteLine();
            }
        }
    }
    
    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.Error.WriteLine("Usage: ParallelScheduling inputFile [outputFile] [timeLimit]");
            return;
        }
        string instanceFile = args[0];
        string solFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "5";
        try
        {
            var model = new ParallelScheduling();
            model.ReadInstance(instanceFile);
            if (!int.TryParse(strTimeLimit, out int limit))
                limit = 5;
            model.Solve(limit);
            if (solFile != null)
                model.WriteSolution(solFile);
        }
        catch (Exception e)
        {
            Console.Error.WriteLine("An error occurred: " + e.Message);
        }
    }
}
Compilation / Execution (Windows)
javac ParallelScheduling.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. ParallelScheduling instances\p_cmax-n120-m3.txt
Compilation / Execution (Linux)
javac ParallelScheduling.java -cp /opt/hexaly_14_0/bin/hexaly.jar
java -cp /opt/hexaly_14_0/bin/hexaly.jar:. ParallelScheduling instances/p_cmax-n120-m3.txt
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;

public class ParallelScheduling {
    // Number of tasks
    private int nbTasks;

    // Number of machines
    private int nbMachines;

    // Lengths of the tasks
    private long[] taskLengths;

    // Lowerbound on the makespan
    private long makespanLB;

    // Set of tasks assigned to machine
    private HxExpression[] machineTasks;

    // Makespan of each machine 
    private HxExpression[] machineMakespan;

    // Hexaly Optimizer
    private final HexalyOptimizer optimizer;

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

    /* Read instance data */
    private void readInstance(String fileName) throws IOException {
        try (Scanner input = new Scanner(new File(fileName))) {
            input.useLocale(Locale.ROOT);
            String[] line = input.nextLine().split(" ");
            nbTasks = Integer.parseInt(line[2]);
            nbMachines = Integer.parseInt(line[3]);

            taskLengths = new long[nbTasks];
            long totalLengths = 0;
            for (int i = 0; i < nbTasks; ++i) {
                taskLengths[i] = input.nextInt();
                totalLengths += taskLengths[i];
            }
            makespanLB = totalLengths / nbMachines;
        }
    }

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

        machineTasks = new HxExpression[nbMachines];
        machineMakespan = new HxExpression[nbMachines];

        // Set decisions: machineTasks[k] represents the tasks in machine k
        for (int k = 0; k < nbMachines; ++k) 
            machineTasks[k] = model.setVar(nbTasks);

        // Each task must be scheduled on exactly one machine
        model.constraint(model.partition(machineTasks));

        // Create an array and a lambda function to retrieve the tasks' lengths
        HxExpression lengths = model.array(taskLengths);
        HxExpression lengthLambda = model.lambdaFunction(i -> model.at(lengths, i));

        for (int k = 0; k < nbMachines; ++k)
            machineMakespan[k] = model.sum(machineTasks[k], lengthLambda);

        // Minimize the makespan
        HxExpression makespan = model.max(machineMakespan);
        model.minimize(makespan);
        
        model.close();

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

        // Stop the search if the lower threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, makespanLB);

        optimizer.solve();
    }

    /* Write the solution in a file */
    private void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            for (int k = 0; k < nbMachines; ++k) {
                output.print("Makespan machine " + k + ": " 
                        + machineMakespan[k].getValue() + " | Items: ");
                HxCollection machineCollection = machineTasks[k].getCollectionValue();
                if (machineCollection.count() == 0)
                    continue;
                for (int i = 0; i < machineCollection.count(); ++i) 
                    output.print(machineCollection.get(i) + " ");
                output.println();
            }
        }
    }


    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java ParallelScheduling 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()) {
            ParallelScheduling model = new ParallelScheduling(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);
        }
    }
}