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
3 changes: 0 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ The `jres_solver` executable is a client that uses the `jres_solver` library.
# Run with a file
./jres_solver -i ../data/short_race.json -s integrated

# Pipe from stdin
cat ../data/24h_race.json | ./jres_solver -s sequential --allow-no-spotter

# Run diagnostics on a failing schedule
./jres_solver -i ../data/infeasible.json --diagnose

Expand Down
31 changes: 23 additions & 8 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# ⚠️ CRITICAL AGENT INSTRUCTIONS ⚠️

**Adhere strictly to the following mandates. Failure to do so is a critical error.**

1. **NO GIT MODIFICATIONS:**
* **STATUS:** **STRICTLY READ-ONLY**.
* **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.
* **ALLOWED COMMANDS:** `git status`, `git log`, `git diff`, `git show`.
* **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.

2. **Verbosity:** Low. Do not explain the code unless asked. Just output the diff or the file.
3. **Reasoning:** Perform deep reasoning internally, but output only the final solution.

---

# Gemini Project: JRES Solver C++

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

There is also a script to test the formatter's stdout functionality:
```bash
./test_formatter_stdout.sh
```

### Running the CLI Tools

The compiled executables are located in the `build/` directory.
Expand All @@ -74,8 +94,8 @@ The compiled executables are located in the `build/` directory.
# Run with an input file
./jres_solver -i ../data/short_race.json -s integrated

# Pipe data from stdin and output to a file
cat ../data/24h_race.json | ./jres_solver -s sequential -o /tmp/24_race_solution.json
# Run with an input file and output to a file
./jres_solver -i ../data/24h_race.json -s sequential -o /tmp/24_race_solution.json

# Run diagnostics on an infeasible schedule
./jres_solver -i ../data/short_race_no_solution.json --diagnose
Expand All @@ -96,9 +116,4 @@ The formatter takes the JSON output from the solver and can generate different r
* **Dependencies:** C++ library dependencies are managed as Git submodules (`cxxopts`, `nlohmann/json`). The HiGHS solver is an external dependency.
* **Testing:** The test suite is built with GoogleTest and run via CTest. New tests should be added to the `test/` directory.
* **API Design:** The core logic is exposed as a C-API for wider compatibility. Helper functions are provided for JSON serialization and deserialization.
* **Code Style:** The codebase is written in C++. Please follow the existing coding style when contributing.

# MODEL INSTRUCTIONS
- **Verbosity:** Low. Do not explain the code unless asked. Just output the diff or the file.
- **Reasoning:** Perform deep reasoning internally, but output only the final solution.
- **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.
* **Code Style:** The codebase is written in C++. Please follow the existing coding style when contributing.
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

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

>[!NOTE]
>this is based on the python JRES Solver https://github.com/popmonkey/jres_solver

## Additional Documentation

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

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

