Skip to content

popmonkey/jres_solver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

To use these scripts, you need Python 3 and a few external libraries.

python3 -m venv env
. env/bin/activate
python3 -m pip install -r requirements.txt

JSON.Racing Enduro Scheduler (JRES) Solver

This is a two-part tool designed to generate and report on optimized schedules for endurance racing teams.


Table of Contents


Overview

The system consists of two main scripts:

  1. solver.py: This ingests race and team data from a JSON input to compute an optimal, fair, and balanced schedule. It considers driver availability, stint limits, and rest requirements to create the schedule.
  2. reporter.py: This takes the schedule generated by the solver and produces user-friendly reports in multiple formats, including detailed per-member itineraries in their local timezones.

Stage 1: solver.py - The Schedule Optimizer

This script is the core of the scheduling system. It formulates the scheduling problem as a mathematical optimization problem and solves it to find the best possible assignment of drivers and spotters to stints.

How to Run

Execute the script from the command line. You can provide the JSON input via a file path or pipe it directly to the script using standard input (stdin). The distribution includes an example data file at test_data/24hr.json.

Example using a file:

python solver.py test_data/24hr.json --output solved_schedule.json --spotter-mode integrated

Example using stdin:

cat test_data/24hr.json | python solver.py --output solved_schedule.json --spotter-mode integrated

Command-Line Arguments:

Argument Description Default
input_file (Optional) Path to the JSON input file. If omitted, the script reads from stdin. None
--output (Optional) Path to save the resulting schedule JSON file. None
--time-limit (Optional) Maximum time in seconds for the solver to run. 30
--spotter-mode (Optional) The method for scheduling spotters: none, integrated, or sequential. none
--allow-no-spotter (Optional) Allows stints to have no assigned spotter when a spotter mode is active. False
--optimality-gap (Optional) A float between 0.0 and 1.0 that tells the solver when a solution is "good enough". 0.0
--quiet (Optional) Suppresses informational log messages during execution. False

Understanding Spotter Scheduling Modes

The --spotter-mode and --allow-no-spotter flags give you fine-grained control over how spotters are assigned to stints.

  • --spotter-mode none (Default):

    • This is the simplest mode. The solver will only schedule drivers and will completely ignore the isSpotter flag on team members and any spotter-related availability.
    • When to use: Use this if you don't need to formally schedule spotters or if you handle spotters outside of this tool.
  • --spotter-mode sequential:

    • This mode performs a two-step calculation:
      1. First, it solves the entire schedule for drivers only, creating the most optimal driver rotation based on their constraints and preferences.
      2. Then, taking the driver schedule as a fixed constraint, it solves for the most optimal spotter rotation in a second pass.
    • When to use: This mode prioritizes the driver schedule above all else. It's useful if the driver rotation is the most critical factor and the spotter schedule is secondary. The downside is that it might lead to a less balanced or even infeasible spotter schedule if the driver assignments create difficult constraints.
  • --spotter-mode integrated:

    • This mode solves for both drivers and spotters simultaneously. It considers all constraints for both roles at the same time to find a globally optimal schedule for the entire team. A key built-in constraint is that a person cannot drive and spot during the same stint.
    • When to use: This is the most comprehensive and balanced approach. Use this when you want the best possible schedule considering the fairness and preferences of both drivers and spotters equally. It may take slightly longer to solve than sequential mode but often produces a better overall team schedule.
  • --allow-no-spotter (Flag):

    • This flag can be used with either sequential or integrated mode.
    • By default, the solver assumes every stint must have a spotter assigned. If it cannot find an available spotter for every single stint, the solver will fail.
    • If you add the --allow-no-spotter flag, you are telling the solver that it's acceptable for some stints to have no spotter. The solver will still try to assign spotters to balance the workload, but it won't fail if it can't find a spotter for every stint.
    • When to use: Use this if your team has a limited number of spotters and it's not feasible to cover every single stint of the race, or if you simply don't require 100% spotter coverage.

