Skip to content

Commit b01f148

Browse files
authored
Merge pull request #42 from popmonkey/v3-cleanup
V3 cleanup
2 parents 1f27493 + de0d16b commit b01f148

19 files changed

Lines changed: 256 additions & 200 deletions

CONTRIBUTING.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ The `jres_solver` executable is a client that uses the `jres_solver` library.
135135
# Run with a file
136136
./jres_solver -i ../data/short_race.json -s integrated
137137

138-
# Pipe from stdin
139-
cat ../data/24h_race.json | ./jres_solver -s sequential --allow-no-spotter
140-
141138
# Run diagnostics on a failing schedule
142139
./jres_solver -i ../data/infeasible.json --diagnose
143140

GEMINI.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# ⚠️ CRITICAL AGENT INSTRUCTIONS ⚠️
2+
3+
**Adhere strictly to the following mandates. Failure to do so is a critical error.**
4+
5+
1. **NO GIT MODIFICATIONS:**
6+
* **STATUS:** **STRICTLY READ-ONLY**.
7+
* **FORBIDDEN COMMANDS:** `git add`, `git commit`, `git push`, `git merge`, `git rebase`, `git checkout` (for creating branches), `git stash`, and ANY other command that modifies the git index, history, or working tree.
8+
* **ALLOWED COMMANDS:** `git status`, `git log`, `git diff`, `git show`.
9+
* **ACTION:** If the user asks you to commit changes, **REFUSE** and remind them that you are an AI assistant without authority to alter the project's version control history. You may propose commit messages or explain what changed, but **DO NOT** execute the commands.
10+
11+
2. **Verbosity:** Low. Do not explain the code unless asked. Just output the diff or the file.
12+
3. **Reasoning:** Perform deep reasoning internally, but output only the final solution.
13+
14+
---
15+
116
# Gemini Project: JRES Solver C++
217

318
This document provides instructions for understanding, building, and contributing to the JRES Solver C++ project.
@@ -64,6 +79,11 @@ The project uses GoogleTest for its test suite. To run the tests, execute the fo
6479
ctest
6580
```
6681

82+
There is also a script to test the formatter's stdout functionality:
83+
```bash
84+
./test_formatter_stdout.sh
85+
```
86+
6787
### Running the CLI Tools
6888
6989
The compiled executables are located in the `build/` directory.
@@ -74,8 +94,8 @@ The compiled executables are located in the `build/` directory.
7494
# Run with an input file
7595
./jres_solver -i ../data/short_race.json -s integrated
7696
77-
# Pipe data from stdin and output to a file
78-
cat ../data/24h_race.json | ./jres_solver -s sequential -o /tmp/24_race_solution.json
97+
# Run with an input file and output to a file
98+
./jres_solver -i ../data/24h_race.json -s sequential -o /tmp/24_race_solution.json
7999
80100
# Run diagnostics on an infeasible schedule
81101
./jres_solver -i ../data/short_race_no_solution.json --diagnose
@@ -96,9 +116,4 @@ The formatter takes the JSON output from the solver and can generate different r
96116
* **Dependencies:** C++ library dependencies are managed as Git submodules (`cxxopts`, `nlohmann/json`). The HiGHS solver is an external dependency.
97117
* **Testing:** The test suite is built with GoogleTest and run via CTest. New tests should be added to the `test/` directory.
98118
* **API Design:** The core logic is exposed as a C-API for wider compatibility. Helper functions are provided for JSON serialization and deserialization.
99-
* **Code Style:** The codebase is written in C++. Please follow the existing coding style when contributing.
100-
101-
# MODEL INSTRUCTIONS
102-
- **Verbosity:** Low. Do not explain the code unless asked. Just output the diff or the file.
103-
- **Reasoning:** Perform deep reasoning internally, but output only the final solution.
104-
- **Git Operations:** READ-ONLY. You may use `git status`, `git log`, or `git diff` to understand the context. You must *NEVER* run `git add`, `git commit`, `git push`, or any command that modifies the git history or index. Leave all version control management to the user.
119+
* **Code Style:** The codebase is written in C++. Please follow the existing coding style when contributing.

README.md

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
This library can be used to solve for optimal driver and spotter schedules for endurance racing events. It uses the **HiGHS** optimization library.
44

5-
>[!NOTE]
6-
>this is based on the python JRES Solver https://github.com/popmonkey/jres_solver
7-
85
## Additional Documentation
96

107
* **[Tools](./TOOLS.md)** - releases include some command line tools that use the library
@@ -16,7 +13,6 @@ The `jres_solver` tool supports several optimization parameters:
1613

1714
* **General:** `-i` (Input), `-o` (Output), `-t` (Time Limit), `-s` (Spotter Mode)
1815
* **Advanced Weights:**
19-
* `--switching-penalty`: Cost for driver swaps (positive disincentivizes switching)
2016
* `--role-coupling-weight`: Incentive for role coupling (positive incentivizes coupling)
2117
* `--rotation-beat-weight`: Penalty for fairness deviation (positive incentivizes adherence)
2218

@@ -37,7 +33,8 @@ The C-API uses the following structs to pass data to and from the solver.
3733
| Field | Type | Description |
3834
| :--- | :--- | :--- |
3935
| `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. |
36+
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. |
37+
| `firstStintDriver` | `const char*` | Hard constraint: The name of the team member who must drive the first stint. |
4138
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members. |
4239
| `teamMembers_len` | `int` | The number of team members. |
4340
| `availability` | `JresMemberAvailability*` | A pointer to an array of availability information. |
@@ -98,7 +95,6 @@ These structs are used to represent the availability of team members.
9895
| `spotterMode` | `JresSpotterMode` | Type of spotter scheduling to use (`NONE`, `INTEGRATED`, `SEQUENTIAL`). |
9996
| `allowNoSpotter` | `bool` | Allow stints to have no spotter assigned. |
10097
| `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`). |
10298
| `roleCouplingWeight`| `double` | Weight for coupling driver and spotter roles (default: `0.0`). |
10399
| `rotationBeatWeight`| `double` | Weight for adhering to a rotation beat or fairness metric (default: `0.0`). |
104100

