curso-reproducibilidad-team4 / zonificacion-climatica-cte

Zonificación climática localidades españolas según severidades del Código Técnico de la Edificación
MIT License
0 stars 2 forks source link

Definir workflow #7

Open pachi opened 2 years ago

pachi commented 2 years ago

Definir flujo de trabajo con script de SnakeMake

pachi commented 2 years ago

Podemos hacer un workflow con Snakemake.

Creo que los procesos serían:

1 - Script src/select_input.py #11 :

Seleccionar datos de entrada relevantes en archivo data/ign/MUNICIPIOS.csv y generar archivo de datos data/output/Municipios.csv que contenga: COD_INE, COD_PROV, PROVINCIA, NOMBRE_ACTUAL, LONGITUD_ETRS89, LATITUD_ETRS89, ALTITUD, ARCHIVO_TMY

2 - Script src/download_TMY.py #3 :

Para cada municipio, descargar, si no existe ya, el archivo TMY del JRC y guardarlo en data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy

3 - Script src/compute_indicators.py #4 :

Para cada municipio, leer el archivo data/output/tmy/$[COD_INE}_${NOMBRE_ACTUAL}.tmy y calcular:

Combinar con los datos del archivo de municipios y generar data/output/Results.csv.

4 - Script src/plot_results.py #5 :

Graficar:

MJmaolu commented 2 years ago

@pachi, si quieres editar que en el paso 2 guardamos la información en data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy ?

pachi commented 2 years ago

@pachi, si quieres editar que en el paso 2 guardamos la información en data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy ?

Sí, pero fíjate que además la extensión es .csv, porque en el PVGIS hay una opción de guardar el TMY como JSON y me pareció así más claro el proceso para alguien que lo siga "manualmente".

MJmaolu commented 2 years ago

Vale, lo había cambiado a .tmy por respetar el que habías puesto al inicio y no sabía si era una convención, pero totalmente de acuerdo en que le pedimos que descargue el formato csv y poner eso como extensión es lo más claro.

MJmaolu commented 2 years ago

@pachi, he visto que has añadido una imagen de binder". Me parece genial porque así estamos dando 3 alternativas de reproducibilidad: conda, snakemake y el propio binder.

Si quieres podríamos añadirlo en el README en el apartado 2. (Cómo usar este flujo de trabajo) a continuación de la opción de con snakemake.

pachi commented 2 years ago

Perfecto. Estoy revisando el README. He visto que has añadido un índice, así que añado también eso y actualizo el índice.

He conseguido también lanzar el snakemake pero no nos vale el wildcard. En el issue correspondiente pongo más información

MJmaolu commented 2 years ago

Justo estaba añadiendo alguna cosa, he hecho un último push al README pero lo dejo ya porque no quiero entrar en conflicto con lo que estuvieras haciendo. Yo voy a dejar corriéndolo en mi local con la opción de conda y después de comer te digo si ha funcionado;)

pachi commented 2 years ago

Guay

pachi commented 2 years ago

Ahora mismo tenemos en el Snakefile estas reglas que usan un wildcard {dataset}:

rule download_data:
    input: 
        "data/output/Municipios.csv"
    output:
        "data/output/tmy/{dataset}.csv"
    script:
        "src/download_TMY.py"

rule compute_indicators:
    input:
        "data/output/Municipios.csv"
        "data/output/tmy/{dataset}.csv"
    output:
        "data/output/Results.csv"
    script: 
        "src/compute_indicators.py"

Pero Snakemake no es capaz de deducir los archivos de entrada a partir de la salida de otras reglas. Hay información sobre cómo establecer esas dependencias de forma maś concreta, tomándolas directamente del archivo de entrada, aunque en nuestro caso es un archivo generado y no es tan sencillo (sin duplicar la lógica de definir los nombres de archivo).

https://stackoverflow.com/questions/49390202/snakemake-wildcards-in-input-files-cannot-be-determined-from-output-files

MJmaolu commented 2 years ago

No sé si esto podría valer (basado en):

rule download_data: input: "data/output/Municipios.csv" output: "data/output/tmy/{tmy}.csv" script: "src/download_TMY.py"

rule compute_indicators: input: "data/output/Municipios.csv" expand("data/output/tmy/{tmy}.csv", tmy=tmy_files) output: "data/output/Results.csv" script: "src/compute_indicators.py"



Lo de conda está funcionando (pasos 1 - 6a) pero todavía está descargando ficheros. 
pachi commented 2 years ago

Creo que no valdría, porque en el estado inicial no existen los archivos csv, solo tras dowload data. Una opción que estoy viendo si es posible es que dowload_data y compute_indicators no sean dos reglas sino una secuencia única. Esto lo "malo" que tiene es que es poco eficiente si hay interrupciones en las descargas, pero es más simple.

