CoolFace
Apppublic

blackopsrepl/meeting-scheduling-python

sourceHugging Faceapache-2.0updated 9mo agoView on Hugging Face
1likes
test_constraints.py563 linesDownload Raw Back to tests
1from solverforge_legacy.solver.test import ConstraintVerifier2 3from meeting_scheduling.domain import (4    Meeting,5    MeetingAssignment,6    MeetingSchedule,7    Person,8    PreferredAttendance,9    RequiredAttendance,10    Room,11    TimeGrain,12)13from meeting_scheduling.constraints import (14    define_constraints,15    room_conflict,16    avoid_overtime,17    required_attendance_conflict,18    required_room_capacity,19    start_and_end_on_same_day,20    required_and_preferred_attendance_conflict,21    preferred_attendance_conflict,22    room_stability,23)24 25 26DEFAULT_TIME_GRAINS = [27    TimeGrain(28        id=str(i + 1), grain_index=i, day_of_year=1, starting_minute_of_day=480 + i * 1529    )30    for i in range(8)31]32 33DEFAULT_ROOM = Room(id="1", name="Room 1", capacity=10)34SMALL_ROOM = Room(id="2", name="Small Room", capacity=1)35LARGE_ROOM = Room(id="3", name="Large Room", capacity=2)36ROOM_A = Room(id="4", name="Room A", capacity=10)37ROOM_B = Room(id="5", name="Room B", capacity=10)38 39 40constraint_verifier = ConstraintVerifier.build(41    define_constraints, MeetingSchedule, MeetingAssignment42)43 44 45def test_room_conflict_unpenalized():46    """Test that no penalty is applied when meetings in the same room do not overlap."""47    meeting1 = create_meeting(1)48    left_assignment = create_meeting_assignment(49        0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM50    )51 52    meeting2 = create_meeting(2)53    right_assignment = create_meeting_assignment(54        1, meeting2, DEFAULT_TIME_GRAINS[4], DEFAULT_ROOM55    )56 57    constraint_verifier.verify_that(room_conflict).given(58        left_assignment, right_assignment59    ).penalizes(0)60 61 62def test_room_conflict_penalized():63    """Test that a penalty is applied when meetings in the same room overlap."""64    meeting1 = create_meeting(1)65    left_assignment = create_meeting_assignment(66        0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM67    )68 69    meeting2 = create_meeting(2)70    right_assignment = create_meeting_assignment(71        1, meeting2, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM72    )73 74    constraint_verifier.verify_that(room_conflict).given(75        left_assignment, right_assignment76    ).penalizes_by(2)77 78 79def test_avoid_overtime_unpenalized():80    """Test that no penalty is applied when a meeting fits within available time grains (no overtime)."""81    meeting = create_meeting(1)82    meeting_assignment = create_meeting_assignment(83        0, meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM84    )85 86    constraint_verifier.verify_that(avoid_overtime).given(87        meeting_assignment, *DEFAULT_TIME_GRAINS88    ).penalizes(0)89 90 91def test_avoid_overtime_penalized():92    """Test that a penalty is applied when a meeting exceeds available time grains (overtime)."""93    meeting = create_meeting(1)94    meeting_assignment = create_meeting_assignment(95        0, meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM96    )97 98    constraint_verifier.verify_that(avoid_overtime).given(99        meeting_assignment100    ).penalizes_by(3)101 102 103def test_required_attendance_conflict_unpenalized():104    """Test that no penalty is applied when a person does not have overlapping required meetings."""105    person = create_person(1)106 107    left_meeting = create_meeting(1, duration=2)108    required_attendance1 = create_required_attendance(0, person, left_meeting)109 110    right_meeting = create_meeting(2, duration=2)111    required_attendance2 = create_required_attendance(1, person, right_meeting)112 113    left_assignment = create_meeting_assignment(114        0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM115    )116    right_assignment = create_meeting_assignment(117        1, right_meeting, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM118    )119 120    constraint_verifier.verify_that(required_attendance_conflict).given(121        required_attendance1, required_attendance2, left_assignment, right_assignment122    ).penalizes(0)123 124 125def test_required_attendance_conflict_penalized():126    """Test that a penalty is applied when a person has overlapping required meetings."""127    person = create_person(1)128 129    left_meeting = create_meeting(1, duration=2)130    required_attendance1 = create_required_attendance(0, person, left_meeting)131 132    right_meeting = create_meeting(2, duration=2)133    required_attendance2 = create_required_attendance(1, person, right_meeting)134 135    left_assignment = create_meeting_assignment(136        0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM137    )138    right_assignment = create_meeting_assignment(139        1, right_meeting, DEFAULT_TIME_GRAINS[1], DEFAULT_ROOM140    )141 142    constraint_verifier.verify_that(required_attendance_conflict).given(143        required_attendance1, required_attendance2, left_assignment, right_assignment144    ).penalizes_by(1)145 146 147def test_required_room_capacity_unpenalized():148    """Test that no penalty is applied when the room has enough capacity for all required and preferred attendees."""149    person1 = create_person(1)150    person2 = create_person(2)151 152    meeting = create_meeting(1, duration=2)153    create_required_attendance(0, person1, meeting)154    create_preferred_attendance(1, person2, meeting)155 156    meeting_assignment = create_meeting_assignment(157        0, meeting, DEFAULT_TIME_GRAINS[0], LARGE_ROOM158    )159 160    constraint_verifier.verify_that(required_room_capacity).given(161        meeting_assignment162    ).penalizes(0)163 164 165def test_required_room_capacity_penalized():166    """Test that a penalty is applied when the room does not have enough capacity for all required and preferred attendees."""167    person1 = create_person(1)168    person2 = create_person(2)169 170    meeting = create_meeting(1, duration=2)171    create_required_attendance(0, person1, meeting)172    create_preferred_attendance(1, person2, meeting)173 174    meeting_assignment = create_meeting_assignment(175        0, meeting, DEFAULT_TIME_GRAINS[0], SMALL_ROOM176    )177 178    constraint_verifier.verify_that(required_room_capacity).given(179        meeting_assignment180    ).penalizes_by(1)181 182 183def test_start_and_end_on_same_day_unpenalized():184    """Test that no penalty is applied when a meeting starts and ends on the same day."""185    # Need custom time grains with day_of_year=0 (DEFAULT_TIME_GRAINS use day_of_year=1)186    start_time_grain = TimeGrain(187        id="1", grain_index=0, day_of_year=0, starting_minute_of_day=480188    )189    end_time_grain = TimeGrain(190        id="2", grain_index=3, day_of_year=0, starting_minute_of_day=525191    )  # Same day192 193    meeting = create_meeting(1)194    meeting_assignment = create_meeting_assignment(195        0, meeting, start_time_grain, DEFAULT_ROOM196    )197 198    constraint_verifier.verify_that(start_and_end_on_same_day).given(199        meeting_assignment, end_time_grain200    ).penalizes(0)201 202 203def test_start_and_end_on_same_day_penalized():204    """Test that a penalty is applied when a meeting starts and ends on different days."""205    # Need custom time grains to test different days (start=day 0, end=day 1)206    start_time_grain = TimeGrain(207        id="1", grain_index=0, day_of_year=0, starting_minute_of_day=480208    )209    end_time_grain = TimeGrain(210        id="2", grain_index=3, day_of_year=1, starting_minute_of_day=525211    )  # Different day212 213    meeting = create_meeting(1)214    meeting_assignment = create_meeting_assignment(215        0, meeting, start_time_grain, DEFAULT_ROOM216    )217 218    constraint_verifier.verify_that(start_and_end_on_same_day).given(219        meeting_assignment, end_time_grain220    ).penalizes_by(1)221 222 223def test_multiple_constraint_violations():224    """Test that multiple constraints can be violated simultaneously."""225    person = create_person(1)226 227    left_meeting = create_meeting(1)228    required_attendance1 = create_required_attendance(0, person, left_meeting)229    left_assignment = create_meeting_assignment(230        0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM231    )232 233    right_meeting = create_meeting(2)234    required_attendance2 = create_required_attendance(1, person, right_meeting)235    right_assignment = create_meeting_assignment(236        1, right_meeting, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM237    )238 239    constraint_verifier.verify_that(room_conflict).given(240        left_assignment, right_assignment241    ).penalizes_by(2)242    constraint_verifier.verify_that(required_attendance_conflict).given(243        required_attendance1, required_attendance2, left_assignment, right_assignment244    ).penalizes_by(2)245 246 247### Helper functions ###248 249 250def create_meeting(id, topic="Meeting", duration=4):251    """Helper to create a meeting with standard parameters."""252    return Meeting(id=str(id), topic=f"{topic} {id}", duration_in_grains=duration)253 254 255def create_meeting_assignment(id, meeting, time_grain, room):256    """Helper to create a meeting assignment."""257    return MeetingAssignment(258        id=str(id), meeting=meeting, starting_time_grain=time_grain, room=room259    )260 261 262def create_person(id):263    """Helper to create a person."""264    return Person(id=str(id), full_name=f"Person {id}")265 266 267def create_required_attendance(id, person, meeting):268    """Helper to create and link required attendance."""269    attendance = RequiredAttendance(id=str(id), person=person, meeting_id=meeting.id)270    meeting.required_attendances = [attendance]271    return attendance272 273 274def create_preferred_attendance(id, person, meeting):275    """Helper to create and link preferred attendance."""276    attendance = PreferredAttendance(id=str(id), person=person, meeting_id=meeting.id)277    meeting.preferred_attendances = [attendance]278    return attendance279 280 281# ========================================282# Required and Preferred Attendance Conflict Tests283# ========================================284 285 286def test_required_and_preferred_attendance_conflict_unpenalized():287    """Test no penalty when required and preferred meetings don't overlap."""288    person = create_person(1)289 290    # Meeting 1: grain 0-3 (duration=4), person required291    meeting1 = create_meeting(1, duration=4)292    attendance1 = create_required_attendance(0, person, meeting1)293    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM)294 295    # Meeting 2: grain 4-7 (duration=4), person preferred296    meeting2 = create_meeting(2, duration=4)297    attendance2 = create_preferred_attendance(1, person, meeting2)298    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[4], ROOM_A)299 300    constraint_verifier.verify_that(required_and_preferred_attendance_conflict).given(301        attendance1, attendance2, assignment1, assignment2302    ).penalizes_by(0)303 304 305def test_required_and_preferred_attendance_conflict_penalized():306    """Test penalty when person required at one meeting and preferred at overlapping meeting."""307    person = create_person(1)308 309    # Meeting 1: grain 0-3 (duration=4), person required310    meeting1 = create_meeting(1, duration=4)311    attendance1 = create_required_attendance(0, person, meeting1)312    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM)313 314    # Meeting 2: grain 2-5 (duration=4), person preferred, overlaps grains 2-3 (2 grains)315    meeting2 = create_meeting(2, duration=4)316    attendance2 = create_preferred_attendance(1, person, meeting2)317    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[2], ROOM_A)318 319    # Overlap: grains 2-3 = 2 grains320    constraint_verifier.verify_that(required_and_preferred_attendance_conflict).given(321        attendance1, attendance2, assignment1, assignment2322    ).penalizes_by(2)323 324 325# ========================================326# Preferred Attendance Conflict Tests327# ========================================328 329 330def test_preferred_attendance_conflict_unpenalized():331    """Test no penalty when preferred attendee has non-overlapping meetings."""332    person = create_person(1)333 334    # Meeting 1: grain 0-3 (duration=4), person preferred335    meeting1 = create_meeting(1, duration=4)336    attendance1 = create_preferred_attendance(0, person, meeting1)337    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM)338 339    # Meeting 2: grain 4-7 (duration=4), person preferred340    meeting2 = create_meeting(2, duration=4)341    attendance2 = create_preferred_attendance(1, person, meeting2)342    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[4], ROOM_A)343 344    constraint_verifier.verify_that(preferred_attendance_conflict).given(345        attendance1, attendance2, assignment1, assignment2346    ).penalizes_by(0)347 348 349def test_preferred_attendance_conflict_penalized():350    """Test penalty when person preferred at multiple overlapping meetings."""351    person = create_person(1)352 353    # Meeting 1: grain 0-3 (duration=4), person preferred354    meeting1 = create_meeting(1, duration=4)355    attendance1 = create_preferred_attendance(0, person, meeting1)356    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM)357 358    # Meeting 2: grain 1-4 (duration=4), person preferred, overlaps grains 1-3 (3 grains)359    meeting2 = create_meeting(2, duration=4)360    attendance2 = create_preferred_attendance(1, person, meeting2)361    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[1], ROOM_A)362 363    # Overlap: grains 1-3 = 3 grains364    constraint_verifier.verify_that(preferred_attendance_conflict).given(365        attendance1, attendance2, assignment1, assignment2366    ).penalizes_by(3)367 368 369# ========================================370# Room Stability Tests371# ========================================372 373 374def test_room_stability_same_room_no_penalty():375    """376    Test that no penalty is applied when a person attends consecutive377    meetings in the same room (stability is maintained).378    """379    person = create_person(1)380 381    # Meeting 1: time grain 0-1 (duration=2) in ROOM_A382    meeting1 = create_meeting(1, duration=2)383    attendance1 = create_required_attendance(0, person, meeting1)384    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], ROOM_A)385 386    # Meeting 2: time grain 3-4 (duration=2) in ROOM_A (same room, gap of 1)387    meeting2 = create_meeting(2, duration=2)388    attendance2 = create_required_attendance(1, person, meeting2)389    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[3], ROOM_A)390 391    # Same room should not penalize392    constraint_verifier.verify_that(room_stability).given(393        attendance1, attendance2, assignment1, assignment2394    ).penalizes(0)395 396 397def test_room_stability_different_room_with_required_attendance():398    """399    Test that a penalty is applied when a person with required attendance400    has to change rooms between closely scheduled meetings.401    Weighted penalty: back-to-back switches cost more than switches with gaps.402    """403    person = create_person(1)404 405    # Meeting 1: time grain 0-1 (duration=2) in ROOM_A406    left_grain_index = 0407    left_duration = 2408    meeting1 = create_meeting(1, duration=left_duration)409    attendance1 = create_required_attendance(0, person, meeting1)410    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[left_grain_index], ROOM_A)411 412    # Meeting 2: time grain 3-4 (duration=2) in ROOM_B (different room)413    right_grain_index = 3414    meeting2 = create_meeting(2, duration=2)415    attendance2 = create_required_attendance(1, person, meeting2)416    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[right_grain_index], ROOM_B)417 418    # Weighted penalty: 3 - gap, where gap = right_grain - left_duration - left_grain419    gap = right_grain_index - left_duration - left_grain_index420    expected_penalty = 3 - gap421 422    constraint_verifier.verify_that(room_stability).given(423        attendance1, attendance2, assignment1, assignment2424    ).penalizes_by(expected_penalty)425 426 427def test_room_stability_different_room_with_preferred_attendance():428    """429    Test that a penalty is applied when a person with preferred attendance430    has to change rooms between closely scheduled meetings.431    Weighted penalty applies to preferred attendance too.432    """433    person = create_person(1)434 435    # Meeting 1: time grain 0-1 (duration=2) in ROOM_A436    left_grain_index = 0437    left_duration = 2438    meeting1 = create_meeting(1, duration=left_duration)439    attendance1 = create_preferred_attendance(0, person, meeting1)440    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[left_grain_index], ROOM_A)441 442    # Meeting 2: time grain 3-4 (duration=2) in ROOM_B (different room)443    right_grain_index = 3444    meeting2 = create_meeting(2, duration=2)445    attendance2 = create_preferred_attendance(1, person, meeting2)446    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[right_grain_index], ROOM_B)447 448    # Weighted penalty: 3 - gap449    gap = right_grain_index - left_duration - left_grain_index450    expected_penalty = 3 - gap451 452    constraint_verifier.verify_that(room_stability).given(453        attendance1, attendance2, assignment1, assignment2454    ).penalizes_by(expected_penalty)455 456 457def test_room_stability_mixed_attendance_types():458    """459    Test that room stability penalty applies when mixing required and preferred460    attendance types for the same person.461    """462    person = create_person(1)463 464    # Meeting 1 with required attendance465    left_grain_index = 0466    left_duration = 2467    meeting1 = create_meeting(1, duration=left_duration)468    required_attendance = create_required_attendance(0, person, meeting1)469    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[left_grain_index], ROOM_A)470 471    # Meeting 2 with preferred attendance472    right_grain_index = 3473    meeting2 = create_meeting(2, duration=2)474    preferred_attendance = create_preferred_attendance(1, person, meeting2)475    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[right_grain_index], ROOM_B)476 477    # Weighted penalty: 3 - gap478    gap = right_grain_index - left_duration - left_grain_index479    expected_penalty = 3 - gap480 481    constraint_verifier.verify_that(room_stability).given(482        required_attendance, preferred_attendance, assignment1, assignment2483    ).penalizes_by(expected_penalty)484 485 486def test_room_stability_far_apart_meetings_no_penalty():487    """488    Test that no penalty is applied when meetings are far apart in time,489    even if they're in different rooms.490    """491    person = create_person(1)492 493    # Meeting 1: time grain 0-1 (duration=2) in ROOM_A494    meeting1 = create_meeting(1, duration=2)495    attendance1 = create_required_attendance(0, person, meeting1)496    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], ROOM_A)497 498    # Meeting 2: time grain 6-7 (duration=2) in ROOM_B499    # gap = grain_index(6) - duration_in_grains(2) - grain_index(0) = 6 - 2 - 0 = 4 > 2500    meeting2 = create_meeting(2, duration=2)501    attendance2 = create_required_attendance(1, person, meeting2)502    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[6], ROOM_B)503 504    # Far apart meetings should not penalize even with room change505    constraint_verifier.verify_that(room_stability).given(506        attendance1, attendance2, assignment1, assignment2507    ).penalizes(0)508 509 510def test_room_stability_different_people_no_penalty():511    """512    Test that no penalty is applied when different people have meetings513    in different rooms (room stability is per-person).514    """515    person1 = create_person(1)516    person2 = create_person(2)517 518    # Person 1's meeting in ROOM_A519    meeting1 = create_meeting(1, duration=2)520    attendance1 = create_required_attendance(0, person1, meeting1)521    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], ROOM_A)522 523    # Person 2's meeting in ROOM_B (different person, should not affect stability)524    meeting2 = create_meeting(2, duration=2)525    attendance2 = create_required_attendance(1, person2, meeting2)526    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[3], ROOM_B)527 528    # Different people should not trigger room stability penalty529    constraint_verifier.verify_that(room_stability).given(530        attendance1, attendance2, assignment1, assignment2531    ).penalizes(0)532 533 534def test_room_stability_back_to_back_highest_penalty():535    """536    Test that back-to-back room switches (gap=0) incur the highest penalty (3).537    This verifies the weighted penalty gradient: closer switches cost more.538    """539    person = create_person(1)540 541    # Meeting 1: grain 0-1 (duration=2) in ROOM_A542    left_grain_index = 0543    left_duration = 2544    meeting1 = create_meeting(1, duration=left_duration)545    attendance1 = create_required_attendance(0, person, meeting1)546    assignment1 = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[left_grain_index], ROOM_A)547 548    # Meeting 2: grain 2-3 (immediately after) in ROOM_B549    right_grain_index = 2  # Starts right after meeting1 ends550    meeting2 = create_meeting(2, duration=2)551    attendance2 = create_required_attendance(1, person, meeting2)552    assignment2 = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[right_grain_index], ROOM_B)553 554    # gap = 2 - 2 - 0 = 0, penalty = 3 - 0 = 3555    gap = right_grain_index - left_duration - left_grain_index556    expected_penalty = 3 - gap557    assert gap == 0, f"Test setup error: expected gap=0, got {gap}"558    assert expected_penalty == 3, f"Test setup error: expected penalty=3, got {expected_penalty}"559 560    constraint_verifier.verify_that(room_stability).given(561        attendance1, attendance2, assignment1, assignment2562    ).penalizes_by(expected_penalty)563