@@ -123,7 +119,6 @@ options.timeLimit = 5;
123119
options.spotterMode = JRES_SPOTTER_MODE_INTEGRATED;
124120
options.allowNoSpotter = false;
125121
options.optimalityGap = 0.2;
126-
options.switchingPenalty = 10.0; // Encourage longer driver shifts
127122

128123
// Create input struct from JSON
129124
JresSolverInput* input = jres_input_from_json(raceDataJson);
@@ -165,14 +160,6 @@ Mixed Integer Programming problems like race scheduling are NP-hard. The solver
165160
166161
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.
167162
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-
176163
-----
177164
178165
### JSON Helper Functions
@@ -194,7 +181,8 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
194181
| Field | Type | Required | Description |
195182
| :--- | :--- | :--- | :--- |
196183
| `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. |
184+
| `minimumRestHours` | Integer | No (Default `0`) | Hard constraint: Minimum contiguous rest time required once per race. |
185+
| `firstStintDriver` | String | No | Hard constraint: The name of the team member who must drive the first stint. |
198186
| `teamMembers` | Array | Yes | List of drivers and spotters (see below). |
199187
| `availability` | Object | Yes | Map of availability constraints (see below). |
200188
| `stints` | Array | Yes | List of pre-defined race stints (see below). |
@@ -238,6 +226,7 @@ The `availability` object maps a **Team Member's Name** to a dictionary of **Tim
238226
{
239227
"consecutiveStints": 2,
240228
"minimumRestHours": 4,
229+
"firstStintDriver": "Niki",
241230
"teamMembers": [
242231
{
243232
"name": "Niki",
@@ -333,6 +322,10 @@ When the solver fails, the `schedule` array will be empty, and the `diagnosis` a
333322
}
334323
```
335324

325+
---
326+
>[!NOTE]
327+
>this is based on the python JRES Solver https://github.com/popmonkey/jres_solver
328+
336329
---
337330

338331
_Created by popmonkey, Gemini 2.5, Gemini 3.0, and ChatGPT 5.1_

TOOLS.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This suite consists of two command-line tools designed to generate and format driver schedules for endurance racing events.
44

