Skip to content

Commit d6f54c8

Browse files
authored
Merge pull request #43 from popmonkey/max-busy-hours
maxBusyHours constraint
2 parents 04d9ce9 + ef2ce07 commit d6f54c8

14 files changed

Lines changed: 512 additions & 0 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ add_library(jres_solver_lib
164164
src/analysis/capacity_analyzer.cpp
165165
src/constraints/balancing.cpp
166166
src/constraints/minimum_rest.cpp
167+
src/constraints/max_busy_time.cpp
167168
)
168169
set_target_properties(jres_solver_lib PROPERTIES OUTPUT_NAME "jres_solver")
169170

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The C-API uses the following structs to pass data to and from the solver.
3737
| :--- | :--- | :--- |
3838
| `consecutiveStints` | `int` | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
3939
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. |
40+
| `maxBusyHours` | `int` | Hard constraint: Maximum total time (driving + spotting) a member can work before a required rest. |
4041
| `firstStintDriver` | `const char*` | Hard constraint: The name of the team member who must drive the first stint. |
4142
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members. |
4243
| `teamMembers_len` | `int` | The number of team members. |
@@ -185,6 +186,7 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
185186
| :--- | :--- | :--- | :--- |
186187
| `consecutiveStints` | Integer | No (Default `1`) | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
187188
| `minimumRestHours` | Integer | No (Default `0`) | Hard constraint: Minimum contiguous rest time required once per race. |
189+
| `maxBusyHours` | Integer | No (Default `8`) | Hard constraint: Maximum total time (driving + spotting) a member can work before a required rest. |
188190
| `firstStintDriver` | String | No | Hard constraint: The name of the team member who must drive the first stint. |
189191
| `teamMembers` | Array | Yes | List of drivers and spotters (see below). |
190192
| `availability` | Object | Yes | Map of availability constraints (see below). |

TOOLS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ jres_solver.exe [options]
5858
| `-d` | `--diagnose` | Run in **Diagnostic Mode** to explain why a schedule is infeasible. | `false` |
5959
| `-h` | `--help` | Print usage instructions. | |
6060