Expand All @@ -37,7 +33,8 @@ The C-API uses the following structs to pass data to and from the solver.
| Field | Type | Description |
| :--- | :--- | :--- |
| `consecutiveStints` | `int` | Hard constraint: Required number of consecutive stints a driver must perform (block size). |
| `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. |
| `minimumRestHours` | `int` | Hard constraint: Minimum contiguous rest time required once per race. |
| `firstStintDriver` | `const char*` | Hard constraint: The name of the team member who must drive the first stint. |
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members. |
| `teamMembers_len` | `int` | The number of team members. |
| `availability` | `JresMemberAvailability*` | A pointer to an array of availability information. |
Expand Down Expand Up @@ -98,7 +95,6 @@ These structs are used to represent the availability of team members.
| `spotterMode` | `JresSpotterMode` | Type of spotter scheduling to use (`NONE`, `INTEGRATED`, `SEQUENTIAL`). |
| `allowNoSpotter` | `bool` | Allow stints to have no spotter assigned. |
| `optimalityGap` | `double` | Solver stops when the gap to optimal is less than this (e.g., `0.2`). |
| `switchingPenalty`| `double` | Penalty applied when switching drivers between stints (default: `0.0`). |
| `roleCouplingWeight`| `double` | Weight for coupling driver and spotter roles (default: `0.0`). |
| `rotationBeatWeight`| `double` | Weight for adhering to a rotation beat or fairness metric (default: `0.0`). |

Expand All @@ -123,7 +119,6 @@ options.timeLimit = 5;
options.spotterMode = JRES_SPOTTER_MODE_INTEGRATED;
options.allowNoSpotter = false;
options.optimalityGap = 0.2;
options.switchingPenalty = 10.0; // Encourage longer driver shifts

// Create input struct from JSON
JresSolverInput* input = jres_input_from_json(raceDataJson);
Expand Down Expand Up @@ -165,14 +160,6 @@ Mixed Integer Programming problems like race scheduling are NP-hard. The solver

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.

#### Switching Penalty

The `switchingPenalty` option adds a cost to the optimization objective every time the driver changes between two consecutive stints.

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

-----

### JSON Helper Functions
Expand All @@ -194,7 +181,8 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `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. <br> **Integrated Mode:** Applies to combined Driving and Spotting time. <br> **Sequential Mode:** Applies only to Driving. |
| `minimumRestHours` | Integer | No (Default `0`) | Hard constraint: Minimum contiguous rest time required once per race. |
| `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). |
| `stints` | Array | Yes | List of pre-defined race stints (see below). |
Expand Down Expand Up @@ -238,6 +226,7 @@ The `availability` object maps a **Team Member's Name** to a dictionary of **Tim
{
"consecutiveStints": 2,
"minimumRestHours": 4,
"firstStintDriver": "Niki",
"teamMembers": [
{
"name": "Niki",
Expand Down Expand Up @@ -333,6 +322,10 @@ When the solver fails, the `schedule` array will be empty, and the `diagnosis` a
}
```

---
>[!NOTE]
>this is based on the python JRES Solver https://github.com/popmonkey/jres_solver

---

_Created by popmonkey, Gemini 2.5, Gemini 3.0, and ChatGPT 5.1_
16 changes: 3 additions & 13 deletions TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This suite consists of two command-line tools designed to generate and format driver schedules for endurance racing events.

> [!NOTE]
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)
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)

## Platform Support

Expand Down Expand Up @@ -39,21 +39,20 @@ jres_solver.exe [options]

### Input/Output Behavior

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

### Options

| Flag | Long Flag | Description | Default |
| :--- | :------------------- | :-------------------------------------------------------------------------------------------- | :------ |
| `-i` | `--input` | Path to the race data `.json` file. Reads from `stdin` if omitted. | `stdin` |
| `-i` | `--input` | Path to the race data `.json` file. (Required) | |
| `-o` | `--output` | Path to save the calculated schedule (JSON). **Required for the Formatter.** | `stdout`|
| `-t` | `--time-limit` | Maximum time (in seconds) to let the optimizer run. | `5` |
| `-q` | `--quiet` | Suppress INFO logs and the printed schedule summary. | `false` |
| `-s` | `--spotter-mode` | Strategy for assigning spotters. Options: `none`, `integrated`, `sequential`. | `none` |
| | `--allow-no-spotter` | Allow specific stints to have no spotter assigned (if spotter mode is active). | `false` |
| `-g` | `--optimality-gap` | Stop solver when the solution is within this gap of perfection (e.g., `0.2` for 20%). | `0.2` |
| | `--switching-penalty`| Penalty cost for switching drivers (positive disincentivizes switching). | `0.0` |
| | `--role-coupling-weight`| Reward weight for coupling roles (positive incentivizes driver->spotter). | `0.0` |
| | `--rotation-beat-weight`| Penalty weight for rotation deviation (positive incentivizes adherence). | `0.0` |
| `-d` | `--diagnose` | Run in **Diagnostic Mode** to explain why a schedule is infeasible. | `false` |
Expand Down Expand Up @@ -113,16 +112,7 @@ For complex schedules, allow more time while maintaining a practical optimality
./jres_solver -i race_config.json -o solution.json -t 30 --optimality-gap 0.2
```

**Pipeline Usage:**
Pipe a JSON generator directly into the solver.

```sh
# Linux / macOS
cat race_data.json | ./jres_solver -o solution.json

