Ограничения состава Minizinc

Моя задача - создать ростер со следующим типом смен:

• Утро (m): с 7:30 до 14:45 • Раннее утро (m1): с 6:45 до 14:00

• По вызову утром (im): с 06:30 до 14:45

• После полудня (t): с 14:45 до 22:00

• По вызову после обеда (ит): с 13:45 до 22:00

• Ночь (ночное время): с 22:00 до 7:30 следующего дня.

• Ночь по вызову (ino): с 21:00 до 7:30 следующего дня.

• Офис: с 10 до 14

• День отдыха (l): считается днем ​​отдыха на следующий день после ночи, даже если рабочий закончил в 7 утра того дня.

Применяемые правила: цикл состоит из 6 или менее указанных выше смен (кроме офиса и отдыха), прежде чем потребуется 48 или 54 часа отдыха (см. Ниже).

  1. Если цикл состоит из 5 рабочих дней (m, m1, im, t, it, n, ino), оставшаяся часть должна быть 48 часов или более.
  2. Если цикл состоит из 6 рабочих дней (m, m1, im, t, it, n, ino), оставшаяся часть должна быть 54 часа или более.
  3. Между окончанием смены и началом следующей смены (включая офисную) должно пройти не менее 12 часов. Таким образом, невозможно иметь n, t, например
  4. Ночи: после одной ночи (n) или ночи по вызову (ino) должен быть 48 часов отдыха, если нет другой ночи (nn) или отдыха и ночи (nln), когда вам нужно 54 часа отдыха. Так что допустимыми примерами являются nllm (предоставляется 48-часовой отдых) или nnllt или nlnllt (предоставляется 54-часовой отдых).
  5. Допускается не более 5 утренних / ранних утренних / утренних звонков в цикле.
  6. Офис не считается рабочим днем, но он не считается отдыхом, поэтому он должен соблюдать 12-часовой отдых, как и другие смены, но он не считается рабочим днем ​​в течение 5 или 6 дней за цикл.
  7. Максимум 2 смены по вызову за цикл.

Состав должен соответствовать определенной конфигурации, определяемой количеством рабочих на каждую смену (m, m1, t, n) каждый день. Дежурство по вызову не является обязательным. Никаких проблем по этой части.

Пока правила с 3 по 7 выполнены. Проблема, которая у меня есть, касается 1 и 2, поскольку я не могу сделать это с обычным ограничением (это становится слишком сложным). Я пробовал подход создания и набора часов отдыха между последовательными сменами, что я и сделал (третья строка изображения). Пример:

Желаемое поведение

Проблема в четвертой строке: просуммируйте последовательный отдых (от конца смены + дни отдыха + до начала следующей смены), т.е. суммируйте нули вместе с оставшимся рабочим днем ​​(1). Затем подсчитайте рабочие дни до> = 48 часов, чтобы проверить, осталось ли их 5 или меньше; посчитайте рабочие дни до> = 54 часов, чтобы проверить, осталось ли 6 или меньше ... просто идея. Спасибо за вашу помощь!

Это код до сих пор (я включил в код действительный RosterCalculated, который можно было бы изменить для проверки кода, изменив его вручную вместо var RosterCalculated. В этом случае ограничение для проверки конфигурации также должно быть снято). Я считаю, что это лучше проверить, действительно ли ограничения работают ...

include "globals.mzn";


%Definitions
enum TypeOfShift = {l,m1,m,t,n,im,it,ino,o};  %Types of shifts
array[TypeOfShift] of float: StartTypeOfShift=[10, 6.75, 7.5, 14.75, 22, 6.5, 13.75, 21, 10]; %Starting hour. The time for l is just to put something convenient
array[TypeOfShift] of float: DurationTypeOfShift=[0, 7.25, 7.25, 7.25, 9.5, 8.25, 8.25, 10.5, 6]; %Duration of shifts (hours)
enum Staff={AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM};
array[int] of int: DaysInRoster=[28, 29, 30, 31, 1, 2, 3, 4, 5,6,7,8,9,10]; %Dias a los que corresponde el turnero


int: NumberWorkers = card(Staff); 
int: NumDaysInRoster=length(DaysInRoster);
array[1..NumDaysInRoster,TypeOfShift] of int: Configuration = array2d(1..NumDaysInRoster,TypeOfShift,[ (if (tu==m1) then 1 else
                                                                                                   if (tu==m) then 2 else
                                                                                                   if (tu==t) then 2 else
                                                                                                   if (tu==o) then 0 else
                                                                                                   if (tu==im) then 1 else
                                                                                                   if (tu==n) then 2 else 0
                                                                                                   endif endif endif endif endif endif)|  d in 1..NumDaysInRoster, tu in TypeOfShift ]); %Easy example of configuration

