1
0
This repository has been archived on 2025-03-06. You can view files and clone it, but cannot push or open issues or pull requests.

598 lines
23 KiB
MiniZinc

%------------------------------------------------------------------------------%
% Rotating Workforce Scheduling
%
% The rotating workforce scheduling problem aims to schedule workers satisfying
% shift sequence constraints and ensuring enough shifts are covered on each day,
% where every worker completes the same schedule, just starting at different days
% in the schedule.
%
% This model is taken from the publication of the paper listed below.
%
% Publication:
% Nysret Musliu, Andreas Schutt, Peter J. Stuckey (2018): "Solver independent
% rotating workforce scheduling", CPAIOR 2018.
% (https://link.springer.com/chapter/10.1007/978-3-319-93031-2_31)
%
% Submitter: Andreas Schutt
%
%------------------------------------------------------------------------------%
%------------------------------------------------------------------------------%
% Input Parameters
%------------------------------------------------------------------------------%
%------------------------------------------------------------------------------%
% Schedule Parameters
int: week_length; % Length of the schedule
int: nb_workers; % Number of the workers/employees
int: min_daysoff; % Minimal length of days-off blocks
int: max_daysoff; % Maximal length of days-off blocks
int: min_work; % Minimal length of work blocks
int: max_work; % Maximal lenght of work blocks
%------------------------------------------------------------------------------%
% Shifts
int: nb_shifts; % Number of shifts
array[SHIFT] of string: shift_name; % Name of shifts
array[SHIFT] of int: shift_start; % Start of shift
array[SHIFT] of int: shift_length; % Length of shift
array[SHIFT] of int: shift_block_min; % Minimal length of blocks
array[SHIFT] of int: shift_block_max; % Maximal length of blocks
%------------------------------------------------------------------------------%
% Temporal Requirements
array[SHIFT, DAY] of int: temp_req; % Temporal requirements
%------------------------------------------------------------------------------%
% Forbidden Shift Sequences
int: nb_forbidden; % Number of forbidden shift sequences
array[FORBIDDEN] of int: forbidden_before; % The shift before in the forbidden sequence
array[FORBIDDEN] of int: forbidden_after; % The shift after in the forbidden sequence
array[FORBIDDEN] of bool: forbidden_daysoff; % If a days-off period is between forbidden shifts
%------------------------------------------------------------------------------%
% Sorting of shifts
array[SHIFT0] of float: shift_sort;
shift_sort = sort_none;
%------------------------------------------------------------------------------%
%------------------------------------------------------------------------------%
% Derived Parameters
%
% This file contains all parameters derived from the input parameters
%------------------------------------------------------------------------------%
%------------------------------------------------------------------------------%
% Instance Parameters
% Problem size calculated by the total number of worker days and the number
% of possible shifts per day
int: problem_size = schedule_length * card(SHIFT0);
%------------------------------------------------------------------------------%
% Schedule Parameters
% The number of workers plus the number of workers repeated at the end of the schedule
int: nb_workers0 = nb_workers + 1;
% The length of the schedule / the total number of worker days of the schedule
int: schedule_length = week_length * nb_workers;
% The length of the extended schedule / the total number of worker days of the extended schedule
int: schedule_length0 = week_length * nb_workers0;
set of int: DAY = 1..week_length; % The length of a week for a worker
set of int: WEEK = 1..nb_workers; % The set of weeks in the schedule
set of int: WEEK0 = 1..nb_workers0; % The set of weeks in the extended schedule
set of int: SCHEDULE = 1..schedule_length; % The set of days in the planning horizon
set of int: SCHEDULE0 = 1..schedule_length0; % The set of days in the planning horizon plus the extra week at the end
%------------------------------------------------------------------------------%
% Shifts
int: nb_shifts0 = nb_shifts + 1; % The number of shifts including the off-duty days
set of int: SHIFT = 1..nb_shifts; % Set of shifts
set of int: SHIFT0 = 1..nb_shifts0; % Set of shifts including off-duty day
int: OFF = nb_shifts0; % Identifier of the off-duty day
% Total number of required shift days per shift
array[SHIFT] of int: nb_shift_days_req = [ sum(d in DAY)(temp_req[s, d]) | s in SHIFT ];
% Minimal number of shift blocks per shift
array[SHIFT] of float: min_shift_blocks = [ int2float(nb_shift_days_req[s]) / int2float(shift_block_max[s]) | s in SHIFT ];
% Maximimal number of shift blocks per shift
array[SHIFT] of float: max_shift_blocks = [ int2float(nb_shift_days_req[s]) / int2float(shift_block_min[s]) | s in SHIFT ];
%------------------------------------------------------------------------------%
% On-Duty Parameters
% Total number of required worker days
int: nb_on_duty_days = sum(nb_shift_days_req);
% Minimal number of on-duty blocks
float: min_on_duty_blocks = int2float(nb_on_duty_days) / int2float(max_work);
% Maximal number of on-duty blocks
float: max_on_duty_blocks = int2float(nb_on_duty_days) / int2float(min_work);
% Average number of off-duty days per week
float: avg_on_duty_days_per_week = int2float(nb_on_duty_days) / int2float(nb_workers);
%------------------------------------------------------------------------------%
% Off-Duty Parameters
% Total number of off-duty days
int: nb_off_duty_days = schedule_length - nb_on_duty_days;
% Minimal number of off-duty blocks
float: min_off_duty_blocks = int2float(nb_off_duty_days) / int2float(max_daysoff);
% Maximal number of off-duty blocks
float: max_off_duty_blocks = int2float(nb_off_duty_days) / int2float(min_daysoff);
% Average number of off-duty days per week
float: avg_off_duty_days_per_week = int2float(nb_off_duty_days) / int2float(nb_workers);
%------------------------------------------------------------------------------%
% Block Parameters
% Minimal number of on- and off-duty blocks in the schedule
int: min_blocks = ceil( max(min_on_duty_blocks, min_off_duty_blocks) );
% Maximal number of on- and off-duty blocks in the schedule
int: max_blocks = floor( min(max_on_duty_blocks, max_off_duty_blocks) );
%------------------------------------------------------------------------------%
% Forbidden Shift Sequences
set of int: FORBIDDEN = 1..nb_forbidden; % Set of forbidden shift sequences
%------------------------------------------------------------------------------%
% Mapping from sorted shifts to shifts
array[SHIFT0] of SHIFT0: map_sorted_shift = arg_sort(shift_sort);
%------------------------------------------------------------------------------%
%------------------------------------------------------------------------------%
% Assertions
%
% This file contains assertion for checking the correctness of the input data.
%------------------------------------------------------------------------------%
constraint assert(not (OFF in SHIFT), "OFF (\(OFF)) must be different to elements in SHIFT (\(SHIFT))!\n");
constraint forall(s in SHIFT)(
assert(shift_block_max[s] <= max_work, "The maximal shift length \(shift_name[s]) must be less than or equal to the maximal on-duty block length!\n")
);
% Cross-checking on the minimal and maximal number of on and off duty blocks
% NOTE that it is not wrapped in an assertion, so that the instance can fail
% on the solver level and the print out "=====UNSATISFIABLE=====" is made.
constraint ceil(min_on_duty_blocks ) <= floor(max_on_duty_blocks );
constraint ceil(min_off_duty_blocks) <= floor(max_off_duty_blocks);
constraint ceil(min_on_duty_blocks ) <= floor(max_off_duty_blocks);
constraint ceil(min_off_duty_blocks) <= floor(max_on_duty_blocks );
%------------------------------------------------------------------------------%
% Shift Sorting criteria
% ID / No sorting
array[SHIFT0] of float: sort_none = [ s | s in SHIFT0 ];
% OFF then input order
array[SHIFT0] of float: sort_off_then_input_order = [
if s = OFF then 0.0 else int2float(s) endif
| s in SHIFT0 ];
% input order then OFF
array[SHIFT0] of float: sort_input_order_then_off = [
int2float(s)
| s in SHIFT0 ];
% OFF then reverse input order
array[SHIFT0] of float: sort_off_then_reverse_input_order = [
int2float(nb_shifts0 - s)
| s in SHIFT0 ];
% reverse input order then OFF
array[SHIFT0] of float: sort_reverse_input_order_then_off = [
if s = OFF then int2float(nb_shifts0) else int2float(nb_shifts0 - s) endif
| s in SHIFT0 ];
array[SHIFT0] of float: sort_min_slack1 = [
int2float(
if s = OFF then
floor(max_blocks) * max_daysoff - nb_off_duty_days
else
floor(min(max_blocks, max_shift_blocks[s])) * shift_block_max[s] - nb_shift_days_req[s]
endif
)
| s in SHIFT0 ];
% Peter's one (over approximation of block usages of other shifts)
array[SHIFT0] of float: sort_min_slack2 = [
int2float(
if s = OFF then
floor(max_blocks) * max_daysoff - nb_off_duty_days
else
min(
floor(min(max_blocks, max_shift_blocks[s])) * shift_block_max[s] - nb_shift_days_req[s],
max_work * (max_blocks - sum(s2 in SHIFT where s2 != s)(ceil(min_shift_blocks[s2]))) - nb_shift_days_req[s]
)
endif
)
| s in SHIFT0];
% Corrected Peter's one without over approximation
array[SHIFT0] of float: sort_min_slack3 = [
if s = OFF then
floor(max_blocks) * max_daysoff - nb_off_duty_days
else
min(
floor(min(max_blocks, max_shift_blocks[s])) * shift_block_max[s] - nb_shift_days_req[s],
max_work * max_blocks - sum(s2 in SHIFT where s2 != s)(nb_shift_days_req[s2]) - nb_shift_days_req[s]
)
endif
| s in SHIFT0];
% Andreas' one
array[SHIFT0] of float: sort_min_block_diff1 = [
if s = OFF then
max_blocks - min_on_duty_blocks
else
max_blocks - min_shift_blocks[s]
endif
| s in SHIFT0 ];
array[SHIFT0] of float: sort_min_block_diff2 = [
if s = OFF then
max_blocks - (max_blocks - min_off_duty_blocks) / 2
else
max_blocks - (min(max_blocks, min_shift_blocks[s]) - min_shift_blocks[s]) / 2
endif
| s in SHIFT0 ];
%------------------------------------------------------------------------------%
% Variables and linking constraints between those variables.
%------------------------------------------------------------------------------%
% Variables
% The schedule in that the shifts are represented by their identifiers
array[WEEK0,DAY] of var SHIFT0: plan =
if forall(sh in SHIFT0)(sh = map_sorted_shift[sh]) then
plan_sort
else
array2d(WEEK0, DAY, [ map_sorted_shift[plan_sort[w,d]] | w in WEEK0, d in DAY ])
endif;
% The schedule representing `plan` in an one-dimensional array
array[SCHEDULE0] of var SHIFT0: dplan = array1d(plan);
% The schedule in that the shifts are sorted with respect to a certain criteria
array[WEEK0,DAY] of var SHIFT0: plan_sort;
%------------------------------------------------------------------------------%
% Linking constraints between variables
% Linking the first worker/week to the additional one at the end
%
constraint forall(d in DAY)(
plan[1, d] = plan[nb_workers0, d]
);
%------------------------------------------------------------------------------%
% Constraints for Modelling the Temporary Requirements of Shifts for each day
% via GCC
%------------------------------------------------------------------------------%
% Inlcudes
include "global_cardinality_low_up.mzn";
%------------------------------------------------------------------------------%
% Constraints
% Requirements of shifts for over all days
%
constraint forall(d in DAY)(
global_cardinality_low_up(
[ plan[w,d] | w in WEEK ],
[ sh | sh in SHIFT ] ++ [ OFF ],
[ temp_req[sh,d] | sh in SHIFT ] ++ [ nb_workers - sum(sh in SHIFT)(temp_req[sh,d]) ],
[ temp_req[sh,d] | sh in SHIFT ] ++ [ nb_workers - sum(sh in SHIFT)(temp_req[sh,d]) ]
)
:: domain
);
%------------------------------------------------------------------------------%
% Constraints for Modelling the Constraints between Shifts using the Global
% Constraint 'regular'
%
% NOTE that this regular constraint assumes that the last day in the schedule
% is an off-day and minimal length of an off-day block is at most 2!
%------------------------------------------------------------------------------%
% Asumptions
constraint assert(
min_daysoff <= 2,
"Regular constraint assumes that the minimal length of days-off block is at most 2."
);
constraint assert(
sum(sh in SHIFT)(temp_req[sh, week_length]) < nb_workers,
"Regular constraint assumes that the last day in schedule is a day-off."
);
%------------------------------------------------------------------------------%
% Includes
include "regular.mzn";
%------------------------------------------------------------------------------%
% Computing the parameters for the allowed transitions
% Transitions with a sequence of length 3
array[SHIFT,SHIFT] of bool: transition3 = array2d(SHIFT,SHIFT, [
not exists(f in FORBIDDEN where forbidden_daysoff[f])(
forbidden_before[f] == i /\ forbidden_after[f] == j
)
| i,j in SHIFT ]
);
% Transitions with a sequence of length 2
array[SHIFT,SHIFT] of bool: transition = array2d(SHIFT,SHIFT, [
not exists(f in FORBIDDEN where not forbidden_daysoff[f])(
forbidden_before[f] == i /\ forbidden_after[f] == j
)
| i,j in SHIFT ]
);
%------------------------------------------------------------------------------%
% Computing the parameters for the regular constraint
int: START = 1; % Start (initial or first) state of the DFA
int: OFF1 = 2; % The state of the DFA when the first day in the block is an off-duty day
% after the initial start or an on-duty block ending with a specific shift
int: OFF2 = nb_shifts0 + 1; % The state of the DFA when the current block is an off-duty one
% and at least of length two
% The number of (feasible) states
int: nb_states = nb_shifts + max_daysoff + sum(sh in SHIFT)(max_work * shift_block_max[sh]);
% The state of the DFA when the first day in the block is an on-duty day for each shift
array[SHIFT] of int: first = [
1 + nb_shifts + max_daysoff
+ sum(s1 in SHIFT where s1 < sh)( max_work * shift_block_max[s1] )
| sh in SHIFT ];
% The state of the DFA after the first on-duty day in a block succeeding an
% on-duty block ending with shift sh
function STATE: off1(SHIFT: sh) = 1 + sh;
% The state of the DFA in an on-duty block, for which the current length
% is 'dayswork', the current shift 'shift' (at position 'dyaswork'), and
% the current shift lengths 'days'
function STATE: state(SHIFT: shift, int: days, int: dayswork) =
first[shift] + (max_work * (days - 1)) + (dayswork - 1);
% The set of (feasible) states and in the case also the set of accepting states
set of int: STATE = 1..nb_states;
% The set of all states including the failing state
set of int: STATE0 = 0..nb_states;
% The DFA
array[STATE, SHIFT0] of STATE0: dfa = array2d(STATE, SHIFT0,
[
% Transition from the first state
% previous state: none
% states: 1
if s = OFF then
% First day in the schedule is an off-duty day assume previous is off
OFF2
else
% First day in the schedule is an on-duty day
first[s]
endif
| s in SHIFT0
] ++ [
% Transitions after the first off-duty day in a block
% previous state: off-duty day (first in the block/sequence)
% states: 2 -- nb_shifts0
if s = OFF then
% Second off-duty day in an off-duty block
if 1 < max_daysoff then
OFF2
else
0
endif
elseif 1 >= min_daysoff /\ transition3[sh,s] then
% Switch from an off-duty block of length one to an on-duty block
first[s]
else
0
endif
| sh in SHIFT, s in SHIFT0
] ++ [
% Transitions after a sequence of at least two off-duty days
% previous state/day: off-duty day
% states: nb_shifts0 + 1 -- nb_shifts0 + max_daysoff - 1
if s = OFF then
% (d + 1)th off-duty day in an off-duty block
if d < max_daysoff then
nb_shifts0 + d
else
0
endif
elseif d >= min_daysoff then
% Switch from an of-duty block of length of at least two to an on-duty block
first[s]
else
0
endif
| d in 2..max_daysoff, s in SHIFT0
] ++ [
% Transitions after a sequence of on-duty days
% previous state/day: on-duty day
% states: nb_shifts0 + max_daysoff --
if s = OFF then
% Switch from an on-duty block to an off-duty block
if ds >= shift_block_min[sh] /\ dw >= min_work then
off1(sh)
else
0
endif
else
% Continuation of the on-duty block
if s = sh then
% Continuation with the same shift
if ds < shift_block_max[sh] /\ dw < max_work then
state(sh, ds + 1, dw + 1)
else
0
endif
else
% Switch to another on-duty shift
if ds >= shift_block_min[sh] /\ dw < max_work /\ transition[sh, s] then
state(s, 1, dw + 1)
else
0
endif
endif
endif
| sh in SHIFT, ds in 1..shift_block_max[sh], dw in 1..max_work, s in SHIFT0
]
);
%------------------------------------------------------------------------------%
% Constraining the day transitions
constraint regular(dplan, nb_states, nb_shifts0, dfa, START, STATE);
%------------------------------------------------------------------------------%
% Redundant constraints that is looking at the number of the remaining off-duty
% and on-duty blocks in the remaining schedule.
%------------------------------------------------------------------------------%
constraint redundant_constraint(
let {
% Count of the number of off-duty days from first day until the end of the week
array[WEEK] of var 0..nb_off_duty_days: count_off_duty;
% Count of the number of off-duty days after the end of the week
array[WEEK] of var 0..nb_off_duty_days: rem_off =
[ nb_off_duty_days - count_off_duty[w] | w in WEEK ];
% Count of the number of on-duty days after the end of the week
array[WEEK] of var 0..nb_on_duty_days: rem_on =
[ nb_on_duty_days - week_length * w + count_off_duty[w] | w in WEEK ];
% Lower bound of off-duty blocks
array[WEEK] of var int: lb_off_blocks = [
(rem_off[w] div max_daysoff) + ((rem_off[w] mod max_daysoff) > 0) - (plan[1,1] != OFF /\ plan[w+1, 1] = OFF)
| w in WEEK ];
% Upper bound of on-duty blocks
array[WEEK] of var int: ub_on_blocks = [
(rem_on[w] div min_work) + (plan[1,1] != OFF /\ plan[w, week_length] != OFF)
| w in WEEK ];
% Lower bound of on-duty blocks
array[WEEK] of var int: lb_on_blocks = [
(rem_on[w] div max_work) + ((rem_on[w] mod max_work) > 0) - (plan[1,1] = OFF /\ plan[w+1, 1] != OFF)
| w in WEEK ];
% Upper bound of off-duty blocks
array[WEEK] of var int: ub_off_blocks = [
(rem_off[w] div min_daysoff) + (plan[1,1] = OFF /\ plan[w, week_length] = OFF)
| w in WEEK ];
} in (
% First week is just the sum of the off-duty days in that week
count_off_duty[1] = sum(d in DAY)( plan[1,d] = OFF )
% The remaining weeks are the sum of the previous weeks and the current one
/\ forall(w in 2..nb_workers)(
count_off_duty[w] = count_off_duty[w - 1] + sum(d in DAY)( plan[w,d] = OFF )
)
% The sum of the last week must be equal to the number of off duty days
/\ count_off_duty[nb_workers] = nb_off_duty_days
% The lower bound on the remaining off-duty blocks must be not greater
% than the upper bound on the remaining on-duty blocks
/\ forall(w in WEEK)(lb_off_blocks[w] <= ub_on_blocks[w])
% The lower bound on the remaining on-duty blocks must be not greater
% than the upper bound on the remaining off-duty blocks
/\ forall(w in WEEK)(lb_on_blocks[w] <= ub_off_blocks[w])
)
);
%------------------------------------------------------------------------------%
% Symmetry breaking constraint which assigns an off-duty day at the end of
% the schedule and an on-duty day at the beginning if possible
%------------------------------------------------------------------------------%
constraint symmetry_breaking_constraint(
if sum(sh in SHIFT)(temp_req[sh, week_length]) < nb_workers then
% There must be at least one off-duty day at the end of the week
plan[nb_workers, week_length] = OFF
else
true
endif
/\ if
% Temporary requirements are the same over the week for each shift,
% thus a symmetric solution can be obtained by day shifting a solution
forall(sh in SHIFT, d in DAY where d < week_length)(
temp_req[sh, d] = temp_req[sh, d + 1]
)
% There are more on-duty shifts at the beginning of the week in comparison
% to the end of the week, thus there is at least one on-duty starts
% at the beginning of the week in any solution
\/ sum(sh in SHIFT)(temp_req[sh, week_length])
< sum(sh in SHIFT)(temp_req[sh, 1])
then
% Fixing that the first day is an on-duty day in the schedule
plan[1,1] != OFF
else
true
endif
);
%------------------------------------------------------------------------------%
% Search
ann: ff_search = int_search(array1d(plan_sort), first_fail, indomain_min, complete);
%------------------------------------------------------------------------------%
% Solve item
solve :: ff_search satisfy;
%------------------------------------------------------------------------------%
% Output item
output [
if d = 1 then
"% "
else
""
endif ++
if fix(plan[w,d]) in SHIFT then
shift_name[ fix(plan[w,d]) ]
else
"."
endif ++
if d = week_length then
"\n"
else
" "
endif
| w in WEEK, d in DAY
] ++ [
"plan_sort = array2d(\(WEEK0), \(DAY), \(plan_sort));\n"
];