# Windows PowerShell
Get-Content race_data.json | .\jres_solver.exe -o solution.json
```

---

Expand Down
48 changes: 27 additions & 21 deletions cmd/formatter/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ int main(int argc, char* argv[]) {

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

if (!result.count("output")) {
std::cerr << "Error: Output file is required (-o)" << std::endl;
return 1;
}

std::string input_path = result["input"].as<std::string>();
std::string output_path = result["output"].as<std::string>();
std::string output_path = "";
if (result.count("output")) {
output_path = result["output"].as<std::string>();
}

std::string format;

// --- Logic: Determine Format ---
if (result.count("format")) {
// Explicitly provided by user
format = result["format"].as<std::string>();
} else {
// Auto-detect from extension
std::string ext = get_extension(output_path);
if (ext == ".csv") {
format = "csv";
} else if (ext == ".txt") {
if (output_path.empty()) {
format = "txt";
} else if (ext == ".zip") {
format = "zip";
} else {
// Unknown extension and no flag -> Error
std::cerr << "Error: Could not determine output format from filename extension ("
<< ext << ")." << std::endl;
std::cerr << "Please explicitly specify format using -f <zip|csv|txt> or use a standard file extension." << std::endl;
return 1;
// Auto-detect from extension
std::string ext = get_extension(output_path);
if (ext == ".csv") {
format = "csv";
} else if (ext == ".txt") {
format = "txt";
} else if (ext == ".zip") {
format = "zip";
} else {
// Unknown extension and no flag -> Error
std::cerr << "Error: Could not determine output format from filename extension ("
<< ext << ")." << std::endl;
std::cerr << "Please explicitly specify format using -f <zip|csv|txt> or use a standard file extension." << std::endl;
return 1;
}
std::cout << "Auto-detected format: " << format << std::endl;
}
std::cout << "Auto-detected format: " << format << std::endl;
}

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

// Call into our library
jres::write_output(solved_data, output_path, format);
std::cout << "Successfully generated " << format << " output: " << output_path << std::endl;

if (!output_path.empty()) {
std::cout << "Successfully generated " << format << " output: " << output_path << std::endl;
}

} catch (const cxxopts::exceptions::exception& e) {
std::cerr << "Error parsing options: " << e.what() << std::endl;
Expand Down
41 changes: 18 additions & 23 deletions cmd/solver/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ int main(int argc, char **argv)
cxxopts::Options options("solver", "JRES endurance race solver.");

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

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

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

// Input file is now required
if (!result.count("input"))
{
std::cerr << "Error: Input file is required." << std::endl;
std::cout << options.help() << std::endl;
return 1;
}

bool quiet = result["quiet"].as<bool>();
bool runDiagnostics = result["diagnose"].as<bool>();

Expand All @@ -72,28 +79,17 @@ int main(int argc, char **argv)
std::string raceDataJsonString;
try
{
if (result.count("input"))
{
std::string inputPath = result["input"].as<std::string>();
if (!quiet)
std::cout << "[App] Loading data from file: " << inputPath << std::endl;
std::ifstream f(inputPath);
if (!f.is_open())
{
throw std::runtime_error("Could not open input file: " + inputPath);
}
std::stringstream buffer;
buffer << f.rdbuf();
raceDataJsonString = buffer.str();
}
else
std::string inputPath = result["input"].as<std::string>();
if (!quiet)
std::cout << "[App] Loading data from file: " << inputPath << std::endl;
std::ifstream f(inputPath);
if (!f.is_open())
{
if (!quiet)
std::cout << "[App] Loading data from stdin..." << std::endl;
std::stringstream buffer;
buffer << std::cin.rdbuf();
raceDataJsonString = buffer.str();
throw std::runtime_error("Could not open input file: " + inputPath);
}
std::stringstream buffer;
buffer << f.rdbuf();
raceDataJsonString = buffer.str();
}
catch (const std::exception &e)
{
Expand Down Expand Up @@ -121,7 +117,6 @@ int main(int argc, char **argv)

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

Expand Down
Loading