Key Scheduling Controls (via JSON Input)

The JSON input provides a comprehensive set of options for configuring the race schedule. While every setting in the JSON Input Structure section below plays a role, the following controls have the most significant impact on the solver's output. Understanding these primary controls is key to tailoring the schedule to your team's exact requirements.

Global Race Controls

These are top-level settings that affect the entire race schedule.

  • firstStintDriver: You can designate a specific driver to take the very first stint of the race. This is useful for assigning your qualifying driver or a seasoned starter to the opening laps. If this key is omitted, the solver will choose the best driver for the first stint based on other constraints.

Per-Member Controls

These settings are defined for each person within the teamMembers array and control their individual constraints.

  • preferredStints: For each team member, you can define the maximum number of consecutive stints they are willing to do. This prevents driver fatigue and ensures variety.
  • minimumRestHours: You can enforce a mandatory, uninterrupted rest period for any team member. The solver will ensure that it finds a block of time in the schedule where the member has no duties for at least this many hours.

Team Availability Control (availability object)

This is the most powerful control for managing your team's schedule in detail.

  • Structure: The availability object contains keys that match the name of a team member. The value for each member is another object where keys are hourly UTC timestamps that mark the beginning of an hour (e.g., 1973-06-09T14:00:00.000Z).
  • Hourly Blocks: The solver checks a stint's start time against these hourly blocks. For accurate scheduling, you should define availability for every hour of the race for any member who is not available for the entire event.
  • Status Values:
    • Unavailable: The solver is strictly forbidden from assigning any duty (driving or spotting) to the member if a stint starts within this hour.
    • Preferred: The solver is heavily incentivized to assign a duty to the member during this hour. This is a great way to accommodate members who want to drive/spot at a specific time.
    • Available (Default): If an hour is not specified for a member, they are considered available. There is no need to explicitly set a status of "Available".

JSON Input Structure

The solver requires a JSON input with a specific structure to function correctly.

One good source of this input is the associated JRES Availability Sheet.

Example JSON Input:

{
  "raceStartUTC": "1973-06-09T14:37:00.000Z",
  "durationHours": 24,
  "avgLapTimeInSeconds": 220.5,
  "pitTimeInSeconds": 150,
  "fuelTankSize": 120,
  "fuelUsePerLap": 9.2,
  "firstStintDriver": "James",
  "teamMembers": [
    {
      "name": "Nikki",
      "isDriver": true,
      "isSpotter": true,
      "preferredStints": 1,
      "timezone": 1,
      "minimumRestHours": 8
    },
    {
      "name": "Ayrton",
      "isDriver": true,
      "isSpotter": false,
      "preferredStints": 2,
      "timezone": -3,
      "minimumRestHours": 8
    },
    {
      "name": "Jack",
      "isDriver": true,
      "isSpotter": true,
      "preferredStints": 3,
      "timezone": 11,
      "minimumRestHours": 8
    },
    {
      "name": "James",
      "isDriver": true,
      "isSpotter": true,
      "preferredStints": 2,
      "timezone": 0,
      "minimumRestHours": 8
    },
    {
      "name": "Mario",
      "isDriver": true,
      "isSpotter": true,
      "preferredStints": 1,
      "timezone": -5,
      "minimumRestHours": 8
    },
    {
      "name": "Ricky",
      "isDriver": false,
      "isSpotter": true,
      "preferredStints": 2,
      "timezone": -6,
      "minimumRestHours": 8
    }
  ],
  "availability": {
    "Ayrton": {
      "1973-06-10T02:00:00.000Z": "Unavailable",
      "1973-06-10T03:00:00.000Z": "Unavailable"
    },
    "Ricky": {
      "1973-06-09T14:00:00.000Z": "Unavailable",
      "1973-06-09T15:00:00.000Z": "Unavailable"
    }
  }
}
  • raceStartUTC (string): The race start time in UTC (ISO 8601 format).
  • durationHours (integer): The total length of the race in hours.
  • avgLapTimeInSeconds (integer): The average time for a single lap.
  • pitTimeInSeconds (integer): The time added for a pit stop.
  • fuelTankSize (integer): The size of the car's fuel tank.
  • fuelUsePerLap (float): Fuel consumed per lap, used to calculate stint length.
  • firstStintDriver (string, optional): The name of the driver who must take the first stint.
  • teamMembers (array): A list of team member objects.
    • name (string): The member's name.
    • isDriver / isSpotter (boolean): Flags to indicate the member's roles.
    • preferredStints (integer): The maximum number of consecutive stints this person can do.
    • timezone (integer, optional): The member's UTC offset, used by the reporter.
    • minimumRestHours (integer, optional): A mandatory continuous rest period for this member.
  • availability (object): Defines when members are "Unavailable" or "Preferred" for duties. The keys are member names, and the values are objects where keys are hourly UTC timestamps.

Stage 2: reporter.py - The Report Generator

This script processes the JSON file produced by the solver to create detailed, human-readable reports.

How to Run

Execute the script from the command line, providing the solved schedule file and specifying your desired output.

Example Command:

python reporter.py solved_schedule.json --output race_schedule.xlsx --format xlsx

Command-Line Arguments:

Argument Description Default
input_file (Required) Path to the solved schedule JSON file from solver.py. None
output_file (Required) Path to save the generated report file. None
--format (Optional) The output format for the report: xlsx, csv, or txt. xlsx

Output Format Examples

The reporter can generate three different types of files.

📄 Text (.txt)

A comprehensive plain text file that includes summaries, a master schedule, and detailed itineraries for each team member.

--- DRIVER SUMMARY ---
================================================================================
Driver               | Total Stints    | Total Laps
--------------------------------------------------------------------------------
James                | 16              | 208
Nikki                | 15              | 195
Ayrton               | 16              | 208
Jack                 | 15              | 195
Mario                | 16              | 208

--- SPOTTER SUMMARY ---
================================================================================
Spotter              | Total Stints
--------------------------------------------------------------------------------
Ricky                | 26
Nikki                | 16
Jack                 | 21
James                | 15

--- MASTER SCHEDULE (UTC) ---
================================================================================
Stint   | Start Time (UTC)       | End Time (UTC)         | Driver          | Spotter
---------------------------------------------------------------------------
1       | 1973-06-09 14:37:00    | 1973-06-09 15:25:21    | James           | Nikki
2       | 1973-06-09 15:27:51    | 1973-06-09 16:16:12    | James           | Nikki
3       | 1973-06-09 16:18:42    | 1973-06-09 17:07:03    | Ayrton          | Jack
...

--- MEMBER ITINERARIES (LOCAL TIME) ---
================================================================================

--- Itinerary for James ---
  1973-06-09 14:37 to 1973-06-09 16:16 -> Driving Stints #1-2 for 1 hour and 39 minutes
  1973-06-09 16:16 to 1973-06-09 18:40 -> Resting for 2 hours and 24 minutes
  1973-06-09 18:40 to 1973-06-09 20:18 -> Spotting Stints #5-6 for 1 hour and 38 minutes
...

🧾 CSV (.csv)

A simple CSV file containing the master race schedule, perfect for importing into other applications.

Stint,Start Time (UTC),End Time (UTC),Assigned Driver,Assigned Spotter,Laps
1,1973-06-09 14:37:00,1973-06-09 15:25:21,James,Nikki,13
2,1973-06-09 15:27:51,1973-06-09 16:16:12,James,Nikki,13
3,1973-06-09 16:18:42,1973-06-09 17:07:03,Ayrton,Jack,13
4,1973-06-09 17:09:33,1973-06-09 17:57:54,Ayrton,Jack,13

📊 Excel (.xlsx)

The most detailed output. This multi-sheet Excel workbook provides:

  • A Summaries sheet with stint and lap counts for all drivers and spotters.
  • A Master Schedule sheet with the full event schedule in UTC.
  • A separate sheet for each team member displaying a color-coded calendar of their duties (Driving, Spotting, Resting) in their specified local time. This is ideal for quick reference during the race.

