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
5 changes: 5 additions & 0 deletions .github/workflows/linux-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ jobs:
- name: Run Tests
run: ctest --test-dir build --output-on-failure -C Release

- name: Run Integration Tests
run: |
chmod +x test/integration/run_test.sh
./test/integration/run_test.sh

# ---------------------------------------------------------
# Install (Stage Files)
# ---------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/macos-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ jobs:
- name: Run Tests
run: ctest --test-dir build --output-on-failure -C Release

- name: Run Integration Tests
run: |
chmod +x test/integration/run_test.sh
./test/integration/run_test.sh

# ---------------------------------------------------------
# Install (Stage Files)
# ---------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/win-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ jobs:
cmake --build build --config Release --target formatter_tests
ctest --test-dir build --output-on-failure -C Release

- name: Run Integration Tests
if: inputs.run_integration_tests == true
shell: pwsh
run: ./test/integration/run_test.bat

# ---------------------------------------------------------
# Install (Stage Files)
# ---------------------------------------------------------
Expand Down
104 changes: 104 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Gemini Project: JRES Solver C++

This document provides instructions for understanding, building, and contributing to the JRES Solver C++ project.

## Project Overview

JRES Solver 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 various constraints.

The project is structured as a C-API library (`libjres_solver.a`) with two command-line interface (CLI) clients:
* `jres_solver`: A tool that uses the library to solve race schedules.
* `jres_formatter`: A tool to format the output of the solver.

The library provides helper functions to convert between its C-API data structures and JSON, making it easier to integrate with other systems.

### Public headers

The library exposes its API via headers in `include/jres_solver/`. Designed for seamless Foreign Function Interface (FFI) integration with languages like Python, Ruby, and Go, these headers are strictly:
* **C-Compatible:** All exported symbols utilize extern "C" linkage.
* **Self-Contained:** The interface uses standard C types and opaque handles only, ensuring no dependency on the C++ Standard Library (STL) at the boundary.
* **Standalone:** Each header is fully self-sufficient and requires no prior includes.

## Building and Running

This project uses CMake for building.

### Dependencies

* **CMake (version 3.15+)**
* **Git** (for managing submodules)
* **HiGHS Optimization Solver**: This must be installed on your system.
* **macOS (Homebrew):** `brew install highs`
* **Linux:** Build from source (see `CONTRIBUTING.md`).
* **Windows (vcpkg):** `vcpkg install highs:x64-windows-static`

### Build Steps

1. **Clone the repository and initialize submodules:**
```bash
git clone <repository_url> jres_solver_cpp
cd jres_solver_cpp
git submodule update --init --recursive
```

2. **Configure the project with CMake:**
```bash
mkdir build
cd build
# Add platform-specific flags if needed, e.g., for macOS with Homebrew:
# cmake .. -DCMAKE_PREFIX_PATH=/opt/homebrew
cmake ..
```

3. **Build the project:**
```bash
cmake --build .
```
This will generate the library and executables in the `build/` directory.

### Running Tests

The project uses GoogleTest for its test suite. To run the tests, execute the following command from the `build` directory:

```bash
ctest
```

### Running the CLI Tools

The compiled executables are located in the `build/` directory.

**Solver (`jres_solver`):**

```bash
# 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 diagnostics on an infeasible schedule
./jres_solver -i ../data/short_race_no_solution.json --diagnose
```

**Formatter (`jres_formatter`):**

The formatter takes the JSON output from the solver and can generate different report formats.

```bash
# Load a solution and generate a txt summary
./jres_formatter -i /tmp/24_race_solution.json -o /tmp/summary.txt
```

## Development Conventions

* **Build System:** The project uses CMake for building and configuration.
* **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.
- **Execution:** Don't stage commits or otherwise try to manage the repo.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ The C-API uses the following structs to pass data to and from the solver.
| `name` | `const char*` | Unique identifier for the member. |
| `isDriver` | `int` | `1` if the member can drive, `0` otherwise. |
| `isSpotter` | `int` | `1` if the member can spot, `0` otherwise. |
| `preferredStints`| `int` | Soft constraint: solver attempts to limit consecutive stints to this number. |
| `maxStints`| `int` | Hard constraint: Maximum number of consecutive stints a member can perform. |
| `minimumRestHours` | `int` | Hard constraint: Minimum rest time required after a driving shift before driving again. |
| `tzOffset` | `double` | Timezone offset in hours from UTC. |

