night shift balancing now works!

This commit is contained in:
Ross
2020-05-16 19:01:35 +01:00
parent 12f7b5270c
commit 63dcc7ce78
4 changed files with 136 additions and 185 deletions
+1 -1
View File
@@ -1 +1 @@
,ross,maverick.fritz.box,14.05.2020 23:29,file:///home/ross/.config/libreoffice/4;
,ross,maverick.fritz.box,16.05.2020 17:19,file:///home/ross/.config/libreoffice/4;
+95 -35
View File
@@ -241,19 +241,25 @@ class RotaBuilder(object):
initialize=0,
)
self.model.worker_weekend_assigned = Var(
((worker.id, week) for worker in self.workers
for week in self.weeks),
domain=NonNegativeReals,
initialize=0,
)
self.model.night_shift_count_w = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeReals,
initialize=0,
)
self.model.works_weekend_count = Var(
((worker.id, week) for worker in self.workers
for week in self.weeks),
domain=NonNegativeIntegers,
initialize=0,
)
# self.model.worker_weekend_assigned = Var(
# ((worker.id, week) for worker in self.workers
# for week in self.weeks),
# domain=NonNegativeReals,
# initialize=0,
# )
# self.model.works_weekend_count = Var(
# ((worker.id, week) for worker in self.workers
# for week in self.weeks),
# domain=NonNegativeIntegers,
# initialize=0,
# )
# self.model.works_saturday = Var(
# ((worker.id, week) for worker in self.workers
# for week in self.weeks),
@@ -274,18 +280,39 @@ class RotaBuilder(object):
# initialize=0,
# )
self.model.works_weekend = Var(
((worker.id, week) for worker in self.workers
for week in self.weeks),
domain=Binary,
initialize=0,
)
if self.constraint_options["balance_weekends"]:
self.model.worker_weekend_count = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeIntegers,
initialize=0,
)
self.model.works_weekend = Var(
((worker.id, week) for worker in self.workers
for week in self.weeks),
domain=Binary,
initialize=0,
)
self.model.worker_weekend_count = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeIntegers,
initialize=0,
)
self.model.weekend_shift_count_t1 = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeReals,
initialize=0,
)
self.model.weekend_shift_count_t2 = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeReals,
initialize=0,
)
self.model.weekend_shift_count_w = Var(
((worker.id) for worker in self.workers),
domain=NonNegativeReals,
initialize=0,
)
# self.model.weekend_count_t1 = Var(
# ((worker.id) for worker in self.workers),
@@ -418,12 +445,12 @@ class RotaBuilder(object):
)
def nightShiftMinST4Rule(model, week, shift):
single_workers = [w for w in self.workers if w.grade > 3]
single_workers = [w for w in self.workers if w.grade >= 4]
if not single_workers:
print(single_workers)
return Constraint.Skip
return sum(model.shift_week_worker_assigned[shift, week, w.id]
for w in single_workers) >= 2
for w in single_workers) >= 1
if self.constraint_options["ensure_1_st4_plus_on_nights"]:
self.model.night_shifts_min_st4_constraint = Constraint(
@@ -592,6 +619,31 @@ class RotaBuilder(object):
self.model.night_shift_count[worker.id] -
night_shift_target_number)
xU = 6
xL = 1
self.model.constraints.add(
inequality(
xL,
self.model.night_shift_count_t1[worker.id] +
self.model.night_shift_count_t2[worker.id] + 1,
xU,
))
self.model.constraints.add(
self.model.night_shift_count_w[worker.id] >= xL *
(self.model.night_shift_count_t1[worker.id] +
self.model.night_shift_count_t2[worker.id] + 1) * 2 -
xL * xL)
self.model.constraints.add(
self.model.night_shift_count_w[worker.id] >= xU *
(self.model.night_shift_count_t1[worker.id] +
self.model.night_shift_count_t2[worker.id] + 1) * 2 -
xU * xU)
# self.model.constraints.add(
# self.model.night_shift_count_w[worker.id] >= 0)
# Ensure worker is not allocated shifts on non working days
if worker.nwd:
for week, day, shift in self.get_all_shiftclass_combinations():
@@ -775,11 +827,12 @@ class RotaBuilder(object):
shift_balancing = 0
if self.constraint_options["balance_nights"]:
night_balance_modifier_constant = 2000
night_balance_modifier_constant = 1
night_shift_balancing = sum(
night_balance_modifier_constant *
(self.model.night_shift_count_t1[(worker.id)] +
self.model.night_shift_count_t2[(worker.id)])
self.model.night_shift_count_w[(worker.id)]
# (self.model.night_shift_count_t1[(worker.id)] +
# self.model.night_shift_count_t2[(worker.id)])
for worker in self.workers)
else:
night_shift_balancing = 0
@@ -816,7 +869,7 @@ class RotaBuilder(object):
return night_shift_balancing
#return shift_balancing + preferences + blocks_balancing
#return shift_balancing + preferences + nights_site_balancing + blocks_balancing
return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing
#return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing
# add objective function to the model. rule (pass function) or expr (pass expression directly)
self.model.obj = Objective(rule=obj_rule, sense=minimize)
@@ -1267,9 +1320,16 @@ class RotaResults(object):
def get_night_details(self):
for worker in self.rota.workers:
print(
worker.name,
self.rota.model.night_shift_count[(worker.id)].value,
self.rota.model.night_shift_count_t1[(worker.id)].value,
self.rota.model.night_shift_count_t2[(worker.id)].value,
worker.shift_target_number["night_weekday"] +
worker.shift_target_number["night_weekend"])
"{:20}".format(worker.name),
"worked: {},".format(
self.rota.model.night_shift_count[(worker.id)].value),
"target: {},".format(
worker.shift_target_number["night_weekday"] +
worker.shift_target_number["night_weekend"]),
"target_diff: {},".format(
self.rota.model.night_shift_count_t1[(worker.id)].value +
self.rota.model.night_shift_count_t2[(worker.id)].value +
1),
"balance: {},".format(
self.rota.model.night_shift_count_w[worker.id].value),
)
-118
View File
@@ -1,118 +0,0 @@
Solver command line: ['/usr/bin/cbc', '-seconds', '1200', '-allow', '4000', '-printingOptions', 'all', '-import', '/tmp/tmpeun6jpa7.pyomo.lp', '-stat=1', '-solve', '-solu', '/tmp/tmpeun6jpa7.pyomo.soln']
Welcome to the CBC MILP Solver
Version: 2.9.9
Build Date: Aug 7 2019
command line - /usr/bin/cbc -seconds 1200 -allow 4000 -printingOptions all -import /tmp/tmpeun6jpa7.pyomo.lp -stat=1 -solve -solu /tmp/tmpeun6jpa7.pyomo.soln (default strategy 1)
seconds was changed from 1e+100 to 1200
allowableGap was changed from 1e-10 to 4000
Option for printingOptions changed from normal to all
Presolve 57035 (-82138) rows, 23862 (-89251) columns and 226642 (-883011) elements
Statistics for presolved model
Original problem has 112272 integers (112112 of which binary)
Presolved problem has 23696 integers (23696 of which binary)
==== 23751 zero objective 2 different
23751 variables have objective of 0
111 variables have objective of 2000
==== absolute objective values 2 different
23751 variables have objective of 0
111 variables have objective of 2000
==== for integers 23696 zero objective 1 different
23696 variables have objective of 0
==== for integers absolute objective values 1 different
23696 variables have objective of 0
===== end objective counts
Problem has 57035 rows, 23862 columns (111 with objective) and 226642 elements
There are 111 singletons with objective 988 singletons with no objective
Column breakdown:
110 of type 0.0->inf, 56 of type 0.0->up, 0 of type lo->inf,
0 of type lo->up, 0 of type free, 0 of type fixed,
0 of type -inf->0.0, 0 of type -inf->up, 23696 of type 0.0->1.0
Row breakdown:
2843 of type E 0.0, 676 of type E 1.0, 0 of type E -1.0,
289 of type E other, 0 of type G 0.0, 0 of type G 1.0,
231 of type G other, 2788 of type L 0.0, 48596 of type L 1.0,
1612 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other,
0 of type Free
Continuous objective value is 497995 - 22.34 seconds
Cgl0002I 82402 variables fixed
Cgl0003I 0 fixed, 55 tightened bounds, 34225 strengthened rows, 50251 substitutions
Cgl0004I processed model has 17402 rows, 14104 columns (13939 integer (13939 of which binary)) and 89454 elements
Cbc0038I Initial state - 511 integers unsatisfied sum - 146.075
Cbc0038I Pass 1: (33.84 seconds) suminf. 63.26415 (238) obj. 530335 iterations 2863
Cbc0038I Pass 2: (33.87 seconds) suminf. 52.41969 (214) obj. 526335 iterations 226
Cbc0038I Pass 3: (33.90 seconds) suminf. 51.56248 (211) obj. 526335 iterations 61
Cbc0038I Pass 4: (33.93 seconds) suminf. 51.15178 (211) obj. 526335 iterations 79
Cbc0038I Pass 5: (33.96 seconds) suminf. 45.85375 (210) obj. 529354 iterations 94
Cbc0038I Pass 6: (33.99 seconds) suminf. 45.71180 (212) obj. 529354 iterations 41
Cbc0038I Pass 7: (34.04 seconds) suminf. 35.87037 (187) obj. 529354 iterations 322
Cbc0038I Pass 8: (34.08 seconds) suminf. 33.85554 (195) obj. 526761 iterations 264
Cbc0038I Pass 9: (34.11 seconds) suminf. 33.14549 (176) obj. 526830 iterations 105
Cbc0038I Pass 10: (34.14 seconds) suminf. 31.13224 (172) obj. 526830 iterations 115
Cbc0038I Pass 11: (34.21 seconds) suminf. 30.55620 (160) obj. 529691 iterations 562
Cbc0038I Pass 12: (34.25 seconds) suminf. 28.64178 (153) obj. 534012 iterations 205
Cbc0038I Pass 13: (34.29 seconds) suminf. 29.16979 (146) obj. 527774 iterations 240
Cbc0038I Pass 14: (34.31 seconds) suminf. 28.35432 (144) obj. 523774 iterations 4
Cbc0038I Pass 15: (34.37 seconds) suminf. 25.43149 (140) obj. 525658 iterations 341
Cbc0038I Pass 16: (34.39 seconds) suminf. 25.05606 (138) obj. 525658 iterations 13
Cbc0038I Pass 17: (34.42 seconds) suminf. 25.05606 (133) obj. 525988 iterations 132
Cbc0038I Pass 18: (34.45 seconds) suminf. 25.05606 (133) obj. 525988 iterations 6
Cbc0038I Pass 19: (34.52 seconds) suminf. 25.01303 (143) obj. 526804 iterations 492
Cbc0038I Pass 20: (34.55 seconds) suminf. 24.96852 (141) obj. 526288 iterations 33
Cbc0038I Pass 21: (34.63 seconds) suminf. 22.73642 (133) obj. 527464 iterations 691
Cbc0038I Pass 22: (34.64 seconds) suminf. 22.73642 (133) obj. 527464 iterations 0
Cbc0038I Pass 23: (34.69 seconds) suminf. 22.27803 (129) obj. 527464 iterations 346
Cbc0038I Pass 24: (34.72 seconds) suminf. 22.01961 (123) obj. 527464 iterations 76
Cbc0038I Pass 25: (34.79 seconds) suminf. 22.04026 (123) obj. 526572 iterations 541
Cbc0038I Pass 26: (34.82 seconds) suminf. 22.04026 (121) obj. 526572 iterations 47
Cbc0038I Pass 27: (34.86 seconds) suminf. 22.15035 (111) obj. 527464 iterations 339
Cbc0038I Pass 28: (34.89 seconds) suminf. 22.04689 (111) obj. 527464 iterations 91
Cbc0038I Pass 29: (34.95 seconds) suminf. 22.01936 (123) obj. 526475 iterations 455
Cbc0038I Pass 30: (34.98 seconds) suminf. 21.93681 (123) obj. 526475 iterations 40
Cbc0038I No solution found this major pass
Cbc0038I Before mini branch and bound, 13026 integers at bound fixed and 50 continuous
Cbc0038I Full problem 17402 rows 14104 columns, reduced to 796 rows 583 columns
Cbc0038I Mini branch and bound did not improve solution (35.07 seconds)
Cbc0038I After 37.37 seconds - Feasibility pump exiting - took 3.92 seconds
Cbc0031I 166 added rows had average density of 73.783133
Cbc0013I At root node, 166 cuts changed objective from 497995.24 to 497995.24 in 10 passes
Cbc0014I Cut generator 0 (Probing) - 298 row cuts average 8.2 elements, 0 column cuts (123 active) in 1.134 seconds - new frequency is 1
Cbc0014I Cut generator 1 (Gomory) - 985 row cuts average 585.6 elements, 0 column cuts (0 active) in 0.785 seconds - new frequency is 1
Cbc0014I Cut generator 2 (Knapsack) - 170 row cuts average 2.9 elements, 0 column cuts (0 active) in 0.100 seconds - new frequency is 1
Cbc0014I Cut generator 3 (Clique) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.012 seconds - new frequency is -100
Cbc0014I Cut generator 4 (MixedIntegerRounding2) - 285 row cuts average 68.8 elements, 0 column cuts (0 active) in 0.137 seconds - new frequency is 1
Cbc0014I Cut generator 5 (FlowCover) - 26 row cuts average 28.7 elements, 0 column cuts (0 active) in 0.019 seconds - new frequency is -100
Cbc0014I Cut generator 6 (TwoMirCuts) - 723 row cuts average 138.8 elements, 0 column cuts (0 active) in 0.677 seconds - new frequency is -100
Cbc0010I After 0 nodes, 1 on tree, 1e+50 best solution, best possible 497995.24 (43.62 seconds)
Cbc0010I After 100 nodes, 57 on tree, 1e+50 best solution, best possible 497995.24 (59.88 seconds)
Cbc0010I After 200 nodes, 109 on tree, 1e+50 best solution, best possible 497995.24 (64.07 seconds)
Cbc0010I After 300 nodes, 161 on tree, 1e+50 best solution, best possible 497995.24 (68.27 seconds)
Cbc0012I Integer solution of 497995.24 found by rounding after 15921 iterations and 379 nodes (71.58 seconds)
Cbc0038I Full problem 17402 rows 14104 columns, reduced to 0 rows 0 columns
Cbc0001I Search completed - best objective 497995.2442909057, took 15921 iterations and 379 nodes (71.87 seconds)
Cbc0032I Strong branching done 2148 times (28125 iterations), fathomed 0 nodes and fixed 0 variables
Cbc0035I Maximum depth 100, 0 variables fixed on reduced cost
Cuts at root node changed objective from 497995 to 497995
Probing was tried 119 times and created 537 cuts of which 123 were active after adding rounds of cuts (1.734 seconds)
Gomory was tried 119 times and created 1156 cuts of which 0 were active after adding rounds of cuts (1.679 seconds)
Knapsack was tried 119 times and created 219 cuts of which 0 were active after adding rounds of cuts (1.038 seconds)
Clique was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.012 seconds)
MixedIntegerRounding2 was tried 119 times and created 374 cuts of which 0 were active after adding rounds of cuts (1.404 seconds)
FlowCover was tried 10 times and created 26 cuts of which 0 were active after adding rounds of cuts (0.019 seconds)
TwoMirCuts was tried 10 times and created 723 cuts of which 0 were active after adding rounds of cuts (0.677 seconds)
ImplicationCuts was tried 15 times and created 16 cuts of which 0 were active after adding rounds of cuts (0.023 seconds)
Result - Optimal solution found
Objective value: 497995.24429091
Enumerated nodes: 379
Total iterations: 15921
Time (CPU seconds): 72.51
Time (Wallclock seconds): 72.80
Total time (CPU seconds): 74.98 (Wallclock seconds): 75.41
+40 -31
View File
@@ -77,31 +77,31 @@ Rota.add_shifts(
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
SingleShift(("exeter", ),
"weekend_exeter",
12.5,
days[5:],
assign_as_block=True),
SingleShift(("truro", ),
"weekend_truro",
12.5,
days[5:],
assign_as_block=True),
SingleShift(("torquay", ),
"weekend_torquay",
12.5,
days[5:],
assign_as_block=True),
SingleShift(("plymouth", ),
"weekend_plymouth",
8,
days[5:],
workers_required=2,
assign_as_block=True),
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
# SingleShift(("exeter", ),
# "weekend_exeter",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("truro", ),
# "weekend_truro",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("torquay", ),
# "weekend_torquay",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("plymouth", ),
# "weekend_plymouth",
# 8,
# days[5:],
# workers_required=2,
# assign_as_block=True),
SingleShift(
(sites),
"night_weekday",
@@ -154,8 +154,13 @@ if use_test_workers:
for i in range(9, 17)
])
Rota.add_workers([
Worker(Rota, i, "Plym {}".format(i), "plymouth", 4, 80)
for i in range(17, 19)
Worker(Rota,
i,
"Plym {}".format(i),
"plymouth",
4,
80,
end_date="2020/10/05") for i in range(17, 19)
])
# Rota.add_workers([
# Worker(
@@ -196,16 +201,20 @@ else:
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
#print(nwds, end_date, oop)
# Rota.add_worker(
# Worker(Rota, n, name, site.lower(), int(grade[2]), 100,
# ))
Rota.add_worker(
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
nwds, end_date, oop))
Rota.constraint_options["limit_to_1_st1_on_nights"] = True
Rota.constraint_options["limit_to_1_st1_on_nights"] = False
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = True
Rota.constraint_options["balance_nights"] = True
Rota.constraint_options["constrain_time_off_after_nights"] = True
Rota.constraint_options["constrain_time_off_after_nights"] = False
Rota.constraint_options["balance_nights_across_sites"] = False
Rota.constraint_options["balance_blocks"] = False
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["balance_weekends"] = False
print(0)
Rota.build_shifts_and_workers()
@@ -226,7 +235,7 @@ else:
tee=True,
options={
"seconds": 1200,
"allow": 4000,
"allow": 15,
},
logfile="test.log"
) # solve the model with the, options="seconds=60" selected solver