55
> [!NOTE]
6-
Currently the easiest way to generate input for the tools (and library) is to use [the JRES Availability Planner spreadsheet](https://docs.google.com/spreadsheets/d/1k2WaNDhXjyHXLirju2IQKxrYExQpDCT5z6jCBM71KVc/edit?usp=sharing)
6+
Currently the easiest way to generate input for the tools (and library) is to use [the JRES Availability Planner spreadsheet](https://docs.google.com/spreadsheets/d/1hayKeug7IdZqwmta68PlqOCc8Y1JaL-QeaVal_-APxA/edit?usp=sharing)
77

88
## Platform Support
99

@@ -39,21 +39,20 @@ jres_solver.exe [options]
3939

4040
### Input/Output Behavior
4141

42-
* **Input:** Accepts a JSON file via the `-i` flag. If no input flag is provided, it reads from **Standard Input (stdin)**.
42+
* **Input:** Accepts a JSON file via the `-i` flag (Required).
4343
* **Output:** Prints the schedule summary to **stdout** (unless `-q` is used). To save the raw solution for the Formatter, use the `-o` flag.
4444

4545
### Options
4646

4747
| Flag | Long Flag | Description | Default |
4848
| :--- | :------------------- | :-------------------------------------------------------------------------------------------- | :------ |
49-
| `-i` | `--input` | Path to the race data `.json` file. Reads from `stdin` if omitted. | `stdin` |
49+
| `-i` | `--input` | Path to the race data `.json` file. (Required) | |
5050
| `-o` | `--output` | Path to save the calculated schedule (JSON). **Required for the Formatter.** | `stdout`|
5151
| `-t` | `--time-limit` | Maximum time (in seconds) to let the optimizer run. | `5` |
5252
| `-q` | `--quiet` | Suppress INFO logs and the printed schedule summary. | `false` |
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` |
5756
| | `--role-coupling-weight`| Reward weight for coupling roles (positive incentivizes driver->spotter). | `0.0` |
5857
| | `--rotation-beat-weight`| Penalty weight for rotation deviation (positive incentivizes adherence). | `0.0` |
5958
| `-d` | `--diagnose` | Run in **Diagnostic Mode** to explain why a schedule is infeasible. | `false` |
@@ -113,16 +112,7 @@ For complex schedules, allow more time while maintaining a practical optimality
113112
./jres_solver -i race_config.json -o solution.json -t 30 --optimality-gap 0.2
114113
```
115114

116-
**Pipeline Usage:**
117-
Pipe a JSON generator directly into the solver.
118115

119-
```sh
120-
# Linux / macOS
121-
cat race_data.json | ./jres_solver -o solution.json
122-
123-
# Windows PowerShell
124-
Get-Content race_data.json | .\jres_solver.exe -o solution.json
125-
```
126116

127117
---
128118

cmd/formatter/cli.cpp

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ int main(int argc, char* argv[]) {
3434

3535
options.add_options()
3636
("i,input", "Input JSON file (solved schedule)", cxxopts::value<std::string>())
37-
("o,output", "Output file path", cxxopts::value<std::string>())
37+
("o,output", "Output file path (optional, defaults to stdout if omitted)", cxxopts::value<std::string>())
3838
("f,format", "Output format (zip, csv, txt). Auto-detected from output filename if omitted.", cxxopts::value<std::string>())
3939
("v,version", "Print version information and exit.")
4040
("h,help", "Print usage")
@@ -60,36 +60,39 @@ int main(int argc, char* argv[]) {
6060
return 1;
6161
}
6262

63-
if (!result.count("output")) {
64-
std::cerr << "Error: Output file is required (-o)" << std::endl;
65-
return 1;
66-
}
67-
6863
std::string input_path = result["input"].as<std::string>();
69-
std::string output_path = result["output"].as<std::string>();
64+
std::string output_path = "";
65+
if (result.count("output")) {
66+
output_path = result["output"].as<std::string>();
67+
}
68+
7069
std::string format;
7170

7271
// --- Logic: Determine Format ---
7372
if (result.count("format")) {
7473
// Explicitly provided by user
7574
format = result["format"].as<std::string>();
7675
} else {
77-
// Auto-detect from extension
78-
std::string ext = get_extension(output_path);
79-
if (ext == ".csv") {
80-
format = "csv";
81-
} else if (ext == ".txt") {
76+
if (output_path.empty()) {
8277
format = "txt";
83-
} else if (ext == ".zip") {
84-
format = "zip";
8578
} else {
86-
// Unknown extension and no flag -> Error
87-
std::cerr << "Error: Could not determine output format from filename extension ("
88-
<< ext << ")." << std::endl;
89-
std::cerr << "Please explicitly specify format using -f <zip|csv|txt> or use a standard file extension." << std::endl;
90-
return 1;
79+
// Auto-detect from extension
80+
std::string ext = get_extension(output_path);
81+
if (ext == ".csv") {
82+
format = "csv";
83+
} else if (ext == ".txt") {
84+
format = "txt";
85+
} else if (ext == ".zip") {
86+
format = "zip";
87+
} else {
88+
// Unknown extension and no flag -> Error
89+
std::cerr << "Error: Could not determine output format from filename extension ("
90+
<< ext << ")." << std::endl;
91+
std::cerr << "Please explicitly specify format using -f <zip|csv|txt> or use a standard file extension." << std::endl;
92+
return 1;
93+
}
94+
std::cout << "Auto-detected format: " << format << std::endl;
9195
}
92-
std::cout << "Auto-detected format: " << format << std::endl;
9396
}
9497

9598
std::ifstream f(input_path);
@@ -113,7 +116,10 @@ int main(int argc, char* argv[]) {
113116

114117
// Call into our library
115118
jres::write_output(solved_data, output_path, format);
116-
std::cout << "Successfully generated " << format << " output: " << output_path << std::endl;
119+
120+
if (!output_path.empty()) {
121+
std::cout << "Successfully generated " << format << " output: " << output_path << std::endl;
122+
}
117123

118124
} catch (const cxxopts::exceptions::exception& e) {
119125
std::cerr << "Error parsing options: " << e.what() << std::endl;

cmd/solver/cli.cpp

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ int main(int argc, char **argv)
2929
cxxopts::Options options("solver", "JRES endurance race solver.");
3030

3131
options.add_options("General")
32-
("i,input", "Path to the race data .json file. Reads from stdin if not provided.", cxxopts::value<std::string>())
32+
("i,input", "Path to the race data .json file. (Required)", cxxopts::value<std::string>())
3333
("o,output", "Optional. Path to save the schedule as a JSON file.", cxxopts::value<std::string>())
3434
("q,quiet", "Suppress INFO logs and final schedule print-out.", cxxopts::value<bool>()->default_value("false"))
3535
("v,version", "Print version information and exit.")
@@ -43,7 +43,6 @@ int main(int argc, char **argv)
4343
("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"));
4444

4545
options.add_options("Advanced Optimization")
46-
("switching-penalty", "Penalty cost for switching drivers (positive disincentivizes switching).", cxxopts::value<double>()->default_value("0.0"))
4746
("role-coupling-weight", "Reward weight for coupling roles (positive incentivizes driver->spotter).", cxxopts::value<double>()->default_value("0.0"))
4847
("rotation-beat-weight", "Penalty weight for rotation deviation (positive incentivizes adherence).", cxxopts::value<double>()->default_value("0.0"));
4948

@@ -61,6 +60,14 @@ int main(int argc, char **argv)
6160
return 0;
6261
}
6362

63+
// Input file is now required
64+
if (!result.count("input"))
65+
{
66+
std::cerr << "Error: Input file is required." << std::endl;
67+
std::cout << options.help() << std::endl;
68+
return 1;
69+
}
70+
6471
bool quiet = result["quiet"].as<bool>();
6572
bool runDiagnostics = result["diagnose"].as<bool>();
6673

@@ -72,28 +79,17 @@ int main(int argc, char **argv)
7279
std::string raceDataJsonString;
7380
try
7481
{
75-
if (result.count("input"))
76-
{
77-
std::string inputPath = result["input"].as<std::string>();
78-
if (!quiet)
79-
std::cout << "[App] Loading data from file: " << inputPath << std::endl;
80-
std::ifstream f(inputPath);
81-
if (!f.is_open())
82-
{
83-
throw std::runtime_error("Could not open input file: " + inputPath);
84-
}
85-
std::stringstream buffer;
86-
buffer << f.rdbuf();
87-
raceDataJsonString = buffer.str();
88-
}
89-
else
82+
std::string inputPath = result["input"].as<std::string>();
83+
if (!quiet)
84+
std::cout << "[App] Loading data from file: " << inputPath << std::endl;
85+
std::ifstream f(inputPath);
86+
if (!f.is_open())
9087
{
91-
if (!quiet)
92-
std::cout << "[App] Loading data from stdin..." << std::endl;
93-
std::stringstream buffer;
94-
buffer << std::cin.rdbuf();
95-
raceDataJsonString = buffer.str();
88+
throw std::runtime_error("Could not open input file: " + inputPath);
9689
}
90+
std::stringstream buffer;
91+
buffer << f.rdbuf();
92+
raceDataJsonString = buffer.str();
9793
}
9894
catch (const std::exception &e)
9995
{
@@ -121,7 +117,6 @@ int main(int argc, char **argv)
121117

122118
solverOptions.allowNoSpotter = result["allow-no-spotter"].as<bool>();
123119
solverOptions.optimalityGap = result["optimality-gap"].as<double>();
124-
solverOptions.switchingPenalty = result["switching-penalty"].as<double>();
125120
solverOptions.roleCouplingWeight = result["role-coupling-weight"].as<double>();
126121
solverOptions.rotationBeatWeight = result["rotation-beat-weight"].as<double>();
127122

0 commit comments

Comments
 (0)