Metro en el Arco Ártabro — Transporte Público Metropolitano

Metro en el Arco Ártabro: Explorando un Sistema de Transporte Metropolitano

Las comarcas de A Coruña, Ferrol, Betanzos y Eume concentran más de 450.000 habitantes en un área de unos 1.600 km². Sin embargo, la conexión entre sus núcleos urbanos sigue dependiendo casi exclusivamente de la red viaria (AP-9, AC-12, N-651) y de un servicio ferroviario de cercanías con frecuencias limitadas. Este artículo explora la viabilidad técnica e ingenieril de un sistema de metro ligero o tren-tram que vertebre el Arco Ártabro, usando datos abiertos y análisis geoespacial.

Estado actual: tercera iteración. El modelo de coste tiene 6 capas con gradiente de distancia corregido (la capa de infraestructura ahora decae desde 0 en la vía hasta ~1 a varios km, en vez del valor binario anterior). Las coordenadas de estaciones sobre la línea C-1 (Miño, Pontedeume, Ferrol) se han ajustado a los apeaderos reales de OSM. El escenario «Seguir ferroviaria» usa waypoints intermedios (Perbes, Barallobre) que anclan el trazado al corredor costero de la ría de Betanzos: el tramo Betanzos→Pontedeume ahora tiene una distancia media de 99m al ferrocarril real.

Troncal Alimentación Est. principal Est. alimentación Autopista/autovía Ferrocarril
Variables del modelo de coste
VariableFuenteParámetroPeso (por defecto)
Pendiente MDT 5m → numpy.gradient lineal hasta 6% 0.30
Coste suelo OSM landuse → clasificación rural=0.3 … urbano=0.8 0.25
Población OSM buildings → KDE σ=800m beneficio (negativo) -0.15
POIs Hospitales, universidades, industrial exp(-dist/σ) σ=1000m -0.10
Ferrocarril OSM railways → gradiente corredor σ=1.500m (atractivo) 0.05
Carreteras OSM highways → penalización cruce σ=200m (obstáculo) 0.03
Puentes OSM bridges → pasillo de cruce σ=300m + bridge_clear ~40m 0.02
Geología Proxy: elevación + pendiente α=0.5 elev + 0.5 slope 0.10
Barrera agua MDT ≤ 0m + excepción puentes binaria
Barreras OSM Protected areas, military, cemetery binaria

Fórmula de coste: coste(i,j) = Σ(wk × capak(i,j))

Las 6 capas continuas se normalizan a rango [0, 1] independientemente antes de combinarlas. Cada píxel del ráster final contiene un escalar que representa el coste estimado de construir el metro en esa celda. Las capas de beneficio (población y POIs) reciben pesos negativos: «atraen» el trazado hacia zonas pobladas en lugar de penalizarlas. El ferrocarril, aunque codificado con peso positivo, también actúa como atractivo: la capa usa la fórmula 1 – exp(–dist/σ) donde cerca de la vía = valor bajo (0) y lejos = valor alto (1), por lo que un peso positivo penaliza alejarse del corredor ferroviario. Las barreras (agua, espacios protegidos, cementerios) asignan coste infinito a sus celdas, forzando al algoritmo MCP (minimum cost path) a rodearlas. Los puentes generan una excepción en la barrera de agua mediante una máscara de corredor (bridge_clear, dilatada ~40m) que permite al trazado cruzar rías por puntos viables. Cada escenario aplica pesos distintos sobre las mismas 6 capas precomputadas (archivos .npy), sin necesidad de recalcular los gradientes ni volver a consultar OSM.

Infraestructura desagregada: la capa única de infraestructura se compone de tres sub-componentes con radios de influencia (sigma) distintos, reflejados en la tabla superior como filas independientes. El ferrocarril (σ=1.500m) genera un corredor amplio de atracción — la ruta tiende a seguir la traza ferroviaria existente donde es viable. Las carreteras principales (σ=200m) penalizan los cruces con una banda estrecha: atravesar una autopista requiere paso elevado o subterráneo. Los puentes (σ=300m) son puntos de paso habilitados sobre el agua; su máscara dilatada (bridge_clear) perfora la barrera de agua para que el trazado pueda cruzar rías por los mismos puntos donde ya existe infraestructura.

Comparativa de escenarios (calculando...)
EscenarioLongitud (km)Coste acumuladoRutas troncalesRutas alimentación
Por defecto
Priorizar cobertura
Minimizar coste
Seguir ferroviaria

Índice

