v1.0 · mayo 2026 354 nodos · GDL
tanGo

Sistema de coordinación inteligente de semáforos · ZMG · Guadalajara, México

354
Nodos del grafo
672
Aristas (segmentos viales)
550
Episodios de entrenamiento PPO
15×
Menos vehículos detenidos vs baseline

tanGo es un sistema académico de coordinación de semáforos inteligentes que combina un algoritmo inspirado en SCOOT (Split Cycle Offset Optimisation Technique) con Reinforcement Learning (PPO) para minimizar el tiempo de detención en intersecciones del centro de Guadalajara.

El sistema opera sobre un grafo real extraído de OpenStreetMap con 354 intersecciones y 672 segmentos viales del área metropolitana. Cada hora, un DAG de Airflow consulta datos reales de clima (Open-Meteo) y velocidades de tráfico (TomTom), corre 15 ticks de simulación, y exporta el estado al dashboard.

Resultado principal

El agente PPO (sim3) logra 24.18 vehículos detenidos por tick frente a 367 del baseline de timers fijos (sim0) — una reducción del 93%. Además es el único sistema que completa rutas: 1,036 llegadas en 60 ticks vs 0 de sim0 y sim1.

Adaptabilidad

Aunque tanGo fue desarrollado y calibrado para Guadalajara, el sistema es en teoría adaptable a cualquier ciudad del mundo. El grafo vial se genera automáticamente desde OpenStreetMap — cualquier ciudad con datos OSM puede ser el nuevo objetivo. Solo se necesita cambiar las coordenadas en city_config.json y correr python graph/city_loader.py. El algoritmo, el pipeline de Airflow y el agente PPO son independientes de la geografía.

Cómo navegar esta documentación

Usa la barra lateral para explorar secciones. El menú El algoritmo explica el motor central paso a paso. Simulaciones incluye visualizaciones interactivas de cada versión. Infraestructura cubre el stack de producción.

Licencia — GNU AGPL v3.0

El código fuente de tanGo es libre bajo los términos de la GNU Affero General Public License v3.0. Puedes usarlo, modificarlo y distribuirlo siempre que cualquier versión derivada — incluyendo versiones ofrecidas como servicio de red — también publique su código fuente bajo la misma licencia. Ver licencia completa →

Arquitectura del sistema

Flujo de datos desde Overpass/TomTom hasta el dashboard

Arquitectura tanGo Overpass API Open-Meteo TomTom API Airflow DAG verificar_grafo enriquecer_contexto correr_simulacion × 15 exportar_estado @hourly core/ TrafficAlgorithm WeightEngine MovementEngine tango_state.json intersections · metrics · context · n_ticks_run · n_blind city_graph.json 354 nodos · 672 aristas FastAPI :8000 /intersections · /metrics · /pressure-map · /status tanGo_dashboard.html consumidor principal · modo claro/oscuro Leaflet · fetch cada 60s · :8000 PRINCIPAL Streamlit dashboard compañero :8501 run_tick()

FIG 1 — Arquitectura completa. tanGo_dashboard.html es el consumidor principal — centraliza todas las fuentes del DAG vía FastAPI. Streamlit es secundario.

Principio de diseño

El sistema sigue una arquitectura de un escritor, múltiples lectores. El DAG de Airflow es el único proceso que escribe tango_state.json. FastAPI lo sirve como API REST, y tanGo_dashboard.html es el consumidor principal — centraliza todas las fuentes: intersecciones, métricas, contexto de clima, factor de tráfico TomTom y estado de la simulación. Hace fetch a la API cada 60 segundos de forma autónoma, sin recargar la página.

Streamlit (app.py) es el dashboard del compañero — consume la misma API pero es secundario. Ambos coexisten sin conflicto porque solo leen.

Separación de entornos

Ray/RLlib (sim3) tiene conflictos de dependencias con Airflow. Por eso existen dos entornos virtuales separados: ~/tango_env para entrenamiento RL y ~/airflow_env para el pipeline de producción. Docker los aísla en contenedores distintos.

Stack tecnológico

Python 3.11 · WSL2 Debian · i3-11va · 20GB RAM

Tecnologías principales

ComponenteTecnologíaVersiónFunción
Algoritmonetworkx3.3Grafo de la ciudad, betweenness, PageRank, Dijkstra
RLray[rllib]2.55Entrenamiento PPO, gestión de workers
RL frameworkgymnasium0.29Entorno TanGoEnv
RL modelotorch2.1+Red neuronal FCNet 256-256-128
Pipelineapache-airflow2.9DAG horario, orquestación de tareas
APIfastapi + uvicorn0.111 / 0.30Endpoints REST para dashboard
Dashboardstreamlit1.35Dashboard del compañero
VisualizaciónLeaflet.js1.9.4Mapas interactivos (HTML puro)
Datos externosOverpass, Open-Meteo, TomTomGrafo OSM, clima, velocidades
Contenedoresdocker + compose29.2Despliegue de 3 servicios

Estructura del proyecto

36.TanGo/
├── core/              # Motor del algoritmo — independiente de UI
│   ├── algorithm.py   # TrafficAlgorithm (3 pasos)
│   ├── movement.py    # MovementEngine — Dijkstra + velocidades
│   ├── road.py        # Intersection, Phase, RoadSegment
│   ├── entities.py    # Vehicle, Pedestrian, VehicleType
│   ├── context.py     # TrafficContext (hora, clima, tráfico)
│   └── weight_engine.py  # Presión, ola verde, pesos
├── graph/
│   ├── city_graph.json   # 354 nodos, 672 aristas — GDL centro
│   ├── tango_state.json  # Estado horario generado por DAG
│   └── city_loader.py    # Overpass → JSON + pesos estáticos
├── dags/
│   └── tango_queries_dag.py  # Pipeline @hourly
├── dashboard/
│   ├── api.py        # FastAPI :8000
│   ├── app.py        # Streamlit :8501
│   └── tanGo_dashboard.html
├── tests/
│   ├── sim0/ sim1/ sim2/  # Evolución del simulador
│   └── sim3/
│       ├── tango_env.py   # TanGoEnv (gymnasium)
│       ├── train.py
│       ├── evaluate.py
│       └── tango_sim3.py  # Visualización PPO
└── docker/
    ├── Dockerfile.api / .airflow / .dashboard
    └── docker-compose.yml

El algoritmo

Tres pasos secuenciales por tick · Inspirado en SCOOT + coordinación vecinal

El corazón de tanGo es TrafficAlgorithm.run_tick(). Con cada llamada, el sistema ejecuta tres pasos secuenciales que transforman el estado de las entidades en la red en decisiones de fase para cada semáforo.

Los tres pasos del algoritmo tanGo entities_by_node + TrafficContext PASO 1 Presión propia WeightEngine PASO 2 Colmena + green wave propagate_signals PASO 3 Fases + clusters adjust_phase() input p_own p_combined 1 tick ≈ 30 segundos simulados

FIG 2 — Los tres pasos de run_tick(). La presión fluye de izquierda a derecha; las decisiones de fase son el output final.

1

Presión propia — _compute_own_pressures()

Para cada intersección, WeightEngine.aggregate_pressure() calcula una presión escalar a partir de las entidades presentes: tipo de vehículo, cantidad, presencia de emergencias y contexto (hora, lluvia, temperatura). Es una función pura — mismo input, mismo output.

2

Mente colmena — _propagate_neighbor_signals()

Cada nodo recibe señales de sus vecinos upstream. Si un vecino está en verde, se calcula el offset temporal (distancia / velocidad) para saber cuándo llegará su flujo. Si el offset se ha cumplido, el nodo actual se marca _wave_forced=True — la ola verde llegó.

3

Ajuste de fases — _adjust_phases()

Aplica la máquina de estados (RED → GREEN → YELLOW → RED) con exclusión mutua NS/EW, timeout de equidad, modo BLINK para intersecciones vacías, y coordinación de cluster: el nodo con mayor presión en el cluster gana el verde; los demás ceden.

Paso 1 — Presión propia

WeightEngine.aggregate_pressure() · Función pura