`JresStint`

Expand Down Expand Up @@ -72,6 +73,8 @@ These structs are used to represent the availability of team members.
| `schedule_len` | `int` | The number of schedule entries. |
| `diagnosis` | `const char**` | An array of strings with diagnostic information. Empty on success. |
| `diagnosis_len` | `int` | The number of diagnosis strings. |
| `teamMembers` | `JresTeamMember*` | A pointer to an array of team members, including their tzOffset. |
| `teamMembers_len` | `int` | The number of team members. |

`JresScheduleEntry`

Expand Down Expand Up @@ -174,6 +177,7 @@ The `raceDataJson` string passed to `jres_input_from_json` must strictly follow
| `isSpotter` | Boolean | `false` | Can this member spot? |
| `maxStints` | Integer| `1` | Hard constraint: Maximum number of consecutive stints a member can perform. |
| `minimumRestHours` | Integer| `0` | Hard constraint: Minimum rest time required after a driving shift before driving again. |
| `tzOffset` | Number | `0.0` | Timezone offset in hours from UTC. |

#### Availability Map & Time Formatting

Expand Down Expand Up @@ -233,6 +237,7 @@ The `jres_output_to_json` function returns a JSON string containing the solution
| `schedule` | Array | List of optimized stint assignments. |
| `diagnosis`| Array | List of strings with diagnostic information. Empty on success. |
| `stats` | Object | Solver performance and complexity metrics. |
| `teamMembers` | Array | List of team members and their properties. |

##### Stats Object

Expand Down
4 changes: 2 additions & 2 deletions cmd/formatter/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ int main(int argc, char* argv[]) {
return 1;
}

