directional headway consistency - #58
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an overestimation bug in fleet sizing by carrying an n_directions signal through the optimization data flow and using it to convert aggregate headways into directional headways before computing required vehicles.
Changes:
- Add
n_directionsto extracted optimization route data and pass it through fleet calculations. - Update the shared fleet calculator to use directional headways derived from aggregate headways.
- Update tests/fixtures to include and forward
n_directions.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/transit_opt/preprocessing/prepare_gtfs.py |
Adds n_directions into extracted route essentials and optimization data; updates round-trip-time method to return direction count. |
src/transit_opt/optimisation/utils/fleet_calculations.py |
Updates fleet sizing formula to use directional_headway = aggregate_headway * n_directions. |
src/transit_opt/optimisation/problems/base.py |
Passes n_directions into shared fleet calculator when evaluating a solution. |
src/transit_opt/gtfs/solution_manager.py |
Passes n_directions into shared fleet calculator for solution reporting/export stats. |
tests/test_prepare_gtfs.py |
Updates edge-case fleet test data to include n_directions. |
tests/test_constraints.py |
Updates constraint tests’ mock optimization data to include n_directions. |
tests/fixtures/__init__.py |
Forwards n_directions into fixture fleet pre-calculation. |
notebooks/x_boundary_service_patterns.ipynb |
Updates notebook outputs/metadata related to boundary service patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Fixes fleet sizing during optimization by correctly converting optimizer “aggregate headways” into directional headways using a propagated n_directions value, aligning fleet calculations with GTFS export behavior.
Changes:
- Add
n_directionsextraction in GTFS preprocessing and propagate it through the optimization data structure. - Update the shared fleet calculator to convert aggregate headway → directional headway before computing vehicles needed.
- Update tests/fixtures and notebooks to reflect the new
n_directionsplumbing.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_prepare_gtfs.py | Adds n_directions to fleet edge-case test inputs (but doesn’t yet assert the new 2-direction conversion behavior). |
| tests/test_constraints.py | Updates mock optimization data to include n_directions so constraint tests keep working. |
| tests/fixtures/init.py | Passes n_directions into calculate_fleet_requirements for fixture-based expected fleet computations. |
| src/transit_opt/preprocessing/prepare_gtfs.py | Extracts n_directions per route, stores it in optimization data, and passes it into baseline fleet analysis. |
| src/transit_opt/optimisation/utils/fleet_calculations.py | Adds n_directions support and applies aggregate→directional headway conversion in fleet calculations. |
| src/transit_opt/optimisation/problems/base.py | Threads n_directions into fleet-from-solution calculation (but currently has a dict/array bug in the call). |
| src/transit_opt/gtfs/solution_manager.py | Threads n_directions into solution fleet reporting. |
| notebooks/x_boundary_service_patterns.ipynb | Updates notebook outputs/metadata after reruns. |
| notebooks/4_validate_gtfs_io.ipynb | Updates notebook outputs/metadata after reruns. |
Comments suppressed due to low confidence (2)
src/transit_opt/preprocessing/prepare_gtfs.py:605
_calculate_round_trip_timenow returns a(round_trip_time, n_directions)tuple, but the docstring “Returns” section still describes only a single float and mentions returningdefault_round_trip_timeon failure. Update the docstring to document both return values and their meaning/defaults so callers don’t misinterpret the API.
def _calculate_round_trip_time(self, route_id: str, route_trips: pd.DataFrame) -> tuple[float, int]:
"""
Calculate round-trip time with turnaround buffer for fleet sizing.
Estimates the total time a vehicle needs to complete a round trip,
including turnaround time at terminals. Used for fleet size calculations
in optimization constraints.
Args:
route_id: GTFS route_id identifier
service_trips: DataFrame of trips for this service
Returns:
Round-trip time in MINUTES including turnaround buffer.
Returns default_round_trip_time if calculation fails.
src/transit_opt/optimisation/utils/fleet_calculations.py:37
calculate_fleet_requirementsgained ann_directionsparameter and now converts aggregate headway to directional headway internally, but the function docstring doesn’t documentn_directionsin the Args section or explain the aggregate→directional conversion. This can cause callers to pass directional headways and inadvertently under-estimate fleet (double conversion). Please document expected headway semantics (aggregate vs directional) and the role ofn_directions.
def calculate_fleet_requirements(
headways_matrix: np.ndarray,
round_trip_times: np.ndarray,
operational_buffer: float = 1.15,
no_service_threshold: float = 480,
allowed_headways: np.ndarray | None = None,
no_service_index: int | None = None,
n_directions: np.ndarray | None = None,
) -> dict[str, Any]:
"""
Unified fleet calculation for both baseline analysis and optimization constraints.
This function implements the standardized fleet calculation logic used across
the transit optimization system, ensuring consistency between baseline analysis
(GTFSDataPreparator) and optimization constraints (BaseConstraintHandler).
Args:
headways_matrix: Matrix of headway values OR choice indices (n_routes × n_intervals)
round_trip_times: Round-trip times per route (n_routes,)
operational_buffer: Buffer factor for vehicle scheduling (default: 1.15)
no_service_threshold: Headways above this are no-service in minutes (default: 480)
allowed_headways: For decoding solution indices (optimization context only)
no_service_index: Index representing no-service (optimization context only)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
This PR fixes fleet sizing to account for aggregate (bi-directional) headways by propagating an n_directions value from GTFS extraction through to the shared fleet calculator, ensuring optimization-time fleet estimates match those derived from written GTFS outputs.
Changes:
- Add and propagate
n_directionsthrough optimization data extraction and fleet computation so aggregate headways are converted to directional headways during fleet sizing. - Update shared fleet calculation (
calculate_fleet_requirements) and its call sites (optimizer + solution export) to apply the new directional logic consistently. - Update tests/fixtures and validation notebooks to reflect the corrected fleet-sizing semantics.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_prepare_gtfs.py | Updates fleet-sizing edge case test data and printed formula to include n_directions. |
| tests/test_constraints.py | Adds n_directions to mock optimization data for constraint handler tests. |
| tests/fixtures/init.py | Passes n_directions through fixture-based fleet calculations; minor formatting cleanups. |
| src/transit_opt/preprocessing/prepare_gtfs.py | Extracts n_directions, returns it from round-trip calculation, and uses it in baseline fleet analysis. |
| src/transit_opt/optimisation/utils/fleet_calculations.py | Adds n_directions parameter and applies directional headway conversion in the unified fleet calculator. |
| src/transit_opt/optimisation/problems/base.py | Ensures fleet is computed from the PT matrix (not the full solution dict) and passes n_directions. |
| src/transit_opt/gtfs/solution_manager.py | Passes n_directions into fleet calculation for exported solution stats. |
| notebooks/x_boundary_service_patterns.ipynb | Updates notebook outputs consistent with updated round-trip/fleet logic. |
| notebooks/4_validate_gtfs_io.ipynb | Updates validation notebook execution/output metadata and displayed discrepancy messaging. |
Comments suppressed due to low confidence (1)
src/transit_opt/preprocessing/prepare_gtfs.py:605
_calculate_round_trip_timenow returns(round_trip_time, n_directions), but the docstring still documents only a single float return value. Please update theReturns:section (and any examples) to reflect the tuple and whatn_directionsrepresents (e.g., 1=loop, 2=bi-directional).
def _calculate_round_trip_time(self, route_id: str, route_trips: pd.DataFrame) -> tuple[float, int]:
"""
Calculate round-trip time with turnaround buffer for fleet sizing.
Estimates the total time a vehicle needs to complete a round trip,
including turnaround time at terminals. Used for fleet size calculations
in optimization constraints.
Args:
route_id: GTFS route_id identifier
service_trips: DataFrame of trips for this service
Returns:
Round-trip time in MINUTES including turnaround buffer.
Returns default_round_trip_time if calculation fails.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was an error in how the pipeline was calculating the required number of buses (fleet size) during optimization.
The optimizer operates using aggregate frequencies for routes. For example, an aggregate headway of 15 minutes on a standard two-way route means a bus starts its journey somewhere on the route every 15 minutes (in either direction). Physically, this translates to:
The central fleet calculator was ignoring the number of directions. It took the aggregate headway and applied it mathematically as if it were the directional headway, assuming the route needed to have buses in both directions every 15 minutes. When writing gtfs, we converted aggregate headway to directional headway (see #57 ). Calculating fleet size from optimisation data structure led to around double the fleet size compared to calculation based on output gtfs
Example
Imagine a two-way route takes 120 minutes to complete a full round trip (60 minutes each way), and the optimizer tests a 15-minute aggregate headway.
Reality (Expected Cost):
Fleet = Round Trip Time / Directional Headway120 min / 30 min = 4 buses total.The Bug (Actual Calculation):
120 min / 15 min = 8 buses total.The optimizer was effectively charging "double the price" for every standard two-way route.
Fix
We introduced an
n_directionsparameter that travels from the initial data extraction directly into the central fleet calculator. The optimizer now dynamically converts the aggregate headway into a directional headway before performing the calculationResult
the fleet requirements (as calculated in solution_manager.py based on the python data structure should now be the same as (or similar to) the fleet requirements calculated from the output gtfs file