diff --git a/gen.py b/gen.py index de09de4..8b8215e 100644 --- a/gen.py +++ b/gen.py @@ -28,18 +28,19 @@ from rota.workers import ( suspend_on_finish = True solve = True time_to_run = 258500 -time_to_run = 36000 +time_to_run = 166000 # allow = 5000 -ratio = 0.05 +ratio = 0.01 +#ratio = 0.9 start_date = datetime.date(2022, 9, 5) -#start_date = datetime.date(2022, 10, 10) +# start_date = datetime.date(2022, 10, 10) Rota = RotaBuilder(start_date, weeks_to_rota=26) Rota.constraint_options["balance_weekends"] = True -Rota.constraint_options["max_night_frequency"] = 4 -Rota.constraint_options["max_weekend_frequency"] = 4 +Rota.constraint_options["max_night_frequency"] = 3 +Rota.constraint_options["max_weekend_frequency"] = 3 # Rota.constraint_options["avoid_st2_first_month"] = True Rota.add_shifts( @@ -48,22 +49,27 @@ Rota.add_shifts( name="exeter_twilight", length=12.5, days=days[:5], - #balance_offset=3, - constraint=[{"name": "max_shifts_per_week", "options": 2, }], + balance_offset=2, + constraint=[ + { + "name": "max_shifts_per_week", + "options": 2, + } + ], ), SingleShift( sites=("truro", "truro twilights"), name="truro_twilight", length=12.5, days=days[:4], - #balance_offset=3, + balance_offset=2, ), SingleShift( sites=("torbay", "torbay twilights"), name="torbay_twilight", length=12.5, days=days[:4], - #balance_offset=4, + balance_offset=2, assign_as_block=True, ), SingleShift( @@ -71,7 +77,7 @@ Rota.add_shifts( name="plymouth_twilight", length=12.5, days=days[:5], - #balance_offset=4, + balance_offset=2, workers_required=1, constraint=[{"name": "max_shifts_per_week", "options": 2}], ), @@ -80,18 +86,18 @@ Rota.add_shifts( name="weekend_exeter", length=12.5, days=days[5:], - #balance_offset=4, + balance_offset=2, rota_on_nwds=True, force_as_block=True, 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( sites=("truro",), name="weekend_truro", length=12.5, days=days[4:], - #balance_offset=4, + balance_offset=3, rota_on_nwds=True, force_as_block=True, constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], @@ -102,7 +108,7 @@ Rota.add_shifts( name="weekend_torbay", length=12.5, days=days[4:], - #balance_offset=4, + balance_offset=3, rota_on_nwds=True, force_as_block=True, constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], @@ -113,7 +119,7 @@ Rota.add_shifts( name="weekend_plymouth1", length=8, days=days[5:], - #balance_offset=4, + balance_offset=2, workers_required=1, rota_on_nwds=True, force_as_block=True, @@ -124,7 +130,7 @@ Rota.add_shifts( name="weekend_plymouth2", length=8, days=days[5:], - #balance_offset=4, + balance_offset=2, workers_required=1, rota_on_nwds=True, force_as_block=True, @@ -135,7 +141,7 @@ Rota.add_shifts( name="plymouth_bank_holidays", length=8, days=days[5:], - #balance_offset=2, + balance_offset=2, workers_required=1, rota_on_nwds=True, force_as_block=False, @@ -146,7 +152,7 @@ Rota.add_shifts( name="night_weekday", length=12.25, days=days[:4], - #balance_offset=4.9, + # balance_offset=4.9, balance_weighting=1, # hard_constrain_shift=False, workers_required=3, @@ -157,6 +163,8 @@ Rota.add_shifts( {"name": "pre", "options": 5}, {"name": "post", "options": 5}, {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, + {"name": "limit_grade_number", "options": {2: 1}}, + {"name": "minimum_grade_number", "options": (4, 1)}, ], ), SingleShift( @@ -164,7 +172,7 @@ Rota.add_shifts( name="night_weekend", length=12.25, days=days[4:], - #balance_offset=3.9, + # balance_offset=3.9, balance_weighting=1, # hard_constrain_shift=False, workers_required=3, @@ -175,6 +183,8 @@ Rota.add_shifts( {"name": "pre", "options": 5}, {"name": "post", "options": 5}, {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, + {"name": "limit_grade_number", "options": {2: 1}}, + {"name": "minimum_grade_number", "options": (4, 1)}, ], ), ) @@ -283,15 +293,15 @@ if load_leave: ) ) -#Rota.build_workers() -#Rota.build_model() +# Rota.build_workers() +# Rota.build_model() solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10} start_time = time.time() 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() print(f"Time taken {end_time-start_time}") diff --git a/output/individual_shifts/timetable.css b/output/individual_shifts/timetable.css deleted file mode 100644 index dac3438..0000000 --- a/output/individual_shifts/timetable.css +++ /dev/null @@ -1,301 +0,0 @@ -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; - -} \ No newline at end of file diff --git a/output/individual_shifts/timetable.css b/output/individual_shifts/timetable.css new file mode 120000 index 0000000..8c357d2 --- /dev/null +++ b/output/individual_shifts/timetable.css @@ -0,0 +1 @@ +../timetable.css \ No newline at end of file diff --git a/output/individual_shifts/timetable.js b/output/individual_shifts/timetable.js deleted file mode 100644 index 82ca10a..0000000 --- a/output/individual_shifts/timetable.js +++ /dev/null @@ -1,437 +0,0 @@ -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(`
| ${th.dataset.date} | `); + $(`table#main-table td[data-date='${th.dataset.date}'][data-shift='${evt.target.dataset.shift}']`).each((n, td) => { + + //row.append(`${} | `) + row.append($(td).closest("tr").children("td:first").clone()) + + }); + + table.append(row) + }); + $("#shift-timetable-div").append(table); + + }) + + $("#shift-timetable-options").append(button) +}); + //$("table.summary") // $("body").prepend("
| ")); - } - newrows[colToRowIndex].append($(this)); - }); - }); - $this.find("tr").remove(); - $.each(newrows, function(){ - $this.append(this); +$("#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] = $(" | |
| ")); + } + 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) - }); - }); \ No newline at end of file + $this.find("td.rota-day").each((n, el) => { + $(el).text(el.dataset.shift) + }); +}); + +function hsv2rgb(h, s, v) { + // adapted from http://schinckel.net/2012/01/10/hsv-to-rgb-in-javascript/ + var rgb, i, data = []; + if (s === 0) { + rgb = [v, v, v]; + } else { + h = h / 60; + i = Math.floor(h); + data = [v * (1 - s), v * (1 - s * (h - i)), v * (1 - s * (1 - (h - i)))]; + switch (i) { + case 0: + rgb = [v, data[2], data[0]]; + break; + case 1: + rgb = [data[1], v, data[0]]; + break; + case 2: + rgb = [data[0], v, data[2]]; + break; + case 3: + rgb = [data[0], data[1], v]; + break; + case 4: + rgb = [data[2], data[0], v]; + break; + default: + rgb = [v, data[0], data[1]]; + break; + } + } + return '#' + rgb.map(function (x) { + return ("0" + Math.round(x * 255).toString(16)).slice(-2); + }).join(''); +}; \ No newline at end of file diff --git a/rota/shifts.py b/rota/shifts.py index d0a42fa..3de709c 100644 --- a/rota/shifts.py +++ b/rota/shifts.py @@ -26,6 +26,8 @@ from io import StringIO import sys from rich.pretty import pprint +from rich.progress import track +from rich.panel import Panel import logging @@ -177,9 +179,9 @@ class RotaBuilder(object): SHIFT_BOUNDS=SHIFT_BOUNDS, ): - print(f"Start time: {datetime.datetime.now()}") - print(f"Use previous shifts {use_previous_shifts}") - print(f"Use previous shifts (balance_extra) {use_shift_balance_extra}") + console.print(Panel(f"""[white] +{locals()} + """, style="green", title="Generating Rota"), justify="left") self.SHIFT_BOUNDS = SHIFT_BOUNDS self.shifts: List[SingleShift] = [] # type List[SingleShift] @@ -203,7 +205,7 @@ class RotaBuilder(object): self.rota_days_length = len(self.weeks) * 7 self.rota_end_date = self.start_date + datetime.timedelta(self.rota_days_length) - print( + console.print( f"Weeks to rota: {weeks_to_rota} ({self.start_date} - {self.rota_end_date})" ) @@ -961,7 +963,7 @@ class RotaBuilder(object): # - self.model.night_per_site_t2[week, block, site] # == self.model.night_per_site[week, block, site] - 1 # ) - for site in self.sites: + for site in track(self.sites, description="Generating site balance constraints..."): for shift in self.get_shifts_with_constraint("balance_across_groups"): if site in shift.sites: block = shift.name @@ -985,7 +987,7 @@ class RotaBuilder(object): weeks_days = self.get_week_day_combinations() - for week in self.weeks: + for week in track(self.weeks, description="Generating week constraints..."): for shift_name in self.shifts_to_assign_or_force_as_blocks(): shift = self.get_shift_by_name(shift_name) for worker in self.get_workers_for_shift(shift): @@ -1073,6 +1075,8 @@ class RotaBuilder(object): # Most of our constraints apply per worker for worker in self.workers: + console.rule(f"Generate worker constraints: [bold blue]{worker.name}[/bold blue]") + logging.debug(f"Generate worker constraints: {worker.name}") if self.constraint_options["minimise_shift_diffs"]: self.model.constraints.add( @@ -1097,6 +1101,7 @@ class RotaBuilder(object): except ValueError: # Occurs if there are no shifts within a defined block # TODO: test if this breaks (and we should check rathar than except) + logging.debug(f"Failed to constrain max_shifts_per_month for worker: {worker.name}") pass for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]: @@ -1161,7 +1166,7 @@ class RotaBuilder(object): # Balance shifts within required limits (set by balance_offset) # If balance_offset is too restrictive (for the rota length) # no solution will be possible - for shift in self.get_shifts(): + for shift in track(self.get_shifts(), description="Generate shift balance constraints"): if ( worker.site in shift.sites ): # Each site specfies which sites self.workers can fullfill it @@ -1777,7 +1782,7 @@ class RotaBuilder(object): # def get_pre_may(week_days, max_pre) - for n in range(len(weeks_days)): + for n in track(range(len(weeks_days)), description="Generate week/day constraints"): week, day = weeks_days[n] @@ -2358,7 +2363,7 @@ class RotaBuilder(object): self.workers_id_map = {} self.workers_name_map = {} - for worker in self.workers: + for worker in track(self.workers, description="Building workers"): worker.load_rota(self) wid = worker.id if wid in self.workers_id_map: @@ -2377,7 +2382,7 @@ class RotaBuilder(object): self.full_time_equivalent_sites = {} self.workers_at_sites = {} - for site in self.sites: + for site in track(self.sites, description="Generating site workers"): self.workers_at_sites[site] = [w for w in self.workers if w.site == site] self.full_time_equivalent_sites[site] = sum( w.fte_adj for w in self.workers if w.site == site @@ -2920,9 +2925,9 @@ class RotaBuilder(object): for week, day in self.get_week_day_combinations(): d = self.start_date + datetime.timedelta(n) - th_class = "" + th_class = "date" if d in bank_holiday_map: - th_class = "bank-holiday" + th_class = th_class + " bank-holiday" date_row.append( f" | Week {week}: {day} | " @@ -3140,6 +3145,13 @@ class RotaBuilder(object):
|---|