From 90c0b0c77e380a9cc6bc134cbdc0abea57b5ecca Mon Sep 17 00:00:00 2001 From: mykle hoban <3262131+mhoban@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:54:13 -0700 Subject: [PATCH] Revert "Dev" --- .editorconfig | 1 - .nf-core.yml | 12 +- CHANGELOG.md | 4 +- CITATIONS.md | 32 - LICENSE | 2 +- README.md | 133 +- assets/adaptivecard.json | 2 +- assets/assesspool_report.Rmd | 1662 +++++++++-------- assets/email_template.html | 14 +- assets/email_template.txt | 10 +- assets/input.csv | 2 - assets/samplesheet.csv | 3 + assets/schema_input.json | 12 +- assets/slackreport.json | 2 +- conf/base.config | 4 +- conf/filters.config | 72 - conf/modules.config | 204 +- conf/test.config | 40 +- conf/test_full.config | 41 +- defaults.yml | 70 - docs/README.md | 4 +- docs/images/logo.png | Bin 114763 -> 0 bytes docs/output.md | 77 +- docs/usage.md | 85 +- main.nf | 6 +- modules.json | 9 +- .../local/extractsequences/environment.yml | 9 - modules/local/extractsequences/main.nf | 58 - modules/local/extractsequences/meta.yml | 68 - .../resources/usr/bin/extractsequences.R | 97 - .../local/extractsequences/tests/main.nf.test | 73 - modules/local/fishertest/environment.yml | 7 +- modules/local/fishertest/main.nf | 22 +- .../fishertest/resources/usr/bin/fisher.R | 195 +- modules/local/grenedalf/frequency.nf | 4 + modules/local/grenedalf/sync.nf | 10 +- modules/local/joinfreq/environment.yml | 6 +- modules/local/joinfreq/main.nf | 16 +- .../joinfreq/resources/usr/bin/joinfreq.R | 130 +- modules/local/poolfstat/fst/environment.yml | 5 +- modules/local/poolfstat/fst/main.nf | 16 +- .../fst/resources/usr/bin/poolfstat.R | 49 +- .../nf-core/bcftools/reheader/environment.yml | 5 + modules/nf-core/bcftools/reheader/main.nf | 79 + modules/nf-core/bcftools/reheader/meta.yml | 76 + .../bcftools/reheader/tests/bcf.config | 4 + .../bcftools/reheader/tests/main.nf.test | 394 ++++ .../bcftools/reheader/tests/main.nf.test.snap | 469 +++++ .../nf-core/bcftools/reheader/tests/tags.yml | 2 + .../bcftools/reheader/tests/vcf.config | 4 + .../bcftools/reheader/tests/vcf.gz.config | 4 + .../reheader/tests/vcf_gz_index.config | 4 + .../reheader/tests/vcf_gz_index_csi.config | 4 + .../reheader/tests/vcf_gz_index_tbi.config | 5 + nextflow.config | 35 +- nextflow_schema.json | 25 +- subworkflows/local/filter.nf | 37 +- .../{filter_sim_vcf => filter_sim}/main.nf | 8 +- subworkflows/local/filter_sim/meta.yml | 51 + .../tests/main.nf.test | 4 +- subworkflows/local/filter_sim_vcf/meta.yml | 34 - subworkflows/local/poolstats.nf | 89 +- subworkflows/local/postprocess.nf | 71 +- subworkflows/local/preprocess.nf | 108 +- .../utils_nfcore_assesspool_pipeline/main.nf | 33 +- workflows/assesspool.nf | 25 +- 66 files changed, 2576 insertions(+), 2262 deletions(-) delete mode 100644 assets/input.csv create mode 100644 assets/samplesheet.csv delete mode 100644 defaults.yml delete mode 100644 docs/images/logo.png delete mode 100644 modules/local/extractsequences/environment.yml delete mode 100644 modules/local/extractsequences/main.nf delete mode 100644 modules/local/extractsequences/meta.yml delete mode 100755 modules/local/extractsequences/resources/usr/bin/extractsequences.R delete mode 100644 modules/local/extractsequences/tests/main.nf.test create mode 100644 modules/nf-core/bcftools/reheader/environment.yml create mode 100644 modules/nf-core/bcftools/reheader/main.nf create mode 100644 modules/nf-core/bcftools/reheader/meta.yml create mode 100644 modules/nf-core/bcftools/reheader/tests/bcf.config create mode 100644 modules/nf-core/bcftools/reheader/tests/main.nf.test create mode 100644 modules/nf-core/bcftools/reheader/tests/main.nf.test.snap create mode 100644 modules/nf-core/bcftools/reheader/tests/tags.yml create mode 100644 modules/nf-core/bcftools/reheader/tests/vcf.config create mode 100644 modules/nf-core/bcftools/reheader/tests/vcf.gz.config create mode 100644 modules/nf-core/bcftools/reheader/tests/vcf_gz_index.config create mode 100644 modules/nf-core/bcftools/reheader/tests/vcf_gz_index_csi.config create mode 100644 modules/nf-core/bcftools/reheader/tests/vcf_gz_index_tbi.config rename subworkflows/local/{filter_sim_vcf => filter_sim}/main.nf (96%) create mode 100644 subworkflows/local/filter_sim/meta.yml rename subworkflows/local/{filter_sim_vcf => filter_sim}/tests/main.nf.test (95%) delete mode 100644 subworkflows/local/filter_sim_vcf/meta.yml diff --git a/.editorconfig b/.editorconfig index f630cf7..168cbd3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,6 @@ indent_style = space [*.{md,yml,yaml,html,css,scss,js,r,rmd,R,Rmd}] indent_size = 2 -trim_trailing_whitespace = false # These files are edited and tested upstream in nf-core/modules [/modules/nf-core/**] diff --git a/.nf-core.yml b/.nf-core.yml index be29fb5..fd11031 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,14 +1,16 @@ lint: - nextflow_config: - - manifest.homePage + files_exist: + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml files_unchanged: - .github/CONTRIBUTING.md - - assets/email_template.html - assets/sendmail_template.txt - .github/CONTRIBUTING.md - assets/sendmail_template.txt - template_strings: - - assets/assesspool_report.Rmd multiqc_config: false nf_core_version: 3.3.1 repository_type: pipeline diff --git a/CHANGELOG.md b/CHANGELOG.md index acb497e..5a0cd82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ -# assessPool: Changelog +# nf-core/assesspool: Changelog The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## v1.0.0dev - [date] -Initial release of assessPool, created with the [nf-core](https://nf-co.re/) template. +Initial release of nf-core/assesspool, created with the [nf-core](https://nf-co.re/) template. ### `Added` diff --git a/CITATIONS.md b/CITATIONS.md index 564eba3..237ecb4 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,38 +10,6 @@ ## Pipeline tools -- [PoPoolation2](https://sourceforge.net/p/popoolation2/wiki/Tutorial/) - - > Kofler, R., Pandey, R. V., & Schlötterer, C. (2011). PoPoolation2: identifying differentiation between populations using sequencing of pooled DNA samples (Pool-Seq). Bioinformatics, 27(24), 3435-3436. - -- [poolfstat](https://doi.org/10.1111/1755-0998.13557) - - > Gautier, M., Vitalis, R., Flori, L., & Estoup, A. (2022). f‐Statistics estimation and admixture graph construction with Pool‐Seq or allele count data using the R package poolfstat. Molecular Ecology Resources, 22(4), 1394-1416. - -- [grenedalf](https://github.com/lczech/grenedalf) - - > Czech, L., Spence, J. P., & Expósito-Alonso, M. (2024). grenedalf: population genetic statistics for the next generation of pool sequencing. Bioinformatics, 40(8), btae508. - -- [bcftools](https://samtools.github.io/bcftools/bcftools.html) - - > Danecek, P., Bonfield, J. K., Liddle, J., Marshall, J., Ohan, V., Pollard, M. O., ... & Li, H. (2021). Twelve years of SAMtools and BCFtools. Gigascience, 10(2), giab008. - -- [vcftools](https://vcftools.github.io/) - - > Danecek, P., Auton, A., Abecasis, G., Albers, C. A., Banks, E., DePristo, M. A., ... & 1000 Genomes Project Analysis Group. (2011). The variant call format and VCFtools. Bioinformatics, 27(15), 2156-2158. - -- [samtools](https://samtools.github.io/) - - > Danecek, P., Bonfield, J. K., Liddle, J., Marshall, J., Ohan, V., Pollard, M. O., ... & Li, H. (2021). Twelve years of SAMtools and BCFtools. Gigascience, 10(2), giab008. - -- [Rsamtools](https://www.bioconductor.org/packages/release/bioc/html/Rsamtools.html) - - > Morgan M, Pagès H, Obenchain V, Hayden N (2024). _Rsamtools: Binary alignment (BAM), FASTA, variant call (BCF), and tabix - file import_. doi:10.18129/B9.bioc.Rsamtools , R package version 2.22.0, - . - - - ## Software packaging/containerisation tools - [Anaconda](https://anaconda.com) diff --git a/LICENSE b/LICENSE index af4672c..eff90ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) The assessPool team +Copyright (c) The nf-core/assesspool team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e27f8b6..814bba1 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,64 @@

- - assessPool + + nf-core/assesspool