Visión general: ¿por qué un metro en el Arco Ártabro?

El Arco Ártabro —la fachada litoral entre las rías de A Coruña y Ferrol— presenta una de las densidades de población más altas de Galicia fuera del eje atlántico Vigo-Pontevedra. Los 15 municipios costeros del arco suman más de 350.000 habitantes, con fuertes flujos diarios de movilidad laboral entre A Coruña y su área metropolitana (Oleiros, Culleredo, Cambre, Arteixo, Sada, Bergondo).

Un sistema de metro ligero o tren-tram:

Situación actual del transporte público en el Arco Ártabro

Ferrocarril de cercanías (Renfe Cercanías AM — antigua FEVE)

La línea Ferrol ↔ Ortigueira es la única conexión ferroviaria de cercanías en la zona. No existe un servicio de cercanías entre A Coruña y Ferrol por ferrocarril. La conexión ferroviaria A Coruña-Ferrol es de Media Distancia (MD) con paso por Betanzos-Infesta, con frecuencias limitadas (3-4 trenes diarios por sentido) y material rodante diésel en vía única sin electrificar.

Red de autobuses metropolitanos

Infraestructura ciclista y peatonal

El vial del puerto entre A Coruña y O Burgo (vía Ártabra) es un carril bici de referencia, pero no está conectado a una red metropolitana más amplia. La mayoría de los desplazamientos intermunicipales carecen de infraestructura ciclista segregada.

Datos geoespaciales disponibles

Todos los datos mencionados a continuación son gratuitos y de libre descarga (salvo indicación contraria), con licencia CC-BY 4.0 o equivalente. La mayoría están disponibles a través del Centro de Descargas del CNIG (IGN) o de servicios WMS/WFS públicos.

Modelo Digital del Terreno (elevación)

ProductoResoluciónFormatoFuenteCoste
MDT02 (2ª Cobertura LiDAR)2 mCOG (Cloud Optimized GeoTIFF)CNIG/IGNGratuito
MDT05 (1ª Cobertura)5 mCOG / ASCIICNIG/IGNGratuito
MDT2525 mCOG / ASCIICNIG/IGNGratuito
LiDAR 2ª Cobertura (nube de puntos)0.5–4 pts/m²LAZCNIG/IGNGratuito

Información catastral y valor del suelo

ProductoContenidoFormatoFuente
Catastro WFS INSPIRE (BU)Edificios: huella, altura, uso, año construcciónWFS 2.0 / GMLD.G. Catastro
Catastro WFS INSPIRE (CP)Parcelas catastrales: geometría, uso del sueloWFS 2.0 / GMLD.G. Catastro
Cartografía Catastral WMSMapa base catastral (parcelario + edificios)WMSD.G. Catastro
Mapas de valores urbanosValor de referencia del suelo por zonaPDF/visorSede Catastro
Sede Catastro (datos no protegidos)Superficie, uso, año construcción por parcelaREST/SOAPSede Catastro

Población, demografía y movilidad

ProductoResoluciónFuente
Secciones censales INE~1.000–2.500 hab por secciónINE GeoServer (WFS)
INE Atlas de Renta (ADRH)Renta media por sección censalineAtlas.data (GitHub)
Datos de movilidad (INE 2021)Flujos residencia-trabajo intermunicipalesINE Censos 2021

Infraestructuras y red viaria

ProductoContenidoFuente
Red de Transporte IGN (IGR-RT)Red viaria, ferroviaria, portuariaCNIG/IGN
CartoCiudadCallejero completo, direcciones, portalesCartoCiudad (CNIG)
OpenStreetMapRed viaria, POIs, edificios, usos del sueloOpenStreetMap

Planeamiento urbanístico

ProductoFuente
PGOM A Coruña (capas WMS)IDE Coruña
Planes Generales municipalesSedes electrónicas municipales / SIOTUGA (Xunta)
SIOSE (Sistema de Información de Ocupación del Suelo)CNIG/IGN

Metodología: algoritmo de coste mínimo para trazado

El núcleo técnico del proyecto es encontrar trazados óptimos entre estaciones minimizando una función de coste multivariable. El enfoque consiste en separar la combinación de variables del algoritmo de pathfinding: primero se construye una superficie de coste escalar combinando todas las variables, y luego se ejecuta el algoritmo de ruta mínima sobre esa superficie.

¿Por qué funciona con librerías estándar?

