blackopsrepl's picture
update
e7cf451
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