La presión de una intersección es una medida escalar de cuánta urgencia existe para dar el verde. Se calcula únicamente a partir de las entidades presentes — sin considerar vecinos ni historial.

Factores de presión

FactorPeso baseDescripción
Vehículo normal (CAR, TRUCK…)1.0Cada vehículo suma 1 unidad de presión
EMERGENCY10.0Ambulancias y patrullas tienen prioridad absoluta
BUS2.5Mayor capacidad = mayor impacto en flujo
Hora pico (7–9h, 17–20h)×1.4Multiplicador por contexto temporal
Lluvia×1.2Velocidades reducidas aumentan congestion
Temperatura extrema×1.1Afecta comportamiento vial
node_weight×[0.5–1.5]Intersecciones más importantes amplifican presión

Propiedad clave: statelesness

Diseño funcional

aggregate_pressure() no tiene efectos secundarios ni lee estado global. Para el mismo conjunto de entidades y contexto, siempre devuelve el mismo valor. Esto hace el algoritmo reproducible, testeable y fácil de calibrar.

# Ejemplo simplificado de cálculo de presión
def aggregate_pressure(entities, inter, ctx):
    base = 0.0
    for e in entities:
        w = VEHICLE_WEIGHTS.get(e.vehicle_type, 1.0)
        base += w
    # Modificadores de contexto
    if ctx.is_rush_hour:  base *= 1.4
    if ctx.is_raining:    base *= 1.2
    # Amplificar por importancia del nodo
    return base * inter.node_weight

Paso 2 — Mente colmena

Coordinación vecinal + green wave anticipatoria

El paso más sofisticado del algoritmo. Cada nodo no solo reacciona a su propio tráfico — también escucha a sus vecinos upstream para anticipar el flujo que se aproxima.

Componentes

a) Señal vecinal de presión

La influencia de un vecino decae con la distancia. Un nodo a 50m tiene más influencia que uno a 500m. Los nodos tipo MASTER amplifican su señal ×1.3.

influence = neighbor_pressure / (1 + distance_m / speed_kmh)
combined  += influence * 0.25  # NEIGHBOR_WEIGHT

b) Boost anticipatorio

Si un vecino upstream está en verde pero el flujo aún no llega (offset pendiente), se aplica un boost proporcional a la urgencia — cuanto más cerca está el flujo de llegar, mayor el boost.

urgency    = 1.0 / (1.0 + ticks_remaining * TICK_DURATION_S / 20.0)
wave_boost = neighbor_pressure * urgency
combined  += wave_boost * 0.15  # WAVE_BOOST_WEIGHT

c) Ola verde forzada (offset cumplido)

Cuando los ticks transcurridos desde que el vecino se puso en verde superan el offset_ticks (distancia / velocidad / TICK_DURATION_S), el flujo ya debería haber llegado. El nodo se marca _wave_forced=True y en el Paso 3 se fuerza a verde independientemente de la presión local.

Por qué esto es similar a SCOOT

SCOOT (Robertson & Bretherton, 1991) coordina semáforos calculando offsets temporales entre intersecciones para crear "olas verdes" en avenidas principales. tanGo implementa el mismo principio pero de forma distribuida: cada nodo calcula su propio offset a partir de sus vecinos upstream, sin un controlador central.

Parámetros ajustables (knobs RL)

ConstanteValorEfecto
NEIGHBOR_WEIGHT0.25Influencia de la presión del vecino
WAVE_BOOST_WEIGHT0.15Boost anticipatorio de ola verde
WAVE_URGENCY_S20.0sReferencia de urgencia (decaimiento)
MASTER_AMPLIFIER1.3×Amplificación de señal para nodos MASTER

Paso 3 — Ajuste de fases

Máquina de estados con coordinación de cluster

Máquina de estados

Máquina de estados de semáforo RED presión < umbral GREEN presión ≥ umbral YELLOW transición (1 tick) BLINK sin tráfico p ≥ umbral timeout fin de amarillo

FIG 3 — Máquina de estados del semáforo. BLINK es un estado especial para intersecciones sin tráfico.

Coordinación de cluster