Tanto skimage.graph.route_through_array (Python) como PathFinding.js (JavaScript) operan sobre un único array de costes por celda. No necesitan saber que el coste proviene de 8 variables distintas. Esa combinación es una etapa previa independiente del pathfinding:

coste_total[i][j] = w₁ × pendiente_norm[i][j] + w₂ × suelo_norm[i][j] + w₃ × demanda_norm[i][j] + …

# Y luego, una sola llamada:
path, cost = route_through_array(coste_total, start, end)

Esto es exactamente el flujo estándar en GIS para least-cost path: álgebra de mapas para combinar capas → algoritmo de ruta mínima sobre el resultado. Las librerías de pathfinding son perfectamente válidas porque la complejidad multivariable se resuelve antes de llamarlas.

Lo que SÍ requiere trabajo propio

Superficie de coste

Se genera una malla ráster donde cada píxel contiene un valor de «coste de construcción» por metro lineal. Este coste es la suma ponderada de varias capas normalizadas:

  1. Pendiente del terreno (elevación): Calculada a partir del MDT02 (2 m). Las pendientes superiores al 4-6% disparan los costes de túnel o viaducto. Se puede derivar un modelo de «coste de excavación/túnel» a partir de la diferencia entre la cota del terreno y una rasante de referencia.
  2. Coste del suelo (expropiación): Aproximado a partir de:
    • Valores de referencia del Catastro (mapas de valores urbanos/rústicos)
    • Clasificación urbanística del suelo (urbano consolidado, urbanizable, rústico, protegido)
    • Usos del suelo de SIOSE / Catastro
  3. Proximidad a demanda (población): Las celdas cercanas a centros de población (secciones censales con alta densidad) reciben una «bonificación» que atrae el trazado hacia ellas. Se modela como un coste negativo (o beneficio).
  4. Proximidad a POIs: Estaciones de tren, hospitales (CHUAC, Arquitecto Marcide), universidades (UDC Elviña, UDC Zapateira), polígonos industriales (A Grela, Sabón, Río do Pozo), centros comerciales, intercambiadores.
  5. Restricciones absolutas: Barreras infranqueables (masas de agua — rías, embalses de Cecebre), espacios naturales protegidos (Reserva de la Biosfera Mariñas Coruñesas), suelo militar, cementerios.
  6. Geología y tipo de terreno: Afecta al coste de excavación. Roca granítica (más caro) vs. sedimentos aluviales (más barato). Datos disponibles en el IGME (Instituto Geológico y Minero).
  7. Longitud del trazado: Penalización por distancia euclidiana. El algoritmo minimiza naturalmente la longitud, pero se puede modular el peso relativo frente a otras variables.
  8. Interferencia con infraestructura existente: Cruce con autopistas, vías férreas, ríos. No es una barrera absoluta pero incrementa el coste (necesidad de pasos elevados o túneles).

Algoritmo de trazado

Una vez generada la superficie de coste escalar (combinación ponderada de todas las variables), se aplica MCP (Minimum Cost Path) sobre la matriz para encontrar la ruta de mínimo coste entre pares de estaciones. Esto lo resuelve route_through_array sin necesidad de implementar el algoritmo. Los pasos:

  1. Definir estaciones potenciales en config.yaml: coordenadas de inicio/fin y paradas intermedias candidatas (núcleos de población, equipamientos). El script las convierte a índices de matriz con rasterio.index().
  2. Generar superficie de coste: álgebra de mapas con NumPy — cada variable normalizada se multiplica por su peso y se suman. Las barreras (agua, protegido) reciben np.inf. Una línea de código.
  3. Ejecutar MCP: route_through_array(coste_total, start, end, fully_connected=True, geometric=True). Devuelve la lista de índices de la ruta óptima y el coste acumulado.
  4. Postprocesar: convertir índices a coordenadas WGS84, simplificar la polilínea para respetar radios de curvatura, exportar a GeoJSON.

El script itera sobre los escenarios definidos en config.yaml (distintas combinaciones de pesos) y genera un GeoJSON de trazado por escenario. El front solo elige cuál mostrar.

Consideraciones de ingeniería ferroviaria

Stack tecnológico para la implementación web

Principio clave: scripts offline a resolución nativa (2m), resultados estáticos, front ligero. El MDT02 a 2m sobre el Arco Ártabro (~1.600 km²) son del orden de 400 millones de celdas. Eso no cabe en un navegador ni se puede rutear en JS con tiempos razonables. Pero en Python con NumPy y un A* eficiente sobre una matriz en memoria tarda 30-60 segundos por trazado — perfectamente asumible para un script que se ejecuta una vez.

