Skip to content

Commit 1f27493

Browse files
authored
Merge pull request #41 from popmonkey/v3
v3 - blocks and global stints and rest
2 parents 7c8fe9f + 2e3de54 commit 1f27493

39 files changed

Lines changed: 1430 additions & 729 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: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,34 @@ This library can be used to solve for optimal driver and spotter schedules for e
1010
* **[Tools](./TOOLS.md)** - releases include some command line tools that use the library
1111
* **[Development](./CONTRIBUTING.md)** - instructions for development of the library
1212

13+
## CLI Quick Start
14+
15+
The `jres_solver` tool supports several optimization parameters:
16+
17+
* **General:** `-i` (Input), `-o` (Output), `-t` (Time Limit), `-s` (Spotter Mode)
18+
* **Advanced Weights:**
19+
* `--switching-penalty`: Cost for driver swaps (positive disincentivizes switching)
20+
* `--role-coupling-weight`: Incentive for role coupling (positive incentivizes coupling)
21+
* `--rotation-beat-weight`: Penalty for fairness deviation (positive incentivizes adherence)
22+
23+
See [TOOLS.md](./TOOLS.md) for full usage.
24+
1325
## The Library
1426

15-
**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.
1628

1729
### Data Structures
1830

1931
The C-API uses the following structs to pass data to and from the solver.
2032

2133
#### Input Structures
2234

23-
`JresSolverInput` is the main input struct. It contains arrays of the other input structs.
35+
`JresSolverInput` is the main input struct. It contains arrays of the other input structs and global constraints.
2436

2537
| Field | Type | Description |
2638
| :--- | :--- | :--- |
39+
| `consecutiveStints` | `int` | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
40+
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. <br> **Integrated Mode:** Applies to combined Driving and Spotting time. <br> **Sequential Mode:** Applies only to Driving. |
2741
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members. |
2842
| `teamMembers_len` | `int` | The number of team members. |
2943
| `availability` | `JresMemberAvailability*` | A pointer to an array of availability information. |
@@ -38,8 +52,6 @@ The C-API uses the following structs to pass data to and from the solver.
3852
| `name` | `const char*` | Unique identifier for the member. |
3953
| `isDriver` | `int` | `1` if the member can drive, `0` otherwise. |
4054
| `isSpotter` | `int` | `1` if the member can spot, `0` otherwise. |
41-
| `maxStints`| `int` | Hard constraint: Maximum number of consecutive stints a member can perform. |
42-
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. <br> **Integrated Mode:** Applies to combined Driving and Spotting time. <br> **Sequential Mode:** Applies only to Driving. |
4355
| `tzOffset` | `double` | Timezone offset in hours from UTC. |
4456

4557
`JresStint`
@@ -78,6 +90,18 @@ These structs are used to represent the availability of team members.
7890
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members, including their tzOffset. |
7991
| `teamMembers_len` | `int` | The number of team members. |
8092

93+
`JresSolverOptions`
94+
95+
| Field | Type | Description |
96+
| :--- | :--- | :--- |
97+
| `timeLimit` | `int` | Maximum time in seconds to let the solver run. |
98+
| `spotterMode` | `JresSpotterMode` | Type of spotter scheduling to use (`NONE`, `INTEGRATED`, `SEQUENTIAL`). |
99+
| `allowNoSpotter` | `bool` | Allow stints to have no spotter assigned. |
100+
| `optimalityGap` | `double` | Solver stops when the gap to optimal is less than this (e.g., `0.2`). |
101+
| `switchingPenalty`| `double` | Penalty applied when switching drivers between stints (default: `0.0`). |
102+
| `roleCouplingWeight`| `double` | Weight for coupling driver and spotter roles (default: `0.0`). |
103+
| `rotationBeatWeight`| `double` | Weight for adhering to a rotation beat or fairness metric (default: `0.0`). |
104+
81105
`JresScheduleEntry`
82106

83107
| Field | Type | Description |
@@ -99,6 +123,7 @@ options.timeLimit = 5;
99123
options.spotterMode = JRES_SPOTTER_MODE_INTEGRATED;
100124
options.allowNoSpotter = false;
101125
options.optimalityGap = 0.2;
126+
options.switchingPenalty = 10.0; // Encourage longer driver shifts
102127