if (!solved_data.contains("schedule") || !solved_data.contains("raceData")) {
std::cerr << "Error: Invalid JSON. Expected keys 'schedule' and 'raceData'." << std::endl;
if (!solved_data.contains("schedule") || !solved_data.contains("teamMembers")) {
std::cerr << "Error: Invalid JSON. Expected keys 'schedule' and 'teamMembers'." << std::endl;
return 1;
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/solver/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ int main(int argc, char **argv)
if (solverOutput->schedule_len > 0) {
for (int i = 0; i < solverOutput->schedule_len; ++i) {
std::stringstream ss;
ss << "Stint " << std::setw(3) << solverOutput->schedule[i].stintId
ss << "Stint " << std::setw(3) << solverOutput->schedule[i].id
<< ": Driver: " << std::setw(15) << std::left << solverOutput->schedule[i].driver;

if (hasSpotters) {
Expand Down
6 changes: 6 additions & 0 deletions data/24h_race.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,47 @@
"name": "Niki",
"isDriver": true,
"isSpotter": true,
"tzOffset": 1,
"maxStints": 1,
"minimumRestHours": 8
},
{
"name": "Ayrton",
"isDriver": true,
"isSpotter": false,
"tzOffset": -3,
"maxStints": 2,
"minimumRestHours": 8
},
{
"name": "Jack",
"isDriver": true,
"isSpotter": true,
"tzOffset": 11,
"maxStints": 3,
"minimumRestHours": 8
},
{
"name": "James",
"isDriver": true,
"isSpotter": true,
"tzOffset": 0,
"maxStints": 2,
"minimumRestHours": 8
},
{
"name": "Mario",
"isDriver": true,
"isSpotter": true,
"tzOffset": -5,
"maxStints": 1,
"minimumRestHours": 8
},
{
"name": "Ricky",
"isDriver": false,
"isSpotter": true,
"tzOffset": -6,
"maxStints": 2,
"minimumRestHours": 8
}
Expand Down
6 changes: 3 additions & 3 deletions data/short_race.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
"teamMembers": [
{
"name": "Niki",
"timezone": "1",
"tzOffset": 1,
"isDriver": true,
"isSpotter": true,
"preferredStints": 2,
"minimumRestHours": 8
},
{
"name": "Ayrton",
"timezone": "-3",
"tzOffset": -3,
"isDriver": true,
"isSpotter": true,
"preferredStints": 2,
"minimumRestHours": 8
},
{
"name": "Alain",
"timezone": "1",
"tzOffset": 1,
"isDriver": false,
"isSpotter": true,
"preferredStints": 2,
Expand Down
67 changes: 67 additions & 0 deletions data/short_race_no_solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"success": true,
"teamMembers": [
{
"name": "Niki",
"tzOffset": 1,
"isDriver": true,
"isSpotter": true,
"preferredStints": 2,
"minimumRestHours": 8
},
{
"name": "Ayrton",
"tzOffset": -3,
"isDriver": true,
"isSpotter": true,
"preferredStints": 2,
"minimumRestHours": 8
},
{
"name": "Alain",
"tzOffset": 1,
"isDriver": false,
"isSpotter": true,
"preferredStints": 2,
"minimumRestHours": 8
}
],
"availability": {
"Niki": {
"1973-06-09T14:00:00.000Z": "Unavailable",
"1973-06-09T15:00:00.000Z": "Unavailable",
"1973-06-09T16:00:00.000Z": "Available",
"1973-06-09T17:00:00.000Z": "Available"
},
"Ayrton": {
"1973-06-09T14:00:00.000Z": "Available",
"1973-06-09T15:00:00.000Z": "Unavailable",
"1973-06-09T16:00:00.000Z": "Available",
"1973-06-09T17:00:00.000Z": "Available"
},
"Alain": {
"1973-06-09T14:00:00.000Z": "Available",
"1973-06-09T15:00:00.000Z": "Available",
"1973-06-09T16:00:00.000Z": "Available",
"1973-06-09T17:00:00.000Z": "Unavailable"
}
},
"stints": [
{
"id": 1,
"startTime": "1973-06-09T14:37:00.000Z",
"endTime": "1973-06-09T15:27:16.500Z"
},
{
"id": 2,
"startTime": "1973-06-09T15:27:16.500Z",
"endTime": "1973-06-09T16:17:33.000Z"
},
{
"id": 3,
"startTime": "1973-06-09T16:17:33.000Z",
"endTime": "1973-06-09T16:37:00.000Z"
}
],
"firstStintDriver": null
}
10 changes: 10 additions & 0 deletions include/jres_solver/jres_json_converter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ void free_jres_solver_input(JresSolverInput* input);
*/
void free_jres_solver_output(JresSolverOutput* output);

/**
* @brief Retrieves the last error message that occurred in a C-API function.
*
* The caller does NOT own the returned string and must NOT free it.
* The message is thread-local and valid until the next C-API call on the same thread.
*
* @return A C-string containing the last error message, or an empty string if no error.
*/
const char* jres_get_last_error();

#ifdef __cplusplus
} // extern "C"
#endif
Expand Down
14 changes: 13 additions & 1 deletion include/jres_solver/jres_solver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ struct JresTeamMember {
int maxStints;
/** @brief Minimum rest time in hours required after a shift. */
int minimumRestHours;
/** @brief Timezone offset, in hours from UTC. */
double tzOffset;
};

/**
Expand Down Expand Up @@ -135,7 +137,11 @@ struct JresSolverInput {
*/
struct JresScheduleEntry {
/** @brief The ID of the stint. */
int stintId;
int id;
/** @brief ISO 8601 timestamp for the start of the stint. */
const char* startTime;
/** @brief ISO 8601 timestamp for the end of the stint. */
const char* endTime;
/** @brief Name of the assigned driver. */
const char* driver;
/** @brief Name of the assigned spotter. */
Expand Down Expand Up @@ -176,6 +182,12 @@ struct JresSolverOutput {
int diagnosis_len;
/** @brief Solver performance and complexity metrics. */
JresSolverStats* stats;
/** @brief The options used to generate this solution. */
JresSolverOptions* options;
/** @brief A pointer to an array of team members, including their tzOffset. */
JresTeamMember* teamMembers;
/** @brief The number of team members. */
int teamMembers_len;
};

// --- Public C-API Functions ---
Expand Down
Loading
Loading