array[Staff, 1..NumDaysInRoster] of TypeOfShift: RosterCalculated = [|t, n, n, l, l, l, l, m, m1, m, t, l, m1, l|
m, l, l, n, l, l, t, t, n, l, l, l, m, l|
n, l, l, m, m1, m, m, n, l, l, m, m, l, m1|
l, t, l, n, l, l, m, m, t, n, l, l, t, l|
t, l, t, l, l, m, t, l, m, t, n, n, l, l|
m, m, m, l, l, m1, m1, n, l, l, m, l, l, t|
n, l, l, l, t, n, n, l, l, l, m1, t, n, n|
l, m, n, l, n, l, l, l, t, n, l, l, t, l|
l, t, l, m, m, l, l, l, m, m, l, m1, m, m|
l, l, m, m1, t, t, l, m1, n, l, l, n, l, m|
l, l, m1, t, l, l, l, t, l, t, t, t, n, l|
m1, m1, t, t, n, n, l, l, l, m1, l, m, l, n|
l, n, l, l, m, t, n, l, l, l, n, l, l, t|];



% Variables
%array[Staff, 1..NumDaysInRoster] of var TypeOfShift: RosterCalculated;  % To create the roster. Remove this line if what we want is to check if the code is working
var int: NumberWorkersNeeded =  sum (i in Staff) ((sum(d in 1..NumDaysInRoster) (RosterCalculated[i,d] != l)));                                       
array[Staff, 1..NumDaysInRoster-1] of var float: RosterCalculatedRests = array2d(Staff, 1..NumDaysInRoster-1,[(24*(d)+StartTypeOfShift[RosterCalculated[i,d+1]]) - (24*(d-1)+StartTypeOfShift[RosterCalculated[i,d]] + DurationTypeOfShift[RosterCalculated[i,d]]) | i in Staff, d in 1..NumDaysInRoster-1]);


% Satisfy configuration. Remove this constraint if what we want is to check if the code is working
/*
constraint forall(d in 1..NumDaysInRoster) 
              (((sum(i in Staff) (RosterCalculated[i,d] == m)) == Configuration[d,m]) /\ ((sum(i in Staff) (RosterCalculated[i,d] == m1)) == Configuration[d,m1]) /\ 
              ((sum(i in Staff) (RosterCalculated[i,d] == t)) == Configuration[d,t]) /\  ((sum(i in Staff) (RosterCalculated[i,d] == n)) == Configuration[d,n]));
*/

% Satisfy configuration on call not necessary to comply
constraint forall(d in 1..NumDaysInRoster) 
              (((sum(i in Staff) (RosterCalculated[i,d] == im)) <= Configuration[d,im]) /\ ((sum(i in Staff) (RosterCalculated[i,d] == it)) <= Configuration[d,it]) /\ 
              ((sum(i in Staff) (RosterCalculated[i,d] == ino)) <= Configuration[d,ino]) /\ ((sum(i in Staff) (RosterCalculated[i,d] == o)) <= Configuration[d,o]));            

% El tiempo transcurrido entre la salida de un turno y la entrada al siguiente tiene que ser igual o superior a 12h. NO NECESARIOS CON MATRIZ V4 (MAS LENTO)
constraint forall(i in Staff, d in 1..NumDaysInRoster-1)
              ((RosterCalculated[i,d+1] != l ) -> (24*(d-1)+StartTypeOfShift[RosterCalculated[i,d]] + DurationTypeOfShift[RosterCalculated[i,d]] + 12 <= 24*d+StartTypeOfShift[RosterCalculated[i,d+1]]));


% Rest after night or on call night (could be changed by regular constraint) 48h or more 
constraint forall(i in Staff, d in 1..NumDaysInRoster-3)
              (((RosterCalculated[i,d] == n) \/ (RosterCalculated[i,d] == ino)) -> ((RosterCalculated[i,d+1]==l \/ RosterCalculated[i,d+1]==n \/ RosterCalculated[i,d+1]==ino) /\
              (RosterCalculated[i,d+2]==l \/ RosterCalculated[i,d+2]==n \/ RosterCalculated[i,d+2]==ino) /\
              (StartTypeOfShift[RosterCalculated[i,d+3]] >= 7.5 \/ RosterCalculated[i,d+3]==l)));  