61+
> [!TIP]
62+
> **Constraint Configuration:**
63+
> While some options are available as CLI flags, core schedule constraints such as `maxBusyHours`, `minimumRestHours`, and `consecutiveStints` are defined strictly within the **Input JSON** file. See [README](./README.md#input-json-specification) for details.
64+
6165
### Spotter Modes
6266

6367
#### Integrated Mode (`JRES_SPOTTER_MODE_INTEGRATED`)

include/jres_solver/jres_solver.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ struct JresSolverInput {
122122
int consecutiveStints;
123123
/** @brief Minimum rest time in hours required after a shift. */
124124
int minimumRestHours;
125+
/** @brief Maximum busy time in hours (driving or spotting) before a rest is required. */
126+
int maxBusyHours;
125127
/** @brief The name of the team member who must drive the first stint. */
126128
const char* firstStintDriver;
127129
/** @brief A pointer to an array of team members. */

src/constraints/max_busy_time.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "max_busy_time.hpp"
2+
#include "Highs.h"
3+
#include <chrono>
4+
#include <map>
5+
6+
namespace jres::constraints {
7+
8+
void apply_max_busy_time_constraints(
9+
Highs &highs,
10+
const jres::internal::SolverInput& input,
11+
const std::vector<jres::internal::TeamMember> &participants,
12+
const std::map<std::pair<std::string, int>, int>& driverVars,
13+
const std::map<std::pair<std::string, int>, int>& spotterVars,
14+
bool enforceCombined,
15+
std::map<int, jres::internal::SlackInfo>& slackInfo,
16+
const std::vector<jres::internal::ScheduleEntry>* fixedSchedule
17+
)
18+
{
19+
using namespace jres::internal;
20+
21+
if (input.maxBusyHours <= 0) return;
22+
23+
// Calculate durations
24+
std::vector<double> stintDurations;
25+
stintDurations.reserve(input.stints.size());
26+
for (const auto& stint : input.stints) {
27+
auto s = TimeHelpers::stringToTimePoint(stint.startTime);
28+
auto e = TimeHelpers::stringToTimePoint(stint.endTime);
29+
long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(e - s).count();
30+
stintDurations.push_back(static_cast<double>(ms) / 3600000.0);
31+
}
32+
33+
for (const auto &p : participants)
34+
{
35+
for (size_t s = 0; s < input.stints.size(); ++s) {
36+
double currentDuration = 0.0;
37+
for (size_t e = s; e < input.stints.size(); ++e) {
38+
currentDuration += stintDurations[e];
39+
40+
if (currentDuration > input.maxBusyHours) {
41+
// Violation if assigned to ALL stints in [s, e]
42+
// Constraint: Sum(coeff * x[k]) <= (e - s)
43+
44+
std::map<int, double> coefficients;
45+
int fixedAssignments = 0;
46+
47+
for (size_t k = s; k <= e; ++k) {
48+
// Driver
49+
if (fixedSchedule) {
50+
// Sequential Mode: Check fixed schedule
51+
if (k < fixedSchedule->size() && (*fixedSchedule)[k].driver == p.name) {
52+
fixedAssignments++;
53+
}
54+
} else {
55+
// Integrated Mode: Add driver var to constraint
56+
if (driverVars.count({p.name, (int)k})) {
57+
coefficients[driverVars.at({p.name, (int)k})] += 1.0;
58+
}
59+
}
60+
61+
// Spotter
62+
if (spotterVars.count({p.name, (int)k})) {
63+
if (fixedSchedule || enforceCombined) {
64+
coefficients[spotterVars.at({p.name, (int)k})] += 1.0;
65+
}
66+
}
67+
}
68+
69+
std::vector<int> idx;
70+
std::vector<double> val;
71+
idx.reserve(coefficients.size());
72+
val.reserve(coefficients.size());
73+
74+
for(const auto& [col, coeff] : coefficients) {
75+
idx.push_back(col);
76+
val.push_back(coeff);
77+
}
78+
79+
double maxAssignments = static_cast<double>(e - s);
80+
81+
if (fixedSchedule) {
82+
// Adjust RHS
83+
maxAssignments -= fixedAssignments;
84+
}
85+
86+
if (!idx.empty() || maxAssignments < 0) {
87+
highs.addRow(-kHighsInf, maxAssignments, (int)idx.size(), idx.data(), val.data());
88+
}
89+
90+
break;
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
} // namespace jres::constraints

src/constraints/max_busy_time.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
#include "../jres_internal_types.hpp"
3+
#include <map>
4+
#include <vector>
5+
6+
class Highs;
7+
8+
namespace jres::constraints {
9+
10+
void apply_max_busy_time_constraints(
11+
Highs &highs,
12+
const jres::internal::SolverInput& input,
13+
const std::vector<jres::internal::TeamMember> &participants,
14+
const std::map<std::pair<std::string, int>, int>& driverVars,
15+
const std::map<std::pair<std::string, int>, int>& spotterVars,
16+
bool enforceCombined,
17+
std::map<int, jres::internal::SlackInfo>& slackInfo,
18+
const std::vector<jres::internal::ScheduleEntry>* fixedSchedule = nullptr
19+
);
20+
21+
}

src/jres_internal_types.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ SolverInput from_c_input(const JresSolverInput* c_input) {
6767

6868
input.consecutiveStints = c_input->consecutiveStints;
6969
input.minimumRestHours = c_input->minimumRestHours;
70+
input.maxBusyHours = c_input->maxBusyHours;
7071
if (c_input->firstStintDriver) {
7172
input.firstStintDriver = c_input->firstStintDriver;
7273
}

src/jres_internal_types.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ struct SolverInput
4747
{
4848
int consecutiveStints = 1;
4949
int minimumRestHours = 0;
50+
int maxBusyHours = 8;
5051
std::string firstStintDriver;
5152
std::vector<TeamMember> teamMembers;
5253
std::map<std::string, std::map<std::string, Availability>> availability;

src/jres_json_converter.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ JRES_SOLVER_API JresSolverInput* jres_input_from_json(const char* jsonData) {
7676
// Global Constraints
7777
input->consecutiveStints = j.value("consecutiveStints", 1);
7878
input->minimumRestHours = j.value("minimumRestHours", 0);
79+
input->maxBusyHours = j.value("maxBusyHours", 8);
7980

8081
if (j.contains("firstStintDriver") && !j["firstStintDriver"].is_null()) {
8182
input->firstStintDriver = allocate_and_copy(j["firstStintDriver"]);

src/jres_standard_solver.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "analysis/capacity_analyzer.hpp"
88
#include "constraints/balancing.hpp"
99
#include "constraints/minimum_rest.hpp"
10+
#include "constraints/max_busy_time.hpp"
1011
#include <algorithm>
1112
#include <cmath>
1213
#include <chrono>
@@ -322,6 +323,7 @@ jres::internal::SolverOutput JresStandardSolver::solve()
322323
// Apply Rest Constraints
323324
bool enforceCombinedRest = (m_options.spotterMode == JRES_SPOTTER_MODE_INTEGRATED);
324325
jres::constraints::apply_minimum_rest_constraints(*m_highs, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, enforceCombinedRest, m_slackInfo);
326+
jres::constraints::apply_max_busy_time_constraints(*m_highs, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, enforceCombinedRest, m_slackInfo);
325327

326328

327329
// --- Solve Main Model (Drivers + Spotters if Integrated) ---
@@ -462,6 +464,9 @@ jres::internal::SolverOutput JresStandardSolver::solve()
462464
}
463465
}
464466

467+
// Apply Max Busy Constraints (taking fixed drivers into account)
468+
jres::constraints::apply_max_busy_time_constraints(spotterSolver, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, true, m_slackInfo, &output.schedule);
469+
465470
// Incentivize Spotting Adjacent to Driving (Proximity & Role Coupling)
466471
// Calculate Rewards per Block Var
467472
std::map<int, double> spotterRewards;

0 commit comments

Comments
 (0)