Workforce Shift Scheduling Problem
SchedulingProblem
In the Workforce Shift Scheduling Problem, we consider a fixed number of agents and a set of tasks (referred to as activities) to schedule. Each activity has a specified duration and must be performed within a defined time window. Similarly, each agent has a limited availability window during which they can work. Additional constraints include the maximum number of shifts an agent can work daily and the required minimum break time between consecutive shifts. The primary goals of the problem are to minimize understaffing—that is, the total number of unassigned agents across all activities—and to minimize the total working time across all agents.
Principles learned
- Define a multi-objective model and identify the priority of each objective
- Learn about Hexaly Optimizer’s modeling style: distinguish decision variables from intermediate expressions
Data
The format of the data files for the Workforce Shift Scheduling Problem is as follows:
- First line: number of agents
- Second line: number of activities
- Third line: planning horizon (in days)
- Fourth line: increment of time (in hours)
- The following lines:
- For each activity : the activity time window (time interval, in hours)
- For each agent: its availability time window (time interval, in hours)
Model
The Hexaly model for the Workforce Shift Scheduling Problem uses Boolean decision variables. Each variable indicates whether an agent is assigned to a specific shift. For every activity, the model generates a set of possible shifts. Each shift represents a time interval when an agent can perform that activity. We create these shifts by discretizing the planning horizon using a fixed time increment between possible start times.
To prevent overlapping assignments, the model enforces a no-overlap constraint. If two shifts overlap in time, the same agent cannot take both. This rule applies to all pairs of incompatible shifts. Alternatively, the model can apply the constraint over a discretized timeline. We ensure no agent works to multiple shifts during any time slot (e.g., every 15 minutes or 1 hour). This approach avoids listing all conflicting shift pairs and can reduce the number of constraints when there are many shifts. The choice between the two methods depends on the number of shifts and the total number of time steps.
The model also requires a minimum one-hour break between any two shifts assigned to the same agent. This is handled similarly to the no-overlap rule. We flag pairs of shifts that would violate the break time and treat them as incompatible. To do this, we add one hour to the end time of each shift when checking for conflicts.
Each agent must work exactly two shifts per day. The model enforces this by summing the agent’s decision variables and setting the total to two.
To ensure proper coverage, the model divides the planning horizon into equal time intervals. For each interval and activity, we check which shifts overlap with that interval. We then count the number of agents assigned to those shifts. If there are not enough, we register the understaffing. We sum up understaffing across all activities and intervals, and we minimize it.
To reduce unnecessary workload, we minimize the total work time across all agents. This objective promotes fairer scheduling and limits overwork. The two objectives are optimized lexicographically: minimizing understaffing is the top priority, and balancing workload comes second.
- Execution
-
hexaly workforce_scheduling_shifts.hxm inFileName=instances/3agents7tasks_n1.txt [outFileName=] [hxTimeLimit=]
use io;
/**
* Activity class represents a work item with its scheduling constraints
* - id: Unique identifier for the activity
* - minStart: minimum possible start time (in seconds)
* - maxEnd: maximum possible end time (in seconds)
* - duration: Duration of the activity (in seconds)
*/
class Activity {
id; // int
minStart; // int
maxEnd; // int
duration; // int
constructor(id, minStart, maxEnd, duration) {
this.id = id;
this.minStart = minStart;
this.maxEnd = maxEnd;
this.duration = duration;
}
}
/**
* Agent class represents an employee with their availability window
* - id: Unique identifier for the agent
* - availabilityStart: Start of availability period (in seconds)
* - availabilityEnd: End of availability period (in seconds)
*/
class Agent {
id; // int
availabilityStart; // int
availabilityEnd; // int
constructor(id, avStart, avEnd) {
this.id = id;
this.availabilityStart = avStart;
this.availabilityEnd = avEnd;
}
}
/**
* Shift class represents a scheduled work period for an activity
* - id: Unique identifier for the shift
* - activityId: ID of the activity performed
* - shiftStart: Start time of the shift (in seconds)
* - shiftEnd: End time of the shift (in seconds)
*/
class Shift {
id; // int
activityId; // int
shiftStart; // int
shiftEnd; // int
constructor(id, actId, shiftS, shiftE){
this.id = id;
this.activityId = actId;
this.shiftStart = shiftS;
this.shiftEnd = shiftE;
}
}
/**
* Generates all possible shifts for activities with given time increment
* @param activities Array of Activity objects
* @param shiftIncrement Time increment in seconds
* @return Array of generated Shift objects
*/
function generateShifts(activities, shiftIncrement){
i = 0;
for[a in activities]{
currentTime = a.minStart;
while(currentTime + a.duration <= a.maxEnd){
shifts[i].id = i;
shifts[i].activityId = a.id;
shifts[i].shiftStart = currentTime;
shifts[i].shiftEnd = currentTime + a.duration;
currentTime += shiftIncrement;
i += 1;
}
}
return shifts;
}
/**
* Checks if two shifts are compatible
* @param shift1 First shift
* @param shift2 Second shift
* @return true if shifts overlap, false otherwise
*/
function incompatibleShifts(shift1, shift2){
startShift1 = shift1.shiftStart;
endShift1 = shift1.shiftEnd;
startShift2 = shift2.shiftStart;
endShift2 = shift2.shiftEnd;
actId1 = shift1.activityId;
actId2 = shift2.activityId;
return !(startShift1 >= endShift2 || endShift1 <= startShift2);
}
/**
* Checks if there is at least a one-hour break between two shifts
* @param shift1 First shift
* @param shift2 Second shift
* @return true if there is not enough break, false otherwise
*/
function notEnoughBreak(shift1, shift2){
startShift1 = shift1.shiftStart;
endShift1 = shift1.shiftEnd;
startShift2 = shift2.shiftStart;
endShift2 = shift2.shiftEnd;
actId1 = shift1.activityId;
actId2 = shift2.activityId;
return!(startShift1 >= endShift2 + 3600 || endShift1 + 3600 <= startShift2);
}
/**
* Checks if two time intervals overlap
* @param interval1Start Start of first interval
* @param interval1End End of first interval
* @param interval2Start Start of second interval
* @param interval2End End of second interval
* @return true if intervals overlap, false otherwise
*/
function overlappingIntervals(interval1Start, interval1End, interval2Start, interval2End){
return !(interval1Start >= interval2End || interval1End <= interval2Start);
}
/**
* Reads and parses input data from file
* @param fileName Path to input file
*/
function readData() {
local inFile = io.openRead(inFileName);
// Filter out comments
i = 0;
endLoop = false;
while(!endLoop){
line = inFile.readln();
endLoop = inFile.eof();
newElems = false;
if(!line.startsWith("#")){
if(line.indexOf("#") != -1){
elementsInLine = line.substring(0, line.indexOf("#")).trim().split();
indexInLine = 0;
elements[i] = {};
for[j in 0...elementsInLine.count()]{
if(elementsInLine[j].length() > 0){
elements[i].add(elementsInLine[j]);
indexInline = indexInLine + 1;
newElems = true;
}
}
}
else{
elementsInLine = line.trim().split();
indexInline = 0;
for[j in 0...elementsInLine.count()]{
if(elementsInLine[j].length() > 0){
elements[i][indexInLine] = elementsInLine[j];
indexInLine = indexInLine + 1;
newElems = true;
}
}
}
}
if(newElems){
i = i + 1;
}
}
// Read problem dimensions
nbAgents = elements[0][0].toInt();
nbActivities = elements[1][0].toInt();
timeHorizon = elements[2][0].toInt();
shiftIncrement = (elements[3][0].toDouble() * 3600);
// Read activities informations
// Duration time of each activity
index = 4;
for[i in 0...nbActivities]{
activities[i].id = i;
activities[i].duration = elements[index][0].toInt() * 3600;
index = index + 1;
}
// Maximum and minimum starting date of each activity
for[i in 0...nbActivities]{
activities[i].minStart = elements[index][0].toInt() * 3600;
activities[i].maxEnd = elements[index][1].toInt() * 3600;
index = index + 1;
}
// Read agents informations
// Availability period in the day of each agent
for[a in 0...nbAgents]{
agents[a].id = a;
agents[a].availabilityStart = elements[index][0].toInt() * 3600;
agents[a].availabilityEnd = elements[index][1].toInt() * 3600;
index = index + 1;
}
// Generation of all possible shifts for each activity with a given increment
shifts = generateShifts(activities, shiftIncrement);
// Validates the instance data
validateInstance();
}
/**
* Validates the instance data for consistency
*/
function validateInstance() {
// Check if all activities can be completed within their time windows
for [activity in activities] {
if (activity.duration > (activity.maxEnd - activity.minStart)) {
throw "Activity " + activity.id + " duration (" + (activity.duration/3600) + "h) exceeds its time window";
}
}
// Check if workers' availability windows are valid
for [agent in agents] {
if (agent.availabilityStart >= agent.availabilityEnd) {
throw "Agent " + agent.id + " has invalid availability window";
}
}
// Check if activities can be performed within workers' availability
local earliestStart = min[act in activities](act.minStart);
local latestEnd = max[act in activities](act.maxEnd);
for [agent in agents] {
if (agent.availabilityStart > earliestStart || agent.availabilityEnd < latestEnd) {
println("Warning: Agent " + agent.id + " availability might not cover all tasks");
}
}
}
/* Read input data */
function input() {
local usage = "Usage: hexaly workforce_scheduling_shifts.hxm inFileName=instanceFile "
+ "[outFileName=outputFile] [hxTimeLimit=timeLimit]";
if (inFileName == nil) throw usage;
readData();
}
/**
* Defines the optimization model
*/
function model(){
// Decision variables : agentShift[a][s] is true when the agent a works the shift s
agentShift[a in 0...nbAgents][s in shifts]<-bool();
// Constraints
// 1. The shifts performed by an agent must not overlap
for[a in 0...nbAgents][s1 in shifts][s2 in shifts : s1 != s2]{
if(incompatibleShifts(s1, s2)){
constraint (agentShift[a][s1]) + (agentShift[a][s2]) <= 1 ;
}
}
// 2.An agent must take at least a one-hour break between two shifts
for[a in 0...nbAgents][s1 in shifts][s2 in shifts : s1 != s2]{
if(notEnoughBreak(s1, s2)){
constraint (agentShift[a][s1]) + (agentShift[a][s2]) <= 1 ;
}
}
// 3.Each agent must work exactly two shifts in the day
for[a in 0...nbAgents]{
constraint sum[s in shifts](agentShift[a][s]) == 2;
}
// Objective 1 : Minimize understaffing
// For each activity and at each interval of time of the increment length
understaffing = {};
for [activity in activities] {
local currentTime = activity.minStart;
while(currentTime < activity.maxEnd) {
// Current interval of time
local currentIntervalStart = currentTime;
local currentIntervalEnd = currentTime + shiftIncrement;
// The activity will not be pursued after its maximum possible end
if (currentIntervalEnd > activity.maxEnd) {
currentIntervalEnd = activity.maxEnd;
}
// Shifts performed in the current time interval
local shiftsAtCurrent = {};
for [shift in shifts] {
if (shift.activityId != activity.id) continue;
if (overlappingIntervals(currentIntervalStart, currentIntervalEnd, shift.shiftStart, shift.shiftEnd)) {
shiftsAtCurrent.add(shift);
}
}
// Number of agents currently performing the task
local totalAgentWorking = {};
for [shift in shiftsAtCurrent] {
local agentsWorking <- sum[a in 0...nbAgents](agentShift[a][shift]);
totalAgentWorking.add(agentsWorking);
}
currentStaffing <- sum[w in totalAgentWorking](w);
// Number of agents missing
understaffing.add(max(0, 1 - currentStaffing));
currentTime += shiftIncrement;
}
}
// Total understaffing
totalUnderstaffing <- sum[missingAgent in understaffing](missingAgent) * shiftIncrement;
totalunderstaffing.name = "understaffing";
minimize totalUnderstaffing;
// Objective 2 : Minimize total working time of agents
for[a in 0...nbAgents]{
agentWorkingTime[a] <- sum[s in shifts](agentShift[a][s] * activities[s.activityId].duration);
}
totalWorkingTime <- sum[a in 0...nbAgents](agentWorkingTime[a]);
minimize totalWorkingTime;
}
/**
* Configures solver parameters
*/
function param() {
if (hxTimeLimit == nil) hxTimeLimit = 5;
}
/**
* Outputs solution and prepares visualization data
*/
function output() {
// Print objective values
println("Agent Missing Time in seconds: " + totalUnderstaffing.value);
// Days indicators for the bar chart
days[d in 1..timeHorizon] = "Day "+ d;
// Colors for the dashboard
colorPalette = {
"#5E35B1", "#FDD835", "#43A047", "#039BE5", "#D81B60", "#546E7A",
"#3949AB", "#00ACC1", "#7CB342", "#FFB300", "#6D4C41", "#8E24AA",
"#1E88E5", "#00897B", "#C0CA33", "#FB8C00", "#757575", "#79F8F8",
"#D473D4", "#87E990"
};
// Colors for the bar chart
colors[a in 0...nbAgents] = colorPalette[a];
// Colors for the Gantt chart and the line charts
colors_tasks[i in 0...nbActivities] = colorPalette[i];
// Add tasks for the Gantt chart
solution = {};
solution["start"] = {};
solution["end"] = {};
solution["color_task"] = {};
solution["agent"] = {};
solution["task"] = {};
for [a in 0...nbAgents]{
for [s in shifts]{
if (agentShift[a][s].value){
solution["start"].add(s.shiftStart * 1000);
solution["end"].add((s.shiftEnd) * 1000);
solution["color_task"].add(colors_tasks[s.activityId]);
solution["agent"].add("Agent_" + a);
solution["task"].add("Task_" + s.activityId);
}
}
}
}
- Execution (Windows)
-
set PYTHONPATH=%HX_HOME%\bin\pythonpython workforce_scheduling_shifts instances\3agents7tasks_n1.txt
- Execution (Linux)
-
export PYTHONPATH=/opt/hexaly_14_0/bin/pythonpython workforce_scheduling_shifts.py instances/3agents7tasks_n1.txt
import hexaly.optimizer
import sys
# Activity class represents a work item with its scheduling constraints
# - id: Unique identifier for the task
# - min_start: minimum possible start time (in seconds)
# - max_end: maximum possible end time (in seconds)
# - duration: Duration of the task (in seconds)
class Activity:
def __init__(self, id, min_start, max_end, duration):
self.id = id #int
self.min_start = min_start #int
self.max_end = max_end #int
self.duration = duration #int
# Agent class represents an employee with their availability window
# - id: Unique identifier for the agent
# - availability_start: Start of availability period (in seconds)
# - availability_end: End of availability period (in seconds)
class Agent:
def __init__(self, id, av_start, av_end):
self.id = id #int
self.availability_start = av_start #int
self.availability_end = av_end #int
# Shift class represents a work item with its scheduling constraints
# - id: Unique identifier for the shift
# - activity_id: ID of the activity performed
# - shift_start: Start time of the shift (in seconds)
# - shift_end: End time of the shift (in seconds)
class Shift:
def __init__(self, id, activity_id, shift_start, shift_end):
self.id = id #int
self.activity_id = activity_id #int
self.shift_start = shift_start #int
self.shift_end = shift_end #int
# Generates all possible shifts for activities with given time increment
# @param activities Array of Activity objects
# @param shift_increment Time increment in seconds
# @return Array of generated Shift objects
def generateShifts(activities, shift_increment):
i = 0
shifts = []
for a in activities:
current_time = a.min_start
while current_time + a.duration <= a.max_end:
shifts.append(Shift(i, a.id, current_time, current_time + a.duration))
i += 1
current_time += shift_increment
return shifts
# Checks if two shifts are compatible
# @param slot1 First shift
# @param slot2 Second shift
# @return true if shifts overlap, false otherwise
def incompatibleShifts(shift1, shift2):
startShift1 = shift1.shift_start
endShift1 = shift1.shift_end
startShift2 = shift2.shift_start
endShift2 = shift2.shift_end
return not (startShift1 >= endShift2 or endShift1 <= startShift2)
# Checks if there is at least a one-hour break between two shifts
# @param slot1 First shift
# @param slot2 Second shift
# @return true if there is not enough break, false otherwise
def notEnoughBreak(shift1, shift2):
startShift1 = shift1.shift_start
endShift1 = shift1.shift_end
startShift2 = shift2.shift_start
endShift2 = shift2.shift_end
return not (startShift1 >= endShift2 + 3600 or endShift1 + 3600 <= startShift2)
# Checks if two time intervals overlap
# @param interval1Start Start of first interval
# @param interval1End End of first interval
# @param interval2Start Start of second interval
# @param interval2End End of second interval
# @return true if intervals overlap, false otherwise
def overlappingIntervals(int1_start, int1_end, int2_start, int2_end):
return not (int1_start >= int2_end or int1_end <= int2_start)
if len(sys.argv) < 2:
print("Usage: python tsp.py inputFile [outputFile] [timeLimit]")
sys.exit(1)
# Filters out comments from a text file
# @param filename Path to input file
def read_elem(filename):
with open(filename) as f:
lines = f.readlines()
result = []
for line in lines:
line = line.split("#")[
0
].strip() # Split at '#' and take the part before it
if line: # Only add non-empty lines
result.extend(
line.split()
) # Split the line into elements and add them to the result
return result
# Validates the instance data for consistency
def validateInstance():
# Check if all activities can be completed within their time windows
for activity in activities:
if activity.duration > (activity.max_end - activity.min_start):
raise ValueError(
"Activity "
+ activity.id
+ " duration ("
+ (activity.duration / 3600)
+ "h) exceeds its time window"
)
# Check if workers' availability windows are valid
for agent in agents:
if agent.availability_start >= agent.availability_end:
raise ValueError("Agent " + agent.id + " has invalid availability window")
# Check if activities can be performed within workers' availability
earliest_start = min([act.min_start for act in activities])
latest_end = max([act.max_end for act in activities])
for agent in agents:
if (
agent.availability_start > earliest_start
or agent.availability_end < latest_end
):
print(
"Warning: Agent "
+ str(agent.id)
+ " availability might not cover all tasks"
)
with hexaly.optimizer.HexalyOptimizer() as optimizer:
#
# Read path of input file
#
file_it = iter(read_elem(sys.argv[1]))
# Read dimensions of the problem
nb_agents = int(next(file_it))
nb_activities = int(next(file_it))
time_horizon = int(next(file_it))
shift_increment = round(float(next(file_it)) * 60)
# Read activities informations
# Duration time of each activity
activities = []
for i in range(nb_activities):
activities.append(Activity(i, 0, 0, int(next(file_it)) * 3600))
# Maximum end and minimum possible start hour of each activity
for i in range(nb_activities):
activities[i].min_start = int(next(file_it)) * 3600
activities[i].max_end = int(next(file_it)) * 3600
# Read agents informations
# Availability period in the day of each agent
agents = [
Agent(i, int(next(file_it)), int(next(file_it))) for i in range(nb_agents)
]
# Generation of all possible shifts for each activity with a given increment
shifts = generateShifts(activities, shift_increment)
# Validates the instance data
validateInstance()
#
# Declare the optimization model
#
model = optimizer.model
# Decision variables : agent_shift[a][s] is true when the agent a works the shift s
agent_shift = [[model.bool() for j in range(len(shifts))] for i in range(nb_agents)]
# Constraints
# 1. The shifts performed by an agent must not overlap
for a in range(nb_agents):
for s1 in shifts:
for s2 in shifts:
if incompatibleShifts(s1, s2) and s1 != s2:
model.constraint(agent_shift[a][s1.id] + agent_shift[a][s2.id] <= 1)
# 2. An agent must take at least a one-hour break between two shifts
for a in range(nb_agents):
for s1 in shifts:
for s2 in shifts:
if notEnoughBreak(s1, s2) and s1 != s2:
model.constraint(agent_shift[a][s1.id] + agent_shift[a][s2.id] <= 1)
# 3. Each agent must work exactly two shifts in the day
for a in range(nb_agents):
model.constraint(sum([agent_shift[a][s.id] for s in shifts]) == 2)
# Objective 1 : Minimize understaffing
# For each activity and at each interval of time of the increment length
total_understaffing = 0
for activity in activities:
current_time = activity.min_start
while current_time < activity.max_end:
# Current interval of time
current_interval_start = current_time
current_interval_end = current_time + shift_increment
# The activity will not be pursued after its maximum possible end
if current_interval_end > activity.max_end:
current_interval_end = activity.max_end
# Shifts performed in the current time interval
shifts_at_current = []
for shift in shifts:
if shift.activity_id != activity.id:
continue
if overlappingIntervals(
current_interval_start,
current_interval_end,
shift.shift_start,
shift.shift_end,
):
shifts_at_current.append(shift)
# Number of agents currently performing the task
total_agents_working = 0
for shift in shifts_at_current:
agents_working = sum(
[agent_shift[a][shift.id] for a in range(nb_agents)]
)
total_agents_working += agents_working
# Number of agents missing
total_understaffing += model.max(0, 1 - total_agents_working)
current_time += shift_increment
total_understaffing *= shift_increment
model.minimize(total_understaffing)
# Objective 2 : Minimize the total worktime of all the agents
total_working_time = 0
for a in range(nb_agents):
total_working_time += sum(
[agent_shift[a][s.id] * activities[s.activity_id].duration for s in shifts]
)
model.minimize(total_working_time)
model.close()
# Parameterize the optimizer
if len(sys.argv) >= 4:
optimizer.param.time_limit = int(sys.argv[3])
optimizer.param.verbosity = 2
else:
optimizer.param.time_limit = 5
optimizer.solve()
- Compilation / Execution (Windows)
-
cl /EHsc surgeries_scheduling.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly140.libworkforce_scheduling_shifts instances\3agents7tasks_n1.txt
- Compilation / Execution (Linux)
-
g++ workforce_scheduling_shifts.cpp -I/opt/hexaly_14_0/include -lhexaly140 -lpthread -o surgeries_scheduling./workforce_scheduling_shifts instances/3agents7tasks_n1.txt
#include "optimizer/hexalyoptimizer.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <set>
#include <string.h>
#include <vector>
using namespace hexaly;
using namespace std;
/**
* Activity class represents a work item with its scheduling constraints
* - id: Unique identifier for the activity
* - minStart: minimum possible start time (in seconds)
* - maxEnd: maximum possible end time (in seconds)
* - duration: Duration of the activity (in seconds)
*/
class Activity {
public:
int id;
int minStart;
int maxEnd;
int duration;
Activity(int ID, int minS, int max, int d) {
id = ID;
minStart = minS;
maxEnd = maxEnd;
duration = d;
}
};
/**
* Agent class represents an employee with their availability window
* - id: Unique identifier for the agent
* - availabilityStart: Start of availability period (in seconds)
* - availabilityEnd: End of availability period (in seconds)
*/
class Agent {
public:
int id; // int
int availabilityStart; // int
int availabilityEnd; // int
Agent(int ID, int avStart, int avEnd) {
id = ID;
availabilityStart = avStart;
availabilityEnd = avEnd;
}
};
/**
* Shift class represents a scheduled work period for an activity
* - id: Unique identifier for the shift
* - activityId: ID of the activity performed
* - shiftStart: Start time of the shift (in seconds)
* - shiftEnd: End time of the shift (in seconds)
*/
class Shift {
public:
int id; // int
int activityId; // int
int shiftStart; // int
int shiftEnd; // int
Shift(int ID, int actId, int shiftS, int shiftE) {
id = ID;
activityId = actId;
shiftStart = shiftS;
shiftEnd = shiftE;
}
};
/**
* Generates all possible shifts for activities with given time increment
* @param activities Array of Activity objects
* @param shiftIncrement Time increment in seconds
* @return Array of generated Shift objects
*/
void generateShifts(vector<Shift> &shifts, vector<Activity> activities, int shiftIncrement) {
int i = 0;
for (int j = 0; j < activities.size(); j++) {
int currentTime = activities[j].minStart;
while (currentTime + activities[j].duration <= activities[j].maxEnd) {
Shift s = Shift(i, activities[j].id, currentTime, currentTime + activities[j].duration);
shifts.push_back(s);
currentTime += shiftIncrement;
i += 1;
}
}
};
/**
* Checks if two shifts are compatible
* @param shif1 First shift
* @param shift2 Second shift
* @return true if shifts overlap, false otherwise
*/
bool incompatibleShifts(Shift shift1, Shift shift2) {
int startShift1 = shift1.shiftStart;
int endShift1 = shift1.shiftEnd;
int startShift2 = shift2.shiftStart;
int endShift2 = shift2.shiftEnd;
int actId1 = shift1.activityId;
int actId2 = shift2.activityId;
return !(startShift1 >= endShift2 || endShift1 <= startShift2);
};
/**
* Checks if there is at least a one-hour break between two shifts
* @param shift1 First shift
* @param shift2 Second shift
* @return true if there is not enough break, false otherwise
*/
bool notEnoughBreak(Shift shift1, Shift shift2) {
int startShift1 = shift1.shiftStart;
int endShift1 = shift1.shiftEnd;
int startShift2 = shift2.shiftStart;
int endShift2 = shift2.shiftEnd;
int actId1 = shift1.activityId;
int actId2 = shift2.activityId;
return !(startShift1 >= endShift2 + 3600 || endShift1 + 3600 <= startShift2);
};
/**
* Checks if two time intervals overlap
* @param interval1Start Start of first interval
* @param interval1End End of first interval
* @param interval2Start Start of second interval
* @param interval2End End of second interval
* @return true if intervals overlap, false otherwise
*/
bool overlappingIntervals(int int1Start, int int1End, int int2Start, int int2End) {
return !(int1Start >= int2End || int1End <= int2Start);
};
class WorkshopSchedulingShifts {
private:
// Number of agents
int nbAgents;
// Number of activities
int nbActivities;
// Number of days
int timeHorizon;
// Increment
int shiftIncrement;
// Activities informations
vector<Activity> activities;
// Agents informations
vector<Agent> agents;
// Possible shifts
vector<Shift> shifts;
// Hexaly Optimizer
HexalyOptimizer optimizer;
// Decision variables: time range of each task
vector<vector<HxExpression>> agentShift;
// Objective = minimize the missing agent time and total working time of the agents
HxExpression totalUnderstaffing;
HxExpression totalWorkingTime;
public:
// The input files follow the "Taillard" format
void readInstance(const string &fileName) {
ifstream infile;
if (!infile) {
cerr << "Unable to open file: " << fileName << endl;
}
infile.open(fileName.c_str());
std::string line;
std::vector<std::string> elements;
while (std::getline(infile, line)) {
std::istringstream iss(line);
std::string elem;
while (iss >> elem) {
if (elem[0] == '#')
break; // Ignore the rest of the line if a comment is found
elements.push_back(elem);
}
}
// Read problem dimensions
auto it = elements.begin();
nbAgents = std::stoi(*it++);
nbActivities = std::stoi(*it++);
timeHorizon = std::stoi(*it++);
shiftIncrement = std::stof(*it++) * 3600;
infile.ignore(numeric_limits<streamsize>::max(), '\n');
// Read activities informations
// Duration time of each activity
for (int i = 0; i < nbActivities; i++) {
int activityDuration;
activityDuration = stoi(*it++);
activities.push_back(Activity(i, 0, 0, activityDuration * 3600));
}
// Maximum end and minimum possible start hour of each activity
for (int i = 0; i < nbActivities; i++) {
int minStart = std::stoi(*it++);
int maxEnd = std::stoi(*it++);
activities[i].minStart = minStart * 3600;
activities[i].maxEnd = maxEnd * 3600;
}
// Read agents informations
for (int i = 0; i < nbAgents; i++) {
int availabilityStart = std::stoi(*it++);
int availabilityEnd = std::stoi(*it++);
infile >> availabilityStart >> availabilityEnd;
agents.push_back(Agent(i, availabilityStart * 3600, availabilityEnd * 3600));
}
// Generation of all possible shifts for each activity with a given increment
generateShifts(shifts, activities, shiftIncrement);
// Validates the instance data
validateInstance();
infile.close();
}
/**
* Validates the instance data for consistency
*/
void validateInstance() {
// Check if all activities can be completed within their time windows
for (Activity activity : activities) {
if (activity.duration > (activity.maxEnd - activity.minStart)) {
throw("Activity " + to_string(activity.id) + " duration (" + to_string(activity.duration / 3600) +
"h) exceeds its time window");
}
}
// Check if workers' availability windows are valid
for (Agent agent : agents) {
if (agent.availabilityStart >= agent.availabilityEnd) {
throw("Agent " + to_string(agent.id) + " has invalid availability window");
}
}
// Check if activities can be performed within workers' availability
auto earliestStartIt =
std::min_element(activities.begin(), activities.end(),
[](const Activity &a, const Activity &b) { return a.minStart < b.minStart; });
auto latestEndIt =
std::max_element(activities.begin(), activities.end(),
[](const Activity &a, const Activity &b) { return a.minStart < b.minStart; });
int earliestStart = earliestStartIt->minStart;
int latestEnd = latestEndIt->maxEnd;
for (Agent agent : agents) {
if (agent.availabilityStart > earliestStart || agent.availabilityEnd < latestEnd) {
cout << "Warning: Agent " << agent.id << " availability might not cover all tasks" << endl;
}
}
}
void solve(int timeLimit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
// Decision variables : AgentShift[a][s] is true when the agent a works the shift s
agentShift.resize(nbAgents);
for (unsigned int i = 0; i < nbAgents; i++) {
agentShift[i].resize(shifts.size());
for (unsigned int j = 0; j < shifts.size(); j++) {
agentShift[i][j] = model.boolVar();
}
}
// Constraints
// 1. The shifts performed by an agent must not overlap
for (int a = 0; a < nbAgents; a++) {
for (auto s1 = shifts.begin(); s1 != shifts.end(); s1++) {
for (auto s2 = shifts.begin(); s2 != shifts.end(); s2++) {
if (incompatibleShifts(*s1, *s2) && s1 != s2) {
model.constraint(agentShift[a][s1->id] + agentShift[a][s2->id] <= 1);
}
}
}
}
// 2. An agent must take at least a one-hour break between two shifts
for (int a = 0; a < nbAgents; a++) {
for (auto s1 = shifts.begin(); s1 != shifts.end(); s1++) {
for (auto s2 = shifts.begin(); s2 != shifts.end(); s2++) {
if (notEnoughBreak(*s1, *s2) && s1 != s2) {
model.constraint(agentShift[a][s1->id] + agentShift[a][s2->id] <= 1);
}
}
}
}
// 3. Each agent must work exactly two shifts in the day
for (int a = 0; a < nbAgents; a++) {
HxExpression shiftsDone = model.sum();
for (int id = 0; id < shifts.size(); id++) {
shiftsDone.addOperand(agentShift[a][id]);
}
model.constraint(shiftsDone == 2);
}
// Objective 1 : Minimize understaffing
HxExpression totalUnderstaffing = model.sum();
for (auto activity = activities.begin(); activity != activities.end(); activity++) {
int currentTime = activity->minStart;
while (currentTime < activity->maxEnd) {
// Current interval of time
int currentIntervalStart = currentTime;
int currentIntervalEnd = currentTime + shiftIncrement;
// The activity will not be pursued after its maximum possible end
if (currentIntervalEnd > activity->maxEnd) {
currentIntervalEnd = activity->maxEnd;
}
// Shifts performed in the current time interval
vector<Shift> shiftsAtCurr;
for (auto shift = shifts.begin(); shift != shifts.end(); shift++) {
if (shift->activityId != activity->id) {
continue;
}
if (overlappingIntervals(currentIntervalStart, currentIntervalEnd, shift->shiftStart,
shift->shiftEnd)) {
shiftsAtCurr.push_back(*shift);
}
}
// Number of agents currently performing the task
HxExpression totalAgentsWorking = model.sum();
for (auto shift = shiftsAtCurr.begin(); shift != shiftsAtCurr.end(); shift++) {
for (int a = 0; a < nbAgents; a++) {
totalAgentsWorking.addOperand(agentShift[a][shift->id]);
}
}
// Number of agents missing
totalUnderstaffing.addOperand(shiftIncrement * model.max(0, 1 - totalAgentsWorking));
currentTime += shiftIncrement;
}
}
model.minimize(totalUnderstaffing);
// Objective 2 : Minimize the total worktime of all the agents
HxExpression totalWorkingTime = model.sum();
for (int a = 0; a < nbAgents; a++) {
for (auto shift = shifts.begin(); shift != shifts.end(); shift++) {
totalWorkingTime.addOperand(agentShift[a][shift->id] * activities[shift->activityId].duration);
}
}
model.minimize(totalWorkingTime);
model.close();
// Parametrize the optimizer
optimizer.getParam().setTimeLimit(10);
optimizer.solve();
}
};
int main(int argc, char **argv) {
if (argc < 2) {
cerr << "Usage: workforce_scheduling_shifts inputFile [outputFile] [timeLimit]" << endl;
return 1;
}
const char *instanceFile = argv[1];
const char *strTimeLimit = argc > 2 ? argv[2] : "5";
try {
WorkshopSchedulingShifts model;
model.readInstance(instanceFile);
model.solve(atoi(strTimeLimit));
} catch (const exception &e) {
cerr << "An error occurred: " << e.what() << endl;
return 1;
}
};
- Compilation / Execution (Windows)
-
copy %HX_HOME%\bin\Hexaly.NET.dll .csc WorkforceSchedulingShifts.cs /reference:Hexaly.NET.dllWorkforceSchedulingShifts instances\3agents7tasks_n1.txt
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using Hexaly.Optimizer;
/**
* Activity class represents a work item with its scheduling constraints
* - id: Unique identifier for the activity
* - minStart: minimum possible start time (in seconds)
* - maxEnd: maximum possible end time (in seconds)
* - duration: Duration of the activity (in seconds)
*/
class Activity
{
public int activityId;
public int actMinStart;
public int actMaxEnd;
public int actDuration;
public Activity(int actId, int actMinStart, int actMaxEnd, int actDuration)
{
this.activityId = actId;
this.actMinStart = actMinStart;
this.actMaxEnd = actMaxEnd;
this.actDuration = actDuration;
}
}
/**
* Agent class represents an employee with their availability window
* - id: Unique identifier for the agent
* - availabilityStart: Start of availability period (in seconds)
* - availabilityEnd: End of availability period (in seconds)
*/
class Agent
{
public int agentId;
public int availabilityStart;
public int availabilityEnd;
public Agent(int agentId, int avStart, int avEnd)
{
this.agentId = agentId;
this.availabilityStart = avStart;
this.availabilityEnd = avEnd;
}
}
/**
* Shift class represents a scheduled work period for an activity
* - id: Unique identifier for the shift
* - activityId: ID of the activity performed
* - shiftStart: Start time of the shift (in seconds)
* - shiftEnd: End time of the shift (in seconds)
*/
public class Shift
{
public int shiftId;
public int shiftActivity;
public int shiftStart;
public int shiftEnd;
public Shift(int id, int actId, int start, int end)
{
this.shiftId = id;
this.shiftActivity = actId;
this.shiftStart = start;
this.shiftEnd = end;
}
}
public class WorkforceSchedulingShifts : IDisposable
{
// Dimensions of the problem
int nbAgents;
int nbActivities;
int timeHorizon;
int shiftIncrement;
// Activities
Activity[] activities;
// Agents
Agent[] agents;
// Shifts
int nbShifts;
Shift[] shifts;
// Hexaly Optimizer
HexalyOptimizer optimizer;
// Decision variables
HxExpression[,] agentShift;
// Objectives
HxExpression totalUnderstaffing;
HxExpression totalWorkingTime;
public WorkforceSchedulingShifts()
{
optimizer = new HexalyOptimizer();
}
public void Dispose()
{
if (optimizer != null)
optimizer.Dispose();
}
private void Solve(int limit)
{
// Declare the optimization model
HxModel model = optimizer.GetModel();
// Decision variables : agentShift[a, s] is true when the agent a works the shift s
agentShift = new HxExpression[nbAgents, nbShifts];
for (int i = 0; i < nbAgents; i++)
{
for (int j = 0; j < nbShifts; j++)
{
agentShift[i, j] = model.Bool();
}
}
// Constraints
// 1. The shifts performed by an agent must not overlap
for (int a = 0; a < nbAgents; a++)
{
for (int s1 = 0; s1 < nbShifts; s1++)
{
for (int s2 = 0; s2 < nbShifts; s2++)
{
if (OverlappingShifts(shifts[s1], shifts[s2]) && s1 != s2)
{
model.Constraint(model.Leq(model.Sum(agentShift[a, s1], agentShift[a, s2]), 1));
}
}
}
}
// 2. An agent must take at least a one-hour break between two shifts
for (int a = 0; a < nbAgents; a++)
{
for (int s1 = 0; s1 < nbShifts; s1++)
{
for (int s2 = 0; s2 < nbShifts; s2++)
{
if (NotEnoughBreak(shifts[s1], shifts[s2]) && s1 != s2)
{
model.Constraint(model.Leq(model.Sum(agentShift[a, s1], agentShift[a, s2]), 1));
}
}
}
}
// 3. Each agent must work exactly two shifts in the day
for (int a = 0; a < nbAgents; a++)
{
HxExpression shiftsDone = model.Sum();
for (int id = 0; id < nbShifts; id++)
{
shiftsDone.AddOperand(agentShift[a, id]);
}
model.Constraint(model.Eq(shiftsDone, 2));
}
// Objective 1 : Minimize understaffing
HxExpression totalUnderstaffing = model.Sum();
for (int i = 0; i < nbActivities; i++)
{
int currentTime = activities[i].actMinStart;
while (currentTime < activities[i].actMaxEnd)
{
// Current interval of time
int currentIntervalStart = currentTime;
int currentIntervalEnd = currentTime + shiftIncrement;
// The activity will not be pursued after its maximum possible end
if (currentIntervalEnd > activities[i].actMaxEnd)
{
currentIntervalEnd = activities[i].actMaxEnd;
}
// Shifts performed in the current time interval
HxExpression totalAgentsWorking = model.Sum();
for (int j = 0; j < nbShifts; j++)
{
if (shifts[j].shiftActivity != i)
{
continue;
}
if (OverlappingIntervals(currentIntervalStart, currentIntervalEnd, shifts[j].shiftStart, shifts[j].shiftEnd))
{
for (int a = 0; a < nbAgents; a++)
{
totalAgentsWorking.AddOperand(agentShift[a, j]);
}
}
}
// Number of agents missing
totalUnderstaffing.AddOperand(model.Prod(shiftIncrement, model.Max(0, model.Sub(1, totalAgentsWorking))));
currentTime += shiftIncrement;
}
}
model.Minimize(totalUnderstaffing);
// Objective 2 : Minimize the total worktime of all the agents
HxExpression totalWorkingTime = model.Sum();
for (int a = 0; a < nbAgents; a++)
{
for (int s = 0; s < nbShifts; s++)
{
totalWorkingTime.AddOperand(model.Prod(agentShift[a, s], activities[shifts[s].shiftActivity].actDuration));
}
}
model.Minimize(totalWorkingTime);
model.Close();
// Parametrize the optimizer
optimizer.GetParam().SetTimeLimit(10);
optimizer.Solve();
}
/**
* Reads and parses input data from file
* @param fileName Path to input file
*/
void ReadInstance(string fileName)
{
using (StreamReader input = new StreamReader(fileName))
{
string[] splitted;
// Read problem dimensions
nbAgents = int.Parse(ReadLineIgnoringComments(input));
nbActivities = int.Parse(ReadLineIgnoringComments(input));
timeHorizon = int.Parse(ReadLineIgnoringComments(input));
shiftIncrement = (int)(double.Parse(ReadLineIgnoringComments(input), CultureInfo.InvariantCulture) * 3600);
// Read activities informations
// Activity duration
activities = new Activity[nbActivities];
for (int i = 0; i < nbActivities; i++)
{
activities[i] = new Activity(0, 0, 0, int.Parse(ReadLineIgnoringComments(input)) * 3600);
}
// Possible period to do the activity
for (int i = 0; i < nbActivities; i++)
{
splitted = ReadLineIgnoringComments(input).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
activities[i].actMinStart = int.Parse(splitted[0]) * 3600;
activities[i].actMaxEnd = int.Parse(splitted[1]) * 3600;
}
//Read agents information
agents = new Agent[nbAgents];
// Availability period in the day of each agent
for (int i = 0; i < nbAgents; i++)
{
splitted = ReadLineIgnoringComments(input).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
agents[i] = new Agent(i, int.Parse(splitted[0]) * 3600, int.Parse(splitted[1]) * 3600);
};
// Generation of all possible shifts for each activity with a given increment
shifts = GenerateShifts(activities, shiftIncrement, shifts);
// Validates the instance data
ValidateInstance();
}
}
/**
* Validates the instance data for consistency
*/
public void ValidateInstance()
{
// Check if all activities can be completed within their time windows
foreach (Activity activity in activities)
{
if (activity.actDuration > (activity.actMaxEnd - activity.actMinStart))
{
throw new ArgumentException($"Activity {activity.activityId} duration ({activity.actDuration / 3600}h) exceeds its time window");
}
}
// Check if workers' availability windows are valid
foreach (Agent agent in agents)
{
if (agent.availabilityStart >= agent.availabilityEnd)
{
throw new ArgumentException($"Agent {agent.agentId} has invalid availability window");
}
}
// Check if activities can be performed within workers' availability
int earliestStart = activities.Min(a => a.actMinStart);
int latestEnd = activities.Max(a => a.actMaxEnd);
foreach (Agent agent in agents)
{
if (agent.availabilityStart > earliestStart || agent.availabilityEnd < latestEnd)
{
Console.WriteLine($"Warning: Agent {agent.agentId} availability might not cover all tasks");
}
}
}
/**
* Ignore comments in a text file
* @param input scanner object, reading data
* @return next line in the file, ignoring comments
*/
string ReadLineIgnoringComments(StreamReader input)
{
string line;
do
{
line = input.ReadLine();
if (line == null) return null;
line = line.Split('#')[0].Trim();
} while (string.IsNullOrEmpty(line));
return line;
}
/**
* Generates all possible shifts for activities with given time increment
* @param activities Array of Activity objects
* @param shiftIncrement Time increment in seconds
* @return Array of generated Shift objects
*/
Shift[] GenerateShifts(Activity[] activities, int shiftIncrement, Shift[] shifts)
{
int i = 0;
for (int j = 0; j < nbActivities; j++)
{
int currentTime = activities[j].actMinStart;
while (currentTime + activities[j].actDuration <= activities[j].actMaxEnd)
{
Array.Resize(ref shifts, i + 1);
shifts[i] = new Shift(i, j, currentTime, currentTime + activities[j].actDuration);
currentTime += shiftIncrement;
i += 1;
}
}
nbShifts = i;
return shifts;
}
/**
* Checks if there is at least a one-hour break between two shifts
* @param s1 First shift
* @param s2 Second shift
* @return true if there is not enough break, false otherwise
*/
bool NotEnoughBreak(Shift s1, Shift s2)
{
return !(s1.shiftStart >= s2.shiftEnd + 3600 || s1.shiftEnd + 3600 <= s2.shiftStart);
}
/**
* Checks if two shifts are compatible
* @param s1 First shift
* @param s2 Second shift
* @return true if shifts overlap, false otherwise
*/
bool OverlappingShifts(Shift s1, Shift s2)
{
return !(s1.shiftStart >= s2.shiftEnd || s1.shiftEnd <= s2.shiftStart);
}
/**
* Checks if two time intervals overlap
* @param interval1Start Start of first interval
* @param interval1End End of first interval
* @param interval2Start Start of second interval
* @param interval2End End of second interval
* @return true if intervals overlap, false otherwise
*/
bool OverlappingIntervals(int interval1Start, int interval1End, int interval2Start, int interval2End)
{
return !(interval1Start >= interval2End || interval1End <= interval2Start);
}
public static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: WorkforceSchedulingShifts inputFile [solFile] [timeLimit]");
Environment.Exit(1);
}
string instanceFile = args[0];
string outputFile = args.Length > 1 ? args[1] : null;
string strTimeLimit = args.Length > 2 ? args[2] : "20";
using (WorkforceSchedulingShifts model = new WorkforceSchedulingShifts())
{
model.ReadInstance(instanceFile);
model.Solve(int.Parse(strTimeLimit));
}
}
}
- Compilation / Execution (Windows)
-
javac WorkforceSchedulingShifts.java -cp %HX_HOME%\bin\hexaly.jarjava -cp %HX_HOME%\bin\hexaly.jar;. WorkforceSchedulingShifts instances\3agents7tasks_n1txt
- Compilation / Execution (Linux)
-
javac WorkforceSchedulingShifts.java -cp /opt/hexaly_14_0/bin/hexaly.jarjava -cp /opt/hexaly_14_0/bin/hexaly.jar:. WorkforceSchedulingShifts instances/3agents7tasks_n1.txt
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;
/**
* Activity class represents a work item with its scheduling constraints
* - id: Unique identifier for the activity
* - minStart: minimum possible start time (in seconds)
* - maxEnd: maximum possible end time (in seconds)
* - duration: Duration of the activity (in seconds)
*/
class Activity{
int activityId;
int actMinStart;
int actMaxEnd;
int actDuration;
Activity(int actId, int actMinStart, int actMaxEnd, int actDuration){
this.activityId = actId;
this.actMinStart = actMinStart;
this.actMaxEnd = actMaxEnd;
this.actDuration = actDuration;
}
}
/**
* Agent class represents an employee with their availability window
* - id: Unique identifier for the agent
* - availabilityStart: Start of availability period (in seconds)
* - availabilityEnd: End of availability period (in seconds)
*/
class Agent{
int agentId;
int availabilityStart;
int availabilityEnd;
Agent(int agentId, int avStart, int avEnd){
this.agentId = agentId;
this.availabilityStart = avStart;
this.availabilityEnd = avEnd;
}
}
/**
* Shift class represents a scheduled work period for an activity
* - id: Unique identifier for the shift
* - activityId: ID of the activity performed
* - shiftStart: Start time of the shift (in seconds)
* - shiftEnd: End time of the shift (in seconds)
*/
class Shift{
int shiftId;
int shiftActivity;
int shiftStart;
int shiftEnd;
Shift(int shiftId, int shiftActivity, int shiftStart, int shiftEnd){
this.shiftId = shiftId;
this.shiftActivity = shiftActivity;
this.shiftStart = shiftStart;
this.shiftEnd = shiftEnd;
}
}
public class WorkforceSchedulingShifts {
private final HexalyOptimizer optimizer;
// Dimensions of the problem
int nbAgents;
int nbActivities;
int timeHorizon;
int shiftIncrement;
// Activities
Activity[] activities;
// Agents
Agent[] agents;
// Shifts
int nbShifts;
Shift[] shifts;
// Decision variables: time range of each task
HxExpression[][] agentShift;
// Objective = minimize the missing agent time and total working time of the agents
HxExpression totalUnderstaffing;
HxExpression totalWorkingTime;
private WorkforceSchedulingShifts(HexalyOptimizer optimizer) {
this.optimizer = optimizer;
}
private void solve(int limit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
agentShift = new HxExpression[nbAgents][nbShifts];
for(int i = 0; i< nbAgents; i++){
for(int j = 0 ; j < nbShifts; j++){
agentShift[i][j] = model.boolVar();
}
}
// Constraints
// 1. The shifts performed by an agent must not overlap
for (int a = 0; a < nbAgents; a++){
for(int s1 = 0 ; s1 < nbShifts; s1++){
for(int s2 = 0; s2 < nbShifts; s2++){
if(overlappingShifts(shifts[s1], shifts[s2]) && s1 != s2){
model.constraint(model.leq(model.sum(agentShift[a][s1], agentShift[a][s2]), 1));
}
}
}
}
// 2. An agent must take at least a one-hour break between two shifts
for (int a=0; a < nbAgents; a++){
for(int s1 = 0; s1 < nbShifts; s1++){
for(int s2 = 0; s2 < nbShifts; s2++){
if(notEnoughBreak(shifts[s1], shifts[s2]) && s1 != s2){
model.constraint(model.leq(model.sum(agentShift[a][s1], agentShift[a][s2]), 1));
}
}
}
}
// 3. Each agent must work exactly two shifts in the day
for(int a = 0; a < nbAgents; a++){
HxExpression shiftsDone = model.sum();
for(int id = 0; id < nbShifts; id++){
shiftsDone.addOperand(agentShift[a][id]);
}
model.constraint(model.eq(shiftsDone, 2));
}
// Objective 1 : Minimize understaffing
HxExpression totalUnderstaffing=model.sum();
for(int i = 0; i < nbActivities; i++){
int currentTime = activities[i].actMinStart;
while (currentTime < activities[i].actMaxEnd){
// Current interval of time
int currentIntervalStart = currentTime;
int currentIntervalEnd = currentTime + shiftIncrement;
// The activity will not be pursued after its maximum possible end
if (currentIntervalEnd > activities[i].actMaxEnd){
currentIntervalEnd = activities[i].actMaxEnd;
}
// Shifts performed in the current time interval
HxExpression totalAgentsWorking=model.sum();
for(int j = 0 ; j < nbShifts; j++){
if(shifts[j].shiftActivity != i){
continue;
}
if(overlappingIntervals(currentIntervalStart, currentIntervalEnd, shifts[j].shiftStart, shifts[j].shiftEnd)){
for(int a = 0; a < nbAgents; a++){
totalAgentsWorking.addOperand(agentShift[a][j]);
}
}
}
// Number of agents missing
totalUnderstaffing.addOperand(model.prod(shiftIncrement, model.max(0, model.sub(1, totalAgentsWorking))));
currentTime += shiftIncrement;
}
}
model.minimize(totalUnderstaffing);
// Objective 2 : Minimize the total worktime of all the agents
HxExpression totalWorkingTime = model.sum();
for (int a = 0; a < nbAgents; a++){
for(int s = 0; s < nbShifts; s++){
totalWorkingTime.addOperand(model.prod(agentShift[a][s], activities[shifts[s].shiftActivity].actDuration));
}
}
model.minimize(totalWorkingTime);
model.close();
// Parametrize the optimizer
optimizer.getParam().setTimeLimit(10);
optimizer.solve();
}
/**
* Ignore comments in a text file
* @param input scanner object, reading data
* @return next int in the file, ignoring comments
*/
private static int nextIntIgnoringComments(Scanner input) {
while (input.hasNextLine()) {
String line = input.nextLine().split("#")[0].trim();
if (!line.isEmpty()) {
Scanner lineScanner = new Scanner(line);
if (lineScanner.hasNextInt()) {
return lineScanner.nextInt();
}
}
}
throw new IllegalArgumentException("No valid integer found");
}
/**
* Ignore comments in a text file
* @param input scanner object, reading data
* @return next double in the file, ignoring comments
*/
private static double nextDoubleIgnoringComments(Scanner input) {
while (input.hasNextLine()) {
String line = input.nextLine().split("#")[0].trim();
if (!line.isEmpty()) {
Scanner lineScanner = new Scanner(line).useLocale(Locale.ROOT);
if (lineScanner.hasNextDouble()) {
return lineScanner.nextDouble();
}
}
}
throw new IllegalArgumentException("No valid double found");
}
/**
* Ignore comments in a text file
* @param input scanner object, reading data
* @return next line in the file, ignoring comments
*/
private static String nextLineIgnoringComments(Scanner input) {
while (input.hasNextLine()) {
String line = input.nextLine().split("#")[0].trim();
if (!line.isEmpty()) {
return line;
}
}
throw new IllegalArgumentException("No valid line found");
}
/*
* Read instance data
*/
private void readInstance(String fileName) throws IOException {
try (Scanner input = new Scanner(new File(fileName))) {
// Read problem dimensions
input.useLocale(Locale.ROOT);
nbAgents = nextIntIgnoringComments(input);
nbActivities = nextIntIgnoringComments(input);
timeHorizon = nextIntIgnoringComments(input);
shiftIncrement = (int) (nextDoubleIgnoringComments(input) * 3600);
// Read activities informations
activities = new Activity[nbActivities];
for(int i = 0; i < nbActivities; i++){
activities[i]= new Activity(i, 0, 0, 0);
activities[i].actDuration = nextIntIgnoringComments(input) * 3600;
}
for(int i = 0; i < nbActivities; i++){
String line = nextLineIgnoringComments(input);
Scanner lineScanner = new Scanner(line);
activities[i].actMinStart = lineScanner.nextInt() * 3600;
activities[i].actMaxEnd = lineScanner.nextInt() * 3600;
}
// Read agents informations
agents = new Agent[nbAgents];
for(int i = 0; i < nbAgents; i++){
String line = nextLineIgnoringComments(input);
Scanner lineScanner = new Scanner(line);
agents[i] = new Agent(i, 0, 0);
agents[i].availabilityStart = lineScanner.nextInt() * 3600;
agents[i].availabilityEnd = lineScanner.nextInt() * 3600;
}
// Generation of all possible shifts for each activity with a given increment
generateShifts(activities, shiftIncrement);
// Validates the instance data
validateInstance();
}
}
/**
* Generates all possible shifts for activities with given time increment
* @param activities Array of Activity objects
* @param shiftIncrement Time increment in seconds
* @return Array of generated Shift objects
*/
void generateShifts(Activity[] activities, int shiftIncrement){
int i = 0;
shifts = new Shift[]{};
for(int j =0; j< nbActivities ; j++){
int currentTime = activities[j].actMinStart;
while(currentTime+activities[j].actDuration <= activities[j].actMaxEnd){
Shift[] newshifts = Arrays.copyOf(shifts, shifts.length + 1);
shifts = newshifts;
shifts[i] = new Shift(i, 0, 0, 0);
shifts[i].shiftId = i;
shifts[i].shiftActivity = j;
shifts[i].shiftStart = currentTime;
shifts[i].shiftEnd = currentTime + activities[j].actDuration;
currentTime += shiftIncrement;
i += 1;
}
}
nbShifts = i;
}
/**
* Checks if there is at least a one-hour break between two shifts
* @param shift1 First shift
* @param shift2 Second shift
* @return true if there is not enough break, false otherwise
*/
boolean notEnoughBreak(Shift s1, Shift s2){
return !(s1.shiftStart >= s2.shiftEnd + 3600 || s1.shiftEnd + 3600 <= s2.shiftStart);
}
/**
* Checks if two shifts are compatible
* @param shift1 First shift
* @param shift2 Second shift
* @return true if shifts overlap, false otherwise
*/
boolean overlappingShifts(Shift s1, Shift s2){
return !(s1.shiftStart >= s2.shiftEnd || s1.shiftEnd <= s2.shiftStart);
}
/**
* Checks if two time intervals overlap
* @param interval1Start Start of first interval
* @param interval1End End of first interval
* @param interval2Start Start of second interval
* @param interval2End End of second interval
* @return true if intervals overlap, false otherwise
*/
boolean overlappingIntervals(int interval1Start, int interval1End, int interval2Start, int interval2End){
return !(interval1Start >= interval2End || interval1End <= interval2Start);
}
/**
* Validates the instance data for consistency
*/
void validateInstance() {
// Check if all activities can be completed within their time windows
for (Activity activity : activities) {
if (activity.actDuration > (activity.actMaxEnd - activity.actMinStart)) {
throw new IllegalArgumentException("Activity " + activity.activityId + " duration (" + (activity.actDuration / 3600) + "h) exceeds its time window");
}
}
// Check if workers' availability windows are valid
for (Agent agent : agents) {
if (agent.availabilityStart >= agent.availabilityEnd) {
throw new IllegalArgumentException("Agent " + agent.agentId + " has invalid availability window");
}
}
// Check if activities can be performed within workers' availability
Activity earliestStartActivity = Arrays.stream(activities).min(Comparator.comparingInt(a -> a.actMinStart)).orElseThrow();
Activity latestEndActivity = Arrays.stream(activities).max(Comparator.comparingInt(a -> a.actMaxEnd)).orElseThrow();
int earliestStart = earliestStartActivity.actMinStart;
int latestEnd = latestEndActivity.actMaxEnd;
for (Agent agent : agents) {
if (agent.availabilityStart > earliestStart || agent.availabilityEnd < latestEnd) {
System.out.println("Warning: Agent " + agent.agentId + " availability might not cover all tasks");
}
}
}
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: java WorkforceSchedulingShifts inputFile [outputFile] [timeLimit]");
System.exit(1);
}
try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
String instanceFile = args[0];
String outputFile = args.length > 1 ? args[1] : null;
String strTimeLimit = args.length > 2 ? args[2] : "20";
WorkforceSchedulingShifts model = new WorkforceSchedulingShifts(optimizer);
model.readInstance(instanceFile);
model.solve(Integer.parseInt(strTimeLimit));
} catch (Exception ex) {
System.err.println(ex);
ex.printStackTrace();
System.exit(1);
}
}
}