| from dataclasses import dataclass, field |
| from typing import List, Optional, Annotated, Union |
| from solverforge_legacy.solver.domain import ( |
| planning_entity, |
| planning_solution, |
| PlanningId, |
| PlanningVariable, |
| PlanningEntityCollectionProperty, |
| ProblemFactCollectionProperty, |
| ValueRangeProvider, |
| PlanningScore, |
| PlanningPin, |
| ) |
| from solverforge_legacy.solver import SolverStatus |
| from solverforge_legacy.solver.score import HardMediumSoftScore |
| from .json_serialization import JsonDomainBase |
| from pydantic import Field |
|
|
| |
| GRAIN_LENGTH_IN_MINUTES = 15 |
|
|
|
|
| @dataclass |
| class Person: |
| id: Annotated[str, PlanningId] |
| full_name: str |
|
|
|
|
| @dataclass |
| class TimeGrain: |
| id: Annotated[str, PlanningId] |
| grain_index: int |
| day_of_year: int |
| starting_minute_of_day: int |
|
|
|
|
| @dataclass |
| class Room: |
| id: Annotated[str, PlanningId] |
| name: str |
| capacity: int |
|
|
|
|
| |
| @dataclass |
| class Attendance: |
| id: Annotated[str, PlanningId] |
| person: Person |
| meeting_id: str |
|
|
|
|
| @dataclass |
| class RequiredAttendance: |
| id: Annotated[str, PlanningId] |
| person: Person |
| meeting_id: str |
|
|
|
|
| @dataclass |
| class PreferredAttendance: |
| id: Annotated[str, PlanningId] |
| person: Person |
| meeting_id: str |
|
|
|
|
| @dataclass |
| class Meeting: |
| id: Annotated[str, PlanningId] |
| topic: str |
| duration_in_grains: int |
| speakers: Optional[List[Person]] = None |
| content: Optional[str] = None |
| entire_group_meeting: bool = False |
| required_attendances: List[RequiredAttendance] = field(default_factory=list) |
| preferred_attendances: List[PreferredAttendance] = field(default_factory=list) |
|
|
| def get_required_capacity(self) -> int: |
| return len(self.required_attendances) + len(self.preferred_attendances) |
|
|
| def add_required_attendant(self, person: Person) -> None: |
| person_id = person.id |
| for r in self.required_attendances: |
| if r.person.id == person_id: |
| raise ValueError( |
| f"The person {person_id} is already assigned to the meeting {self.id}." |
| ) |
| self.required_attendances.append( |
| RequiredAttendance( |
| id=f"{self.id}-{self.get_required_capacity() + 1}", |
| meeting_id=self.id, |
| person=person, |
| ) |
| ) |
|
|
| def add_preferred_attendant(self, person: Person) -> None: |
| person_id = person.id |
| for p in self.preferred_attendances: |
| if p.person.id == person_id: |
| raise ValueError( |
| f"The person {person_id} is already assigned to the meeting {self.id}." |
| ) |
| self.preferred_attendances.append( |
| PreferredAttendance( |
| id=f"{self.id}-{self.get_required_capacity() + 1}", |
| meeting_id=self.id, |
| person=person, |
| ) |
| ) |
|
|
|
|
| @planning_entity |
| @dataclass |
| class MeetingAssignment: |
| id: Annotated[str, PlanningId] |
| meeting: Meeting |
| pinned: Annotated[bool, PlanningPin] = False |
| starting_time_grain: Annotated[Optional[TimeGrain], PlanningVariable] = None |
| room: Annotated[Optional[Room], PlanningVariable] = None |
|
|
| def get_grain_index(self) -> Optional[int]: |
| if self.starting_time_grain is None: |
| return None |
| return self.starting_time_grain.grain_index |
|
|
| def calculate_overlap(self, other: "MeetingAssignment") -> int: |
| if self.starting_time_grain is None or other.starting_time_grain is None: |
| return 0 |
| |
| start = self.starting_time_grain.grain_index |
| end = self.get_last_time_grain_index() + 1 |
| other_start = other.starting_time_grain.grain_index |
| other_end = other.get_last_time_grain_index() + 1 |
|
|
| if other_end < start or end < other_start: |
| return 0 |
|
|
| return min(end, other_end) - max(start, other_start) |
|
|
| def get_last_time_grain_index(self) -> Optional[int]: |
| if self.starting_time_grain is None: |
| return None |
| return ( |
| self.starting_time_grain.grain_index + self.meeting.duration_in_grains - 1 |
| ) |
|
|
| def get_room_capacity(self) -> int: |
| if self.room is None: |
| return 0 |
| return self.room.capacity |
|
|
| def get_required_capacity(self) -> int: |
| return self.meeting.get_required_capacity() |
|
|
|
|
| @planning_solution |
| @dataclass |
| class MeetingSchedule: |
| people: Annotated[List[Person], ProblemFactCollectionProperty] |
| time_grains: Annotated[ |
| List[TimeGrain], ProblemFactCollectionProperty, ValueRangeProvider |
| ] |
| rooms: Annotated[List[Room], ProblemFactCollectionProperty, ValueRangeProvider] |
| meetings: Annotated[List[Meeting], ProblemFactCollectionProperty] |
| required_attendances: Annotated[ |
| List[RequiredAttendance], ProblemFactCollectionProperty |
| ] = field(default_factory=list) |
| preferred_attendances: Annotated[ |
| List[PreferredAttendance], ProblemFactCollectionProperty |
| ] = field(default_factory=list) |
| attendances: Annotated[ |
| List[Attendance], ProblemFactCollectionProperty |
| ] = field(default_factory=list) |
| meeting_assignments: Annotated[ |
| List[MeetingAssignment], PlanningEntityCollectionProperty |
| ] = field(default_factory=list) |
| score: Annotated[Optional[HardMediumSoftScore], PlanningScore] = None |
| solver_status: SolverStatus = SolverStatus.NOT_SOLVING |
|
|
|
|
| |
| class PersonModel(JsonDomainBase): |
| id: str |
| full_name: str |
|
|
|
|
| class TimeGrainModel(JsonDomainBase): |
| id: str |
| grain_index: int |
| day_of_year: int |
| starting_minute_of_day: int |
|
|
|
|
| class RoomModel(JsonDomainBase): |
| id: str |
| name: str |
| capacity: int |
|
|
|
|
| class RequiredAttendanceModel(JsonDomainBase): |
| id: str |
| person: PersonModel |
| meeting_id: str = Field(..., alias="meeting") |
|
|
|
|
| class PreferredAttendanceModel(JsonDomainBase): |
| id: str |
| person: PersonModel |
| meeting_id: str = Field(..., alias="meeting") |
|
|
|
|
| class MeetingModel(JsonDomainBase): |
| id: str |
| topic: str |
| duration_in_grains: int |
| speakers: Optional[List[PersonModel]] = None |
| content: Optional[str] = None |
| entire_group_meeting: bool = False |
| required_attendances: List[RequiredAttendanceModel] = Field( |
| default_factory=list, alias="requiredAttendances" |
| ) |
| preferred_attendances: List[PreferredAttendanceModel] = Field( |
| default_factory=list, alias="preferredAttendances" |
| ) |
|
|
|
|
| class MeetingAssignmentModel(JsonDomainBase): |
| id: str |
| meeting: Union[str, MeetingModel] |
| pinned: bool = False |
| starting_time_grain: Union[str, TimeGrainModel, None] = None |
| room: Union[str, RoomModel, None] = None |
|
|
|
|
| class MeetingScheduleModel(JsonDomainBase): |
| people: List[PersonModel] |
| time_grains: List[TimeGrainModel] |
| rooms: List[RoomModel] |
| meetings: List[MeetingModel] |
| required_attendances: List[RequiredAttendanceModel] = Field( |
| default_factory=list, alias="requiredAttendances" |
| ) |
| preferred_attendances: List[PreferredAttendanceModel] = Field( |
| default_factory=list, alias="preferredAttendances" |
| ) |
| meeting_assignments: List[MeetingAssignmentModel] = Field(default_factory=list) |
| score: Optional[str] = None |
| solver_status: Optional[str] = None |
|
|