blackopsrepl's picture
Upload 31 files
666f6cf verified
from solverforge_legacy.solver.score import (
constraint_provider,
HardMediumSoftScore,
Joiners,
ConstraintFactory,
Constraint,
)
from .domain import (
Attendance,
MeetingAssignment,
PreferredAttendance,
RequiredAttendance,
Room,
TimeGrain,
)
@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
"""
Defines all constraints for the meeting scheduling problem, organized by priority (hard, medium, soft).
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
List[Constraint]: All defined constraints.
"""
return [
# Hard constraints
room_conflict(constraint_factory),
avoid_overtime(constraint_factory),
required_attendance_conflict(constraint_factory),
required_room_capacity(constraint_factory),
start_and_end_on_same_day(constraint_factory),
# Medium constraints
required_and_preferred_attendance_conflict(constraint_factory),
preferred_attendance_conflict(constraint_factory),
# Soft constraints
do_meetings_as_soon_as_possible(constraint_factory),
one_break_between_consecutive_meetings(constraint_factory),
overlapping_meetings(constraint_factory),
assign_larger_rooms_first(constraint_factory),
room_stability(constraint_factory),
]
# ************************************************************************
# Hard constraints
# ************************************************************************
def room_conflict(constraint_factory: ConstraintFactory) -> Constraint:
"""
Hard constraint: Prevents overlapping meetings in the same room.
Penalizes pairs of meetings scheduled in the same room whose time slots overlap, with penalty proportional to the overlap duration.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_unique_pair(
MeetingAssignment,
Joiners.equal(lambda assignment: assignment.room),
Joiners.overlapping(
lambda assignment: assignment.get_grain_index(),
lambda assignment: assignment.get_last_time_grain_index() + 1,
),
)
.penalize(
HardMediumSoftScore.ONE_HARD,
lambda left_assignment,
right_assignment: right_assignment.calculate_overlap(left_assignment),
)
.as_constraint("Room conflict")
)
def avoid_overtime(constraint_factory: ConstraintFactory) -> Constraint:
"""
Hard constraint: Prevents meetings from extending beyond available time slots.
Penalizes meetings that end after the last available time grain, based on how far they extend beyond the schedule.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
)
.if_not_exists(
TimeGrain,
Joiners.equal(
lambda assignment: assignment.get_last_time_grain_index(),
lambda time_grain: time_grain.grain_index,
),
)
.penalize(
HardMediumSoftScore.ONE_HARD,
lambda meeting_assignment: meeting_assignment.get_last_time_grain_index(),
)
.as_constraint("Don't go in overtime")
)
def required_attendance_conflict(constraint_factory: ConstraintFactory) -> Constraint:
"""
Hard constraint: Prevents required attendees from having overlapping meetings.
Penalizes when a person required at multiple meetings is scheduled for overlapping meetings, proportional to the overlap duration.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_unique_pair(
RequiredAttendance, Joiners.equal(lambda attendance: attendance.person)
)
.join(
MeetingAssignment,
Joiners.equal(
lambda left_required, right_required: left_required.meeting_id,
lambda assignment: assignment.meeting.id,
),
)
.join(
MeetingAssignment,
Joiners.equal(
lambda left_required,
right_required,
left_assignment: right_required.meeting_id,
lambda assignment: assignment.meeting.id,
),
Joiners.overlapping(
lambda attendee1, attendee2, assignment: assignment.get_grain_index(),
lambda attendee1,
attendee2,
assignment: assignment.get_last_time_grain_index() + 1,
lambda assignment: assignment.get_grain_index(),
lambda assignment: assignment.get_last_time_grain_index() + 1,
),
)
.penalize(
HardMediumSoftScore.ONE_HARD,
lambda left_required,
right_required,
left_assignment,
right_assignment: right_assignment.calculate_overlap(left_assignment),
)
.as_constraint("Required attendance conflict")
)
def required_room_capacity(constraint_factory: ConstraintFactory) -> Constraint:
"""
Hard constraint: Ensures rooms have enough capacity for required attendees.
Penalizes meetings assigned to rooms with insufficient capacity, proportional to the shortfall.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.get_required_capacity()
> meeting_assignment.get_room_capacity()
)
.penalize(
HardMediumSoftScore.ONE_HARD,
lambda meeting_assignment: meeting_assignment.get_required_capacity()
- meeting_assignment.get_room_capacity(),
)
.as_constraint("Required room capacity")
)
def start_and_end_on_same_day(constraint_factory: ConstraintFactory) -> Constraint:
"""
Hard constraint: Ensures meetings start and end on the same day.
Penalizes meetings that span multiple days.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
)
.join(
TimeGrain,
Joiners.equal(
lambda meeting_assignment: meeting_assignment.get_last_time_grain_index(),
lambda time_grain: time_grain.grain_index,
),
Joiners.filtering(
lambda meeting_assignment,
time_grain: meeting_assignment.starting_time_grain.day_of_year
!= time_grain.day_of_year
),
)
.penalize(HardMediumSoftScore.ONE_HARD)
.as_constraint("Start and end on same day")
)
# ************************************************************************
# Medium constraints
# ************************************************************************
def required_and_preferred_attendance_conflict(
constraint_factory: ConstraintFactory,
) -> Constraint:
"""
Medium constraint: Discourages conflicts between required and preferred attendance for the same person.
Penalizes when a person required at one meeting and preferred at another is scheduled for overlapping meetings, proportional to the overlap duration.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each(RequiredAttendance)
.join(
PreferredAttendance,
Joiners.equal(
lambda required: required.person, lambda preferred: preferred.person
),
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda assignment: assignment.starting_time_grain is not None
),
Joiners.equal(
lambda required, preferred: required.meeting_id,
lambda assignment: assignment.meeting.id,
),
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda assignment: assignment.starting_time_grain is not None
),
Joiners.equal(
lambda required, preferred, left_assignment: preferred.meeting_id,
lambda assignment: assignment.meeting.id,
),
Joiners.overlapping(
lambda required, preferred, assignment: assignment.get_grain_index(),
lambda required,
preferred,
assignment: assignment.get_last_time_grain_index() + 1,
lambda assignment: assignment.get_grain_index(),
lambda assignment: assignment.get_last_time_grain_index() + 1,
),
)
.penalize(
HardMediumSoftScore.ONE_MEDIUM,
lambda required,
preferred,
left_assignment,
right_assignment: right_assignment.calculate_overlap(left_assignment),
)
.as_constraint("Required and preferred attendance conflict")
)
def preferred_attendance_conflict(constraint_factory: ConstraintFactory) -> Constraint:
"""
Medium constraint: Discourages conflicts between preferred attendees.
Penalizes when a person preferred at multiple meetings is scheduled for overlapping meetings, proportional to the overlap duration.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_unique_pair(
PreferredAttendance, Joiners.equal(lambda attendance: attendance.person)
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda assignment: assignment.starting_time_grain is not None
),
Joiners.equal(
lambda left_attendance, right_attendance: left_attendance.meeting_id,
lambda assignment: assignment.meeting.id,
),
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda assignment: assignment.starting_time_grain is not None
),
Joiners.equal(
lambda left_attendance,
right_attendance,
left_assignment: right_attendance.meeting_id,
lambda assignment: assignment.meeting.id,
),
Joiners.overlapping(
lambda attendee1, attendee2, assignment: assignment.get_grain_index(),
lambda attendee1,
attendee2,
assignment: assignment.get_last_time_grain_index() + 1,
lambda assignment: assignment.get_grain_index(),
lambda assignment: assignment.get_last_time_grain_index() + 1,
),
)
.penalize(
HardMediumSoftScore.ONE_MEDIUM,
lambda left_attendance,
right_attendance,
left_assignment,
right_assignment: right_assignment.calculate_overlap(left_assignment),
)
.as_constraint("Preferred attendance conflict")
)
# ************************************************************************
# Soft constraints
# ************************************************************************
def do_meetings_as_soon_as_possible(
constraint_factory: ConstraintFactory,
) -> Constraint:
"""
Soft constraint: Encourages scheduling meetings earlier in the available time slots.
Penalizes meetings scheduled later in the available time grains, proportional to their end time.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
)
.penalize(
HardMediumSoftScore.ONE_SOFT,
lambda meeting_assignment: meeting_assignment.get_last_time_grain_index(),
)
.as_constraint("Do all meetings as soon as possible")
)
def one_break_between_consecutive_meetings(
constraint_factory: ConstraintFactory,
) -> Constraint:
"""
Soft constraint: Penalizes consecutive meetings without a break.
Penalizes pairs of meetings that are scheduled consecutively without at least one time grain break between them.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda assignment: assignment.starting_time_grain is not None
),
Joiners.equal(
lambda left_assignment: left_assignment.get_last_time_grain_index(),
lambda right_assignment: right_assignment.get_grain_index() - 1,
),
)
.penalize(HardMediumSoftScore.of_soft(100))
.as_constraint("One TimeGrain break between two consecutive meetings")
)
def overlapping_meetings(constraint_factory: ConstraintFactory) -> Constraint:
"""
Soft constraint: Discourages overlapping meetings, even in different rooms.
Penalizes pairs of meetings that overlap in time, regardless of room, proportional to the overlap duration.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
)
.join(
constraint_factory.for_each_including_unassigned(MeetingAssignment).filter(
lambda meeting_assignment: meeting_assignment.starting_time_grain
is not None
),
Joiners.greater_than(
lambda left_assignment: left_assignment.meeting.id,
lambda right_assignment: right_assignment.meeting.id,
),
Joiners.overlapping(
lambda assignment: assignment.get_grain_index(),
lambda assignment: assignment.get_last_time_grain_index() + 1,
),
)
.penalize(
HardMediumSoftScore.of_soft(10),
lambda left_assignment, right_assignment: left_assignment.calculate_overlap(
right_assignment
),
)
.as_constraint("Overlapping meetings")
)
def assign_larger_rooms_first(constraint_factory: ConstraintFactory) -> Constraint:
"""
Soft constraint: Penalizes using smaller rooms when larger rooms are available.
Penalizes when a meeting is assigned to a room while larger rooms exist, proportional to the capacity difference.
Args:
constraint_factory (ConstraintFactory): The constraint factory.
Returns:
Constraint: The defined constraint.
"""
return (
constraint_factory.for_each_including_unassigned(MeetingAssignment)
.filter(lambda meeting_assignment: meeting_assignment.room is not None)
.join(
Room,
Joiners.less_than(
lambda meeting_assignment: meeting_assignment.get_room_capacity(),
lambda room: room.capacity,
),
)
.penalize(
HardMediumSoftScore.ONE_SOFT,
lambda meeting_assignment, room: room.capacity
- meeting_assignment.get_room_capacity(),
)
.as_constraint("Assign larger rooms first")
)
def room_stability(constraint_factory: ConstraintFactory) -> Constraint:
"""
Soft constraint: Encourages room stability for people attending multiple meetings.
Penalizes when a person attends meetings in different rooms that are close in time.
Uses weighted penalty: back-to-back room switches cost more than switches with gaps.
"""
return (
constraint_factory.for_each(Attendance)
.join(
Attendance,
Joiners.equal(lambda a: a.person),
Joiners.filtering(lambda left, right: left.meeting_id != right.meeting_id),
)
.join(
MeetingAssignment,
Joiners.equal(
lambda left, right: left.meeting_id,
lambda assignment: assignment.meeting.id,
),
)
.join(
MeetingAssignment,
Joiners.equal(
lambda left, right, left_assignment: right.meeting_id,
lambda assignment: assignment.meeting.id,
),
Joiners.less_than(
lambda left, right, left_assignment: left_assignment.get_grain_index(),
lambda assignment: assignment.get_grain_index(),
),
Joiners.filtering(
lambda left, right, left_assignment, right_assignment: left_assignment.room != right_assignment.room
),
Joiners.filtering(
lambda left, right, left_assignment, right_assignment: right_assignment.get_grain_index()
- left_assignment.meeting.duration_in_grains
- left_assignment.get_grain_index()
<= 2
),
)
.penalize(
HardMediumSoftScore.ONE_SOFT,
lambda left, right, left_assignment, right_assignment: 3 - (
right_assignment.get_grain_index()
- left_assignment.meeting.duration_in_grains
- left_assignment.get_grain_index()
),
)
.as_constraint("Room stability")
)