Cuando dos o más intersecciones están a menos de 60m entre sí (p.ej. un cruce con camellón), forman un cluster de coordinación. Dentro del cluster, solo puede estar en verde una intersección a la vez. El ganador es el de mayor presión combinada.

# Ceder al ganador del cluster
winner_id = max(cluster_members, key=lambda n: pressures[n])
for node_id in cluster_members:
    if node_id != winner_id:
        pressures[node_id] *= 0.3  # CLUSTER_YIELD

Timeout de equidad

Ninguna intersección puede permanecer en rojo indefinidamente. Si un nodo lleva más de red_timeout_ticks en rojo y tiene entidades esperando, su presión se eleva artificialmente para garantizar que eventualmente reciba el verde.

Ola verde SCOOT

Coordinación temporal entre intersecciones · offset = distancia / velocidad

La ola verde es el mecanismo por el que los semáforos se sincronizan para que un vehículo que sale de una intersección en verde llegue a la siguiente cuando esta también esté en verde, sin necesidad de detenerse.

Cálculo del offset

# WeightEngine.compute_green_wave_offset()
offset_s = distance_m / (speed_kmh / 3.6)
# Convertir a ticks (1 tick ≈ 30s simulados)
offset_ticks = max(1, round(offset_s / TICK_DURATION_S))

Ejemplo en Guadalajara

Avenida Federalismo entre dos intersecciones separadas 350m, límite de velocidad 50km/h:

offset_s     = 350 / (50 / 3.6) = 25.2 segundos
offset_ticks = round(25.2 / 30) = 1 tick

Si la intersección upstream se pone en verde en el tick 5, la downstream debe ponerse en verde en el tick 6 — el flujo ya llegó.

Diferencia con SCOOT clásico

SCOOT requiere un controlador central con sensores de lazo inductivo en cada segmento. tanGo aproxima el mismo comportamiento de forma distribuida: cada nodo calcula su propio offset a partir de los datos del grafo OSM (longitud y velocidad de cada segmento), sin infraestructura adicional.

Entidades y contexto

Los actores de la simulación

Tipos de entidades

TipoPeso presiónSpawn (MASTER / NORMAL / BLIND)
CAR1.060% del pool
MOTORCYCLE0.615% del pool
BUS2.510% del pool
TRUCK2.08% del pool
BICYCLE0.35% (2% con lluvia)
EMERGENCY10.02% del pool
PEDESTRIAN0.515% de prob. por tick

TrafficContext

El contexto encapsula las condiciones del mundo real para un tick específico. En producción, se construye con datos reales de Open-Meteo y TomTom.

CampoFuenteEfecto en el algoritmo
timestampSistemaDetecta hora pico (7–9h, 17–20h) → ×1.4
temperature_cOpen-MeteoExtremos → ×1.1 en spawn rate
is_rainingOpen-Meteo×1.2 presión, menos bicis
wind_speed_kmhOpen-MeteoAjuste menor de velocidades
visibility_mOpen-MeteoReducción de velocidades
traffic_factorTomTomMultiplica spawn rate por congestión real

Pesos estáticos

5 dimensiones combinadas en media geométrica · calculados una vez por día

El node_weight de cada intersección cuantifica su importancia estructural en la red vial. Un nodo con peso alto amplifica su presión propia y la señal que envía a sus vecinos.

Las 5 dimensiones

DimensiónCálculoRango
degree_weight1 + (street_count − 1) × 0.15[1.0, 2.5]
betweennessCentralidad de intermediación (NetworkX)[0.5, 1.5]
pagerankPageRank α=0.85 (NetworkX)[0.5, 1.5]
road_qualityPromedio de calidad de vías entrantes[0.1, 1.0]
itype_bonusMASTER=1.3, NORMAL=1.0, BLIND=0.5[0.5, 1.3]
# Media geométrica de las 5 dimensiones (α=β=γ=δ=ε=1)
node_weight = (degree_w * betweenness_w * pagerank_w
               * road_quality * itype_bonus) ** (1/5)
Trabajo futuro — calibración RL

