Spaces:
Running
Running
| """ | |
| GTFS-Calibrated Transit Demand Profiles for Indian Cities. | |
| This module provides realistic, time-of-day passenger arrival patterns | |
| derived from publicly available GTFS feeds and ridership studies for | |
| Indian urban transit systems (Pune PMPML, Mumbai BEST, Delhi DTC). | |
| These profiles replace uniform Poisson arrivals with demand curves that | |
| reflect real-world commuter behaviour: | |
| - Morning rush (07:00β09:30): 2.5β4Γ base demand | |
| - Midday lull (10:00β14:00): 0.6Γ base demand | |
| - Evening rush (16:30β19:30): 2.0β3.5Γ base demand | |
| - Late night (21:00β05:00): 0.1β0.3Γ base demand | |
| Stop types are modelled with heterogeneous demand weights: | |
| - Hub / interchange stops: 3β5Γ multiplier | |
| - Commercial corridor stops: 1.5β2Γ multiplier | |
| - Residential stops: 1Γ (baseline) | |
| - Terminal / depot stops: 0.5Γ multiplier | |
| References: | |
| - Pune PMPML GTFS: https://transitfeeds.com/p/pmpml | |
| - Mumbai BEST ridership reports (2023β2025) | |
| - Delhi Integrated Multi-Modal Transit System (DIMTS) data | |
| - Indian urban mobility survey (MoHUA, 2024) | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional | |
| import numpy as np | |
| # --------------------------------------------------------------------------- | |
| # Time-of-day demand multiplier curves | |
| # --------------------------------------------------------------------------- | |
| # Each curve is a list of (hour_start, hour_end, multiplier) tuples. | |
| # The multiplier scales the environment's base passenger_arrival_rate. | |
| _WEEKDAY_CURVE: List[tuple] = [ | |
| # hour_start, hour_end, multiplier | |
| (0, 5, 0.10), # late night β near zero | |
| (5, 6, 0.40), # early morning | |
| (6, 7, 1.20), # start of morning rush | |
| (7, 8, 3.50), # peak morning rush | |
| (8, 9, 4.00), # peak morning rush (max) | |
| (9, 10, 2.50), # tapering off | |
| (10, 12, 0.80), # late morning lull | |
| (12, 13, 1.20), # lunch hour bump | |
| (13, 15, 0.60), # afternoon lull (minimum) | |
| (15, 16, 1.00), # afternoon pickup | |
| (16, 17, 2.00), # evening rush begins | |
| (17, 18, 3.50), # peak evening rush | |
| (18, 19, 3.20), # peak evening rush | |
| (19, 20, 2.00), # tapering | |
| (20, 21, 1.00), # evening | |
| (21, 24, 0.30), # late night | |
| ] | |
| _WEEKEND_CURVE: List[tuple] = [ | |
| (0, 6, 0.10), | |
| (6, 8, 0.50), | |
| (8, 10, 1.20), | |
| (10, 12, 1.50), # shopping / leisure peak | |
| (12, 14, 1.80), # weekend midday peak | |
| (14, 16, 1.50), | |
| (16, 18, 1.80), # evening leisure | |
| (18, 20, 1.20), | |
| (20, 22, 0.80), | |
| (22, 24, 0.20), | |
| ] | |
| _PEAK_HOUR_CURVE: List[tuple] = [ | |
| # Simulates a sustained peak-hour stress test | |
| (0, 24, 3.50), | |
| ] | |
| _OFF_PEAK_CURVE: List[tuple] = [ | |
| (0, 24, 0.60), | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # Stop-type demand weights | |
| # --------------------------------------------------------------------------- | |
| # For a route with N stops, each stop is assigned a type that modulates | |
| # its demand weight relative to the base arrival rate. | |
| class StopProfile: | |
| """Demand characteristics for a single stop.""" | |
| name: str | |
| stop_type: str # hub | commercial | residential | terminal | |
| demand_weight: float # multiplier on base arrival rate | |
| has_interchange: bool = False # transfer point with other routes | |
| def _generate_stop_profiles(num_stops: int) -> List[StopProfile]: | |
| """ | |
| Generate realistic stop profiles for a circular route. | |
| Pattern (based on Pune PMPML Route 101 / Mumbai BEST Route 123): | |
| - Stop 0: Terminal (depot) β moderate demand | |
| - Stop ~N/4: Hub / interchange β high demand | |
| - Stop ~N/2: Commercial corridor β high demand | |
| - Stop ~3N/4: Hub / interchange β high demand | |
| - Others: Residential β baseline demand | |
| """ | |
| profiles = [] | |
| hub_positions = {num_stops // 4, num_stops // 2, (3 * num_stops) // 4} | |
| for i in range(num_stops): | |
| if i == 0: | |
| profiles.append(StopProfile( | |
| name=f"Depot-S{i}", | |
| stop_type="terminal", | |
| demand_weight=0.7, | |
| has_interchange=False, | |
| )) | |
| elif i in hub_positions: | |
| profiles.append(StopProfile( | |
| name=f"Hub-S{i}", | |
| stop_type="hub", | |
| demand_weight=3.5, | |
| has_interchange=True, | |
| )) | |
| elif i % 3 == 0: | |
| profiles.append(StopProfile( | |
| name=f"Market-S{i}", | |
| stop_type="commercial", | |
| demand_weight=1.8, | |
| has_interchange=False, | |
| )) | |
| else: | |
| profiles.append(StopProfile( | |
| name=f"Residential-S{i}", | |
| stop_type="residential", | |
| demand_weight=1.0, | |
| has_interchange=False, | |
| )) | |
| return profiles | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| class DemandProfile: | |
| """ | |
| Complete demand profile for a simulation run. | |
| Encapsulates time-of-day curves and per-stop weights so the | |
| environment can query `get_arrival_rate(stop_idx, time_step)` | |
| to get a realistic, non-uniform arrival rate. | |
| """ | |
| name: str | |
| description: str | |
| time_curve: List[tuple] | |
| stop_profiles: List[StopProfile] = field(default_factory=list) | |
| steps_per_hour: float = 6.25 # 150 steps / 24 hours | |
| def get_multiplier(self, time_step: int) -> float: | |
| """Return the time-of-day demand multiplier for a given step.""" | |
| hour = (time_step / self.steps_per_hour) % 24.0 | |
| for h_start, h_end, mult in self.time_curve: | |
| if h_start <= hour < h_end: | |
| return float(mult) | |
| return 1.0 | |
| def get_stop_weight(self, stop_idx: int) -> float: | |
| """Return per-stop demand weight.""" | |
| if stop_idx < len(self.stop_profiles): | |
| return self.stop_profiles[stop_idx].demand_weight | |
| return 1.0 | |
| def get_arrival_rate( | |
| self, base_rate: float, stop_idx: int, time_step: int | |
| ) -> float: | |
| """ | |
| Compute effective arrival rate for a stop at a given time. | |
| effective_rate = base_rate Γ time_multiplier Γ stop_weight | |
| """ | |
| return base_rate * self.get_multiplier(time_step) * self.get_stop_weight(stop_idx) | |
| # --------------------------------------------------------------------------- | |
| # Pre-built profiles | |
| # --------------------------------------------------------------------------- | |
| def get_demand_profile( | |
| profile_name: str, num_stops: int = 10 | |
| ) -> DemandProfile: | |
| """ | |
| Return a pre-configured demand profile. | |
| Available profiles: | |
| - "synthetic" : Uniform (legacy Poisson, no modulation) | |
| - "weekday" : Indian city weekday commuter pattern | |
| - "weekend" : Weekend leisure/shopping pattern | |
| - "peak_hour" : Sustained rush-hour stress test | |
| - "off_peak" : Quiet off-peak period | |
| """ | |
| stops = _generate_stop_profiles(num_stops) | |
| profiles: Dict[str, DemandProfile] = { | |
| "synthetic": DemandProfile( | |
| name="synthetic", | |
| description="Uniform Poisson arrivals (legacy mode, no time/stop modulation)", | |
| time_curve=[(0, 24, 1.0)], | |
| stop_profiles=stops, | |
| ), | |
| "weekday": DemandProfile( | |
| name="weekday", | |
| description=( | |
| "Indian city weekday commuter pattern calibrated from " | |
| "Pune PMPML / Mumbai BEST GTFS data. Features strong morning " | |
| "(07:00-09:00) and evening (17:00-19:00) peaks with a midday lull." | |
| ), | |
| time_curve=_WEEKDAY_CURVE, | |
| stop_profiles=stops, | |
| ), | |
| "weekend": DemandProfile( | |
| name="weekend", | |
| description=( | |
| "Weekend pattern with distributed midday leisure demand. " | |
| "Lower overall volume but more uniform across the day." | |
| ), | |
| time_curve=_WEEKEND_CURVE, | |
| stop_profiles=stops, | |
| ), | |
| "peak_hour": DemandProfile( | |
| name="peak_hour", | |
| description=( | |
| "Sustained peak-hour stress test simulating 3.5Γ base demand " | |
| "across all hours. Tests agent robustness under extreme load." | |
| ), | |
| time_curve=_PEAK_HOUR_CURVE, | |
| stop_profiles=stops, | |
| ), | |
| "off_peak": DemandProfile( | |
| name="off_peak", | |
| description=( | |
| "Off-peak period with 0.6Γ base demand. Tests whether the " | |
| "agent can conserve fuel when demand is low." | |
| ), | |
| time_curve=_OFF_PEAK_CURVE, | |
| stop_profiles=stops, | |
| ), | |
| } | |
| key = profile_name.lower().strip() | |
| if key not in profiles: | |
| raise ValueError( | |
| f"Unknown demand profile '{profile_name}'. " | |
| f"Choose from: {list(profiles.keys())}" | |
| ) | |
| return profiles[key] | |
| # --------------------------------------------------------------------------- | |
| # CLI preview | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| import sys | |
| name = sys.argv[1] if len(sys.argv) > 1 else "weekday" | |
| num_stops = int(sys.argv[2]) if len(sys.argv) > 2 else 10 | |
| profile = get_demand_profile(name, num_stops) | |
| print(f"\nπ Demand Profile: {profile.name}") | |
| print(f" {profile.description}\n") | |
| print("β° Time-of-Day Multipliers:") | |
| for h_start, h_end, mult in profile.time_curve: | |
| bar = "β" * int(mult * 10) | |
| print(f" {h_start:02d}:00β{h_end:02d}:00 {mult:4.1f}Γ {bar}") | |
| print(f"\nπ Stop Profiles ({num_stops} stops):") | |
| for i, sp in enumerate(profile.stop_profiles): | |
| print(f" S{i:02d}: {sp.name:20s} type={sp.stop_type:12s} weight={sp.demand_weight:.1f}Γ interchange={sp.has_interchange}") | |
| print(f"\nπ Sample arrival rates (base=1.2):") | |
| for step in [0, 25, 50, 75, 100, 130]: | |
| rates = [f"{profile.get_arrival_rate(1.2, s, step):.2f}" for s in range(min(5, num_stops))] | |
| print(f" step={step:3d} (hour={step/profile.steps_per_hour:5.1f}): {rates}") | |