night shift balancing now works!
This commit is contained in:
@@ -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;
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user