MJmaolu commented 2 years ago

Ok, la otra es preguntarle directamente a @jmoldon para no tener que cambiar código.. A mi es que todavía no se me han descargado todos los tmys pero si tú has probado los scripts 3º y 4º y te funcionan en local, con la opción de conda no va a haber problema.

Es verdad que probablemente el flujo no sea el más eficiente, seguro que podríamos haber paralelizado el cómputo de los indicadores conforme se van descargando los tmys pero creo que intentar resolver esto con lo que tenemos sería lo ideal. Quizás nos pueda echar una mano :grimacing:

pachi commented 2 years ago

Otra opción ingeniosa:

rule download_data:
    input: 
        "data/output/Municipios.csv"
    output:
        "data/output/flag.txt"
    shell:
        "python3 src/download_TMY.py && touch data/output/flag.txt"

rule compute_indicators:
    input:
        "data/output/Municipios.csv",
        "data/output/flag.txt"
    output:
        "data/output/Results.csv"
    script: 
        "src/compute_indicators.py"

Se podría incluso generar un archivo de log.txt en el propio script download_TMY.py que sirviese de bandera y que solo se generase si se descargan todos los archivos (usando el finally de un try / catch / finally).

pachi commented 2 years ago

Voy a probar esa idea, que parece lo más sencillo.

Sí, lo de paralelizar el cálculo de indicadores lo pensé, pero es que tarda tan poco tiempo el cálculo que parecía mucho más complicado montar un archivo a partir de trozos que esperar a que se acabase todo el proceso.

pachi commented 2 years ago

Funciona! He subido los cambios. Ahora voy a sacar alguna gráfica y completo el workflow.

MJmaolu commented 2 years ago

Fenomenal!!! :clap: Cuando acabe la parte de conda te actualizo

MJmaolu commented 2 years ago

El flujo con conda también funciona (a falta de generar algún plot). He visto también el jpynb que has subido, qué chulada de plots los de la severidad de las zonas climáticas!!

pachi commented 2 years ago

En la versión fa33bde he añadido el notebook que realiza las gráficas. No tenemos material todavía para hacer un paper, pero algo es algo. He actualizado también las instrucciones para ejecutar el notebook y generar las gráficas desde la línea de comandos, sin abrir el archivo y he añadido la dependencia de jupyter_core y nbformat a environment.yaml

jmoldon commented 2 years ago

Qué maravilla de hilo, así da gusto! Muy interesantes las discusiones. Os comento algunas cosas sobre wildcards, que son algo complicadas de asimilar, y de hecho a mí aún me cuesta identificarlas en todos los casos. Reabro el hilo.

Las wildcards de snakemake no son magiccards, se pueden encadenar muchas rules con wildcards arbitrarias, pero tarde o temprano se tienen que definir, por ejemplo una de las reglas finales puede ser del tipo expand("dataset}/a.txt",dataset=DATASETS), donde DATASETS es una lista bien definida. Otra opción es usar glob_wildcards, para que el script busque todos los archivos que existan en un lugar, pero con eso perdéis un poco de control de lo que se procesa, aunque también sería posible).

En vuestro caso, una opción puede ser definir en el mismo Snakefile una lista de MUNICIPIOS, que puede ser usada en el último input que necesite ese listado. El resto de inputs/outputs puede usar la wildcard genérica {municipios}, y dejar solo un expand al final. Algo de este estilo creo que funcionaría:

import unicodedata
import pandas as pd

conda: "environment.yaml"

def removeAccents(text):
        text = str(text)
        normalized = unicodedata.normalize("NFKD", text)
        normalized = "".join([c for c in normalized if not unicodedata.combining(c)])
        return normalized.lower()

MUNICIPIOS = list(pd.read_csv("data/output/Municipios.csv")['NOMBRE_ACTUAL'].apply(removeAccents))

rule plot:
    input: "data/output/Results.csv"
    output:
        "data/output/plots/zci-diff-hist.png",
        "data/output/plots/zcv-diff-hist.png",
        "data/output/plots/zc-tmy.png",
    notebook: "notebooks/graficas.ipynb"

rule select_input:
    input: "data/ign/MUNICIPIOS.csv"
    output: "data/output/Municipios.csv"
    script: "src/select_input.py"

rule download_data:
    input: "data/output/Municipios.csv"
    output: "data/output/{municipio}.txt"
    shell: "python3 src/download_TMY.py {municipio}"

rule compute_indicators:
    input: expand("data/output/{municipio}.csv", municipio=MUNICIPIOS)
    output: "data/output/Results.csv"
    script: "src/compute_indicators.py"

