|
| 1 | +# pylint: disable=missing-function-docstring, redefined-outer-name |
| 2 | +import pytest |
| 3 | + |
| 4 | +from job_shop_lib import ( |
| 5 | + JobShopInstance, |
| 6 | + Operation, |
| 7 | + Schedule, |
| 8 | + ScheduledOperation, |
| 9 | +) |
| 10 | +from job_shop_lib.metaheuristics import ( |
| 11 | + get_makespan_with_penalties_objective, |
| 12 | + compute_penalty_for_deadlines, |
| 13 | + compute_penalty_for_due_dates, |
| 14 | +) |
| 15 | + |
| 16 | + |
| 17 | +@pytest.fixture |
| 18 | +def schedule_no_penalties() -> Schedule: |
| 19 | + # Two machines; set due_date/deadline None |
| 20 | + jobs = [[Operation(0, 2)], [Operation(1, 3)]] |
| 21 | + instance = JobShopInstance(jobs, name="NoPenalties") |
| 22 | + # Build schedule manually: M0: job0@t0..2; M1: job1@t0..3 |
| 23 | + s0 = ScheduledOperation(instance.jobs[0][0], start_time=0, machine_id=0) |
| 24 | + s1 = ScheduledOperation(instance.jobs[1][0], start_time=0, machine_id=1) |
| 25 | + schedule = Schedule(instance, [[s0], [s1]]) |
| 26 | + return schedule |
| 27 | + |
| 28 | + |
| 29 | +@pytest.fixture |
| 30 | +def schedule_with_deadlines() -> Schedule: |
| 31 | + # Single machine sequence; second op violates deadline |
| 32 | + jobs = [ |
| 33 | + [Operation(0, 2, deadline=1)], # ends at 2 -> violation |
| 34 | + [Operation(0, 3, deadline=5)], # ends at 5 -> boundary, no violation |
| 35 | + ] |
| 36 | + instance = JobShopInstance(jobs, name="Deadlines") |
| 37 | + s0 = ScheduledOperation(instance.jobs[0][0], start_time=0, machine_id=0) |
| 38 | + s1 = ScheduledOperation(instance.jobs[1][0], start_time=2, machine_id=0) |
| 39 | + schedule = Schedule(instance, [[s0, s1]]) |
| 40 | + return schedule |
| 41 | + |
| 42 | + |
| 43 | +@pytest.fixture |
| 44 | +def schedule_with_due_dates() -> Schedule: |
| 45 | + # Single machine sequence; first op OK, second violates due date |
| 46 | + jobs = [ |
| 47 | + [Operation(0, 1, due_date=1)], # ends at 1 -> equal, OK |
| 48 | + [Operation(0, 4, due_date=3)], # ends at 5 -> violation |
| 49 | + ] |
| 50 | + instance = JobShopInstance(jobs, name="DueDates") |
| 51 | + s0 = ScheduledOperation(instance.jobs[0][0], start_time=0, machine_id=0) |
| 52 | + s1 = ScheduledOperation(instance.jobs[1][0], start_time=1, machine_id=0) |
| 53 | + schedule = Schedule(instance, [[s0, s1]]) |
| 54 | + return schedule |
| 55 | + |
| 56 | + |
| 57 | +@pytest.fixture |
| 58 | +def schedule_with_both() -> Schedule: |
| 59 | + # Mixed: first violates deadline, second violates due date |
| 60 | + jobs = [ |
| 61 | + [Operation(0, 3, deadline=2, due_date=10)], # deadline violation |
| 62 | + [Operation(0, 4, deadline=10, due_date=6)], # due date violation |
| 63 | + ] |
| 64 | + instance = JobShopInstance(jobs, name="Both") |
| 65 | + s0 = ScheduledOperation(instance.jobs[0][0], start_time=0, machine_id=0) |
| 66 | + s1 = ScheduledOperation(instance.jobs[1][0], start_time=3, machine_id=0) |
| 67 | + schedule = Schedule(instance, [[s0, s1]]) |
| 68 | + return schedule |
| 69 | + |
| 70 | + |
| 71 | +def test_compute_penalty_for_deadlines_none(schedule_no_penalties: Schedule): |
| 72 | + assert compute_penalty_for_deadlines(schedule_no_penalties, 1000) == 0.0 |
| 73 | + |
| 74 | + |
| 75 | +def test_compute_penalty_for_due_dates_none(schedule_no_penalties: Schedule): |
| 76 | + assert compute_penalty_for_due_dates(schedule_no_penalties, 100) == 0.0 |
| 77 | + |
| 78 | + |
| 79 | +def test_compute_penalty_for_deadlines(schedule_with_deadlines: Schedule): |
| 80 | + # Only first op violates -> 1 penalty |
| 81 | + assert compute_penalty_for_deadlines(schedule_with_deadlines, 7.5) == 7.5 |
| 82 | + |
| 83 | + |
| 84 | +def test_compute_penalty_for_due_dates(schedule_with_due_dates: Schedule): |
| 85 | + # Only second op violates -> 1 penalty |
| 86 | + assert compute_penalty_for_due_dates(schedule_with_due_dates, 3.0) == 3.0 |
| 87 | + |
| 88 | + |
| 89 | +def test_objective_makespan_only_when_zero_factors( |
| 90 | + schedule_with_both: Schedule, |
| 91 | +): |
| 92 | + objective = get_makespan_with_penalties_objective( |
| 93 | + deadline_penalty_factor=0, due_date_penalty_factor=0 |
| 94 | + ) |
| 95 | + assert objective(schedule_with_both) == schedule_with_both.makespan() |
| 96 | + |
| 97 | + |
| 98 | +def test_objective_with_penalties(schedule_with_both: Schedule): |
| 99 | + # s0: 0..3 (violates deadline=2) -> +d_factor |
| 100 | + # s1: 3..7 (violates due_date=6) -> +dd_factor |
| 101 | + d_factor = 123.0 |
| 102 | + dd_factor = 4.0 |
| 103 | + objective = get_makespan_with_penalties_objective( |
| 104 | + deadline_penalty_factor=d_factor, |
| 105 | + due_date_penalty_factor=dd_factor, |
| 106 | + ) |
| 107 | + expected = schedule_with_both.makespan() + d_factor + dd_factor |
| 108 | + assert objective(schedule_with_both) == expected |
0 commit comments