| | from datetime import date, datetime, time, timedelta |
| | from itertools import product |
| | from enum import Enum |
| | from random import Random |
| | from typing import Generator |
| | from dataclasses import dataclass, field |
| |
|
| | from .domain import Employee, EmployeeSchedule, Shift |
| |
|
| |
|
| | class DemoData(Enum): |
| | SMALL = 'SMALL' |
| | LARGE = 'LARGE' |
| |
|
| |
|
| | @dataclass(frozen=True, kw_only=True) |
| | class CountDistribution: |
| | count: int |
| | weight: float |
| |
|
| |
|
| | def counts(distributions: tuple[CountDistribution, ...]) -> tuple[int, ...]: |
| | return tuple(distribution.count for distribution in distributions) |
| |
|
| |
|
| | def weights(distributions: tuple[CountDistribution, ...]) -> tuple[float, ...]: |
| | return tuple(distribution.weight for distribution in distributions) |
| |
|
| |
|
| | @dataclass(kw_only=True) |
| | class DemoDataParameters: |
| | locations: tuple[str, ...] |
| | required_skills: tuple[str, ...] |
| | optional_skills: tuple[str, ...] |
| | days_in_schedule: int |
| | employee_count: int |
| | optional_skill_distribution: tuple[CountDistribution, ...] |
| | shift_count_distribution: tuple[CountDistribution, ...] |
| | availability_count_distribution: tuple[CountDistribution, ...] |
| | random_seed: int = field(default=37) |
| |
|
| |
|
| | demo_data_to_parameters: dict[DemoData, DemoDataParameters] = { |
| | DemoData.SMALL: DemoDataParameters( |
| | locations=("Ambulatory care", "Critical care", "Pediatric care"), |
| | required_skills=("Doctor", "Nurse"), |
| | optional_skills=("Anaesthetics", "Cardiology"), |
| | days_in_schedule=14, |
| | employee_count=15, |
| | optional_skill_distribution=( |
| | CountDistribution(count=1, weight=3), |
| | CountDistribution(count=2, weight=1) |
| | ), |
| | shift_count_distribution=( |
| | CountDistribution(count=1, weight=0.9), |
| | CountDistribution(count=2, weight=0.1) |
| | ), |
| | availability_count_distribution=( |
| | CountDistribution(count=1, weight=4), |
| | CountDistribution(count=2, weight=3), |
| | CountDistribution(count=3, weight=2), |
| | CountDistribution(count=4, weight=1) |
| | ), |
| | random_seed=37 |
| | ), |
| |
|
| | DemoData.LARGE: DemoDataParameters( |
| | locations=("Ambulatory care", |
| | "Neurology", |
| | "Critical care", |
| | "Pediatric care", |
| | "Surgery", |
| | "Radiology", |
| | "Outpatient"), |
| | required_skills=("Doctor", "Nurse"), |
| | optional_skills=("Anaesthetics", "Cardiology", "Radiology"), |
| | days_in_schedule=28, |
| | employee_count=50, |
| | optional_skill_distribution=( |
| | CountDistribution(count=1, weight=3), |
| | CountDistribution(count=2, weight=1) |
| | ), |
| | shift_count_distribution=( |
| | CountDistribution(count=1, weight=0.5), |
| | CountDistribution(count=2, weight=0.3), |
| | CountDistribution(count=3, weight=0.2) |
| | ), |
| | availability_count_distribution=( |
| | CountDistribution(count=5, weight=4), |
| | CountDistribution(count=10, weight=3), |
| | CountDistribution(count=15, weight=2), |
| | CountDistribution(count=20, weight=1) |
| | ), |
| | random_seed=37 |
| | ) |
| | } |
| |
|
| |
|
| | FIRST_NAMES = ("Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay") |
| | LAST_NAMES = ("Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt") |
| | SHIFT_LENGTH = timedelta(hours=8) |
| | MORNING_SHIFT_START_TIME = time(hour=6, minute=0) |
| | DAY_SHIFT_START_TIME = time(hour=9, minute=0) |
| | AFTERNOON_SHIFT_START_TIME = time(hour=14, minute=0) |
| | NIGHT_SHIFT_START_TIME = time(hour=22, minute=0) |
| |
|
| | SHIFT_START_TIMES_COMBOS = ( |
| | (MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME), |
| | (MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME), |
| | (MORNING_SHIFT_START_TIME, DAY_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME), |
| | ) |
| |
|
| |
|
| | location_to_shift_start_time_list_map = dict() |
| |
|
| |
|
| | def earliest_monday_on_or_after(target_date: date): |
| | """ |
| | Returns the date of the next given weekday after |
| | the given date. For example, the date of next Monday. |
| | |
| | NB: if it IS the day we're looking for, this returns 0. |
| | consider then doing onDay(foo, day + 1). |
| | """ |
| | days = (7 - target_date.weekday()) % 7 |
| | return target_date + timedelta(days=days) |
| |
|
| |
|
| | def generate_demo_data(demo_data_or_parameters: DemoData | DemoDataParameters) -> EmployeeSchedule: |
| | global location_to_shift_start_time_list_map, demo_data_to_parameters |
| | if isinstance(demo_data_or_parameters, DemoData): |
| | parameters = demo_data_to_parameters[demo_data_or_parameters] |
| | else: |
| | parameters = demo_data_or_parameters |
| |
|
| | start_date = earliest_monday_on_or_after(date.today()) |
| | random = Random(parameters.random_seed) |
| | shift_template_index = 0 |
| | for location in parameters.locations: |
| | location_to_shift_start_time_list_map[location] = SHIFT_START_TIMES_COMBOS[shift_template_index] |
| | shift_template_index = (shift_template_index + 1) % len(SHIFT_START_TIMES_COMBOS) |
| |
|
| | name_permutations = [f'{first_name} {last_name}' |
| | for first_name, last_name in product(FIRST_NAMES, LAST_NAMES)] |
| | random.shuffle(name_permutations) |
| |
|
| | employees = [] |
| | for i in range(parameters.employee_count): |
| | count, = random.choices(population=counts(parameters.optional_skill_distribution), |
| | weights=weights(parameters.optional_skill_distribution)) |
| | skills = [] |
| | skills += random.sample(parameters.optional_skills, count) |
| | skills += random.sample(parameters.required_skills, 1) |
| | employees.append( |
| | Employee(name=name_permutations[i], |
| | skills=set(skills)) |
| | ) |
| |
|
| | shifts: list[Shift] = [] |
| |
|
| | def id_generator(): |
| | current_id = 0 |
| | while True: |
| | yield str(current_id) |
| | current_id += 1 |
| |
|
| | ids = id_generator() |
| |
|
| | for i in range(parameters.days_in_schedule): |
| | count, = random.choices(population=counts(parameters.availability_count_distribution), |
| | weights=weights(parameters.availability_count_distribution)) |
| | employees_with_availabilities_on_day = random.sample(employees, count) |
| | current_date = start_date + timedelta(days=i) |
| | for employee in employees_with_availabilities_on_day: |
| | rand_num = random.randint(0, 2) |
| | if rand_num == 0: |
| | employee.unavailable_dates.add(current_date) |
| | elif rand_num == 1: |
| | employee.undesired_dates.add(current_date) |
| | elif rand_num == 2: |
| | employee.desired_dates.add(current_date) |
| | shifts += generate_shifts_for_day(parameters, current_date, random, ids) |
| |
|
| | shift_count = 0 |
| | for shift in shifts: |
| | shift.id = str(shift_count) |
| | shift_count += 1 |
| |
|
| | return EmployeeSchedule( |
| | employees=employees, |
| | shifts=shifts |
| | ) |
| |
|
| |
|
| | def generate_shifts_for_day(parameters: DemoDataParameters, current_date: date, random: Random, |
| | ids: Generator[str, any, any]) -> list[Shift]: |
| | global location_to_shift_start_time_list_map |
| | shifts = [] |
| | for location in parameters.locations: |
| | shift_start_times = location_to_shift_start_time_list_map[location] |
| | for start_time in shift_start_times: |
| | shift_start_date_time = datetime.combine(current_date, start_time) |
| | shift_end_date_time = shift_start_date_time + SHIFT_LENGTH |
| | shifts += generate_shifts_for_timeslot(parameters, shift_start_date_time, shift_end_date_time, |
| | location, random, ids) |
| |
|
| | return shifts |
| |
|
| |
|
| | def generate_shifts_for_timeslot(parameters: DemoDataParameters, timeslot_start: datetime, timeslot_end: datetime, |
| | location: str, random: Random, ids: Generator[str, any, any]) -> list[Shift]: |
| | shift_count, = random.choices(population=counts(parameters.shift_count_distribution), |
| | weights=weights(parameters.shift_count_distribution)) |
| |
|
| | shifts = [] |
| | for i in range(shift_count): |
| | if random.random() >= 0.5: |
| | required_skill = random.choice(parameters.required_skills) |
| | else: |
| | required_skill = random.choice(parameters.optional_skills) |
| | shifts.append(Shift( |
| | id=next(ids), |
| | start=timeslot_start, |
| | end=timeslot_end, |
| | location=location, |
| | required_skill=required_skill)) |
| |
|
| | return shifts |
| |
|