1. Scripts offline (scripts/) — se ejecutan una vez, a 2m

Un directorio scripts/ con Python + GDAL + scikit-image que procesa los datos a resolución nativa y exporta resultados estáticos. Se ejecuta en local, no en el servidor.

Para el pathfinding se usa skimage.graph.route_through_array, que implementa MCP (Minimum Cost Path) sobre un array NumPy n-dimensional con soporte para 8-vecinos y coste geométrico. Es código C bajo el capó — no hay que implementar A* a mano. Una llamada devuelve la ruta óptima y su coste acumulado:

from skimage.graph import route_through_array
path, cost = route_through_array(cost_surface, start_idx, end_idx, fully_connected=True, geometric=True)

Tareas del script:

Alternativas de pathfinding en JavaScript

Si en el futuro se quisiera pathfinding interactivo en el front (requiere reducir resolución porque ~400M celdas no caben en JS), hay librerías probadas que evitan implementar A* desde cero:

Para el escenario actual (sin reducir resolución), estas librerías no aplican en front; el pathfinding se hace 100% en los scripts offline con route_through_array.

scripts/ ├── preprocess.py # Orquestador: descarga MDT, fusiona, recorta, pendientes y coste ├── config.yaml # Pesos de variables, bbox, estaciones, escenarios

2. Servidor estático — el blog PHP/Slim sin cambios

Los resultados de los scripts se copian a /content/coruna/data/metro/. El blog los sirve como ficheros estáticos igual que ya hace con los GeoJSON de municipios y barrios. Cero lógica de servidor nueva.

data/metro/ ├── trazado_escenario_a.geojson # Ruta óptima (escenario 1: pesos por defecto) ├── trazado_escenario_b.geojson # Ruta óptima (escenario 2: priorizar cobertura) ├── trazado_escenario_c.geojson # Ruta óptima (escenario 3: minimizar coste) ├── estaciones.geojson # Ubicación de estaciones propuestas ├── estaciones_buffer.geojson # Área de influencia 800m de cada estación ├── secciones_poblacion.geojson # Secciones censales con densidad de población ├── pois.geojson # Hospitales, universidades, polígonos industriales ├── barreras.geojson # Rías, embalses, espacios protegidos ├── coste_tiles/{z}/{x}/{y}.png # Tiles del mapa de calor de coste (2m → imagen) └── hillshade/{z}/{x}/{y}.png # Tiles de relieve (2m → imagen)

3. Cliente (navegador) — Leaflet + Turf.js, sin pathfinding

El front solo visualiza y deja comparar. Con los trazados precomputados para varios escenarios y pesos, el usuario dispone de toda la información relevante sin necesidad de ejecutar A* en el navegador.

CapaTecnologíaRol
Mapa base + capasLeafletCarga los GeoJSON estáticos (trazados, estaciones, secciones censales, POIs, barreras) y los tiles de hillshade + coste. Selector para alternar entre escenarios precomputados. Igual que ya funciona en /coruna/mapa-barrios.
Geometría interactivaTurf.jsOperaciones ligeras: medir longitud de cada trazado, distancia punto-línea (qué sección censal está más cerca de qué estación), buffers para área de cobertura, intersecciones para calcular población servida por cada escenario, suavizado de líneas.
Relieve y costeLeaflet + tiles XYZCapa de hillshade generada offline con gdaldem hillshade + gdal2tiles a resolución nativa. Capa de calor del coste generada con el mismo pipeline. El front solo carga los tiles.
ComparativaJS + Turf.jsTabla con métricas por escenario: longitud total, coste acumulado, población servida, estaciones, % en túnel/superficie/viaducto. Selector de escenario que cambia el trazado visible en el mapa.

¿Se puede hacer interactivo sin reducir resolución?

Un A* sobre 400M de celdas en JS no es viable (~30-60s en Python contra varios minutos o un crash en JS). Pero hay alternativas que no implican reducir la resolución del dato original:

Resumen del flujo

  1. Scripts (1 vez): python scripts/preprocess.py → procesa MDT02 a 2m, ejecuta A* para N escenarios, exporta GeoJSON + tiles a data/metro/.
  2. Despliegue: Copiar data/metro/ a /content/coruna/data/metro/ en el blog. Sin cambios en PHP.
  3. Front: Leaflet carga los GeoJSON con fetch() y los tiles con L.tileLayer(). Selector de escenarios, tabla comparativa de métricas, hover sobre estaciones para ver población servida (Turf.js). Cero pathfinding en navegador.

Variables consideradas en el modelo de coste

Resumen de todas las variables, su fuente, peso sugerido y modo de normalización:

VariableFuenteTipoPeso sugeridoNormalización
Pendiente del terrenoMDT02 (2m) → GDAL slopeContinua (0-90°)0.30Lineal hasta 6% (óptimo), cuadrática >6%
Coste del sueloCatastro + SIOSECategórica/continua0.25Escala 1-10 según clase urbana y valor ref.
Proximidad a poblaciónINE secciones censalesContinua (densidad)-0.15Kernel density con radio 800m
Proximidad a POIsOSM + IDE CoruñaDistancia euclidiana-0.10Decaimiento exponencial, radio 1km
Longitud de trazadoRáster de costeImplícita1.00Coste por metro lineal en el grafo
Barreras absolutasOSM (agua) + SIOSE (protegido)BinariaCoste infinito = infranqueable
Geología (tipo terreno)IGME Mapa Geológico 1:50kCategórica0.10Factor de excavación: granito 1.5x, aluvial 1.0x
Infraestructura existenteIGR-RT / OSMBinaria0.10Penalización fija por cruce (coste de paso elevado)

Próximos pasos

Fase 1: Preparación de datos ✅ Completado (MDT 5m vía WCS)

  1. MDT descargado vía WCS IGN (5m, 6 tiles, sin login). Fusionado en mdt_artabro_full.tif (7770×9324 px, EPSG:4326).
  2. Pendientes calculadas con numpy (pendiente_artabro.tif).
  3. Secciones censales INE con población para los 35 municipios de Ártabria.
  4. Red viaria y ferroviaria de IGR-RT u OpenStreetMap.
  5. Usos del suelo de SIOSE o Catastro para clasificar áreas urbanas/rústicas/protegidas.
  6. POIs clave (hospitales, universidades, polígonos industriales).

Fase 2: Generación de superficie de coste ✅ Completado (6 capas)

  1. Pendientes calculadas a partir del MDT (normalizadas [0, 1]).
  2. Infraestructura (OSM): ferrocarriles, carreteras primarias+ y puentes → gradiente de proximidad con decaimiento exponencial. Sigma: 500m rail, 200m carretera, 300m puentes.
  3. Población (OSM): densidad de edificios con filtro gaussiano (sigma=800m) → normalizado [0, 1] invertido (alta densidad = bajo coste).
  4. POIs (OSM): hospitales, universidades, estaciones, polígonos industriales y retail → gradiente de proximidad (sigma=1000m).
  5. Uso del suelo (OSM): clasificación por tipo (residencial=0.8, industrial=0.7, rural=0.3, etc.) → rasterizado por categoría.
  6. Barreras: agua (MDT ≤ 0m: 21M celdas) + protección (OSM: protected areas, military, cemetery: 0.6M celdas) = 21.6M celdas bloqueadas.

Fase 3: Trazado de rutas ✅ Primera iteración

  1. 17 estaciones definidas en config.yaml (A Coruña, Culleredo, Cambre, Oleiros, Sada, Bergondo, Betanzos, Miño, Pontedeume, Cabanas, Ares, Mugardos, Fene, Ferrol, Narón).
  2. 12 rutas calculadas con skimage.graph.route_through_array (MCP): 5 troncales + 7 de alimentación.
  3. Trazados suavizados con Douglas-Peucker (ε=0.0002° ≈ 22m).
  4. 3 escenarios exportados como GeoJSON (default, priorizar_cobertura, minimizar_coste).

Fase 4: Scripts de preprocesado y front estático ✅ Completo (2 scripts + 3 escenarios divergentes)

  1. build_layers.py: descarga OSM (infraestructura, población, POIs, uso del suelo, barreras) y rasteriza a 7770×9324 celdas (5m).
  2. preprocess.py: pipeline completo — descarga WCS → merge → pendiente → superficie de coste multivariable → MCP pathfinding → GeoJSON por escenario.
  3. Mapa Leaflet interactivo cargando trazados precomputados con selector de 3 escenarios divergentes.
  4. Capa de tiles de coste para visualizar zonas caras/baratas con opacidad ajustable.
  5. Tabla comparativa de escenarios con métricas (longitud, población servida, coste estimado).
  6. Población cubierta por cada estación (buffer 800m intersectado con secciones censales).

Referencias y enlaces

← Volver al índice de A Coruña