Los exponentes de la media geométrica son actualmente todos 1 (igual peso a cada dimensión). La siguiente fase del proyecto consiste en usar el agente PPO para aprender los exponentes óptimos por hora del día, convirtiendo estos pesos estáticos en dinámicos.

Evolución sim0 → sim3

Cuatro generaciones del simulador, cada una más sofisticada

sim0
Timers fijos
sim1
SCOOT greedy
sim2
Pesos + Dijkstra
sim3
PPO (RL)
VersiónNovedad principalDetenidos/tickLlegadas
sim0Baseline: todos en verde fijo3670
sim1TrafficAlgorithm + presión + ola verde4910
sim2node_weight + MovementEngine + Dijkstravariable
sim3PPO: el agente decide las fases24.181,036
Nota sobre sim1

sim1 muestra más detenidos que sim0 porque el algoritmo de presión crea cuellos de botella mientras aprende a coordinarse. A largo plazo (más ticks) sim1 supera a sim0, pero en la ventana de 60 ticks la comparativa es desfavorable. sim3 resuelve esto con RL.

sim0 — Timers fijos

Baseline · Todos los semáforos en verde permanente

El baseline más simple posible: todas las intersecciones con semáforo se fijan en verde. No hay lógica de coordinación ni reacción al tráfico. Sirve como referencia inferior.

tanGo sim0 — Timers fijos BASELINE
Baseline: semáforos en verde fijo. 367 vehículos detenidos/tick promedio.  ·  ↗ ver en línea

sim1 — SCOOT greedy

TrafficAlgorithm completo · presión + ola verde

Primera versión del algoritmo completo. Introduce presión por entidades, coordinación vecinal y ola verde. Las entidades son sintéticas (sin rutas reales).

tanGo sim1 — SCOOT greedy ALGORITMO
TrafficAlgorithm con presión y ola verde. Escenarios: hora pico, lluvia, fin de semana.

sim2 — Pesos + movimiento

node_weight · MovementEngine · Dijkstra · heatmap

Agrega movimiento real de entidades con rutas calculadas por Dijkstra, pesos estáticos por nodo (betweenness, pagerank, road_quality) y partículas animadas en el mapa.

tanGo sim2 — Pesos estáticos + Movimiento MOVIMIENTO
Partículas animadas con rutas Dijkstra, heatmap de flujo acumulado y pesos estáticos por nodo.

Casos experimentales

Caso 246EXP
No disponible — ver localmente
Caso 369EXP
No disponible — ver localmente
Caso 48CEXP
No disponible — ver localmente
Caso 5AFEXP
No disponible — ver localmente

sim3 — Agente PPO

Reinforcement Learning · PPO · 550 episodios · checkpoint_00550

El agente PPO observa 10 features por cada semáforo (presión, fase, entidades, hora, vecinos…) y decide en cada tick si mantener o cambiar la fase de cada semáforo. Entrenado durante ~5 días en CPU.

Espacio de observación (10 features × N_semáforos)

#FeatureRango
0Presión normalizada[0,1]
1Fase codificada (RED=0, YELLOW=0.5, GREEN=1, BLINK=0.25)[0,1]
2Ticks en fase actual normalizado[0,1]
3Entidades en nodo normalizado[0,1]
4Hay emergencia (binario){0,1}
5node_weight[0.5,1.5]
6Sin(hora/24×2π) normalizado[0,1]
7Cos(hora/24×2π) normalizado[0,1]
8Presión promedio vecinos upstream[0,1]
9wave_offset normalizado[0,1]

Función de recompensa

reward = w_flow   × (entidades_en_verde / total)
       - w_wait   × (espera_acumulada / (total × n_ticks))
       - w_emerg  × emergencias_detenidas
       + w_arrived × (llegadas_este_tick / total)
# Pesos: w_flow=1.0, w_wait=0.5, w_emerg=5.0, w_arrived=2.0
tanGo sim3 — Agente PPO PPO · RL
El agente PPO controla los semáforos en tiempo real. Los pulsos cyan indican semáforos que el agente cambió ese tick. La curva de reward se actualiza tick a tick.  ·  ↗ ver en línea

Resultados comparativos

60 ticks · escenario rush_hour

367
sim0 detenidos/tick
491
sim1 detenidos/tick
24.18
sim3 detenidos/tick
−93%
reducción vs sim0
Métricasim0sim1sim3 (PPO)
Detenidos promedio/tick367.0491.5224.18
% ticks en verde (acum.)12,6006,4383,750
Vehículos llegados001,036
Emergencias detenidas000
Reward sum00−186.07
Ticks evaluados606060
Interpretación del % verde

sim3 tiene menos verde acumulado que sim0. Esto es coherente: el agente aprendió que dar verde sin coordinación no ayuda. En vez de poner todo en verde, coordina cuándo dar el verde — resultado: 15× menos vehículos detenidos.

Por qué sim0 y sim1 tienen llegadas = 0

sim0 y sim1 usan spawn sintético sin rutas reales — las entidades aparecen y desaparecen cada tick sin completar trayectos. sim3 usa MovementEngine con Dijkstra y lifetime real, por eso es el único que registra llegadas al destino.

DAG de Airflow

Pipeline horario · tango_traffic_pipeline · @hourly

DAG de Airflow tanGo inicio verificar _grafo refrescar si >24h enriquecer TomTom+Meteo correr ×15 ticks exportar state.json @hourly · SequentialExecutor · Puerto 8082

FIG 4 — Pipeline DAG horario. calcular_pesos se separó a un DAG diario (no cambia cada hora).

Comandos de operación

# Levantar Airflow
source ~/airflow_env/bin/activate
export AIRFLOW_HOME=~/airflow
export AIRFLOW__CORE__DAGS_FOLDER="/mnt/c/.../36.TanGo/dags"
export AIRFLOW__WEBSERVER__WEB_SERVER_PORT=8082
airflow standalone

# Trigger manual
airflow dags trigger tango_traffic_pipeline

# Ver logs de una tarea
airflow tasks logs tango_traffic_pipeline correr_simulacion <run_id>
Puerto 8080 ocupado

En esta máquina el puerto 8080 está ocupado por EDB Postgres. Airflow corre en el puerto 8082.

VisionIngester YOLO

KAN-16 + KAN-17 · YOLOv8n · Kafka · César Adrián Santos Santacruz

YOLO_PID/semaforo_detector.py es la capa de visión por computadora del sistema. Detecta vehículos detenidos en video usando YOLOv8n y publica eventos a Kafka cuando un vehículo lleva más de 2 segundos quieto, sugiriendo un cambio a verde en el semáforo correspondiente.

Clases detectadas

ID YOLOClasePeso en presión
2car1.0
3motorcycle0.6
5bus2.5
7truck2.0

Parámetros clave

ParámetroValorDescripción
UMBRAL_QUIETO_PX15 pxDesplazamiento máximo para considerar un vehículo detenido
UMBRAL_QUIETO_SEG2.0 sSegundos quieto antes de emitir evento
COOLDOWN_EVENTO5.0 sTiempo mínimo entre eventos del mismo vehículo
FRAME_SKIP3Inferencia cada 3 frames para reducir costo computacional
KAFKA_TOPICsemaforo-eventosTopic donde se publican los eventos

Evento Kafka emitido

{
  "evento":     "objeto_quieto",
  "objeto_id":  42,
  "clase":      "car",
  "posicion":   {"x": 320, "y": 240},
  "seg_quieto": 3.5,
  "timestamp":  1715000000.123,
  "accion":     "cambiar_verde"
}

Setup y ejecución

# Windows — setup automático
cd YOLO_PID
setup.bat

# Correr el detector
python semaforo_detector.py
Estado de integración

En el estado actual del proyecto, el detector YOLO y el DAG de Airflow corren de forma independiente. Kafka es opcional: si no hay broker disponible, el detector genera el video anotado sin enviar eventos (kafka_ok=False). La integración completa está diseñada para producción con cámaras físicas en intersecciones.

FastAPI :8000

Endpoints REST que consume el dashboard