Notas:

Espero que la información sea útil. El código que he puesto no es definitivo, solo he cambiado las partes clave que creo que hacen funcionar el workflow, pero tal vez tangáis que retocar otras partes para adecuarlo a vuestro caso

pachi commented 2 years ago

Jeje, gracias, @Jmoldon, la verdad es que hemos trabajado muy bien.

El otro día estuvimos hablando de este problema de la paralelización de la descarga en la reunión en Zoom porque da pena que algo que, en principio, resulta tan estúpidamente paralelo no esté implementado de esa forma.

Hay, sin embargo, alguna dificultad en este caso porque:

  1. el servidor limita las llamadas a la API a 30/s

    Se podría limitar el número de procesos a < 30 y poner un sleep en cada proceso de 1s para asegurar que no excedemos las peticiones permitidas.

  2. la conexión me resultó poco fiable.

    Es posible que sea un problema con mi conexión (con la activación de la pantalla de bloqueo) pero también que sea poco fiable el servidor. Pensé en mejorar la configuración de la sesión para aumentar los retrys, usar exponential backoff y eliminar el keepalive, por si está saturado el servidor y mejorar la reconexión

Como la descarga es algo que, en principio, se hace solo una vez y, si se corta la conexión, se puede retomar en cuanquier momento sin que se vuelvan a descargar los archivos disponibles, no parecía una ganancia clara para la complejidad que añadía. Pero es cierto que igual merece la pena problar al menos la parte 1.

Respecto al Snakemake, no estoy seguro de que el código que propones funcione, porque MUNICIPIOS depende de Municipios.csv pero se evalúa antes de la regla que lo genera. La resolución de archivos es previa a la ejecución de las reglas... así que creo que no funcionaría.

Como las reglas son código ejecutable, seguramente se podría llevar ese código a download_data pero no estoy seguro de la manera más sencilla (aparte del archivo de bandera) para señalizar que esa regla dependa de la que genera el archivo Municipios.csv.

Podemos probar también a ver si funciona la solución propuesta o a explorar la alternativa.

jmoldon commented 2 years ago

Una ventaja de tenerlo paralelizado (cada descarga es una ejecución independiente de la rule) es que mientras se van descargando nuevos datos, snakemake está libre para ir haciendo cosas con los municipios que ya están disponibles. En vuestro caso, no importa mucho porque el siguiente paso necesita todos los municipios. Pero si tuvierais que procesar más cada municipio antes de juntarlos, esto sería lo más eficiente. Otra ventaja de que sea snakemake quien gestione cada archivo, es que os asegurais de que al final estén todos descargados. Si el input es downloads_done.txt, podría ser que ese archivo exista pero no asegura que todo se haya descargado.

Por el límite de descargas de la API no es un problema, hay un parámetro --max-jobs-per-second (https://snakemake.readthedocs.io/en/v5.1.4/executable.html), aunque nunca lo he probado. También se podrían limitar cuántos jobs se lanzan en paralelo con la opción que hay en el enlace que puse, o con threads y creo que hay alguna opción más en la gestión de resources que hace snakemake. Yo intentaría evitar la opción de usar sleep, pero si no hay más remedio, ok. También hay opciones para reintentar una descarga si falla (--restart-times) y algunas otras utilidades.

Es verdad, así no funciona el código, me equivoqué y al definir MUNICIPIOS el archivo a leer debería ser el que hay en data/ing/MUNICIPIOS.csv no el data/output/Municipios.csv, claro. Lo importante es que es buena práctica que todo el workflow quede fijado con una lista inicial que defina todo lo que se tiene que procesar explícitamente.

pachi commented 2 years ago

Por el límite de descargas de la API no es un problema, hay un parámetro --max-jobs-per-second (https://snakemake.readthedocs.io/en/v5.1.4/executable.html), aunque nunca lo he probado.

Sí, esto parece lo que necesitamos y así evitamos el sleep.

En estos casos mi duda siempre es, ¿hacemos el proyecto dependiente de Snakemake?

En estos momentos Snakemake es una dependencia opcional, ya que tenemos los flujos de trabajo también disponibles simplemente con un entorno Conda y ejecución manual de los script y, como prácticamente solo tiene dependencias de Python y sus librerías, el entorno Conda es meramente para asegurar un entorno e ejecución que es fácil de describir. Pero si Snakemake asume la paralelización, o bien duplicamos esa funcionalidad en los script, o pasa a ser una dependencia siempre necesaria.

En todo caso, creo que intentaré probar a hacerlo ya que me parece útil para otros casos. También me interesa mucho la posibilidad de que se pueda hacer con Snakemake un balanceo de carga en un cluster (no para este caso).