Troubleshooting

If solver.py fails to find an optimal solution, it's typically for one of two reasons: the solver ran out of time, or the problem is impossible to solve with the given constraints.

Solver Timeout

By default, the solver is given 30 seconds to find a solution. For complex schedules with many drivers and constraints, this may not be enough time. The time required also depends heavily on the speed of your computer—a modern M2 MacBook will solve much faster than a Raspberry Pi.

  • Solution: Increase the timeout using the --time-limit argument.
    python solver.py test_data/24hr.json --time-limit 120

Overly Constrained Problem

If increasing the timeout doesn't help, it's likely that your parameters have created a problem with no possible solution. The more restrictive your constraints, the harder the problem is to solve.

Here are the factors most likely to make a solution difficult or impossible, from most to least impactful:

  1. minimumRestHours: This is a very strong constraint. Requesting long, mandatory rest periods for multiple team members severely limits scheduling flexibility.
  2. availability: Having too many team members marked as Unavailable for overlapping periods can make it impossible to cover all the stints.
  3. Small Team Size: The fewer people available to drive or spot, the fewer options the solver has.
  4. integrated Spotter Mode: Requiring a driver and a spotter for every single stint is much more restrictive than only scheduling drivers.
  5. Low preferredStints Values: If all team members can only do one stint at a time, it can conflict with mandatory rest periods.

How to Relax Constraints: If you suspect the problem is too constrained, try relaxing the parameters in this order:

  • First, try adding the --allow-no-spotter flag if you are using integrated or sequential spotter mode.
  • Reduce the minimumRestHours value for one or more team members.
  • Review the availability object. Can any Unavailable block be removed?
  • Increase the preferredStints value for one or more team members.

Finding a 'Good Enough' Solution (--optimality-gap)

Sometimes the solver uses the full time limit not because a solution is hard to find, but because there are many good solutions, and it's spending a lot of time making tiny, insignificant improvements to find the absolute "best" one.

The --optimality-gap argument lets you tell the solver to stop as soon as it finds a solution that is "good enough." The value is a float from 0.0 to 1.0, representing a percentage. For example, a value of 0.1 means the solver can stop once it finds a solution that is guaranteed to be within 10% of the theoretical best possible score.

  • When to use: This is extremely useful if the solver is hitting the time limit. It can dramatically cut down on runtime while still providing an excellent, well-balanced schedule.
  • Solution: Start by trying a gap of 10% or 20%.
    # Stop when a solution within 10% of optimal is found
    python solver.py test_data/24hr.json --optimality-gap 0.1
    
    # Stop when a solution within 20% of optimal is found
    python solver.py test_data/24hr.json --optimality-gap 0.2

How It Works

This tool leverages the power of mathematical optimization to solve the complex problem of race scheduling.

The core of solver.py is the pulp library, a popular open-source package for modeling and solving linear programming (LP) and mixed-integer programming (MIP) problems in Python.

Here's a high-level overview of the process:

  1. Model Formulation: The script reads the JSON input and translates the race parameters, team member roles, and constraints (like availability, rest times, and consecutive stint limits) into a mathematical model.
  2. Decision Variables: It creates binary decision variables for each possible assignment (e.g., "should Driver A take Stint 5?").
  3. Objective Function: The script defines a primary goal, which is to create the most "fair" schedule possible. It does this by minimizing the difference in the number of stints between team members and also minimizing the number of driver/spotter changes to reduce disruption.
  4. Solving: The pulp library takes this model and uses an underlying solver (like CBC, which is included with pulp) to find the optimal set of "yes" or "no" answers for all the decision variables that satisfies all constraints and best achieves the objective function.
  5. Output: The final, optimal assignments are formatted into the solved_schedule.json file, which is then used by reporter.py to generate the human-readable reports.

This project was created by popmonkey and Gemini 2.5 Pro

About

generate optimized schedules for endurance racing teams

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages