Notas del Módulo: impedance.py#

¿Qué hace este archivo en resumen? Es el “tasador de tiempo” de la red. Una vez que graph_builder.py construyó el grafo con sus nodos y aristas, este módulo recorre cada segmento y le inyecta un peso temporal en minutos basado en tres factores reales: la distancia física del tramo, la velocidad comercial del sistema, y el nivel de interferencia del tráfico urbano sobre esa vía. Es la implementación directa de la Ecuación VFT.


## 1. `FrictionCalculator` — El Coeficiente de Fricción Vial (Cf)

Este módulo auxiliar traduce el tipo de derecho de vía de una línea en un penalizador matemático. La idea central: no es lo mismo correr por un túnel del Metro (sin semáforos, sin tráfico) que circular en un camión por Insurgentes entre autos particulares.

La Constante de Saturación de la CDMX#

BETA_SATURACION_CDMX = 0.759

Este valor viene del TomTom Traffic Index para Ciudad de México, que mide el nivel promedio de congestión vial. Un beta de 0.759 significa que la ciudad opera al ~76% de saturación en condiciones promedio.

De Derecho de Vía a Coeficiente#

El método get_friction() usa un alpha que escala el impacto de la saturación:

alpha_map = {
    "exclusivo": 0.0,   # Metro, Suburbano: aislado del tráfico
    "confinado": 0.2,   # Metrobús, Trolé elevado: carril propio pero visible
    "compartido": 0.5,  # Trolebús convencional: comparte vía pero tiene prioridad
    "mixto": 1.0        # RTP, CC: completamente inmerso en el tráfico urbano
}
cf = 1.0 + (alpha * BETA_SATURACION_CDMX)

Resultado numérico por tipo de vía:

Derecho de Vía

Alpha

Cf calculado

Significado

exclusivo

0.0

1.000

Sin penalización (flujo ideal)

confinado

0.2

1.152

+15% de tiempo por tráfico

compartido

0.5

1.380

+38% de tiempo por tráfico

mixto

1.0

1.759

+76% de tiempo por tráfico

El valor por defecto si no se conoce el derecho de vía es "mixto" (el peor caso).


## 2. `VFTImpedanceModel` — El Motor de Impedancia

2.1 Tablas de Fallback#

Si un segmento del GeoJSON no reporta velocidad o frecuencia, el modelo usa valores calibrados por sistema. Estas son velocidades comerciales ajustadas (no de diseño):

FALLBACK_VELOCIDAD = {
    "SUB": 65.0,        # Tren Suburbano
    "METRO": 36.0,      # STC Metro
    "MB": 16.3,         # Metrobús
    "TL": 22.0,         # Tren Ligero
    "TROLE": 18.0,      # Trolebús
    "RTP": 14.0,        # Red de Transporte de Pasajeros
    "CC": 11.0,         # Corredores Concesionados
    "CBB": 20.0,        # Cablebús
    "MEXICABLE": 20.0,
    "MEXIBÚS": 16.3,
    "PUMABUS": 11.0
    # "INTERURBANO": 160.0  ← ⚠️ Revisar: parece velocidad de diseño, no comercial
}
FALLBACK_FRECUENCIA = {
    "METRO": 3.0,   # minutos entre trenes
    "MB": 5.0,
    "CBB": 10.0,    # ⚠️ Difiere de graph_builder.py donde CBB=1.0
    "CC": 15.0,
    "RTP": 15.0,
    ...
}

Nota de inconsistencia: CBB tiene frecuencia 10.0 min aquí pero 1.0 min en graph_builder.py. Requiere definición canónica.


### 2.2 `haversine()` — La Distancia Real del Segmento
@staticmethod
def haversine(lon1, lat1, lon2, lat2) -> float:
    R = 6371000  # Radio de la Tierra en metros
    phi_1 = math.radians(lat1)
    phi_2 = math.radians(lat2)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)
    a = math.sin(delta_phi/2)**2 + math.cos(phi_1)*math.cos(phi_2)*math.sin(delta_lambda/2)**2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
    return R * c

¿Por qué Haversine y no distancia euclidiana? Porque las coordenadas son geográficas (lon/lat en grados). Una resta directa de coordenadas daría distancias en “grados”, no en metros, y además ignoraría la curvatura de la Tierra. Haversine traza la línea recta más corta sobre la superficie esférica terrestre (ortodrómica), dando metros reales con precisión suficiente para distancias urbanas (error < 0.3% dentro de la CDMX).

Firma importante: recibe (lon, lat, lon, lat) — primero longitud, luego latitud. Los nodos del grafo están almacenados como (lon, lat), así que la llamada siempre es haversine(u[0], u[1], v[0], v[1]).


### 2.3 `apply_impedance()` — La Inyección sobre el Grafo

Este método itera sobre todas las aristas del grafo DiGraph y aplica la Ecuación VFT en 6 pasos:

for u, v, data in self.G.edges(data=True):

    # PASO 0: Proteger transbordos peatonales (ya calculados por graph_builder)
    if data.get("tipo") == "transfer":
        continue

    # PASO 1: Distancia geodésica del segmento
    distancia_m = haversine(u[0], u[1], v[0], v[1])

    # PASO 2: Velocidad comercial (del GeoJSON o fallback por sistema)
    v_kmh = data.get("velocidad_promedio_kmh") or FALLBACK_VELOCIDAD.get(sistema, 15.0)

    # PASO 3: Conversión a m/min para compatibilidad de unidades
    v_m_min = v_kmh * (1000.0 / 60.0)

    # PASO 4: Coeficiente de fricción según derecho de vía
    cf = FrictionCalculator.get_friction(data.get("derecho_de_via", "mixto"))

    # PASO 5: Tiempo de traslado puro (la Ecuación VFT)
    travel_time_min = (distancia_m / v_m_min) * cf

    # PASO 6: Boarding cost (frecuencia / 2, solo para análisis; no suma al weight)
    boarding_cost_min = frecuencia / 2.0

Atributos que inyecta en cada arista:

Atributo

Unidad

Descripción

distancia_segmento_m

metros

Distancia Haversine del segmento

travel_time_min

minutos

Tiempo de traslado con fricción

boarding_cost_min

minutos

Costo de abordaje (F/2) — informativo

coeficiente_friccion

adimensional

Cf aplicado

weight

minutos

= travel_time_min (lo que usan los algoritmos de rutas)

Decisión de diseño clave: el weight solo incluye travel_time_min, no boarding_cost_min. El boarding cost se registra como atributo informativo pero no penaliza el camino en los segmentos normales. La penalización por espera ocurre únicamente en las aristas de transbordo peatonal, donde sí se suma al weight (tiempo_caminata + wait).


## 3. El Flujo Completo en Contexto
GeoJSON  →  VFTGraphBuilder  →  [Grafo base + transbordos peatonales]
                                           ↓
                              VFTImpedanceModel.apply_impedance()
                                           ↓
              Cada arista normal recibe:  weight = (D / V) × Cf
              Cada arista transfer tiene: weight = t_caminata + (F_destino / 2)
                                           ↓
                              Grafo listo para Dijkstra / análisis topológico

## 4. Issues Abiertos sobre este Módulo
  1. Velocidad INTERURBANO (160 km/h): El fallback de 160 km/h corresponde a velocidad de diseño del tren, no comercial. La velocidad operativa real es ~70 km/h. Requiere corrección.

  2. Inconsistencia CBB: FALLBACK_FRECUENCIA["CBB"] = 10.0 aquí vs 1.0 en graph_builder.py. Definir el valor canónico.

  3. Boarding cost no suma al weight: Esta es una decisión de diseño consciente. Verificar si el análisis de rutas debe o no incluirlo para los segmentos normales (actualmente solo aplica en transbordos).

  4. Fuente de los coeficientes alpha: Pendiente citar la tabla exacta del Manual de Calles SEDATU 2019 que fundamenta los valores alpha = {0.0, 0.2, 0.5, 1.0}.