Resource-Constrained Project Scheduling Problem (RCPSP)
Problem
In the Resource-Constrained Project Scheduling Problem (RCPSP), a project consists of a set of tasks to schedule. Each task has a given duration and cannot be interrupted. There are precedence constraints between the tasks: each task must end before any of its successors can start. There are a set of renewable resources. Each task has a given resource requirement or weight (possibly zero) on each resource, representing the amount of resource it consumes while it is active. Each resource has a given maximum capacity: it can process several tasks at once, but the sum of the processed tasks’s weights can never exceed this maximum capacity. The goal is to find a schedule that minimizes the makespan: the time when all tasks have been processed.
Principles learned
- Add interval decision variables to model the tasks
- Define lambda functions to model the cumulative resource constraints
Data
The instances provided follow the Patterson [1] format:
- First line:
- Number of tasks (including two additional dummy tasks with duration 0: the source and the sink)
- Number of renewable resources
- Second line: maximum capacity for each resource
- From the third line, for each task:
- Duration of the task
- Resource requirement (weight) for each resource
- Number of successors
- Task ID for each successor
Program
The Hexaly model for the Resource-Constrained Project Scheduling Problem (RCPSP) uses interval decision variables to represent the tasks. The length of each interval is equal to the duration of the corresponding task.
We then write the precedence constraints: each task must end before any of its successors can start.
The cumulative resource constraints can be formulated as follows: for each resource, and for each time slot t, the amount of resource consumed by the tasks that are being processed must not exceed the resource’s capacity. To model these constraints, we sum up, for each resource and each time slot, the weights of all active tasks. We use a variadic ‘and’ formulation with a lambda function to ensure the resource capacity is respected at any time. Thanks to this variadic ‘and’, the constraint formulation remains compact and efficient even if the time horizon is very large.
The makespan to minimize is the time when all the tasks have ended.
- Execution
-
hexaly rcpsp.hxm inFileName=instances/Pat1.rcp [hxTimeLimit=] [solFileName=]
use io;
// The input files follow The "Patterson" Format
function input() {
local usage = "Usage: hexaly rcpsp.hxm inFileName=instanceFile "
+ "[outFileName=outputFile] [hxTimeLimit=timeLimit]";
if (inFileName == nil) throw usage;
inFile = io.openRead(inFileName);
nbTasks = inFile.readInt();
nbResources = inFile.readInt();
// Maximum capacity of each resource
for [r in 0...nbResources] {
capacity[r] = inFile.readInt();
}
for [i in 0...nbTasks] {
// Duration of each task
duration[i] = inFile.readInt();
for[r in 0...nbResources] {
// Resource weight of resource r required for task i
weight[i][r] = inFile.readInt();
}
nbSuccessors[i] = inFile.readInt();
// Successors of each task i
for [s in 0...nbSuccessors[i]] {
successors[i][s] = inFile.readInt() - 1;
}
}
// Trivial upper bound for the start times of the tasks
horizon = sum[i in 0...nbTasks](duration[i]);
inFile.close();
}
function model() {
// Interval decisions: time range of each task
tasks[i in 0...nbTasks] <- interval(0, horizon);
// Task duration constraints
for [i in 0...nbTasks]
constraint length(tasks[i]) == duration[i];
// Precedence constraints between the tasks
for[i in 0...nbTasks][s in 0...nbSuccessors[i]] {
constraint tasks[i] < tasks[successors[i][s]];
}
// Makespan: end of the last task
makespan <- max[i in 0...nbTasks] (end(tasks[i]));
// Cumulative resource constraints
for [r in 0...nbResources] {
constraint and(0...makespan,
t => sum[i in 0...nbTasks](weight[i][r] * contains(tasks[i], t)) <= capacity[r]);
}
// Minimize the makespan
minimize makespan;
}
// Parameterize the solver
function param() {
if (hxTimeLimit == nil) hxTimeLimit = 60;
}
/* Write the solution in a file with the following format:
* - total makespan
* - for each task, the task id, the start and end times */
function output() {
if (outFileName != nil) {
outFile = io.openWrite(outFileName);
println("Solution written in file ", outFileName);
outFile.println(makespan.value);
for [i in 0...nbTasks] {
outFile.println(i + 1, " ", tasks[i].value.start, " ", tasks[i].value.end);
}
}
}
- Execution (Windows)
-
set PYTHONPATH=%HX_HOME%\bin\pythonpython rcpsp.py instances\Pat1.rcp
- Execution (Linux)
-
export PYTHONPATH=/opt/hexaly_13_0/bin/pythonpython rcpsp.py instances/Pat1.rcp
import hexaly.optimizer
import sys
# The input files follow the "Patterson" format
def read_instance(filename):
with open(filename) as f:
lines = f.readlines()
first_line = lines[0].split()
# Number of tasks
nb_tasks = int(first_line[0])
# Number of resources
nb_resources = int(first_line[1])
# Maximum capacity of each resource
capacity = [int(lines[1].split()[r]) for r in range(nb_resources)]
# Duration of each task
duration = [0 for i in range(nb_tasks)]
# Resource weight of resource r required for task i
weight = [[] for i in range(nb_tasks)]
# Number of successors
nb_successors = [0 for i in range(nb_tasks)]
# Successors of each task i
successors = [[] for i in range(nb_tasks)]
for i in range(nb_tasks):
line = lines[i + 2].split()
duration[i] = int(line[0])
weight[i] = [int(line[r + 1]) for r in range(nb_resources)]
nb_successors[i] = int(line[nb_resources + 1])
successors[i] = [int(line[nb_resources + 2 + s]) - 1 for s in range(nb_successors[i])]
# Trivial upper bound for the start times of the tasks
horizon = sum(duration[i] for i in range(nb_tasks))
return (nb_tasks, nb_resources, capacity, duration, weight, nb_successors, successors, horizon)
def main(instance_file, output_file, time_limit):
nb_tasks, nb_resources, capacity, duration, weight, nb_successors, successors, horizon = read_instance(
instance_file)
with hexaly.optimizer.HexalyOptimizer() as optimizer:
#
# Declare the optimization model
#
model = optimizer.model
# Interval decisions: time range of each task
tasks = [model.interval(0, horizon) for i in range(nb_tasks)]
# Task duration constraints
for i in range(nb_tasks):
model.constraint(model.length(tasks[i]) == duration[i])
# Precedence constraints between the tasks
for i in range(nb_tasks):
for s in range(nb_successors[i]):
model.constraint(tasks[i] < tasks[successors[i][s]])
# Makespan: end of the last task
makespan = model.max([model.end(tasks[i]) for i in range(nb_tasks)])
# Cumulative resource constraints
for r in range(nb_resources):
capacity_respected = model.lambda_function(
lambda t: model.sum(weight[i][r] * model.contains(tasks[i], t)
for i in range(nb_tasks))
<= capacity[r])
model.constraint(model.and_(model.range(makespan), capacity_respected))
# Minimize the makespan
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:
# - total makespan
# - for each task, the task id, the start and end times
#
if output_file != None:
with open(output_file, "w") as f:
print("Solution written in file", output_file)
f.write(str(makespan.value) + "\n")
for i in range(nb_tasks):
f.write(str(i + 1) + " " + str(tasks[i].value.start()) + " " + str(tasks[i].value.end()))
f.write("\n")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python rcpsp.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 rcpsp.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly130.librcpsp instances\Pat1.rcp
- Compilation / Execution (Linux)
-
g++ rcpsp.cpp -I/opt/hexaly_13_0/include -lhexaly130 -lpthread -o rcpsp./rcpsp instances/Pat1.rcp
#include "optimizer/hexalyoptimizer.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <numeric>
#include <vector>
using namespace hexaly;
class Rcpsp {
private:
// Number of tasks
int nbTasks;
// Number of resources
int nbResources;
// Maximum capacity of each resource
std::vector<int> capacity;
// Duration of each task
std::vector<int> duration;
// Resource weight of resource r required for task i
std::vector<std::vector<int>> weight;
// Number of successors
std::vector<int> nbSuccessors;
// Successors for each task i
std::vector<std::vector<int>> successors;
// Trivial upper bound for the start times of the tasks
int horizon = 0;
// Hexaly Optimizer
HexalyOptimizer optimizer;
// Decision variables: time range of each task
std::vector<HxExpression> tasks;
// Objective = minimize the makespan: end of the last task of the last job
HxExpression makespan;
public:
Rcpsp(const std::string& fileName) : optimizer() {}
// The input files follow the "Patterson" format
void readInstance(const std::string& fileName) {
std::ifstream infile;
infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
infile.open(fileName.c_str());
infile >> nbTasks;
infile >> nbResources;
// The maximum capacity of each resource
capacity.resize(nbResources);
for (int r = 0; r < nbResources; ++r) {
infile >> capacity[r];
}
duration.resize(nbTasks);
weight.resize(nbTasks);
nbSuccessors.resize(nbTasks);
successors.resize(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
infile >> duration[i];
weight[i].resize(nbResources);
for (int r = 0; r < nbResources; ++r) {
infile >> weight[i][r];
}
infile >> nbSuccessors[i];
successors[i].resize(nbSuccessors[i]);
for (int s = 0; s < nbSuccessors[i]; ++s) {
int x;
infile >> x;
successors[i][s] = x - 1;
}
horizon += duration[i];
}
infile.close();
}
void solve(int TimeLimit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
// Interval decisions: time range of each task
tasks.resize(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
// Task duration constraints
model.constraint(model.length(tasks[i]) == duration[i]);
}
// Precedence constraints between the tasks
for (int i = 0; i < nbTasks; ++i) {
for (int s = 0; s < nbSuccessors[i]; ++s) {
model.constraint(tasks[i] < tasks[successors[i][s]]);
}
}
// Makespan: end of the last task
makespan = model.max();
for (int i = 0; i < nbTasks; ++i) {
makespan.addOperand(model.end(tasks[i]));
}
// Cumulative resource constraints
for (int r = 0; r < nbResources; ++r) {
HxExpression capacityRespected = model.createLambdaFunction([&](HxExpression t) {
HxExpression totalWeight = model.sum();
for (int i = 0; i < nbTasks; ++i) {
HxExpression taskWeight = weight[i][r] * model.contains(tasks[i], t);
totalWeight.addOperand(taskWeight);
}
return model.leq(totalWeight, capacity[r]);
});
model.constraint(model.and_(model.range(0, makespan), capacityRespected));
}
// Minimize the makespan
model.minimize(makespan);
model.close();
// Parameterize the optimizer
optimizer.getParam().setTimeLimit(TimeLimit);
optimizer.solve();
}
/* Write the solution in a file with the following format:
* - total makespan
* - for each task, the task id, the start and end times */
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;
outfile << makespan.getValue() << std::endl;
for (int i = 0; i < nbTasks; ++i) {
outfile << i + 1 << " " << tasks[i].getIntervalValue().getStart() << " "
<< tasks[i].getIntervalValue().getEnd() << std::endl;
}
outfile.close();
}
};
int main(int argc, char** argv) {
if (argc < 2) {
std::cout << "Usage: Rcpsp 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";
Rcpsp model(instanceFile);
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 Rcpsp.cs /reference:Hexaly.NET.dllRcpsp instances\Pat1.rcp
using System;
using System.IO;
using Hexaly.Optimizer;
public class Rcpsp : IDisposable
{
// Number of tasks
private int nbTasks;
// Number of resources
private int nbResources;
// Maximum capacity of each resource
private int[] capacity;
// Duration of each task
private int[] duration;
// Resource weight of resource r required for task i
private int[,] weight;
// Number of successors
private int[] nbSuccessors;
// Successors for each task i
private int[][] successors;
// Trivial upper bound for the start times of the tasks
private int horizon = 0;
// Hexaly Optimizer
private HexalyOptimizer optimizer;
// Decision variables: time range of each task
private HxExpression[] tasks;
// Objective = minimize the makespan: end of the last task of the last job
private HxExpression makespan;
public Rcpsp(string fileName)
{
optimizer = new HexalyOptimizer();
}
string[] SplitInput(StreamReader input)
{
string line = input.ReadLine();
if (line == null)
return new string[0];
return line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
}
// The input files follow the "Patterson" format
private void ReadInstance(string fileName)
{
using (StreamReader input = new StreamReader(fileName))
{
string[] splitted = SplitInput(input);
if (splitted.Length == 0)
splitted = SplitInput(input);
nbTasks = int.Parse(splitted[0]);
nbResources = int.Parse(splitted[1]);
// The maximum capacity of each resource
capacity = new int[nbResources];
splitted = SplitInput(input);
for (int r = 0; r < nbResources; ++r)
capacity[r] = int.Parse(splitted[r]);
duration = new int[nbTasks];
weight = new int[nbTasks, nbResources];
nbSuccessors = new int[nbTasks];
successors = new int[nbTasks][];
for (int i = 0; i < nbTasks; ++i)
{
splitted = SplitInput(input);
if (splitted.Length == 0)
splitted = SplitInput(input);
duration[i] = int.Parse(splitted[0]);
for (int r = 0; r < nbResources; ++r)
weight[i, r] = int.Parse(splitted[r + 1]);
nbSuccessors[i] = int.Parse(splitted[nbResources + 1]);
successors[i] = new int[nbSuccessors[i]];
for (int s = 0; s < nbSuccessors[i]; ++s)
successors[i][s] = int.Parse(splitted[s + nbResources + 2]) - 1;
horizon += duration[i];
}
}
}
public void Dispose()
{
optimizer.Dispose();
}
public void Solve(int timeLimit)
{
// Declare the optimization model
HxModel model = optimizer.GetModel();
// Interval decisions: time range of each task
tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i)
{
tasks[i] = model.Interval(0, horizon);
// Task duration constraints
model.Constraint(model.Length(tasks[i]) == duration[i]);
}
// Precedence constraints between the tasks
for (int i = 0; i < nbTasks; ++i)
{
for (int s = 0; s < nbSuccessors[i]; ++s)
{
model.Constraint(tasks[i] < tasks[successors[i][s]]);
}
}
// Makespan: end of the last task
makespan = model.Max();
for (int i = 0; i < nbTasks; ++i)
makespan.AddOperand(model.End(tasks[i]));
// Cumulative resource constraints
for (int r = 0; r < nbResources; ++r)
{
HxExpression capacityRespected = model.LambdaFunction(t =>
{
HxExpression totalWeight = model.Sum();
for (int i = 0; i < nbTasks; ++i)
{
totalWeight.AddOperand(weight[i, r] * model.Contains(tasks[i], t));
}
return totalWeight <= capacity[r];
}
);
model.Constraint(model.And(model.Range(0, makespan), capacityRespected));
}
// Minimize the makespan
model.Minimize(makespan);
model.Close();
// Parameterize the optimizer
optimizer.GetParam().SetTimeLimit(timeLimit);
optimizer.Solve();
}
/* Write the solution in a file with the following format:
* - total makespan
* - for each task, the task id, the start and end times */
public void WriteSolution(string fileName)
{
using (StreamWriter output = new StreamWriter(fileName))
{
Console.WriteLine("Solution written in file " + fileName);
output.WriteLine(makespan.GetValue());
for (int i = 0; i < nbTasks; ++i)
{
output.Write((i + 1) + " " + tasks[i].GetIntervalValue().Start() + " " + tasks[i].GetIntervalValue().End());
output.WriteLine();
}
}
}
public static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: Rcpsp 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 (Rcpsp model = new Rcpsp(instanceFile))
{
model.ReadInstance(instanceFile);
model.Solve(int.Parse(strTimeLimit));
if (outputFile != null)
model.WriteSolution(outputFile);
}
}
}
- Compilation / Execution (Windows)
-
javac Rcpsp.java -cp %HX_HOME%\bin\hexaly.jarjava -cp %HX_HOME%\bin\hexaly.jar;. Rcpsp instances\Pat1.rcp
- Compilation / Execution (Linux)
-
javac Rcpsp.java -cp /opt/hexaly_13_0/bin/hexaly.jarjava -cp /opt/hexaly_13_0/bin/hexaly.jar:. Rcpsp instances/Pat1.rcp
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;
public class Rcpsp {
// Number of tasks
private int nbTasks;
// Number of resources
private int nbResources;
// Maximum capacity of each resource
private int[] capacity;
// Duration of each task
private int[] duration;
// Resource weight of resource r required for task i
private int[][] weight;
// Number of successors
private int[] nbSuccessors;
// Successors for each task i
private int[][] successors;
// Trivial upper bound for the start times of the tasks
private int horizon = 0;
// Hexaly Optimizer
final HexalyOptimizer optimizer;
// Decision variables: time range of each task
private HxExpression[] tasks;
// Objective = minimize the makespan: end of the last task of the last job
private HxExpression makespan;
public Rcpsp(HexalyOptimizer optimizer, String fileName) throws IOException {
this.optimizer = optimizer;
}
// The input files follow the "Patterson" format
private void readInstance(String fileName) throws IOException {
try (Scanner input = new Scanner(new File(fileName))) {
nbTasks = input.nextInt();
nbResources = input.nextInt();
// The maximum capacity of each resource
capacity = new int[nbResources];
for (int r = 0; r < nbResources; ++r) {
capacity[r] = input.nextInt();
}
duration = new int[nbTasks];
weight = new int[nbTasks][nbResources];
nbSuccessors = new int[nbTasks];
successors = new int[nbTasks][];
for (int i = 0; i < nbTasks; ++i) {
duration[i] = input.nextInt();
for (int r = 0; r < nbResources; ++r) {
weight[i][r] = input.nextInt();
}
nbSuccessors[i] = input.nextInt();
successors[i] = new int[nbSuccessors[i]];
for (int s = 0; s < nbSuccessors[i]; ++s) {
successors[i][s] = input.nextInt() - 1;
}
horizon += duration[i];
}
}
}
public void solve(int timeLimit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
// Interval decisions: time range of each task
tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
// Task duration constraints
model.constraint(model.eq(model.length(tasks[i]), duration[i]));
}
// Precedence constraints between the tasks
for (int i = 0; i < nbTasks; ++i) {
for (int s = 0; s < nbSuccessors[i]; ++s) {
model.constraint(model.lt(tasks[i], tasks[successors[i][s]]));
}
}
// Makespan: end of the last task
makespan = model.max();
for (int i = 0; i < nbTasks; ++i) {
makespan.addOperand(model.end(tasks[i]));
}
// Cumulative resource constraints
for (int r = 0; r < nbResources; ++r) {
final int rL = r;
HxExpression capacityRespected = model.lambdaFunction(t -> {
HxExpression totalWeight = model.sum();
for (int i = 0; i < nbTasks; ++i) {
totalWeight.addOperand(model.prod(
weight[i][rL],
model.contains(tasks[i], t)));
}
return model.leq(totalWeight, capacity[rL]);
});
model.constraint(model.and(model.range(0, makespan), capacityRespected));
}
// Minimize the makespan
model.minimize(makespan);
model.close();
// Parameterize the optimizer
optimizer.getParam().setTimeLimit(timeLimit);
optimizer.solve();
}
/*
* Write the solution in a file with the following format:
* - total makespan
* - for each task, the task id, the start and end times
*/
public void writeSolution(String fileName) throws IOException {
try (PrintWriter output = new PrintWriter(fileName)) {
System.out.println("Solution written in file " + fileName);
output.println(makespan.getValue());
for (int i = 0; i < nbTasks; ++i) {
output.println((i + 1) + " " + tasks[i].getIntervalValue().getStart() + " "
+ tasks[i].getIntervalValue().getEnd());
}
}
}
public static void main(String[] args) {
if (args.length < 1) {
System.out.println("Usage: java Rcpsp 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()) {
Rcpsp model = new Rcpsp(optimizer, instanceFile);
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 optimality gap of 1.9% for the Resource-Constrained Project Scheduling Problem (RCPSP) in 1 minute of running time on the RG300 [2] literature benchmark, composed of instances with 300 tasks. Our Resource-Constrained Project Scheduling Problem (RCPSP) benchmark page illustrates how Hexaly Optimizer outperforms traditional general-purpose optimization solvers, like Gurobi and OR-Tools, on this challenging problem.
[1] Patterson, J. H.,( 1984), A comparison of exact approaches for solving the multiple constrained resource, Project Scheduling Problem, Management Science, Vol. 30, p854-867
[2] D. Debels & M. Vanhoucke (2007). A Decomposition-Based Genetic Algorithm for the Resource-Constrained Project-Scheduling Problem. Operations Research 55(3):457-469.