Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ add_library(jres_solver_lib
src/analysis/capacity_analyzer.cpp
src/constraints/balancing.cpp
src/constraints/minimum_rest.cpp
src/constraints/max_busy_time.cpp
)
set_target_properties(jres_solver_lib PROPERTIES OUTPUT_NAME "jres_solver")

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The C-API uses the following structs to pass data to and from the solver.
| :--- | :--- | :--- |
| `consecutiveStints` | `int` | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. |
| `maxBusyHours` | `int` | Hard constraint: Maximum total time (driving + spotting) a member can work before a required rest. |
| `firstStintDriver` | `const char*` | Hard constraint: The name of the team member who must drive the first stint. |
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members. |
| `teamMembers_len` | `int` | The number of team members. |
Expand Down Expand Up @@ -182,6 +183,7 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
| :--- | :--- | :--- | :--- |
| `consecutiveStints` | Integer | No (Default `1`) | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
| `minimumRestHours` | Integer | No (Default `0`) | Hard constraint: Minimum contiguous rest time required once per race. |
| `maxBusyHours` | Integer | No (Default `8`) | Hard constraint: Maximum total time (driving + spotting) a member can work before a required rest. |
| `firstStintDriver` | String | No | Hard constraint: The name of the team member who must drive the first stint. |
| `teamMembers` | Array | Yes | List of drivers and spotters (see below). |
| `availability` | Object | Yes | Map of availability constraints (see below). |
Expand Down
4 changes: 4 additions & 0 deletions TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ jres_solver.exe [options]
| `-d` | `--diagnose` | Run in **Diagnostic Mode** to explain why a schedule is infeasible. | `false` |
| `-h` | `--help` | Print usage instructions. | |

> [!TIP]
> **Constraint Configuration:**
> 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.

### Spotter Modes