103128
// Create input struct from JSON
104129
JresSolverInput* input = jres_input_from_json(raceDataJson);
@@ -140,6 +165,14 @@ Mixed Integer Programming problems like race scheduling are NP-hard. The solver
140165
141166
The solver prioritizes hard constraints (rest times, fuel, availability) first. The optimality gap only affects soft preferences like minimizing consecutive stints. A 20% gap on these preferences is imperceptible in real-world use.
142167
168+
#### Switching Penalty
169+
170+
The `switchingPenalty` option adds a cost to the optimization objective every time the driver changes between two consecutive stints.
171+
172+
* **Goal**: To encourage the solver to keep the same driver in the car for multiple stints (e.g., doing a "double stint"), even if it's not strictly required by the `consecutiveStints` constraint.
173+
* **Relationship to `consecutiveStints`**: `consecutiveStints` is a **hard constraint** (the solver *must* schedule blocks of this size). The `switchingPenalty` is a **soft incentive** that applies at the boundaries of these blocks to discourage swapping drivers even when a block ends.
174+
* **Recommended Value**: Start with `10.0`. If the solver still switches drivers too often for your preference, increase it. If it starts violating preferred availability slots just to avoid a switch, decrease it.
175+
143176
-----
144177
145178
### JSON Helper Functions
@@ -160,6 +193,8 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
160193
161194
| Field | Type | Required | Description |
162195
| :--- | :--- | :--- | :--- |
196+
| `consecutiveStints` | Integer | No (Default `1`) | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
197+
| `minimumRestHours` | Integer | No (Default `0`) | Hard constraint: Minimum contiguous rest time required once per race. <br> **Integrated Mode:** Applies to combined Driving and Spotting time. <br> **Sequential Mode:** Applies only to Driving. |
163198
| `teamMembers` | Array | Yes | List of drivers and spotters (see below). |
164199
| `availability` | Object | Yes | Map of availability constraints (see below). |
165200
| `stints` | Array | Yes | List of pre-defined race stints (see below). |
@@ -179,8 +214,6 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
179214
| `name` | String | **Required** | Unique identifier for the member. |
180215
| `isDriver` | Boolean | `true` | Can this member drive? |
181216
| `isSpotter` | Boolean | `false` | Can this member spot? |
182-
| `maxStints` | Integer| `1` | Hard constraint: Maximum number of consecutive stints a member can perform. |
183-
| `minimumRestHours` | Integer| `0` | Hard constraint: Minimum contiguous rest time required once per race. <br> **Integrated Mode:** Applies to combined Driving and Spotting time. <br> **Sequential Mode:** Applies only to Driving. |
184217
| `tzOffset` | Number | `0.0` | Timezone offset in hours from UTC. |
185218
186219
#### Availability Map & Time Formatting
@@ -203,13 +236,13 @@ The `availability` object maps a **Team Member's Name** to a dictionary of **Tim
203236
204237
```json
205238
{
239+
"consecutiveStints": 2,
240+
"minimumRestHours": 4,
206241
"teamMembers": [
207242
{
208243
"name": "Niki",
209244
"isDriver": true,
210-
"isSpotter": true,
211-
"maxStints": 2,
212-
"minimumRestHours": 4
245+
"isSpotter": true
213246
},
214247
{
215248
"name": "Alain",

TOOLS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jres_solver.exe [options]
5353
| `-s` | `--spotter-mode` | Strategy for assigning spotters. Options: `none`, `integrated`, `sequential`. | `none` |
5454
| | `--allow-no-spotter` | Allow specific stints to have no spotter assigned (if spotter mode is active). | `false` |
5555
| `-g` | `--optimality-gap` | Stop solver when the solution is within this gap of perfection (e.g., `0.2` for 20%). | `0.2` |
56+
| | `--switching-penalty`| Penalty cost for switching drivers (positive disincentivizes switching). | `0.0` |
57+
| | `--role-coupling-weight`| Reward weight for coupling roles (positive incentivizes driver->spotter). | `0.0` |
58+
| | `--rotation-beat-weight`| Penalty weight for rotation deviation (positive incentivizes adherence). | `0.0` |
5659
| `-d` | `--diagnose` | Run in **Diagnostic Mode** to explain why a schedule is infeasible. | `false` |
5760
| `-h` | `--help` | Print usage instructions. | |
5861

cmd/formatter/cli.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ int main(int argc, char* argv[]) {
7171

7272
// --- Logic: Determine Format ---
7373
if (result.count("format")) {
74-
// 1. Explicitly provided by user
74+
// Explicitly provided by user
7575
format = result["format"].as<std::string>();
7676
} else {
77-
// 2. Auto-detect from extension
77+
// Auto-detect from extension
7878
std::string ext = get_extension(output_path);
7979
if (ext == ".csv") {
8080
format = "csv";
@@ -83,7 +83,7 @@ int main(int argc, char* argv[]) {
8383
} else if (ext == ".zip") {
8484
format = "zip";
8585
} else {
86-
// 3. Unknown extension and no flag -> Error
86+
// Unknown extension and no flag -> Error
8787
std::cerr << "Error: Could not determine output format from filename extension ("
8888
<< ext << ")." << std::endl;
8989
std::cerr << "Please explicitly specify format using -f <zip|csv|txt> or use a standard file extension." << std::endl;

cmd/solver/cli.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,26 @@ int main(int argc, char **argv)
2727
{
2828
// --- Parse Command-Line Arguments ---
2929
cxxopts::Options options("solver", "JRES endurance race solver.");
30-
options.add_options()
30+
31+
options.add_options("General")
3132
("i,input", "Path to the race data .json file. Reads from stdin if not provided.", cxxopts::value<std::string>())
3233
("o,output", "Optional. Path to save the schedule as a JSON file.", cxxopts::value<std::string>())
33-
("t,time-limit", "Maximum time in seconds to let the solver run.", cxxopts::value<int>()->default_value("5"))
3434
("q,quiet", "Suppress INFO logs and final schedule print-out.", cxxopts::value<bool>()->default_value("false"))
35-
("s,spotter-mode", "Method for scheduling spotters (none, integrated, sequential).", cxxopts::value<std::string>()->default_value("none"))
36-
("allow-no-spotter", "Allow stints to have no spotter assigned.", cxxopts::value<bool>()->default_value("false"))
37-
("g,optimality-gap", "Solver stops when the gap to optimal is less than this (e.g., 0.2 for 20%).", cxxopts::value<double>()->default_value("0.2"))
38-
("d,diagnose", "Run diagnostics to explain why a schedule is infeasible.", cxxopts::value<bool>()->default_value("false"))
3935
("v,version", "Print version information and exit.")
4036
("h,help", "Print usage.");
4137

38+
options.add_options("Solver Configuration")
39+
("t,time-limit", "Maximum time in seconds to let the solver run.", cxxopts::value<int>()->default_value("5"))
40+
("d,diagnose", "Run diagnostics to explain why a schedule is infeasible.", cxxopts::value<bool>()->default_value("false"))
41+
("s,spotter-mode", "Method for scheduling spotters (none, integrated, sequential).", cxxopts::value<std::string>()->default_value("none"))
42+
("allow-no-spotter", "Allow stints to have no spotter assigned.", cxxopts::value<bool>()->default_value("false"))
43+
("g,optimality-gap", "Solver stops when the gap to optimal is less than this (e.g., 0.2 for 20%).", cxxopts::value<double>()->default_value("0.2"));
44+
45+
options.add_options("Advanced Optimization")
46+
("switching-penalty", "Penalty cost for switching drivers (positive disincentivizes switching).", cxxopts::value<double>()->default_value("0.0"))
47+
("role-coupling-weight", "Reward weight for coupling roles (positive incentivizes driver->spotter).", cxxopts::value<double>()->default_value("0.0"))
48+
("rotation-beat-weight", "Penalty weight for rotation deviation (positive incentivizes adherence).", cxxopts::value<double>()->default_value("0.0"));
49+
4250
auto result = options.parse(argc, argv);
4351

4452
if (result.count("version"))
@@ -113,6 +121,9 @@ int main(int argc, char **argv)
113121

114122
solverOptions.allowNoSpotter = result["allow-no-spotter"].as<bool>();
115123
solverOptions.optimalityGap = result["optimality-gap"].as<double>();
124+
solverOptions.switchingPenalty = result["switching-penalty"].as<double>();
125+
solverOptions.roleCouplingWeight = result["role-coupling-weight"].as<double>();
126+
solverOptions.rotationBeatWeight = result["rotation-beat-weight"].as<double>();
116127

117128
// Call the Solver Library
118129
JresSolverInput* solverInput = jres_input_from_json(raceDataJsonString.c_str());

data/24h_race.json

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,43 @@
11
{
22
"success": true,
3+
"consecutiveStints": 2,
4+
"minimumRestHours": 6,
35
"teamMembers": [
46
{
57
"name": "Niki",
68
"isDriver": true,
79
"isSpotter": true,
8-
"tzOffset": 1,
9-
"maxStints": 1,
10-
"minimumRestHours": 6
10+
"tzOffset": 1
1111
},
1212
{
1313
"name": "Ayrton",
1414
"isDriver": true,
1515
"isSpotter": false,
16-
"tzOffset": -3,
17-
"maxStints": 2,
18-
"minimumRestHours": 6
16+
"tzOffset": -3
1917
},
2018
{
2119
"name": "Jack",
2220
"isDriver": true,
2321
"isSpotter": true,
24-
"tzOffset": 11,
25-
"maxStints": 3,
26-
"minimumRestHours": 6
22+
"tzOffset": 11
2723
},
2824
{
2925
"name": "James",
3026
"isDriver": true,
3127
"isSpotter": true,
32-
"tzOffset": 0,
33-
"maxStints": 2,
34-
"minimumRestHours": 6
28+
"tzOffset": 0
3529
},
3630
{
3731
"name": "Mario",
3832
"isDriver": true,
3933
"isSpotter": true,
40-
"tzOffset": -5,
41-
"maxStints": 1,
42-
"minimumRestHours": 6
34+
"tzOffset": -5
4335
},
4436
{
4537
"name": "Ricky",
4638
"isDriver": false,
4739
"isSpotter": true,
48-
"tzOffset": -6,
49-
"maxStints": 2,
50-
"minimumRestHours": 6
40+
"tzOffset": -6
5141
}
5242
],
5343
"availability": {
@@ -368,4 +358,4 @@
368358
}
369359
],
370360
"firstStintDriver": "James"
371-
}
361+
}

data/no_solution.json

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,31 @@
11
{
2+
"consecutiveStints": 2,
3+
"minimumRestHours": 0,
24
"teamMembers": [
35
{
46
"name": "Brandon",
57
"isDriver": true,
6-
"isSpotter": true,
7-
"preferredStints": 2,
8-
"minimumRestHours": 0
8+
"isSpotter": true
99
},
1010
{
1111
"name": "Cesar",
1212
"isDriver": true,
13-
"isSpotter": true,
14-
"preferredStints": 2,
15-
"minimumRestHours": 0
13+
"isSpotter": true
1614
},
1715
{
1816
"name": "Harvey",
1917
"isDriver": true,
20-
"isSpotter": true,
21-
"preferredStints": 2,
22-
"minimumRestHours": 0
18+
"isSpotter": true
2319
},
2420
{
2521
"name": "Jay",
2622
"isDriver": true,
27-
"isSpotter": true,
28-
"preferredStints": 2,
29-
"minimumRestHours": 0
23+
"isSpotter": true
3024
},
3125
{
3226
"name": "Jack",
3327
"isDriver": true,
34-
"isSpotter": true,
35-
"preferredStints": 2,
36-
"minimumRestHours": 0
28+
"isSpotter": true
3729
}
3830
],
3931
"availability": {

data/short_race.json

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
{
22
"success": true,
3+
"consecutiveStints": 2,
4+
"minimumRestHours": 0,
35
"teamMembers": [
46
{
57
"name": "Niki",
68
"tzOffset": 1,
79
"isDriver": true,
8-
"isSpotter": true,
9-
"preferredStints": 2,
10-
"minimumRestHours": 0
10+
"isSpotter": true
1111
},
1212
{
1313
"name": "Ayrton",
1414
"tzOffset": -3,
1515
"isDriver": true,
16-
"isSpotter": true,
17-
"preferredStints": 2,
18-
"minimumRestHours": 0
16+
"isSpotter": true
1917
},
2018
{
2119
"name": "Alain",
2220
"tzOffset": 1,
2321
"isDriver": false,
24-
"isSpotter": true,
25-
"preferredStints": 2,
26-
"minimumRestHours": 0
22+
"isSpotter": true
2723
}
2824
],
2925
"availability": {

0 commit comments

Comments
 (0)