This commit is contained in:
Ross
2022-07-14 14:51:11 +01:00
parent 8f8e5a2a28
commit fecddb2db1
5 changed files with 925 additions and 153 deletions
+1
View File
@@ -2,6 +2,7 @@ sample.txt
*.html *.html
*.log *.log
*.pyc *.pyc
*.bak
current.log current.log
extended proc nights only.log extended proc nights only.log
no nights.log no nights.log
+131 -131
View File
@@ -25,11 +25,10 @@ from rota.workers import (
) )
suspend_on_finish = False suspend_on_finish = True
solve = True solve = True
time_to_run = 258500 time_to_run = 258500
time_to_run = 36000 time_to_run = 36000
time_to_run = 200
# allow = 5000 # allow = 5000
ratio = 0.05 ratio = 0.05
@@ -39,145 +38,145 @@ start_date = datetime.date(2022, 9, 5)
Rota = RotaBuilder(start_date, weeks_to_rota=26) Rota = RotaBuilder(start_date, weeks_to_rota=26)
Rota.constraint_options["balance_weekends"] = True Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["max_night_frequency"] = 3 Rota.constraint_options["max_night_frequency"] = 4
Rota.constraint_options["max_weekend_frequency"] = 3 Rota.constraint_options["max_weekend_frequency"] = 4
# Rota.constraint_options["avoid_st2_first_month"] = True # Rota.constraint_options["avoid_st2_first_month"] = True
Rota.add_shifts( Rota.add_shifts(
#SingleShift( SingleShift(
# sites=("exeter", "exeter twilights and weekends"), sites=("exeter", "exeter twilights and weekends"),
# name="exeter_twilight", name="exeter_twilight",
# length=12.5, length=12.5,
# days=days[:5], days=days[:5],
# balance_offset=3, #balance_offset=3,
# constraint=[{"name": "max_shifts_per_week", "options": 2, }], constraint=[{"name": "max_shifts_per_week", "options": 2, }],
#), ),
#SingleShift( SingleShift(
# sites=("truro", "truro twilights"), sites=("truro", "truro twilights"),
# name="truro_twilight", name="truro_twilight",
# length=12.5, length=12.5,
# days=days[:4], days=days[:4],
# balance_offset=3, #balance_offset=3,
#), ),
#SingleShift( SingleShift(
# sites=("torbay", "torbay twilights"), sites=("torbay", "torbay twilights"),
# name="torbay_twilight", name="torbay_twilight",
# length=12.5, length=12.5,
# days=days[:4], days=days[:4],
# balance_offset=5, #balance_offset=4,
# assign_as_block=True, assign_as_block=True,
#), ),
SingleShift( SingleShift(
sites=("plymouth", "plymouth twilights"), sites=("plymouth", "plymouth twilights"),
name="plymouth_twilight", name="plymouth_twilight",
length=12.5, length=12.5,
days=days[:5], days=days[:5],
balance_offset=2, #balance_offset=4,
workers_required=1, workers_required=1,
constraint=[{"name": "max_shifts_per_week", "options": 2}], constraint=[{"name": "max_shifts_per_week", "options": 2}],
), ),
#SingleShift( SingleShift(
# sites=("exeter", "exeter twilights and weekends"), sites=("exeter", "exeter twilights and weekends"),
# name="weekend_exeter", name="weekend_exeter",
# length=12.5, length=12.5,
# days=days[5:], days=days[5:],
# balance_offset=4, #balance_offset=4,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=True, force_as_block=True,
# constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}], constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}],
# #constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},], #constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
#), ),
#SingleShift( SingleShift(
# sites=("truro",), sites=("truro",),
# name="weekend_truro", name="weekend_truro",
# length=12.5, length=12.5,
# days=days[4:], days=days[4:],
# balance_offset=4, #balance_offset=4,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=True, force_as_block=True,
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# # force_as_block_unless_nwd=True # force_as_block_unless_nwd=True
#), ),
#SingleShift( SingleShift(
# sites=("torbay",), sites=("torbay",),
# name="weekend_torbay", name="weekend_torbay",
# length=12.5, length=12.5,
# days=days[4:], days=days[4:],
# balance_offset=4, #balance_offset=4,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=True, force_as_block=True,
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# # force_as_block_unless_nwd=True # force_as_block_unless_nwd=True
#), ),
#SingleShift( SingleShift(
# sites=("plymouth",), sites=("plymouth",),
# name="weekend_plymouth1", name="weekend_plymouth1",
# length=8, length=8,
# days=days[5:], days=days[5:],
# balance_offset=4, #balance_offset=4,
# workers_required=1, workers_required=1,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=True, force_as_block=True,
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
#), ),
#SingleShift( SingleShift(
# sites=("plymouth",), sites=("plymouth",),
# name="weekend_plymouth2", name="weekend_plymouth2",
# length=8, length=8,
# days=days[5:], days=days[5:],
# balance_offset=4, #balance_offset=4,
# workers_required=1, workers_required=1,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=True, force_as_block=True,
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
#), ),
#SingleShift( SingleShift(
# sites=("plymouth", "plymouth twilights"), sites=("plymouth", "plymouth twilights"),
# name="plymouth_bank_holidays", name="plymouth_bank_holidays",
# length=8, length=8,
# days=days[5:], days=days[5:],
# balance_offset=2, #balance_offset=2,
# workers_required=1, workers_required=1,
# rota_on_nwds=True, rota_on_nwds=True,
# force_as_block=False, force_as_block=False,
# bank_holidays_only=True, bank_holidays_only=True,
#), ),
#SingleShift( SingleShift(
# sites=(sites), sites=(sites),
# name="night_weekday", name="night_weekday",
# length=12.25, length=12.25,
# days=days[:4], days=days[:4],
# balance_offset=4.9, #balance_offset=4.9,
# balance_weighting=1, balance_weighting=1,
# # hard_constrain_shift=False, # hard_constrain_shift=False,
# workers_required=3, workers_required=3,
# force_as_block=True, force_as_block=True,
# rota_on_nwds=True, rota_on_nwds=True,
# constraint=[ constraint=[
# {"name": "night"}, {"name": "night"},
# {"name": "pre", "options": 5}, {"name": "pre", "options": 5},
# {"name": "post", "options": 5}, {"name": "post", "options": 5},
# {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
# ], ],
#), ),
#SingleShift( SingleShift(
# sites=(sites), sites=(sites),
# name="night_weekend", name="night_weekend",
# length=12.25, length=12.25,
# days=days[4:], days=days[4:],
# balance_offset=3.9, #balance_offset=3.9,
# balance_weighting=1, balance_weighting=1,
# # hard_constrain_shift=False, # hard_constrain_shift=False,
# workers_required=3, workers_required=3,
# force_as_block=True, force_as_block=True,
# rota_on_nwds=True, rota_on_nwds=True,
# constraint=[ constraint=[
# {"name": "night"}, {"name": "night"},
# {"name": "pre", "options": 5}, {"name": "pre", "options": 5},
# {"name": "post", "options": 5}, {"name": "post", "options": 5},
# {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
# ], ],
#), ),
) )
Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"]) Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"])
@@ -290,7 +289,8 @@ if load_leave:
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10} solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
start_time = time.time() start_time = time.time()
Rota.solve_shifts_individually(solver_options) Rota.build_and_solve(solver_options)
#Rota.solve_shifts_individually(solver_options)
#Rota.solve_model(options=solver_options) #Rota.solve_model(options=solver_options)
end_time = time.time() end_time = time.time()
+301
View File
@@ -0,0 +1,301 @@
html {
/* color: red; */
}
table {
/* margin-top: 50px; */
padding-top: 100px;
text-align: center;
/* table-layout : fixed; */
/* width:150px */
/* width: 200%; */
/* overflow-x: auto;
display: block;
position: relative; */
border-collapse: separate;
border-spacing: 0;
}
.shift-table {
padding-top: 10px;
}
.shift-table thead {
font-weight: bold;
}
.table-div {
/* overflow-x: auto; */
width: 80%;
overflow-x: scroll;
margin-left: 200px;
overflow-y: visible;
padding: 0;
}
.table-div th {
text-align: center;
white-space: nowrap;
transform: rotate(90deg) translateX(-100px);
position: sticky;
top: 0;
}
.table-div th p {
margin: 0 -100%;
display: inline-block;
}
.table-div th p:before {
content: '';
width: 0;
padding-top: 110%;
/* takes width as reference, + 10% for faking some extra padding */
display: inline-block;
vertical-align: middle;
}
.table-div td,
th {
max-width: 10px;
padding: 0px;
}
.table-div tr {
padding: 0px;
}
.table-div tr:hover {
background-color: lightblue;
}
.table-div tr:hover .unavailable {
background-color: lightgray;
}
.Sat {
/* opacity: 30%; */
/* background-color: lightsteelblue; */
border-left: solid 1px lightsteelblue;
}
.Sun {
border-right: solid 1px lightsteelblue;
}
.bank-holiday {
border-bottom: dotted 1px blue;
}
th.bank-holiday {
color: darkblue;
}
/* th.worker {
max-width: 100%;
width: 200px;
} */
.plymouth {
color: #9169FF;
}
.exeter {
color: #5F60E8;
}
.truro {
color: #0074D9;
}
.torquay, .torbay {
color: #5FA8E8;
}
.taunton {
color: #e85fdf;
}
.unavailable {
color: lightgray;
}
.shift-requested {
color: green;
}
.worker-summary {
position: absolute;
display: none;
overflow-x: visible;
width: 100%;
text-align: left;
}
.site-summary {
display: inline-flex;
}
.site-summary .site {
border: 1px solid black;
padding: 5px;
margin: 20px;
}
.site-name {
font-weight: bold;
}
.worker-summary span {
min-width: 160px;
display: inline-flex;
}
.training-days-lost {
padding-top: 5px;
}
.tdl-settings {
padding: 2px;
}
.table-div+pre {
display: none;
}
.shift-breakdown-button+pre {
display: none;
}
.shift-breakdown-button {
display: block;
}
.nwd {
color: lightcoral;
}
.th {}
.table-div th.worker,
td.worker {
position: absolute;
width: 200px;
max-width: 200px;
left: 0;
top: auto;
border-top-width: 1px;
/*only relevant for first row*/
margin-top: -1px;
/*compensate for top border*/
}
.hidden {
opacity: 0%;
}
.tsummary {
max-width: auto;
}
.header th {
max-width: 200px;
}
/* [data-night-at-derriford="0"] {
border: 1px dotted orange;
} */
th.site-title {
transform: unset;
}
.name {
max-width: 90px;
display: inline-flex;
white-space: nowrap;
overflow: hidden;
/*text-overflow: ellipsis;*/
}
.shift-highlight {
background-color: palegreen;
}
.pair-match {
background-color: red;
}
summary h2 {
display: inline;
}
.remote-site-match.shift-highlight {
background-color: yellow;
}
.large-shift-diff::before {
content: "*";
color: red;
}
/* table.transposed {
border-collapse: collapse;
}
table.transposed tr {
display: block;
float: left;
}
table.transposed th,
table.transposed td {
display: block;
border: 1px solid black;
max-width: 100px;
} */
table.transposed {
}
table.transposed tr {
}
table.transposed th,
table.transposed td {
width: fit-content;
max-width: unset;
position: relative;
border-top: solid 0.5px black;
border-right: solid 0.5px black;
}
table.transposed .Sat {
/* opacity: 30%; */
/* background-color: lightsteelblue; */
border-top: solid 2px lightsteelblue !important;
}
table.transposed .Sun {
border-bottom: solid 2px lightsteelblue !important;
}
table.transposed .bank-holiday {
border-bottom: dotted 2px blue !important;
}
table.transposed th.bank-holiday {
color: darkblue;
}
+437
View File
@@ -0,0 +1,437 @@
Object.defineProperties(Array.prototype, {
count: {
value: function (query) {
/*
Counts number of occurrences of query in array, an integer >= 0
Uses the javascript == notion of equality.
*/
var count = 0;
for (let i = 0; i < this.length; i++)
if (this[i] == query)
count++;
return count;
}
}
});
const mean = arr => arr.reduce((p, c) => p + c, 0) / arr.length;
tables = $(".table-div");
function generateExtra() {
$(".auto-generated").remove();
global_shifts = new Set();
total_summed_shift_diffs = 0;
total_summed_shift_diffs_abs = 0;
tables.each((n, table) => {
rows = $(table).find(".worker-row");
workers = {};
rows.each((n, tr) => {
worker_td = $(tr).find("td:first");
worker = worker_td.attr("data-worker");
shift_tds = $(tr).find(".rota-day");
shifts = []
shift_tds.each((n, td) => {
shifts.push($(td).attr("data-shift"));
})
shifts_unique = new Set(shifts);
shifts_unique.delete("");
global_shifts = new Set([...global_shifts, ...shifts_unique]);
weekends_worked = 0;
weeks = shift_tds.length / 7;
whole_weekends_worked = 0;
for (var i = 1; i <= weeks; i++) {
if ($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" || $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") {
weekends_worked = weekends_worked + 1;
}
if ($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" && $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") {
whole_weekends_worked = whole_weekends_worked + 1;
}
}
shift_counts = {};
total_shifts = 0;
days_lost = 0;
shifts_unique.forEach(el => {
shift_counts[el] = shifts.count(el);
total_shifts = total_shifts + shifts.count(el);
// why like this?
if (el == "night_weekend") {
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
}
if ($("#global-settings").length == 1) {
n = $(`#${el}-tdl`).val();
if (!isNaN(n)) {
days_lost = days_lost + shifts.count(el) * n;
}
}
// // 5 days for weekday nights
// if(el == "night_weekday") { days_lost = days_lost + shifts.count(el) / 4 * 5 }
// // 4 days for weekend nights
// if(el == "night_weekend") {
// days_lost = days_lost + shifts.count(el) / 3 * 4
// //whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
// }
// // 0.5 for twilights
// if(el.includes("twilight")) {
// twilight_days_lost = shifts.count(el) / 2
// days_lost = days_lost + twilight_days_lost
// }
});
// Also 1 day for a whole weekend
days_lost = days_lost + whole_weekends_worked
// Highlight non working days
nwds = JSON.parse(worker_td.attr("data-nwds"));
nwds.forEach(element => {
// Restrict to date ranges
start_date = new Date(Date.parse(element[1]))
end_date = new Date(Date.parse(element[2]))
$(shift_tds).filter(`.${element[0]}`).each(
(n, td_el) => {
td_date = new Date(Date.parse(td_el.dataset.date));
if (td_date >= start_date && td_date < end_date) {
$(td_el).addClass("nwd");
}
}
)
});
workers[worker] = {
"site": worker_td.attr("data-site"),
"nwds": worker_td.attr("data-nwds"),
"fte": worker_td.attr("data-fte_adj"),
"end_date": worker_td.attr("data-end_date"),
"shift_diff": worker_td.attr("data-shift-diff"),
"shifts": shifts,
"shift_types": shifts_unique,
"weekends_worked": weekends_worked,
"shift_counts": shift_counts,
"days_lost": days_lost,
//"shifts"
}
sc = JSON.stringify(shift_counts);
bank_holidays = shift_tds.filter(".bank-holiday").filter(function (i) {
return $(this).attr('data-shift') != "";
});
console.log("BH", bank_holidays.length)
bank_holidays_worked = bank_holidays.length;
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
total_summed_shift_diffs = total_summed_shift_diffs + summed_shift_diff;
total_summed_shift_diffs_abs = total_summed_shift_diffs_abs + Math.abs(summed_shift_diff);
worker_td.after(`<div class='worker-summary auto-generated'>
<span>Total shifts: ${total_shifts}, </span>
<span>Weekends: ${weekends_worked}, </span>
<span>Bank holidays: ${bank_holidays_worked}, </span>
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
</div>`)
if (Math.abs(summed_shift_diff) >= 1) {
worker_td.addClass("large-shift-diff")
$(tr).find(".shift-diff-span").addClass("large-shift-diff")
}
})
sites = {};
sites_days_lost = {}
for (const worker in workers) {
//console.log(worker, workers[worker]);
w = workers[worker];
if (w.fte == 100) {
//console.log(w.shift_types);
for (let shift of w.shift_types) {
//console.log(shift);
if (!(w.site in sites)) {
sites[w.site] = {};
}
if (!(shift in sites[w.site])) {
sites[w.site][shift] = [w.shift_counts[shift]];
} else {
sites[w.site][shift].push(w.shift_counts[shift]);
}
}
if (!(w.site in sites_days_lost)) {
sites_days_lost[w.site] = [w.days_lost];
} else {
sites_days_lost[w.site].push(w.days_lost);
}
}
}
// Calculate site shift summaries
// $(table).append("<div class='site-summary auto-generated'>Average shifts for a 100% FTE trainee</div>");
// for(const site in sites) {
// $(table).find(".site-summary").append(`<div class='site site-${site}'><span class='site-name'>${site}</span></div>`);
// for(const shift in sites[site]) {
// average = mean(sites[site][shift])
// $(table).find(`.site-${site}`).append(`<div>${shift}: ${average.toFixed(2)}</div>`);
// }
// days_lost_avg = mean(sites_days_lost[site]);
// $(table).find(`.site-${site}`).append(`<div class='training-days-lost'>Training days lost: ${days_lost_avg.toFixed(2)}</div>`);
// }
summary_button = $("<button class='auto-generated'>Toggle Summary</button>").click(() => {
$(table).find(".rota-day").toggleClass("hidden");
$(table).find(".worker-summary").toggle();
});
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs}</span><br/>`));
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs_abs}</span><br/>`));
$(table).before(summary_button);
});
return global_shifts
}
shifts = generateExtra();
console.log(shifts)
if (shifts.size < 1) {
shifts = $("#shifts-container").data("shifts");
}
console.log(shifts)
oshifts = Array.from(shifts).sort()
$("body").append("<div><table class='tsummary'></table></div>")
$("table.tsummary").append("<tr class='header'></tr>");
h = $("table.tsummary tr.header");
h.append(`<th>Worker</th>`);
h.append(`<th>Site</th>`);
h.append(`<th>FTE</th>`);
h.append(`<th>FTE (adj)</th>`);
oshifts.forEach((s) => {
h.append(`<th>${s}</th>`);
});
$(".table-div .worker-row .worker").each((n, tr) => {
//worker_td = $(tr).find("td").first();
jtr = $(tr);
row = $("<tr></tr>");
row.append(`<td>${jtr.data("worker")}</td>`)
row.append(`<td>${jtr.data("site")}</td>`)
row.append(`<td>${jtr.data("fte")}</td>`)
row.append(`<td>${parseFloat(jtr.data("fte_adj")).toFixed(2)}</td>`)
shift_counts = jtr.data("shift-counts")
shift_targets = jtr.data("worker-targets")
oshifts.forEach((s) => {
if (s in shift_targets && shift_targets[s] > 0) {
if (s in shift_counts) {
c = shift_counts[s];
} else {
c = 0;
}
row.append(`<td>${c} (${shift_targets[s].toFixed(2)})</td>`)
} else {
row.append(`<td>-</td>`)
}
})
$("table.tsummary").append(row)
})
//$("table.summary")
// $("body").prepend("<div id='global-settings'>Settings:<br />Customise training days lost per shift (you need to press recalculate for changes to take effect)<form></form></div>")
// shifts.forEach((s) => {
// disable = false;
// if(s.includes("twilight")) {
// value = 0.5;
// } else if(s == "night_weekend") {
// value = 4 / 3;
// } else if(s == "night_weekday") {
// value = 5 / 4;
// } else {
// disable = true;
// value = 0;
// }
// if(!disable) {
// $("#global-settings form").append(`<span class='tdl-settings'><label for="${s}-tdl">${s}</label><input type="number" id="${s}-tdl" name="${s}-tdl" value='${value}'></span>`);
// }
// })
// $("#global-settings").append($("<button >Recalculate</button>").click(() => {
// generateExtra();
// }))
// generateExtra();
$(".table-div + pre").each((n, pre) => {
$(pre).before($("<button class='shift-breakdown-button'>Show shift breakdown</button>").click(() => {
$(pre).toggle();
}));
})
$("td").hover((e) => {
//start hover
worker_td = $(e.target).closest('tr').find('td:first')[0]
pair = worker_td.dataset.pair;
index = $(e.target).index() - 1;
if (pair > 0) {
$(`td[data-pair=${pair}]`).each((n, el) => {
$($(el).closest('tr').find('td').get(index)).addClass("pair-match")
})
}
if (e.target.dataset.shift != undefined && e.target.dataset.shift.length > 0) {
$(`td[data-shift=${e.target.dataset.shift}]`).addClass("shift-highlight")
console.log(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`)
console.log($(e.target).closest('tr').find(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`))
$(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`).closest('tr').find(`td[data-shift=${e.target.dataset.shift}]`).addClass("remote-site-match")
// console.log(e.target.dataset.shiftRemoteSite , worker_td.dataset.remoteSite)
// if (e.target.dataset.shiftRemoteSite == worker_td.dataset.remoteSite) {
// $(`td[data-shift-remote-site=${e.target.dataset.shiftRemoteSite}]`).addClass("remote-site-match")
// }
}
}, (e) => {
//end hover
$("td.pair-match").removeClass("pair-match")
$("td.shift-highlight").removeClass("shift-highlight")
$("td.remote-site-match").removeClass("remote-site-match")
})
$("#rota-table tr td:first-child").each((n, td) => {
$(td).click(() => {
viewWorker(td);
})
})
function viewWorker(worker) {
ds = worker.dataset;
dlg = $(`<div class='dialog' title='${ds.worker}'>
Site: ${ds.site}</br>
FTE: ${ds.fte} (${ds.fte_adj})</br>
Start date: ${ds.start_date}</br>
End date: ${ds.end_date}</br>
Non working days: ${ds.nwds}</br>
Remote site: ${ds.remoteSite}
</div>`)
$("body").append(dlg)
console.log(ds.shiftCounts)
shifts = JSON.parse(ds.shiftCounts)
shift_targets = JSON.parse(ds.workerTargets)
shift_diffs = JSON.parse(ds.shiftDiff)
let shift_count_table = document.createElement("table")
shift_count_table.classList.add("shift-table")
let header_row = shift_count_table.createTHead().insertRow();
header_row.insertCell().appendChild(document.createTextNode("Shift"))
header_row.insertCell().appendChild(document.createTextNode("Count"))
header_row.insertCell().appendChild(document.createTextNode("Target"))
header_row.insertCell().appendChild(document.createTextNode("Diffs"))
table_body = shift_count_table.createTBody()
for (const shift in shifts) {
let row = table_body.insertRow();
row.insertCell().appendChild(document.createTextNode(shift))
row.insertCell().appendChild(document.createTextNode(shifts[shift]))
row.insertCell().appendChild(document.createTextNode(shift_targets[shift].toPrecision(2)))
row.insertCell().appendChild(document.createTextNode(shift_diffs[shift].toPrecision(2)))
//console.log(shift, shifts[shift]);
}
dlg.get(0).appendChild(shift_count_table)
dlg.dialog()
console.log(worker);
}
$("#export-div").append($("#main-table").clone().attr("id", "gen-table").addClass("transposed"))
$("#gen-table").each(function() {
var $this = $(this);
var newrows = [];
//$this.find("th.site-title").parent().remove();
$this.find("tr").each(function(rowToColIndex){
$(this).find("td, th").each(function(colToRowIndex){
if(newrows[colToRowIndex] === undefined) { newrows[colToRowIndex] = $("<tr></tr>"); }
while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){
newrows[colToRowIndex].append($("<td></td>"));
}
newrows[colToRowIndex].append($(this));
});
});
$this.find("tr").remove();
$.each(newrows, function(){
$this.append(this);
});
$this.find("td.rota-day").each((n, el) => {
console.log(el)
$(el).text(el.dataset.shift)
});
});
+54 -21
View File
@@ -1,5 +1,6 @@
from calendar import week from calendar import week
import datetime import datetime
from distutils.log import debug
import itertools import itertools
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
@@ -13,6 +14,7 @@ from pyomo.environ import *
from pyomo.opt import SolverFactory from pyomo.opt import SolverFactory
from rota.workers import Worker from rota.workers import Worker
from rota.console import console
import json import json
@@ -25,6 +27,15 @@ import sys
from rich.pretty import pprint from rich.pretty import pprint
import logging
logging.basicConfig(
filename="rota.log",
filemode="w",
format="%(name)s - %(levelname)s - %(message)s",
level=logging.DEBUG,
)
ShiftName = str ShiftName = str
DayStr = str DayStr = str
WeekInt = int WeekInt = int
@@ -38,9 +49,12 @@ from govuk_bank_holidays.bank_holidays import BankHolidays
bank_holidays = BankHolidays() bank_holidays = BankHolidays()
bank_holiday_map = {} bank_holiday_map = {}
logging.debug("Creating bank holiday map")
for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"): for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"):
bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"] bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"]
logging.debug(bank_holiday_map)
SHIFT_BOUNDS = { SHIFT_BOUNDS = {
"bank_holiday": (0, 9), "bank_holiday": (0, 9),
"shift_count": (0, 400), "shift_count": (0, 400),
@@ -253,12 +267,16 @@ class RotaBuilder(object):
self.ignore_valid_shifts = False self.ignore_valid_shifts = False
def solve_model( def solve_model(
self, solver: str = "cbc", use_neos: bool = False, options: dict = {} self,
solver: str = "cbc",
use_neos: bool = False,
options: dict = {},
debug_if_fail: bool = False,
): ):
print("Setting up solver") console.print("Setting up solver")
self.opt = SolverFactory(solver) self.opt = SolverFactory(solver)
print("Solving") console.print("Solving")
if use_neos: if use_neos:
solver_manager = SolverManagerFactory("neos") # Solve using neos server solver_manager = SolverManagerFactory("neos") # Solve using neos server
# results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log") # results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
@@ -278,8 +296,12 @@ class RotaBuilder(object):
self.results = results self.results = results
print(results) console.print(f"Complete - outcome: {results.solver.status}")
results.solver.status
if results.solver.status != "ok" and debug_if_fail:
console.print(f"Attempting each shift individually")
self.solve_shifts_individually(options)
if not results.solver.status: if not results.solver.status:
sys.exit(0) sys.exit(0)
@@ -289,28 +311,32 @@ class RotaBuilder(object):
outcomes = {} outcomes = {}
for shift in shifts: for shift in shifts:
console.print(f"Testing shifts: {shift.name}")
self.ignore_valid_shifts = True self.ignore_valid_shifts = True
self.clear_shifts() self.clear_shifts()
self.add_shift(shift) self.add_shift(shift)
self.build_and_solve(options) self.build_and_solve(options)
self.export_rota_to_html(filename=shift.name, folder=folder) self.export_rota_to_html(filename=shift.name, folder=folder)
outcomes[shift.name] = (self.results.solver.status, self.results.solver.termination_condition) outcomes[shift.name] = (
pprint(outcomes) self.results.solver.status,
self.results.solver.termination_condition,
)
pprint(outcomes)
console.print(outcomes)
def build_and_solve( def build_and_solve(
self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True self,
options={"ratio": 0.1, "seconds": 1000, "threads": 10},
solve=True,
debug_if_fail: bool = False,
): ):
self.build_shifts() self.build_shifts()
self.build_workers() self.build_workers()
self.build_model() self.build_model()
if solve: if solve:
self.solve_model(options=options) self.solve_model(options=options, debug_if_fail=debug_if_fail)
def build_model(self): def build_model(self):
# Initialize model # Initialize model
@@ -1078,7 +1104,7 @@ class RotaBuilder(object):
set(self.get_shift_names()) set(self.get_shift_names())
): ):
if self.ignore_valid_shifts: if self.ignore_valid_shifts:
continue # Skip if we don't want to raise an error continue # Skip if we don't want to raise an error
else: else:
raise InvalidShift( raise InvalidShift(
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}" f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
@@ -1478,7 +1504,9 @@ class RotaBuilder(object):
shift.name, week, worker.id shift.name, week, worker.id
] ]
for week in week_blocks for week in week_blocks
for shift in self.get_shifts_with_constraint("night") for shift in self.get_shifts_with_constraint(
"night"
)
) )
) )
@@ -1747,7 +1775,7 @@ class RotaBuilder(object):
# #
#def get_pre_may(week_days, max_pre) # def get_pre_may(week_days, max_pre)
for n in range(len(weeks_days)): for n in range(len(weeks_days)):
@@ -1955,7 +1983,7 @@ class RotaBuilder(object):
) )
for constraint_shift in self.get_shifts_with_constraints( for constraint_shift in self.get_shifts_with_constraints(
"pre", "pre",
): ):
for n in range(0, constraint_shift.constraint_options["pre"]): for n in range(0, constraint_shift.constraint_options["pre"]):
if day in constraint_shift.days: if day in constraint_shift.days:
@@ -1965,7 +1993,10 @@ class RotaBuilder(object):
worker.id, week, day, constraint_shift.name worker.id, week, day, constraint_shift.name
] ]
+ sum( + sum(
pre_map[n][0] * self.model.works[w.id, pre_map[n][1], pre_map[n][2], shiftname] pre_map[n][0]
* self.model.works[
w.id, pre_map[n][1], pre_map[n][2], shiftname
]
for shiftname in self.get_shift_names_by_week_day( for shiftname in self.get_shift_names_by_week_day(
pre_map[n][1], pre_map[n][2] pre_map[n][1], pre_map[n][2]
) )
@@ -1973,7 +2004,7 @@ class RotaBuilder(object):
for w in workers for w in workers
) )
) )
#for constraint_shift in self.get_shifts_with_constraint( "pre"): # for constraint_shift in self.get_shifts_with_constraint( "pre"):
# to_sum = [] # to_sum = []
# for n in range( # for n in range(
# 0, constraint_shift.constraint_options["pre"] # 0, constraint_shift.constraint_options["pre"]
@@ -1988,7 +2019,6 @@ class RotaBuilder(object):
# w.id, wk, dy, shiftname # w.id, wk, dy, shiftname
# ]) # ])
# if day in constraint_shift.days: # if day in constraint_shift.days:
# self.model.constraints.add( # self.model.constraints.add(
# 1 # 1
@@ -2403,7 +2433,10 @@ class RotaBuilder(object):
set: Set of ShiftName set: Set of ShiftName
""" """
return set((week, day, shift_name) for shift_name in self.week_day_shifts_dict[(week, day)]) return set(
(week, day, shift_name)
for shift_name in self.week_day_shifts_dict[(week, day)]
)
def get_shift_by_name(self, name: str) -> SingleShift: def get_shift_by_name(self, name: str) -> SingleShift:
""" """
@@ -2738,7 +2771,7 @@ class RotaBuilder(object):
return self.start_date + datetime.timedelta(weeks=week) return self.start_date + datetime.timedelta(weeks=week)
# RESULTS # RESULTS
def export_rota_to_html(self, filename: str = "rota", folder = None): def export_rota_to_html(self, filename: str = "rota", folder=None):
if folder is not None: if folder is not None:
output_file = Path("output", folder, f"{filename}.html") output_file = Path("output", folder, f"{filename}.html")
else: else: