Skip to content

directional headway consistency - #58

Merged
Hussein-Mahfouz merged 3 commits into
mainfrom
fix_directional_headway
Mar 19, 2026
Merged

directional headway consistency#58
Hussein-Mahfouz merged 3 commits into
mainfrom
fix_directional_headway

Conversation

@Hussein-Mahfouz

@Hussein-Mahfouz Hussein-Mahfouz commented Mar 18, 2026

Copy link
Copy Markdown
Owner

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:

  • A Northbound bus departing every 30 minutes.
  • A Southbound bus departing every 30 minutes.

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):

    • 15 min aggregate = 30 min directional wait time.
    • Formula: Fleet = Round Trip Time / Directional Headway
    • 120 min / 30 min = 4 buses total.
    • (Those 4 buses, spaced 30 minutes apart along the 120-minute loop, cover the schedule).
  • The Bug (Actual Calculation):

    • The code accidentally skipped the directional conversion and plugged the aggregate target straight into the denominator:
    • 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_directions parameter 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 calculation

Result

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

@Hussein-Mahfouz
Hussein-Mahfouz requested a review from Copilot March 19, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_directions to 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.

Comment thread src/transit_opt/preprocessing/prepare_gtfs.py
Comment thread src/transit_opt/preprocessing/prepare_gtfs.py
Comment thread src/transit_opt/optimisation/utils/fleet_calculations.py Outdated
Comment thread src/transit_opt/optimisation/utils/fleet_calculations.py
Comment thread tests/test_prepare_gtfs.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_directions extraction 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_directions plumbing.

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_time now returns a (round_trip_time, n_directions) tuple, but the docstring “Returns” section still describes only a single float and mentions returning default_round_trip_time on 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_requirements gained an n_directions parameter and now converts aggregate headway to directional headway internally, but the function docstring doesn’t document n_directions in 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 of n_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.

Comment thread src/transit_opt/preprocessing/prepare_gtfs.py
Comment thread src/transit_opt/optimisation/problems/base.py
Comment thread tests/test_prepare_gtfs.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_directions through 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_time now returns (round_trip_time, n_directions), but the docstring still documents only a single float return value. Please update the Returns: section (and any examples) to reflect the tuple and what n_directions represents (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.

Comment thread src/transit_opt/preprocessing/prepare_gtfs.py
Comment thread src/transit_opt/optimisation/utils/fleet_calculations.py
Comment thread tests/test_constraints.py
Comment thread tests/test_constraints.py
@Hussein-Mahfouz
Hussein-Mahfouz merged commit ed5aace into main Mar 19, 2026
7 checks passed
@Hussein-Mahfouz
Hussein-Mahfouz deleted the fix_directional_headway branch March 19, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants