$title A Nurse Scheduling Problem (NURSES,SEQ=428) $onText This nurse scheduling problem allocates nurses to shifts. The objective contains cost and fairness parts. Moreover, the model demonstrates the use of a user specified decomposition for the ODHCplex solver as well as the limited variable domain feature. The model has been adapted from an example nurses.py provided by IBM (https://ibmdecisionoptimization.github.io/docplex-doc/mp/nurses.html) Keywords: scheduling, decomposition, limited variable domain $offText set nh 'NurseData header' / Seniority, Qualification, 'Pay rate' / sh 'ShiftData header' / 'Start time', 'End time', 'Minimum requirement', 'Maximum requirement' / nurse 'Nurses' shift 'Shifts' department 'Departments' skill 'Nurse skills' day 'Days of the week' / monday, tuesday, wednesday, thursday, friday, saturday, sunday / ; Table nurseData(nurse<,nh) 'Nurse Data' $onDelim Name, Seniority, Qualification, 'Pay rate' Anne, 11, 1, 25 Bethanie, 4, 5, 28 Betsy, 2, 2, 17 Cathy, 2, 2, 17 Cecilia, 9, 5, 38 Chris, 11, 4, 38 Cindy, 5, 2, 21 David, 1, 2, 15 Debbie, 7, 2, 24 Dee, 3, 3, 21 Edith, 8, 2, 25 Elaina, 8, 4, 31 Elaine, 8, 3, 28 Eleanor, 8, 3, 28 Elena, 8, 3, 28 Eliana, 8, 2, 27 Elianna, 8, 4, 31 Elisa, 8, 4, 32 Elise, 8, 4, 30 Elizabeth, 8, 5, 34 Ella, 8, 4, 31 Elle, 8, 4, 30 Elliana, 8, 5, 32 Ellie, 8, 4, 30 Eloise, 8, 2, 25 Elsie, 8, 5, 33 Ethel, 8, 3, 26 Ember, 8, 5, 30 Emelia, 8, 5, 30 Gloria, 8, 2, 25 Isabelle, 3, 1, 16 Jane, 3, 4, 23 Janelle, 4, 3, 22 Janice, 2, 2, 17 Jemma, 2, 4, 22 Joan, 5, 3, 24 Joanna, 5, 2, 18 Joyce, 8, 3, 29 Jude, 4, 3, 22 Julie, 6, 2, 22 Juliet, 7, 4, 31 Kate, 5, 3, 24 Mary, 9, 5, 36 Nancy, 8, 4, 32 Nathalie, 9, 5, 38 Nicole, 0, 2, 14 Patricia, 1, 1, 13 Pippa, 1, 1, 25 Patrick, 6, 1, 19 Roberta, 3, 5, 26 Suzanne, 5, 1, 18 Vickie, 7, 1, 20 Wendie, 5, 2, 21 Zoe, 8, 3, 29 $offDelim ; Table shiftData(shift<,department<,day,sh) 'Nurse Data' $onDelim Shift, Department, Day, 'Start time', 'End time', 'Minimum requirement', 'Maximum requirement' s1, Emergency, monday, 2, 8, 3, 5 s2, Emergency, monday, 8, 12, 4, 7 s3, Emergency, monday, 12, 18, 2, 5 s4, Emergency, monday, 18, 2, 3, 7 s5, Consultation, monday, 8, 12, 10, 13 s6, Consultation, monday, 12, 18, 8, 12 s7, Cardiac_Care, monday, 8, 12, 10, 13 s8, Cardiac_Care, monday, 12, 18, 8, 12 s9, Geriatrics, monday, 8, 12, 8, 10 s10, Geriatrics, monday, 12, 18, 8, 15 s11, Emergency, tuesday, 8, 12, 4, 7 s12, Emergency, tuesday, 12, 18, 2, 5 s13, Emergency, tuesday, 18, 2, 3, 7 s14, Consultation, tuesday, 8, 12, 10, 13 s15, Consultation, tuesday, 12, 18, 8, 12 s16, Cardiac_Care, tuesday, 8, 12, 4, 7 s17, Cardiac_Care, tuesday, 12, 18, 2, 5 s18, Cardiac_Care, tuesday, 18, 2, 3, 7 s19, Geriatrics, tuesday, 8, 12, 8, 10 s20, Geriatrics, tuesday, 12, 18, 8, 15 s21, Emergency, wednesday, 2, 8, 3, 5 s22, Emergency, wednesday, 8, 12, 4, 7 s23, Emergency, wednesday, 12, 18, 2, 5 s24, Emergency, wednesday, 18, 2, 3, 7 s25, Consultation, wednesday, 8, 12, 10, 13 s26, Consultation, wednesday, 12, 18, 8, 12 s27, Geriatrics, wednesday, 8, 12, 8, 10 s28, Geriatrics, wednesday, 12, 18, 8, 15 s29, Emergency, thursday, 2, 8, 3, 5 s30, Emergency, thursday, 8, 12, 4, 7 s31, Emergency, thursday, 12, 18, 2, 5 s32, Emergency, thursday, 18, 2, 3, 7 s33, Consultation, thursday, 8, 12, 10, 13 s34, Consultation, thursday, 12, 18, 8, 12 s35, Geriatrics, thursday, 8, 12, 8, 10 s36, Geriatrics, thursday, 12, 18, 8, 15 s37, Emergency, friday, 2, 8, 3, 5 s38, Emergency, friday, 8, 12, 4, 7 s39, Emergency, friday, 12, 18, 2, 5 s40, Emergency, friday, 18, 2, 3, 7 s41, Consultation, friday, 8, 12, 10, 13 s42, Consultation, friday, 12, 18, 8, 12 s43, Geriatrics, friday, 8, 12, 8, 10 s44, Geriatrics, friday, 12, 18, 8, 15 s45, Emergency, saturday, 2, 12, 5, 7 s46, Emergency, saturday, 12, 20, 7, 9 s47, Emergency, saturday, 20, 2, 12, 12 s48, Geriatrics, saturday, 8, 12, 8, 10 s49, Geriatrics, saturday, 12, 18, 8, 15 s50, Emergency, sunday, 2, 12, 5, 7 s51, Emergency, sunday, 12, 20, 7, 9 s52, Emergency, sunday, 20, 2, 12, 12 s53, Geriatrics, sunday, 8, 12, 8, 10 s54, Geriatrics, sunday, 12, 18, 8, 15 $offDelim ; Set nurseSkills(nurse,skill<) 'Nurse has particular skill' / Anne .(Anaesthesiology, Oncology, Pediatrics) Betsy .(Cardiac_Care) Cathy .(Anaesthesiology) Cecilia .(Anaesthesiology, Oncology, Pediatrics) Chris .(Cardiac_Care, Oncology, Geriatrics) Gloria .(Pediatrics) Jemma .(Cardiac_Care) Joyce .(Anaesthesiology, Pediatrics) Julie .(Geriatrics) Juliet .(Pediatrics) Kate .(Pediatrics) Nancy .(Cardiac_Care) Nathalie .(Anaesthesiology, Geriatrics) Patrick .(Oncology) Suzanne .(Pediatrics) Wendie .(Geriatrics) Zoe .(Cardiac_Care) /; Parameter SkillRequirements(department, skill) / Emergency.Cardiac_Care 1 /; Set vacation(nurse,day) / Anne .(friday, sunday) Cathy .(thursday,tuesday) Joan .(thursday,saturday) Juliet .(monday,thursday) Nathalie .(sunday,thursday) Isabelle .(monday,thursday) Patricia .(saturday,wednesday) Nicole .(friday,wednesday) Jude .(tuesday,friday) Debbie .(saturday,wednesday) Joyce .(sunday,thursday) Chris .(thursday,tuesday) Cecilia .(friday,wednesday) Patrick .(saturday,sunday) Cindy .(sunday) Dee .(tuesday,friday) Jemma .(friday,wednesday) Bethanie .(wednesday,tuesday) Betsy .(monday,thursday) David .(monday) Gloria .(monday) Jane .(saturday,sunday) Janelle .(wednesday,friday) Julie .(sunday) Kate .(tuesday,monday) Nancy .(sunday) Roberta .(friday,saturday) Janice .(tuesday,friday) Suzanne .(monday) Vickie .(wednesday,friday) Wendie .(thursday,saturday) Zoe .(saturday,sunday) /; Set nurseAssoc(nurse,nurse) / Isabelle.Dee, Anne.Patrick /; Set nurseIncompat(nurse,nurse) 'cannot work together' / Patricia.Patrick Janice.Wendie Suzanne.Betsy Janelle.Jane Gloria.David Dee.Jemma Bethanie.Dee Roberta.Zoe Nicole.Patricia Vickie.Dee Joan.Anne /; Scalar maxWorkTime / 40 / fairnessWeight / 100 / assignmentWeight / 10 /; Set s(shift,department,day) 'Shift Department Day'; option s= startTime(s) and startTime(t) < endTime(s)).. nurseAssignments[n,s] + nurseAssignments[n,t] =l= 1; * Nurse-Nurse incompatibility defNurseIncompat(nurseIncompat(n,nurse),s).. nurseAssignments[n,s] + nurseAssignments[nurse,s] =l= 1; * Nurse association defNurseAssoc(nurseAssoc(n,nurse),s).. nurseAssignments[n,s] =e= nurseAssignments[nurse,s]; * Skill requirements defSkillReq(d,skill,s(shift,d,day))$skillRequirements(d, skill).. sum(nurseSkills(n,skill), nurseAssignments[n,s]) =g= skillRequirements(d, skill); * compute average hours defAvgHours.. card(nurse)*nurseAvgHours =e= sum(n, nurseWorkTime(n)); * fairness: want each nurse's allocated hours to be similar (there is an objetive penalty if not) defOverUnderTime(n).. nurseWorkTime[n] =e= nurseAvgHours + nurseMoreThanAvgHours[n] - nurseLessThanAvgHours[n]; defFairness.. fairness =e= sum(n, NurseMoreThanAvgHours[n] + NurseLessThanAvgHours[n]); * total assignments defTotalAssign.. totalAssignments =e= sum((n,s), nurseAssignments[n,s]); $if not set onduty $set ONDUTY 0 $ifThen %ONDUTY%==0 * Nurse vacations nurseAssignments.fx[n,s(shift,d,day)]$vacation(n,day) = 0; model nurseScheduling / all /; $else $onText Rather than fixing assignment variables to 0 for vacation days one could exclude the assignment variables from the model via a dynamic set that only containts the available shifts (onDuty) for a nurse. Instead of using this set everywhere in the constraint definition one can also convenienyly use the limited variable domain feature (https://www.gams.com/latest/docs/UG_ModelSolve.html#UG_ModelSolve_LimitedDomain). $offText set onDuty(nurse,shift,department,day); onDuty(n,s) = yes; onDuty(n,s(shift,d,day))$vacation(n,day) = no; model nurseScheduling / all, nurseAssignments(onDuty) /; $endIf $ifThenI "%gams.mip%"=="odhcplex" * Try some custom decomposition scheme for ODHCplex file fopt /odhcplex.opt/; put fopt 'decomposition 2'; loop(n, put / 'nurseAssignments.key("' n.tl:0 '",*,*,*) ' ord(n):0:0); *loop(shift, put / 'nurseAssignments.key(*,"' shift.tl:0 '",*,*) ' ord(shift):0:0); *loop(d, put / 'nurseAssignments.key(*,*,"' d.tl:0 '",*) ' ord(d):0:0); *loop(day, put / 'nurseAssignments.key(*,*,*,"' day.tl:0 '") ' ord(day):0:0); putclose fopt; nurseScheduling.optFile = 1; $endIf * This makes the model harder *shiftData(s,'Minimum requirement') = max(round(shiftData(s,'Minimum requirement')*.25),1); nurseScheduling.resLim = 100; solve nurseScheduling min obj us mip; abort.noError$(nurseScheduling.modelStat<>%modelStat.optimal% and nurseScheduling.modelStat<>%modelStat.integerSolution%) 'no solution'; Parameter kpi; $onDotL kpi("Total salary cost") = sum(d, CostByDepartments[d]); kpi("Total number of assignments") = TotalAssignments; kpi("Average work time") = NurseAvgHours; kpi("Total over-average worktime") = sum(n, NurseMoreThanAvgHours[n]); kpi("Total under-average worktime") = sum(n, NurseLessThanAvgHours[n]); kpi("Total fairness") = fairness; display kpi; file frep / report.lst /; put frep 'Allocation By Department:'; loop(d, put / ' ' d.tl:15 ':' (sum((n,s(shift,d,day)), NurseAssignments[n,s])):4:0); put / 'Cost By Department:'; loop(d, put / ' ' d.tl:15 ':' CostByDepartments.l(d):7:0); put / 'Nurses Assignments:'; loop(n, put / ' ' n.tl:10 ': total hours:' nurseWorkTime.l(n):3:0; loop(s(shift,d,day)$(nurseAssignments[n,s]>0.5), put / ' ' day.tl:10 ':' d.tl:15 shiftData(s,'Start time'):2:0 '-' shiftData(s,'End time'):2:0; ); );