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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,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. |
| `maximumBusyHours` | 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
2 changes: 1 addition & 1 deletion TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jres_solver.exe [options]

> [!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.
> While some options are available as CLI flags, core schedule constraints such as `maximumBusyHours`, `minimumRestHours`, and `consecutiveStints` are defined strictly within the **Input JSON** file. See [README](./README.md#input-json-specification) for details.

### Spotter Modes

Expand Down
18 changes: 17 additions & 1 deletion include/jres_solver/jres_solver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ struct JresSolverInput {
/** @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;
int maximumBusyHours;
/** @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 Expand Up @@ -176,6 +176,20 @@ struct JresSolverStats {
double spotterSolveDurationMs;
};

/**
* @brief Configuration parameters used for the solution.
*/
struct JresInputConfig {
/** @brief Required consecutive stints (drivers must do this many stints in a row, except potentially the last one). */
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 maximumBusyHours;
/** @brief The name of the team member who must drive the first stint. */
const char* firstStintDriver;
};

/**
* @brief The main output struct from the solver.
*/
Expand All @@ -192,6 +206,8 @@ struct JresSolverOutput {
JresSolverStats* stats;
/** @brief The options used to generate this solution. */
JresSolverOptions* options;
/** @brief The input configuration used to generate this solution. */
JresInputConfig* config;
/** @brief A pointer to an array of team members, including their tzOffset. */
JresTeamMember* teamMembers;
/** @brief The number of team members. */
Expand Down
4 changes: 2 additions & 2 deletions src/constraints/max_busy_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ void apply_max_busy_time_constraints(
{
using namespace jres::internal;

if (input.maxBusyHours <= 0) return;
if (input.maximumBusyHours <= 0) return;

// Calculate durations
std::vector<double> stintDurations;
Expand All @@ -37,7 +37,7 @@ void apply_max_busy_time_constraints(
for (size_t e = s; e < input.stints.size(); ++e) {
currentDuration += stintDurations[e];

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

Expand Down
40 changes: 29 additions & 11 deletions src/jres_internal_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +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;
input.maximumBusyHours = c_input->maximumBusyHours;
if (c_input->firstStintDriver) {
input.firstStintDriver = c_input->firstStintDriver;
}
Expand Down Expand Up @@ -131,15 +131,35 @@ JresSolverOutput* to_c_output(const SolverOutput& output, const JresSolverOption
c_output->diagnosis[i] = allocate_and_copy(output.diagnosis[i]);
}

c_output->stats = new JresSolverStats();
c_output->stats->modelColumns = output.stats.modelColumns;
c_output->stats->modelRows = output.stats.modelRows;
c_output->stats->searchNodes = output.stats.searchNodes;
c_output->stats->finalGap = output.stats.finalGap;
c_output->stats->setupDurationMs = output.stats.setupDurationMs;
c_output->stats->driverSolveDurationMs = output.stats.driverSolveDurationMs;
c_output->stats->spotterSolveDurationMs = output.stats.spotterSolveDurationMs;
if (output.stats.modelRows > 0 || output.stats.modelColumns > 0) {
c_output->stats = new JresSolverStats();
c_output->stats->modelColumns = output.stats.modelColumns;
c_output->stats->modelRows = output.stats.modelRows;
c_output->stats->searchNodes = output.stats.searchNodes;
c_output->stats->finalGap = output.stats.finalGap;
c_output->stats->setupDurationMs = output.stats.setupDurationMs;
c_output->stats->driverSolveDurationMs = output.stats.driverSolveDurationMs;
c_output->stats->spotterSolveDurationMs = output.stats.spotterSolveDurationMs;
} else {
c_output->stats = nullptr;
}

// Copy options
c_output->options = new JresSolverOptions();
*c_output->options = options;

// Copy config
c_output->config = new JresInputConfig();
c_output->config->consecutiveStints = output.config.consecutiveStints;
c_output->config->minimumRestHours = output.config.minimumRestHours;
c_output->config->maximumBusyHours = output.config.maximumBusyHours;
if (!output.config.firstStintDriver.empty()) {
c_output->config->firstStintDriver = allocate_and_copy(output.config.firstStintDriver);
} else {
c_output->config->firstStintDriver = nullptr;
}

// Copy teamMembers
c_output->teamMembers_len = output.teamMembers.size();
c_output->teamMembers = new JresTeamMember[c_output->teamMembers_len];
for (size_t i = 0; i < output.teamMembers.size(); ++i) {
Expand All @@ -149,8 +169,6 @@ JresSolverOutput* to_c_output(const SolverOutput& output, const JresSolverOption
c_output->teamMembers[i].tzOffset = output.teamMembers[i].tzOffset;
}

c_output->options = new JresSolverOptions(options);

return c_output;
}

Expand Down
10 changes: 9 additions & 1 deletion src/jres_internal_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct SolverInput
{
int consecutiveStints = 1;
int minimumRestHours = 0;
int maxBusyHours = 8;
int maximumBusyHours = 8;
std::string firstStintDriver;
std::vector<TeamMember> teamMembers;
std::map<std::string, std::map<std::string, Availability>> availability;
Expand Down Expand Up @@ -80,12 +80,20 @@ struct SolverStats {
double spotterSolveDurationMs = 0.0;
};

struct InputConfig {
int consecutiveStints = 1;
int minimumRestHours = 0;
int maximumBusyHours = 8;
std::string firstStintDriver;
};

struct SolverOutput
{
std::vector<ScheduleEntry> schedule;
std::vector<std::string> diagnosis;
SolverStats stats;
std::vector<TeamMember> teamMembers;
InputConfig config;
// Add any other output fields here, like diagnosis or metrics
};

Expand Down
19 changes: 18 additions & 1 deletion src/jres_json_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +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);
input->maximumBusyHours = j.value("maximumBusyHours", 8);

if (j.contains("firstStintDriver") && !j["firstStintDriver"].is_null()) {
input->firstStintDriver = allocate_and_copy(j["firstStintDriver"]);
Expand Down Expand Up @@ -194,6 +194,17 @@ JRES_SOLVER_API char* jres_output_to_json(const JresSolverOutput* output) {
j["options"] = options_json;
}

if (output->config) {
json config_json;
config_json["consecutiveStints"] = output->config->consecutiveStints;
config_json["minimumRestHours"] = output->config->minimumRestHours;
config_json["maximumBusyHours"] = output->config->maximumBusyHours;
if (output->config->firstStintDriver) {
config_json["firstStintDriver"] = output->config->firstStintDriver;
}
j["configuration"] = config_json;
}

std::string json_str = j.dump();
return allocate_and_copy(json_str);

Expand Down Expand Up @@ -238,6 +249,12 @@ JRES_SOLVER_API void free_jres_solver_output(JresSolverOutput* output) {
if (output->options) {
delete output->options;
}
if (output->config) {
if (output->config->firstStintDriver) {
delete[] output->config->firstStintDriver; // allocated via allocate_and_copy
}
delete output->config;
}
delete output;
}

Expand Down
6 changes: 6 additions & 0 deletions src/jres_standard_solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ jres::internal::SolverOutput JresStandardSolver::solve()
auto startTotal = high_resolution_clock::now();
jres::internal::SolverOutput output;

// Populate Config
output.config.consecutiveStints = m_input.consecutiveStints;
output.config.minimumRestHours = m_input.minimumRestHours;
output.config.maximumBusyHours = m_input.maximumBusyHours;
output.config.firstStintDriver = m_input.firstStintDriver;

// --- Arithmetic Pre-flight Check ---
int totalStints = (int)m_input.stints.size();
auto capAnalysis = jres::internal::CapacityAnalyzer::calculate_max_potential_capacity(m_driverPool, m_input);
Expand Down
8 changes: 4 additions & 4 deletions test/test_max_busy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ TEST(MaxBusyTest, MaxBusyHoursInfeasible) {
// Driver cannot do S1, S2, S3 continuously.

std::string json_str = R"({
"maxBusyHours": 2,
"maximumBusyHours": 2,
"teamMembers": [
{ "name": "D1", "isDriver": true }
],
Expand Down Expand Up @@ -56,7 +56,7 @@ TEST(MaxBusyTest, MaxBusyHoursFeasible) {
// D1: S1, S2. D2: S3. Valid.

std::string json_str = R"({
"maxBusyHours": 2,
"maximumBusyHours": 2,
"teamMembers": [
{ "name": "D1", "isDriver": true },
{ "name": "D2", "isDriver": true }
Expand Down Expand Up @@ -101,7 +101,7 @@ TEST(MaxBusyTest, MaxBusySpotterIntegrated) {
// Spotter cannot do S1, S2, S3 continuously.

std::string json_str = R"({
"maxBusyHours": 2,
"maximumBusyHours": 2,
"teamMembers": [
{ "name": "D1", "isDriver": true, "isSpotter": false },
{ "name": "D2", "isDriver": true, "isSpotter": false },
Expand Down Expand Up @@ -153,7 +153,7 @@ TEST(MaxBusyTest, MaxBusySequential) {
// Combined Kyle = 8h. > 6h.

std::string json_str = R"({
"maxBusyHours": 6,
"maximumBusyHours": 6,
"teamMembers": [
{ "name": "Kyle", "isDriver": true, "isSpotter": true },
{ "name": "D2", "isDriver": true, "isSpotter": false },
Expand Down
4 changes: 2 additions & 2 deletions test/test_max_busy_defaults.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ TEST(MaxBusyDefaults, DefaultValueCheck) {
// Verify manually that input->maxBusyHours is 8 (if we can inspect it? It's in the opaque struct)
// Actually we can inspect it because we have the struct definition in header.
// But JresSolverInput definition is in jres_solver.hpp which is included.
EXPECT_EQ(input->maxBusyHours, 8);
EXPECT_EQ(input->maximumBusyHours, 8);

JresSolverOutput* output = solve_race_schedule(input, &options);

Expand All @@ -56,7 +56,7 @@ TEST(MaxBusyDefaults, DefaultValueCheck) {
}

if (feasible) {
FAIL() << "Solver found schedule despite default maxBusyHours=8 violation.";
FAIL() << "Solver found schedule despite default maximumBusyHours=8 violation.";
}

free_jres_solver_output(output);
Expand Down
4 changes: 2 additions & 2 deletions test/test_max_busy_mixed_roles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ TEST(MaxBusyMixedRoles, AlternatingWorkload) {

std::string json_str = R"({
"consecutiveStints": 2,
"maxBusyHours": 8,
"maximumBusyHours": 8,
"teamMembers": [
{ "name": "Kyle", "isDriver": true, "isSpotter": true }
],
Expand Down Expand Up @@ -68,7 +68,7 @@ TEST(MaxBusyMixedRoles, AlternatingWorkload) {
for(int i=0; i<12; ++i) {
std::cout << i << ": " << output->schedule[i].driver << " / " << output->schedule[i].spotter << std::endl;
}
FAIL() << "Solver produced a schedule violating maxBusyHours with alternating roles.";
FAIL() << "Solver produced a schedule violating maximumBusyHours with alternating roles.";
}

free_jres_solver_output(output);
Expand Down