#### Integrated Mode (`JRES_SPOTTER_MODE_INTEGRATED`)
Expand Down
2 changes: 2 additions & 0 deletions include/jres_solver/jres_solver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ struct JresSolverInput {
int consecutiveStints;
/** @brief Minimum rest time in hours required after a shift. */
int minimumRestHours;
/** @brief Maximum busy time in hours (driving or spotting) before a rest is required. */
int maxBusyHours;
/** @brief The name of the team member who must drive the first stint. */
const char* firstStintDriver;
/** @brief A pointer to an array of team members. */
Expand Down
97 changes: 97 additions & 0 deletions src/constraints/max_busy_time.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include "max_busy_time.hpp"
#include "Highs.h"
#include <chrono>
#include <map>

namespace jres::constraints {

void apply_max_busy_time_constraints(
Highs &highs,
const jres::internal::SolverInput& input,
const std::vector<jres::internal::TeamMember> &participants,
const std::map<std::pair<std::string, int>, int>& driverVars,
const std::map<std::pair<std::string, int>, int>& spotterVars,
bool enforceCombined,
std::map<int, jres::internal::SlackInfo>& slackInfo,
const std::vector<jres::internal::ScheduleEntry>* fixedSchedule
)
{
using namespace jres::internal;

if (input.maxBusyHours <= 0) return;

// Calculate durations
std::vector<double> stintDurations;
stintDurations.reserve(input.stints.size());
for (const auto& stint : input.stints) {
auto s = TimeHelpers::stringToTimePoint(stint.startTime);
auto e = TimeHelpers::stringToTimePoint(stint.endTime);
long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(e - s).count();
stintDurations.push_back(static_cast<double>(ms) / 3600000.0);
}

for (const auto &p : participants)
{
for (size_t s = 0; s < input.stints.size(); ++s) {
double currentDuration = 0.0;
for (size_t e = s; e < input.stints.size(); ++e) {
currentDuration += stintDurations[e];

if (currentDuration > input.maxBusyHours) {
// Violation if assigned to ALL stints in [s, e]
// Constraint: Sum(coeff * x[k]) <= (e - s)

std::map<int, double> coefficients;
int fixedAssignments = 0;

for (size_t k = s; k <= e; ++k) {
// Driver
if (fixedSchedule) {
// Sequential Mode: Check fixed schedule
if (k < fixedSchedule->size() && (*fixedSchedule)[k].driver == p.name) {
fixedAssignments++;
}
} else {
// Integrated Mode: Add driver var to constraint
if (driverVars.count({p.name, (int)k})) {
coefficients[driverVars.at({p.name, (int)k})] += 1.0;
}
}

// Spotter
if (spotterVars.count({p.name, (int)k})) {
if (fixedSchedule || enforceCombined) {
coefficients[spotterVars.at({p.name, (int)k})] += 1.0;
}
}
}

std::vector<int> idx;
std::vector<double> val;
idx.reserve(coefficients.size());
val.reserve(coefficients.size());

for(const auto& [col, coeff] : coefficients) {
idx.push_back(col);
val.push_back(coeff);
}

double maxAssignments = static_cast<double>(e - s);

if (fixedSchedule) {
// Adjust RHS
maxAssignments -= fixedAssignments;
}

if (!idx.empty() || maxAssignments < 0) {
highs.addRow(-kHighsInf, maxAssignments, (int)idx.size(), idx.data(), val.data());
}

break;
}
}
}
}
}

} // namespace jres::constraints
21 changes: 21 additions & 0 deletions src/constraints/max_busy_time.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once
#include "../jres_internal_types.hpp"
#include <map>
#include <vector>

class Highs;

namespace jres::constraints {

void apply_max_busy_time_constraints(
Highs &highs,
const jres::internal::SolverInput& input,
const std::vector<jres::internal::TeamMember> &participants,
const std::map<std::pair<std::string, int>, int>& driverVars,
const std::map<std::pair<std::string, int>, int>& spotterVars,
bool enforceCombined,
std::map<int, jres::internal::SlackInfo>& slackInfo,
const std::vector<jres::internal::ScheduleEntry>* fixedSchedule = nullptr
);

}
1 change: 1 addition & 0 deletions src/jres_internal_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ SolverInput from_c_input(const JresSolverInput* c_input) {

input.consecutiveStints = c_input->consecutiveStints;
input.minimumRestHours = c_input->minimumRestHours;
input.maxBusyHours = c_input->maxBusyHours;
if (c_input->firstStintDriver) {
input.firstStintDriver = c_input->firstStintDriver;
}
Expand Down
1 change: 1 addition & 0 deletions src/jres_internal_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct SolverInput
{
int consecutiveStints = 1;
int minimumRestHours = 0;
int maxBusyHours = 8;
std::string firstStintDriver;
std::vector<TeamMember> teamMembers;
std::map<std::string, std::map<std::string, Availability>> availability;
Expand Down
1 change: 1 addition & 0 deletions src/jres_json_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ JRES_SOLVER_API JresSolverInput* jres_input_from_json(const char* jsonData) {
// Global Constraints
input->consecutiveStints = j.value("consecutiveStints", 1);
input->minimumRestHours = j.value("minimumRestHours", 0);
input->maxBusyHours = j.value("maxBusyHours", 8);

if (j.contains("firstStintDriver") && !j["firstStintDriver"].is_null()) {
input->firstStintDriver = allocate_and_copy(j["firstStintDriver"]);
Expand Down
5 changes: 5 additions & 0 deletions src/jres_standard_solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "analysis/capacity_analyzer.hpp"
#include "constraints/balancing.hpp"
#include "constraints/minimum_rest.hpp"
#include "constraints/max_busy_time.hpp"
#include <algorithm>
#include <cmath>
#include <chrono>
Expand Down Expand Up @@ -322,6 +323,7 @@ jres::internal::SolverOutput JresStandardSolver::solve()
// Apply Rest Constraints
bool enforceCombinedRest = (m_options.spotterMode == JRES_SPOTTER_MODE_INTEGRATED);
jres::constraints::apply_minimum_rest_constraints(*m_highs, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, enforceCombinedRest, m_slackInfo);
jres::constraints::apply_max_busy_time_constraints(*m_highs, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, enforceCombinedRest, m_slackInfo);


// --- Solve Main Model (Drivers + Spotters if Integrated) ---
Expand Down Expand Up @@ -462,6 +464,9 @@ jres::internal::SolverOutput JresStandardSolver::solve()
}
}

// Apply Max Busy Constraints (taking fixed drivers into account)
jres::constraints::apply_max_busy_time_constraints(spotterSolver, m_input, m_input.teamMembers, m_driverWorkVars, m_spotterWorkVars, true, m_slackInfo, &output.schedule);

// Incentivize Spotting Adjacent to Driving (Proximity & Role Coupling)
// Calculate Rewards per Block Var
std::map<int, double> spotterRewards;
Expand Down
3 changes: 3 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ add_executable(solver_tests
test_rotation_beat.cpp
test_spotter_balancing.cpp
test_first_stint.cpp
test_max_busy.cpp
test_max_busy_mixed_roles.cpp
test_max_busy_defaults.cpp
)

add_dependencies(solver_tests jres_solver_lib)
Expand Down
Loading