-[![Nextflow](https://img.shields.io/badge/nextflow-%E2%89%A50.24.04.2-brightgreen.svg)](https://www.nextflow.io/) +[![GitHub Actions CI Status](https://github.com/nf-core/assesspool/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/assesspool/actions/workflows/ci.yml) +[![GitHub Actions Linting Status](https://github.com/nf-core/assesspool/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/assesspool/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/assesspool/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) +[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) + +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A524.04.2-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) +[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.3.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.3.1) +[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) +[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/) +[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/) +[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/assesspool) + +[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23assesspool-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/assesspool)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) ## Introduction -**assessPool** is a population genetics analysis pipeline designed for pooled sequencing runs (pool-seq). Starting from raw genomic variants (VCF or sync format), assessPool performs the following operations: - - * Filters SNPs based on adjustable criteria with suggestions for pooled data - * Calculates population genetic statistics using [PoPoolation2](https://sourceforge.net/p/popoolation2/wiki/Main/), [poolfstat](https://doi.org/10.1111/1755-0998.13557), and/or [grenedalf](https://github.com/lczech/grenedalf). - * Generates an HTML report including visualizations of population genetic statistics - * Outputs results in tabular format for downstream analyses - -Required inputs are a variant description file (sync or VCF) and a reference assembly (FASTA). These can be output from any number of reduced representation data processing pipelines (e.g., [grenepipe](https://github.com/moiexpositoalonsolab/grenepipe), [dDocent](https://ddocent.com/), etc.). - -Major pipeline operations: - -1. Import, index, and/or compress variant description and reference -1. Perform stepwise filtering to determine effects of individual filter options (count lost loci): - 1. Min/max read depth - 1. Minor allele count - 1. Hardy-Weinberg equilibrium cutoff - 1. Missing data - 1. Allele length - 1. Quality:depth ratio - 1. Minimum read quality - 1. Variant type - 1. Mispaired read likelihood - 1. Alternate observations - 1. Mapping quality - 1. Mapping ratio - 1. Overall depth - 1. Number of pools with data - 1. Read balance - 1. Variant thinning -1. Perform cumulative filtering (for VCF input) -1. Generate sync files - 1. Unified (all pools) - 1. Split pairwise -1. Generate allele frequency table -1. Calculate Fst - 1. PoPoolation2 - 1. {poolfstat} - 1. grenedalf -1. Calculate Fisher's exact test for individual SNPs - 1. PoPoolation2 - 1. assessPool native -1. Join frequency data to Fst results -1. Extract contigs containing (user-configurable) strongly-differentiated loci -1. Create HTML report -1. Save all output data in tabular format for downstream analysis +**nf-core/assesspool** is a bioinformatics pipeline that ... + + + ## Usage > [!NOTE] > If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data. -First, prepare a sample sheet with your input data that looks as follows: + Now, you can run the pipeline using: + + ```bash -nextflow run tobodev/assesspool \ +nextflow run nf-core/assesspool \ -profile \ --input samplesheet.csv \ --outdir @@ -96,47 +67,41 @@ nextflow run tobodev/assesspool \ > [!WARNING] > Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files). -For more details and further functionality, please refer to the [usage documentation](docs/usage.md) and the [parameter documentation](parameters.md). - -## Testing the pipeline -assessPool comes with two built-in profiles that allow the user to test the pipeline with a fully-functional input dataset. These profiles (whose descriptions can be found in [conf/test.confg](conf/test.config) and [conf/test_full.config](conf/test_full.config)) will run assessPool with either a full or reduced dataset of SNPs sequenced from wild populations of the coral *Montipora capitata*. Pipeline tests can be run by passing either `test` or `test_full` to the `-profile` option, along with a software/container management subsystem. For example, using singularity: - -``` -nextflow run tobodev/assesspool \ - -profile test,singularity -``` -or -``` -nextflow run tobodev/assesspool \ - -profile test_full,singularity -``` +For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/assesspool/usage) and the [parameter documentation](https://nf-co.re/assesspool/parameters). ## Pipeline output -Results of an example test run with a full size dataset can be found [here](https://tobodev.github.io/assesspool/). +To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/assesspool/results) tab on the nf-core website pipeline page. For more details about the output files and reports, please refer to the -[output documentation](docs/output.md). +[output documentation](https://nf-co.re/assesspool/output). ## Credits -assessPool was originally written by Evan B Freel, Emily E Conklin, Mykle L Hoban, Derek W Kraft, Jonathan L Whitney, Ingrid SS Knapp, Zac H Forsman, Robert J Toonen. +nf-core/assesspool was originally written by Evan B Freel, Emily E Conklin, Mykle L Hoban, Derek W Kraft, Jonathan L Whitney, Ingrid SS Knapp, Zac H Forsman, Robert J Toonen. We thank the following people for their extensive assistance in the development of this pipeline: -Richard Coleman, ʻAleʻalani Dudoit, and Cataixa López, who used assessPool during development and helped identify issues and suggest key feature improvements. We would also like to thank Iliana Baums, Tanya Beirne, Dave Carlon, Greg Conception, Matt Craig, Jeff Eble, Scott Godwin, Matt Iacchei, Frederique Kandel, Steve Karl, Jim Maragos, Bob Moffitt, Joe O'Malley, Lawrie Provost, Jennifer Salerno, Derek Skillings, Michael Stat, Ben Wainwright, and Kim Weersing, for their efforts in sample collection. + ## Contributions and Support If you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md). +For further information or help, don't hesitate to get in touch on the [Slack `#assesspool` channel](https://nfcore.slack.com/channels/assesspool) (you can join with [this invite](https://nf-co.re/join/slack)). + ## Citations -A list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. + + + + + +An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. -This pipeline uses code and infrastructure developed and maintained by the [nf-core](https://nf-co.re) community, reused here under the [MIT license](https://github.com/nf-core/tools/blob/master/LICENSE). +You can cite the `nf-core` publication as follows: -> The nf-core framework for community-curated bioinformatics pipelines. +> **The nf-core framework for community-curated bioinformatics pipelines.** > > Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen. > -> Nat Biotechnol. 2020 Feb 13. doi: 10.1038/s41587-020-0439-x. +> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x). diff --git a/assets/adaptivecard.json b/assets/adaptivecard.json index c0b10f0..b5fc0c0 100644 --- a/assets/adaptivecard.json +++ b/assets/adaptivecard.json @@ -17,7 +17,7 @@ "size": "Large", "weight": "Bolder", "color": "<% if (success) { %>Good<% } else { %>Attention<%} %>", - "text": "assessPool v${version} - ${runName}", + "text": "nf-core/assesspool v${version} - ${runName}", "wrap": true }, { diff --git a/assets/assesspool_report.Rmd b/assets/assesspool_report.Rmd index 979812a..e1d7549 100644 --- a/assets/assesspool_report.Rmd +++ b/assets/assesspool_report.Rmd @@ -11,8 +11,8 @@ output: df_print: paged # tables are printed as an html table with support for pagination over rows and columns highlight: pygments pdf_document: true - self_contained: false - lib_dir: artifacts + pdf_document: + toc: yes date: "`r Sys.Date()`" params: nf: null @@ -24,86 +24,6 @@ params: fisher: null tz: null --- -```{css, echo=FALSE} -.sliderbar { - position: fixed; - bottom: 40%; - margin-left: 0%; - padding: 10px; -} -.ui-slider-wrapper { - width: 95% -} -``` - - - - - - - - - - - ```{r setup, echo=FALSE, include=FALSE} # when run in a container, there are # weird issues with the timezone and lubridate @@ -130,13 +48,16 @@ if (!is.null(params$tz)) { # load libraries library(tidyr) -library(data.table) library(knitr) +library(yaml) library(plotly) library(DT) +library(readr) +library(dplyr) library(stringr) library(purrr) library(paletteer) +library(fs) library(htmltools) if (params$debug) { @@ -145,6 +66,7 @@ if (params$debug) { # set some global options knitr::opts_chunk$set(echo = FALSE, warning = FALSE) +options(dplyr.summarise.inform = FALSE) ``` ```{r init, include=FALSE} @@ -156,6 +78,19 @@ datatable( # load the plotly js plot_ly() +# plotly horizontal line +hline <- function(y = 0, color = "black") { + list( + type = "line", + x0 = 0, + x1 = 1, + xref = "paper", + y0 = y, + y1 = y, + line = list(color = color) + ) +} + # geometric mean function gm_mean = function(x, na.rm=TRUE, zero.propagate = FALSE){ if(any(x < 0, na.rm = TRUE)){ @@ -172,19 +107,13 @@ gm_mean = function(x, na.rm=TRUE, zero.propagate = FALSE){ } # get a sequence of coverage cutoffs from input parameters -cutoffs <- seq(params$nf$min_coverage_cutoff,params$nf$max_coverage_cutoff,params$nf$coverage_cutoff_step) +cutoffs <- seq(params$nf$report_min_coverage,params$nf$report_max_coverage,params$nf$report_coverage_step) # load pool data # start with fst show_fst <- !is.null(params$fst) if (show_fst) { - pool_data <- fread(params$fst) - pool_data[,pair := paste0(pop1," - ",pop2)] - pool_data[,`:=`( - pop1 = factor(pop1), - pop2 = factor(pop2) - )] - setorder(pool_data,method,chrom,pos) + pool_data <- read_tsv(params$fst) } else { pool_data <- FALSE } @@ -194,116 +123,125 @@ fisher_data <- NULL # load fisher data if applicable show_fisher <- !is.null(params$fisher) if (show_fisher) { - fisher_data <- fread(params$fisher) - fisher_data[,c(names(fisher_data)[!names(fisher_data) %in% c('chrom','pos','pop1','pop2','avg_min_cov','log_fisher','method')]) := NULL] - # setnames(fisher_data,'fisher','log_fisher') - setcolorder(fisher_data,c('chrom','pos','pop1','pop2','avg_min_cov','log_fisher','method')) - setorder(fisher_data,method,chrom,pos) + fisher_data <- read_tsv(params$fisher) %>% + select(chrom,pos,pop1,pop2,avg_min_cov,log_fisher=fisher,method) } # whether to show VCF filtering show_filter <- params$nf$visualize_filters & !is.null(params$filter) if (show_filter) { - filters <- fread(params$filter,col.names = c('filter','count')) + filters <- read_tsv(params$filter) } else { filters <- NULL } -# whether to show cumulative filtering show_final_filter <- params$nf$visualize_filters & !is.null(params$final_filter) -if (show_final_filter) { - final_filters <- fread(params$final_filter,col.names = c('filter','count')) +if (show_final_filter) { + final_filters <- read_tsv(params$final_filter) } else { final_filters <- NULL } -# whether to show any filtering at all any_filter <- show_filter | show_final_filter # used for tab selection -pools_shared <- c( - 'All SNPs'=FALSE, - 'SNPs shared by all pools'=TRUE -) +all_pools <- c('All SNPs'=FALSE,'Shared loci (all pools)'=TRUE) + +# place where stuff gets saved +data_dir <- "artifacts" +# create artifacts dir +# if it already exists nobody will care +dir_create(data_dir) + +# move fst file into artifacts dir +# file_move(params$fst,data_dir) ``` + +```{r, results='asis', eval=params$debug} +cat("# Debug info\n") +``` + +```{r, eval=params$debug} +if (params$debug) { + # print(params$meta$pools) + cat("params\n\n") + str(params) +} +``` + + # Abstract This report summarizes the output of population genetic analyses of a pool-seq RAD library. - -```{r filtering, results='asis', eval=any_filter} +```{r, results='asis', eval=any_filter} # TODO: show filter option values on plot/table cat("# Filtering {.tabset}\n") -# filtering tabs filter_headers <- c( "## Stepwise filtering reuslts", "## Cumulative filtering results" ) -# notes to display in filter tabs filter_notes <- c( 'Note: filtering results displayed are independent, not cumulative.', - 'Cumulative filter results
(Note: filtered SNP count may differ from SNP counts associated with Fst calculations).' + 'Cumulative filter results' ) -# list of filter data to use in the loop below report_data <- list(filters,final_filters) -# walk through different filter tabs c(show_filter,show_final_filter) %>% iwalk(\(show_report,i) { if (show_report) { # show tab header cat(filter_headers[i],"\n\n") - + # get relevant filtering data - filter_data <- report_data[[i]][order(-count)] - - # show some header info and notes + filter_data <- report_data[[i]] + print( tagList( h5('SNPs remaining after each filtering step.'), h5(tags$small(HTML(filter_notes[i]))) ) ) - - # rearrange filter data a bit - # make sure 'Unfiltered' is in bold and comes first - filter_data[,filter := replace(filter,which(filter == "before"),"Unfiltered")] - filter_data[,filter := replace(filter,which(filter == "cumulative"),"All filters")] - filter_data[,filter := forcats::fct_reorder(filter,-count)] - filter_data[,filter := forcats::fct_relevel(filter,"Unfiltered")] - filter_data[,filtered := count[filter == "Unfiltered"] - count] - filter_data[,filtered := ifelse(filter == "Unfiltered",count,filtered)] - filter_data[,changed := filtered > 0] - - # figure out which options did nothing - changed <- filter_data[changed == TRUE] - unchanged <- as.character(filter_data[changed == FALSE]$filter) - unchanged <- str_flatten_comma(unchanged) - # show options that didn't do anything + filter_data <- filter_data %>% + arrange(desc(count)) %>% + mutate( + filter = replace(filter,which(filter == "before"),"Unfiltered"), + filter = replace(filter,which(filter == "cumulative"),"All filters"), + filter = forcats::fct_reorder(filter,-count), + filter = forcats::fct_relevel(filter,"Unfiltered"), + filtered = count[filter == "Unfiltered"] - count, + filtered = case_when( + filter == "Unfiltered" ~ count, + .default = filtered + ), + changed = filtered > 0 + ) #%>% + # filter(filtered > 0) + changed <- filter_data %>% + filter(changed) + unchanged <- filter_data %>% + filter(!changed) %>% + pull(filter) %>% + as.character() + unchanged <- str_glue("{unchanged}") + unchanged <- str_c(unchanged,collapse=", ") if (unchanged != "") { print(h5(tags$small(HTML(str_glue( "Filter options with no effect: {unchanged}." ))))) } - - # fixed-width font for tooltip + font <- list( family = "Courier New", size = 14 ) - - # quick formatting function + label <- list( font = font ) num <- scales::comma - # tooltip template t_fmt <- 'Before filtering: {num(count[filter == "Unfiltered"])} SNPs
After filtering: {num(count)} SNPs
' - - # make the bar chart - # we use ggplot because it's better at doing continuous color scales - # for bar plots, then convert it to a plotly plot after the fact fig <- ggplot(changed) + geom_bar(aes(x=filter,y=count,fill=count,text=str_glue(t_fmt)),stat="identity") + scale_fill_paletteer_c("grDevices::Turku",direction = -1,name="SNPs remaining",labels=scales::comma) + @@ -313,7 +251,6 @@ c(show_filter,show_final_filter) %>% axis.title.x = element_text(margin = margin(t = -20)), plot.title = element_text(hjust = 0.5), plot.margin = unit(c(0.2,0.2,0.2,0.2), "cm")) - # plotly-ize it and do a little configuration fig <- fig %>% ggplotly(tooltip="text") %>% config(displayModeBar = FALSE) %>% @@ -321,454 +258,484 @@ c(show_filter,show_final_filter) %>% xaxis = list(title = "Filter option", fixedrange = TRUE), yaxis = list(title = "SNPs remaining", fixedrange = TRUE) ) %>% - style(hoverlabel = list(font = font)) + style(hoverlabel = label) print(tagList(fig)) - } + } }) ``` - -```{r summaries, results='asis', eval=show_fst} +```{r, summaries, results='asis', eval=show_fst} cat("# High-level summaries {.tabset}\n\n") -nothing <- pool_data[,{ - fst_data <- .SD - cat(str_glue("## {method} {{.tabset}}\n\n")) +summary_datasets <- pool_data %>% + group_by(method) %>% + group_walk(\(fst_data,method) { + # output tab header + cat(str_glue("## {method$method} {{.tabset}}\n\n")) - pools_shared %>% - iwalk(\(shared,hdr) { - cat(str_glue("### {hdr} {{.tabset}}\n\n")) + # walk through all_shared elements + # (whether or not we're looking only at snps shared across all pools) + all_pools %>% + iwalk(\(all_shared,hdr) { + # output tab header + cat(str_glue("### {hdr} {{.tabset}}\n\n")) - # filter data for all pools if necessary - if (shared) { - fst_data <- fst_data[ fst_data[, .I[all( unique(c(levels(fst_data$pop1),levels(pop2))) %in% c(pop1,pop2) )], by=c('chrom','pos')]$V1 ] - setorder(fst_data,chrom,pos,pop1,pop2) - } + # filter data for all pools if necessary + if (all_shared) { + fst_data <- fst_data %>% + mutate(pop1=factor(pop1),pop2=factor(pop2)) %>% + group_by(chrom,pos) %>% + filter( all( unique(c(levels(pop1),levels(pop2))) %in% c(pop1,pop2) ) ) %>% + ungroup() %>% + arrange(chrom,pos,pop1,pop2) + } - fst_cov <- cutoffs %>% - map(\(cutoff) { - fst_data[avg_min_cov >= cutoff][,cutoff := cutoff][,{ - fst_sd <- sd(fst,na.rm=TRUE) - fst_se <- fst_sd / sqrt(.N) - n_snps <- uniqueN(.SD[,.(chrom,pos)],na.rm=TRUE) - n_contigs <- uniqueN(.SD$chrom,na.rm=TRUE) - fst_mean <- mean(fst,na.rm=TRUE) - .( - fst = fst_mean, - fst_sd = fst_sd, - snps = n_snps, # snp count - contigs = n_contigs, - snps_per_contig = n_snps / n_contigs - ) - },by=cutoff] - }) %>% - rbindlist() - setorder(fst_cov,cutoff) - setcolorder(fst_cov,c('cutoff','fst','fst_sd','snps','contigs','snps_per_contig')) - setnames( fst_cov, - c('cutoff','fst','fst_sd','snps','contigs','snps_per_contig'), - c('Coverage cutoff', 'Mean Fst', 'Fst (SD)', 'SNPs', 'Contigs', 'SNPs per contig') - ) - - cat(str_glue("#### Plot\n\n")) - print(h5("Global summary statistics by coverage cutoff")) - - fig <- fst_cov %>% - plot_ly() %>% - add_trace( - name = 'Mean Fst', - type = 'scatter', - mode = 'lines+markers', - visible=TRUE, - x = ~`Coverage cutoff`, - y = ~`Mean Fst`, - error_y = ~list( - type='data', - array = `Fst (SD)` - ) - ) %>% - add_trace( - name = 'SNPs', - type = 'scatter', - mode = 'lines+markers', - visible=FALSE, - x = ~`Coverage cutoff`, - y = ~`SNPs` - ) %>% - add_trace( - name = 'SNPs per contig', - type = 'scatter', - mode = 'lines+markers', - visible=FALSE, - x = ~`Coverage cutoff`, - y = ~`SNPs per contig` - ) %>% - add_trace( - name = 'Contigs', - type = 'scatter', - mode = 'lines+markers', - visible=FALSE, - x = ~`Coverage cutoff`, - y = ~`Contigs` - ) - - button_layout <- list( - type = "buttons", - direction = "right", - xanchor = 'center', - yanchor = "top", - pad = list('r' = 0, 't' = 10, 'b' = 10), - x = 0.5, - y = 1.27, - buttons = list( - list( - method = "update", - args = list( - list( - visible = list( - T, F, F, F + # repeatedly filter the dataset for different coverage levels + # and bind those together into a new dataset + fst_cov <- cutoffs %>% + map(\(cov) { + fst_data %>% + # filter by average minimum coverage + filter(avg_min_cov >= cov) %>% + # retain the cutoff level in the data + mutate(cutoff=cov) + }) %>% + # and bind resulting subsets together into a superset + list_rbind() + + # summarize the data by comparison and cutoff level + fst_grp <- fst_cov %>% + group_by(cutoff) %>% + summarise( + fst_sd = sd(fst), # std. dev. of fst + fst = mean(fst), # mean fst + fst_se = fst_sd / sqrt(n()), # std. err. of fst + cov = mean(avg_min_cov), # mean coverage + snps = n_distinct(chrom,pos,na.rm = TRUE), # snp count + contigs = n_distinct(chrom,na.rm=TRUE), # contig count + snps_per_contig = snps / contigs, # mean snps per contig + ci_lower= fst - qt(0.975, n() -1) * fst_se, + ci_upper= fst + qt(0.975, n() -1) * fst_se + ) %>% + arrange(cutoff) %>% + ungroup() %>% + select( + `Coverage cutoff`=cutoff, + `Mean Fst`=fst, + `Fst (SD)`=fst_sd, + `Fst (SEM)`=fst_se, + `Mean coverage`=cov, + SNPs=snps, + Contigs=contigs, + `SNPs per contig`=snps_per_contig, + `CI (lower)`=ci_lower, + `CI (upper)`=ci_upper + ) + + shared <- ifelse(all_shared,"all","shared") + filename <- str_glue("{method$method}_{shared}_summary") + + # a javascript callback that rounds numbers to the + # nearest thousand. otherwise the table is full of stuff like 0.3412341234123412341234 + js <- DT::JS( + "function ( data, type, row, meta ) {", + "return type === 'display' ?", + "''+(Math.round((data+Number.EPSILON)*1000)/1000).toLocaleString()+'' :", + "data;", + "}" + ) + + # show plot table + cat(str_glue("#### Plot\n\n")) + + print(h5("Global summary statistics by coverage cutoff")) + + # TODO: use error_y for SD of fst + # plot_ly( + # x = ~cov, + # y = ~fst, + # type='scatter', + # mode='markers+lines', + # error_y = ~list( + # type='data', + # array = fst_sd + # ) + # ) + + # create interactive plot and add traces + fig <- fst_grp %>% + plot_ly( + y = ~ `Mean Fst`, + x = ~`Coverage cutoff`, + name = "Mean Fst", + visible = T, + mode = "lines+markers", + type = "scatter" + ) %>% + add_trace( + y = ~ `Fst (SD)`, + x = ~`Coverage cutoff`, + name = "Fst (SD)", + visible = F, + mode = "lines+markers", + type = "scatter" + ) %>% + add_trace( + y = ~SNPs, + x = ~`Coverage cutoff`, + name = "SNPs", + mode = "lines+markers", + type = "scatter", + visible=F + ) %>% + add_trace( + y = ~`SNPs per contig`, + x = ~`Coverage cutoff`, + name = "Mean SNPs per contig", + visible = F, + mode = "lines+markers", + type = "scatter" + ) %>% + add_trace( + y = ~Contigs, + x = ~`Coverage cutoff`, + name = "Contigs", + visible = F, + mode = "lines+markers", + type = "scatter" + ) + + # configure button layout and behavior + button_layout <- list( + type = "buttons", + direction = "right", + xanchor = 'center', + yanchor = "top", + pad = list('r' = 0, 't' = 10, 'b' = 10), + x = 0.5, + y = 1.27, + buttons = list( + list( + method = "update", + args = list( + list( + visible = list( + T, F, F, F, F + ) + ), + list( + yaxis = list(title = "Mean Fst") ) ), - list( - yaxis = list(title = "Mean Fst") - ) + label = "Fst" ), - label = "Fst" - ), - list( - method = "update", - args = list( - list( - visible = list( - F, T, F, F + list( + method = "update", + args = list( + list( + visible = list( + F, T, F, F, F + ) + ), + list(yaxis = list(title = "Fst (standard deviation)") ) ), - list(yaxis = list(title = "SNPs") - ) + label = "Fst (SD)" ), - label = "SNPs" - ), - list( - method = "update", - args = list( - list( - visible = list( - F, F, T, F + list( + method = "update", + args = list( + list( + visible = list( + F, F, T, F, F + ) + ), + list(yaxis = list(title = "SNPs") ) ), - list( - yaxis = list(title = "Mean SNPs per contig") - ) + label = "SNPs" ), - label = "SNPs per contig" - ), - list( - method = "update", - args = list( - list( - visible = list( - F, F, F, T + list( + method = "update", + args = list( + list( + visible = list( + F, F, F, T, F + ) + ), + list( + yaxis = list(title = "Mean SNPs per contig") ) ), - list( - yaxis = list(title = "Number of contigs") - ) + label = "SNPs per contig" ), - label = "Contigs" + list( + method = "update", + args = list( + list( + visible = list( + F, F, F, F, T + ) + ), + list( + yaxis = list(title = "Number of contigs") + ) + ), + label = "Contigs" + ) ) ) - ) - - # configure annotation properties - annotation <- list( - list( - text = "Statistic", - x = .15, - y = 1.22, - xref = 'paper', - yref = 'paper', - showarrow = FALSE + + # configure annotation properties + annotation <- list( + list( + text = "Statistic", + x = .15, + y = 1.22, + xref = 'paper', + yref = 'paper', + showarrow = FALSE + ) ) - ) - - # add layout and annotation to figure - fig <- fig %>% - layout( + + # add layout and annotation to figure + fig <- fig %>% layout( yaxis = list(title = "Mean Fst",fixedrange=TRUE), xaxis = list(title = "Coverage cutoff",fixedrange=TRUE), - title = list(text=str_glue("Summary statistics by coverage cutoff - {method}"), yref= 'paper', y=1, font=list(size=12)), + title = list(text=str_glue("Summary statistics by coverage cutoff - {method$method}"), yref= 'paper', y=1), showlegend = F, updatemenus = list(button_layout), annotations = annotation ) %>% - config(displayModeBar = FALSE) + config(displayModeBar = FALSE) - # display figure - print(tagList(fig)) - - # show data table tab - cat(str_glue("#### Table\n\n")) - - # construct csv download filename - shared <- ifelse(shared,"all","shared") - filename <- str_glue("{method}_{shared}_summary") - - # a javascript callback that rounds numbers to the - # nearest thousand. otherwise the table is full of stuff like 0.3412341234123412341234 - js <- DT::JS( - "function ( data, type, row, meta ) {", - "return type === 'display' ?", - "''+(Math.round((data+Number.EPSILON)*1000)/1000).toLocaleString()+'' :", - "data;", - "}" - ) - - print(h5("Global summary statistics by coverage cutoff")) - - # output the datatable - print( - tagList( - datatable( - fst_cov, - escape = FALSE, - rownames = FALSE, - filter = "none", - autoHideNavigation = TRUE, - extensions = 'Buttons', - options = list( - dom = 'tB', - buttons = list( - list( - extend='csv', - title=filename, - exportOptions = list( modifier = list(page = "all"), orthogonal = "export" + # display figure + print(tagList(fig)) + + # show data table tab + cat(str_glue("#### Table\n\n")) + + print(h5("Global summary statistics by coverage cutoff")) + + # output the datatable + print( + tagList( + datatable( + fst_grp, + escape = FALSE, + rownames = FALSE, + filter = "none", + autoHideNavigation = TRUE, + extensions = 'Buttons', + options = list( + dom = 'tB', + buttons = list( + list( + extend='csv', + title=filename, + exportOptions = list( modifier = list(page = "all"), orthogonal = "export" + ) + ) + ), + scrollX = TRUE, + columnDefs = list( + list( + targets = c(2:5,8:10)-1, + render = js ) - ) - ), - scrollX = TRUE, - columnDefs = list( - list( - targets = c(2:3)-1, - render = js ) ) - ) - ) %>% - formatRound(c(1,4:6),digits=0) + ) %>% + formatRound(c(1,6,7),digits=0) + ) ) - ) - - - }) - -},by=method] + }) + }) ``` + ```{r fst-cov-all, results='asis', eval=show_fst} cat("# Pairwise Fst by minimum coverage {.tabset}\n\n") +# TODO: tie slider position in this plot to heatmap coverage slider +# (corresponding to method, shared snps, etc.) + # walk through calculation methods -nothing <- pool_data[,{ - # output tab header - cat(str_glue("## {method} {{.tabset}}\n\n")) - - fst_data <- .SD - - # walk through whether or not we're looking only at snps shared across all pools - pools_shared %>% - iwalk(\(shared,hdr) { - # output tab header - cat(str_glue("### {hdr} {{.tabset}}\n\n")) - shr <- ifelse(shared,"shared","all") - - # filter data for all pools if necessary - if (shared) { - fst_data <- fst_data[ fst_data[, .I[all( unique(c(levels(fst_data$pop1),levels(pop2))) %in% c(pop1,pop2) )], by=c('chrom','pos')]$V1 ] - setorder(fst_data,chrom,pos,pop1,pop2) - } - - # calculate plot x-axis range - # basically, get the min and max mean fst across all coverage cutoffs - # we're doing this so all the different versions of the plot have the - # same x-axis. (it might be a bit data intensive but worthwhile) - xrange <- reduce(cutoffs,\(range,cutoff) { - r <- range(fst_data[avg_min_cov >= cutoff][,.(fst = mean(fst,na.rm=TRUE)),by=.(pop1,pop2)]$fst) - c(min(range[1],r[1]),max(range[2],r[2])) - },.init = c(0,0) ) - xrange <- c(min(xrange[1]-abs(0.1*xrange[1]),0),min(xrange[2]+abs(0.1*xrange[2]),1)) - - # get initial figures and output json script tags - fst_figs <- cutoffs %>% - reduce(\(figs,cutoff) { - # summarize the data by comparison and cutoff level - # precompute box plot stats because the datasets are too big - # and pandoc runs out of memory - fst_grp <- fst_data[avg_min_cov >= cutoff][,{ - fst_sd <- sd(fst,na.rm=TRUE) - n_snps <- uniqueN(.SD[,.(chrom,pos)],na.rm=TRUE) - n_contigs <- uniqueN(.SD$chrom,na.rm=TRUE) - .( - fst_sd = fst_sd, # std. dev. of fst - q1 = as.numeric(quantile(fst,na.rm=TRUE)[2]), - q3 = as.numeric(quantile(fst,na.rm=TRUE)[4]), - lwr = boxplot.stats(fst)$stats[1], - upr = boxplot.stats(fst)$stats[5], - med = median(fst,na.rm=TRUE), - min = min(fst,na.rm=TRUE), - max = max(fst,na.rm=TRUE), - fst_se = fst_sd / sqrt(.N), # std. err. of fst - fst = mean(fst,na.rm=TRUE), # mean fst - cov = mean(avg_min_cov,na.rm=TRUE), # mean coverage - snps = n_snps, # snp count - contigs = n_contigs, # contig count - snps_per_contig = n_snps / n_contigs # mean snps per contig - ) - },by=.(pop1,pop2,pair)] - setorder(fst_grp,pair) - - # plot the scatterplot - fig <- fst_grp %>% - plot_ly( - x = ~ fst, - y = ~ pair, - color = ~snps, - # frame = ~str_glue("≥{cutoff}"), - # hover text templates - text = ~str_c(snps," SNPs
",contigs," contigs"), - hovertemplate = "Fst (%{y}): %{x}
%{text}", - type = 'scatter', - mode = 'markers', - marker = list(size = 12) - ) %>% - colorbar(title = "SNPs") %>% - layout( - margin=list(l = 100, r = 20, b = 10, t = 30), - yaxis = list(title="",tickangle=-35,tickfont=list(size=12)), - xaxis = list(title="Fst",tickfont=list(size=12), range=as.list(xrange)), - title = list(text=str_glue("Fst - {hdr} (coverage ≥{cutoff})"), y=0.95, font=list(size=12)) - ) %>% - config(toImageButtonOptions = list( - format = 'svg', - width = 900, - height = 600 - )) - - # output the figure to html - if (is.null(figs$scatter_fig)) { - # assign div id to figure - fig_id <- str_glue("fst-fig-{method}-{shr}") - fig$elementId <- fig_id - - figs$scatter_fig <- tagList( - div( - `data-fig`=fig_id, - `data-json-prefix`=str_glue("scatter-json-{method}-{shr}"), - class="fig-container", - tagList(fig) - ) +pool_data %>% + group_by(method) %>% + group_walk(\(fst_data,method) { + # output tab header + cat(str_glue("## {method$method} {{.tabset}}\n\n")) + + # walk through all_shared elements + # (whether or not we're looking only at snps shared across all pools) + all_pools %>% + iwalk(\(all_shared,hdr) { + # output tab header + cat(str_glue("### {hdr} {{.tabset}}\n\n")) + + # filter data for all pools if necessary + if (all_shared) { + # get all unique pool names + all_pools <- sort( + union( + unique(fst_data$pop1), + unique(fst_data$pop2) ) - } - - # get and print the json for the figure - fig_json <- fig %>% - plotly_json(jsonedit = FALSE) %>% - jsonlite::minify() - json_tag <- tags$script( - id = str_glue("scatter-json-{method}-{shr}-{cutoff}"), - type = 'application/json', - HTML(fig_json) ) - print(json_tag) - - # get outlier points, limit to a subsample of 30 per pair because - # too many makes the thing crash. - outs <- fst_data[avg_min_cov >= cutoff][ ,{ - outliers <- boxplot.stats(fst)$out - .( outs = sample(outliers,min(length(outliers),30)) ) - },by=.(pop1,pop2,pair)] - - # generate boxplot figure - fig <- fst_grp %>% - plot_ly( - type = "box", - y = ~pair, - q1 = ~q1, - q3 = ~q3, - median = ~med, - lowerfence = ~lwr, - upperfence = ~upr - ) %>% - add_trace( - data = outs, - inherit = FALSE, - y = ~pair, - x = ~outs, - type="scatter", - mode="markers", - frame = ~str_glue("≥{cutoff}") - ) %>% - layout(showlegend = FALSE) %>% - layout( - yaxis = list(tickangle=-35,tickfont=list(size=12),title=""), - xaxis = list(tickfont=list(size=12),title="Fst"), - title = list(text=str_glue("Fst distribution - {hdr} (coverage ≥{cutoff})"), y=0.95, font=list(size=12)) - ) %>% - config(toImageButtonOptions = list( - format = "svg", - width = 900, - height = 600 - )) - - # save figure for output - if (is.null(figs$box_fig)) { - fig_id <- str_glue("box-fig-{method}-{shr}") - fig$elementId <- fig_id - figs$box_fig <- tagList( - div( - `data-fig`=fig_id, - `data-json-prefix`=str_glue("box-json-{method}-{shr}"), - class="fig-container", - tagList(fig) - ) - ) - } - - # get/print figure json - fig_json <- fig %>% - plotly_json(jsonedit = FALSE) %>% - jsonlite::minify() - json_tag <- tags$script( - id = str_glue("box-json-{method}-{shr}-{cutoff}"), - type = 'application/json', - HTML(fig_json) + + # group data by snp (chrom/pos) and retain + # snps where all populations are represented in comparisons + # (either side) + fst_data <- fst_data %>% + group_by(chrom,pos) %>% + filter(all(all_pools %in% unique(c(unique(pop1),unique(pop2))))) %>% + ungroup() + } + + + + # repeatedly filter the dataset for different coverage levels + # and bind those together into a new dataset + fst_cov <- cutoffs %>% + map(\(cov) { + fst_data %>% + # filter by average minimum coverage + filter(avg_min_cov >= cov) %>% + # retain the cutoff level in the data + mutate(cutoff=cov) + }) %>% + # and bind resulting subsets together into a superset + list_rbind() %>% + # create a text representation of the comparison + mutate(pair = str_glue("{pop1} - {pop2}")) + + # summarize the data by comparison and cutoff level + # precompute box plot stats because the datasets are too big + # and pandoc runs out of memory + fst_grp <- fst_cov %>% + group_by(pop1,pop2,pair,cutoff) %>% + summarise( + fst_sd = sd(fst), # std. dev. of fst + q1 = as.numeric(quantile(fst)[2]), + q3 = as.numeric(quantile(fst)[4]), + lwr = boxplot.stats(fst)$stats[1], + upr = boxplot.stats(fst)$stats[5], + med = median(fst), + min = min(fst), + max = max(fst), + fst = mean(fst), # mean fst + fst_se = fst_sd / sqrt(n()), # std. err. of fst + cov = mean(avg_min_cov), # mean coverage + snps = n_distinct(chrom,pos,na.rm=TRUE), # snp count + contigs = n_distinct(chrom,na.rm=TRUE), # contig count + snps_per_contig = snps / contigs # mean snps per contig + ) %>% + # sort by cutoff and comparison + arrange(cutoff,pair) %>% + ungroup() + + # plot scatter plot header + cat("#### Scatter plot (means)\n\n") + + # plot the scatterplot + fig <- fst_grp %>% + plot_ly( + x = ~ fst, + y = ~ pair, + color = ~snps, + frame = ~str_glue("≥{cutoff}"), + # hover text templates + text = ~str_c(snps," SNPs
",contigs," contigs"), + hovertemplate = "Fst (%{y}): %{x}
%{text}", + type = 'scatter', + mode = 'markers', + marker = list(size = 12) + ) %>% + colorbar(title = "SNPs") %>% + layout( + margin=list(l = 100, r = 20, b = 10, t = 30), + yaxis = list(title="",tickangle=-35,tickfont=list(size=12)), + xaxis = list(title="Fst",tickfont=list(size=12)), + title = list(text=str_glue("Fst - {hdr}"), y=0.95) + ) %>% + # customize slider and remove play button + animation_slider(currentvalue = list(prefix = "Minimum coverage: ", font = list(color = "black"))) %>% + animation_button(visible = FALSE) + + # assign div id to figure + shr <- ifelse(all_shared,"shared","all") + fig_id <- str_glue("fst-fig-{method$method}-{shr}") + fig$elementId <- fig_id + + # output the figure to html + print(tagList(fig)) + + # TODO: slider change event + # fig_script <- tags$script( + # HTML(str_glue(' + # $(function() {{ + # $("#{fig_id}").on("plotly_sliderchange",function(e,data) {{ + # var cutoff = parseInt(data.slider.steps[data.slider.active].label.replaceAll(/[^0-9]+/g,"")); + # console.log("Cutoff: " + cutoff); + # }}) + # }}); + # ')) + # ) + # print(fig_script) + + # print box plot header + cat("#### Box plot (distributions)\n\n") + + # get outlier points + outs <- fst_cov %>% + group_by(pop1,pop2,pair,cutoff) %>% + summarise(outs = boxplot.stats(fst)$out) %>% + ungroup() + + # generate boxplot figure + fig <- fst_grp %>% + plot_ly( + type = "box", + y = ~pair, + q1 = ~q1, + q3 = ~q3, + median = ~med, + frame = ~str_glue("≥{cutoff}"), + lowerfence = ~lwr, + upperfence = ~upr + ) %>% + add_trace( + data = outs, + inherit = FALSE, + y = ~pair, + x = ~outs, + type="scatter", + mode="markers", + frame = ~str_glue("≥{cutoff}") + ) %>% + layout(showlegend = FALSE) %>% + animation_slider(currentvalue = list(prefix = "Minimum coverage: ", font = list(color = "black"))) %>% + animation_button(visible = FALSE) %>% + layout( + #margin=list(l = 200, r = 20, b = 80, t = 40), + yaxis = list(tickangle=-35,tickfont=list(size=12),title=""), + xaxis = list(tickfont=list(size=12),title="Fst") ) - print(json_tag) - return(figs) - },.init = list(scatter_fig = NULL, box_fig = NULL)) - - # show scatter plot tab - cat("#### Scatter plot (means)\n\n") - - print(fst_figs$scatter_fig) - - # show box plot tab - cat("#### Box plot (distributions)\n\n") - print(h5(tags$small(HTML( - "Note: subsampled to 30 outliers per pair." - )))) - - print(fst_figs$box_fig) - - }) -},by=method] + print(tagList(fig)) + }) + }) ``` -```{r heatmaps, results='asis', eval=show_fst} +```{r, heatmap-setup, results='asis', eval=show_fst} +# TODO: add all snps/shared loci to heatmap plots cat("# Summary statistic heatmaps {.tabset}\n\n") - -# configure variable pairs for heatmap plots +# a list of parameters we'll use to plot each figure +# each item consists of: display name, bottom-left heatmap, and (optionally) top-right heatmap +# individual heatmap options are: x value, y value, variable name, color palette (from paletteer_c), +# number format (passed to sprintf), and missing color var_pairs <- list( list( name="fst", @@ -819,259 +786,269 @@ cscale <- function(pal,n=10,na="darkgrey",min=0.00001) { }) if (!is.na(na)) { s[[1]][[1]] <- min - s <- c(list(list(0,na), list(min,na)),s) + c(list(list(0,na), list(min,na)),s) } - s } # replace "NA" (not NA) in a string vector fix_na <- function(x,na="n/a") replace(x,which(x == "NA"),na) -# walk through calculation methods -nothing <- pool_data[,{ - # show tab header - fst_data <- .SD - cat(str_glue("## {method} {{.tabset}}\n\n")) - - # walk through shared status - pools_shared %>% - iwalk(\(shared,hdr) { - # show tab header - cat(str_glue("### {hdr} {{.tabset}}\n\n")) - shr <- ifelse(shared,"shared","all") - - # filter data for all pools if necessary - if (shared) { - fst_data <- fst_data[ fst_data[, .I[all( unique(c(levels(fst_data$pop1),levels(pop2))) %in% c(pop1,pop2) )], by=c('chrom','pos')]$V1 ] - setorder(fst_data,chrom,pos,pop1,pop2) - } - - # walk through variable pairs - var_pairs %>% - walk(\(trace_vars) { - # show yet another tab header - cat(str_glue("#### {trace_vars$title}\n\n")) - - print(h5(HTML(trace_vars$description))) - print(h5(tags$small(HTML( - "Note: Grey cells indicate missing data." - )))) - - # generate figure and output - # all necessary figure json - heatmap_fig <- cutoffs %>% - reduce(\(plot_fig,cutoff) { - # summarize by pair and cutoff value - fst_grp <- fst_data[avg_min_cov >= cutoff][,{ - fst_sd <- sd(fst,na.rm=TRUE) - cov_sd <- sd(avg_min_cov,na.rm=TRUE) - n_snps <- uniqueN(.SD[,.(chrom,pos)],na.rm=TRUE) - n_contigs <- uniqueN(.SD$chrom,na.rm=TRUE) - .( - fst_sd = fst_sd, # std. dev. of fst - fst_se = fst_sd / sqrt(.N), # std. err. of fst - fst = mean(fst,na.rm=TRUE), # mean fst - cov = mean(avg_min_cov,na.rm=TRUE), # mean coverage - cov_sd = cov_sd, - cov_se = cov_sd / sqrt(.N), - snps = n_snps, # snp count - contigs = n_contigs, # contig count - snps_per_contig = n_snps / n_contigs # mean snps per contig - ) - },by=.(pop1,pop2)] - fst_grp[,cutoff := cutoff] - - # all this next bit is so that the heatmaps look good - # with all relevant rows of both the upper and lower triangles - - # get all possible pool names - all_pops <- sort(union(unique(fst_grp$pop1),unique(fst_grp$pop2))) - - # create a tibble of all possible combinations (sorted) - full_table <- all_pops %>% - combn(2) %>% - apply(MARGIN = 2,FUN=\(x) sort(x)) %>% - t() %>% - as.data.table() - setnames(full_table,c('pop1','pop2')) - - # convert pool names to factors with all possible levels - # make sure pop2 has levels in reverse so the figure looks right - # (i.e., a nice triangle) - # we left join from the table of all combos so we have all combos, even - # if some have missing data - hmd <- merge(full_table,fst_grp,by = c('pop1','pop2'),all=TRUE)[order(pop1,-pop2)] - hmd[,`:=`( +# summarize dataset (as above) by method and pool comparison +pool_data %>% + group_by(method) %>% + group_walk(\(fst_data,method) { + # output tab header + cat(str_glue("## {method$method} {{.tabset}}\n\n")) + + # now we walk through our different display pairs + var_pairs %>% + walk(\(trace_vars) { + # and show the tab header + cat(str_glue("### {trace_vars$title}\n\n")) + print(h5(HTML(trace_vars$description))) + + # create cutoff slider + cutoff_sel <- div( + class="coverage-slider", + tagList( + tags$label( + id=str_glue("heatmap-cutoff-label-{method$method}-{trace_vars$name}"), + `for`=str_glue("heatmap-cutoff-sel-{method$method}-{trace_vars$name}"), + str_glue('Minimum coverage: {params$nf$report_min_coverage}') + ), + div( + id=str_glue("heatmap-cutoff-sel-{method$method}-{trace_vars$name}") + ) + ) + ) + print(cutoff_sel) + + # cutoff slider handler code (js) + # note double curly braces because the whole thing is + # wrapped in str_glue + cutoff_script <- tags$script(HTML(str_glue( + ' + $(function() {{ + $("#heatmap-cutoff-sel-{method$method}-{trace_vars$name}").labeledslider({{ + min: {params$nf$report_min_coverage}, + max: {params$nf$report_max_coverage}, + step: {params$nf$report_coverage_step}, + tickInterval: {params$nf$report_coverage_step}, + change: function (e, ui) {{ + var fig = $("#heatmap-fig-{method$method}-{trace_vars$name}")[0]; + var figdata = $(`#heatmap-json-{method$method}-{trace_vars$name}-${{ui.value}}`).text(); + $("#heatmap-cutoff-label-{method$method}-{trace_vars$name}").text(`Minimum coverage: ${{ui.value}}`); + Plotly.react(fig,JSON.parse(figdata)); + }}, + slide: function (e, ui) {{ + var fig = $("#heatmap-fig-{method$method}-{trace_vars$name}")[0]; + var figdata = $(`#heatmap-json-{method$method}-{trace_vars$name}-${{ui.value}}`).text(); + $("#heatmap-cutoff-label-{method$method}-{trace_vars$name}").text(`Minimum coverage: ${{ui.value}}`); + Plotly.react(fig,JSON.parse(figdata)); + }} + }}); + + }}); + ' + ))) + print(cutoff_script) + + print(h5(tags$small(HTML( + "Note: Grey cells indicate missing data." + )))) + + # now walk through coverage cutoff levels + # and create figure and/or figure json for each one + cutoffs %>% + walk(\(cutoff) { + # filter data by coverage cutoff + # and summarize by pool comparison + fst_data <- fst_data %>% + filter(avg_min_cov >= cutoff) %>% + group_by(pop1,pop2) %>% + summarise( + fst_sd = sd(fst,na.rm=TRUE), + fst_se = fst_sd / sqrt(n()), + cov = mean(avg_min_cov,na.rm=TRUE), + cov_sd = sd(avg_min_cov,na.rm = TRUE), + cov_se = cov_sd / sqrt(n()), + snps = n_distinct(chrom,pos,na.rm=TRUE), + contigs = n_distinct(chrom,na.rm = TRUE), + snps_per_contig = snps / contigs, + fst = mean(fst,na.rm=TRUE) + ) %>% + ungroup() %>% + # retain the cutoff level in the data + mutate(cutoff=cutoff) + + # get all pool names + all_pops <- sort(union(unique(fst_data$pop1),unique(fst_data$pop2))) + + # create a tibble of all possible combinations (sorted) + full_table <- all_pops %>% + combn(2) %>% + apply(MARGIN = 2,FUN=\(x) sort(x)) %>% + t() %>% + as_tibble() %>% + rename(pop1=1,pop2=2) + + # convert pool names to factors with all possible levels + # make sure pop2 has levels in reverse so the figure looks right + # (i.e., a nice triangle) + # we left join from the table of all combos so we have all combos, even + # if some have missing data + hmd <- full_table %>% + left_join(fst_data,by=c("pop1","pop2")) %>% + arrange(pop1,desc(pop2)) %>% + mutate( pop1 = factor(pop1,levels=all_pops), pop2 = factor(pop2,levels=rev(all_pops)) - )] - hmd[, c(paste0(names(.SD),'_na')) := lapply(.SD,is.na), .SDcols = is.numeric] - hmd[, c(paste0(names(.SD),'_val')) := .SD, .SDcols = is.numeric ] - setorder(hmd,pop1,-pop2) - - # get the factor level that would be the top row of the plot - top_level <- last(levels(hmd$pop2)) - - # now we make a dummy table for just that top row - # if we don't do this, the top triangle is missing its top row - + ) %>% + mutate( + # create columns for the tooltip text + across(where(is.numeric),\(x) x,.names = "{.col}_val"), + # replace misisng integers with -1 + across(where(is.integer) & !ends_with("_val"),\(x) replace_na(x,-1)), + # replace missing floats with min(x)*1e-5 + across(where(is.double) & !ends_with("_val"),\(x) replace_na(x,min(x,na.rm = TRUE)*0.00001)) + ) %>% + arrange(pop1,desc(pop2)) + + # get the factor level that would be the top row of the plot + top_level <- last(levels(hmd$pop2)) + + # now we make a dummy table for just that top row + top_row <- hmd %>% + filter(pop1 == top_level) %>% # switch pop1 and pop2 - top_row <- hmd[pop1 == top_level] - top_row[, temp := pop1 ] - top_row[,`:=`( pop1=pop2, pop2=temp, temp=NULL )] - + mutate(temp=pop1,pop1=pop2,pop2=temp) %>% + # drop the temp column + select(-temp) %>% # set all numeric values to NA - # cols = names(top_row)[!names(top_row) %in% c('pop1','pop2')] - # top_row[, c(cols) := NA ] - top_row[, (names(top_row)[sapply(top_row, is.numeric)]) := lapply(.SD, \(x) NA), .SDcols = is.numeric] - - # bind dummy values to plot data - # it seems to matter that we bind these - # to the end, rather than the other way around - hmd <- rbind(hmd,top_row) - - # start making the figure - fig <- plot_ly(hoverongaps=FALSE) - - # finish making the figure - fig <- trace_vars$pairs %>% - reduce2(c(1,2),\(plot_fig,v,i) { - if (!all(is.na(v))) { - # add the trace as either the upper or lower triangle - # depending on whether its #1 or #2 - col <- str_glue("{v[3]}_na") - hmdd <- hmd[get(col) | is.na(get(col))] - hmdd[,c(v[3]) := fifelse(.SD[[col]] == TRUE,0,NA) ] - - plot_fig %>% - { - if (sum(hmd[[str_glue("{v[3]}_na")]],na.rm = T) > 0 ) { - add_trace( - ., - type="heatmap", - hoverinfo = 'none', - data = hmdd, - x = ~.data[[v[1]]], - y = ~.data[[v[2]]], - z = ~.data[[v[3]]], - colorscale = 'Greys', - showscale = FALSE - ) - } else . - } %>% - add_trace( - type = "heatmap", - data = hmd, - x = ~.data[[v[1]]], # x variable - y = ~.data[[v[2]]], # y variable - z = ~.data[[v[3]]], # z (color) variable - # this is what gets shown in the tooltip - customdata = ~fix_na( sprintf( str_c("%",v[5]), .data[[ str_glue("{v[3]}_val") ]] ) ), - colorscale = cscale(v[4],n = 10,na = NA), # color palette, based on 100 gradations - # style the colorbar - colorbar = list( - # show the appropriate display name - title=list(text=value_map[v[3]]), - # determine the position of the colorbar - # bottom if it's for the bottom triangle, top if it's for the top - y = ifelse(i == 1,0,1), - # which side of the colorbar we reckon the y-anchor from - yanchor = ifelse(i == 1,"bottom","top"), - len = 0.5, - lenmode = "fraction" - ), - # text template - text = ~str_glue("{pop1} - {pop2}"), - # hover text template (hide the trace name with ) - hovertemplate=str_glue("%{{text}}
{value_map[v[3]]}: %{{customdata}}") - ) - } else return(plot_fig) - },.init = fig) %>% - layout( - # name the figure axes and don't let the user zoom around - xaxis = list(title = "Pool 1", fixedrange = TRUE), - yaxis = list(title = "Pool 2", fixedrange = TRUE), - title = list( - text = str_glue("{trace_vars$title} (coverage cutoff: ≥{cutoff})"), - font = list( - size = 12 + mutate(across(-c(pop1,pop2),~NA)) + + # bind dummy values to plot data + # it seems to matter that we bind these + # to the end, rather than the other way around + hmd <- hmd %>% + bind_rows(top_row) + + # create an empty plotly figure + # don't show tooltips if there's no data + fig <- hmd %>% + plot_ly(hoverongaps=FALSE) + + # now we go through each figure pair + # and reduce them to a single plotly figure + # that c(1,2) is just to keep track of if we're on + # the lower or upper triangle + fig <- trace_vars$pairs %>% + reduce2(c(1,2),\(f,v,i) { + # if the element is valid + if (!all(is.na(v))) { + # add a heatmap trace + f %>% + add_trace( + type = "heatmap", + x = ~.data[[v[1]]], # x variable + y = ~.data[[v[2]]], # y variable + z = ~.data[[v[3]]], # z (color) variable + # this is what gets shown in the tooltip + customdata = ~fix_na( sprintf( str_c("%",v[5]), .data[[ str_glue("{v[3]}_val") ]] ) ), + # zmin and zmax are trying to make it so the "missing" + # color doesn't show up in the color scale legend + zmin = ~min(.data[[v[[3]]]],na.rm = TRUE)*0.00001, + zmax = ~max(.data[[v[3]]],na.rm = TRUE), + colorscale = cscale(v[4],n = 100,na = v[6]), # color palette, based on 100 gradations + # style the colorbar + colorbar = list( + # show the appropriate display name + title=list(text=value_map[v[3]]), + # determine the position of the colorbar + # bottom if it's for the bottom triangle, top if it's for the top + y = ifelse(i == 1,0,1), + # which side of the colorbar we reckon the y-anchor from + yanchor = ifelse(i == 1,"bottom","top"), + len = 0.5, + lenmode = "fraction" + ), + # text template + text = ~str_glue("{pop1} - {pop2}"), + # hover text template (hide the trace name with ) + hovertemplate=str_glue("%{{text}}
{value_map[v[3]]}: %{{customdata}}") ) - ) - ) %>% - # hide the mode bar - config(displayModeBar = FALSE) - - # get the figure for output - if (is.null(plot_fig)) { - fig_id <- str_glue("heatmap-fig-{method}-{shr}-{trace_vars$name}") - fig$elementId <- fig_id - # we stick it in a div with class 'fig-container' so - # they're easy to find in the DOM using jquery - plot_fig <- tagList( - div( - `data-fig`=fig_id, - `data-json-prefix`=str_glue("heatmap-json-{method}-{shr}-{trace_vars$name}"), - class="fig-container", - tagList(fig) - ) - ) - } - - # get the figure json and output it - fig_json <- fig %>% - plotly_json(jsonedit = FALSE) %>% - jsonlite::minify() - # create and print script tag with figure data json - # we use this to draw the other cutoff level figures when the slider is dragged - json_tag <- tags$script( - id = str_glue("heatmap-json-{method}-{shr}-{trace_vars$name}-{cutoff}"), - type = 'application/json', - HTML(fig_json) - ) - print(json_tag) - - return(plot_fig) - },.init = NULL) - - # output the heatmap figure - print(heatmap_fig) - }) - }) -},by=method] + } else return(f) + },.init = fig) %>% + layout( + # name the figure axes and don't let the user zoom around + xaxis = list(title = "Pool 1", fixedrange = TRUE), + yaxis = list(title = "Pool 2", fixedrange = TRUE) + ) %>% + # hide the mode bar + config(displayModeBar = FALSE) -``` + # assign id attribute to plotly div + fig_id <- str_glue("heatmap-fig-{method$method}-{trace_vars$name}") + fig$elementId <- fig_id + + if (cutoff == first(cutoffs)) { + # output the figure to the report, but only if it's the first coverage cutoff + print(tagList(fig)) + } + # get minified figure json + fig_json <- fig %>% + plotly_json(jsonedit = FALSE) %>% + jsonlite::minify() -```{r fst-correlation, results='asis', eval=show_fst} + # create and print script tag with figure data json + # we use this to draw the other cutoff level figures when the slider is dragged + script_tag <- tags$script( + id = str_glue("heatmap-json-{method$method}-{trace_vars$name}-{cutoff}"), + type = 'application/json', + HTML(fig_json) + ) + print(script_tag) + }) + + }) + }) +``` + +```{r, fst-correlation, results='asis', eval=show_fst} cat("# Fst method correlations {.tabset}\n\n") # get calculation methods methods <- unique(pool_data$method) # split the dataset into subsets by calculation method +splot <- pool_data %>% + split(.$method) methods %>% combn(2) %>% array_branch(2) %>% - walk(\(methods) { - cat(str_glue("## {methods[1]} vs {methods[2]}\n\n")) + walk(\(pair) { + cat(str_glue("## Fst correlation: {pair[1]} vs {pair[2]}\n\n")) print(h5(tags$small(HTML("Note: Due to potentially large datasets, only a subsample of 1,000 Fst values is shown here.")))) - - # make a joined table with both methods and subsample to max 1000 rows - corr <- merge( - pool_data[ method == methods[1] ][sample(.N,1000),], - pool_data[ method == methods[2] ], - by = c('chrom','pos','pop1','pop2') - ) - + + # make a joined table with both methods and subsample to 1000 rows + corr <- splot[[pair[1]]] %>% + sample_n(1000) %>% + inner_join(splot[[pair[2]]],by=c("chrom","pos","pop1","pop2")) + # do a linear regression lmm <- lm(fst.y ~ fst.x, data=corr) # get predicted data for the trend line - corr[,predicted := predict(lmm,corr[,.(fst.x)])] - + corr <- corr %>% + mutate( + predicted = lmm %>% + predict(corr %>% select(fst.x)) + ) + # get method names - xtitle <- methods[1] - ytitle <- methods[2] - + xtitle <- pair[1] + ytitle <- pair[2] + # plot the figure fig <- corr %>% plot_ly() %>% @@ -1083,7 +1060,7 @@ methods %>% mode = 'lines', line = list(color = '#232323', width = 0.7) ) %>% - # add scatter + # add scatter add_trace( x = ~fst.x, y = ~fst.y, @@ -1105,75 +1082,100 @@ methods %>% yaxis = list(title = str_glue("Fst ({ytitle})")) ) %>% # disappear the legends - hide_guides() %>% - config(toImageButtonOptions = list( - format = 'svg', - width = 900, - height = 600 - )) - + hide_guides() + print(tagList(fig)) }) ``` -```{r fisher, results='asis', eval=show_fisher} -cat("# Fisher's test plots {.tabset .fisher-tabs}\n\n") +```{r, fisher, results='asis', eval=show_fisher} +cat("# Fisher test plots {.tabset}\n\n") print(h5(tags$small( - "Fisher test results are presented as static plots since these datasets can get huge, making plotly go slow or crash." + "Fisher test results are presented as static plots due to the potentially large size of the datasets." ))) -nothing <- fisher_data[,{ - fishr <- .SD - cat(str_glue("## {method}\n\n")) - - print(h6(HTML("Points above red line indicate p < 0.05."))) - - figs <- cutoffs %>% - # set_names() %>% - map(\(cov) { - - # filter by coverage cutoff and get mean fisher p-value by snp - figdata <- fishr[avg_min_cov >= cov][,.(log_fisher = mean(log_fisher,na.rm=TRUE)), by=.(chrom,pos)][order(chrom,pos)] - figdata[,snp := .I] - - fig <- ggplot(figdata) + - geom_point(aes(x=snp,y=log_fisher),size=1) + - geom_hline(yintercept = -log10(0.05), color="red") + - theme_bw() + - labs(x = 'SNP', y=expression(paste(-log[10]~"(p-value)"))) - - disp <- if(cov == first(cutoffs)) "" else "display: none" - plotTag( - fig, - alt=str_glue("Fisher's exast test p-values for coverage ≥{cov}"), - width = 672, - attribs = list( - `data-method` = method, - `data-cutoff` = cov, - id = str_glue("fisher-plot-{method}-{cov}"), - class = c(str_glue('fisher-plotz'),str_glue('fisher-plotz-{cov}')), - style = disp +fisher_data %>% + group_by(method) %>% + group_walk(\(fishr,method) { + cat(str_glue("## {method$method}\n\n")) + + # make slider for coverage cutoff + cutoff_slider <- div( + class="coverage-slider", + tagList( + tags$label( + id=str_glue("fisher-cutoff-label-{method$method}"), + `for`=str_glue("fisher-cutoff-sel-{method$method}"), + str_glue('Minimum coverage: {params$nf$report_min_coverage}') + ), + div( + id=str_glue("fisher-cutoff-sel-{method$method}") ) ) - }) - - # for some reason enclosing this in another div() - # call makes the image tags show as text - cat('
') - print( - tagList( - div( - class = 'visible-fisher', - tagList(figs[1]) - ), - div( - class = 'hidden-fisher', - style = 'display: none', - tagList(figs[-1]) - ) ) - ) - cat('
') - -},by=method] + print(cutoff_slider) + + cutoff_script <- tags$script(HTML(str_glue( + ' + $(function() {{ + $("#fisher-cutoff-sel-{method$method}").labeledslider({{ + min: {params$nf$report_min_coverage}, + max: {params$nf$report_max_coverage}, + step: {params$nf$report_coverage_step}, + tickInterval: {params$nf$report_coverage_step}, + change: function (e, ui) {{ + $(".fisher-plotz-{method$method}").hide(); + $(`#fisher-plot-{method$method}-${{ui.value}}`).show(); + $("#fisher-cutoff-label-{method$method}").text(`Minimum coverage: ${{ui.value}}`); + }}, + slide: function (e, ui) {{ + $(".fisher-plotz-{method$method}").hide(); + $(`#fisher-plot-{method$method}-${{ui.value}}`).show(); + $("#fisher-cutoff-label-{method$method}").text(`Minimum coverage: ${{ui.value}}`); + }} + }}); + }}); + ' + ))) + print(cutoff_script) + + print(h6(HTML("Points above red line indicate p < 0.05."))) + + figs <- cutoffs %>% + # set_names() %>% + map(\(cov) { + # cat(str_glue("### Coverage ≥{cov}\n\n")) + fig <- fishr %>% + # filter by average minimum coverage + filter(avg_min_cov >= cov) %>% + # retain the cutoff level in the data + # mutate(cutoff=cov) %>% + group_by(chrom,pos) %>% + summarise( log_fisher = mean(log_fisher) ) %>% + ungroup() %>% + arrange(chrom,pos) %>% + mutate(snp = row_number()) %>% + ggplot() + + geom_point(aes(x=snp,y=log_fisher),size=1) + + geom_hline(yintercept = -log10(0.05), color="red") + + theme_bw() + + labs(x = 'SNP', y=expression(paste(-log[10]~"(p-value)"))) + + disp <- if(cov == first(cutoffs)) "" else "display: none" + plotTag( + fig, + alt=str_glue("Fisher's exast test p-values for coverage ≥{cov}"), + width = 672, + attribs = list( + id = str_glue("fisher-plot-{method$method}-{cov}"), + class = str_glue('fisher-plotz-{method$method}'), + style = disp + ) + ) + }) + print(div( + id='fisher-container', + tagList( figs ) + )) + }) ``` diff --git a/assets/email_template.html b/assets/email_template.html index d33c051..d00307e 100644 --- a/assets/email_template.html +++ b/assets/email_template.html @@ -4,21 +4,21 @@ - - assessPool Pipeline Report + + nf-core/assesspool Pipeline Report
-

assessPool ${version}

+

nf-core/assesspool ${version}

Run Name: $runName

<% if (!success){ out << """
-

assessPool execution completed unsuccessfully!

+

nf-core/assesspool execution completed unsuccessfully!

The exit status of the task that caused the workflow execution to fail was: $exitStatus.

The full error message was:

${errorReport}
@@ -27,7 +27,7 @@

assessPool execution completed unsucce } else { out << """
- assessPool execution completed successfully! + nf-core/assesspool execution completed successfully!
""" } @@ -44,8 +44,8 @@

Pipeline Configuration:

-

assessPool

-

https://github.com/tobodev/assesspool

+

nf-core/assesspool

+

https://github.com/nf-core/assesspool

diff --git a/assets/email_template.txt b/assets/email_template.txt index ac6600d..6d43a5b 100644 --- a/assets/email_template.txt +++ b/assets/email_template.txt @@ -4,15 +4,15 @@ |\\ | |__ __ / ` / \\ |__) |__ } { | \\| | \\__, \\__/ | \\ |___ \\`-._,-`-, `._,._,' - assessPool ${version} + nf-core/assesspool ${version} ---------------------------------------------------- Run Name: $runName <% if (success){ - out << "## assessPool execution completed successfully! ##" + out << "## nf-core/assesspool execution completed successfully! ##" } else { out << """#################################################### -## assessPool execution completed unsuccessfully! ## +## nf-core/assesspool execution completed unsuccessfully! ## #################################################### The exit status of the task that caused the workflow execution to fail was: $exitStatus. The full error message was: @@ -35,5 +35,5 @@ Pipeline Configuration: <% out << summary.collect{ k,v -> " - $k: $v" }.join("\n") %> -- -assessPool -https://github.com/tobodev/assesspool +nf-core/assesspool +https://github.com/nf-core/assesspool diff --git a/assets/input.csv b/assets/input.csv deleted file mode 100644 index 182019d..0000000 --- a/assets/input.csv +++ /dev/null @@ -1,2 +0,0 @@ -project,input,vcf_index,reference,pools,pool_sizes -poolseq_test,data/pools.vcf.gz,data/pools.vcf.gz.tbi,data/ref.fasta,,"35,38,22,52,17,19" diff --git a/assets/samplesheet.csv b/assets/samplesheet.csv new file mode 100644 index 0000000..5f653ab --- /dev/null +++ b/assets/samplesheet.csv @@ -0,0 +1,3 @@ +sample,fastq_1,fastq_2 +SAMPLE_PAIRED_END,/path/to/fastq/files/AEG588A1_S1_L002_R1_001.fastq.gz,/path/to/fastq/files/AEG588A1_S1_L002_R2_001.fastq.gz +SAMPLE_SINGLE_END,/path/to/fastq/files/AEG588A4_S4_L003_R1_001.fastq.gz, diff --git a/assets/schema_input.json b/assets/schema_input.json index 4bcda41..8acf4a5 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -13,25 +13,25 @@ "errorMessage": "Project name must be provided and cannot contain spaces", "meta": ["id"] }, - "input": { + "vcf": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.(vcf|sync)(\\.gz)?(\\?.+)?$", - "errorMessage": "Input file must be provided in the `input` column, cannot contain spaces, and must have extension '.vcf', '.sync', 'vcf.gz', or '.sync.gz'" + "pattern": "^\\S+\\.vcf(\\.gz)?$", + "errorMessage": "VCF file must be provided in the `vcf` column, cannot contain spaces, and must have extension '.vcf' or '.vcf.gz'" }, "vcf_index": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.vcf\\.gz\\.tbi(\\?.+)?$", + "pattern": "^\\S+\\.vcf\\.gz\\.tbi$", "errorMessage": "VCF index file, must look like vcf_filename.vcf.gz.tbi" }, "reference": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.fa(s?ta)?(\\.gz)?(\\?.+)?$", + "pattern": "^\\S+\\.fa(s?ta)?(\\.gz)?$", "errorMessage": "Reference FASTA must be provided in the `reference` column, cannot contain spaces, and must have extension '.fa/fa.gz' or '.fasta/fasta.gz'" }, "pools": { @@ -42,6 +42,6 @@ "errorMessage": "Must provide pool size(s), either a single number or a comma-separated list" } }, - "required": ["project","input", "reference","pool_sizes"] + "required": ["project","vcf", "reference","pool_sizes"] } } diff --git a/assets/slackreport.json b/assets/slackreport.json index a8c8caf..8d0feea 100644 --- a/assets/slackreport.json +++ b/assets/slackreport.json @@ -3,7 +3,7 @@ { "fallback": "Plain-text summary of the attachment.", "color": "<% if (success) { %>good<% } else { %>danger<%} %>", - "author_name": "assesspool ${version} - ${runName}", + "author_name": "nf-core/assesspool ${version} - ${runName}", "author_icon": "https://www.nextflow.io/docs/latest/_static/favicon.ico", "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors<% } %>", "fields": [ diff --git a/conf/base.config b/conf/base.config index bbc63fd..83d3666 100644 --- a/conf/base.config +++ b/conf/base.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - assesspool Nextflow base config file + nf-core/assesspool Nextflow base config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A 'blank slate' config file, appropriate for general use on most high performance compute environments. Assumes that all software is installed and available on @@ -10,6 +10,7 @@ process { + // TODO nf-core: Check the defaults for all processes cpus = { 1 * task.attempt } memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } @@ -23,6 +24,7 @@ process { // These labels are used and recognised by default in DSL2 files hosted on nf-core/modules. // If possible, it would be nice to keep the same label naming convention when // adding in your local modules too. + // TODO nf-core: Customise requirements for specific processes. // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors withLabel:process_single { cpus = { 1 } diff --git a/conf/filters.config b/conf/filters.config index d4e4847..54a19d3 100644 --- a/conf/filters.config +++ b/conf/filters.config @@ -1,10 +1,6 @@ process { // bcftools filters withName: MAX_ALLELE_LENGTH { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -21,10 +17,6 @@ process { } } withName: QUALITY_DEPTH_RATIO { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -41,10 +33,6 @@ process { } } withName: MIN_QUALITY { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -61,10 +49,6 @@ process { } } withName: VARIANT_TYPE { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -81,10 +65,6 @@ process { } } withName: MISPAIRED_READS { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -101,10 +81,6 @@ process { } } withName: ALTERNATE_OBSERVATIONS { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -121,10 +97,6 @@ process { } } withName: MIN_MAPPING_QUALITY { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -141,10 +113,6 @@ process { } } withName: MAPPING_RATIO { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -162,10 +130,6 @@ process { } } withName: MIN_DEPTH { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -182,10 +146,6 @@ process { } } withName: MIN_POOLS { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -202,10 +162,6 @@ process { } } withName: READ_BALANCE { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ @@ -225,10 +181,6 @@ process { // vcftools filters withName: MAX_MEAN_DEPTH { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ "--recode --recode-INFO-all", @@ -236,10 +188,6 @@ process { ].join(' ').trim() } } withName: MIN_MEAN_DEPTH { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ "--recode --recode-INFO-all", @@ -247,20 +195,12 @@ process { ].join(' ').trim() } } withName: HWE_CUTOFF { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ params.hwe_cutoff ? "--hwe ${params.hwe_cutoff}" : "" ].join(' ').trim() } } withName: MINOR_ALLELE_COUNT { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ "--recode --recode-INFO-all", @@ -268,10 +208,6 @@ process { ].join(' ').trim() } } withName: MAX_MISSING { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ "--recode --recode-INFO-all", @@ -279,10 +215,6 @@ process { ].join(' ').trim() } } withName: THIN { - publishDir = [ - path: { "${params.outdir}/filter/incremental" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_${meta.filter}_filter" } ext.args = { [ "--recode --recode-INFO-all", @@ -294,10 +226,6 @@ process { // ripgrep wrapper to count snps in a vcf // works whether zipped or not withName: 'COUNT_SNPS.*' { - publishDir = [ - path: '', - enabled: false - ] ext.prefix = { "${meta.filter}_filter_count" } ext.args = { [ '-z', diff --git a/conf/modules.config b/conf/modules.config index dd59259..8688e19 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -20,25 +20,17 @@ process { withName: RMARKDOWNNOTEBOOK { conda = { [ - 'r-rmarkdown=2.29', - 'r-yaml=2.3.10', - 'r-tidyr=1.3.1', - 'r-data.table=1.17.8', - 'r-knitr=1.50', - 'r-plotly=4.11.0', - 'r-dt=0.33', - 'r-stringr=1.5.1', - 'r-purrr=1.1.0', - 'r-paletteer=1.6.0', - 'r-htmltools=0.5.8.1', - 'r-ggplot2=3.5.2', - 'r-forcats=1.0.0', - 'r-scales=1.4.0', - 'r-jsonlite=2.0.0' + 'conda-forge::r-rmarkdown=2.29', + 'conda-forge::r-knitr=1.50', + 'conda-forge::r-yaml=2.3.10', + 'conda-forge::r-plotly=4.10.4', + 'conda-forge::r-dt=0.33', + 'conda-forge::r-dplyr=1.1.4', + 'conda-forge::r-readr=2.1.5', + 'conda-forge::r-forcats=1.0.0', + 'conda-forge::r-paletteer=1.6.0' ].join(' ').trim() } - container = { "${ workflow.containerEngine == 'singularity' && !ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/mulled-v2-b5ef69f92fe3bbdbe29f16d80675330b2bd45a66:a66dc1806b3ac5c904f1d0359e0504581f5495cd-0' : - 'biocontainers/mulled-v2-b5ef69f92fe3bbdbe29f16d80675330b2bd45a66:a66dc1806b3ac5c904f1d0359e0504581f5495cd-0' }" } + container = { "fishbotherer/buildreport:1.0.1" } publishDir = [ [ path: { "${params.outdir}/report" }, @@ -47,17 +39,13 @@ process { ], [ path: { "${params.outdir}/report" }, - mode: params.publish_dir_mode, + mode: "symlink", pattern: 'artifacts/*' ] ] } withName: REHEADER_FST { - publishDir = [ - path: { "${params.outdir}/fst/popoolation" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_" + meta.pools.keySet().sort().join("-") } ext.suffix = { "reheadered.fst" } ext.args = { @@ -65,32 +53,27 @@ process { .keySet() .sort() .withIndex() - .collect{ k,i -> "-v pop${i+1}='${k}'" } - ).join(' ').trim() - } - ext.args2 = { - """' - BEGIN{ - FS=OFS="\\t" - print "chrom\\tpos\\twindow_size\\tcovered_fraction\\tavg_min_cov\\tpop1\\tpop2\\tfst\\tmethod" - } { - gsub(/^.+=/,"",\$NF) - fst=\$NF; nf=NF - \$(nf+3)="popoolation" - \$(nf+2)=fst - \$(nf+1)=pop2 - \$nf=pop1 - print - } - '""" + .collect{ k,i -> "-v pop${i+1}='${k}'" } + + [ + """' + BEGIN{ + FS=OFS="\\t" + print "chrom\\tpos\\twindow_size\\tcovered_fraction\\tavg_min_cov\\tpop1\\tpop2\\tfst\\tmethod" + } { + gsub(/^.+=/,"",\$NF) + fst=\$NF; nf=NF + \$(nf+3)="popoolation" + \$(nf+2)=fst + \$(nf+1)=pop2 + \$nf=pop1 + print + } + '""" + ] ).join(' ').trim() } } withName: REHEADER_FISHER { - publishDir = [ - path: { "${params.outdir}/fisher/popoolation" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_" + meta.pools.keySet().sort().join("-") } ext.suffix = { "reheadered.fisher" } ext.args = { @@ -98,32 +81,27 @@ process { .keySet() .sort() .withIndex() - .collect{ k,i -> "-v pop${i+1}='${k}'" } - ).join(' ').trim() - } - ext.args2 = { - """' - BEGIN{ - FS=OFS="\\t" - print "chrom\\tpos\\twindow_size\\tcovered_fraction\\tavg_min_cov\\tpop1\\tpop2\\tlog_fisher\\tmethod" - } { - gsub(/^.+=/,"",\$NF) - fisher=\$NF; nf=NF - \$(nf+3)="popoolation" - \$(nf+2)=fisher - \$(nf+1)=pop2 - \$nf=pop1 - print - } - '""" + .collect{ k,i -> "-v pop${i+1}='${k}'" } + + [ + """' + BEGIN{ + FS=OFS="\\t" + print "chrom\\tpos\\twindow_size\\tcovered_fraction\\tavg_min_cov\\tpop1\\tpop2\\tfisher\\tmethod" + } { + gsub(/^.+=/,"",\$NF) + fisher=\$NF; nf=NF + \$(nf+3)="popoolation" + \$(nf+2)=fisher + \$(nf+1)=pop2 + \$nf=pop1 + print + } + '""" + ] ).join(' ').trim() } } withName: SPLIT_SYNC { - publishDir = [ - path: { "${params.outdir}/sync/pairwise" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_" + meta.pools.keySet().sort().join("-") } ext.suffix = { "sync" } ext.args = { @@ -131,27 +109,24 @@ process { .keySet() .sort() .withIndex() - .collect{ k,i -> "-v col${i+1}='${k}'" } - ).join(' ').trim() - } - ext.args2 = { - """' - BEGIN { FS=OFS="\\t" } - NR == 1 { - for (i=1; i <= NF; i++) { cols[\$i]=i } - next - } - NR > 1 { print \$1, \$2, \$3, \$cols[col1], \$cols[col2] } - '""" + .collect{ k,i -> "-v col${i+1}='${k}'" } + + [ + "-F \$'\\t'", + "-v OFS='\\t'", + """' + NR == 1 { + for (i=1; i <= NF; i++) { cols[\$i]=i } + # print \$1, \$2, \$3, \$cols[col1], \$cols[col2] + next + } + NR > 1 { print \$1, \$2, \$3, \$cols[col1], \$cols[col2] } + '""" + ] ).join(' ').trim() } } // grenedalf frequency options withName: GRENEDALF_FREQUENCY { - publishDir = [ - path: '', - enabled: false - ] ext.args = { [ "--allow-file-overwriting", "--write-sample-counts", @@ -166,10 +141,6 @@ process { // grenedalf sync options withName: GRENEDALF_SYNC { - publishDir = [ - path: { "${params.outdir}/sync" }, - mode: params.publish_dir_mode - ] ext.args = { [ params.missing_zeroes ? "--no-missing-marker" : "", "--filter-total-snp-min-count ${params.min_count}", @@ -180,10 +151,6 @@ process { // grenedalf fst options withName: GRENEDALF_FST { - publishDir = [ - path: { "${params.outdir}/fst/grenedalf" }, - mode: params.publish_dir_mode - ] ext.args = { ( [ "--method ${params.fst_method}", @@ -210,10 +177,6 @@ process { // r script to merge frequency and fst results withName: JOINFREQ { - publishDir = [ - path: '', - enabled: false - ] ext.prefix = { "${meta.id}_" + meta.pools.keySet().sort().join("-") } ext.args = { [ "--window-type ${params.window_type}", @@ -227,10 +190,6 @@ process { // r-based fisher test options withName: FISHERTEST { - publishDir = [ - path: { "${params.outdir}/fisher/assesspool" }, - mode: params.publish_dir_mode - ] ext.args = { [ "--window-type ${params.window_type}", "--min-count ${params.min_count}", @@ -243,10 +202,6 @@ process { // popoolation2 fisher test options withName: POPOOLATION2_FISHERTEST { - publishDir = [ - path: '', - enabled: false - ] ext.args = { [ "--min-count ${params.min_count}", "--min-coverage ${params.min_coverage}", @@ -257,10 +212,6 @@ process { // popoolation2 fst options withName: POPOOLATION2_FST { - publishDir = [ - path: '', - enabled: false - ] ext.args = { [ "--min-count ${params.min_count}", "--min-coverage ${params.min_coverage}", @@ -274,10 +225,6 @@ process { // poolfstat options withName: POOLFSTAT_FST { - publishDir = [ - path: { "${params.outdir}/fst/poolfstat" }, - mode: params.publish_dir_mode - ] ext.args = { [ "--min-coverage ${params.min_coverage}", "--max-coverage ${params.max_coverage}", @@ -290,10 +237,6 @@ process { } withName: COMPRESS_VCF { - publishDir = [ - path: '', - enabled: false - ] ext.args = { [ "--output-type z", "--write-index=tbi" @@ -301,10 +244,6 @@ process { } withName: BCFTOOLS_COMPRESS_INDEX_FILTERED { - publishDir = [ - path: '', - enabled: false - ] ext.prefix = { "filtered_final" } ext.args = { [ "--output-type z", @@ -313,10 +252,6 @@ process { } withName: BCFTOOLS_FILTER { - publishDir = [ - path: { "${params.outdir}/filter/bcftools" }, - mode: params.publish_dir_mode - ] ext.prefix = { "bcftools_filtered" } ext.args = { def f = params.filter @@ -352,10 +287,6 @@ process { } withName: THIN_SNPS { - publishDir = [ - path: { "${params.outdir}/filter/thin" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_vcftools_filtered_thinned" } ext.args = { [ "--recode --recode-INFO-all", @@ -364,10 +295,6 @@ process { } withName: VCFTOOLS_FILTER { - publishDir = [ - path: { "${params.outdir}/filter/vcftools" }, - mode: params.publish_dir_mode - ] ext.prefix = { "${meta.id}_vcftools_filtered" } ext.args = { [ "--recode --recode-INFO-all", @@ -380,30 +307,15 @@ process { } withName: VCF_SAMPLES { - publishDir = [ - path: '', - enabled: false - ] ext.args = { "-l" } ext.prefix = { "sample_names" } ext.suffix = { "txt" } } withName: VCF_RENAME { - publishDir = [ - path: '', - enabled: false - ] ext.args2 = { [ "--output-type z", "--write-index=tbi" ].join(' ').trim() } } - - withName: EXTRACT_SEQUENCES { - publishDir = [ - path: { "${params.outdir}/sequences" }, - mode: params.publish_dir_mode - ] - } } diff --git a/conf/test.config b/conf/test.config index 3eb19d7..85c1f3d 100644 --- a/conf/test.config +++ b/conf/test.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run tobodev/assesspool -profile test, --outdir + nextflow run nf-core/assesspool -profile test, --outdir ---------------------------------------------------------------------------------------- */ @@ -13,7 +13,7 @@ process { resourceLimits = [ cpus: 4, - memory: '24.GB', + memory: '15.GB', time: '1.h' ] } @@ -23,37 +23,7 @@ params { config_profile_description = 'Minimal test dataset to check pipeline function' // Input data - input = params.pipelines_testdata_base_path + 'files/test.csv' - filter = true - max_missing = 0.5 - min_minor_allele_count = 2 - min_mapping_quality = 30 - min_mapping_quality_ref = 30 - min_mapping_ratio = 0.75 - max_mapping_ratio = 1.25 - quality_depth_ratio = 0.25 - mispaired_reads = true - read_balance_left = 0 - read_balance_right = 0 - min_pools = 5 - min_depth = 30 - max_allele_length = 10 - min_quality = 30 - variant_type = 'snp' - min_alternate_observations = 2 - min_mean_depth = 3 - max_mean_depth = 500 - grenedalf = true - all_fst_columns = true - popoolation2 = true - poolfstat = true - suppress_noninformative = true - match_allele_count = true - fisher_test = true - fisher_test_popoolation = true - outdir = 'output' - missing_zeroes = true - visualize_filters = true - filter_only = false - extract_sequences = true + // TODO nf-core: Specify the paths to your test data on nf-core/test-datasets + // TODO nf-core: Give any required params for the test so that command line flags are not needed + input = params.pipelines_testdata_base_path + 'viralrecon/samplesheet/samplesheet_test_illumina_amplicon.csv' } diff --git a/conf/test_full.config b/conf/test_full.config index 6cf77af..00a8eb7 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a full size pipeline test. Use as follows: - nextflow run tobodev/assesspool -profile test_full, --outdir + nextflow run nf-core/assesspool -profile test_full, --outdir ---------------------------------------------------------------------------------------- */ @@ -15,37 +15,10 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data for full size test - input = params.pipelines_testdata_base_path + 'files/test_full.csv' - filter = true - max_missing = 0.5 - min_minor_allele_count = 2 - min_mapping_quality = 30 - min_mapping_quality_ref = 30 - min_mapping_ratio = 0.75 - max_mapping_ratio = 1.25 - quality_depth_ratio = 0.25 - mispaired_reads = true - read_balance_left = 0 - read_balance_right = 0 - min_pools = 5 - min_depth = 30 - max_allele_length = 10 - min_quality = 30 - variant_type = 'snp' - min_alternate_observations = 2 - min_mean_depth = 3 - max_mean_depth = 500 - grenedalf = true - all_fst_columns = true - popoolation2 = true - poolfstat = true - suppress_noninformative = true - match_allele_count = true - fisher_test = true - fisher_test_popoolation = true - outdir = 'output' - missing_zeroes = true - visualize_filters = true - filter_only = false - extract_sequences = true + // TODO nf-core: Specify the paths to your full test data ( on nf-core/test-datasets or directly in repositories, e.g. SRA) + // TODO nf-core: Give any required params for the test so that command line flags are not needed + input = params.pipelines_testdata_base_path + 'viralrecon/samplesheet/samplesheet_full_illumina_amplicon.csv' + + // Fasta references + fasta = params.pipelines_testdata_base_path + 'viralrecon/genome/NC_045512.2/GCF_009858895.2_ASM985889v3_genomic.200409.fna.gz' } diff --git a/defaults.yml b/defaults.yml deleted file mode 100644 index 2611293..0000000 --- a/defaults.yml +++ /dev/null @@ -1,70 +0,0 @@ -# general options -outdir: output - -# filtering options -filter: true -keep_indel: false -keep_multiallelic: false - -# vcftools filters -max_missing: 0.5 -min_minor_allele_count: 2 -min_mean_depth: 3 -max_mean_depth: 500 -hwe_cutoff: null -thin_snps: null - -# vcflib filters -min_mapping_quality: 30 -min_mapping_quality_ref: 30 -min_mapping_ratio: 0.75 -max_mapping_ratio: 1.25 -read_balance_left: 0 -read_balance_right: 0 -quality_depth_ratio: 0.25 -mispaired_reads: true -# min_pools recommended value: 1/2 number of pools -min_pools: null -min_depth: 30 -max_allele_length: 10 -min_quality: 30 -variant_type: snp # snp, ins, del, complex -min_alternate_observations: 2 - -# visualization/output options -visualize_filters: true -filter_only: false -min_coverage_cutoff: 10 -max_coverage_cutoff: 70 -coverage_cutoff_step: 10 -extract_sequences: true -fst_cutoff: 0.7 - -#fst calculations -popoolation2: false -grenedalf: true -poolfstat: true -missing_zeroes: true -window_size: 1 -window_stride: 0 -window_type: 'single' -window_region: false -window_region_list: null -window_region_skip_empty: false - -# popoolation/poolfstat -min_count: 2 -min_coverage: 10 -max_coverage: 1000 -min_covered_fraction: 1 -suppress_noninformative: false -min_minor_allele_frequency: 0 - -# grenedalf -match_allele_count: true -fst_method: 'kofler' -all_fst_columns: false - -# fisher test -fisher_test: true -fisher_test_popoolation: true diff --git a/docs/README.md b/docs/README.md index 38c1025..4c14826 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# assessPool: Documentation +# nf-core/assesspool: Documentation -The assessPool documentation is split into the following pages: +The nf-core/assesspool documentation is split into the following pages: - [Usage](usage.md) - An overview of how the pipeline works, how to run it and a description of all of the different command-line flags. diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 7768ffc622621c80f7c582efe575e46217554424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114763 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

ad;|3x$@%Z^get77ry&%{) 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 diff --git a/docs/output.md b/docs/output.md index 64136e2..bc7ee01 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,4 +1,4 @@ -# assessPool: Output +# nf-core/assesspool: Output ## Introduction @@ -14,81 +14,6 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution -### Index - -

-Output files - -- `index/` - - `*.fai/gzi`: FAI index of FASTA reference genome/assembly. - - `*.tbi`: VCF index. - -
- -Results from calling `samtools faidx` on the reference assembly and `tabix` on the input VCF. - -### Filtering results - -
-Output files - -- `filter/` - - `incremental/` - - VCF files (`*.vcf.gz`) and their associated index files (`*.tbi`) containing results of stepwise filtering operations. - - `bcftools/` - - Results of cumulative filtering operations performed using `bcftools` (`*.vcf.gz`, `*.tbi`) - - `vcftools/` - - Results of cumulative filtering operations performed using `cftools` (`*.vcf.gz`, `*.tbi`) - -
- -Results of VCF filtering. Contains both stepwise and and cumulative filter operations. - -### grenedalf sync - -
-Output files - -- `sync/` - - `pairwise/` - - `_-.sync`: Individual (headerless) pairwise sync file. - - `_sync.sync`: Sync file (in grenedalf headered format) for all pools. - -
- -Sync files generated from input VCF (or copied / filtered from input sync file). - -### Fisher's test results - -
-Output files - -- `fisher/` - - `assesspool/*.tsv`: pairwise Fisher's test results using built-in assessPool method - - `popoolation/*.fisher`: pairwise Fisher's test results using PoPoolation2 - - `combined/.fisher`: All concatenated pairwise Fisher's test results - - -
- -Results of Fisher's exact test calculations for each selected calculation method, both concatenated and single pairwise comparisons. - -### Fst results - -
-Output files - -- `fst/` - - `grenedalf/*.tsv`: Fst results calculated using `grenedalf`. - - `popoolation/*.fst`: Fst results calculated using `PoPoolation2`. - - `poolfstat/*.tsv`: Fst results calculated using `poolfstat`. - - `.fst`: Concatenated pairwise Fst results for all methods - -
- -Pairwise Fst results by individual calculation method and concatenated. - - ### Pipeline information
diff --git a/docs/usage.md b/docs/usage.md index 2e5788e..f3afd2c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,40 +1,63 @@ -# assessPool: Usage +# nf-core/assesspool: Usage + +## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/assesspool/usage](https://nf-co.re/assesspool/usage) + +> _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ ## Introduction -## Input spreadsheet + -First, prepare an input spreadsheet with your input data that looks as follows. Both csv and tsv formats are supported. The order of columns may be arbitrary, but column names must match those given below. +## Samplesheet input -`input.csv`: +You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row as shown in the examples below. -```csv -project,input,vcf_index,reference,pools,pool_sizes -poolseq_test,data/pools.vcf.gz,data/pools.vcf.gz.tbi,data/ref.fasta,,"35,38,22,52,17,19" +```bash +--input '[path to samplesheet file]' ``` -Each row represents a complete pool-seq project or experiment. Column descriptions: +### Multiple runs of the same sample -| Column | Required? | Description | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `project` | required | A brief unique identifier for this pool-seq project. | -| `input` | required | Variant description file in sync or VCF format (optionally compressed using `bgzip`) | -| `vcf_index` | optional | TABIX-format index of the input VCF file (generated if not supplied) | -| `reference` | required | Reference assembly (FASTA, optionally compressed using `bgzip`) | -| `pools` | optional | Comma-separated list of pool names (will replace any names in the input file) | -| `pool_sizes` | required | Number of individuals in each pool. Either a single number for uniform pool sizes or a comma-separated list of sizes for each pool. | +The `sample` identifiers have to be the same when you have re-sequenced the same sample more than once e.g. to increase sequencing depth. The pipeline will concatenate the raw reads before performing any downstream analysis. Below is an example for the same sample sequenced across 3 lanes: -An [example input sheet](../assets/input.csv) has been provided with the pipeline. +```csv title="samplesheet.csv" +sample,fastq_1,fastq_2 +CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz +CONTROL_REP1,AEG588A1_S1_L003_R1_001.fastq.gz,AEG588A1_S1_L003_R2_001.fastq.gz +CONTROL_REP1,AEG588A1_S1_L004_R1_001.fastq.gz,AEG588A1_S1_L004_R2_001.fastq.gz +``` -> [!NOTE] -> assessPool accepts compressed VCF and/or FASTA input, but it must be compressed using `bgzip` rather than `gzip`. +### Full samplesheet + +The pipeline will auto-detect whether a sample is single- or paired-end using the information provided in the samplesheet. The samplesheet can have as many columns as you desire, however, there is a strict requirement for the first 3 columns to match those defined in the table below. + +A final samplesheet file consisting of both single- and paired-end data may look something like the one below. This is for 6 samples, where `TREATMENT_REP3` has been sequenced twice. + +```csv title="samplesheet.csv" +sample,fastq_1,fastq_2 +CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz +CONTROL_REP2,AEG588A2_S2_L002_R1_001.fastq.gz,AEG588A2_S2_L002_R2_001.fastq.gz +CONTROL_REP3,AEG588A3_S3_L002_R1_001.fastq.gz,AEG588A3_S3_L002_R2_001.fastq.gz +TREATMENT_REP1,AEG588A4_S4_L003_R1_001.fastq.gz, +TREATMENT_REP2,AEG588A5_S5_L003_R1_001.fastq.gz, +TREATMENT_REP3,AEG588A6_S6_L003_R1_001.fastq.gz, +TREATMENT_REP3,AEG588A6_S6_L004_R1_001.fastq.gz, +``` + +| Column | Description | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Custom sample name. This entry will be identical for multiple sequencing libraries/runs from the same sample. Spaces in sample names are automatically converted to underscores (`_`). | +| `fastq_1` | Full path to FastQ file for Illumina short reads 1. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | +| `fastq_2` | Full path to FastQ file for Illumina short reads 2. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | + +An [example samplesheet](../assets/samplesheet.csv) has been provided with the pipeline. ## Running the pipeline The typical command for running the pipeline is as follows: ```bash -nextflow run tobodev/assesspool --input ./input.csv --outdir ./results -profile docker +nextflow run nf-core/assesspool --input ./samplesheet.csv --outdir ./results -profile docker ``` This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. @@ -44,7 +67,7 @@ Note that the pipeline will create the following files in your working directory ```bash work # Directory containing the nextflow working files # Finished results in specified location (defined with --outdir) -.nextflow.log.* # Log file from Nextflow +.nextflow_log # Log file from Nextflow # Other nextflow hidden files, eg. history of pipeline runs and old logs. ``` @@ -58,37 +81,36 @@ Pipeline settings can be provided in a `yaml` or `json` file via `-params-file < The above pipeline run specified with a params file in yaml format: ```bash -nextflow run tobodev/assesspool -profile docker -params-file params.yaml +nextflow run nf-core/assesspool -profile docker -params-file params.yaml ``` with: ```yaml title="params.yaml" -# params.yaml -input: './input.csv' +input: './samplesheet.csv' outdir: './results/' <...> ``` - +You can also generate such `YAML`/`JSON` files via [nf-core/launch](https://nf-co.re/launch). ### Updating the pipeline When you run the above command, Nextflow automatically pulls the pipeline code from GitHub and stores it as a cached version. When running the pipeline after this, it will always use the cached version if available - even if the pipeline has been updated since. To make sure that you're running the latest version of the pipeline, make sure that you regularly update the cached version of the pipeline: ```bash -nextflow pull tobodev/assesspool +nextflow pull nf-core/assesspool ``` ### Reproducibility It is a good idea to specify the pipeline version when running the pipeline on your data. This ensures that a specific version of the pipeline code and software are used when you run your pipeline. If you keep using the same tag, you'll be running the same version of the pipeline, even if there have been changes to the code since. -First, go to the [tobodev/assesspool releases page](https://github.com/tobodev/assesspool/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. +First, go to the [nf-core/assesspool releases page](https://github.com/nf-core/assesspool/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. -To further assist in reproducibility, you can share and reuse [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. +To further assist in reproducibility, you can use share and reuse [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. > [!TIP] > If you wish to share such profile (such as upload as supplementary material for academic publications), make sure to NOT include cluster specific paths to files, nor institutional specific profiles. @@ -116,12 +138,7 @@ If `-profile` is not specified, the pipeline will run locally and expect all sof - `test` - A profile with a complete configuration for automated testing - - Reduced-size input data for fast execution - - Includes links to test data so needs no other parameters other than optional container/package system (e.g., singularity) -- `test_full` - - A profile with a complete configuration for automated testing - - Full-sized input data - - Includes links to test data so needs no other parameters other than optional container/package system (e.g., singularity) + - Includes links to test data so needs no other parameters - `docker` - A generic configuration profile to be used with [Docker](https://docker.com/) - `singularity` diff --git a/main.nf b/main.nf index 278c271..ba59f81 100644 --- a/main.nf +++ b/main.nf @@ -1,9 +1,11 @@ #!/usr/bin/env nextflow /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - assesspool + nf-core/assesspool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Github : https://github.com/tobodev/assesspool + Github : https://github.com/nf-core/assesspool + Website: https://nf-co.re/assesspool + Slack : https://nfcore.slack.com/channels/assesspool ---------------------------------------------------------------------------------------- */ diff --git a/modules.json b/modules.json index 74e4ad7..d848745 100644 --- a/modules.json +++ b/modules.json @@ -1,6 +1,6 @@ { - "name": "assessPool", - "homePage": "https://github.com/tobodev/assesspool", + "name": "nf-core/assesspool", + "homePage": "https://github.com/nf-core/assesspool", "repos": { "https://github.com/nf-core/modules.git": { "modules": { @@ -15,6 +15,11 @@ "git_sha": "0e9cb409c32d3ec4f0d3804588e4778971c09b7e", "installed_by": ["modules"] }, + "bcftools/reheader": { + "branch": "master", + "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", + "installed_by": ["modules"] + }, "bcftools/view": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", diff --git a/modules/local/extractsequences/environment.yml b/modules/local/extractsequences/environment.yml deleted file mode 100644 index d7636b5..0000000 --- a/modules/local/extractsequences/environment.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json -channels: - - conda-forge - - bioconda -dependencies: - - bioconda::bioconductor-rsamtools=2.22.0 - - conda-forge::r-optparse=1.7.5 - - conda-forge::r-data.table=1.17.8 diff --git a/modules/local/extractsequences/main.nf b/modules/local/extractsequences/main.nf deleted file mode 100644 index 658a206..0000000 --- a/modules/local/extractsequences/main.nf +++ /dev/null @@ -1,58 +0,0 @@ -process EXTRACT_SEQUENCES { - tag "$meta.id" - label 'process_single_mem' - - conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/mulled-v2-24c2519e6f79268102473627bccca112fa9f03b6:b565a85a493491c260acda69b780ef6ffa1cda87-0': - 'biocontainers/mulled-v2-24c2519e6f79268102473627bccca112fa9f03b6:b565a85a493491c260acda69b780ef6ffa1cda87-0' }" - - input: - tuple val(meta), path(fasta), path(fai) - tuple val(meta), path(fst) - val(fst_cutoff) - - output: - tuple val(meta), path("*.fasta"), emit: fasta - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - """ - extractsequences.R \\ - $args \\ - --fst-cutoff ${fst_cutoff} \\ - --output ${prefix}.fasta \\ - --index ${fai} \\ - $fasta \\ - $fst - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") - optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - Rsamtools: \$(Rscript -e "cat(paste(packageVersion('Rsamtools')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") - END_VERSIONS - """ - - stub: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - """ - - touch ${prefix}.fasta - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") - optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - Rsamtools: \$(Rscript -e "cat(paste(packageVersion('Rsamtools')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") - END_VERSIONS - """ -} diff --git a/modules/local/extractsequences/meta.yml b/modules/local/extractsequences/meta.yml deleted file mode 100644 index e628be8..0000000 --- a/modules/local/extractsequences/meta.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json -name: "extractsequences" -## TODO nf-core: Add a description of the module and list keywords -description: write your description here -keywords: - - sort - - example - - genomics -tools: - - "extractsequences": - ## TODO nf-core: Add a description and other details for the software below - description: "" - homepage: "" - documentation: "" - tool_dev_url: "" - doi: "" - licence: - identifier: - -## TODO nf-core: Add a description of all of the variables used as input -input: - # Only when we have meta - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'sample1' ]` - - ## TODO nf-core: Delete / customise this example input - - bam: - type: file - description: Sorted BAM/CRAM/SAM file - pattern: "*.{bam,cram,sam}" - ontologies: - - edam: "http://edamontology.org/format_2572" # BAM - - edam: "http://edamontology.org/format_2573" # CRAM - - edam: "http://edamontology.org/format_3462" # SAM - -## TODO nf-core: Add a description of all of the variables used as output -output: - - bam: - #Only when we have meta - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'sample1' ]` - ## TODO nf-core: Delete / customise this example output - - "*.bam": - type: file - description: Sorted BAM/CRAM/SAM file - pattern: "*.{bam,cram,sam}" - ontologies: - - edam: "http://edamontology.org/format_2572" # BAM - - edam: "http://edamontology.org/format_2573" # CRAM - - edam: "http://edamontology.org/format_3462" # SAM - - - versions: - - "versions.yml": - type: file - description: File containing software versions - pattern: "versions.yml" - -authors: - - "@mhoban" -maintainers: - - "@mhoban" diff --git a/modules/local/extractsequences/resources/usr/bin/extractsequences.R b/modules/local/extractsequences/resources/usr/bin/extractsequences.R deleted file mode 100755 index d0b2de6..0000000 --- a/modules/local/extractsequences/resources/usr/bin/extractsequences.R +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env Rscript - -library(optparse) -library(Rsamtools) -library(data.table) - -# help message formatter -nice_formatter <- function(object) { - cat(object@usage, fill = TRUE) - cat(object@description, fill = TRUE) - cat("Options:", sep = "\n") - - options_list <- object@options - for (ii in seq_along(options_list)) { - option <- options_list[[ii]] - cat(" ") - if (!is.na(option@short_flag)) { - cat(option@short_flag) - if (optparse:::option_needs_argument(option)) { - cat(" ", toupper(option@metavar), sep = "") - } - cat(", ") - } - if (!is.null(option@long_flag)) { - cat(option@long_flag) - if (optparse:::option_needs_argument(option)) { - cat("=", toupper(option@metavar), sep = "") - } - } - cat("\n ") - cat(sub("%default", optparse:::as_string(option@default), option@help)) - cat("\n\n") - } - cat(object@epilogue, fill = TRUE) - return(invisible(NULL)) -} - -option_list <- list( - make_option(c("-i", "--index"), action="store", default=NA, type='character', help="FASTA index file"), - make_option(c("-o", "--output"), action="store", default=NA, type='character', help="Output FASTA file"), - make_option(c("-f", "--fst-cutoff"), action="store", default=0.7, type='double', help="Fst cutoff value") -) - -# use debug arguments if we have 'em -opt_args <- if (exists("debug_args")) debug_args else commandArgs(TRUE) - -# parse command-line options -opt <- parse_args( - OptionParser( - option_list=option_list, - formatter=nice_formatter, - prog="extractsequences.R", - usage="%prog [options] " - ), - convert_hyphens_to_underscores = TRUE, - positional_arguments = 2, - args = opt_args -) - -index_file <- opt$options$index -output_file <- opt$options$output -fst_cutoff <- opt$options$fst_cutoff - -if (!file.exists(opt$args[1])) { - stop(sprintf("FASTA file %s does not exist.",opt$args[1])) -} -if (!file.exists(opt$args[2])) { - stop(sprintf("Fst file %s does not exist.",opt$args[2])) -} -if (!is.na(index_file) & !file.exists(index_file)) { - stop(sprintf("Index file %s does not exist.",index_file)) -} - -# load indexed fasta file & get sequence lengths -fasta <- FaFile(file=opt$args[1],index=index_file) -lengths <- seqlengths(fasta) - -# use data.table for speed & memory (presumably) -# get sites with "strong" fst and create genomic ranges from them -fst <- fread(opt$args[2])[fst > fst_cutoff, ] -# group by contig and position, and string together calculation methods that got these high Fst values -fst <- fst[,.(methods = paste0(unique(method),collapse=',')), by=list(chrom,pos)][order(chrom,pos)] -# group by just contig and make a string that looks like pos1[];pos2[];etc. -fst <- fst[,.(positions = paste0(pos,"[",methods,"]",collapse=";")), by = chrom] -# pull in sequence lengths and make a 'name' column that's positions= -fst[, `:=`(len = lengths[chrom],name=paste0(chrom,' positions=',positions))] -# create range column -fst[,range := paste0(chrom,':1-',len)] - -# pull sequences from fasta using genomic ranges -ranges <- GRanges(fst$range) -seqs <- getSeq(fasta,ranges) -# assign new names to sequences including strongly differentiated positions -names(seqs) <- fst$name - -# save to fasta -writeXStringSet(seqs,output_file,format="fasta") diff --git a/modules/local/extractsequences/tests/main.nf.test b/modules/local/extractsequences/tests/main.nf.test deleted file mode 100644 index cdebcf8..0000000 --- a/modules/local/extractsequences/tests/main.nf.test +++ /dev/null @@ -1,73 +0,0 @@ -// TODO nf-core: Once you have added the required tests, please run the following command to build this file: -// nf-core modules test extractsequences -nextflow_process { - - name "Test Process EXTRACT_SEQUENCES" - script "../main.nf" - process "EXTRACT_SEQUENCES" - - tag "modules" - tag "modules_" - tag "extractsequences" - - // TODO nf-core: Change the test name preferably indicating the test-data and file-format used - test("sarscov2 - bam") { - - // TODO nf-core: If you are created a test for a chained module - // (the module requires running more than one process to generate the required output) - // add the 'setup' method here. - // You can find more information about how to use a 'setup' method in the docs (https://nf-co.re/docs/contributing/modules#steps-for-creating-nf-test-for-chained-modules). - - when { - process { - """ - // TODO nf-core: define inputs of the process here. Example: - - input[0] = [ - [ id:'test', single_end:false ], // meta map - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), - ] - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - //TODO nf-core: Add all required assertions to verify the test output. - // See https://nf-co.re/docs/contributing/tutorials/nf-test_assertions for more information and examples. - ) - } - - } - - // TODO nf-core: Change the test name preferably indicating the test-data and file-format used but keep the " - stub" suffix. - test("sarscov2 - bam - stub") { - - options "-stub" - - when { - process { - """ - // TODO nf-core: define inputs of the process here. Example: - - input[0] = [ - [ id:'test', single_end:false ], // meta map - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), - ] - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - //TODO nf-core: Add all required assertions to verify the test output. - ) - } - - } - -} diff --git a/modules/local/fishertest/environment.yml b/modules/local/fishertest/environment.yml index bbc8b6e..f101652 100644 --- a/modules/local/fishertest/environment.yml +++ b/modules/local/fishertest/environment.yml @@ -2,5 +2,10 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-readr=2.1.5 + - conda-forge::r-janitor=2.2.1 + - conda-forge::r-purrr=1.0.4 - conda-forge::r-optparse=1.7.5 - - conda-forge::r-data.table=1.17.8 + - conda-forge::r-matrixstats=1.5.0 diff --git a/modules/local/fishertest/main.nf b/modules/local/fishertest/main.nf index 510687e..ca3d5cb 100644 --- a/modules/local/fishertest/main.nf +++ b/modules/local/fishertest/main.nf @@ -4,8 +4,8 @@ process FISHERTEST { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/mulled-v2-91e4157032c00e9d9ac47f9837a67abee8f5afc2:7945c7dcdead60dfcbf06b19f4758634d6ad230a-0': - 'biocontainers/mulled-v2-91e4157032c00e9d9ac47f9837a67abee8f5afc2:7945c7dcdead60dfcbf06b19f4758634d6ad230a-0' }" + 'https://depot.galaxyproject.org/singularity/mulled-v2-dcb5179d6045c4df41d4319c99d446681271222e:4856497379063722ac7efef973f14ae775bd88ca-0': + 'biocontainers/mulled-v2-dcb5179d6045c4df41d4319c99d446681271222e:4856497379063722ac7efef973f14ae775bd88ca-0' }" input: tuple val(meta), val(pools), path(frequency) @@ -32,8 +32,13 @@ process FISHERTEST { cat <<-END_VERSIONS > versions.yml "${task.process}": R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") - optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") + optparse: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") + tibble: \$(Rscript -e "cat(paste(packageVersion('tidyr')),sep='.')") + janitor: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('janitor')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('matrixstats')),sep='.')") END_VERSIONS """ @@ -46,8 +51,13 @@ process FISHERTEST { cat <<-END_VERSIONS > versions.yml "${task.process}": R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") - optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") + optparse: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") + tibble: \$(Rscript -e "cat(paste(packageVersion('tidyr')),sep='.')") + janitor: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('janitor')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('matrixstats')),sep='.')") END_VERSIONS """ } diff --git a/modules/local/fishertest/resources/usr/bin/fisher.R b/modules/local/fishertest/resources/usr/bin/fisher.R index 70106dd..b0ce9c1 100755 --- a/modules/local/fishertest/resources/usr/bin/fisher.R +++ b/modules/local/fishertest/resources/usr/bin/fisher.R @@ -1,7 +1,15 @@ #!/usr/bin/env Rscript -suppressPackageStartupMessages(library(data.table)) +suppressPackageStartupMessages(library(dplyr)) +suppressPackageStartupMessages(library(tidyr)) +suppressPackageStartupMessages(library(readr)) +suppressPackageStartupMessages(library(janitor)) +suppressPackageStartupMessages(library(purrr)) suppressPackageStartupMessages(library(optparse)) +suppressPackageStartupMessages(library(matrixStats)) + +options(dplyr.summarise.inform = FALSE) + # help message formatter nice_formatter <- function(object) { @@ -51,7 +59,12 @@ gm_mean = function(x, na.rm=TRUE, zero.propagate = FALSE){ pval_callback <- function(opt, flag, val, parser, ...) { - char.expand(val,c("multiply","geometric-mean")) + val <- char.expand(val,c("multiply","geometric-mean")) + switch( + val, + 'multiply' = prod, + 'geometric-mean' = gm_mean + ) } expand_callback <- function(opt, flag, val, parser, args, ...) { @@ -78,7 +91,7 @@ option_list <- list( make_option(c("-C", "--min-count"), action="store", default=2, type='double', help="Minimum Minimal allowed read count per base"), make_option(c("-c", "--min-coverage"), action="store", default=0, type='double', help="Minimum coverage per pool"), make_option(c("-x", "--max-coverage"), action="store", default=1e03, type='double', help="Maximum coverage per pool"), - make_option(c("-a", "--pval-aggregate"), action="callback", default='multiply', type='character', help="P-value aggregation method",callback=pval_callback), + make_option(c("-a", "--pval-aggregate"), action="callback", default=prod, type='character', help="P-value aggregation method",callback=pval_callback), make_option(c("-A", "--all-snps"), action="store_true", default=FALSE, type='logical', help="Save fisher test results for all SNPs, regardless of window size"), make_option(c("-j", "--adjust-pval"), action="store_true", default=FALSE, type='logical', help="Adjust p-value for multiple comparisons"), make_option(c("-J", "--adjust-method"), action="store", default="bonferroni", type='character', help="P-value adjustment method"), @@ -123,11 +136,7 @@ threads <- opt$options$threads thread_lines <- opt$options$lines_per_thread file_prefix <- opt$options$prefix outdir <- opt$options$output_dir -p_combine <- switch( - opt$options$pval_aggregate, - 'multiply' = prod, - 'geometric-mean' = gm_mean -) +p_combine <- opt$options$pval_aggregate save_all <- opt$options$all_snps adjust_p <- opt$options$adjust_pval adjust_method <- opt$options$adjust_method @@ -149,12 +158,20 @@ if (!file.exists(opt$args[1])) { stop(sprintf("Frequency file %s does not exist.",opt$args[1])) } -freq_snps <- fread(opt$args[1]) +# filter frequency table down to just what we'd consider snps +# this filtering should match popoolation/poolfst + +# split(ceiling(seq_along(.)/50)) %>% + +freq_snps <- read_tsv(opt$args[1],col_types = cols(),progress = FALSE) if (all(is.na(pools))) { - pools <- melt(freq_snps[1], measure = measure(pool,measure,pattern = r'[^(.+)\.(REF_CNT)$]'),value.name = 'count')[ - pool != 'TOTAL' - ][order(pool)]$pool + pools <- freq_snps %>% + slice(1) %>% + pivot_longer(-c(CHROM:ALT,starts_with("TOTAL",ignore.case = FALSE)),names_to = c("pool","measure"),values_to="count",names_pattern = "^(.+)\\.([^.]+)$") %>% + pull(pool) %>% + sort() %>% + unique() } # make sure we're dealing with two pools @@ -164,102 +181,88 @@ if (npool != 2) { stop("Frequency file must contain exactly two pools") } -cols <- names(freq_snps) -pr <- paste0('^(',paste0(pools,collapse='|'),')') - +freq_snp_og <- freq_snps # continue filtering -# try to do as much data.table stuff in-place (using `:=`) as possible to increase memory efficiency - -# first, delete columns we don't care about -freq_snps[,cols[-c(1:4,grep(pr,cols))] := NULL ] -# add a total ref count -freq_snps[,TOTAL.REF_CNT := rowSums(.SD),.SDcols=patterns(r'[\.REF_CNT$]')] -# add a total alt count -freq_snps[,TOTAL.ALT_CNT := rowSums(.SD),.SDcols=patterns(r'[\.ALT_CNT$]')] -# add total depth -freq_snps[,TOTAL.DEPTH := rowSums(.SD),.SDcols=patterns(r'[\.DEPTH$]')] -# add window start -freq_snps[,start := floor((POS-1)/window_step)*window_step+1] -# add window end -freq_snps[,end := start+window_size-1] -# add window center (position) -freq_snps[,middle := floor((end+start)/2)] -# add window snp count -freq_snps[,snp_count := .N, by=.(CHROM,middle)] -# filter to minimum depth -freq_snps <- freq_snps[ freq_snps[,do.call(pmin,.SD) >= min_depth, .SDcols = patterns(r'[\.DEPTH$]')] ] -# filter to minimum count, remove invariant sites, and minimum depth -freq_snps <- freq_snps[ freq_snps[ , do.call(pmin,.SD) >= min_count, .SDcols = patterns(r'[^TOTAL\..+_CNT$]') ] ][ - TOTAL.REF_CNT != TOTAL.DEPTH & TOTAL.DEPTH >= min_depth -] -setnames(freq_snps,old=c("CHROM","POS","REF","ALT"),new=tolower) -setcolorder(freq_snps,neworder = c('chrom','pos','middle','ref','alt')) -freq_snps[,grep('total',names(freq_snps),ignore.case = TRUE) := NULL ] -cols <- names(freq_snps) -mv <- c(cols[1:3],'snp_count','start','end',grep(r'[\.DEPTH$]',cols,value=TRUE),grep(r'[_CNT$]',cols,value=TRUE)) -setcolorder(freq_snps,mv) - -# calculate fisher results - -# reshape frequency data and order ref and alt counts appropriately -fisher_results <- melt(freq_snps, measure = measure(pop,measure,pattern = r'[^(.+)\.(.+_CNT)]'),value.name = 'count')[order(chrom,pos,-measure,pop)] - -# get columns to group by -byc <- grep('^(pop|count|measure|ref|alt)$',names(fisher_results),invert = T,value = TRUE) -# calculate fisher tests for each combination -fisher_results <- fisher_results[,.(pval = fisher.test(matrix(count,ncol=2))$p.value),by=byc][order(chrom,pos)] -fisher_results[,avg_min_cov := do.call(pmin,.SD),.SDcols = patterns(r'[\.DEPTH$]')] +freq_snps <- freq_snp_og %>% + select(CHROM:ALT,starts_with(paste0(pools,"."),ignore.case = FALSE)) %>% + mutate( + `TOTAL.REF_CNT` = rowSums(pick(ends_with(".REF_CNT",ignore.case = FALSE))), + `TOTAL.ALT_CNT` = rowSums(pick(ends_with(".ALT_CNT",ignore.case = FALSE))), + `TOTAL.DEPTH` = rowSums(pick(ends_with(".DEPTH",ignore.case = FALSE))), + ) %>% + mutate( + lwr = floor((POS-1)/window_step)*window_step+1, + upr = lwr+window_size-1, + middle=floor((upr+lwr)/2) + ) %>% + filter( if_all(ends_with(".DEPTH"),~.x >= min_depth) ) %>% + add_count(CHROM,middle,name="snp_count") %>% + rename_with(make_clean_names,.cols = starts_with("TOTAL.",ignore.case = FALSE)) %>% + filter( + if_all(starts_with("total_",ignore.case = FALSE) & ends_with("_cnt",ignore.case = FALSE),~.x >= min_count), + total_ref_cnt != total_depth, + total_depth >= min_depth + ) %>% + rename_with(CHROM:ALT,.fn=make_clean_names) %>% + select(chrom,pos,middle,ref:alt,ends_with(".REF_CNT"),ends_with(".ALT_CNT"),starts_with("total_"),everything()) + +fisher_results <- freq_snps %>% + select(order(colnames(.))) %>% + select( + chrom, pos, middle, snp_count,start=lwr,end=upr, + ends_with(".DEPTH",ignore.case = FALSE), + ends_with(".REF_CNT",ignore.case = FALSE), + ends_with(".ALT_CNT",ignore.case = FALSE) + ) %>% + pivot_longer( + -c(chrom,pos,middle,snp_count,start,end,ends_with(".DEPTH",ignore.case = FALSE)), + names_to = c("pop","measure"), + values_to = "count", + names_pattern = "^(.+)\\.([^.]+)$" + ) %>% + group_by(chrom,pos,middle,start,end,snp_count,across(ends_with(".DEPTH",ignore.case = FALSE))) %>% + summarise(pval = fisher.test(matrix(count,ncol=2))$p.value) %>% + ungroup() %>% + mutate(min_cov = matrixStats::rowMins(as.matrix(pick(ends_with(".DEPTH",ignore.case = FALSE))))) if (adjust_p) { - fisher_results[, pval := p.adjust(pval,method=adjust_method)] + fisher_results <- fisher_results %>% + mutate(p_adj = p.adjust(pval,method=adjust_method)) } if (save_all & window_size > 1) { ss <- sprintf("%s/%s_%s_all_snps_fisher.tsv",outdir,file_prefix,paste0(pools,collapse="-")) - fisher_results[,`:=`( - log_fisher = -log10(pval), - pop1 = pools[1], - pop2 = pools[2], - window_size = 1, - covered_fraction = 1, - start=pos, - end=pos - )] - - fwrite( - fisher_results[,c('chrom','pos','covered_fraction','avg_min_cov','pop1','pop2','fisher'),with=FALSE], - file = ss, - sep="\t" - ) + fisher_results %>% + mutate( + fisher = -log10(pval), + pop1 = pools[1], + pop2 = pools[2], + window_size = 1, + covered_fraction = 1, + start=pos, + end=pos, + ) %>% + select(chrom,pos,snps,covered_fraction,avg_min_cov = min_cov,pop1,pop2,fisher) %>% + write_tsv(ss) } -if (window_type != 'single') { - fisher_results <- fisher_results[,.( - log_fisher = -log10(p_combine(pval)), +fisher_results <- fisher_results %>% + group_by(chrom,middle,start,end,snp_count) %>% + summarise( + fisher = -log10(p_combine(pval)), pop1 = pools[1], pop2 = pools[2], - window_size = .N, + window_size = n(), covered_fraction = unique(snp_count)/window_size, - avg_min_cov = mean(avg_min_cov) - ), by = .(chrom,middle,start,end,snp_count)] - fisher_results[,pos := floor((start+end)/2)] -} else { - fisher_results[,`:=`( - covered_fraction = 1, - window_size = 1, - pop1 = pools[1], - pop2 = pools[2], - log_fisher = -log10(pval) - )] -} - -fisher_results[,method := 'assesspool'] -cols <- names(fisher_results) -cn <- which(cols %in% c('chrom','pos','window_size','covered_fraction','avg_min_cov','pop1','pop2','log_fisher','method')) -fisher_results[,cols[-cn] := NULL] -setcolorder(fisher_results,c('chrom','pos','window_size','covered_fraction','avg_min_cov','pop1','pop2','log_fisher','method')) - + avg_min_cov = mean(min_cov) + ) %>% + ungroup() %>% + mutate( + pos = floor((start+end)/2), + method = 'assesspool' + ) %>% + select(chrom,pos,window_size,covered_fraction,avg_min_cov,pop1,pop2,fisher,method) ss <- sprintf("%s/%s_%s_window_%d_%d_fisher.tsv",outdir,file_prefix,paste0(pools,collapse="-"),window_size,window_step) -fwrite(fisher_results,ss,sep="\t") \ No newline at end of file +write_tsv(fisher_results,ss) \ No newline at end of file diff --git a/modules/local/grenedalf/frequency.nf b/modules/local/grenedalf/frequency.nf index 28c451b..4973c2f 100644 --- a/modules/local/grenedalf/frequency.nf +++ b/modules/local/grenedalf/frequency.nf @@ -25,6 +25,10 @@ process GRENEDALF_FREQUENCY { def fasta_arg = fasta ? "--reference-genome-fasta ${fasta}" : "" def fai_arg = fai ? "--reference-genome-fai ${fai}" : "" """ + # TODO: grenedalf command here + # // --sync-path ../two_pops.sync + # // --reference-genome-fai ../ref.fasta.fai + # // --file-prefix freq grenedalf frequency \\ --file-prefix "${prefix}_" \\ --threads ${task.cpus} \\ diff --git a/modules/local/grenedalf/sync.nf b/modules/local/grenedalf/sync.nf index 739f222..b558d42 100644 --- a/modules/local/grenedalf/sync.nf +++ b/modules/local/grenedalf/sync.nf @@ -9,10 +9,8 @@ process GRENEDALF_SYNC { input: tuple val(meta), path(vcf), path(index) - tuple val(meta), path(sync) tuple val(meta), path(fasta) tuple val(meta), path(fai) - tuple val(meta), path(sample_map) output: tuple val(meta), path("*.sync"), emit: sync @@ -26,18 +24,14 @@ process GRENEDALF_SYNC { def prefix = task.ext.prefix ?: "${meta.id}" def fasta_arg = fasta ? "--reference-genome-fasta ${fasta}" : "" def fai_arg = fai ? "--reference-genome-fai ${fai}" : "" - def input_opt = vcf ? '--vcf-path' : (sync ? '--sync-path' : '') - def input_arg = vcf ?: (sync ?: '') - def remap_arg = sample_map ? "--rename-samples-list ${sample_map}" : "" """ grenedalf sync \\ --threads ${task.cpus} \\ ${args} \\ ${fasta_arg} \\ ${fai_arg} \\ - ${remap_arg} \\ - ${input_opt} ${input_arg} \\ - --file-prefix "${prefix}_" + --file-prefix "${prefix}_" \\ + --vcf-path ${vcf} cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/joinfreq/environment.yml b/modules/local/joinfreq/environment.yml index 6a14ca3..7ff4f81 100644 --- a/modules/local/joinfreq/environment.yml +++ b/modules/local/joinfreq/environment.yml @@ -2,6 +2,10 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-readr=2.1.5 + - conda-forge::r-purrr=1.0.4 + - conda=forge::r-stringr=1.5.1 - conda-forge::r-optparse=1.7.5 - - conda-forge::r-data.table=1.17.8 diff --git a/modules/local/joinfreq/main.nf b/modules/local/joinfreq/main.nf index de88bb5..050dd97 100644 --- a/modules/local/joinfreq/main.nf +++ b/modules/local/joinfreq/main.nf @@ -4,8 +4,8 @@ process JOINFREQ { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/mulled-v2-91e4157032c00e9d9ac47f9837a67abee8f5afc2:7945c7dcdead60dfcbf06b19f4758634d6ad230a-0': - 'biocontainers/mulled-v2-91e4157032c00e9d9ac47f9837a67abee8f5afc2:7945c7dcdead60dfcbf06b19f4758634d6ad230a-0' }" + 'https://depot.galaxyproject.org/singularity/mulled-v2-1021c2bc41756fa99bc402f461dad0d1c35358c1:b0c847e4fb89c343b04036e33b2daa19c4152cf5-0': + 'biocontainers/mulled-v2-1021c2bc41756fa99bc402f461dad0d1c35358c1:b0c847e4fb89c343b04036e33b2daa19c4152cf5-0' }" input: tuple val(meta), path(frequency), path(fst), val(method) @@ -32,8 +32,12 @@ process JOINFREQ { cat <<-END_VERSIONS > versions.yml "${task.process}": R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") + dplyr: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") + tidyr: \$(Rscript -e "cat(paste(packageVersion('tidyr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + stringr: \$(Rscript -e "cat(paste(packageVersion('stringr')),sep='.')") optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") END_VERSIONS """ @@ -46,8 +50,12 @@ process JOINFREQ { cat <<-END_VERSIONS > versions.yml "${task.process}": R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") + dplyr: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") + tidyr: \$(Rscript -e "cat(paste(packageVersion('tidyr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + stringr: \$(Rscript -e "cat(paste(packageVersion('stringr')),sep='.')") optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") END_VERSIONS """ } diff --git a/modules/local/joinfreq/resources/usr/bin/joinfreq.R b/modules/local/joinfreq/resources/usr/bin/joinfreq.R index 014398c..05399e3 100755 --- a/modules/local/joinfreq/resources/usr/bin/joinfreq.R +++ b/modules/local/joinfreq/resources/usr/bin/joinfreq.R @@ -1,10 +1,17 @@ #!/usr/bin/env Rscript suppressPackageStartupMessages({ + library(dplyr) + library(tidyr) + library(readr) + library(purrr) + library(stringr) library(optparse) - library(data.table) }) +options(dplyr.summarise.inform = FALSE) + + # help message formatter nice_formatter <- function(object) { cat(object@usage, fill = TRUE) @@ -70,7 +77,7 @@ opt <- parse_args( OptionParser( option_list=option_list, formatter=nice_formatter, - prog="joinfreq.R", + prog="fisher.R", usage="%prog [options] " ), convert_hyphens_to_underscores = TRUE, @@ -98,73 +105,62 @@ if (!file.exists(opt$args[2])) { stop(sprintf("Fst file %s does not exist.",opt$args[1])) } -freq_snps <- fread(opt$args[1]) -fst <- fread(opt$args[2]) - -if (all(c("start","end") %in% names(fst))) { - fst[, pos := floor((start+end)/2) ] - fst[, c('start','end') := NULL ] - setcolorder(fst,c('chrom','pos')) +freq_snps <- read_tsv(opt$args[1],col_types = cols(),progress = FALSE) +fst_wide <- read_tsv(opt$args[2],col_types = cols(),progress = FALSE) +if (all(c("start","end") %in% names(fst_wide))) { + fst_wide <- fst_wide %>% + mutate(pos = floor((start+end)/2)) %>% + select(-c(start,end)) %>% + select(chrom,pos,everything()) } # load fst file, pivot long, and filter out NAs -if (calc_method == "grenedalf") { - # delete unused columns - cols <- names(fst) - fst[,c(cols[grep(r'[\.(missing|numeric|passed|masked|empty|invariant)$]',cols)]) := NULL] - - # make sure the pool name pairs are sorted, so that they - # look like a:b.fst and not like b:a.fst - - # first, pull out fst column names and strip off '.fst' - old_cols <- grep(r'[\.fst$]',names(fst),value=TRUE) - fst_cols <- gsub(r'[\.fst$]','',old_cols) - # split them into pairs by colon, sort the pairs, smash them back together, and re-add '.fst' - fst_cols <- paste0(sapply(strsplit(fst_cols,':'),\(x) paste0(sort(x),collapse=':')),'.fst') - # rename the original columns to the sorted version - setnames(fst,old = old_cols, new=fst_cols) - - # now convert to long format - fst <- melt(fst, measure = measure(pop1,pop2,pattern = r'[^([^:]+):(.+)\.fst$]'),value.name = 'fst') -} else { - setcolorder(fst,c('chrom','pos','pop1','pop2','fst')) - fst[,c(names(fst)[-c(1:5)]) := NULL] -} +fst <- fst_wide %>% + { + if (calc_method == "grenedalf") { + select(.,chrom:pos,ends_with(".fst",ignore.case = FALSE)) %>% + rename_with(\(n) { + str_match(n,"^(.+):(.+)\\.(fst)$") %>% + array_branch(1) %>% + map_chr(\(x) { + f <- sort(x[2:3]) + str_c(f[1],":",f[2],".",x[4]) + }) + },.cols = -c(chrom:pos)) %>% + pivot_longer(-c(chrom:pos),names_to=c("pop1","pop2",".value"),names_pattern = "^([^:]+):(.+)\\.(fst)$") + } else select(.,chrom,pos,pop1,pop2,fst) + } %>% + filter(!is.na(fst)) + +freq_snps <- freq_snps %>% + pivot_longer(-c(CHROM:ALT,starts_with("TOTAL",ignore.case = FALSE)),names_to = c("pool","measure"),values_to="count",names_pattern = "^(.+)\\.([^.]+)$") %>% + select(CHROM,POS,pool,measure,count) %>% + pivot_wider(names_from="measure",values_from = "count") %>% + rename_with(str_to_lower,.cols = c(1:2)) + +# continue filtering +combined <- fst %>% + left_join(freq_snps,by=c("chrom","pos","pop1" = "pool")) %>% + left_join(freq_snps,by=c("chrom","pos","pop2" = "pool"),suffix=c(".pop1",".pop2")) %>% + mutate( + # summarize read depths + TOTAL.REF_CNT = REF_CNT.pop1 + REF_CNT.pop2, + TOTAL.ALT_CNT = ALT_CNT.pop1 + ALT_CNT.pop2, + TOTAL.DEPTH = DEPTH.pop1 + DEPTH.pop2 + ) %>% + filter( + # filter by minimum minimum depth and others + DEPTH.pop1 >= min_depth & DEPTH.pop2 >= min_depth, + TOTAL.ALT_CNT >= min_count & TOTAL.REF_CNT >= min_count, + TOTAL.REF_CNT != TOTAL.DEPTH, + TOTAL.DEPTH >= min_depth + ) %>% + mutate( + avg_min_cov = pmin(DEPTH.pop1, DEPTH.pop2), + method = calc_method + ) %>% + arrange(chrom,pos,pop1,pop2) %>% + rename_with(str_to_lower) -# pivot frequency table longer -freq_snps[,c(grep('^TOTAL',names(freq_snps))) := NULL ] -freq_snps <- melt(freq_snps, measure = measure(pool,measure,pattern = r'[^(.+)\.([A-Z_]+)$]'),value.name = 'count') -freq_snps <- dcast(freq_snps,CHROM+POS+pool ~ measure,value.var = "count")[order(CHROM,POS,pool)] -setnames(freq_snps,1:2,new=tolower) - -combined <- merge(fst,freq_snps,by.x = c('chrom','pos','pop1'),by.y = c('chrom','pos','pool')) -combined <- merge(combined,freq_snps,by.x = c('chrom','pos','pop2'),by.y = c('chrom','pos','pool')) -setnames( - combined, - c('REF_CNT.x','REF_CNT.y','ALT_CNT.x','ALT_CNT.y','DEPTH.x','DEPTH.y'), - c('REF_CNT.pop1','REF_CNT.pop2','ALT_CNT.pop1','ALT_CNT.pop2','DEPTH.pop1','DEPTH.pop2') -) -combined[,`:=`( - TOTAL.REF_CNT = REF_CNT.pop1 + REF_CNT.pop2, - TOTAL.ALT_CNT = ALT_CNT.pop1 + ALT_CNT.pop2, - TOTAL.DEPTH = DEPTH.pop1 + DEPTH.pop2 -)] -combined <- combined[ - DEPTH.pop1 >= min_depth & DEPTH.pop2 >= min_depth & - TOTAL.ALT_CNT >= min_count & TOTAL.REF_CNT >= min_count & - TOTAL.REF_CNT != TOTAL.DEPTH & - TOTAL.DEPTH >= min_depth -][order(chrom,pos,pop1,pop2)] -combined[,`:=`( - avg_min_cov = do.call(pmin,.SD), - method = calc_method -), .SDcols = patterns('^DEPTH\\.')] -setnames(combined,new=tolower) -setcolorder( - combined, - c('chrom','pos','pop1','pop2','fst','avg_min_cov','method','alt_cnt.pop1','alt_cnt.pop2', - 'ref_cnt.pop1','ref_cnt.pop2','depth.pop1','depth.pop2','total.alt_cnt','total.ref_cnt','total.depth') -) - outfile <- sprintf("%s/%s_fst_frequency.tsv",outdir,file_prefix) -fwrite(combined,outfile,sep="\t") \ No newline at end of file +write_tsv(combined,outfile) \ No newline at end of file diff --git a/modules/local/poolfstat/fst/environment.yml b/modules/local/poolfstat/fst/environment.yml index 6f1a026..f2ca38b 100644 --- a/modules/local/poolfstat/fst/environment.yml +++ b/modules/local/poolfstat/fst/environment.yml @@ -4,4 +4,7 @@ channels: dependencies: - conda=forge::r-poolfstat=3.0.0 - conda=forge::r-optparse=1.7.5 - - conda-forge::r-data.table=1.17.8 + - conda=forge::r-tibble=3.2.1 + - conda=forge::r-stringr=1.5.1 + - conda=forge::r-readr=2.1.5 + - conda=forge::r-dplyr=1.1.4 diff --git a/modules/local/poolfstat/fst/main.nf b/modules/local/poolfstat/fst/main.nf index f4b7622..79835ab 100644 --- a/modules/local/poolfstat/fst/main.nf +++ b/modules/local/poolfstat/fst/main.nf @@ -4,8 +4,8 @@ process POOLFSTAT_FST { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/mulled-v2-618be27569d1bdadeed1044f0388638f6bfca2ae:095239e9e7e75cc2b7273c9a69167d844dd9b634-0': - 'biocontainers/mulled-v2-618be27569d1bdadeed1044f0388638f6bfca2ae:095239e9e7e75cc2b7273c9a69167d844dd9b634-0' }" + 'https://depot.galaxyproject.org/singularity/mulled-v2-5d425a6e26ab032a4dcba5bc997f43ab9299830a:a1958d454debf3df6f397d04d5f094df68682ef9-0': + 'biocontainers/mulled-v2-5d425a6e26ab032a4dcba5bc997f43ab9299830a:a1958d454debf3df6f397d04d5f094df68682ef9-0' }" input: tuple val(meta), val(pool_map), path(sync) @@ -39,7 +39,11 @@ process POOLFSTAT_FST { R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") poolfstat: \$(Rscript -e "cat(paste(packageVersion('poolfstat')),sep='.')") optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") + tibble: \$(Rscript -e "cat(paste(packageVersion('tibble')),sep='.')") + stringr: \$(Rscript -e "cat(paste(packageVersion('stringr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") END_VERSIONS """ @@ -54,7 +58,11 @@ process POOLFSTAT_FST { R: \$(Rscript -e "cat(paste(R.version[c('major','minor')],collapse='.'))") poolfstat: \$(Rscript -e "cat(paste(packageVersion('poolfstat')),sep='.')") optparse: \$(Rscript -e "cat(paste(packageVersion('optparse')),sep='.')") - data.table: \$(Rscript -e "cat(paste(packageVersion('data.table')),sep='.')") + tibble: \$(Rscript -e "cat(paste(packageVersion('tibble')),sep='.')") + stringr: \$(Rscript -e "cat(paste(packageVersion('stringr')),sep='.')") + readr: \$(Rscript -e "cat(paste(packageVersion('readr')),sep='.')") + purrr: \$(Rscript -e "cat(paste(packageVersion('purrr')),sep='.')") + dplyr: \$(Rscript -e "cat(paste(packageVersion('dplyr')),sep='.')") END_VERSIONS """ } diff --git a/modules/local/poolfstat/fst/resources/usr/bin/poolfstat.R b/modules/local/poolfstat/fst/resources/usr/bin/poolfstat.R index e484d62..08ef2d1 100755 --- a/modules/local/poolfstat/fst/resources/usr/bin/poolfstat.R +++ b/modules/local/poolfstat/fst/resources/usr/bin/poolfstat.R @@ -3,7 +3,10 @@ suppressPackageStartupMessages({ library(poolfstat) library(optparse) - library(data.table) + library(tibble) + library(stringr) + library(readr) + library(dplyr) }) @@ -96,31 +99,33 @@ pooldata <- popsync2pooldata( fst <- computeFST(pooldata,sliding.window.size=opt$options$window_size) # get population combo string -fn <- paste0(pooldata@poolnames,collapse='-') +fn <- str_c(pooldata@poolnames,collapse='-') # save global Fst -global_fst <- data.table(pop1=pooldata@poolnames[1],pop2=pooldata@poolnames[2],fst=fst$Fst[1]) -fwrite(global_fst,sprintf("%s_global_%s.tsv",opt$options$prefix,fn),sep="\t") +global_fst <- tibble(pop1=pooldata@poolnames[1],pop2=pooldata@poolnames[2],fst=fst$Fst[1]) +write_tsv(global_fst,str_glue("{opt$options$prefix}_global_{fn}.tsv")) # save sliding window Fst, if they exist if (!is.null(fst$sliding.windows.fvalues)) { - setDT(fst$sliding.windows.fvalues) - setnames(fst$sliding.windows.fvalues,new=c('chrom','start','end','mid','cum_mid','fst')) - fwrite(fst$sliding.windows.fvalues,sprintf("%s_sliding-%d_%s.tsv",opt$options$prefix,opt$options$window_size,fn),sep="\t") + sliding <- as_tibble(fst$sliding.windows.fvalues) %>% + rename(chrom=1,start=2,end=3,mid=4,cum_mid=5,fst=6) + write_tsv(sliding,str_glue("{opt$options$prefix}_sliding-{opt$options$window_size}_{fn}.tsv")) } -pools <- paste0(paste0(pooldata@poolnames,collapse=':'),".fst") - -setDT(pooldata@snp.info) -cols <- names(pooldata@snp.info) -pooldata@snp.info[,cols[-c(1:2)] := NULL] -setnames(pooldata@snp.info,c('chrom','pos')) -pooldata@snp.info[,`:=`( - window_size = opt$options$window_size, - covered_fraction = 1, - avg_min_cov = do.call(pmin,as.data.table(pooldata@readcoverage)), - pop1 = opt$options$pool_names[1], - pop2 = opt$options$pool_names[2], - fst = fst$snp.Fstats$Fst -)] -fwrite(pooldata@snp.info,sprintf("%s_%s.fst",opt$options$prefix,fn),col.names = opt$options$headers) +pools <- str_c(str_c(pooldata@poolnames,collapse=':'),".fst") + +# save per-snp Fst, match popoolation output +snp_fst <- pooldata@snp.info %>% + as_tibble() %>% + select(chrom=1,pos=2) %>% + mutate( + window_size = opt$options$window_size, + covered_fraction = 1, + # depth = rowSums(pooldata@readcoverage), + avg_min_cov = do.call(pmin,as.data.frame(pooldata@readcoverage)), + pop1 = opt$options$pool_names[1], + pop2 = opt$options$pool_names[2], + fst = fst$snp.Fstats$Fst + ) %>% + select(chrom,pos,window_size,covered_fraction,avg_min_cov,pop1,pop2,fst) +write_tsv(snp_fst,str_glue("{opt$options$prefix}_{fn}.fst"),col_names=opt$options$headers) \ No newline at end of file diff --git a/modules/nf-core/bcftools/reheader/environment.yml b/modules/nf-core/bcftools/reheader/environment.yml new file mode 100644 index 0000000..5c00b11 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/environment.yml @@ -0,0 +1,5 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bcftools=1.20 diff --git a/modules/nf-core/bcftools/reheader/main.nf b/modules/nf-core/bcftools/reheader/main.nf new file mode 100644 index 0000000..9cf6d0d --- /dev/null +++ b/modules/nf-core/bcftools/reheader/main.nf @@ -0,0 +1,79 @@ +process BCFTOOLS_REHEADER { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/bcftools:1.20--h8b25389_0': + 'biocontainers/bcftools:1.20--h8b25389_0' }" + + input: + tuple val(meta), path(vcf), path(header), path(samples) + tuple val(meta2), path(fai) + + output: + tuple val(meta), path("*.{vcf,vcf.gz,bcf,bcf.gz}"), emit: vcf + tuple val(meta), path("*.{csi,tbi}") , emit: index, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def fai_argument = fai ? "--fai $fai" : "" + def header_argument = header ? "--header $header" : "" + def samples_argument = samples ? "--samples $samples" : "" + + def args2 = task.ext.args2 ?: '--output-type z' + def extension = args2.contains("--output-type b") || args2.contains("-Ob") ? "bcf.gz" : + args2.contains("--output-type u") || args2.contains("-Ou") ? "bcf" : + args2.contains("--output-type z") || args2.contains("-Oz") ? "vcf.gz" : + args2.contains("--output-type v") || args2.contains("-Ov") ? "vcf" : + "vcf" + """ + bcftools \\ + reheader \\ + $fai_argument \\ + $header_argument \\ + $samples_argument \\ + $args \\ + --threads $task.cpus \\ + $vcf \\ + | bcftools view \\ + $args2 \\ + --output ${prefix}.${extension} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + bcftools: \$(bcftools --version 2>&1 | head -n1 | sed 's/^.*bcftools //; s/ .*\$//') + END_VERSIONS + """ + + stub: + def args2 = task.ext.args2 ?: '--output-type z' + def prefix = task.ext.prefix ?: "${meta.id}" + + def extension = args2.contains("--output-type b") || args2.contains("-Ob") ? "bcf.gz" : + args2.contains("--output-type u") || args2.contains("-Ou") ? "bcf" : + args2.contains("--output-type z") || args2.contains("-Oz") ? "vcf.gz" : + args2.contains("--output-type v") || args2.contains("-Ov") ? "vcf" : + "vcf" + def index = args2.contains("--write-index=tbi") || args2.contains("-W=tbi") ? "tbi" : + args2.contains("--write-index=csi") || args2.contains("-W=csi") ? "csi" : + args2.contains("--write-index") || args2.contains("-W") ? "csi" : + "" + def create_cmd = extension.endsWith(".gz") ? "echo '' | gzip >" : "touch" + def create_index = extension.endsWith(".gz") && index.matches("csi|tbi") ? "touch ${prefix}.${extension}.${index}" : "" + + """ + ${create_cmd} ${prefix}.${extension} + ${create_index} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + bcftools: \$(bcftools --version 2>&1 | head -n1 | sed 's/^.*bcftools //; s/ .*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/bcftools/reheader/meta.yml b/modules/nf-core/bcftools/reheader/meta.yml new file mode 100644 index 0000000..47e5344 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/meta.yml @@ -0,0 +1,76 @@ +name: bcftools_reheader +description: Reheader a VCF file +keywords: + - reheader + - vcf + - update header +tools: + - reheader: + description: | + Modify header of VCF/BCF files, change sample names. + homepage: http://samtools.github.io/bcftools/bcftools.html + documentation: http://samtools.github.io/bcftools/bcftools.html#reheader + doi: 10.1093/gigascience/giab008 + licence: ["MIT"] + identifier: biotools:bcftools +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - vcf: + type: file + description: VCF/BCF file + pattern: "*.{vcf.gz,vcf,bcf}" + - header: + type: file + description: New header to add to the VCF + pattern: "*.{header.txt}" + - samples: + type: file + description: File containing sample names to update (one sample per line) + pattern: "*.{samples.txt}" + - - meta2: + type: map + description: | + Groovy Map containing reference information + e.g. [ id:'genome' ] + - fai: + type: file + description: Fasta index to update header sequences with + pattern: "*.{fai}" +output: + - vcf: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.{vcf,vcf.gz,bcf,bcf.gz}": + type: file + description: VCF with updated header, bgzipped per default + pattern: "*.{vcf,vcf.gz,bcf,bcf.gz}" + - index: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.{csi,tbi}": + type: file + description: Index of VCF with updated header + pattern: "*.{csi,tbi}" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@bjohnnyd" + - "@jemten" + - "@ramprasadn" +maintainers: + - "@bjohnnyd" + - "@jemten" + - "@ramprasadn" diff --git a/modules/nf-core/bcftools/reheader/tests/bcf.config b/modules/nf-core/bcftools/reheader/tests/bcf.config new file mode 100644 index 0000000..2b7dff5 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/bcf.config @@ -0,0 +1,4 @@ +process { + ext.args2 = { "--no-version --output-type b" } + ext.prefix = "tested" +} \ No newline at end of file diff --git a/modules/nf-core/bcftools/reheader/tests/main.nf.test b/modules/nf-core/bcftools/reheader/tests/main.nf.test new file mode 100644 index 0000000..96c1b7b --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/main.nf.test @@ -0,0 +1,394 @@ +nextflow_process { + + name "Test Process BCFTOOLS_REHEADER" + script "../main.nf" + process "BCFTOOLS_REHEADER" + tag "modules" + tag "modules_nfcore" + tag "bcftools" + tag "bcftools/reheader" + + test("sarscov2 - [vcf, [], []], fai - vcf output") { + + config "./vcf.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output") { + + config "./vcf.gz.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - index") { + + config "./vcf_gz_index.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.vcf, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.versions + ).match() }, + { assert process.out.index[0][1].endsWith(".csi") } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - csi index") { + + config "./vcf_gz_index_csi.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.vcf, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.versions + ).match() }, + { assert process.out.index[0][1].endsWith(".csi") } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - tbi index") { + + config "./vcf_gz_index_tbi.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.vcf, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.versions + ).match() }, + { assert process.out.index[0][1].endsWith(".tbi") } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - bcf output") { + + config "./bcf.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, header, []], []") { + + config "./vcf.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf', checkIfExists: true), + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + [] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], samples], fai") { + + config "./vcf.config" + when { + + process { + """ + ch_no_samples = Channel.of([ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [] + ]) + ch_samples = Channel.of(["samples.txt", "new_name"]) + .collectFile(newLine:true) + input[0] = ch_no_samples.combine(ch_samples) + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - stub") { + + options "-stub" + config "./vcf.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.vcf[0][1]).name, + process.out.versions, + ).match() } + ) + } + + } + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - index -stub") { + + options "-stub" + config "./vcf_gz_index.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - csi index -stub") { + + options "-stub" + config "./vcf_gz_index_csi.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - [vcf, [], []], fai - vcf.gz output - tbi index -stub") { + + options "-stub" + config "./vcf_gz_index_tbi.config" + when { + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/vcf/test.vcf.gz', checkIfExists: true), + [], + [] + ] + input[1] = [ + [ id:'genome' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/bcftools/reheader/tests/main.nf.test.snap b/modules/nf-core/bcftools/reheader/tests/main.nf.test.snap new file mode 100644 index 0000000..87a3654 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/main.nf.test.snap @@ -0,0 +1,469 @@ +{ + "sarscov2 - [vcf, [], []], fai - vcf.gz output - tbi index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,8e722884ffb75155212a3fc053918766" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.tbi" + ] + ], + [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T10:09:05.955833763" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output - index -stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:52:41.444952182" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output - tbi index -stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.tbi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.tbi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:53:04.314827944" + }, + "sarscov2 - [vcf, [], []], fai - vcf output": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,8e722884ffb75155212a3fc053918766" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,8e722884ffb75155212a3fc053918766" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:50:41.983008108" + }, + "sarscov2 - [vcf, [], []], fai - bcf output": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.bcf.gz:md5,c8a304c8d2892039201154153c8cd536" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.bcf.gz:md5,c8a304c8d2892039201154153c8cd536" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:51:43.072513252" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf.gz:md5,8e722884ffb75155212a3fc053918766" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf.gz:md5,8e722884ffb75155212a3fc053918766" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:50:53.055630152" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output - index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,8e722884ffb75155212a3fc053918766" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi" + ] + ], + [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T10:08:37.999924355" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output - csi index -stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:52:52.512269206" + }, + "sarscov2 - [vcf, [], []], fai - stub": { + "content": [ + "tested.vcf", + [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-31T15:16:36.337112514" + }, + "sarscov2 - [vcf, [], []], fai - vcf.gz output - csi index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz:md5,8e722884ffb75155212a3fc053918766" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test_vcf.vcf.gz.csi" + ] + ], + [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T10:08:55.434831174" + }, + "sarscov2 - [vcf, [], samples], fai": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,c64c373c10b0be24b29d6f18708ec1e8" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,c64c373c10b0be24b29d6f18708ec1e8" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:52:12.216002665" + }, + "sarscov2 - [vcf, header, []], []": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,3189bc9a720d5d5d3006bf72d91300cb" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ], + "index": [ + + ], + "vcf": [ + [ + { + "id": "test", + "single_end": false + }, + "tested.vcf:md5,3189bc9a720d5d5d3006bf72d91300cb" + ] + ], + "versions": [ + "versions.yml:md5,486e3d4ebc1dbf5c0a4dfaebae12ea34" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "24.04.2" + }, + "timestamp": "2024-09-03T09:51:54.062386022" + } +} \ No newline at end of file diff --git a/modules/nf-core/bcftools/reheader/tests/tags.yml b/modules/nf-core/bcftools/reheader/tests/tags.yml new file mode 100644 index 0000000..c252941 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/tags.yml @@ -0,0 +1,2 @@ +bcftools/reheader: + - modules/nf-core/bcftools/reheader/** diff --git a/modules/nf-core/bcftools/reheader/tests/vcf.config b/modules/nf-core/bcftools/reheader/tests/vcf.config new file mode 100644 index 0000000..820f2ae --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/vcf.config @@ -0,0 +1,4 @@ +process { + ext.args2 = { "--no-version" } + ext.prefix = "tested" +} \ No newline at end of file diff --git a/modules/nf-core/bcftools/reheader/tests/vcf.gz.config b/modules/nf-core/bcftools/reheader/tests/vcf.gz.config new file mode 100644 index 0000000..c3031c3 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/vcf.gz.config @@ -0,0 +1,4 @@ +process { + ext.args2 = { "--no-version --output-type z" } + ext.prefix = "tested" +} \ No newline at end of file diff --git a/modules/nf-core/bcftools/reheader/tests/vcf_gz_index.config b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index.config new file mode 100644 index 0000000..1e050ec --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index.config @@ -0,0 +1,4 @@ +process { + ext.prefix = { "${meta.id}_vcf" } + ext.args2 = "--output-type z --write-index --no-version" +} diff --git a/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_csi.config b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_csi.config new file mode 100644 index 0000000..536e4b4 --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_csi.config @@ -0,0 +1,4 @@ +process { + ext.prefix = { "${meta.id}_vcf" } + ext.args2 = "--output-type z --write-index=csi --no-version" +} diff --git a/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_tbi.config b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_tbi.config new file mode 100644 index 0000000..91a80db --- /dev/null +++ b/modules/nf-core/bcftools/reheader/tests/vcf_gz_index_tbi.config @@ -0,0 +1,5 @@ +process { + ext.prefix = { "${meta.id}_vcf" } + ext.args2 = "--output-type z --write-index=tbi --no-version" + +} diff --git a/nextflow.config b/nextflow.config index d06e4e4..6441ffa 100644 --- a/nextflow.config +++ b/nextflow.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - assessPool Nextflow config file + nf-core/assesspool Nextflow config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default config options for all compute environments ---------------------------------------------------------------------------------------- @@ -9,6 +9,7 @@ // Global default params, used in configs params { + // TODO nf-core: Specify your pipeline's command line flags // Input options input = null @@ -45,18 +46,16 @@ params { visualize_filters = true filter_only = false - // report/output options - min_coverage_cutoff = 10 - max_coverage_cutoff = 70 - coverage_cutoff_step = 10 - extract_sequences = false + // report options + report_min_coverage = 10 + report_max_coverage = 70 + report_coverage_step = 10 // fst options popoolation2 = false grenedalf = false poolfstat = false missing_zeroes = false - fst_cutoff = 0.7 // popoolation/poolfstat min_count = 2 min_coverage = 10 @@ -82,7 +81,7 @@ params { // Boilerplate options - outdir = 'output' + outdir = null publish_dir_mode = 'symlink' email = null email_on_fail = null @@ -93,7 +92,7 @@ params { help_full = false show_hidden = false version = false - pipelines_testdata_base_path = 'https://zenodo.org/records/19058481/' + pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' trace_report_suffix = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss')// Config options config_profile_name = null config_profile_description = null @@ -233,11 +232,12 @@ profiles { // Load nf-core custom profiles from different institutions // If params.custom_config_base is set AND either the NXF_OFFLINE environment variable is not set or params.custom_config_base is a local path, the nfcore_custom.config file from the specified base path is included. -// Load assesspool custom profiles from different institutions. +// Load nf-core/assesspool custom profiles from different institutions. includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/nfcore_custom.config" : "/dev/null" -// Load assesspool custom profiles from different institutions. +// Load nf-core/assesspool custom profiles from different institutions. +// TODO nf-core: Optionally, you can add a pipeline-specific nf-core config at https://github.com/nf-core/configs // includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/assesspool.config" : "/dev/null" // Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile @@ -298,9 +298,10 @@ dag { } manifest { - name = 'assessPool' + name = 'nf-core/assesspool' author = """Evan B Freel, Emily E Conklin, Mykle L Hoban, Derek W Kraft, Jonathan L Whitney, Ingrid SS Knapp, Zac H Forsman, Robert J Toonen""" // The author field is deprecated from Nextflow version 24.10.0, use contributors instead contributors = [ + // TODO nf-core: Update the field with the details of the contributors to your pipeline. New with Nextflow version 24.10.0 [ name: 'Evan B Freel', affiliation: '', @@ -366,8 +367,8 @@ manifest { orcid: '' ], ] - homePage = 'https://github.com/tobodev/assesspool' - description = """nextflow-enabled version of assessPool pooled RADseq processing pipeline""" + homePage = 'https://github.com/nf-core/assesspool' + description = """nextflow-enabled version of assessPool radseq processing pipeline""" mainScript = 'main.nf' defaultBranch = 'main' nextflowVersion = '!>=24.04.2' @@ -385,7 +386,7 @@ validation { monochromeLogs = params.monochrome_logs help { enabled = true - command = "nextflow run tobodev/assesspool -profile --input samplesheet.csv --outdir " + command = "nextflow run nf-core/assesspool -profile --input samplesheet.csv --outdir " fullParameter = "help_full" showHiddenParameter = "show_hidden" beforeText = """ @@ -395,7 +396,7 @@ validation { \033[0;34m |\\ | |__ __ / ` / \\ |__) |__ \033[0;33m} {\033[0m \033[0;34m | \\| | \\__, \\__/ | \\ |___ \033[0;32m\\`-._,-`-,\033[0m \033[0;32m`._,._,\'\033[0m -\033[0;35m assessPool ${manifest.version}\033[0m +\033[0;35m nf-core/assesspool ${manifest.version}\033[0m -\033[2m----------------------------------------------------\033[0m- """ afterText = """${manifest.doi ? "\n* The pipeline\n" : ""}${manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${manifest.doi ? "\n" : ""} @@ -403,7 +404,7 @@ validation { https://doi.org/10.1038/s41587-020-0439-x * Software dependencies - https://github.com/tobodev/assesspool/blob/main/CITATIONS.md + https://github.com/nf-core/assesspool/blob/main/CITATIONS.md """ } summary { diff --git a/nextflow_schema.json b/nextflow_schema.json index 7b54eea..e350264 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/tobodev/assesspool/main/nextflow_schema.json", - "title": "assessPool pipeline parameters", + "$id": "https://raw.githubusercontent.com/nf-core/assesspool/main/nextflow_schema.json", + "title": "nf-core/assesspool pipeline parameters", "description": "nextflow-enabled version of assessPool radseq processing pipeline", "type": "object", "$defs": { @@ -19,7 +19,7 @@ "schema": "assets/schema_input.json", "mimetype": "text/csv", "pattern": "^\\S+\\.(csv|tsv)$", - "description": "Path to comma/tab-separated file containing information about pools..", + "description": "Path to comma-separated file containing information about the samples in the experiment.", "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/assesspool/usage#samplesheet-input).", "fa_icon": "fas fa-file-csv" }, @@ -147,7 +147,7 @@ "type": "string", "fa_icon": "far fa-check-circle", "description": "Base URL or local path to location of pipeline test dataset files", - "default": "https://zenodo.org/records/19058481/", + "default": "https://raw.githubusercontent.com/nf-core/test-datasets/", "hidden": true }, "trace_report_suffix": { @@ -201,7 +201,7 @@ "description": "p-value threshold for Hardy-Weinberg equlibrium test." }, "thin_snps": { - "type": "integer", + "type": "boolean", "description": "Thin sites so that no two sites are within the specified distance from one another." }, "min_mapping_quality": { @@ -287,11 +287,6 @@ "type": "boolean", "description": "Calculate Fst using `poolfstat`" }, - "fst_cutoff": { - "type": "number", - "default": 0.7, - "description": "Threshold at which Fst values are considered \"strongly differentiated.\"" - }, "match_allele_count": { "type": "boolean", "description": "Only keep sites where with matching allele counts across all pools (if something goes wrong early on, try this option)." @@ -383,24 +378,20 @@ "description": "", "default": "", "properties": { - "min_coverage_cutoff": { + "report_min_coverage": { "type": "integer", "default": 10, "description": "Minimum coverage cutoff for report visualizations." }, - "max_coverage_cutoff": { + "report_max_coverage": { "type": "integer", "default": 70, "description": "Maximum coverage cutoff for report visualizations." }, - "coverage_cutoff_step": { + "report_coverage_step": { "type": "integer", "default": 10, "description": "Report coverage cutoff step." - }, - "extract_sequences": { - "type": "boolean", - "description": "Extract reference contigs with sites having Fst > fst_cutoff (for any pairwise comparison)." } } } diff --git a/subworkflows/local/filter.nf b/subworkflows/local/filter.nf index 896dfc3..3e010da 100644 --- a/subworkflows/local/filter.nf +++ b/subworkflows/local/filter.nf @@ -6,20 +6,12 @@ include { BCFTOOLS_VIEW as BCFTOOLS_COMPRESS_INDEX_FILTERED } from '../../module workflow FILTER { take: - ch_input // channel: [ val(meta), [ vcf ] ] + ch_vcf // channel: [ val(meta), [ vcf ] ] main: ch_versions = Channel.empty() - ch_input = ch_input - .branch { meta, file, index -> - vcf: meta.format == 'vcf' - sync: meta.format == 'sync' - } - - ch_vcf = ch_input.vcf - // decide whether to run bcftools filter def bcff = params.match_allele_count || ( params.filter && ( params.min_mapping_quality || params.min_mapping_quality_ref || @@ -34,8 +26,10 @@ workflow FILTER { .map{ meta, vcf -> [ meta.id, meta, vcf ] } .join( BCFTOOLS_FILTER.out.tbi.map{ meta, index -> [ meta.id, index ] } ) .map{ id, meta, vcf, index -> [ meta, vcf, index ] } - .set{ ch_vcf } + .set{ ch_vcf2 } ch_versions = ch_versions.mix(BCFTOOLS_FILTER.out.versions.first()) + } else { + ch_vcf2 = ch_vcf } // decide whether to run vcftools filter @@ -43,39 +37,42 @@ workflow FILTER { params.max_mean_depth || params.hwe_cutoff ) if (vcft) { - VCFTOOLS_FILTER ( ch_vcf.map{ meta, vcf, index -> [ meta, vcf ] }, [], [] ) + VCFTOOLS_FILTER ( ch_vcf2.map{ meta, vcf, index -> [ meta, vcf ] }, [], [] ) ch_versions = ch_versions.mix(VCFTOOLS_FILTER.out.versions.first()) // TODO: re-compress and index output VCFTOOLS_FILTER.out.vcf .map{ meta, vcf -> [ meta, vcf, [] ] } - .set{ ch_vcf } + .set{ ch_vcf3 } + } else { + ch_vcf3 = ch_vcf2 } if (params.thin_snps) { - THIN_SNPS( ch_vcf.map{ meta, vcf, index -> [ meta, vcf ] }, [], [] ) + THIN_SNPS( ch_vcf3.map{ meta, vcf, index -> [ meta, vcf ] }, [], [] ) ch_versions = ch_versions.mix(THIN_SNPS.out.versions.first()) THIN_SNPS.out.vcf .map{ meta, vcf -> [ meta, vcf, [] ] } - .set{ ch_vcf } + .set{ ch_vcf4 } + } else { + ch_vcf4 = ch_vcf3 } if (vcft || params.thin_snps) { // re-index and compress if vcftools was run - BCFTOOLS_COMPRESS_INDEX_FILTERED( ch_vcf, [], [], [] ).vcf + BCFTOOLS_COMPRESS_INDEX_FILTERED( ch_vcf4, [], [], [] ).vcf .map{ meta,vcf -> [ meta.id, meta, vcf ] } .join( BCFTOOLS_COMPRESS_INDEX_FILTERED.out.tbi.map{ meta,index -> [ meta.id, index ] } ) .map{ id, meta, vcf, index -> [ meta, vcf, index ] } - .set{ ch_vcf } + .set{ ch_vcf_final } ch_versions = ch_versions.mix(BCFTOOLS_COMPRESS_INDEX_FILTERED.out.versions.first()) + } else { + ch_vcf_final = ch_vcf4 } - ch_vcf.mix( ch_input.sync ).set{ ch_output } - - // TODO: visualize filter results? (rmd section 'filter_visualization') emit: - output = ch_output + vcf = ch_vcf_final versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/local/filter_sim_vcf/main.nf b/subworkflows/local/filter_sim/main.nf similarity index 96% rename from subworkflows/local/filter_sim_vcf/main.nf rename to subworkflows/local/filter_sim/main.nf index 72f8a7b..b8671fa 100644 --- a/subworkflows/local/filter_sim_vcf/main.nf +++ b/subworkflows/local/filter_sim/main.nf @@ -19,7 +19,7 @@ include { RIPGREP as COUNT_SNPS } from '../../../modules/nf include { BCFTOOLS_VIEW as BCFTOOLS_COMPRESS_INDEX_FILTERED } from '../../../modules/nf-core/bcftools/view/main' -workflow FILTER_SIM_VCF { +workflow FILTER_SIM { take: ch_vcf @@ -179,7 +179,11 @@ workflow FILTER_SIM_VCF { // turn all the count maps into tsv files describing filtering results ch_filter_summary = ch_filter_summary - .collectFile(newLine: true, sort: true) { meta, filter -> [ "${meta.id}.filter", "${filter.filter}\t${filter.count}" ] } + .map{ meta, count -> meta.subMap('id') } + .unique() + .map{ meta -> [ meta, [ filter: 'filter', count: 'count' ] ] } + .concat( ch_filter_summary ) + .collectFile(newLine: true, sort: false) { meta, filter -> [ "${meta.id}.filter", "${filter.filter}\t${filter.count}" ] } .map{ [it.baseName, it] } // join back to meta tag diff --git a/subworkflows/local/filter_sim/meta.yml b/subworkflows/local/filter_sim/meta.yml new file mode 100644 index 0000000..35caf11 --- /dev/null +++ b/subworkflows/local/filter_sim/meta.yml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "filter_sim" +## TODO nf-core: Add a description of the subworkflow and list keywords +description: Sort SAM/BAM/CRAM file +keywords: + - sort + - bam + - sam + - cram +## TODO nf-core: Add a list of the modules and/or subworkflows used in the subworkflow +components: + - samtools/sort + - samtools/index +## TODO nf-core: List all of the channels used as input with a description and their structure +input: + - ch_bam: + type: file + description: | + The input channel containing the BAM/CRAM/SAM files + Structure: [ val(meta), path(bam) ] + pattern: "*.{bam/cram/sam}" +## TODO nf-core: List all of the channels used as output with a descriptions and their structure +output: + - bam: + type: file + description: | + Channel containing BAM files + Structure: [ val(meta), path(bam) ] + pattern: "*.bam" + - bai: + type: file + description: | + Channel containing indexed BAM (BAI) files + Structure: [ val(meta), path(bai) ] + pattern: "*.bai" + - csi: + type: file + description: | + Channel containing CSI files + Structure: [ val(meta), path(csi) ] + pattern: "*.csi" + - versions: + type: file + description: | + File containing software versions + Structure: [ path(versions.yml) ] + pattern: "versions.yml" +authors: + - "@mhoban" +maintainers: + - "@mhoban" diff --git a/subworkflows/local/filter_sim_vcf/tests/main.nf.test b/subworkflows/local/filter_sim/tests/main.nf.test similarity index 95% rename from subworkflows/local/filter_sim_vcf/tests/main.nf.test rename to subworkflows/local/filter_sim/tests/main.nf.test index 5f41de6..e5956fb 100644 --- a/subworkflows/local/filter_sim_vcf/tests/main.nf.test +++ b/subworkflows/local/filter_sim/tests/main.nf.test @@ -2,9 +2,9 @@ // nf-core subworkflows test filter_sim nextflow_workflow { - name "Test Subworkflow FILTER_SIM_VCF" + name "Test Subworkflow FILTER_SIM" script "../main.nf" - workflow "FILTER_SIM_VCF" + workflow "FILTER_SIM" tag "subworkflows" tag "subworkflows_" diff --git a/subworkflows/local/filter_sim_vcf/meta.yml b/subworkflows/local/filter_sim_vcf/meta.yml deleted file mode 100644 index 19e78a4..0000000 --- a/subworkflows/local/filter_sim_vcf/meta.yml +++ /dev/null @@ -1,34 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json -name: "filter_sim" -description: Sort SAM/BAM/CRAM file -keywords: - - sort - - bam - - sam - - cram -components: - - vcftools - - bcftools/filter - - ripgrep -input: - - ch_vcf: - type: file - description: | - Input VCF file on which to perform stepwise filtering - pattern: "*.{vcf/vcf.gz}" -output: - - filter_summary: - type: file - description: | - Tabular data showing SNPs remaining after various filtering steps - pattern: "*.filter" - - versions: - type: file - description: | - File containing software versions - Structure: [ path(versions.yml) ] - pattern: "versions.yml" -authors: - - "@mhoban" -maintainers: - - "@mhoban" diff --git a/subworkflows/local/poolstats.nf b/subworkflows/local/poolstats.nf index 8e159a6..2d714e4 100644 --- a/subworkflows/local/poolstats.nf +++ b/subworkflows/local/poolstats.nf @@ -13,7 +13,7 @@ include { combn } from "../../modules/local/popoolation2/combn workflow POOLSTATS { take: - ch_input // channel: [ val(meta), path(vcf), path(index) ] + ch_vcf // channel: [ val(meta), path(vcf), path(index) ] ch_ref // channel: path(ref) main: @@ -25,61 +25,34 @@ workflow POOLSTATS { // prepare vcf channel for input to grenedalf sync // (join fasta reference index) - ch_input - .map{ meta, file, index -> [ meta.id, meta, file, index ] } + ch_vcf + .map{ meta, vcf, index -> [ meta.id, meta, vcf, index ] } .join( ch_ref.map{ meta, ref, fai -> [ meta.id, ref, fai ] } ) - .map{ id, meta, file, index, ref, fai -> [ meta, file, index, ref, fai ] } + .map{ id, meta, vcf, index, ref, fai -> [ meta, vcf, index, ref, fai ] } .set{ ch_prep } - ch_prep - .map{ meta, file, index, ref, fai -> [ meta, meta.rename ? meta.pool_map : [:] ] } - // .filter { it[0].rename } - .collectFile(sort: true) { meta, pool_map -> [ "${meta.id}.map", pool_map ? pool_map.collect{k,v -> "${k}\t${v}"}.join("\n") : "" ] } - .map{ [ it.baseName, it ]} - .set{ ch_samplemap } - - ch_prep - .map{ [ it[0].id ] + it } - .join( ch_samplemap ) - .map{ id, meta, file, index, ref, fai, pool_map -> - [meta, file, index, ref, fai, pool_map.size() ? pool_map : [] ] - } - .set{ ch_prep } - - - // create/modify sync files using grenedalf + // create sync file using grenedalf GRENEDALF_SYNC( - ch_prep.map{ meta, file, index, ref, fai, map -> [ meta, meta.format == 'vcf' ? file : [], index ] }, - ch_prep.map{ meta, file, index, ref, fai, map -> [ meta, meta.format == 'sync' ? file : [] ] }, - ch_prep.map{ meta, file, index, ref, fai, map -> [ meta, [] ] }, - ch_prep.map{ meta, file, index, ref, fai, map -> [ meta, fai ] }, - ch_prep.map{ meta, file, index, ref, fai, map -> [ meta, map ] } + ch_prep.map{ meta, vcf, index, ref, fai -> [ meta, vcf, index ] }, + ch_prep.map{ meta, vcf, index, ref, fai -> [ meta, [] ] }, + ch_prep.map{ meta, vcf, index, ref, fai -> [ meta, fai ] } ) ch_versions = ch_versions.mix(GRENEDALF_SYNC.out.versions.first()) - // prepare and split input channel into pairwise comparisons - if (params.popoolation2 || params.poolfstat || params.fisher_test_popoolation) { - GRENEDALF_SYNC.out.sync - .map{ meta, sync -> [ meta, sync, combn(meta.pools.keySet() as String[],2) ] } - .transpose() - .map{ meta, sync, pair -> - // println meta.pools.subMap(pair).sort{ it.key }.values().toArray().getClass() - [ - meta + [ - pools: meta.pools.subMap(pair).sort{ it.key }, - pool_map: meta.pool_map.findAll{ it.value as String in pair as String[] }.sort{ it.key } - ], - sync - ] - } - .set{ ch_split_sync } + // prepare input channel to split sync file into pairwise comparisons + GRENEDALF_SYNC.out.sync + .map{ meta, sync -> [ meta, sync, combn(meta.pools.keySet() as String[],2) ] } + .transpose() + .map{ meta, sync, pair -> + [ [ id: meta.id, pools: meta.pools.subMap(pair).sort{ it.key } ], sync ] + } + .set{ ch_split_sync } - // pairwise split sync file - SPLIT_SYNC( ch_split_sync, [] ).output - .map{ meta, sync -> [ meta, meta.pools, sync ] } - .set{ ch_split_sync } - ch_versions = ch_versions.mix(SPLIT_SYNC.out.versions.first()) - } + // pairwise split sync file + SPLIT_SYNC( ch_split_sync, [] ).output + .map{ meta, sync -> [ meta, meta.pools, sync ] } + .set{ ch_split_sync } + ch_versions = ch_versions.mix(SPLIT_SYNC.out.versions.first()) // prepare input for frequency calculation GRENEDALF_SYNC.out.sync @@ -103,6 +76,8 @@ workflow POOLSTATS { ch_versions = ch_versions.mix(POPOOLATION2_FST.out.versions.first()) ch_versions = ch_versions.mix(REHEADER_FST.out.versions.first()) + // mix fst results + // ch_fst = ch_fst.mix( POPOOLATION2_FST.out.fst.map { meta, fst -> [ meta, fst, "popoolation" ] } ) ch_fst = ch_fst.mix( REHEADER_FST.out.output.map { meta, fst -> [ meta, fst, "popoolation" ] } ) } @@ -117,7 +92,7 @@ workflow POOLSTATS { // run grenedalf fst if requested if (params.grenedalf) { - // create pool size map file + // create pool size map GRENEDALF_SYNC.out.sync .collectFile { meta, sync -> [ "${meta.id}.ps", meta.pools.collect{ k, v -> "${k}\t${v}"}.join("\n") ] @@ -154,17 +129,17 @@ workflow POOLSTATS { // join pairwise fst results to snp frequencies and concatenate into one big file per input ch_join = JOINFREQ( ch_join ).fst_freq - .collectFile( keepHeader: true, sort: true, storeDir: 'output/fst', cache: true ){ meta, joined -> [ "${meta.id}.fst", joined ] } + .collectFile( keepHeader: true, sort: false, storeDir: 'output/fst' ){ meta, joined -> [ "${meta.id}.fst", joined ] } .map{ [it.baseName, it ] } - // rejoin meta tags - ch_join = ch_input + ch_join = ch_vcf .map{ it[0] } .unique() .map{ [ it.id, it ] } .join( ch_join ) .map{ id, meta, f -> [ meta, f ] } + ch_versions = ch_versions.mix(JOINFREQ.out.versions.first()) // run fisher tests if requested @@ -174,14 +149,7 @@ workflow POOLSTATS { .map{ meta, freq -> [ meta, freq, combn(meta.pools.keySet() as String[],2) ] } .transpose() .map{ meta, freq, pair -> - [ - meta + [ - pools: meta.pools.subMap(pair).sort{ it.key }, - pool_map: meta.pool_map.findAll{ it.value as String in pair as String[] }.sort{ it.key } - ], - pair.sort(), - freq - ] + [ [ id: meta.id, pools: meta.pools.subMap(pair).sort{ it.key } ], pair.sort(), freq ] } .set{ ch_split_freq } @@ -191,7 +159,6 @@ workflow POOLSTATS { // mix fisher results & versions ch_fisher = ch_fisher.mix( FISHERTEST.out.fisher ) ch_versions = ch_versions.mix(FISHERTEST.out.versions.first()) - } // run fisher tests with popoolation2 diff --git a/subworkflows/local/postprocess.nf b/subworkflows/local/postprocess.nf index 511eb6d..ec12ef9 100644 --- a/subworkflows/local/postprocess.nf +++ b/subworkflows/local/postprocess.nf @@ -1,11 +1,10 @@ -include { RMARKDOWNNOTEBOOK as CREATE_REPORT } from '../../modules/nf-core/rmarkdownnotebook/main' -include { RIPGREP as COUNT_SNPS_FINAL } from '../../modules/nf-core/ripgrep/main' -include { EXTRACT_SEQUENCES } from '../../modules/local/extractsequences/main' +include { RMARKDOWNNOTEBOOK as CREATE_REPORT } from '../../modules/nf-core/rmarkdownnotebook/main' +include { RIPGREP as COUNT_SNPS_FINAL } from '../../modules/nf-core/ripgrep/main' workflow POSTPROCESS { take: - ch_input + ch_vcf ch_unfiltered ch_fst ch_ref @@ -19,29 +18,21 @@ workflow POSTPROCESS { ch_versions = Channel.empty() - // extract reference contigs with "strongly differentiated" snps - if (params.extract_sequences) { - EXTRACT_SEQUENCES( ch_ref, ch_fst, params.fst_cutoff ) - ch_versions = ch_versions.mix(EXTRACT_SEQUENCES.out.versions.first()) - } - - // collapse pairwise fisher tests into single files ch_fisher_collapsed = ch_fisher - .collectFile( keepHeader: true, sort: true, storeDir: 'output/fisher/combined' ){ meta, fish -> [ "${meta.id}.fisher", fish ] } + .collectFile( keepHeader: true, sort: false, storeDir: 'output/fishertest' ){ meta, fish -> [ "${meta.id}.fisher", fish ] } .map{ [ it.baseName, it ] } - // join them back to meta tags - ch_fisher_collapsed = ch_input + ch_fisher_collapsed = ch_vcf .map{ it[0] } .unique() .map{ [ it.id, it ] } .join( ch_fisher_collapsed ) .map{ id, meta, f -> [ meta, f ] } - ch_count = ch_input - .map{ meta, input, index -> [ meta + [filter: 'cumulative'], input ] } - .mix( ch_unfiltered.map{ meta, input, index -> [ meta + [filter: 'before'], input ] } ) + ch_count = ch_vcf + .map{ meta, vcf, index -> [ meta + [filter: 'cumulative'], vcf ] } + .mix( ch_unfiltered.map{ meta, vcf, index -> [ meta + [filter: 'before'], vcf ] } ) // count final filtered SNPs into map COUNT_SNPS_FINAL( ch_count, '^#', false ) @@ -49,11 +40,14 @@ workflow POSTPROCESS { // collect final filter summary into tsv files ch_filter_final = ch_filter_final - .collectFile(newLine: true, sort: true ) { meta, filter -> [ "${meta.id}.final_filter", "${filter.filter}\t${filter.count}" ] } + .map{ meta, count -> meta.subMap('id') } + .unique() + .map{ meta -> [ meta, [ filter: 'filter', count: 'count' ] ] } + .concat( ch_filter_final ) + .collectFile(newLine: true, sort: false) { meta, filter -> [ "${meta.id}.final_filter", "${filter.filter}\t${filter.count}" ] } .map{ [ it.baseName, it ] } - // join them back to the meta tags - ch_filter_final = ch_input + ch_filter_final = ch_vcf .map{ it[0] } .unique() .map{ [ it.id, it ] } @@ -61,9 +55,9 @@ workflow POSTPROCESS { .map{ id, meta, f -> [ meta, f ] } // build rmarkdown report input and params - + def file_keys = [ 'fst', 'fisher', 'filter', 'final_filter' ] // get report file channel as [ meta, reportfile ] - ch_report = ch_input.map { [ it[0].id, it[0], file("${projectDir}/assets/assesspool_report.Rmd") ] } + ch_report = ch_vcf.map { [ it[0], file("${projectDir}/assets/assesspool_report.Rmd") ] } // generate input files channel ch_input_files = ch_fst.ifEmpty{ [] } @@ -71,43 +65,20 @@ workflow POSTPROCESS { .mix( ch_filter.ifEmpty{ [] } ) .mix( ch_filter_final.ifEmpty{ [] } ) .groupTuple() - .map{ [it[0].id] + it[1..-1] } - - - // subset the params object because there's at least one value that changes - // every time, which invalidates the caching - nf_params = [ - 'coverage_cutoff_step', - 'max_coverage_cutoff', - 'min_coverage_cutoff', - 'visualize_filters' - ] + // .collect() ch_params = ch_input_files. map{ meta, files -> [ meta, files.collect{ [ it.extension, it.name ] }.collectEntries() ] } .map{ meta, p -> [ meta, p + [ - nf: params.subMap(nf_params), + nf: params, tz: TimeZone.getDefault().getID() ]]} - // join everything together - ch_report - .join( ch_params ) - .join( ch_input_files ) - .map { it[1..-1] } - .set{ ch_report } - - // tuple val(meta), path(notebook) - // val parameters - // path input_files - CREATE_REPORT( - ch_report.map{ meta, report, params, files -> [meta, report] }, - ch_report.map{ meta, report, params, files -> params }, - ch_report.map{ meta, report, params, files -> files } - ) - ch_versions = ch_versions.mix(CREATE_REPORT.out.versions.first()) + CREATE_REPORT( ch_report, ch_params.map{meta, p -> p}, ch_input_files.map{meta, f -> f} ) emit: + // // TODO nf-core: edit emitted channels + versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/local/preprocess.nf b/subworkflows/local/preprocess.nf index 7f84061..bb19ca8 100644 --- a/subworkflows/local/preprocess.nf +++ b/subworkflows/local/preprocess.nf @@ -1,19 +1,9 @@ include { BCFTOOLS_QUERY as VCF_SAMPLES } from '../../modules/nf-core/bcftools/query/main' +include { BCFTOOLS_REHEADER as VCF_RENAME } from '../../modules/nf-core/bcftools/reheader/main' include { BCFTOOLS_VIEW as COMPRESS_VCF } from '../../modules/nf-core/bcftools/view/main' include { TABIX_TABIX as INDEX_VCF } from '../../modules/nf-core/tabix/tabix/main' include { SAMTOOLS_FAIDX as INDEX_REFERENCE } from '../../modules/nf-core/samtools/faidx/main' -// read the first line from a gzip'd file -def gz_head(Path file) { - new java.io.BufferedReader( - new java.io.InputStreamReader( - new java.util.zip.GZIPInputStream(new java.io.FileInputStream(file.toFile())) - ) - ).withCloseable { reader -> - return reader.readLine() - } -} - workflow PREPROCESS { take: @@ -25,7 +15,7 @@ workflow PREPROCESS { // save ref genome channel ch_samplesheet - .map { meta, input, index, ref -> [ meta, ref ] } + .map { meta, vcf, index, ref -> [ meta, ref ] } .set { ch_ref } // generate fasta ref index from reference fasta INDEX_REFERENCE( ch_ref, ch_ref.map{ meta, ref -> [ meta, [] ] } ) @@ -36,75 +26,68 @@ workflow PREPROCESS { .map{ id, meta, fasta, fai -> [ meta, fasta, fai ] } .set{ ch_ref } - // save input channel + // save vcf channel ch_samplesheet - .map { meta, input, index, ref -> [ meta, input, index ] } - .set { ch_input } + .map { meta, vcf, index, ref -> [ meta, vcf, index ] } + .set { ch_vcf } - // branch inputs into sync and/or vcf - ch_input - .branch { meta, input, index -> - vcf: input.toString() =~ /(?i)\.vcf(\.gz)?$/ - sync: input.toString() =~ /(?i)\.sync(\.gz)?$/ - } - .set{ ch_input } - - // preprocess VCF input(s) // Get VCF sample names from header regardless of whether we're renaming them - VCF_SAMPLES(ch_input.vcf,[],[],[]).output + VCF_SAMPLES(ch_vcf,[],[],[]).output .splitText() .map { meta, pool -> [ meta.id, pool.trim() ] } .groupTuple() .set { ch_samplenames } - ch_versions = ch_versions.mix(VCF_SAMPLES.out.versions.first()) + // Extract VCF sample names and smash them into a pool map - ch_input.vcf + ch_vcf .map{ meta, vcf, index -> [ meta.id, meta, vcf, index ] } .join( ch_samplenames ) .map { id, meta, vcf, index, pools -> def pp = meta.pools ?: pools def ps = meta.pool_sizes.size() > 1 ? meta.pool_sizes : (1..pools.size()).collect{ meta.pool_sizes[0] } - [ [ id: meta.id, format: 'vcf', rename: meta.rename, pools: [pp,ps].transpose().collectEntries(), pool_map: [pools,pp].transpose().collectEntries() ], vcf, index ] + [ [ id: meta.id, rename: meta.rename, pools: [pp,ps].transpose().collectEntries(), pool_map: [pools,pp].transpose().collectEntries() ], vcf, index ] } .set{ ch_vcf } - // preprocess sync input(s) - // Extract/get sync sample names the same way, more or less - ch_input.sync - .map{ meta, sync, index -> - def pools = (sync.extension.toLowerCase() == 'gz' ? gz_head(sync) : sync.withReader { it.readLine() }).split(/\t/) - def sync_hdr = !!(pools[0] =~ /^#chr/) - pools = sync_hdr ? pools[3..-1] : (1..(pools.size()-3)).collect{ "${sync.baseName}.${it}"} - - def pp = meta.pools ?: pools - assert pp.size() == pools.size() - def ps = meta.pool_sizes.size() > 1 ? meta.pool_sizes : (1..pools.size()).collect{ meta.pool_sizes[0] } - [ - meta + [ - format: 'sync', - sync_hdr: sync_hdr, - pools: [pp,ps].transpose().collectEntries{k,v -> [ k.toString(), v ]}, - pool_map: [pools,pp].transpose().collectEntries{k,v -> [ k.toString(), v.toString() ]} - ], - sync, - [] - ] - } - .set { ch_sync } - - // Figure out how to process vcf samples + // First, branch out which ones we need to rename, zip or index ch_vcf .branch { meta, vcf, index -> - // rename: meta.rename - zip: /*!meta.rename &&*/ vcf.extension != "gz" - index: /*!meta.rename &&*/ vcf.extension == "gz" && !index - keep_as_is: /*!meta.rename &&*/ vcf.extension == "gz" && index + rename: meta.rename + zip: !meta.rename && vcf.extension != "gz" + index: !meta.rename && vcf.extension == "gz" && !index + keep_as_is: !meta.rename && vcf.extension == "gz" && index } .set{ ch_vcf } + // rename VCF samples ----------------------------------- + // Create a sample name map file for bcftools reheader + ch_vcf.rename + // mapfiles named for meta.id + .collectFile { meta, vcf, index -> + [ "${meta.id}.map", meta.pool_map.collect{ k, v -> "${k} ${v}"}.join("\n") ] + } + // use mapfile basename for meta.id to join below + .map { f -> [ f.baseName, f ] } + .set{ ch_remap } + + // Join sample map back to VCF channel + ch_vcf.rename + .map { meta, vcf, index -> [ meta.id, meta, vcf ] } + .join( ch_remap ) + .map { id, meta, vcf, mapfile -> [ meta, vcf, [], mapfile ] } + .set{ ch_remap } + + // Rename samples in the VCF header + VCF_RENAME( ch_remap,ch_remap.map{ [it[0],[]]} ).vcf + .map{ meta, vcf -> [meta.id, meta, vcf] } + .join( VCF_RENAME.out.index.map{ meta, index -> [ meta.id, index ] } ) + .map{ id, meta, vcf, index -> [ meta, vcf, index ] } + .set { ch_renamed } + ch_versions = ch_versions.mix(VCF_RENAME.out.versions.first()) + // rename VCF samples ----------------------------------- // zip unzipped VCF ------------------------------------- COMPRESS_VCF( ch_vcf.zip,[],[],[] ) @@ -114,6 +97,7 @@ workflow PREPROCESS { .map{ id, meta, vcf, index -> [meta,vcf,index] } .set{ ch_zipped } ch_versions = ch_versions.mix(COMPRESS_VCF.out.versions.first()) + // zip unzipped VCF ------------------------------------- // index unindexed VCF ---------------------------------- INDEX_VCF( ch_vcf.index.map{ meta, vcf, index -> [ meta,vcf ] } ) @@ -122,17 +106,19 @@ workflow PREPROCESS { .map{ id, meta, vcf, index -> [meta, vcf, index] } .set{ ch_indexed } ch_versions = ch_versions.mix(INDEX_VCF.out.versions.first()) + // index unindexed VCF ---------------------------------- - // Mix back in any renamed bits + // // Mix back in any renamed bits ch_vcf.keep_as_is + .mix( ch_renamed ) .mix( ch_zipped ) .mix( ch_indexed ) - .mix( ch_sync ) - .set{ ch_input } + .set{ ch_vcf } + emit: - output = ch_input + vcf = ch_vcf ref = ch_ref versions = ch_versions // channel: [ versions.yml ] diff --git a/subworkflows/local/utils_nfcore_assesspool_pipeline/main.nf b/subworkflows/local/utils_nfcore_assesspool_pipeline/main.nf index dc5780d..82b585f 100644 --- a/subworkflows/local/utils_nfcore_assesspool_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_assesspool_pipeline/main.nf @@ -1,5 +1,5 @@ // -// Subworkflow with functionality specific to the assessPool pipeline +// Subworkflow with functionality specific to the nf-core/assesspool pipeline // /* @@ -73,17 +73,17 @@ workflow PIPELINE_INITIALISATION { Channel .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) .map { - meta, input, index, ref, pools, pool_sizes -> + meta, vcf, index, ref, pools, pool_sizes -> def pp = pools ? pools.split(/,/) : [] def ps = (pool_sizes instanceof Number) ? [pool_sizes] : pool_sizes.split(/,/) if (pp.size() != ps.size()) { if (pp && ps.size() != 1) { error "Pool sizes must either be a single number or a list the same length as `pools`" } else if (pp && ps.size() == 1) { - ps = (1..pp.size()).collect{ ps[0] } + ps = [1..pp.size()].collect{ ps[0] } } } - return [ meta + [ pools: pp, pool_sizes: ps, rename: !(!pools) ], input, index, ref ] + return [ meta + [ pools: pp, pool_sizes: ps, rename: !(!pools) ], vcf, index, ref ] } .set { ch_samplesheet } @@ -163,17 +163,11 @@ def validateInputSamplesheet(input) { // Generate methods description for MultiQC // def toolCitationText() { + // TODO nf-core: Optionally add in-text citation tools to this list. // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "Tool (Foo et al. 2023)" : "", // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ "Tools used in the workflow included:", - "PoPoolation2 (Kofler et al., 2011)", - "poolfstat (Gautier et al., 2022)", - "grenedalf (Czech & Expósito-Alonso, 2024)", - "bcftools (Danecek et al., 2021)", - "vcftools (Danecek et al., 2011)", - "samtools (Danecek et al., 2021)", - "Rsamtools (Morgan et al., 2024)", "." ].join(' ').trim() @@ -181,13 +175,10 @@ def toolCitationText() { } def toolBibliographyText() { + // TODO nf-core: Optionally add bibliographic entries to this list. + // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "
  • Author (2023) Pub name, Journal, DOI
  • " : "", + // Uncomment function in methodsDescriptionText to render in MultiQC report def reference_text = [ - "Kofler, R., Pandey, R. V., & Schlötterer, C. (2011). PoPoolation2: identifying differentiation between populations using sequencing of pooled DNA samples (Pool-Seq). Bioinformatics, 27(24), 3435-3436", - "Gautier, M., Vitalis, R., Flori, L., & Estoup, A. (2022). f-Statistics estimation and admixture graph construction with Pool-Seq or allele count data using the R package poolfstat. Molecular Ecology Resources, 22(4), 1394-1416", - "Czech, L., Spence, J. P., & Expósito-Alonso, M. (2024). grenedalf: population genetic statistics for the next generation of pool sequencing. Bioinformatics, 40(8), btae508", - "Danecek, P., Bonfield, J. K., Liddle, J., Marshall, J., Ohan, V., Pollard, M. O., ... & Li, H. (2021). Twelve years of SAMtools and BCFtools. Gigascience, 10(2), giab008", - "Danecek, P., Auton, A., Abecasis, G., Albers, C. A., Banks, E., DePristo, M. A., ... & 1000 Genomes Project Analysis Group. (2011). The variant call format and VCFtools. Bioinformatics, 27(15), 2156-2158", - "Morgan M, Pagès H, Obenchain V, Hayden N (2024). Rsamtools: Binary alignment (BAM), FASTA, variant call (BCF), and tabix file import. doi:10.18129/B9.bioc.Rsamtools, R package version 2.22.0, " ].join(' ').trim() return reference_text @@ -230,8 +221,12 @@ def methodsDescriptionText(mqc_methods_yaml) { meta["nodoi_text"] = meta.manifest_map.doi ? "" : "
  • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
  • " // Tool references - meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") - meta["tool_bibliography"] = toolBibliographyText() + meta["tool_citations"] = "" + meta["tool_bibliography"] = "" + + // TODO nf-core: Only uncomment below if logic in toolCitationText/toolBibliographyText has been filled! + // meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") + // meta["tool_bibliography"] = toolBibliographyText() def methods_text = mqc_methods_yaml.text diff --git a/workflows/assesspool.nf b/workflows/assesspool.nf index 281bf69..8980583 100644 --- a/workflows/assesspool.nf +++ b/workflows/assesspool.nf @@ -7,10 +7,10 @@ include { paramsSummaryMap } from 'plugin/nf-schema' include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_assesspool_pipeline' include { FILTER } from '../subworkflows/local/filter.nf' -include { FILTER_SIM_VCF } from '../subworkflows/local/filter_sim_vcf/main.nf' +include { FILTER_SIM } from '../subworkflows/local/filter_sim/main.nf' include { PREPROCESS } from '../subworkflows/local/preprocess.nf' include { POOLSTATS } from '../subworkflows/local/poolstats.nf' -include { POSTPROCESS } from '../subworkflows/local/postprocess.nf' +include { POSTPROCESS } from '../subworkflows/local/postprocess.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -24,12 +24,11 @@ workflow ASSESSPOOL { ch_samplesheet // channel: samplesheet read in from --input main: - ch_versions = Channel.empty() // run VCF preprocessing PREPROCESS( ch_samplesheet ) - ch_input = PREPROCESS.out.output + ch_vcf = PREPROCESS.out.vcf ch_ref = PREPROCESS.out.ref ch_versions = ch_versions.mix(PREPROCESS.out.versions.first()) @@ -42,21 +41,21 @@ workflow ASSESSPOOL { ch_fisher = Channel.empty() ch_filtered = Channel.empty() - // // perform stepwise filtration - // // for visualization and evaluation + // perform stepwise filtration + // for visualization and evaluation + + if (params.visualize_filters) { - FILTER_SIM_VCF( ch_input.filter { meta, f, index -> meta.format == 'vcf' } ) - ch_filter_sim = FILTER_SIM_VCF.out.filter_summary + FILTER_SIM( ch_vcf ) + ch_filter_sim = FILTER_SIM.out.filter_summary } - // TODO: add cumulative filtering to the filter_only workflow - // run downstream processing unless we're only // visualizing filters if (!params.filter_only) { // run filtering if desired - FILTER( ch_input ) - ch_filtered = FILTER.out.output + FILTER( ch_vcf ) + ch_filtered = FILTER.out.vcf ch_versions = ch_versions.mix(FILTER.out.versions.first()) // calculate pool statistics (fst, fisher, etc.) @@ -73,7 +72,7 @@ workflow ASSESSPOOL { // run post-processing steps POSTPROCESS( ch_filtered, - ch_input, + ch_vcf, ch_fst, ch_ref, ch_sync,