EndpointDescripciónRetorna
GET /Health checkstatus, updated_at, n_nodes
GET /intersectionsLista completa de interseccionesArray de nodos con fase, presión, coords
GET /metricsMétricas generales del sistemagreen_count, red_count, traffic_factor…
GET /pressure-mapMapa de presión{node_id: pressure}
GET /intersections/{id}Detalle de un nodoNodo completo con vecinos
GET /statusEstado completo para debuggingphase_counts, context, weight_stats

Correr localmente

source ~/airflow_env/bin/activate
cd "/mnt/c/.../36.TanGo"
uvicorn dashboard.api:app --reload --port 8000

Docker

3 contenedores · 1 volumen compartido · docker compose up

ServicioPuertoDescripción
tango-api8000FastAPI + core/ + graph/
tango-airflow8082Airflow + DAG (usuario: admin/admin)
tango-dashboard8501Streamlit app

Comandos

# Primera vez (o después de cambiar código)
docker compose up --build

# Demos posteriores (sin reconstruir)
docker compose up

# Apagar
docker compose down

# Trigger manual del DAG dentro de Docker
docker compose exec tango-airflow airflow dags trigger tango_traffic_pipeline
Prerequisito

Docker Desktop debe estar abierto en Windows con la integración WSL activada antes de correr los comandos en WSL. El RL (Ray/torch) no está dockerizado — es offline y tiene conflictos de dependencias. Se mantiene en ~/tango_env.

KANs completados

Registro de hitos del proyecto

sim0 — Baseline timers fijos

Primer simulador, referencia inferior para comparativas.

sim1 — TrafficAlgorithm + SCOOT greedy

Algoritmo completo con presión, ola verde y coordinación vecinal. Casos experimentales: 246, 369, 48C, 5AF.

sim2 — Pesos estáticos + MovementEngine

node_weight (betweenness, pagerank, road_quality). Entidades con rutas Dijkstra, partículas animadas, heatmap.

sim3 — Entorno RL + PPO

TanGoEnv (gymnasium), entrenamiento PPO con Ray 2.55 (~5 días CPU), checkpoint_00550. 15× mejora vs baseline.

KAN-10 — DAG Airflow

Pipeline @hourly con Overpass, TomTom, Open-Meteo. 15 ticks por run, filtro de nodos blind.

KAN-11 — FastAPI + Dashboard HTML

Endpoints REST. Dashboard técnico modo claro/oscuro con Leaflet, métricas en tiempo real.

KAN-12 — Docker

3 contenedores (API, Airflow, Streamlit), volumen compartido, docker compose up.

Trabajo pendiente

En orden de prioridad

KAN-16 — VisionIngester / YOLO

semaforo_detector.py (César Adrián Santos Santacruz): YOLOv8n detecta vehículos detenidos en video y publica eventos a Kafka cuando un vehículo lleva más de 2 segundos quieto. Kafka es opcional con fallback automático.

KAN-17 — Kafka

Integrado en semaforo_detector.py. Publica eventos al topic semaforo-eventos con id, clase, posición, segundos detenido y acción sugerida. La integración completa con el DAG está diseñada para producción.

Integración DAG-Kafka en producción

El detector YOLO y el DAG corren de forma independiente. La integración completa (Kafka → DAG → algoritmo) y el cuarto contenedor Docker están diseñados para cuando el sistema se despliegue con cámaras físicas.

sim3b — Multi-agente (descartado)

Extensión de sim3 con múltiples agentes independientes por intersección. Descartado por restricciones de hardware: sim3 (agente único) requirió ~500,000 segundos de CPU para 550 episodios en un i3-11va con 20GB RAM. Un esquema multi-agente multiplicaría ese costo por el número de agentes activos — inviable sin infraestructura de cómputo distribuido (GPU cluster o cloud). Queda documentado como trabajo futuro para entornos con recursos adecuados.

Calibración de pesos RL

Usar PPO para aprender los exponentes óptimos de la media geométrica de node_weight por hora del día.

Referencias

Fundamentos teóricos de tanGo

Algoritmo de semáforos

SCOOT — Robertson, D.I. & Bretherton, R.D. (1991). Optimizing networks of traffic signals in real time — the SCOOT method. IEEE Transactions on Vehicular Technology, 40(1), 11–15.

