Nextflow: Mezclando multiples lenguajes

Una vez hecha la introducción a Nextflow (ver artículo anterior) vamos a empezar a explorar algunas de las funcionalides que nos ofrece.

Supongamos que tenemos un flujo de trabajo que requiere que ciertas partes se realicen usando un programa (o lenguaje de programación) y otras partes otros.

Por ejemplo existe una librería Python que realiza unos cálculos y tenemos otra en Java que realiza otros.

Probablemente usaríamos un bash donde ejecutaríamos cada fase, usando las salidas de unos como entradas de otros.

El problema es que esto nos obliga a tener instalados los diferentes programas (o runtimes) con el engorro de controlar versiones, instalaciones, etc. Una forma de mitigar este problema sería usando, por ejemplo, Docker, montando volumenes, compartiendo ficheros, etc.

Caso de uso

En este ejemplo vamos a desarrollar un pipeline tal que un Python va a crear un número indeterminado de ficheros ( le pasaremos por párametro cuántos queremos generar), cada uno de ello va a ser leído y "modificado" por un comando bash y por último un proceso Java va a pasar a mayúsculas cada fichero (como ves, un ejemplo sin mucha utilidad)

La "gracia" va a ser que:

  • cada fichero se va a tratar de forma paralela e independiente

  • vamos a usar Docker "sin manos" y hacer que cada proceso se ejecute en una imagen de Docker diferente (Python y Java)

Procesos

Para usar la capacidad de ejecutar containers necesitamos activar docker. Para ello crearemos el fichero de configuración nextflow.config con el siguiente contenido:

nextflow.config
docker{
    enabled = true
}

Generar ficheros (Python)

process generarFicheros {
   container 'python:latest'
   input:
      val size
   output:
      path 'example.*'

   """
   #!/usr/bin/env python
   import sys
   total = int($size)

   for x in range(0, total):
      with open('example.'+str(x), 'w') as f:
         f.write('line 1 '+str(x)+'\\n')
         f.write('line 2 '+str(x)+'\\n')
         f.write('line 3 '+str(x)+'\\n')
   """
}

El proceso generarFicheros se va a ejecutar en un container usando la imagen python:latest

Va a recibir un párametro de entrada size que usará en el programa a ejecutar (total=int($size))

Cada fichero consistirá en 3 lineas que contendrán entre otras cosas, el índice del fichero simplemente a modo de demostrar que se están procesando en paralelo

Por cada fichero que se cree con el nombre example.XXX el proceso emitirá un output

Limpieza de fichero (bash)

process eachFile{
   input:
     path file
   output:
     stdout
   """
   head -n 1 $file
   """
}

El proceso eachFile recibe un fichero y simplemente muestra su contenido por consola.

Supongamos que este proceso fuera por ejemplo una limpieza del fichero, quitar líneas mal formateadas, etc. En nuestro ejemplo simplemente va a mostrar la primera línea de cada uno (es decir mostrará "line 1 XX" donde XX es el índice del fichero que generó el proceso Python)

Procesado (Java)

Por último vamos a ejecutar por cada fichero un Java que lo único que hace es pasar a mayusculas el argumento proporcionado:

import java.util.stream.Collectors;
import java.util.stream.Stream;

class MyJava{

    public static void main(String[]args){
        System.out.println( Stream.of(args).collect(Collectors.joining(" ")).toUpperCase());
    }
}

En un proyecto típico, este código lo compilaríamos y generaríamos un Jar para ejecutarlo con java -jar myproject.jar

Para este ejemplo lo que vamos a usar es JBang, una funcionalidad de Java que permite que le pasemos el código y él se encarga de compilarlo y ejecutarlo directamente.

Así pues el proceso a definir en nuestro pipeline quedaría como:

process toUpperString{
   container 'jbangdev/jbang-action'
   containerOptions "--volume $projectDir:/ws"
   input:
      val str
   output:
      stdout
   """
      jbang /ws/src/MyJava.java $str
   """
}

En este process usamos una característica más de Nextflow. Además de indicarle la imagen que queremos usar en el process, podemos proporcionar una cadena de configuración con containerOptions que usaremos para montar el directorio de trabajo

Nextflow se encarga de descargar, montar volumenes y ejecutar en el container creado el comando

jbang /ws/src/MyJava.java $str donde $str va a ser una cadena que se reciba como input

Workflow

Por último nos queda definir el workflow que une a estos tres process

workflow{
   generarFicheros(params.size).flatMap() | eachFile | toUpperString | view
}

El workflow llama en primer lugar a generarFicheros y mediante el operador flatMap convertimos la salida (una lista de ficheros) en una emisión en paralelo

Cada fichero será procesado por eachFile y cada salida de eachFile se enviará a una instancia de toUpperString

Pipeline

El pipeline queda pues de esta forma:

process generarFicheros {
   container 'python'
   input:
      val size
   output:
      path 'example.*'

   """
   #!/usr/bin/env python
   import sys
   size = int($size)

   for x in range(0, size):
      with open('example.'+str(x), 'w') as f:
         f.write('line 1 '+str(x)+'\\n')
         f.write('line 2 '+str(x)+'\\n')
         f.write('line 3 '+str(x)+'\\n')
   """
}


process eachFile{
   input:
     path file
   output:
     stdout
   """
   head -n 1 $file
   """
}

process toUpperString{
   container 'jbangdev/jbang-action'
   containerOptions "--volume $projectDir:/ws"
   input:
      val str
   output:
      stdout
   """
      jbang /ws/src/MyJava.java $str
   """
}

workflow{
   generarFicheros(params.size).flatMap() | eachFile | toUpperString | view
}

Y lo ejecutaremos:

nextflow run ./multilang.nf --size=5

Si todo va bien iremos viendo por consola las primeras lineas de los ficheros convertidas a mayúsculas.

N E X T F L O W  ~  version 22.04.5
Launching `./multilang.nf` [hungry_laplace] DSL2 - revision: bd507594b6
executor >  local (11)
[2a/65dd40] process > generarFicheros   [100%] 1 of 1 ✔
[79/0c4662] process > eachFile (2)      [100%] 5 of 5 ✔
[94/01790c] process > toUpperString (2) [100%] 5 of 5 ✔
LINE 1 0

LINE 1 4

LINE 1 3

LINE 1 1

LINE 1 2

Es interesante observar en esta salida cómo generarFicheros se ha ejecutado 1 of 1 mientras que las otras tareas se han ejecutado 5 of 5 (ya que indicamos size=5 como parámetro de ejecución)

Conclusión

En este post hemos visto cómo usar los process para ejecutar aplicaciones diversas, como en este caso Python y Java, así como el procesado en paralelo de las tareas

Así mismo hemos visto cómo podemos usar Docker incluso con imágenes diferentes en cada proceso junto con una pequeña introducción al fichero de configuración nextflow.config el cual explicaré en un futuro post en más detalle

Follow comments at Telegram group Or subscribe to the Channel Telegram channel

2019 - 2022 | Mixed with Bootstrap | Baked with JBake v2.6.7