Skip to content

Commit 2e3de54

Browse files
committed
refactor solver into smaller pieces
1 parent f6e2b17 commit 2e3de54

12 files changed

Lines changed: 483 additions & 404 deletions

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ add_library(jres_solver_lib
161161
src/jres_internal_types.cpp
162162
src/jres_solver_base.cpp
163163
src/jres_standard_solver.cpp
164+
src/analysis/capacity_analyzer.cpp
165+
src/constraints/balancing.cpp
166+
src/constraints/minimum_rest.cpp
164167
)
165168
set_target_properties(jres_solver_lib PROPERTIES OUTPUT_NAME "jres_solver")
166169

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ It has been structured as a C-API library (`jres_solver`) and a simple CLI clien
1515
| └── jres_solver/ # The public C-API header for the library
1616
├── src/ # The C++ library implementation
1717
| ├── jres_solver.cpp # C-API Wrapper and Orchestrator
18-
| ├── jres_solver_base.cpp # Shared logic for both solvers
19-
| ├── jres_standard_solver.cpp # Optimized Strict Solver
20-
| ├── jres_diagnostic_solver.cpp # Relaxed Diagnostic Solver
18+
| ├── jres_solver_base.cpp # Shared logic for the solver
19+
| ├── jres_standard_solver.cpp # Standard Solver (Elastic/Diagnostic enabled)
2120
| ├── jres_internal_types.cpp # Internal C++ data structures
2221
| ├── jres_json_converter.cpp # JSON conversion logic
22+
| ├── analysis/ # Analysis logic (e.g. capacity)
23+
| ├── constraints/ # Constraint implementations
2324
| ├── formatter/ # Formatter implementation
2425
| └── utils/ # Utility functions
2526
├── lib/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ See [TOOLS.md](./TOOLS.md) for full usage.
2424

2525
## The Library
2626

27-
**JresSolver** is a C++ library designed to optimize endurance racing schedules. It uses the **HiGHS** Mixed Integer Programming (MIP) solver to assign drivers (and optional spotters) to race stints while satisfying constraints such as fuel usage, maximum drive times, minimum rest periods, and driver availability.
27+
**JresSolver** is a C++ library designed to optimize endurance racing schedules. It uses the **HiGHS** Mixed Integer Programming (MIP) solver to assign drivers (and optional spotters) to race stints while satisfying constraints such as fuel usage, maximum drive times, minimum rest periods, and driver availability. The library utilizes a modular constraint architecture for flexibility and extensibility.
2828

2929
### Data Structures
3030

src/analysis/capacity_analyzer.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#include "capacity_analyzer.hpp"
2+
#include <sstream>
3+
#include <iomanip>
4+
#include <chrono>
5+
6+
namespace jres::internal {
7+
8+
CapacityAnalysis CapacityAnalyzer::calculate_max_potential_capacity(
9+
const std::vector<TeamMember>& participants,
10+
const SolverInput& input)
11+
{
12+
// Parse stint times once
13+
std::vector<std::chrono::system_clock::time_point> startTimes;
14+
std::vector<std::chrono::system_clock::time_point> endTimes;
15+
startTimes.reserve(input.stints.size());
16+
endTimes.reserve(input.stints.size());
17+
18+
std::chrono::system_clock::time_point raceStart;
19+
std::chrono::system_clock::time_point raceEnd;
20+
bool raceTimesInit = false;
21+
22+
for (const auto& stint : input.stints) {
23+
auto s = TimeHelpers::stringToTimePoint(stint.startTime);
24+
auto e = TimeHelpers::stringToTimePoint(stint.endTime);
25+
startTimes.push_back(s);
26+
endTimes.push_back(e);
27+
28+
if(!raceTimesInit) {
29+
raceStart = s;
30+
raceEnd = e;
31+
raceTimesInit = true;
32+
} else {
33+
if(s < raceStart) raceStart = s;
34+
if(e > raceEnd) raceEnd = e;
35+
}
36+
}
37+
38+
CapacityAnalysis analysis;
39+
analysis.totalCapacity = 0;
40+
std::ostringstream ss;
41+
42+
for (const auto& p : participants) {
43+
// Build Availability
44+
std::vector<bool> is_available(input.stints.size(), true);
45+
auto member_availability_it = input.availability.find(p.name);
46+
if (member_availability_it != input.availability.end()) {
47+
for (size_t s = 0; s < input.stints.size(); ++s) {
48+
std::string key = TimeHelpers::timePointToKey(startTimes[s]);
49+
auto time_it = member_availability_it->second.find(key);
50+
if (time_it != member_availability_it->second.end() &&
51+
time_it->second == Availability::Unavailable) {
52+
is_available[s] = false;
53+
}
54+
}
55+
}
56+
57+
std::vector<bool> planned_drive(input.stints.size(), false);
58+
int base_capacity = 0;
59+
double driver_total_hours = 0.0;
60+
61+
for(size_t s=0; s<input.stints.size(); ++s) {
62+
if (is_available[s]) {
63+
planned_drive[s] = true;
64+
base_capacity++;
65+
66+
auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(endTimes[s] - startTimes[s]).count();
67+
driver_total_hours += static_cast<double>(duration_ms) / 3600000.0;
68+
}
69+
}
70+
71+
// Adjust for Global Minimum Rest (One Instance)
72+
int final_capacity = base_capacity;
73+
if (input.minimumRestHours > 0) {
74+
auto minRestDuration = std::chrono::hours(input.minimumRestHours);
75+
int min_loss = base_capacity;
76+
bool found_valid_window = false;
77+
78+
std::vector<std::chrono::system_clock::time_point> candidateStarts;
79+
candidateStarts.push_back(raceStart);
80+
for(const auto& t : endTimes) candidateStarts.push_back(t);
81+
82+
for(const auto& tStart : candidateStarts) {
83+
auto tEnd = tStart + minRestDuration;
84+
if (tEnd > raceEnd) continue;
85+
found_valid_window = true;
86+
87+
int current_loss = 0;
88+
for(size_t s=0; s<input.stints.size(); ++s) {
89+
if (planned_drive[s]) {
90+
if (startTimes[s] < tEnd && endTimes[s] > tStart) {
91+
current_loss++;
92+
}
93+
}
94+
}
95+
if (current_loss < min_loss) min_loss = current_loss;
96+
}
97+
98+
if (!found_valid_window) {
99+
final_capacity = 0; // Impossible to satisfy rest
100+
} else {
101+
final_capacity -= min_loss;
102+
}
103+
}
104+
105+
analysis.totalCapacity += final_capacity;
106+
107+
ss << "\n- " << p.name << ": " << final_capacity
108+
<< " stints (approx " << std::fixed << std::setprecision(1) << driver_total_hours
109+
<< "h, MinRest=" << input.minimumRestHours << "h)";
110+
}
111+
analysis.details = ss.str();
112+
return analysis;
113+
}
114+
115+
} // namespace jres::internal

src/analysis/capacity_analyzer.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
#include "../jres_internal_types.hpp"
3+
4+
namespace jres::internal {
5+
6+
struct CapacityAnalysis {
7+
int totalCapacity;
8+
std::string details;
9+
};
10+
11+
class CapacityAnalyzer {
12+
public:
13+
static CapacityAnalysis calculate_max_potential_capacity(
14+
const std::vector<TeamMember>& participants,
15+
const SolverInput& input
16+
);
17+
};
18+
19+
}

src/constraints/balancing.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#include "balancing.hpp"
2+
#include "Highs.h"
3+
#include <cmath>
4+
5+
namespace jres::constraints {
6+
7+
static const double kCostFairness = 10.0;
8+
9+
void add_role_coupling_incentive(
10+
Highs* highs,
11+
const std::vector<jres::internal::TeamMember>& pool,
12+
const std::map<std::pair<std::string, int>, int>& driverVars,
13+
const std::map<std::pair<std::string, int>, int>& spotterVars,
14+
size_t numStints,
15+
double weight)
16+
{
17+
if (std::abs(weight) < 1e-6) return;
18+
19+
for (const auto &p : pool) {
20+
for (size_t s = 0; s < numStints - 1; ++s) {
21+
bool hasDriver = driverVars.count({p.name, (int)s});
22+
bool hasSpotter = spotterVars.count({p.name, (int)s + 1});
23+
24+
if (hasDriver && hasSpotter) {
25+
int d_var = driverVars.at({p.name, (int)s});
26+
int s_var = spotterVars.at({p.name, (int)s + 1});
27+
28+
// If there is a transition from driving (stint s) to spotting (stint s+1), reward it.
29+
30+
int coupling_var = highs->getNumCol();
31+
highs->addVar(0.0, 1.0);
32+
highs->changeColIntegrality(coupling_var, HighsVarType::kInteger);
33+
highs->changeColCost(coupling_var, -weight);
34+
35+
// z <= d_var
36+
highs->addRow(-kHighsInf, 0.0, 2, std::vector<int>{coupling_var, d_var}.data(), std::vector<double>{1.0, -1.0}.data());
37+
// z <= s_var
38+
highs->addRow(-kHighsInf, 0.0, 2, std::vector<int>{coupling_var, s_var}.data(), std::vector<double>{1.0, -1.0}.data());
39+
}
40+
}
41+
}
42+
}
43+
44+
void add_balancing_constraints(
45+
Highs &highs,
46+
const std::vector<jres::internal::TeamMember> &participants,
47+
const jres::internal::SolverInput& input,
48+
const std::map<std::pair<std::string, int>, int>& workVars,
49+
double avgStints)
50+
{
51+
for (const auto &p : participants) {
52+
std::vector<int> stint_indices;
53+
std::vector<double> stint_values;
54+
55+
std::map<int, double> varCounts;
56+
for (size_t s = 0; s < input.stints.size(); ++s) {
57+
if (workVars.count({p.name, (int)s})) {
58+
int v = workVars.at({p.name, (int)s});
59+
varCounts[v] += 1.0;
60+
}
61+
}
62+
for(auto const& [v, count] : varCounts) {
63+
stint_indices.push_back(v);
64+
stint_values.push_back(count);
65+
}
66+
67+
if (stint_indices.empty()) continue;
68+
69+
int total_stints_var = highs.getNumCol();
70+
highs.addVar(0.0, kHighsInf);
71+
stint_indices.push_back(total_stints_var);
72+
stint_values.push_back(-1.0);
73+
highs.addRow(0.0, 0.0, (int)stint_indices.size(), stint_indices.data(), stint_values.data());
74+
75+
int over_avg_var = highs.getNumCol();
76+
highs.addVar(0.0, kHighsInf);
77+
int under_avg_var = highs.getNumCol();
78+
highs.addVar(0.0, kHighsInf);
79+
80+
std::vector<int> idx_over = {over_avg_var, total_stints_var};
81+
std::vector<double> val_over = {1.0, -1.0};
82+
highs.addRow(0.0, kHighsInf, 2, idx_over.data(), val_over.data());
83+
highs.changeRowBounds(highs.getNumRow() - 1, -avgStints, kHighsInf);
84+
85+
std::vector<int> idx_under = {under_avg_var, total_stints_var};
86+
std::vector<double> val_under = {1.0, 1.0};
87+
highs.addRow(0.0, kHighsInf, 2, idx_under.data(), val_under.data());
88+
highs.changeRowBounds(highs.getNumRow() - 1, avgStints, kHighsInf);
89+
90+
highs.changeColCost(over_avg_var, kCostFairness);
91+
highs.changeColCost(under_avg_var, kCostFairness);
92+
}
93+
}
94+
95+
} // namespace jres::constraints

src/constraints/balancing.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 add_role_coupling_incentive(
11+
Highs* highs,
12+
const std::vector<jres::internal::TeamMember>& pool,
13+
const std::map<std::pair<std::string, int>, int>& driverVars,
14+
const std::map<std::pair<std::string, int>, int>& spotterVars,
15+
size_t numStints,
16+
double weight);
17+
18+
void add_balancing_constraints(
19+
Highs &highs,
20+
const std::vector<jres::internal::TeamMember> &participants,
21+
const jres::internal::SolverInput& input,
22+
const std::map<std::pair<std::string, int>, int>& workVars,
23+
double avgStints);
24+
25+
}

0 commit comments

Comments
 (0)