From eba5ccbd4e2de6cbbd8bbf7b8f8896056dc694b6 Mon Sep 17 00:00:00 2001 From: popmonkey Date: Sat, 10 Jan 2026 20:37:25 -0800 Subject: [PATCH 1/2] input parameter changes * maxBusyHours -> maximumBusyHours * now includes input options in the json output --- README.md | 2 +- TOOLS.md | 2 +- include/jres_solver/jres_solver.hpp | 18 ++++++++++- src/constraints/max_busy_time.cpp | 4 +-- src/jres_internal_types.cpp | 46 +++++++++++++++++------------ src/jres_internal_types.hpp | 10 ++++++- src/jres_json_converter.cpp | 19 +++++++++++- src/jres_standard_solver.cpp | 6 ++++ test/test_max_busy.cpp | 8 ++--- test/test_max_busy_defaults.cpp | 4 +-- test/test_max_busy_mixed_roles.cpp | 4 +-- 11 files changed, 89 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3c0fd51..9df71d3 100644 --- a/README.md +++ b/README.md @@ -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). | diff --git a/TOOLS.md b/TOOLS.md index 7683986..f2067a2 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -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 diff --git a/include/jres_solver/jres_solver.hpp b/include/jres_solver/jres_solver.hpp index c68955b..a9f7002 100644 --- a/include/jres_solver/jres_solver.hpp +++ b/include/jres_solver/jres_solver.hpp @@ -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. */ @@ -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. */ @@ -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. */ diff --git a/src/constraints/max_busy_time.cpp b/src/constraints/max_busy_time.cpp index 087254e..58986e7 100644 --- a/src/constraints/max_busy_time.cpp +++ b/src/constraints/max_busy_time.cpp @@ -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 stintDurations; @@ -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) diff --git a/src/jres_internal_types.cpp b/src/jres_internal_types.cpp index 92a0eed..468de94 100644 --- a/src/jres_internal_types.cpp +++ b/src/jres_internal_types.cpp @@ -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; } @@ -131,25 +131,33 @@ 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; - - 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) { - c_output->teamMembers[i].name = allocate_and_copy(output.teamMembers[i].name); - c_output->teamMembers[i].isDriver = output.teamMembers[i].isDriver; - c_output->teamMembers[i].isSpotter = output.teamMembers[i].isSpotter; - c_output->teamMembers[i].tzOffset = output.teamMembers[i].tzOffset; + 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; } - - c_output->options = new JresSolverOptions(options); return c_output; } diff --git a/src/jres_internal_types.hpp b/src/jres_internal_types.hpp index c0430b4..be4aeba 100644 --- a/src/jres_internal_types.hpp +++ b/src/jres_internal_types.hpp @@ -47,7 +47,7 @@ struct SolverInput { int consecutiveStints = 1; int minimumRestHours = 0; - int maxBusyHours = 8; + int maximumBusyHours = 8; std::string firstStintDriver; std::vector teamMembers; std::map> availability; @@ -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 schedule; std::vector diagnosis; SolverStats stats; std::vector teamMembers; + InputConfig config; // Add any other output fields here, like diagnosis or metrics }; diff --git a/src/jres_json_converter.cpp b/src/jres_json_converter.cpp index b431f05..d5011b2 100644 --- a/src/jres_json_converter.cpp +++ b/src/jres_json_converter.cpp @@ -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"]); @@ -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); @@ -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; } diff --git a/src/jres_standard_solver.cpp b/src/jres_standard_solver.cpp index fa47091..b3ab224 100644 --- a/src/jres_standard_solver.cpp +++ b/src/jres_standard_solver.cpp @@ -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); diff --git a/test/test_max_busy.cpp b/test/test_max_busy.cpp index d4000b1..5603289 100644 --- a/test/test_max_busy.cpp +++ b/test/test_max_busy.cpp @@ -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 } ], @@ -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 } @@ -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 }, @@ -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 }, diff --git a/test/test_max_busy_defaults.cpp b/test/test_max_busy_defaults.cpp index 0de5f0f..4413d57 100644 --- a/test/test_max_busy_defaults.cpp +++ b/test/test_max_busy_defaults.cpp @@ -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); @@ -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); diff --git a/test/test_max_busy_mixed_roles.cpp b/test/test_max_busy_mixed_roles.cpp index 1e3f99c..92ff6ed 100644 --- a/test/test_max_busy_mixed_roles.cpp +++ b/test/test_max_busy_mixed_roles.cpp @@ -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 } ], @@ -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); From da5aaa5072bfa0a89838430c04dff7a3506d1c16 Mon Sep 17 00:00:00 2001 From: popmonkey Date: Sat, 10 Jan 2026 21:37:24 -0800 Subject: [PATCH 2/2] fix integration test --- src/jres_internal_types.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/jres_internal_types.cpp b/src/jres_internal_types.cpp index 468de94..bb8c767 100644 --- a/src/jres_internal_types.cpp +++ b/src/jres_internal_types.cpp @@ -159,6 +159,16 @@ JresSolverOutput* to_c_output(const SolverOutput& output, const JresSolverOption 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) { + c_output->teamMembers[i].name = allocate_and_copy(output.teamMembers[i].name); + c_output->teamMembers[i].isDriver = output.teamMembers[i].isDriver; + c_output->teamMembers[i].isSpotter = output.teamMembers[i].isSpotter; + c_output->teamMembers[i].tzOffset = output.teamMembers[i].tzOffset; + } + return c_output; }