Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### `Added`

- [#1021](https://github.com/nf-core/ampliseq/pull/1021) - Comparison of observed ASVs against expected sequences or their abundance is now available with `--expected_*` parameters (by @d4straub)

### `Changed`

- [#1018](https://github.com/nf-core/ampliseq/pull/1018) - Change version to 2.19.0dev (by @d4straub)
Expand Down
175 changes: 175 additions & 0 deletions bin/compare_sequences.r
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env Rscript

# compare_sequences.r

# Get params and files from the command line
args <- commandArgs(trailingOnly=TRUE)
blast6outFILE <- args[1] # expected columns: "query+target+id+alnlen+mism+opens+qilo+qihi+tilo+tihi+ids+gaps+ql+tl+qstrand", see https://torognes.github.io/vsearch/misc/vsearch-userfields.7.html
detabundFILE <- args[2] # detabundFILE # calculated sequences*
Comment thread
d4straub marked this conversation as resolved.
Outdated
expabundFILE <- args[3] # expabundFILE # expected sequences*
prefix <- args[4] # prefix string for output files
similarity_threshold <- as.numeric(args[5]) # similarity threshold for blast6outFILE
query_or_target <- args[6] # use mismatches to quary or to target?
# tab-separated file with header: first column with sequences name, following one or many columns (=samples) with numeric values (=abundance), only presence (>0)/absence are used here

# Input
if (file.exists(expabundFILE)) {
print(paste("Compare file",detabundFILE,"to",expabundFILE))
} else {
print(paste("Compare",detabundFILE,"to all sequences"))
}

# PREPARE

# Read sequence alignment table
blast6out = read.table( blast6outFILE, header = FALSE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE)
colnames(blast6out) <- c("query","target","pident","alnlen","mismatch","gap_openings","qstart","qend","tstart","tend","ids","gaps","qlen","tlen","qstrand")
Comment thread
d4straub marked this conversation as resolved.
Outdated

# Read pipeline's ASV abundance table (either from QIIME2 [skipping first line] or from previous steps)
if (grepl("^# Constructed from biom file", readLines(detabundFILE, n = 1))) {
res = read.table( detabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE, comment.char = "", skip=1 )
} else {
res = read.table( detabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE, comment.char = "" )
}
colnames(res)[1] <- "ID"

# select available samples
res_samples <- colnames(res)[2:ncol(res)]
print(paste( "Measured samples:", paste(res_samples,collapse=",")))
SAMPLES <- res_samples

# Read expected abundance table if it exists
if (file.exists(expabundFILE)) {
exp = read.table( expabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE)
colnames(exp)[1] <- "ID"
# extract samples to analyse
exp_samples <- colnames(exp)[2:ncol(exp)]
print(paste( "Expected samples:", paste( exp_samples ,collapse=",")))
SAMPLES <- exp_samples[exp_samples %in% res_samples]
print(paste( "Investigate samples:", paste( SAMPLES ,collapse=",")))
# warn if samples are missing due to incomplete overlap
if ( !all(res_samples %in% SAMPLES) ) {
print(paste("WARN - sample(s) not in",expabundFILE,"and omitted:",paste(res_samples[!res_samples %in% SAMPLES], collapse="," )))
}
if ( !all(exp_samples %in% SAMPLES) ) {
print(paste("WARN - sample(s) not in pipeline output and omitted:",paste(exp_samples[!exp_samples %in% SAMPLES], collapse="," )))
}
}

# check if there are any samples to analyse
if (length(SAMPLES) == 0) {
stop("ERROR - Found no samples to investigate")
}

# Investigate blast6out, required columns: query,target,qlen,tlen,qstart,qend,tstart,tend,gaps,mismatch
Comment thread
d4straub marked this conversation as resolved.
Outdated
blast6out$qterminalgaps <- blast6out$qlen - (abs(blast6out$qend-blast6out$qstart)+1)
blast6out$qmismatch <- blast6out$gaps + blast6out$qterminalgaps + blast6out$mismatch
blast6out$tterminalgaps <- blast6out$tlen - (abs(blast6out$tend-blast6out$tstart)+1)
blast6out$tmismatch <- blast6out$gaps + blast6out$tterminalgaps + blast6out$mismatch
if( query_or_target=="query" ) {
blast6out$mismatch_final <- blast6out$qmismatch
} else if( query_or_target=="target" ) {
blast6out$mismatch_final <- blast6out$tmismatch
} else if( query_or_target=="alignment" ) {
Comment thread
d4straub marked this conversation as resolved.
blast6out$mismatch_final <- blast6out$gaps + blast6out$mismatch
} else {
stop( paste("ERROR -",query_or_target,"is not valid (valid: query,target,alignment)") )
}

# add sample info if available
if (file.exists(expabundFILE)) {
blast6out_target <- blast6out$target[blast6out$target != "*"]
if( !all(blast6out_target %in% exp$ID) ) {
print(paste("WARN - Some expected sequences in alignment results are not in",expabundFILE,". The following are missing:",paste( unique(blast6out_target[!blast6out_target %in% exp$ID]),collapse=",")))
}
if( !all(exp$ID %in% blast6out$target) ) {
print(paste("WARN - Some expected sequences of",expabundFILE,"are not in the alignment results. The following are missing:",paste( unique(exp$ID[!exp$ID %in% blast6out$target]),collapse=",")))
}
blast6out <- merge(blast6out,exp, by.x='target', by.y='ID', all.x=TRUE, all.y=TRUE)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dangerous in case column names overlap. This can be if at least one sample name is identical to a column name of the VSEARCH output. The likelihood can be reduced by minimizing VSEARCH output columns and adding a test whether column names and samples overlap. Do you agree?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When samples are column names this is of course a risk one should look for. Personally, I only work with "long" tables, i.e. tables with, typically, three or four columns: sample, asv_id, count (and relab if I've calculated relative abundance). This avoids the problem you see here, and makes data smaller since one can remove zeroes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a great point. I didnt consider that. I'll think about it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a long format would have been a great solution, it would need some adaptions. So I omitted merging tables that could have identical column names. This way it works as well, no conflicts possible whatsoever.

# swap target and query again
blast6out <- blast6out[, c("query", "target", setdiff(names(blast6out), c("target", "query")))]
}

# order for reproducibility
blast6out <- blast6out[order(blast6out$query, blast6out$target),]

# write file
outfile <- paste0(prefix,"nucleotide-differences.tsv")
print(paste("write",outfile))
write.table(blast6out, file = outfile, row.names = FALSE, col.names = TRUE, quote = FALSE, na = '', sep="\t")

# ANALYSE

# select matches above threshold
matches_above_threshold <- blast6out[blast6out$target !="*",]

for (sample in SAMPLES) {
if( file.exists(expabundFILE) ) {
# filter expected sequences in that sample (matches_above_threshold$target) with expected abundances (abundance > 0) of exp$ID
keep_cols <- c("ID",sample)
print(paste("Found",nrow(exp),"expected sequences overall in",expabundFILE))
s_exp <- subset(exp, select = keep_cols)
s_exp = s_exp[s_exp[,2] > 0,]
print(paste("Found",nrow(s_exp),"expected sequences in sample", sample,"in",expabundFILE))
s_matches <- matches_above_threshold[matches_above_threshold$target %in% s_exp$ID,]
} else {
s_matches <- matches_above_threshold
}

# sample will be skipped if s_matches has no data
if( nrow(s_matches)==0 ) {
print(paste("WARN - Skipping sample",sample,"because found no matches to expected sequences"))
next
} else {
print(paste("Found",length(unique(s_matches$query)),"to be expected sequences of",length(unique(matches_above_threshold$query)),"in sample",sample))
print(paste("Found",length(unique(s_matches$target)),"matches of",length(unique(matches_above_threshold$target)),"total in sample",sample))
}

# select best match (sort by mismatches and retain only first unique entry)
s_matches <- s_matches[order(s_matches$mismatch_final, as.numeric(s_matches$mismatch_final)), ]
s_matches <- s_matches[!duplicated(s_matches$query), ]
print(paste("Found",length(unique(s_matches$target)),"best matches of",length(unique(matches_above_threshold$target)),"total in sample",sample))

# filter for ASVs in that sample
keep_cols <- c("ID",sample)
s_res <- subset(res, select = keep_cols)
# keep only detected in that sample (abundance > 0)
s_res = s_res[s_res[,2] > 0,]
print(paste("Found",nrow(s_res),"ASVs of",nrow(res),"in sample",sample))

# filter alignment result by detected
s_matches <- s_matches[s_matches$query %in% s_res$ID,]
print(paste("Found",nrow(s_matches),"ASVs with match in sample",sample))
s_below_threshold <- s_res[!s_res$ID %in% s_matches$query,]
print(paste("Found",nrow(s_below_threshold),"ASV without match in sample",sample))

# make barplot
df <- as.data.frame( table(s_matches$mismatch_final) )
df <- rbind(df, data.frame(Var1 = paste0(">=",(1-similarity_threshold)*100,"%"), Freq = length(unique(s_below_threshold$ID))))
colnames(df) <- c("Mismatches to expected sequences","Number of sequences")

outfile <- paste0(prefix,sample,"_nucleotide-distance_barplot.svg")
print(paste("write",outfile))
svg(outfile, height = 4, width = 5)
barplot(df[,2],
names.arg=df[,1],
main=paste("Sample",sample,"with",length(unique(s_res$ID)),"detected sequences\n(",length(unique(s_matches$query)),"sequences above",similarity_threshold*100,"% similarity)"),
xlab="Mismatches to expected sequences",
ylab="Number of sequences")
invisible(dev.off())

outfile <- paste0(prefix,sample,"_nucleotide-distance_barplot.png")
print(paste("write",outfile))
png(outfile, height = 400, width = 500)
barplot(df[,2],
names.arg=df[,1],
main=paste("Sample",sample,"with",length(unique(s_res$ID)),"detected sequences\n(",length(unique(s_matches$query)),"sequences above",similarity_threshold*100,"% similarity)"),
xlab="Mismatches to expected sequences",
ylab="Number of sequences")
invisible(dev.off())

# write file
outfile <- paste0(prefix,sample,"_nucleotide-differences_per-sample.tsv")
print(paste("write",outfile))
write.table(df, file = outfile, row.names = FALSE, col.names = TRUE, quote = FALSE, na = '', sep="\t")
}
24 changes: 24 additions & 0 deletions conf/modules.config
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,30 @@ process {
]
}

withName: VSEARCH_USEARCHGLOBAL_BM {
ext.prefix = "vsearch_usearchglobal"
ext.args = "--strand both --maxaccepts 0 --maxrejects 0 --dbmask none --qmask none --output_no_hits"
publishDir = [
path: { "${params.outdir}/comparison/${meta.id}" },
mode: params.publish_dir_mode
]
}

withName: COMPARE_SEQUENCES {
publishDir = [
[
path: { "${params.outdir}/comparison/${meta.id}" },
mode: params.publish_dir_mode,
pattern: "*{nucleotide-differences.tsv,nucleotide-differences.log,md5sum_version.txt}"
],
[
path: { "${params.outdir}/comparison/${meta.id}/per-sample" },
mode: params.publish_dir_mode,
pattern: "*{.svg,.png,_per-sample.tsv}"
]
]
}

withName: 'MULTIQC' {
ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' }
publishDir = [
Expand Down
4 changes: 4 additions & 0 deletions conf/test_pplace_sheet.config
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ params {
skip_dada_taxonomy = true
qiime_ref_taxonomy = "greengenes85"

// Comparison to expected
expected_sequences = params.pipelines_testdata_base_path + "ampliseq/benchmarking/benchmarking_sequences.fasta"
expected_abundances = params.pipelines_testdata_base_path + "ampliseq/benchmarking/benchmarking_abundances.tsv"

// pplace
pplace_sheet = 'https://raw.githubusercontent.com/nf-core/test-datasets/phyloplace/testdata/phylosearch_input.csv'
}
3 changes: 3 additions & 0 deletions conf/test_qiimecustom.config
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ params {
RV_primer = "GGACTACNVGGGTWTCTAAT"
input = params.pipelines_testdata_base_path + "ampliseq/samplesheets/Samplesheet.tsv"

// Comparison to expected
expected_sequences = params.pipelines_testdata_base_path + "ampliseq/benchmarking/benchmarking_sequences.fasta"

// Custom reference taxonomy
qiime_ref_tax_custom = params.pipelines_testdata_base_path + "ampliseq/testdata/85_greengenes.fna.gz," + params.pipelines_testdata_base_path + "ampliseq/testdata/85_greengenes.tax.gz"

Expand Down
20 changes: 20 additions & 0 deletions docs/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d
- [Alpha diversity rarefaction curves](#alpha-diversity-rarefaction-curves) - Rarefaction curves for quality control
- [Diversity analysis](#diversity-analysis) - High level overview with different diversity indices
- [Differential abundance analysis](#differential-abundance-analysis) - Calling differentially abundant features with ANCOM or ANCOM-BC
- [Compare to expected](#compare-to-expected) - Comparing analysis outcome to expected results
- [PICRUSt2](#picrust2) - Predict the functional potential of a bacterial community
- [SBDI export](#sbdi-export) - Swedish Biodiversity Infrastructure (SBDI) submission file
- [R object](#r-objects) - Phyloseq and TreeSummarizedExperiment R objects
Expand Down Expand Up @@ -657,6 +658,25 @@ On request (`--ancombc2`), ANCOM-BC2 is applied to each suitable or specified me

</details>

### Compare to expected

Comparison evaluates observed results against expected outcomes, typically for samples with known composition such as mock communities, to assess the data and analysis. Steps to evaluate the produced ASVs are implemented in the pipeline.

<details markdown="1">
<summary>Output files</summary>

- `comparison/<parameter md5sum>_<pipeline version>`
- `vsearch_usearchglobal.tsv`: VSEARCH --usearch_global output for ASV to sequence comparisons.
- `md5sum_version.txt`: Contains parameter md5sum and pipeline version that is also the folder name.
- `nucleotide-differences.log`: Log file for comparing matches of ASVs to expected sequences
- `nucleotide-differences.tsv`: Tabl-separated table based on VSEARCH results comparing matches of ASVs to expected sequences
- `comparison/<parameter md5sum>_<pipeline version>/per-sample`
- `<sample>_nucleotide-differences_per-sample.tsv`: Number of sequences and mismatches to expected sequences
- `<sample>_nucleotide-distance_barplot.png`: Barplot of number of sequences versus number of mismatches
- `<sample>_nucleotide-distance_barplot.svg`: Barplot of number of sequences versus number of mismatches

</details>

### PICRUSt2

PICRUSt2 (Phylogenetic Investigation of Communities by Reconstruction of Unobserved States) is a software for predicting functional abundances based only on marker gene sequences. On demand (`--picrust`), Enzyme Classification numbers (EC), KEGG orthologs (KO) and MetaCyc ontology predictions will be made for each sample.
Expand Down
44 changes: 11 additions & 33 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,13 @@

> _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._

## Table of Contents

- [Running the pipeline](#running-the-pipeline)
- [Quick start](#quick-start)
- [Setting parameters in a file](#setting-parameters-in-a-file)
- [Input specifications](#input-specifications)
- [Sample sheet input](#sample-sheet-input)
- [ASV/OTU fasta input](#asvotu-fasta-input)
- [Direct FASTQ input](#direct-fastq-input)
- [Regions of variable length e.g. ITS](#regions-of-variable-length-eg-its)
- [ITS extraction tool](#its-extraction-tool)
- [Decontamination](#decontamination)
- [Taxonomic classification](#taxonomic-classification)
- [Multiple region analysis with Sidle](#multiple-region-analysis-with-sidle)
- [Metadata](#metadata)
- [Differential abundance analysis](#differential-abundance-analysis)
- [Phylogenetic placement](#phylogenetic-placement)
- [Single reference phylogenetic placement](#single-reference-phylogenetic-placement)
- [Multiple reference phylogenetic placement](#multiple-reference-phylogenetic-placement)
- [Placement in database-provided phylogenies](#placement-in-database-provided-phylogenies)
- [Updating the pipeline](#updating-the-pipeline)
- [Reproducibility](#reproducibility)
- [Core Nextflow arguments](#core-nextflow-arguments)
- [`-profile`](#-profile)
- [`-resume`](#-resume)
- [`-c`](#-c)
- [Custom configuration](#custom-configuration)
- [Resource requests](#resource-requests)
- [Custom Containers](#custom-containers)
- [Custom Tool Arguments](#custom-tool-arguments)
- [nf-core/configs](#nf-coreconfigs)
- [Running in the background](#running-in-the-background)
- [Nextflow memory requirements](#nextflow-memory-requirements)
## Introduction

The nf-core/ampliseq pipeline is a Nextflow-based workflow for amplicon sequencing analysis, supporting denoising (via DADA2) and taxonomic assignment (via QIIME2) for 16S, ITS, 18S, and other amplicons, with defaults optimized for Illumina paired-end 16S rRNA data.
By default, it performs quality filtering, denoising, chimera removal, taxonomic classification, and generates diversity metrics, count tables, and interactive visualizations, making it ready to use out-of-the-box for standard microbial community analyses.
You may want to customize the primer sequences, reference databases, or compute resources to match your data or infrastructure.
The pipeline also allows overriding default containers or adding custom parameters for specific tools, which is useful for keeping up with rapidly updated databases or specialized analyses.
For most users, the main sections of interest will be the input parameters (e.g., --input, --FW_primer, --RV_primer, --metadata) and output documentation, as these define how to tailor the pipeline to your project and interpret the results.

## Running the pipeline

Expand Down Expand Up @@ -362,6 +336,10 @@ Sample identifiers should be 36 characters long or less, and also contain only A

The columns which are to be assessed can be specified by `--metadata_category`. If `--metadata_category` isn't specified than all columns that fit the specification are automatically chosen.

### Comparison to expected outcomes

To investigate the quality of data generation and/or data analysis, analysis outcome is compared to expected results. Comparison steps are implemented in the pipeline and can be used with `--expected_*` parameters, details in the [parameter documentation](https://nf-co.re/ampliseq/parameters/#comparison).

### Differential abundance analysis

Differential abundance analysis for relative abundance from microbial community analysis are plagued by multiple issues that aren't fully solved yet. But some approaches seem promising, for example Analysis of Composition of Microbiomes with Bias Correction ([ANCOM-BC](https://pubmed.ncbi.nlm.nih.gov/32665548/)). [ANCOM](https://pubmed.ncbi.nlm.nih.gov/26028277/), ANCOM-BC, and [ANCOM-BC2](https://pubmed.ncbi.nlm.nih.gov/38158428/) are integrated into the pipeline, but only executed on request via `--ancom`, `--ancombc` or `--ancombc2`, more details in the [nf-core/ampliseq website parameter documentation](https://nf-co.re/ampliseq/parameters/#differential-abundance-analysis).
Expand Down
10 changes: 4 additions & 6 deletions modules/local/combine_table.nf
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
process COMBINE_TABLE {
tag "${meta.classifier}-${meta.database}"
label 'process_single'

conda "bioconda::bioconductor-biostrings=2.58.0 conda-forge::r-base=4.0.3"
Expand All @@ -7,17 +8,14 @@ process COMBINE_TABLE {
'biocontainers/bioconductor-biostrings:2.58.0--r40h037d062_0' }"

input:
path(table)
path(seq)
path(tax)
val(outfile)
tuple val(meta),path(tax),path(table),path(seq)

output:
path("${outfile}") , emit: tsv
tuple val(meta), path("rel-table-ASV_with-*-tax.tsv") , emit: tsv
path "versions.yml" , emit: versions_combine_table, topic: versions


script:
def outfile = "rel-table-ASV_with-${meta.classifier}-${meta.database}-tax.tsv"
"""
combine_table.r ${table} ${seq} ${tax}
mv combined_ASV_table.tsv ${outfile}
Expand Down
Loading
Loading