|
|
from random import Random |
|
|
from datetime import datetime, timedelta |
|
|
from enum import Enum |
|
|
from typing import List |
|
|
from dataclasses import dataclass |
|
|
|
|
|
from .domain import Person, TimeGrain, Room, Meeting, MeetingAssignment, MeetingSchedule, RequiredAttendance, PreferredAttendance |
|
|
|
|
|
class DemoData(str, Enum): |
|
|
SMALL = "SMALL" |
|
|
MEDIUM = "MEDIUM" |
|
|
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) |
|
|
|
|
|
def generate_demo_data() -> MeetingSchedule: |
|
|
"""Generate demo data for the meeting scheduling problem.""" |
|
|
rnd = Random(0) |
|
|
|
|
|
|
|
|
people = generate_people(20, rnd) |
|
|
|
|
|
|
|
|
time_grains = generate_time_grains() |
|
|
|
|
|
|
|
|
rooms = [ |
|
|
Room(id="R1", name="Room 1", capacity=30), |
|
|
Room(id="R2", name="Room 2", capacity=20), |
|
|
Room(id="R3", name="Room 3", capacity=16) |
|
|
] |
|
|
|
|
|
|
|
|
meetings = generate_meetings(people, rnd) |
|
|
|
|
|
|
|
|
all_required_attendances = [ra for meeting in meetings for ra in meeting.required_attendances] |
|
|
all_preferred_attendances = [pa for meeting in meetings for pa in meeting.preferred_attendances] |
|
|
new_meetings = [] |
|
|
for m in meetings: |
|
|
new_meetings.append( |
|
|
type(m)( |
|
|
id=m.id, |
|
|
topic=m.topic, |
|
|
duration_in_grains=m.duration_in_grains, |
|
|
speakers=m.speakers, |
|
|
content=m.content or "", |
|
|
entire_group_meeting=m.entire_group_meeting, |
|
|
required_attendances=[a for a in all_required_attendances if a.meeting_id == m.id], |
|
|
preferred_attendances=[a for a in all_preferred_attendances if a.meeting_id == m.id], |
|
|
) |
|
|
) |
|
|
meetings = new_meetings |
|
|
|
|
|
|
|
|
meeting_assignments = generate_meeting_assignments(meetings) |
|
|
|
|
|
|
|
|
schedule = MeetingSchedule( |
|
|
people=people, |
|
|
time_grains=time_grains, |
|
|
rooms=rooms, |
|
|
meetings=meetings, |
|
|
meeting_assignments=meeting_assignments, |
|
|
required_attendances=[ra for meeting in meetings for ra in meeting.required_attendances], |
|
|
preferred_attendances=[pa for meeting in meetings for pa in meeting.preferred_attendances], |
|
|
) |
|
|
|
|
|
return schedule |
|
|
|
|
|
|
|
|
def generate_people(count_people: int, rnd: Random) -> List[Person]: |
|
|
"""Generate a list of people.""" |
|
|
FIRST_NAMES = ["Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay", |
|
|
"Jeri", "Hope", "Avis", "Lino", "Lyle", "Nick", "Dino", "Otha", "Gwen", "Jose", |
|
|
"Dena", "Jana", "Dave", "Russ", "Josh", "Dana", "Katy"] |
|
|
LAST_NAMES = ["Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt", |
|
|
"Howe", "Lowe", "Wise", "Clay", "Carr", "Hood", "Long", "Horn", "Haas", "Meza"] |
|
|
|
|
|
def generate_name() -> str: |
|
|
first_name = rnd.choice(FIRST_NAMES) |
|
|
last_name = rnd.choice(LAST_NAMES) |
|
|
return f"{first_name} {last_name}" |
|
|
|
|
|
return [Person(id=str(i), full_name=generate_name()) for i in range(count_people)] |
|
|
|
|
|
|
|
|
def generate_time_grains() -> List[TimeGrain]: |
|
|
"""Generate time grains for the next 4 days starting from tomorrow.""" |
|
|
time_grains = [] |
|
|
current_date = datetime.now().date() + timedelta(days=1) |
|
|
count = 0 |
|
|
|
|
|
while current_date < datetime.now().date() + timedelta(days=5): |
|
|
current_time = datetime.combine(current_date, datetime.min.time()) + timedelta(hours=8) |
|
|
end_time = datetime.combine(current_date, datetime.min.time()) + timedelta(hours=17, minutes=45) |
|
|
|
|
|
while current_time <= end_time: |
|
|
day_of_year = current_date.timetuple().tm_yday |
|
|
minutes_of_day = current_time.hour * 60 + current_time.minute |
|
|
|
|
|
count += 1 |
|
|
time_grains.append(TimeGrain( |
|
|
id=str(count), |
|
|
grain_index=count, |
|
|
day_of_year=day_of_year, |
|
|
starting_minute_of_day=minutes_of_day |
|
|
)) |
|
|
current_time += timedelta(minutes=15) |
|
|
|
|
|
current_date += timedelta(days=1) |
|
|
|
|
|
return time_grains |
|
|
|
|
|
|
|
|
def generate_meetings(people: List[Person], rnd: Random) -> List[Meeting]: |
|
|
"""Generate meetings with topics and attendees.""" |
|
|
meeting_topics = [ |
|
|
"Strategize B2B", "Fast track e-business", "Cross sell virtualization", |
|
|
"Profitize multitasking", "Transform one stop shop", "Engage braindumps", |
|
|
"Downsize data mining", "Ramp up policies", "On board synergies", |
|
|
"Reinvigorate user experience", "Strategize e-business", "Fast track virtualization", |
|
|
"Cross sell multitasking", "Profitize one stop shop", "Transform braindumps", |
|
|
"Engage data mining", "Downsize policies", "Ramp up synergies", |
|
|
"On board user experience", "Reinvigorate B2B", "Strategize virtualization", |
|
|
"Fast track multitasking", "Cross sell one stop shop", "Reinvigorate multitasking" |
|
|
] |
|
|
|
|
|
meetings = [] |
|
|
for i, topic in enumerate(meeting_topics): |
|
|
meeting = Meeting(id=str(i), topic=topic, duration_in_grains=0) |
|
|
meetings.append(meeting) |
|
|
|
|
|
|
|
|
duration_distribution = ( |
|
|
CountDistribution(count=8, weight=1), |
|
|
CountDistribution(count=12, weight=1), |
|
|
CountDistribution(count=16, weight=1) |
|
|
) |
|
|
|
|
|
for meeting in meetings: |
|
|
duration_time_grains, = rnd.choices(population=counts(duration_distribution), |
|
|
weights=weights(duration_distribution)) |
|
|
meeting.duration_in_grains = duration_time_grains |
|
|
|
|
|
|
|
|
required_attendees_distribution = ( |
|
|
CountDistribution(count=2, weight=0.45), |
|
|
CountDistribution(count=3, weight=0.15), |
|
|
CountDistribution(count=4, weight=0.10), |
|
|
CountDistribution(count=5, weight=0.10), |
|
|
CountDistribution(count=6, weight=0.08), |
|
|
CountDistribution(count=7, weight=0.05), |
|
|
CountDistribution(count=8, weight=0.04), |
|
|
CountDistribution(count=10, weight=0.03) |
|
|
) |
|
|
|
|
|
def add_required_attendees(meeting: Meeting, count: int) -> None: |
|
|
|
|
|
selected_people = rnd.sample(people, count) |
|
|
for person in selected_people: |
|
|
meeting.required_attendances.append( |
|
|
RequiredAttendance( |
|
|
id=f"{meeting.id}-{len(meeting.required_attendances) + 1}", |
|
|
person=person, |
|
|
meeting_id=meeting.id |
|
|
) |
|
|
) |
|
|
|
|
|
for meeting in meetings: |
|
|
count, = rnd.choices(population=counts(required_attendees_distribution), |
|
|
weights=weights(required_attendees_distribution)) |
|
|
add_required_attendees(meeting, count) |
|
|
|
|
|
|
|
|
preferred_attendees_distribution = ( |
|
|
CountDistribution(count=1, weight=0.25), |
|
|
CountDistribution(count=2, weight=0.30), |
|
|
CountDistribution(count=3, weight=0.20), |
|
|
CountDistribution(count=4, weight=0.10), |
|
|
CountDistribution(count=5, weight=0.06), |
|
|
CountDistribution(count=6, weight=0.04), |
|
|
CountDistribution(count=7, weight=0.02), |
|
|
CountDistribution(count=8, weight=0.02), |
|
|
CountDistribution(count=9, weight=0.01), |
|
|
CountDistribution(count=10, weight=0.00) |
|
|
) |
|
|
|
|
|
def add_preferred_attendees(meeting: Meeting, count: int) -> None: |
|
|
|
|
|
required_people_ids = {ra.person.id for ra in meeting.required_attendances} |
|
|
available_people = [person for person in people if person.id not in required_people_ids] |
|
|
|
|
|
|
|
|
if len(available_people) >= count: |
|
|
selected_people = rnd.sample(available_people, count) |
|
|
for person in selected_people: |
|
|
meeting.preferred_attendances.append( |
|
|
PreferredAttendance( |
|
|
id=f"{meeting.id}-{len(meeting.required_attendances) + len(meeting.preferred_attendances) + 1}", |
|
|
person=person, |
|
|
meeting_id=meeting.id |
|
|
) |
|
|
) |
|
|
|
|
|
for meeting in meetings: |
|
|
count, = rnd.choices(population=counts(preferred_attendees_distribution), |
|
|
weights=weights(preferred_attendees_distribution)) |
|
|
add_preferred_attendees(meeting, count) |
|
|
|
|
|
return meetings |
|
|
|
|
|
|
|
|
def generate_meeting_assignments(meetings: List[Meeting]) -> List[MeetingAssignment]: |
|
|
"""Generate meeting assignments for each meeting.""" |
|
|
return [MeetingAssignment(id=str(i), meeting=meeting) for i, meeting in enumerate(meetings)] |
|
|
|