Bonjour à tous, et bienvenue dans le premier épisode d'une (longue ?) série de prise en main de l'outil dédié au pipeline : Snakemake.
Si vous ne connaissez pas encore cet outil, c'est que vous êtes sûrement passés à côté de cet article écrit par Nisaea. Alors, quel sera les bénéfices de retranscrire vos pipelines déjà tout prêt en Snakefile ?
Lisibilité du code, gestion des ressources et reproductibilité
Lorsque vous êtes sur le point de publier, il va bien falloir expliquer aux futurs lecteurs comment vous avez obtenu les résultats. Cela permettra ainsi aux autres bioinformaticiens de partir de vos données brutes, et de retrouver les mêmes résultats que vous. On approche ici un point clé de la bioinformatique : la reproductibilité. Quels ont été les outils utilisés, à quelles versions, avec quels paramètres… Tous ces petits "trucs" qui permettent d'obtenir votre résultat. Un article scientifique avec des résultats majeurs découverts grâce à la bioinformatique DEVRAIT être associé à un pipeline/script permettant de reproduire les résultats à l'identique.
Ces dernières années ont vu le volume de données augmenter de façon quasi exponentielle. En parallèle, les ressources mises à disposition (telles que les clusters de calcul) n'ont cessé de croître en taille (nombre de cpu) et en puissance de calcul (vitesse des cpu), permettant ainsi de rester dans la course. Cependant, pour les exploiter au mieux, il est nécessaire de paralléliser nos tâches. En effet, beaucoup d'outils de bioinformatique ont des options permettant d'utiliser simultanément plusieurs cpu (ex.: bwa, STAR, …). D'autres, en revanche, n'ont pas été conçus pour être utilisés dans un environnement multi-cpu (awk, macs2, bedtools, …). Dans ces cas précis, il faut soit paralléliser à la main (lancer plusieurs tâches en fond avec l'ajout d'un '&' à la fin de la commande), soit mettre les opérations en file d'attente et sous-exploiter la machine sur laquelle vous travaillez (gestion séquentielle des tâches). Nous verrons dans un prochain article qu'avec Snakemake on peut, très facilement, aller plus loin dans la parallélisation.
L'inventeur de Snakemake, Johannes Köster [1], a su tirer avantage de Python (syntaxe claire) et de GNU make (parallélisation, gestion des ressources, système de règles). Avec l'ajout de nombreuses fonctionnalités comme le tracking de version/paramètres, la création de rapport html ou la visualisation des tâches et de leurs dépendances sous forme de graph (DAG), ce langage a tout le potentiel pour avoir un bel avenir.
Principes généraux
Tout comme GNU make, Snakemake fonctionne sur le principe des règles (rule). Une règle est un ensemble d'élément qui vont permettre de créer un fichier, c'est en quelque sorte une étape dans le pipeline.
Chaque règle a au moins un fichier de sortie (output) et un (ou plusieurs) fichier(s) d'entrée (input) (j'ai volontairement commencé par l'output, vision Snakemakienne, je pars de la fin pour aller au début).
La toute première règle du Snakefile doit définir les fichiers que l'on veut à la fin du traitement (fichier cible/target). L'ordre (et le choix) des règles est établi automatiquement à l'aide du nom des fichiers/dossiers cibles. On va ainsi remonter les fichiers de règle en règle jusqu'à trouver une règle avec un input plus récent que l'output. Ensuite, cette règle et toutes celles qui suivent vont être exécutées.
Snakemake fonctionne avec le nom des fichiers
Le plus simple serait de commencer par un exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# règle qui spécifie les fichiers que l'on veut générer rule targets : input : "data/raw/s1.fq.gz" "data/raw/s2.fq.gz" # règle qui permet de compresser un fichier avec gzip rule gzip : input : # On peut utiliser cette syntaxe lorsque # input et output sont dans le même répertoire "{base}" output : "{base}.gz" #on rajoute seulement l'extension à l'output shell : "gzip {input}" |
Vous l'aurez deviné, s'il existe data/raw/s1.fq et data/raw/s2.fq, plus récents que data/raw/s1.fq.gz et data/raw/s2.fq.gz alors la règle gzip va créer/remplacer les cibles. De plus, on peut paralléliser les opérations en passant en paramètre le nombre de processeurs à utiliser (option ‑j N, –jobs=N).
Chaque règle peut avoir plusieurs mots clés
Snakemake utilise des mots clés pour définir les règles.
Entrée/sortie
input = fichier(s) à utiliser pour créer la sortie
output = fichier(s) qui seront générés avec la règle
1 2 3 4 5 6 7 8 9 |
# exemple rule mapping : input : "raw_data/{sample}.fq.gz" # utilisation d'une expression régulière pour définir wildcards.genome # (commence par hg suivit d'un ou plusieurs chiffres) output : "mapping/{genome, ^hg[0–9]+$}/{sample}.bam" shell : "do_mapping ‑genome {wildcards.genome}.fa ‑fastq {input} ‑outbam {output}" |
wildcards = Ce sont des variables qui sont définies avec le {} et une partie du nom/chemin des fichiers de sortie, cela permet de généraliser les règles. Avec cet exemple nous avons utilisé deux wildcards (genome et sample), on peut également utiliser des expressions régulières pour bien délimiter la définition d'un wildcards comme pour genome qui vaut "hg[0–9]+".
Utilisation de l'objet wildcards (par section) :
- log : "mapping/{genome}/{sample}.sort.log"
- shell/run/message : "mapping/{wildcards.genome}/{wildcards.sample}.sort.log"
- params : param1 = lambda wildcards : wildcards.genome + '.fa'
Explication :
En dehors de input/output/log on peut utiliser directement le nom de variable car l'objet wildcards n'est connu que de shell/run/message. C'est pour cela que l'on passe à la forme "wildcards.variable" dans ses 3 dernières sections. En dehors de shell/run/message on peut utiliser "lambda wildcards : wildcards.variable". Ici la fonction lambda permet d'utiliser directement l'objet wildcards dans les sections.
Traitement des fichiers créés
temps = fichier temporaire supprimé après utilisation.
protected = ce fichier sera protégé en écriture après création.
Commande pour générer le(s) fichier(s) sortie (run/shell)
shell = utiliser une commande UNIX pour créer le(s) fichier(s) sortie.
run = utiliser du code python pour construire le(s) fichier(s) sortie.
note : on doit choisir entre run et shell
note2 : avec run on peut utiliser la fonction shell()
Autres paramètres des règles
threads = nombre de processeurs maximum utilisables
message = message à afficher au début
log = fichier log
priority = permet de prioriser les règles (ex : quand plusieurs de règles utilisent le même input)
params* = paramètres des commandes utilisées
version* = version de la règle (ou de la commande utilisée)
*: ces deux mots clés permettent d'associer les paramètres et les versions des règles aux fichiers de sortie. Ceci permet de (re)générer un fichier de sortie si un paramètre ou une version de la règle a été modifiée. Il y a aussi du code tracking, pour suivre l'évolution du pipeline et des fichiers générés.
Un exemple plus concret :
Ici nous allons créer un pipeline d'analyse données génomique (ex : ChIP-seq) qui va aligner/mapper de petites séquences issues du séquençage haut débit (reads/lectures) sur un génome de référence.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
################################ ####### mapping avec BWA ####### ################################ # Tous vos fichers fastq doivent être dans raw_data/xxx.fq.gz # # L'extension doit être exactement fq.gz # # Il doit y avoir hg38.fa dans ./ # # Pour des données paired-end (2 fichiers : _R1/_R2): # input : # hg38.fa # raw_data/sampleX_c3-pe_R1.fq.gz # raw_data/sampleX_c3-pe_R2.fq.gz # output : # mapping/hg38/sampleX_c3-pe.sort.bam # # processus : # Index du génome de référence # Mapping de deux jeux de données (se_7 et c3-pe)(bwa) # Alignement sur l'index du génome (bwa aln) # Transformation des coordonnées de l'index en coordonnées # génomiques (bwa sampe/samse) # Rangement des données par coordonnées génomique (samtools sort) # Indexer le fichier ordonnée (samtools index) # ################################ ################################ # Pour récupérer la version des outils utilisés (voir rule sort/index) import subprocess # Version de bwa bwa_version = "0.7.10" # Fichiers en sortie du pipeline ("hg38.fa.bwt" aurait pu être omis) rule target : input : "hg38.fa.bwt", "mapping/hg38/se_7.sort.bam.bai", "mapping/hg38/c3-pe.sort.bam.bai" # Index du génome de référence avant l'alignement des lectures (séquences) rule bwa_index : input : "{genome}.fa" #2) cherche {wildcards.genome}.fa" output : "{genome}.fa.bwt" #1) crée la variable wildcards.genome. message : "Index du génome de référence :{wildcards.genome}" version : bwa_version threads : 1 shell : #3) lance la commande "bwa index {input}" #"{input}"="{wildcards.genome}.fa" # Alignement des lectures sur le génome de référence rule bwa_aln : input : bwt = "{genome}.fa.bwt", fasta = "{genome}.fa", fq = "raw_data/{samples}.fq.gz" output : temp("mapping/{genome}/{samples}.sai") #fichier temporaire threads : 20 #max cpu autorisé version : bwa_version log : "mapping/{genome}/{samples}.bwa_aln.log" message : "Alignment de {wildcards.samples} sur {wildcards.genome} " shell : "bwa aln " # avec Snakemake "-t {threads} " # vous pouvez "{input.fasta} " # commenter "{input.fq} " # tous les ">{output} " # arguments "2>{log}" # des commandes # Si les données sont appariées (paired-end), autrement dit si les fragments # ont été séquencés dans les deux sens, cette règle sera utilisée rule bwa_sampe_to_bam : input : fasta = "{genome}.fa", sai1 = "mapping/{genome}/{samples}_R1.sai", fq1 = "raw_data/{samples}_R1.fq.gz", sai2 = "mapping/{genome}/{samples}_R2.sai", fq2 = "raw_data/{samples}_R2.fq.gz" output : #fichier temporaire temp("mapping/{genome}/{samples}.bam") log : "mapping/{genome}/{samples}.samse.log" threads : 2 version : bwa_version message : "PE : sai –> bam ({wildcards.genome}/{wildcards.samples})" shell : "bwa sampe " "{input.fasta} " "{input.sai1} {input.sai2} " "{input.fq1} {input.fq2} " "2>{log} | " "samtools view ‑Sbh — > {output}" # Si on a séquencé les fragments dans un sens (Single read) cette règle sera utilisée rule bwa_samse_to_bam : input : fasta = "{genome}.fa", sai = "mapping/{genome}/{samples}.sai", fq = "raw_data/{samples}.fq.gz" output : #fichier temporaire temp("mapping/{genome}/{samples}.bam") log : "mapping/{genome}/{samples}.samse.log" threads : 2 message : "SE : sai –> bam ({wildcards.genome}/{wildcards.samples})" shell : "bwa samse {input.fasta} {input.sai} {input.fq} " "2>{log} | " "samtools view ‑Sbh — > {output}" # Pour ranger les données par coordonnées génomiques rule sort_bam : input : "{base}.bam" output : # fichier protégé en écriture protected("{base}.sort.bam") threads : 10 log : "{base}.sort.log" message : "Ordonne le fichier {wildcards.base}.bam" version : # pour récupérer la version de l'outil avec une commande shell subprocess.getoutput( "samtools –version | " "head ‑1 | " "cut ‑d' ' ‑f2" ) params : niveau_compression = "9", memoire_max_par_cpu = "1G" shell : "samtools sort " "-l {params.niveau_compression} " "-m {params.memoire_max_par_cpu} " "-@ {threads} " "-f {input} " "{output} " "2>{log}" # Pour indexer le fichier trié (balise le fichier pour accéder aux données plus rapidement) rule index_bam : input : "{base}.sort.bam" output : "{base}.sort.bam.bai" priority : 5 # augmente la priorité de la règle (défaut = 1) version : subprocess.getoutput( "samtools –version | " "head ‑1 | " "cut ‑d' ' ‑f2" ) threads : 1 shell : "samtools index {input}" |
Avec environ 130 lignes, ce code est déjà prêt à être utilisé de manière intensive sur pc, serveur ou cluster de calcul 🙂
A partir de ce Snakefile, on peut générer un graphe représentant la dépendance entre les règles avec la commande :
1 |
snakemake –rulegraph | dot | display |
On peut également générer un graphe plus complet qui prend aussi en compte les wildcards avec la commande :
1 |
snakemake –dag | dot | display |
Cet article touche à sa fin, je vous donne rendez-vous au prochain épisode, d'ici là, vous pouvez poser des questions dans les commentaires (ils seront peut être utilisés pour les prochains articles).
Et si ça vous a plu, n'hésitez pas à partager et nous faire part de vos astuces, proposez-nous des règles ou même des Snakefiles dans les commentaires, si cela a du succès nous ouvrirons peut-être un dépôt git pour regrouper toutes ces contributions.
Un grand merci aux relecteurs Yoann M., Lroy et Kumquat[um] pour leur remarques et corrections 😉
[1] Johannes Köster, "Parallelization, Scalability, and Reproducibility in Next-Generation Sequencing Analysis", TU Dortmund 2014
Laisser un commentaire