% Rest after double night has to be 54h or more (could be changed by regular constraint)
constraint forall(i in Staff, d in 1..NumDaysInRoster-4)
              ((((RosterCalculated[i,d] == n) \/ (RosterCalculated[i,d] == ino)) /\ ((RosterCalculated[i,d+1] == n) \/ (RosterCalculated[i,d+1] == ino))) -> ((RosterCalculated[i,d+2]==l) /\
              (RosterCalculated[i,d+3]==l) /\
              (StartTypeOfShift[RosterCalculated[i,d+4]] >= 13.5 \/ RosterCalculated[i,d+4]==l)));  

% Rest after a night free night has to be 54h or more (could be changed by regular constraint)
constraint forall(i in Staff, d in 1..NumDaysInRoster-5)
              ((((RosterCalculated[i,d] == n) \/ (RosterCalculated[i,d] == ino)) /\ (RosterCalculated[i,d+1] == l) /\ ((RosterCalculated[i,d+2] == n) \/ (RosterCalculated[i,d+2] == ino))) -> ((RosterCalculated[i,d+3]==l) /\
              (RosterCalculated[i,d+4]==l) /\
              (StartTypeOfShift[RosterCalculated[i,d+5]] >= 13.5 \/ RosterCalculated[i,d+5]==l)));


% Transition matrix not coping with all the cases...
predicate Max6WorkingDays(array[int] of var TypeOfShift: shift) =
    let {
        array[1..17, 1..5] of 0..17: transition_relation = % Transition matrix not coping with all the cases...
           [|8,   1,    2,  2,  2
            |9,   2,    3,  3,  3
            |10,    3,  4,  4,  4
            |11,    4,  5,  5,  5
            |12,    5,  6,  6,  6
            |13,    6,  7,  7,  15
            |14,    7,  0,  0,  0
            |1,   1,    2,  2,  2
            |1,   2,    3,  3,  3
            |1,   3,    4,  4,  4
            |1,   4,    5,  5,  5
            |1,   5,    6,  6,  6
            |1,   6,    7,  7,  15
            |1,   7,    0,  0,  0
            |16,    0,  0,  0,  0
            |17,    0,  0,  0,  0
            |1,   0,    0,  2,  2
          |];
    } in
        regular(
            [ if (s == l) then 1 else
              if s ==  o then 2 else
              if ((s == m) \/ (s == m1) \/(s == im)) then 3 else
              if ((s == t) \/ (s == it)) then 4 else
                              5 endif
                                endif
                                endif
                                endif
              | s in shift],                % sequence of input values
            17,                             % number of states
            5,                              % number of different input values of state machine
            transition_relation,            % transition relation
            1,                              % initial state
            1..17,                          % final states
         );                                                                                                                                                                                                                                          
constraint forall(i in Staff)
            (Max6WorkingDays([RosterCalculated[i,j] | j in 1..NumDaysInRoster]));                                                              


% Two on calls per cycle as max
predicate Max2OnCall(array[int] of var TypeOfShift: shift) =
    let {
        array[1..5, 1..4] of 0..5: transition_relation =
            [| 1, 2, 1, 1 % im0 (start)
             | 2, 4, 2, 3 % im1_l0
             | 2, 4, 2, 1 % im1_l1
             | 4, 0, 4, 5 % im2_l0
             | 4, 0, 4, 1 % im2_l1
            |];
    } in
        regular(
            [ if ((s == m1) \/ (s == m) \/ (s == t) \/ (s == n)) then 1 else
              if ((s == im) \/ (s == it) \/ (s == ino)) then 2 else
              if s ==  o then 3 else
                              4 endif
                                endif
                                endif
              | s in shift],                % sequence of input values
            5,                              % number of states
            4,                              % number of different input values of state machine
            transition_relation,            % transition relation
            1,                              % initial state
            1..5,                           % final states
         );

constraint forall(i in Staff)
            (Max2OnCall([RosterCalculated[i,j] | j in 1..NumDaysInRoster]));

% Max of 5 mornings per cycle
predicate MaxMsPerCycle(array[int] of var TypeOfShift: shift) =
    let {
        array[1..13, 1..4] of 0..13: transition_relation =
            [| 
              2,    7,  1,  1|
              3,    7,  8,  2|
              4,    7,  9,  3|
              5,    7,  10, 4|
              6,    7,  11, 5|
              0,    7,  12, 6|
              7,    7,  13, 7|
              3,    7,  1,  2|
              4,    7,  1,  3|
              5,    7,  1,  4|
              6,    7,  1,  5|
              0,    7,  1,  6|
              7,    7,  1,  7
            |];
    } in
        regular(
            [ if ((s == m1) \/ (s == m) \/ (s == im)) then 1 else
              if ((s == t) \/ (s == it) \/ (s == n) \/ (s == ino)) then 2 else
              if ((s ==  l)) then 3 else
                              4 endif
                                endif
                                endif
              | s in shift],                % sequence of input values
            13,                              % number of states
            4,                              % number of different input values of state machine
            transition_relation,            % transition relation
            1,                              % initial state
            1..13,                           % final states
         );

