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
This is a two-part tool designed to generate and report on optimized schedules for endurance racing teams.
- Overview
- Stage 1:
solver.py
- The Schedule Optimizer - Stage 2:
reporter.py
- The Report Generator - Troubleshooting
- How It Works
The system consists of two main scripts:
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.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.
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.
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 |
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.
- This is the simplest mode. The solver will only schedule drivers and will completely ignore the
-
--spotter-mode sequential
:- This mode performs a two-step calculation:
- First, it solves the entire schedule for drivers only, creating the most optimal driver rotation based on their constraints and preferences.
- 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.
- This mode performs a two-step calculation:
-
--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
orintegrated
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.
- This flag can be used with either
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.
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.
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.
This is the most powerful control for managing your team's schedule in detail.
- Structure: The
availability
object contains keys that match thename
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"
.
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.
This script processes the JSON file produced by the solver to create detailed, human-readable reports.
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 |
The reporter can generate three different types of files.
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
...
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
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.
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.
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
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:
minimumRestHours
: This is a very strong constraint. Requesting long, mandatory rest periods for multiple team members severely limits scheduling flexibility.availability
: Having too many team members marked asUnavailable
for overlapping periods can make it impossible to cover all the stints.- Small Team Size: The fewer people available to drive or spot, the fewer options the solver has.
integrated
Spotter Mode: Requiring a driver and a spotter for every single stint is much more restrictive than only scheduling drivers.- 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 usingintegrated
orsequential
spotter mode. - Reduce the
minimumRestHours
value for one or more team members. - Review the
availability
object. Can anyUnavailable
block be removed? - Increase the
preferredStints
value for one or more team members.
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
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:
- 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.
- Decision Variables: It creates binary decision variables for each possible assignment (e.g., "should Driver A take Stint 5?").
- 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.
- Solving: The
pulp
library takes this model and uses an underlying solver (like CBC, which is included withpulp
) 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. - Output: The final, optimal assignments are formatted into the
solved_schedule.json
file, which is then used byreporter.py
to generate the human-readable reports.
This project was created by popmonkey and Gemini 2.5 Pro