From 9688541a11cdabf012e1700ec60e010e1f58648a Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 11:07:33 +0000 Subject: [PATCH 01/12] Update generate_rota_excel.py New branch Feb 2026 rota --- generate_rota_excel.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 86340d1..429a579 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -30,15 +30,15 @@ "Amy": {"week1": [0,1,2], "week2": [0,1,2]}, # Thurs and Fri off "Jack": {"week1": [0,1,3,4], "week2": [0,1,3,4]}, #Weds off "HannahW": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "": {"week1": [0,1,2], "week2": [0,1,2]}, - "Ian": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Jane": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Karl": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Liam": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Mona": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Nina": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Oliver": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Paula": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "Morgan": {"week1": [0,1,2], "week2": [0,1,2]}, + "Dowan": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "John": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "HannahP": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "Kirsten": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "David": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "Michael": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "Gus": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "MarkHol": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, } #TO HANNAH - ensure everyone is allocated to one site here From 0643a0b1c6172129ceee2269f2d6813fabac10f7 Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 11:10:50 +0000 Subject: [PATCH 02/12] Update generate_rota_excel.py --- generate_rota_excel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 429a579..dbdaa0c 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -14,7 +14,8 @@ # TO HANNAH - the spelling of the person must be consistent throughout. And the number of people must also remain consistent #TO HANNAH - if you can read this line then I have updated the Friday code and saved succesfully as per our messages on 26.11.25 at 17:00 -# Date 08/12 check for commiting changes +# Checking if commit changes here whether commits onto the main branch 08/12 + people = [ "Tim","Frances","MarkHac","Gavin","Amy","Jack", "HannahW","Morgan","Dowan","John","HannahP","Kirsten", From 5243bc0a34790bde24c9e76c0d1dbaba9b9d399a Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 11:25:35 +0000 Subject: [PATCH 03/12] Update generate_rota_excel.py --- generate_rota_excel.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index dbdaa0c..cd09159 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -31,32 +31,31 @@ "Amy": {"week1": [0,1,2], "week2": [0,1,2]}, # Thurs and Fri off "Jack": {"week1": [0,1,3,4], "week2": [0,1,3,4]}, #Weds off "HannahW": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Morgan": {"week1": [0,1,2], "week2": [0,1,2]}, - "Dowan": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "Morgan": {"week1": [0,2,3,4], "week2": [0,2,3,4]}, #Tues off + "Dowan": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, #FOR JACK - Dowan will be having Thurs off from April "John": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "HannahP": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Kirsten": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "David": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Michael": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, - "Gus": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, + "HannahP": {"week1": [2,3,4], "week2": [2,3,4]}, #Mon and Tues off + "Kirsten": {"week1": [0,1,2,3], "week2": [0,1,2,3]}, #Friday off + "David": {"week1": [0,1,2], "week2": [0,1,2,4]}, #Thurs and Friday off - but keeping 0.8 on calls. + "Michael": {"week1": [2,3,4], "week2": [2,3,4]}, #Mon and Tues off + "Gus": {"week1": [0,1,2,3,4], "week2": [0,1,3]}, #Will be 50% academic - allocated as 60% then as academic "MarkHol": {"week1": [0,1,2,3,4], "week2": [0,1,2,3,4]}, } #TO HANNAH - ensure everyone is allocated to one site here # Groups -southmead_group = ["Alice","Bob","Charlie","Diana","Ethan","Fiona","Karl","Liam"] -uhbw_group = ["George","Hannah","Ian","Jane","Mona","Nina","Oliver","Paula"] +southmead_group = ["Tim","Frances","MarckHac","Gavin","Amy","Jack","Kirsten","David", "Michael", "Gus", "MarkHol"] +uhbw_group = ["HannahW","Morgan","Dowan","John","HannahP"] -#TO HANNAH - only those who can't swap need to be added here # Cannot swap weekend site -cannot_swap_weekend_site = ["Alice","Charlie","George","Mona"] +cannot_swap_weekend_site = ["Frances","John"] #TO HANNAH - only those in ACF need to be added here # ACF - needed for reducing their on call frequency -acf_doctors = ["Fiona", "Hannah", "Liam", "Nina"] +acf_doctors = ["HannahP", "Amy", "Gus"] #Jack not taking lower FTE for academic. Gus is 50% academic. # Example unavailable dates From bc325f3ab28a361dc4ad43cef90ce95d8d09152b Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 11:53:25 +0000 Subject: [PATCH 04/12] Update generate_rota_excel.py --- generate_rota_excel.py | 43 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index cd09159..154f9ac 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -45,8 +45,8 @@ #TO HANNAH - ensure everyone is allocated to one site here # Groups -southmead_group = ["Tim","Frances","MarckHac","Gavin","Amy","Jack","Kirsten","David", "Michael", "Gus", "MarkHol"] -uhbw_group = ["HannahW","Morgan","Dowan","John","HannahP"] +southmead_group = ["Tim","Frances","MarckHac","Gavin","Amy","Jack","Kirsten","David", "Michael", "MarkHol"] +uhbw_group = ["HannahW","Morgan","Dowan","John","HannahP", "Gus"] # Cannot swap weekend site @@ -60,22 +60,35 @@ # Example unavailable dates unavailable_dates = { - "Alice": { - ("2026-03-10", "2026-03-14"): "Annual Leave", #TO HANNAH - this is how to do date ranges - "2026-05-15": "Conference" #TO HANNAH - this is how to do single dates. + #"Tim": { + # ("2026-03-10", "2026-03-14"): "Annual Leave", #TO HANNAH - this is how to do date ranges + # "2026-05-15": "Conference" #TO HANNAH - this is how to do single dates. + #}, #Tim - no requested dates + "Frances": { + ("2026-02-13", "2026-02-15"): "UA", #UA = unavailable + ("2026-02-20", "2026-02-22"): "UA", + ("2026-04-11", "2026-04-16"): "UA", + ("2026-05-01", "2026-05-04"): "UA", + ("2026-05-22", "2026-05-31"): "UA", + ("2026-06-13", "2026-06-14"): "UA", + ("2026-06-19", "2026-06-21"): "UA", + ("2026-06-26", "2026-07-21"): "UA", + }, - "Bob": { - "2026-04-22": "Training", - ("2026-09-05", "2026-09-06"): "Weekend Away" + "MarkHac": { + ("2026-03-28", "2026-03-30"): "UA", + ("2026-06-12", "2026-06-14"): "UA", + ("2026-07-03", "2026-07-05"): "UA", }, - "Charlie": { - ("2026-07-01", "2026-07-03"): "Annual Leave" + "Gavin": { + ("2026-03-16", "2026-04-26"): "UA" }, - "Diana": { - ("2026-06-03", "2026-06-07"): "Annual Leave" - }, - "Ethan": { - "2026-02-18": "Study Leave" + "Amy": { + ("2026-03-13", "2026-03-15"): "UA", + ("2026-04-17", "2026-04-21"): "UA", + ("2026-05-22", "2026-06-01"): "UA", + ("2026-07-10", "2026-07-12"): "UA", + ("2026-07-31", "2026-08-02"): "UA" }, "Fiona": { "2026-07-12": "Personal Leave", From 28eee38938aee19ea025f0e422d3a2a3f763e3aa Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 12:32:09 +0000 Subject: [PATCH 05/12] Update generate_rota_excel.py --- generate_rota_excel.py | 71 +++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 154f9ac..2816055 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -72,13 +72,13 @@ ("2026-05-22", "2026-05-31"): "UA", ("2026-06-13", "2026-06-14"): "UA", ("2026-06-19", "2026-06-21"): "UA", - ("2026-06-26", "2026-07-21"): "UA", + ("2026-06-26", "2026-07-21"): "UA" }, "MarkHac": { ("2026-03-28", "2026-03-30"): "UA", ("2026-06-12", "2026-06-14"): "UA", - ("2026-07-03", "2026-07-05"): "UA", + ("2026-07-03", "2026-07-05"): "UA" }, "Gavin": { ("2026-03-16", "2026-04-26"): "UA" @@ -90,31 +90,60 @@ ("2026-07-10", "2026-07-12"): "UA", ("2026-07-31", "2026-08-02"): "UA" }, - "Fiona": { - "2026-07-12": "Personal Leave", - ("2026-11-20", "2026-11-22"): "Conference" + "Jack": { + ("2026-02-02", "2026-02-06"): "UA", + ("2026-02-09", "2026-02-13"): "UA", + ("2026-02-16", "2026-02-20"): "Academic" }, - "George": { - ("2026-05-01", "2026-05-03"): "Annual Leave" + "HannahW": { + ("2026-03-13", "2026-03-17"): "UA", + ("2026-04-10", "2026-04-27"): "UA", + ("2026-05-01", "2026-05-04"): "UA", + ("2026-05-29", "2026-06-08"): "UA", + ("2026-06-18", "2026-06-22"): "UA", + ("2026-07-15", "2026-07-23"): "UA", + ("2026-07-31", "2026-08-05"): "UA" }, - "Hannah": { - ("2026-02-25", "2026-02-28"): "Sick Leave" + "Morgan": { + ("2026-03-16", "2026-06-20"): "UA", + ("2026-04-10", "2026-04-25"): "UA", + ("2026-05-08", "2026-05-10"): "UA" }, - "Ian": { - "2026-06-10": "Training" + "Dowan": { + "2026-02-27": "UA", + ("2026-03-02", "2026-03-13"): "UA" }, - "Jane": { - "2026-04-08": "Conference", - ("2026-12-23", "2026-12-27"): "Christmas Leave" + #"John": { #No unavailability + # "2026-04-08": "Conference", + # ("2026-12-23", "2026-12-27"): "Christmas Leave" + #}, + "HannahP": { + "2026-02-27": "UA", + "2026-03-09": "UA", + ("2026-04-06", "2026-04-27"): "UA" }, - "Karl": { - ("2026-03-20", "2026-03-22"): "Annual Leave" + "Gus": { + ("2026-06-01", "2026-08-05"): "OffRota" }, - "Liam": { - ("2026-08-15", "2026-08-19"): "Annual Leave" - }, - "Mona": { - "2026-10-03": "Wedding" + "Kirsten": { + "2026-02-09": "UA", + ("2026-02-14", "2026-02-22"): "UA", + "2026-02-28": "UA", + "2026-03-12": "UA", + ("2026-03-21", "2026-03-22"): "UA", + ("2026-03-28", "2026-03-29"): "UA", + "2026-04-02": "UA", + ("2026-04-13", "2026-04-21"): "UA", + ("2026-04-27", "2026-04-29"): "UA", + "2026-05-11": "UA", + "2026-05-25": "UA", + ("2026-05-30", "2026-05-31"): "UA", + ("2026-06-02", "2026-06-04"): "UA", + "2026-07-03": "UA", + "2026-07-07": "UA", + "2026-07-14": "UA", + "2026-07-20": "UA", + }, "Nina": { ("2026-05-10", "2026-05-12"): "Sick Leave" From 2e340e35c41d0ea1519aca7ec472d432035b2661 Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 12:40:55 +0000 Subject: [PATCH 06/12] Update generate_rota_excel.py --- generate_rota_excel.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 2816055..f3bfb00 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -143,16 +143,32 @@ "2026-07-07": "UA", "2026-07-14": "UA", "2026-07-20": "UA", - + ("2026-07-25", "2026-07-30"): "UA" }, - "Nina": { - ("2026-05-10", "2026-05-12"): "Sick Leave" + "David": { + ("2026-02-14", "2026-02-22"): "UA", + ("2026-03-09", "2026-03-13"): "UA", + ("2026-04-10", "2026-04-19"): "UA", + ("2026-05-23", "2026-05-31"): "UA", + ("2026-06-30", "2026-07-02"): "UA" }, - "Oliver": { - ("2026-01-02", "2026-01-04"): "New Year Leave" + "Michael": { + ("2026-02-06", "2026-02-08"): "UA", + "2026-02-21": "UA", + ("2026-03-05", "2026-03-08"): "UA", + ("2026-03-13", "2026-03-15"): "UA", + ("2026-03-21", "2026-03-22"): "UA", + ("2026-04-16", "2026-04-19"): "UA", + ("2026-04-24", "2026-04-26"): "UA", + ("2026-05-23", "2026-05-31"): "UA" }, - "Paula": { - ("2026-09-14", "2026-09-18"): "Annual Leave" + "MarkHol": { + ("2026-04-03", "2026-04-06"): "UA", + ("2026-05-01", "2026-05-04"): "UA", + ("2026-05-21", "2026-05-31"): "UA", + ("2026-06-19", "2026-06-28"): "UA", + ("2026-06-23", "2026-06-26"): "UA" + }, } @@ -187,14 +203,10 @@ def expand_unavailable(unavailable): #TO HANNAH - update this to reflect the days BH cover is needed bank_holidays = { - "2026-01-01": "New Year's Day", "2026-04-03": "Good Friday", "2026-04-06": "Easter Monday", "2026-05-04": "Early May Bank Holiday", - "2026-05-25": "Spring Bank Holiday", - "2026-08-31": "Summer Bank Holiday", - "2026-12-25": "Christmas Day", - "2026-12-28": "Boxing Day (substitute)" + "2026-05-25": "Spring Bank Holiday" } #TO HANNAH - this is start/end date From 0129646a5365f8b73e06ab771a1a457f6dde0069 Mon Sep 17 00:00:00 2001 From: Hannahalise Date: Mon, 8 Dec 2025 13:47:26 +0000 Subject: [PATCH 07/12] Update generate_rota_excel.py --- generate_rota_excel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index f3bfb00..b7fe731 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -45,7 +45,7 @@ #TO HANNAH - ensure everyone is allocated to one site here # Groups -southmead_group = ["Tim","Frances","MarckHac","Gavin","Amy","Jack","Kirsten","David", "Michael", "MarkHol"] +southmead_group = ["Tim","Frances","MarkHac","Gavin","Amy","Jack","Kirsten","David", "Michael", "MarkHol"] uhbw_group = ["HannahW","Morgan","Dowan","John","HannahP", "Gus"] From 993226ef246c90ed8ba9f91151c31737a18f2674 Mon Sep 17 00:00:00 2001 From: JStanleyBris Date: Thu, 11 Dec 2025 17:53:32 +0000 Subject: [PATCH 08/12] on_call_protection --- generate_rota_excel.py | 75 +++++++++++++++++++---------- on_call_rota_16people_colored.xlsx | Bin 8833 -> 10071 bytes 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index b7fe731..e3d060b 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -91,8 +91,8 @@ ("2026-07-31", "2026-08-02"): "UA" }, "Jack": { - ("2026-02-02", "2026-02-06"): "UA", - ("2026-02-09", "2026-02-13"): "UA", + ("2026-02-02", "2026-02-08"): "UA", + ("2026-02-09", "2026-02-15"): "UA", ("2026-02-16", "2026-02-20"): "Academic" }, "HannahW": { @@ -172,7 +172,7 @@ }, } -#Define a function to handle the dat ranges: +#Define a function to handle the date ranges: def expand_unavailable(unavailable): expanded = {} @@ -237,6 +237,25 @@ def expand_unavailable(unavailable): total_fte = sum(fte.values()) +#------------------------- +# Define on call protection +#------------------------ + +#Replace with on_call_protection - blanket avoid on calls 4 days before or after any on call + +oncall_protection = defaultdict(set) + +def protect_around_oncall(person, date): + # Protect 4 days BEFORE + for i in range(1, 5): + d_before = date - timedelta(days=i) + oncall_protection[person].add(d_before.strftime("%Y-%m-%d")) + # Protect 4 days AFTER + for i in range(1, 5): + d_after = date + timedelta(days=i) + oncall_protection[person].add(d_after.strftime("%Y-%m-%d")) + + # ------------------------------- # 4️⃣ Weekend allocation # ------------------------------- @@ -275,22 +294,11 @@ def expand_unavailable(unavailable): weekend_rota[sun.strftime("%Y-%m-%d")] = {"Southmead": chosen_sm, "UHBW": chosen_uh} weekend_assigned_southmead[chosen_sm] += 1 weekend_assigned_uhbw[chosen_uh] += 1 - -# Weekend protection - no on call in the week before/after. I think this protects around BH also but can check -weekend_protection = defaultdict(set) -for sat, sun in weekend_blocks: - weekend_dates = [sat, sun] - for i in range(1,5): - d_before = sat - timedelta(days=i) - if d_before.weekday() < 5: - weekend_dates.append(d_before) - for i in range(1,6): - d_after = sun + timedelta(days=i) - if d_after.weekday() < 5: - weekend_dates.append(d_after) - for p in [weekend_rota[sat.strftime("%Y-%m-%d")]["Southmead"], - weekend_rota[sat.strftime("%Y-%m-%d")]["UHBW"]]: - weekend_protection[p].update(d.strftime("%Y-%m-%d") for d in weekend_dates) + + protect_around_oncall(chosen_sm, sat) + protect_around_oncall(chosen_sm, sun) + protect_around_oncall(chosen_uh, sat) + protect_around_oncall(chosen_uh, sun) # ------------------------------- @@ -308,7 +316,9 @@ def expand_unavailable(unavailable): # Southmead available_sm = [p for p in southmead_group if bh_date_str not in unavailable.get(p, {}) - and bh_date_str not in weekend_protection.get(p,set())] + and bh_date_str not in oncall_protection.get(p, set())] + + # Use tie-breaker: fewest BH shifts / FTE, then fewest weekend shifts / FTE, then random to avoid systematic bias chosen_sm = min( available_sm, @@ -319,7 +329,8 @@ def expand_unavailable(unavailable): ) # UHBW - available_uh = [p for p in uhbw_group if bh_date_str not in unavailable.get(p, {})] + available_uh = [p for p in uhbw_group if bh_date_str not in unavailable.get(p, {}) + and bh_date_str not in oncall_protection.get(p, set())] chosen_uh = min( available_uh, key=lambda x: (bank_holiday_assigned_uhbw[x]/fte[x], @@ -332,6 +343,10 @@ def expand_unavailable(unavailable): bank_holiday_assigned_sm[chosen_sm] += 1 bank_holiday_assigned_uhbw[chosen_uh] += 1 + protect_around_oncall(chosen_sm, bh_date) + protect_around_oncall(chosen_uh, bh_date) + + #------------- # 5.7 Friday allocation @@ -358,7 +373,7 @@ def expand_unavailable(unavailable): if ( assigned_friday_counts[p] < target_fridays[p] #\Anyone can do Friday even if not in working pattern and date_str not in unavailable.get(p,{}) - and date_str not in weekend_protection.get(p,set()) + and date_str not in oncall_protection.get(p,set()) ) ] @@ -366,7 +381,7 @@ def expand_unavailable(unavailable): working_people = [ p for p in people if date_str not in unavailable.get(p,{}) #anyone can do Friday even if not working pattern - and date_str not in weekend_protection.get(p,set()) + and date_str not in oncall_protection.get(p,set()) ] available_people = sorted(working_people, key=lambda x: assigned_friday_counts[x]/fte[x]) @@ -381,6 +396,9 @@ def expand_unavailable(unavailable): rota[date_str] = chosen assigned_friday_counts[chosen] += 1 + protect_around_oncall(chosen, d) + + # ------------------------------- # 5️⃣ Weekday allocation (two-week rolling) @@ -406,7 +424,7 @@ def expand_unavailable(unavailable): d.weekday() in work_schedule[p][week_key] and assigned_weekday_counts[p] < target_shifts[p] and date_str not in unavailable.get(p,{}) - and date_str not in weekend_protection.get(p,set()) + and date_str not in oncall_protection.get(p,set()) ) ] @@ -415,9 +433,14 @@ def expand_unavailable(unavailable): p for p in people if d.weekday() in work_schedule[p][week_key] and date_str not in unavailable.get(p,{}) - and date_str not in weekend_protection.get(p,set()) + and date_str not in oncall_protection.get(p,set()) ] available_people = sorted(working_people, key=lambda x: assigned_weekday_counts[x]/fte[x]) + + if not available_people: # Extreme case: no one available, mark date - HANNAH - this was causing a crash for some reason + rota[date_str] = "No One Available" + print(f"⚠️ No one available for {date_str} (weekday)") + continue chosen = min(available_people, key=lambda x: ( @@ -431,6 +454,8 @@ def expand_unavailable(unavailable): rota[date_str] = chosen assigned_weekday_counts[chosen] += 1 + protect_around_oncall(chosen, d) + # ------------------------------- # 6️⃣ Prepare DataFrame # ------------------------------- diff --git a/on_call_rota_16people_colored.xlsx b/on_call_rota_16people_colored.xlsx index 820d71f2cb234075a6ce80fa378f8bfa58d14a51..930ca21e8601591afac9bfc3a15a2057507cfe44 100644 GIT binary patch delta 6865 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 From 48bb746f5d2b4b211f754cb47305933dd8b3c4ce Mon Sep 17 00:00:00 2001 From: JStanleyBris Date: Wed, 17 Dec 2025 07:57:46 +0000 Subject: [PATCH 09/12] avoid_double_weekend --- generate_rota_excel.py | 18 ++++++++++++++++-- on_call_rota_16people_colored.xlsx | Bin 10071 -> 10050 bytes 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index e3d060b..8ffb718 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -255,6 +255,13 @@ def protect_around_oncall(person, date): d_after = date + timedelta(days=i) oncall_protection[person].add(d_after.strftime("%Y-%m-%d")) +#Avoid two weekends in a row + +last_weekend_assigned = {} # person -> Saturday date of last weekend worked + +def worked_last_weekend(person, current_saturday): + return last_weekend_assigned.get(person) == (current_saturday - timedelta(days=7)) + # ------------------------------- # 4️⃣ Weekend allocation @@ -269,9 +276,12 @@ def protect_around_oncall(person, date): for sat, sun in weekend_blocks: available_sm = [p for p in southmead_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) + and not worked_last_weekend(p, sat)] + available_uh = [p for p in uhbw_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) + and not worked_last_weekend(p, sat)] if not available_sm: available_sm = [p for p in uhbw_group if p not in cannot_swap_weekend_site and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] @@ -300,6 +310,10 @@ def protect_around_oncall(person, date): protect_around_oncall(chosen_uh, sat) protect_around_oncall(chosen_uh, sun) + last_weekend_assigned[chosen_sm] = sat + last_weekend_assigned[chosen_uh] = sat + + # ------------------------------- # 5a️⃣ Bank holiday allocation (separate from weekdays) diff --git a/on_call_rota_16people_colored.xlsx b/on_call_rota_16people_colored.xlsx index 930ca21e8601591afac9bfc3a15a2057507cfe44..eabb3e46a20e2f1f64363d1a24780e56bff9b8bd 100644 GIT binary patch delta 6007 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 From ddcd1ac1108488f03447e35895414f7b6dd78c79 Mon Sep 17 00:00:00 2001 From: JStanleyBris Date: Thu, 18 Dec 2025 16:14:11 +0000 Subject: [PATCH 10/12] updateweekends --- generate_rota_excel.py | 56 +++++++++++++++++++++++------ on_call_rota_16people_colored.xlsx | Bin 10050 -> 10050 bytes 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 8ffb718..c5329be 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -262,6 +262,11 @@ def protect_around_oncall(person, date): def worked_last_weekend(person, current_saturday): return last_weekend_assigned.get(person) == (current_saturday - timedelta(days=7)) +def apply_weekend_exclusion(candidates, sat): + eligible = [p for p in candidates if not worked_last_weekend(p, sat)] + return eligible if eligible else candidates + +weekend_assigned = defaultdict(int) # ------------------------------- # 4️⃣ Weekend allocation @@ -276,35 +281,53 @@ def worked_last_weekend(person, current_saturday): for sat, sun in weekend_blocks: available_sm = [p for p in southmead_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) - and not worked_last_weekend(p, sat)] - + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) + for d in [sat,sun]) + ] + +#No consecutive weekends + available_sm = apply_weekend_exclusion(available_sm, sat) + + available_uh = [p for p in uhbw_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) - and not worked_last_weekend(p, sat)] + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) + for d in [sat,sun]) + ] + +#No consecutive weekends + available_uh = apply_weekend_exclusion(available_uh, sat) + + if not available_sm: available_sm = [p for p in uhbw_group if p not in cannot_swap_weekend_site and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] + available_sm = apply_weekend_exclusion(available_sm, sat) + + if not available_uh: available_uh = [p for p in southmead_group if p not in cannot_swap_weekend_site and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] + available_uh = apply_weekend_exclusion(available_uh, sat) + chosen_sm = min( available_sm, key=lambda x: ( - weekend_assigned_southmead[x]/fte[x], + weekend_assigned[x]/fte[x], random.random()) #If tied make it random so that those earliest in an alphabetical list are not disadvantaged ) + + available_uh = [p for p in available_uh if p != chosen_sm] + chosen_uh = min( available_uh, key=lambda x: ( - weekend_assigned_uhbw[x]/fte[x], + weekend_assigned[x]/fte[x], random.random() ) ) weekend_rota[sat.strftime("%Y-%m-%d")] = {"Southmead": chosen_sm, "UHBW": chosen_uh} weekend_rota[sun.strftime("%Y-%m-%d")] = {"Southmead": chosen_sm, "UHBW": chosen_uh} - weekend_assigned_southmead[chosen_sm] += 1 - weekend_assigned_uhbw[chosen_uh] += 1 + protect_around_oncall(chosen_sm, sat) protect_around_oncall(chosen_sm, sun) protect_around_oncall(chosen_uh, sat) @@ -313,6 +336,14 @@ def worked_last_weekend(person, current_saturday): last_weekend_assigned[chosen_sm] = sat last_weekend_assigned[chosen_uh] = sat + weekend_assigned[chosen_sm] += 1 + weekend_assigned[chosen_uh] += 1 + + weekend_assigned_southmead[chosen_sm] += 1 + weekend_assigned_uhbw[chosen_uh] += 1 + + + # ------------------------------- @@ -343,8 +374,11 @@ def worked_last_weekend(person, current_saturday): ) # UHBW - available_uh = [p for p in uhbw_group if bh_date_str not in unavailable.get(p, {}) - and bh_date_str not in oncall_protection.get(p, set())] + + available_uh = [p for p in uhbw_group + if bh_date_str not in unavailable.get(p, {}) + and bh_date_str not in oncall_protection.get(p, set()) + and p != chosen_sm] chosen_uh = min( available_uh, key=lambda x: (bank_holiday_assigned_uhbw[x]/fte[x], diff --git a/on_call_rota_16people_colored.xlsx b/on_call_rota_16people_colored.xlsx index eabb3e46a20e2f1f64363d1a24780e56bff9b8bd..68254c7b5b2cdadcf181ebba3b044b95621ba629 100644 GIT binary patch delta 423 zcmX@)cgT-7z?+#xgn@y9gTc0O(nQ_^96%~M!udn~#H-r%nU_0u#W>7ya7q&Dh<;KL z?K0V|GjR9z<+{=AP147I{Qq+^EQ?3zwuk>lUXhzYQD4@5^h~;u=Kf59QAqCO%MhLx z`9rnRbDuqwA@Nv`HeGK*czpZo>(?&u*5_z zdvR$#Z?Mti+D*n+OY37V>Z<46@Hp`L@!`)~YVvB6-cB?Mm7XHuSuB0EBGBTiFYo=X zua)1z8_y)oQ1~g5F*|nB^rvs{zOxGsja*w4_%vy=aCYRq6IY+*mi&FOpDQ-|&Aoqz zD+E_a)%~6Qiv`8&(KBP_C=0MLFw9YBU=RidNJWnR=6{S@Y|Ov_*{scBBMN3@$%-?B z7?WERo`D5&6{8rz0;iN7fd$qpdqV{Ns#wDVbFzb~IapPdsvVf#t?CS>8PzPnw2hh> Sm@ZYb0@It+T*34&H4gy6eXpqi delta 423 zcmX@)cgT-7z?+#xgn@y9gW;v!#EHBIIDk}iaJ>G_iC4AjSMs>V>dm%jY&oJLu>Hix zZ5h7Os%~%Z&D)kE%aJGZ`2Xkh=#>||bC_-0uLUP~7hc@Gt5fK&m8DTNr@%)KzvDRy zvlQ=aKbR>u?;%6nV^4XGE}Q+#?rVC}co!Rd-E6p1gZG@*f5BIt2?8tB+R?Kv_3_pYY= zqpys9uXac?^qEleZHwQ!ldp?5-?dd)b)wry)Kx2Nca7@lpswHFFMeJ8U#(>A#_Hev zk3$z|{rH;yN*KlK(KBP_C=0MLFw9YBU=RidNJWnR=6{S@Y|Ov_*{scBBMN3@$%-?B z7?WERo`D5&6{8rz0;iN7fd$qpdqV{Ns#wDVbFzb~IapPdsvVf#t?CS>8PzPnw2hh> Sm@ZYb0@It+T*34&H4gwt(X)*J From a0049a8b9b5e63469810ceae04a323b0f978716f Mon Sep 17 00:00:00 2001 From: JStanleyBris Date: Thu, 18 Dec 2025 19:14:32 +0000 Subject: [PATCH 11/12] update weekend --- generate_rota_excel.py | 45 ++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index c5329be..29308d9 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -262,10 +262,6 @@ def protect_around_oncall(person, date): def worked_last_weekend(person, current_saturday): return last_weekend_assigned.get(person) == (current_saturday - timedelta(days=7)) -def apply_weekend_exclusion(candidates, sat): - eligible = [p for p in candidates if not worked_last_weekend(p, sat)] - return eligible if eligible else candidates - weekend_assigned = defaultdict(int) # ------------------------------- @@ -280,34 +276,30 @@ def apply_weekend_exclusion(candidates, sat): weekend_rota = {} for sat, sun in weekend_blocks: - available_sm = [p for p in southmead_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) - for d in [sat,sun]) - ] - -#No consecutive weekends - available_sm = apply_weekend_exclusion(available_sm, sat) - - - available_uh = [p for p in uhbw_group - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) - for d in [sat,sun]) - ] -#No consecutive weekends - available_uh = apply_weekend_exclusion(available_uh, sat) + globally_eligible = [ + p for p in people + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat, sun]) + and last_weekend_assigned.get(p) != sat - timedelta(days=7) + ] + + available_sm = [p for p in globally_eligible if p in southmead_group] + available_uh = [p for p in globally_eligible if p in uhbw_group] if not available_sm: - available_sm = [p for p in uhbw_group if p not in cannot_swap_weekend_site - and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] - available_sm = apply_weekend_exclusion(available_sm, sat) + available_sm = [p for p in uhbw_group if + p not in cannot_swap_weekend_site + and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) + and last_weekend_assigned.get(p) != sat - timedelta(days=7)] + if not available_uh: - available_uh = [p for p in southmead_group if p not in cannot_swap_weekend_site - and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun])] - available_uh = apply_weekend_exclusion(available_uh, sat) + available_uh = [p for p in southmead_group if + p not in cannot_swap_weekend_site + and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) + and last_weekend_assigned.get(p) != sat - timedelta(days=7)] chosen_sm = min( available_sm, key=lambda x: ( @@ -343,9 +335,6 @@ def apply_weekend_exclusion(candidates, sat): weekend_assigned_uhbw[chosen_uh] += 1 - - - # ------------------------------- # 5a️⃣ Bank holiday allocation (separate from weekdays) # ------------------------------- From 093090be7ff1b2faa8b87247b847d82df5cae66f Mon Sep 17 00:00:00 2001 From: JStanleyBris Date: Thu, 18 Dec 2025 19:42:52 +0000 Subject: [PATCH 12/12] Update weekend --- generate_rota_excel.py | 94 ++++++++++++----------------- on_call_rota_16people_colored.xlsx | Bin 10050 -> 10054 bytes 2 files changed, 37 insertions(+), 57 deletions(-) diff --git a/generate_rota_excel.py b/generate_rota_excel.py index 29308d9..9934c38 100644 --- a/generate_rota_excel.py +++ b/generate_rota_excel.py @@ -265,74 +265,54 @@ def worked_last_weekend(person, current_saturday): weekend_assigned = defaultdict(int) # ------------------------------- -# 4️⃣ Weekend allocation +# 4️⃣ Weekend allocation (improved) # ------------------------------- -weekend_blocks = [(d, d + timedelta(days=1)) for d in all_dates if d.weekday()==5 and (d+timedelta(days=1))<=end_date] -total_weekends = len(weekend_blocks)*2 -target_weekends = {p: total_weekends*(fte[p]/total_fte) for p in people} +weekend_blocks = [(d, d + timedelta(days=1)) for d in all_dates if d.weekday() == 5 and (d + timedelta(days=1)) <= end_date] +total_weekends = len(weekend_blocks) * 2 +target_weekends = {p: total_weekends * (fte[p] / total_fte) for p in people} + +weekend_assigned = defaultdict(int) +last_weekend_assigned = {} -weekend_assigned_southmead = defaultdict(int) -weekend_assigned_uhbw = defaultdict(int) weekend_rota = {} for sat, sun in weekend_blocks: - globally_eligible = [ - p for p in people - if all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat, sun]) - and last_weekend_assigned.get(p) != sat - timedelta(days=7) + # Recompute eligibility dynamically for this weekend + eligible_sm = [ + p for p in southmead_group + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p, {}) for d in [sat, sun]) + and last_weekend_assigned.get(p) != sat - timedelta(days=7) + and weekend_assigned[p] < target_weekends[p] ] - - available_sm = [p for p in globally_eligible if p in southmead_group] - available_uh = [p for p in globally_eligible if p in uhbw_group] + eligible_uh = [ + p for p in uhbw_group + if all(d.strftime("%Y-%m-%d") not in unavailable.get(p, {}) for d in [sat, sun]) + and last_weekend_assigned.get(p) != sat - timedelta(days=7) + and weekend_assigned[p] < target_weekends[p] + ] - if not available_sm: - available_sm = [p for p in uhbw_group if - p not in cannot_swap_weekend_site - and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) - and last_weekend_assigned.get(p) != sat - timedelta(days=7)] - - - - if not available_uh: - available_uh = [p for p in southmead_group if - p not in cannot_swap_weekend_site - and all(d.strftime("%Y-%m-%d") not in unavailable.get(p,{}) for d in [sat,sun]) - and last_weekend_assigned.get(p) != sat - timedelta(days=7)] + # Fallback: use cannot_swap_weekend_site if no eligible people + if not eligible_sm: + eligible_sm = [p for p in uhbw_group if p not in cannot_swap_weekend_site] + if not eligible_uh: + eligible_uh = [p for p in southmead_group if p not in cannot_swap_weekend_site] - chosen_sm = min( - available_sm, key=lambda x: ( - weekend_assigned[x]/fte[x], - random.random()) #If tied make it random so that those earliest in an alphabetical list are not disadvantaged - ) - - available_uh = [p for p in available_uh if p != chosen_sm] - - chosen_uh = min( - available_uh, key=lambda x: ( - weekend_assigned[x]/fte[x], - random.random() - ) - ) + # Choose people with lowest ratio of assigned weekends to FTE, break ties randomly + chosen_sm = min(eligible_sm, key=lambda p: (weekend_assigned[p] / fte[p], random.random())) + eligible_uh = [p for p in eligible_uh if p != chosen_sm] # prevent same person in both sites + chosen_uh = min(eligible_uh, key=lambda p: (weekend_assigned[p] / fte[p], random.random())) + # Assign to rota weekend_rota[sat.strftime("%Y-%m-%d")] = {"Southmead": chosen_sm, "UHBW": chosen_uh} weekend_rota[sun.strftime("%Y-%m-%d")] = {"Southmead": chosen_sm, "UHBW": chosen_uh} - - - protect_around_oncall(chosen_sm, sat) - protect_around_oncall(chosen_sm, sun) - protect_around_oncall(chosen_uh, sat) - protect_around_oncall(chosen_uh, sun) - - last_weekend_assigned[chosen_sm] = sat - last_weekend_assigned[chosen_uh] = sat + # Update tracking weekend_assigned[chosen_sm] += 1 weekend_assigned[chosen_uh] += 1 - - weekend_assigned_southmead[chosen_sm] += 1 - weekend_assigned_uhbw[chosen_uh] += 1 + last_weekend_assigned[chosen_sm] = sat + last_weekend_assigned[chosen_uh] = sat # ------------------------------- @@ -357,7 +337,7 @@ def worked_last_weekend(person, current_saturday): chosen_sm = min( available_sm, key=lambda x: (bank_holiday_assigned_sm[x]/fte[x], - (weekend_assigned_southmead[x] + weekend_assigned_uhbw.get(x,0))/fte[x], + weekend_assigned[x]/fte[x], random.random() ) ) @@ -371,7 +351,7 @@ def worked_last_weekend(person, current_saturday): chosen_uh = min( available_uh, key=lambda x: (bank_holiday_assigned_uhbw[x]/fte[x], - (weekend_assigned_uhbw[x] + weekend_assigned_southmead.get(x,0))/fte[x], + weekend_assigned[x]/fte[x], random.random() ) ) @@ -425,7 +405,7 @@ def worked_last_weekend(person, current_saturday): chosen = min(available_people, key=lambda x: (assigned_friday_counts[x]/fte[x], (bank_holiday_assigned_uhbw[x] + bank_holiday_assigned_sm.get(x,0))/fte[x], - (weekend_assigned_uhbw[x] + weekend_assigned_southmead.get(x,0))/fte[x], + weekend_assigned[x]/fte[x], random.random() ) ) @@ -484,7 +464,7 @@ def worked_last_weekend(person, current_saturday): assigned_weekday_counts[x]/fte[x], assigned_friday_counts[x]/fte[x], (bank_holiday_assigned_uhbw[x] + bank_holiday_assigned_sm.get(x,0))/fte[x], - (weekend_assigned_uhbw[x] + weekend_assigned_southmead.get(x,0))/fte[x], + weekend_assigned[x]/fte[x], random.random() ) ) @@ -581,7 +561,7 @@ def worked_last_weekend(person, current_saturday): fte_pct = round(fte[p] * 100) acf_label = "ACF" if p in acf_doctors else "" - total_weekends_p = weekend_assigned_southmead.get(p, 0) + weekend_assigned_uhbw.get(p, 0) + total_weekends_p = weekend_assigned.get(p, 0) total_bh_p = bank_holiday_assigned_sm.get(p, 0) + bank_holiday_assigned_uhbw.get(p, 0) total_friday_p = assigned_friday_counts.get(p, 0) total_weekday_p = assigned_weekday_counts.get(p, 0) diff --git a/on_call_rota_16people_colored.xlsx b/on_call_rota_16people_colored.xlsx index 68254c7b5b2cdadcf181ebba3b044b95621ba629..ff85622da6992063a70a5e9dbeaca892315dafbe 100644 GIT binary patch delta 5967 zcmZ8lcQjmG*Vm0`qmMqLlVJ3k(V`PX5{VK<4^bvT^qXO{C<&7gBt#@pqlKu!;L(NA zONQv8L>OJfH=cKW>wDLC|G0PUdv^Jq-`V@@wRYR}&g%@8rj%6dWMpI@GSfGy3^+Q| zH)(d|JD8j6spA5GxkWpiA@;oYMc9TIHI-EceLS0ykr@Rlk=3un6Y&C4b9z|qwxR96 z2x)mHIAh}SP7yY5eTjvw=9NwKMtHhd*;I~%$&&{MYXLHbZO`VFMGEeRf8@XBGrP~E zb90PCRm4Z>{ws{WebQ!OwYyd4v(W(xZ!(R5HAzZYwbyssfTi7B#z$4p^?bRCw@<6e zJ$%2%w{I-A6Pv>ECuY;UuEmuGS(q%{8zcHV@Bqf}%gS9-j?~p9sHpdP9ZDLavin((auNwaT z_w@4Rd)SS6a?oahm}Cml>BBONY`5vOaw;;S!R}L#;dxjc5FQGI*Um(oZY?eST=Ctn z2tNrV`s~-8YvroU#4Y=tPi$pi3;brrb`Qux>$G}kD8qIu?6m{{Ao%f7?eRf--| z>;$DWCvY_a2)^Tceijkfa=i0*lCSo>;to(h!~b`FslCy!PqxIOgpfOT@Z`+V;vVpA z>jb%fR6VOH;A4|JCkQCENSoc_gVSc8m)dCML}>|LjUyhq6Zem0^JfmbqF3j<0$UoU zuAV<%{;PFfdff&%JRH>WK5zVUxZEWWRy)}JdvNPni>j;BIsM?K|_fZ_c^@??@2f*BlvseY}7W zbPh~}0gcRIL+gYd-}5ld^RuND>V1v46BP>e4>~}ppwL@DfhVBS0Wn*eBh>ncNA<36 z%ROm7p;mgH;QBuvXArc*aK$s`$`|(dx1k|Kr6?YJVaf91;_k0M_5hRfY%tL@z7lN?NQ?WNn)}OO^z3H z9TfO1F<}6AzN;Qa^HV@1zz|`~wkZKN3}IAw7Mq}kxRd=2Dsm!G?z&>_a)Jo8sAuS^ zH+%6&2U4l)kZtydDH8cAbMMU@ z_ZSBzyy@<2nZS7E)+T$@0-wEmGyGEIq&e_j92M(=66;-nGRQu%0qeOwbTwJl2)sl< zdp!4=9e6LE5bKg4*2@Y@vbTUe?Ov{_T(=i`49zH7A=rlsd8vwdy)$EPD;yA^VkzR| zm5zC%`xyFCiq%^)QHd{k;GH688_4~+iJ_xOq}Mb(e`>ia-cQ9;d3%L`^=_}+xB~Q+ z!IGQ=SKSMzRh4B`_rJ*zH8mE4gE*I`z%)}`A<9G4r%@xc8&g4@m0MSOzrYx(p7oW= z3Vw@X73qrm9L&gy`+Y-e;N&)k!l1W8%7nYsJ$mG5gL=;iC%wX;l|o9syH(01WQR~t z*8T@fW&wuVknXdDC9wPib zn*2SU>Q&70&8{#=>!VYOu2Ya`~K_81!^`-wt-+8uCl}?OB%a9hHEoU<1z_jfeqR)d|JUXoTKWo#7m*O->9Z*!9TiQ$PuuNU)n3`8+F zr4v@&cd5y)D}iNV_40-ana7$_O&SYJk^wlYwcrW%Hc%TJkXaI~&o!3pP-^w9%Q5ES z!=^kohcU3mqL9_NEE3C6_Wo3Avq~{=BZv%C#_2(MA=YNLd@W;U;Y;B-O z6uA!cG9X0!xMmk3t*Ixg8Y{5vs0$Hc-~zKU?p6)4?x|0asLpxI#i#S^kQ0`4C*Oh| z^=5zhgOKbdiRq5UAMuZyYC%Zi8s+>YS($T?R6oQYr1BdikZjnZoWurG~Ve4YL;GkH_2WiF&uhZs<&E}j$?yUFdHats4Ii%-D23#^l2Q z5={Ma6Nj2$`-4)>3>=C-<(|7$C>_$DmKPKWztD_A1q}%e6yM?ngwj&>j1md zWKyHFcoAAUr#fBnzH}>y8e{cg=Z3jmp2lxEwoD7`Kw})k)xH|VKIztY8d5rhQovdq z^H98(bkhYtd6cHRmr}n1zCD4jYp2}0ZCCKcv1Tk(%FyqW^gtuR z^(wAf5qG6^lNy7gK4j1p%I(NqmbirDBR_b6AAV3t7R?SexDp3ulf8C#3$Ms`c2vPI~Jn5aOL_1HRYdz!kvX4DUvcA0UfNC?y6qYlK>k$k1;K| ze5(A#V`?Il#z&2YUr+`OH{z7}v|I6fE96dh$j1%UkGz5uXt)_CRHRuWBlDT-O3lAG zdiy=(i7^|1U42lb;E%cp?eGq&Huy{RXvptB`YXbjA<_!D-Tejf#LWICRK#lan>mP@ zuM2x_JsMv1h}`N28z%90u98s?|ubKp|GXAW#zQ0Wdh(_-e|bJM?f#O zh<;+*wV1xFaPeSk2L%VU3zvc^W~r)tn){p5Gc8DMEc3X3-=|QmW#I95Wi3Q6K!dz? zm&3W0@@3%M|53wr9j~^DuypM9w}%NMN@eul^h=oS9Er zPR$%7WhZlY1~(v8)(;kZj)AX>Sbx}Ry?v%$@p8`E=6jAh;s}I1BK>SSN7?Q-o>%Vj zwmqEzcdl~77)Dx68jE;zJbuiGcz5>p7xb(Ul1|NU{o_--Fp>t2O_(km&QUJUQ+dul zwzJdg_f^VwgCn;H1An?X3Z_Ot*JlK<{DC72D)%{R0pmmq(xszg=mnxEF`4^n%3%+_ zzU^QFXCOuQ(g%_E8-L+S%~W|A^vyqYvoIUgQeU_>pij5L0845Zct5;vEPiVc@pEY8 z1ZKWr3TGi%DP!|~-T0@cV)esQy}^Pw#atO&02DKy>I2eYbDdz1q8lROVFQMyaApwl z8c^g?=)*TQ#Cy!aAIhWt{=O$24LPaTv%;jBXPHlDf2r;<1Q;m{vG!ByLX>tjZ(1Db zJl^SKT_G|gW0&M&q zvPqyBR2w+_5?uIdY!gDqS80K;TkbM3WnOt_LWIA`68E&pxc6IF_x_Pfxrvl(5Sk98 zA1xQ<7u8Oo?K~ZZ#C^Dv#3GpPBb+Wc7{P{WrQyzNVO0#3OR#4;yJs5M4EH_=Wt=~|h@=O`pl$3>}YnTEchB@)A6-TL{ zd_3<1Ev_nox}iw1>RkFQuAc{f0=}(V+%f;KDt?nodGYpTkw6Y_P4mKspW-F4nzsxs+kTO9ubCG5H0sVVv&+z- zO&vdWN@X(g;F#$A*{-wY?$;0BDU*%cPT#?Nk632RfpuO)^puEyby8b2`BvnT|Y&)y#Z^j)+{$vPhPBJK_9?^vu zz8O=@Z1NzsX)B3JYxE*D$C%MGP)O z;*LS>#XZ%)%qyBkljC8kFh+wnV^Jv4B;otdn?F&VkVR5CNWb!HQ+-Io z(mqsN;A5aPdCMd=DuN-O#v5nJ-f1?*|rzrZ55wUqx>@k zGwnnE_|FRMv(SEjh0m(-lp?7k?90X(TYn857SK*GzQ50yZ|;qk;Oel-mjzQj-Kzi6 zHR5C=c}y29C+y*kpXBODwV{R7eDq1a`pyDO+KOdsK6j06VrNH%y>!iK2Hp6$?kUX- zfZe#U&GJcmZ5(b=%W0mk!-sE4r6Z(Z4r+!XAB>tbFb3%()CkY-LR-Bd)b|p_)}bj4 z?Bh!f>FssY>vl}&du2$fN76i{(W>DJr9FX$0$tLUf+(mqNt+aG8Jbeasm*Yd#xeaa zBHF>qF>7gV+gzRU*MP)#TSZD*6!0p@K3mtW%CAC8UaQzt*X(VAH^j<5afM|0LPLc| zZdzII9Zyg;jKGc2-W`W$)p8C@An|r)8XpZlxy(4lxkd#L?a(=&kW@a>JOk0HQ3l;q zUWNwwmNOr%uWil*tc!i5;J0rF&D$z6(V{x!9kX@s3gvz=+MiXktXE0``akJgJA-L6 zj;Gmsz4;2HZwR7VzjCwKCAt?tndT^-$$4f@JaSo+LY8xR&hi-SjlV++#picMz$7Bu zUrAPrZe|YMA|>-K_is-Fn~6L;e|)|Nmn&<%a6aea8#)K2Ndzn(Em4lV8N#2{iK}Y5 z$)+Rx=5+UptI$^kQW*eS)~r16Km+_l-VRT^uqVbW{m|YM@EMK5S?86|%oM!Wngqdn zaZCf_+I&;*C0>f2>L=cmZ9`wFKn9@sc|6dn?~JXa${FpejUqb8Z9HxQ{8_4K)u3JR`SdlQps$!}(bJ;;zy`Q~hZ@9B=Ha>G^Y6!*$p-r> zM&W#7L(M)@$+ySixr02jsOcH4S6y7Eacuc4%)V@+F5+M#CSHR_lX)o4_~_~%qlHSC z!3qt3gueTn)9H%<8eVdPlF7U&u6SX;QHem&{RZlF({*%J1@e8&zknAE7)vy$9co(z zSZD$JEIdz;=K>X}90Z(O<^A^cAFfHwn>taJd8?Jt$67;EY*%2QChpQN*AN^HUKJ50 z2ugv0Zb1#j6AILxJTvlb1N5_B#noHKH4%O`isa{6yyKJ5f-7U)tBBd8mJHARey9QITz}&iGvN3 z_|zj8mIDL?5^^l&_csb&FX+K~w5diiREj z)O(_@3Nh75fT_t@!KY-bhUx+a-;P7~uJM`HQK_%19p#Q4<1pk!o@ z58WO-l9jo5|37WJgFy0PI5`=aBn256Gif6Jzt0nLS8S@7z=fAel1m)+o0#NFT=p;n%qD5!aiOy@7k&#FkExbVzB}#@6J))P< zqKp!qsG|${N7nkj|GVqnb=Ns}@8>*cpJ(s8*6uQFH>5RwNJ_>+L_|bI1c}L@ZKH<7 zys);^-1FVanBal68Fom~Cfd7hehnV=7)}0SqlQ+~aAHcfUWQuVM`@>+G-665tSMu; zzMua2J@wd=mT5-U=o1~wl&RHW#lD`?4>z4vK4K=Go(*%_c;5CXuH4l{PI?A#^A@T4 zqgP&jozqVzqyTs+u9_O`!be)(PjAR5Vt@h5N-?*{Y@WP>tz+})%w$ke)w1Kkwj&ZE z&wl8(ZY_1-Ya^dUIc;aiJmSja&>Nl^*I%x1coF2z{pL-*IEUwo!|MD0)o=M4z)RgF-mt{@tea|=9~ zw_mdCPtG1Ke-Y{6ZfE-f#g9XChY zsQzibGP$>XH8gp&>feC7490ISpM8I4H@B^apcff&a4_P@`cSe2s~Np)8BlO9*~;4P z*euKQP0zFE;myi{-5aOh%I8fV@W<1qj$R|>FFzB_q{KV$wvz6AXxzR`TKavu zUUBs$GVF)s#J$QbP4lt~);q6eR;D)GF7Z$Cz@4d$?JeHpqTP=9HVdQ5p7SQ$ud7{8 zg}GyIo%cpYpBv|wj&?;bloKN9N50R43{h7$5DHy(ut)t9m?~~!i1??F0a{#PzA_6# z{1Yi`C-LVAg#4)mczenDme3@)%w{f{zDgUs6@GSD+w_Nt@0Gq`2>rMW%gpPB*{yu_ zX*y=yxoT&2P|7nNK0}Y6(7Ld>i9$yAS={fTO-Z#J*(n>`+?}l?qEAjcRawKAP3wO5 zD(BNesw1Z7VLWH++Zl+~p=64TOCN?yd0h>Jic?hCZr8c4fRY1ik!r{nz0+$L)m0IMlEu>=fs(%u5${@foZxe4P z2&u_vb`@Bu&Pb?^vCPk!$udynW#708)wa0oI~!Sp>8<~JoSKNODY^{4Oi3PV*79u7 z#TyqVADyjGcX|Z-Pe=Q^`TD!rptWupc4mK6M`ykH&GY-np`b7pb6zX<8Gnm^*g^DD-0-sskBv4nyLj7a3K+6DjLNg`deEpyt$~qRd1V^-GyarI$7J` zA(90S3-}G#Wq*fxs|H+==vU+ro;Y=QrmUbHhOujHi`{70q4izs)RXM+kYpaSL2CCK zD+n{S#L)v}Uo_)q7M0Ij6)#<5&uOp=;9zGbpDOFEG>fddD20b##h1iP1g5X? z+@IBIFdeSpdUL{zUru3*)>j0ZqS#TU$ZQVtM{4T_3Xcy4`hkA}^+D7w)nZM#dLm48 zB}@j!LK;B1d^?U0gF-+=zYu?nO-KSJa3j8SF|(BVwu!EQyU%@oCK+if>bnUYG3M{@ zfAqgS;U^;x=(W_1>s=r*jm@y-toxNfnt`cW$K(VjV(>eib&F%cEasC)?NY=;vr-sa z`glx`HnZ{x75e z4>WY+WV<*_V|SNRXjRi#!fSG2%pyDdhO=7g{*)e_GfnX2qb7S-l&;M^@1(6mi~C!Z zrx< zXz{`m6O?PYl%DS2sL`+46s3RU$AO#F*~JbAKsj%5Q(^AVyCQS%#LM%XsvZ*9cFMuO zt$ei~ueP)^BCrKp=zCRgc#o?C(e;V`Z)xQ>I6pFbN=uA6RoPQ|`27TGKCmq9Ss1mn z=0FGYl;&!bY&ls?!oE46=&{Kji~;TBRGdx!u#&fhn4-^9m5$iwIO4&cY!F1<%UWq} zdO`C?da1dFaHU+1xd-v!>JF`PmPNmXDb&kv${jO$LCWCOpr_CxChP)t6uD?yx3&zF zQ_Os8ktBs8yJrDExEGR&LJd=k6m~SeLuykc`t#rExqR>d#$Q4Fxg+?2@?vHXbdm*I z;J_iG&Gm7E{UdWXwFvUsIPq>IQ@I*Xds{oAoYpQR5o5M~7}UP8u@@~`%y{uHl2is1 znoDq);f{+eBn5c~gQ=Q}3?%=Mbv=c7omynLBO;ZMa3gCe^@HKin>{p*+kUxw zhKh?f=d9zwiaPls4KhEP+^Y=vM9^wCb|o}`=%OSf$xbL^B>GTd>zyI$9a@-f*G2;< zy`f>KC@OPmFpZ%=E3ae+^vX5e0eCEn1D^1~B=}Fow;TI%mD6$pfC^1p0FO>_<#<6P zD`RB#G48%muD5~G6mV`d0j!d$0q9PlQq{ZY9Uki$&O{Y1LTq&f+(x#{EhKnKARoS! zh1w*cvP_Ka2wb!(&2VA4{?qtY!%cJN-m)&mQXq(~G6*CDC8c-WGkL#_7r#qC1~V*V zej_R(vsBPcIBhKJqku|NpPu?(D#mw34gYyp%VMDBOjbOCswkDW?p;8cn>aly`Ia(~ z$yQGNwD39D2@5f*dpQZ>zOigZmQOevdQRn+ULG%Vp0m;E#swa7o2uHzt?K>LhJG9L z&p7rUIbs|qQu}82r(VykvEvV)tzaehCCcwJ4;K@AoT{Zi%2UmrjxIrbWPbgM?KSoK z*XLlxMLRXXm$2_FaSdU)R<*TO`f38?#f+n%<#@T+aUS{GuA~M80V)MCjbWL<>Xc=S zX=n&Y?DyxaHW8WA{^M<}x;c*^s~O>fZBlG0tE6DUkhfKx%}YA)2B8d&z{;#2ewZNx zN2>wt7&HOUY^^<0ik44S8!8!eU_zm^=PRj|D5>o*xP{bCP7wPbcJcVL`_!Z|xsMY0 z&1aG^;9G1J9}lXK+hLZs0v{+WWj?E?Cc{=)KF{3Sw4vbbS@@S$HKVOW=2V};-X%(G z@{Kj6%d9frtP_ccQnUhrONcY*FcV~miJuJ&LAJnr*#35W@gzfUWPiwT^mmfB#$p4$E$GD%p zw44@hR3};Lz(?-=%Mp($)rIB+y^^*zi8(FksZP?=ffcfDy{NL(kMlia{TKIdVxG}s z;?#kvAK!Rn^=c||K$E%wWHM0EI3t%m55jiBzqm$sN#Hxmy^-3Pk3f_*Y>bhDaal63 zQv%E0g_@7Mj~!M4oSYWjR0><_K#864h!{T=zhGDEB$?A<*`~{XA6OXoG{ior_h7)5 zzK{}3kQE`gUwf}bec)*L;IwSRDlnT#cclVwlK6)P3kBgOBV~bym*4FK?#WsYP z$i>FRtQmji6pJ$=#E~;J^*!3!JfTqtL`PF+QUO7mwvVWpnkShBU=Dj5{L`r*#x&No9smKmm67q86nOUy=5d&F~#f~fVS{&{43SAUljws<%#L_Y8wnRu_9 zN>q)wdxBr)RAn;Hz&?mjqx~`U-5cMu0Oz!zwDWAeiW!mIWypyhjmQ>_)kH~w00VY! zQXhcRVHMFDLfmFEG-9vKGgJ@dD_(r%qpNPn!3%nxQx*Ui!L_mef2CsW9qwm}cAR#V zB4O01m6iOUJ8=nOa6-``@FHuBR)z94aY_Dj9QCq^ER$Qp2iAzlAsVi*PXVR?Xs#L$ z`jvJ>`tkes&GOC^d1ZaiQD&{{WCx|bkY7)-Q8{!bm+kVTLo=?edSVNDBw?p<^bzTj zKl4NaVFRtQj+}nCX`o4JJQeOcO4Aol)-q)F^i3(yVQj%0oo!0_`SD7xvpmu%%Ut3vY`@6uIPEG){uU%=K9UXdI!%5W5@*GG*UjaiKH z=C(qc2SZhxGVHO7;u?NV9OCw%GK$PP*bOaMpv1YOG7JI;Ps5C-RK1Odr zj=#g6tH>uuGC^}7{H%r%tLJ>@pumNHx~ur+`@%~Gd%(VPQOH9yby7AB zY(I79G?aaE^cddZ1dtz;Mw}}qn&gPvEfA0RfbKd-bPoL&%IE7i?b6xn*h^TyKxS@# z+&;Ecg1<6eGcEqqG&!GoKM)i>a`VrQ;T~HA+}Rai#Uq87&X(7EPOeB~p9=wyB-_kO5#3AQPFeUsGw%zO^m$+vTWY zErJJ?DE+3k;F1)dEDY8sIm=x9E{xq_(@)TR#KGGXw0p2CA;CHSkmlS(3K2vh4aE1Y z{_F`aNH{HHuy>5i5?hc6=A<{G$##)KFrc!uotLI;h^E?}f}(H3IxRTnS4f~A{JPS{ zVugA71z7UC;I(=!zx+P^^$iLav+oiZk9`K}FBs5aMCPsZ-4hY0fDp`p4B3hyw84 zIOR8>Uq7C5JLZt3u!(Myg{f92O24Qp&;;NYz_1jhg*t%_3l1ZR+3?13;84YcRCFDi zoRd0_^Z-Kt@m+hw1R$AXP&%Ulbgh5Z>-9! zSBlbNvj($QT~I>O=7HgUt>lt{mx&nrvdO5tU<{MjM_gRlp0Vx0(uPyl;KbX{ce(`- zHynWBY5Xc@!0ij1#nPK`U|0FXTX|`3i-(p-VA{XlcvYs`9)(j9U7-ytDnbK_IIFQV zim*;kn4O!&U9hL~Y=&Q{X>z4hUL;G9fmLf1_KHyyu4U_l)Io>o1?d>~CH@d_GKzr7 z3V&uhGF0ce0;sG$pDVj>-3rF>q7FyDAYkaJ|X za`yiCfbrIi<&hi`8O-b(hIE}Xad+>K1y?T)31+{G=sya zRJI4|$=pko$PaE!5$hEQW_!{r{7u$*q5QlGcX+bw*f~j9?JwjMj0TZ5ZC~H68Qg=! z+;E6YlpPz0O-t0{1mPdbdR8t1=*NZB=N>%Y=N>e^OS5cJtya%~TrAOcR-P@c`L5Ag zb6Xy6;hz|8&xbglM(G6$9aV!iC#UmjsvJKi$c_yXYCXy*BK@}0%n3`ayPP*i<>Fty z(gT_|ZuLBgf@N^u7+(Zc!lJS5jn29KtIU}un*UJO@ri5pC)*z4~s}99+x6%{xgJh4Z zc9$=^zOU#^T0)NdAJZ*;&c4=1-01Iwo}l@8af0lVAwKZEh~E{ag(t4ChICup%}OY>}fGb z5DzFfkN2G;t#shnHfh*$D0x8G%DM5=zHi?SLZu_HFQs9{{U-NrEHlotcE8jScmFGD zEzYFx+;aVDJkmY{kvOe|ZvzN*kO4aV(m%f6M3ZKD(c@xz`5f`Gb-(p4`_IPZ@1qUy z6qoet;9sd>hUu}TxgugJEE$EUwTmxvcH)@6no-_9-y@fXv(Mq-s^O(AJJDkcCl=BT zPgJ5BVHbTNm(E$kOGPb{Gu!9>f4*OYhgtaA5jXz%6a00q`EtcYp>^nJ^&%Qs^M~|* z8>=-wH#ta2ATg*UzBR(D#t&pod_5fCU%DPv3+P`DUkIuEe-|31B_w}s5+%e`BQ8vQB>j6}l`#6(2E|AG<`JtzLkkHYbDqjrU9ui-8U*l$rl5n+lQ!E3Ys11_>lVE_OC