constraint forall(i in Staff)
            (MaxMsPerCycle([RosterCalculated[i,j] | j in 1..NumDaysInRoster]));



solve minimize NumberWorkersNeeded;
output[";;"]++["\(DaysInRoster[d]);" | d in 1..NumDaysInRoster];
output[if (d==1) then "\n"++"O3;\(i) " ++ ";" ++ show(RosterCalculated[i,d]) ++ ";" else show(RosterCalculated[i,d]) ++ ";" endif | i in Staff, d in 1..NumDaysInRoster];
output[if (d==1) then "\n"++"O3;\(i) " ++ ";" ++ show(RosterCalculatedRests[i,d]) ++ ";" else show(RosterCalculatedRests[i,d]) ++ ";" endif | i in Staff, d in 1..NumDaysInRoster-1];

person trxw    schedule 08.11.2019    source источник
comment
StackOverflow - это не место, где можно бросить вызов другим пользователям, а место, где можно задать вопросы, которые вы не можете понять. Не могли бы вы перефразировать свою задачу в минимальный пример и задать конкретный вопрос?   -  person Dekker1    schedule 09.11.2019
comment
@ Dekker1 Вообще-то это моя вина. OP задал очень конкретные вопросы здесь и здесь. Я подумал, что эти вопросы были слишком конкретными и что, не видя общей картины, было трудно дать предложения, которые могли бы лучше масштабироваться для решения данной проблемы. Поэтому я любезно попросил его указать исходную проблему в отдельном вопросе. Учитывая ваш опыт, могли бы вы лучше понять, как с этим бороться? :)   -  person Patrick Trentin    schedule 09.11.2019
comment
Привет, изменяю вопрос и код. Это дает результаты, хотя и с теми же проблемами, что уже объяснялись. Я также включил действующий состав, чтобы можно было легко проверить, правильно ли работают ограничения ... Большое спасибо за ваше время, знания и хорошую работу!   -  person trxw    schedule 09.11.2019
comment
Привет, как предложил @ Dekker1, я создал еще один вопрос новое сообщение с более точным вопросом, пытаясь избежать через большой код ... Возможно, мне не хватает других способов решить большую картину, но так оно и есть   -  person trxw    schedule 09.11.2019
comment
Я бы не стал использовать float и вместо этого использовал int (т. Е. Изменить единицу измерения). AFAIK, есть MiniZinc решатели, которые плохо работают (или совсем не работают) с типом float, но очень хороши с int. Расстояние 12 часов не должно требовать никаких вычислений, я думаю, что можно предварительно вычислить действительных преемников. Для многих ограничений вариант on call можно абстрагировать (или наоборот), потому что он на самом деле ничего не меняет.   -  person Patrick Trentin    schedule 10.11.2019
comment
Спасибо, Патрик. Я учту поплавок, как только у меня будет запущена модель. Ограничение расстояния в 12 часов на самом деле работает довольно хорошо, я считаю, лучше, чем регулярное выражение (с точки зрения времени вычислений). Что касается дежурных смен, вы правы, что в большинстве случаев дежурные смены не накладывают больше ограничений по сравнению со стандартными сменами, но есть несколько важных отличий. Например, n, l, l, m возможно, но n, l, l, im - нет. Моя проблема в том, что я могу считать 5 или 6 дней на цикл ... Вот где я застрял   -  person trxw    schedule 10.11.2019
comment
Похоже, проблему можно разделить на две части: 1. найти все возможные и допустимые циклы и 2. запланировать различные циклы для каждого сотрудника так, чтобы они покрывали 30 +7 дней, а затем выберите значение от 1 до 7, чтобы выбрать день начала 30-дневного месяца.   -  person Patrick Trentin    schedule 11.11.2019
comment
Правила в вашем сообщении гласят: Максимум 5 утра / раннего утра / утра по вызову допускается в цикле, но в предыдущих вопросах и в вашем примере кода вы разрешаете до 6 утра. Который правильный?   -  person Patrick Trentin    schedule 11.11.2019
comment
Мое последнее объяснение было полным. Максимум 5 утра. Максимум 6 рабочих дней (то есть как минимум неутренний)   -  person trxw    schedule 12.11.2019
comment
По поводу другого комментария. Это больше вопрос возможности подтвердить, что сгенерированный список удовлетворяет всем ограничениям. Единственная часть, которой не хватает в головоломке, - это остаток между циклами (максимум 5 или 6 дней). Вот почему я создал другой пост с минимальными затратами, потому что понимал, что это возможный выход.   -  person trxw    schedule 12.11.2019
comment
Вы пробовали с явным представлением циклов? например array [Staff, 1..15] of var 1..30: cycle_start; array[Staff, 1..15] of var 0..30: duration;. Вы можете бесплатно получить индексы массива, необходимые для вычисления delta_time между концом одного цикла и началом следующего цикла. Сегодня я начал пытаться автоматически сгенерировать отношение перехода для всех ограничений ожидания XX часов. Это кажется выполнимым, но я не уверен, что решатели с этим справятся.   -  person Patrick Trentin    schedule 12.11.2019
comment
даже если вы определите эти переменные cycle_start и duration, вам нужно будет вычислить остаток, поскольку запуск / завершение циклов с тем или иным типом смены - не одно и то же. Что касается вычисляемого вами delta_time, не уверен, понял ли я, чего вы пытаетесь достичь. Delta_time между двумя последовательными днями уже применяется в этом ограничении ((RosterCalculated [i, d + 1]! = L) - ›(24 * (d-1) + StartTypeOfShift [RosterCalculated [i, d]]] + DurationTypeOfShift [RosterCalculated [i, d]] + 12 ‹= 24 * d + StartTypeOfShift [RosterCalculated [i, d + 1]]));   -  person trxw    schedule 12.11.2019
comment
скажем cycle_start[0] = 0, duration[0] = 3, тогда мы знаем, что shift[cycle_start[0] + duration[0] - 1] - это последний сдвиг в цикле, и мы можем наложить ограничение, сказав, что start_time из shift[cycle_start[1]] должен быть не менее 48h после. Более того, мы можем сказать, что для каждого i между cycle_start[0] + duration[0] и cycle_start[1] мы должны иметь это shift[i] = l. Кроме того, у нас есть способ подсчитать, сколько m/m1/im/t/it/n/ino находится между cycle_start[0] и cycle_start[0] + duration[0], что решит проблему размера скользящего окна.   -  person Patrick Trentin    schedule 13.11.2019
comment
Размер cycle_start и duration равен 1..15, потому что в одном месяце может быть не более 15 циклов (фактически, меньше). Можно ожидать, что конечные значения в этих двух массивах не имеют смысла (например, вне интервала 1..30), поэтому нужно обратить внимание на то, как кодируются ограничения, чтобы избежать фиктивных результатов unsat.   -  person Patrick Trentin    schedule 13.11.2019
comment
Спасибо. Я работаю над вашим последним предложением.   -  person trxw    schedule 13.11.2019
comment
Любая разумная идея о том, как я могу обеспечить, чтобы каждая смена в RosterCalculated принадлежала циклу? Без этого ограничения я всегда получаю cycle_start = 0 для всех из них (что означает, что цикл не используется)   -  person trxw    schedule 13.11.2019
comment
Задание s.t. для каждого i cycle_start[i] = 0 не должно быть действительным из-за требования наличия не менее 48h между одним и следующим циклом. Задание s.t. каждый цикл пуст, также следует сделать недействительным в соответствии с требованием, чтобы смены всех сотрудников, вместе взятых, обеспечивали достаточную оплату труда в течение дня (я полагаю, вы ожидаете, что хотя бы один человек на работе в любой раз, может и больше).   -  person Patrick Trentin    schedule 13.11.2019
comment
Я не слежу за вами ... если мы определяем как максимум 15 циклов, будут несколько пустых циклов. Я определил массив [Staff, 1..MaxNumCycles] переменной 0..NumDaysInRoster: cycle_start; массив [Staff, 1..MaxNumCycles] of var 0..14: duration; Нули относятся к неиспользованным циклам. Я добавил ограничение, которое сортирует возрастание cycle_start, а затем ограничение 48 часов. Проблема в том, что мне нужно ограничение, которое говорит, что каждая смена в RosterCalculated! = L должна принадлежать одному и только одному циклу.   -  person trxw    schedule 13.11.2019
comment
Давайте продолжим это обсуждение в чате.   -  person trxw    schedule 13.11.2019