|
| 1 | +#include "gtest/gtest.h" |
| 2 | +#include "jres_solver/jres_solver.hpp" |
| 3 | +#include "nlohmann/json.hpp" |
| 4 | +#include <vector> |
| 5 | +#include <string> |
| 6 | + |
| 7 | +using json = nlohmann::json; |
| 8 | + |
| 9 | +TEST(OptimizationTest, IncentivizeConsecutiveStints) { |
| 10 | + // Scenario: 4 Drivers, 8 Stints. |
| 11 | + // Each driver can do max 2 stints. |
| 12 | + // Optimal with incentive (Max Consecutive): Driver A (1,2), Driver B (3,4), Driver C (5,6), Driver D (7,8) |
| 13 | + // This results in 4 consecutive pairs: (1-2), (3-4), (5-6), (7-8). |
| 14 | + // Suboptimal (Alternating): A, B, A, B... results in 0 consecutive pairs. |
| 15 | + |
| 16 | + json j; |
| 17 | + j["success"] = true; |
| 18 | + |
| 19 | + json drivers = json::array(); |
| 20 | + std::vector<std::string> names = {"Driver A", "Driver B", "Driver C", "Driver D"}; |
| 21 | + for (const auto& name : names) { |
| 22 | + drivers.push_back({ |
| 23 | + {"name", name}, |
| 24 | + {"isDriver", true}, |
| 25 | + {"isSpotter", false}, |
| 26 | + {"maxStints", 2}, |
| 27 | + {"minimumRestHours", 0} |
| 28 | + }); |
| 29 | + } |
| 30 | + j["teamMembers"] = drivers; |
| 31 | + |
| 32 | + json stints = json::array(); |
| 33 | + for (int i = 0; i < 8; ++i) { |
| 34 | + stints.push_back({ |
| 35 | + {"id", i + 1}, |
| 36 | + {"startTime", "2026-01-17T0" + std::to_string(i) + ":00:00.000Z"}, |
| 37 | + {"endTime", "2026-01-17T0" + std::to_string(i+1) + ":00:00.000Z"} |
| 38 | + }); |
| 39 | + } |
| 40 | + j["stints"] = stints; |
| 41 | + j["availability"] = json::object(); // All available |
| 42 | + j["firstStintDriver"] = nullptr; |
| 43 | + |
| 44 | + std::string json_str = j.dump(); |
| 45 | + |
| 46 | + JresSolverOptions options; |
| 47 | + options.timeLimit = 10; |
| 48 | + options.spotterMode = JRES_SPOTTER_MODE_NONE; // Focus on driver optimization |
| 49 | + options.allowNoSpotter = true; |
| 50 | + options.optimalityGap = 0.0; |
| 51 | + |
| 52 | + JresSolverInput* input = jres_input_from_json(json_str.c_str()); |
| 53 | + ASSERT_NE(input, nullptr); |
| 54 | + |
| 55 | + JresSolverOutput* output = solve_race_schedule(input, &options); |
| 56 | + ASSERT_NE(output, nullptr); |
| 57 | + ASSERT_EQ(output->schedule_len, 8); |
| 58 | + |
| 59 | + int consecutive_count = 0; |
| 60 | + for (int i = 0; i < output->schedule_len - 1; ++i) { |
| 61 | + std::string current = output->schedule[i].driver; |
| 62 | + std::string next = output->schedule[i+1].driver; |
| 63 | + if (current == next) { |
| 64 | + consecutive_count++; |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + // We expect the solver to maximize consecutive stints. |
| 69 | + // In a perfect 2-stint blocks schedule: A A B B C C D D |
| 70 | + // Pairs: (A,A), (A,B), (B,B), (B,C), (C,C), (C,D), (D,D) |
| 71 | + // Consecutive: 4. |
| 72 | + // We accept at least 3 to allow for some minor variation, but definitely > 0. |
| 73 | + EXPECT_GE(consecutive_count, 3) << "Solver should prioritize consecutive stints. Found " << consecutive_count << " consecutive pairs."; |
| 74 | + |
| 75 | + free_jres_solver_input(input); |
| 76 | + free_jres_solver_output(output); |
| 77 | +} |
| 78 | + |
| 79 | +TEST(OptimizationTest, PreferredOverAvailable) { |
| 80 | + // Scenario: 2 Drivers, 2 Stints. maxStints=1 (Disable consecutive bonus). |
| 81 | + // Driver A: Stint 1 (Available), Stint 2 (Preferred) |
| 82 | + // Driver B: Stint 1 (Preferred), Stint 2 (Available) |
| 83 | + // |
| 84 | + // Naive/Round-Robin/Alphabetical Order might try: A then B. |
| 85 | + // - S1 (A, Avail) + S2 (B, Avail) -> Cost 0. |
| 86 | + // |
| 87 | + // Optimal Preference Order: B then A. |
| 88 | + // - S1 (B, Pref) + S2 (A, Pref) -> Cost -2. |
| 89 | + // |
| 90 | + // This forces the solver to pick B first, proving it's looking at the "Preferred" weight |
| 91 | + // and not just assigning in list order. |
| 92 | + |
| 93 | + json j; |
| 94 | + j["success"] = true; |
| 95 | + j["teamMembers"] = { |
| 96 | + {{"name", "Driver A"}, {"isDriver", true}, {"isSpotter", false}, {"maxStints", 1}, {"minimumRestHours", 0}}, |
| 97 | + {{"name", "Driver B"}, {"isDriver", true}, {"isSpotter", false}, {"maxStints", 1}, {"minimumRestHours", 0}} |
| 98 | + }; |
| 99 | + j["stints"] = { |
| 100 | + {{"id", 1}, {"startTime", "2026-01-17T00:00:00.000Z"}, {"endTime", "2026-01-17T01:00:00.000Z"}}, |
| 101 | + {{"id", 2}, {"startTime", "2026-01-17T01:00:00.000Z"}, {"endTime", "2026-01-17T02:00:00.000Z"}} |
| 102 | + }; |
| 103 | + j["availability"] = { |
| 104 | + {"Driver A", {{"2026-01-17T00:00:00.000Z", "Available"}, {"2026-01-17T01:00:00.000Z", "Preferred"}}}, |
| 105 | + {"Driver B", {{"2026-01-17T00:00:00.000Z", "Preferred"}, {"2026-01-17T01:00:00.000Z", "Available"}}} |
| 106 | + }; |
| 107 | + j["firstStintDriver"] = nullptr; |
| 108 | + |
| 109 | + JresSolverOptions options; |
| 110 | + options.timeLimit = 5; |
| 111 | + options.spotterMode = JRES_SPOTTER_MODE_NONE; |
| 112 | + options.allowNoSpotter = true; |
| 113 | + options.optimalityGap = 0.0; |
| 114 | + |
| 115 | + JresSolverInput* input = jres_input_from_json(j.dump().c_str()); |
| 116 | + ASSERT_NE(input, nullptr); |
| 117 | + |
| 118 | + JresSolverOutput* output = solve_race_schedule(input, &options); |
| 119 | + ASSERT_NE(output, nullptr); |
| 120 | + ASSERT_EQ(output->schedule_len, 2); |
| 121 | + |
| 122 | + // Expect B then A |
| 123 | + EXPECT_STREQ(output->schedule[0].driver, "Driver B") << "Stint 1 should be Driver B (Preferred)"; |
| 124 | + EXPECT_STREQ(output->schedule[1].driver, "Driver A") << "Stint 2 should be Driver A (Preferred)"; |
| 125 | + |
| 126 | + free_jres_solver_input(input); |
| 127 | + free_jres_solver_output(output); |
| 128 | +} |
| 129 | + |
| 130 | +TEST(OptimizationTest, ConsecutiveOverPreferred) { |
| 131 | + // Scenario: 2 Drivers, 4 Stints. |
| 132 | + // Availability Pattern (Alternating Preference): |
| 133 | + // Stint 1: A=Pref, B=Avail |
| 134 | + // Stint 2: A=Avail, B=Pref |
| 135 | + // Stint 3: A=Pref, B=Avail |
| 136 | + // Stint 4: A=Avail, B=Pref |
| 137 | + // |
| 138 | + // Option 1 (Alternating/Split): A, B, A, B |
| 139 | + // - Everyone gets their Preferred slots. |
| 140 | + // - Total Preferred = 4. Cost = -4.0. |
| 141 | + // - Consecutive Pairs = 0. Cost = 0.0. |
| 142 | + // - Balance: Perfect (2 each). Cost = 0. |
| 143 | + // - Total Cost = -4.0. |
| 144 | + // |
| 145 | + // Option 2 (Consecutive Blocks): A, A, B, B |
| 146 | + // - A takes S1(Pref), S2(Avail). B takes S3(Avail), S4(Pref). |
| 147 | + // - Total Preferred = 2. Cost = -2.0. |
| 148 | + // - Consecutive Pairs = 2 (A-A, B-B). Cost = 2 * -1.5 = -3.0. |
| 149 | + // - Balance: Perfect (2 each). Cost = 0. |
| 150 | + // - Total Cost = -5.0. |
| 151 | + // |
| 152 | + // Since -5.0 < -4.0, the solver MUST choose Option 2 (Consecutive Blocks). |
| 153 | + // This proves that the Consecutive Bonus (-1.5) outweighs the loss of a Preferred slot (1.0 difference). |
| 154 | + |
| 155 | + json j; |
| 156 | + j["success"] = true; |
| 157 | + j["teamMembers"] = { |
| 158 | + {{"name", "Driver A"}, {"isDriver", true}, {"isSpotter", false}, {"maxStints", 2}, {"minimumRestHours", 0}}, |
| 159 | + {{"name", "Driver B"}, {"isDriver", true}, {"isSpotter", false}, {"maxStints", 2}, {"minimumRestHours", 0}} |
| 160 | + }; |
| 161 | + j["stints"] = { |
| 162 | + {{"id", 1}, {"startTime", "2026-01-17T00:00:00.000Z"}, {"endTime", "2026-01-17T01:00:00.000Z"}}, |
| 163 | + {{"id", 2}, {"startTime", "2026-01-17T01:00:00.000Z"}, {"endTime", "2026-01-17T02:00:00.000Z"}}, |
| 164 | + {{"id", 3}, {"startTime", "2026-01-17T02:00:00.000Z"}, {"endTime", "2026-01-17T03:00:00.000Z"}}, |
| 165 | + {{"id", 4}, {"startTime", "2026-01-17T03:00:00.000Z"}, {"endTime", "2026-01-17T04:00:00.000Z"}} |
| 166 | + }; |
| 167 | + j["availability"] = { |
| 168 | + {"Driver A", { |
| 169 | + {"2026-01-17T00:00:00.000Z", "Preferred"}, |
| 170 | + {"2026-01-17T01:00:00.000Z", "Available"}, |
| 171 | + {"2026-01-17T02:00:00.000Z", "Preferred"}, |
| 172 | + {"2026-01-17T03:00:00.000Z", "Available"} |
| 173 | + }}, |
| 174 | + {"Driver B", { |
| 175 | + {"2026-01-17T00:00:00.000Z", "Available"}, |
| 176 | + {"2026-01-17T01:00:00.000Z", "Preferred"}, |
| 177 | + {"2026-01-17T02:00:00.000Z", "Available"}, |
| 178 | + {"2026-01-17T03:00:00.000Z", "Preferred"} |
| 179 | + }} |
| 180 | + }; |
| 181 | + j["firstStintDriver"] = nullptr; |
| 182 | + |
| 183 | + JresSolverOptions options; |
| 184 | + options.timeLimit = 5; |
| 185 | + options.spotterMode = JRES_SPOTTER_MODE_NONE; |
| 186 | + options.allowNoSpotter = true; |
| 187 | + options.optimalityGap = 0.0; |
| 188 | + |
| 189 | + JresSolverInput* input = jres_input_from_json(j.dump().c_str()); |
| 190 | + ASSERT_NE(input, nullptr); |
| 191 | + |
| 192 | + JresSolverOutput* output = solve_race_schedule(input, &options); |
| 193 | + ASSERT_NE(output, nullptr); |
| 194 | + ASSERT_EQ(output->schedule_len, 4); |
| 195 | + |
| 196 | + // Check for consecutive blocks |
| 197 | + std::string d1 = output->schedule[0].driver; |
| 198 | + std::string d2 = output->schedule[1].driver; |
| 199 | + std::string d3 = output->schedule[2].driver; |
| 200 | + std::string d4 = output->schedule[3].driver; |
| 201 | + |
| 202 | + // We expect pairs like AA BB or BB AA |
| 203 | + EXPECT_EQ(d1, d2) << "Stints 1 and 2 should be consecutive"; |
| 204 | + EXPECT_EQ(d3, d4) << "Stints 3 and 4 should be consecutive"; |
| 205 | + EXPECT_NE(d2, d3) << "Drivers should switch between blocks"; |
| 206 | + |
| 207 | + free_jres_solver_input(input); |
| 208 | + free_jres_solver_output(output); |
| 209 | +} |
0 commit comments