FUSION — TRL / Transport for London (2019). FUSION: Multi-source data fusion for urban traffic management. Aplicación de múltiples fuentes de datos en tiempo real para coordinación de semáforos en Londres.

Reinforcement Learning

PPO — Schulman, J., Wolski, F., Dhariwal, P., Radford, A., & Klimov, O. (2017). Proximal Policy Optimization Algorithms. arXiv:1707.06347.

RLlib — Liang, E., et al. (2018). RLlib: Abstractions for Distributed Reinforcement Learning. ICML. Ray 2.55 con api_stack clásica (enable_rl_module_and_learner=False).

Grafo de la ciudad

OpenStreetMap — datos viales de Guadalajara ZMG descargados via Overpass API. Coordenadas de intersecciones, tipos de vía, límites de velocidad y sentido del tráfico.

Betweenness centrality — Freeman, L.C. (1977). A set of measures of centrality based on betweenness. Sociometry, 40(1), 35–41.

PageRank — Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank citation ranking: Bringing order to the web. Stanford InfoLab.

Datos en tiempo real

Open-Meteo — API meteorológica de código abierto. Temperatura, precipitación, viento y visibilidad en tiempo real sin API key.

TomTom Traffic API — Datos de velocidades actuales por segmento vial. Requiere API key.

Contacto

tanGo · mayo 2026

Licencia

GNU Affero General Public License v3.0

El código fuente es libre — puedes usarlo, modificarlo y distribuirlo bajo los mismos términos. Cualquier versión modificada que se ofrezca como servicio de red debe también publicar su código fuente.

Proyecto académico desarrollado en mayo 2026. Hecho con mucho café y algo de paciencia.

                   ##
                  _[]_
                 [____]
             .----'  '----.
         .===|    .==.    |===.
         \   |   /####\   |   /
         /   |   \####/   |   \
         '===|    `""`    |==='
         .===|    .==.    |===.
         \   |   /::::\   |   /
         /   |   \::::/   |   \
         '===|    `""`    |==='
         .===|    .==.    |===.
         \   |   /&&&&\   |   /
         /   |   \&&&&/   |   \
         '===|    `""`    |==='
      jgs    '--.______.--'
⠀⠀⠀⠀⠀⠀⠀⠀⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡾⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⡼⠙⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⠃⢹⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢰⠃⠀⠘⣿⡄⠀⠀⠀⠀⠀⠀⢀⣿⡇⠀⠘⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⢹⣿⣆⠀⠀⠀⠀⠀⣼⣿⠁⠀⠀⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⡇⠀⠀⢀⠞⢻⣿⡆⠀⠀⠀⢰⣿⣿⡀⠀⠀⢹⡄⠀⠀⠀⠀⢀⣤⣤⡄⠀⠀⣀⣤⣤⣀
⠀⠀⠀⠀⠀⠀⡇⠀⢰⠋⠀⠈⣿⣿⡄⠀⠀⣾⣿⡇⠹⡄⠀⢨⡇⠀⠀⠀⢸⣿⣿⣿⣿⣾⣿⣿⣿⣿⡇
⠀⠀⠀⠀⠀⠀⡇⢀⡏⠀⢀⣴⣿⣿⣿⣿⣾⣿⣿⣧⡀⢳⠀⢸⡁⠀⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀
⠀⠀⠀⠀⠀⠀⢣⠸⣅⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⡀⣸⠀⠀⠀⠀⠀⠀⠙⠻⣿⣿⡿⠋⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠘⣦⣿⣿⡿⠛⠻⢿⣿⣿⣿⣿⡟⠉⠙⢿⣿⣇⠀⠀⠀⣀⣠⡄⠀⠀⠀⠉⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣿⣿⡿⠀⠀⣿⡟⣿⣿⣿⣿⢸⣷⠀⠈⣿⣿⣤⠶⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠚⠉⠉⠉⠙⠒⠲⣿⣿⣷⠀⠀⠙⢡⣿⣿⣽⣿⣌⠁⠀⣰⣿⣿⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣠⠽⢿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠉⠑⠒⠠⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣀⠴⠚⠉⠀⠀⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