From 4c270110e1a57eb766e0cdc01fdd1439b54169e5 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 15 Aug 2024 08:29:42 +0200 Subject: [PATCH 001/147] bump version to v2.8dev --- assets/multiqc_config.yml | 4 ++-- nextflow.config | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index d5fcc74a..584bae69 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -1,7 +1,7 @@ report_comment: > - This report has been generated by the nf-core/scrnaseq + This report has been generated by the nf-core/scrnaseq analysis pipeline. For information about how to interpret these results, please see the - documentation. + documentation. report_section_order: "nf-core-scrnaseq-methods-description": order: -1000 diff --git a/nextflow.config b/nextflow.config index e1b608d2..70753bea 100644 --- a/nextflow.config +++ b/nextflow.config @@ -305,7 +305,7 @@ manifest { description = """Pipeline for processing 10x Genomics single cell rnaseq data""" mainScript = 'main.nf' nextflowVersion = '!>=23.04.0' - version = '2.7.1' + version = '2.8.0dev' doi = '10.5281/zenodo.3568187' } From 3aaf1688a5b9a3c4906d20a0673b19f4a53b9acf Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 15 Aug 2024 08:35:05 +0200 Subject: [PATCH 002/147] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1b6e41..1e6450ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 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). +## [Unreleased] + ## v2.7.1 - 2024-08-13 - Fix that tests have not been executed with nf-test v0.9 ([#359](https://github.com/nf-core/scrnaseq/pull/359)) From 52fa9ca8bb94e6e8b564d03ec41f2a1ae08291ab Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 11:22:53 +0200 Subject: [PATCH 003/147] comment out for development --- workflows/scrnaseq.nf | 62 +++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 10ced221..4c1ef914 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -290,37 +290,37 @@ workflow SCRNASEQ { } // Run emptydrops calling module - if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - - // - // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it - // - if ( params.aligner in [ 'cellranger', 'cellrangermulti', 'kallisto', 'star' ] ) { - ch_mtx_matrices_for_emptydrops = - ch_mtx_matrices.filter { meta, mtx_files -> - mtx_files.toString().contains("raw_feature_bc_matrix") || // cellranger - mtx_files.toString().contains("counts_unfiltered") || // kallisto - mtx_files.toString().contains("raw") // star - } - } else { - ch_mtx_matrices_for_emptydrops = ch_mtx_matrices - } - - EMPTYDROPS_CELL_CALLING( ch_mtx_matrices_for_emptydrops ) - ch_mtx_matrices = ch_mtx_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) - - } - - // Run mtx to h5ad conversion subworkflow - MTX_CONVERSION ( - ch_mtx_matrices, - ch_input, - ch_txp2gene, - ch_star_index - ) - - //Add Versions from MTX Conversion workflow too - ch_versions.mix(MTX_CONVERSION.out.ch_versions) + // if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { + + // // + // // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it + // // + // if ( params.aligner in [ 'cellranger', 'cellrangermulti', 'kallisto', 'star' ] ) { + // ch_mtx_matrices_for_emptydrops = + // ch_mtx_matrices.filter { meta, mtx_files -> + // mtx_files.toString().contains("raw_feature_bc_matrix") || // cellranger + // mtx_files.toString().contains("counts_unfiltered") || // kallisto + // mtx_files.toString().contains("raw") // star + // } + // } else { + // ch_mtx_matrices_for_emptydrops = ch_mtx_matrices + // } + + // EMPTYDROPS_CELL_CALLING( ch_mtx_matrices_for_emptydrops ) + // ch_mtx_matrices = ch_mtx_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) + + // } + + // // Run mtx to h5ad conversion subworkflow + // MTX_CONVERSION ( + // ch_mtx_matrices, + // ch_input, + // ch_txp2gene, + // ch_star_index + // ) + + // //Add Versions from MTX Conversion workflow too + // ch_versions.mix(MTX_CONVERSION.out.ch_versions) // // Collate and save software versions From bbf299c5ea80a4cd03e3e9add5d6d761ece9a1a2 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 12:19:37 +0200 Subject: [PATCH 004/147] refact modules for STAR aligner --- bin/mtx_to_h5ad_star.py | 89 +++++++++++++++++++++++++++++++ modules/local/mtx_to_h5ad_star.nf | 51 ++++++++++++++++++ subworkflows/local/starsolo.nf | 14 +++-- workflows/scrnaseq.nf | 3 -- 4 files changed, 150 insertions(+), 7 deletions(-) create mode 100755 bin/mtx_to_h5ad_star.py create mode 100644 modules/local/mtx_to_h5ad_star.nf diff --git a/bin/mtx_to_h5ad_star.py b/bin/mtx_to_h5ad_star.py new file mode 100755 index 00000000..a8c2ec22 --- /dev/null +++ b/bin/mtx_to_h5ad_star.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +# Set numba chache dir to current working directory (which is a writable mount also in containers) +import os + +os.environ["NUMBA_CACHE_DIR"] = "." + +import scanpy as sc +import pandas as pd +import argparse +from scipy import io +from anndata import AnnData + +def _mtx_to_adata( + mtx_file: str, + barcode_file: str, + feature_file: str, + sample: str, +): + adata = sc.read_mtx(mtx_file) + adata = adata.transpose() + adata.obs_names = pd.read_csv(barcode_file, header=None, sep="\t")[0].values + adata.var_names = pd.read_csv(feature_file, header=None, sep="\t")[0].values + adata.obs["sample"] = sample + + return adata + + +def input_to_adata( + input_data: str, + barcode_file: str, + feature_file: str, + sample: str, + star_index: str, +): + print("Reading in {}".format(input_data)) + + # open main data + adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample) + + # open gene information + txp2gene = "{}/geneInfo.tab".format(star_index) + print("Reading in {}".format(txp2gene)) + t2g = pd.read_table("{}".format(txp2gene), header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1]) + t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") + + # standard format + adata.var.index = t2g.index.values.tolist() + adata.var["gene_symbol"] = t2g["gene_symbol"] + + return adata + +def dump_versions(task_process): + import pkg_resources + + with open("versions.yml", "w") as f: + f.write(f"{task_process}:\n\t") + f.write("\n\t".join([f"{pkg.key}: {pkg.version}" for pkg in pkg_resources.working_set])) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Converts mtx output to h5ad.") + + parser.add_argument("-i", "--input_data", dest="input_data", help="Path to either mtx or mtx h5 file.") + parser.add_argument("-f", "--feature", dest="feature", help="Path to feature file.", nargs="?", const="") + parser.add_argument("-b", "--barcode", dest="barcode", help="Path to barcode file.", nargs="?", const="") + parser.add_argument("-s", "--sample", dest="sample", help="Sample name") + parser.add_argument("-o", "--out", dest="out", help="Output path.") + parser.add_argument("--task_process", dest="task_process", help="Task process name.") + parser.add_argument("--star_index", dest="star_index", help="Star index folder containing geneInfo.tab.", nargs="?", const="") + + args = vars(parser.parse_args()) + + # create the directory with the sample name + os.makedirs(os.path.dirname(args["out"]), exist_ok=True) + + adata = input_to_adata( + input_data=args["input_data"], + barcode_file=args["barcode"], + feature_file=args["feature"], + sample=args["sample"], + star_index=args["star_index"] + ) + + adata.write_h5ad(args["out"], compression="gzip") + + print("Wrote h5ad file to {}".format(args["out"])) + + dump_versions(task_process=args["task_process"]) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf new file mode 100644 index 00000000..ae42262f --- /dev/null +++ b/modules/local/mtx_to_h5ad_star.nf @@ -0,0 +1,51 @@ +process MTX_TO_H5AD_STAR { + tag "$meta.id" + label 'process_medium' + + conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : + 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + + input: + tuple val(meta), path(inputs) + path star_index + + output: + tuple val(meta2), path("${meta.id}/*h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Get a file to check input type. Some aligners bring arrays instead of a single file. + def input_to_check = (inputs instanceof String) ? inputs : inputs[0] + + // check input type of inputs + input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' + meta2 = meta + [input_type: input_type] + mtx_matrix = "${input_type}/matrix.mtx.gz" + barcodes_tsv = "${input_type}/barcodes.tsv.gz" + features_tsv = "${input_type}/features.tsv.gz" + + + """ + # convert file types + mtx_to_h5ad_star.py \\ + --task_process ${task.process} \\ + --sample ${meta.id} \\ + --input $mtx_matrix \\ + --barcode $barcodes_tsv \\ + --feature $features_tsv \\ + --star_index ${star_index} \\ + --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad + """ + + stub: + """ + mkdir ${meta.id} + touch ${meta.id}/${meta.id}_matrix.h5ad + touch versions.yml + """ +} diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 0c11acd1..19dc9221 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -1,5 +1,6 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { STAR_ALIGN } from '../../modules/local/star_align' +include { STAR_ALIGN } from '../../modules/local/star_align' +include { MTX_TO_H5AD_STAR as MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad_star' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ include { GUNZIP } from '../../modules/nf-core/gunzip/main' @@ -53,14 +54,19 @@ workflow STARSOLO { ) ch_versions = ch_versions.mix(STAR_ALIGN.out.versions) + /* + * Perform h5ad conversion + */ + MTX_TO_H5AD ( + STAR_ALIGN.out.raw_counts.mix( STAR_ALIGN.out.filtered_counts ), + star_index.map{ meta, index -> index } + ) + emit: ch_versions // get rid of meta for star index - star_index = star_index.map{ meta, index -> index } star_result = STAR_ALIGN.out.tab star_counts = STAR_ALIGN.out.counts - raw_counts = STAR_ALIGN.out.raw_counts - filtered_counts = STAR_ALIGN.out.filtered_counts for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 4c1ef914..e58978da 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -171,8 +171,6 @@ workflow SCRNASEQ { protocol_config.get('extra_args', ""), ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(STARSOLO.out.raw_counts, STARSOLO.out.filtered_counts) - ch_star_index = STARSOLO.out.star_index ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) } @@ -186,7 +184,6 @@ workflow SCRNASEQ { protocol_config['protocol'] ) ch_versions = ch_versions.mix(CELLRANGER_ALIGN.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_ALIGN.out.cellranger_matrices) ch_star_index = CELLRANGER_ALIGN.out.star_index ch_multiqc_files = ch_multiqc_files.mix(CELLRANGER_ALIGN.out.cellranger_out.map{ meta, outs -> outs.findAll{ it -> it.name == "web_summary.html"} From c3eb5eac38b09f916f9bb7a0091493053f4d5365 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 12:42:17 +0200 Subject: [PATCH 005/147] directly pass txp2gene --- bin/mtx_to_h5ad_star.py | 7 +++---- modules/local/mtx_to_h5ad_star.nf | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bin/mtx_to_h5ad_star.py b/bin/mtx_to_h5ad_star.py index a8c2ec22..0e388e61 100755 --- a/bin/mtx_to_h5ad_star.py +++ b/bin/mtx_to_h5ad_star.py @@ -31,7 +31,7 @@ def input_to_adata( barcode_file: str, feature_file: str, sample: str, - star_index: str, + txp2gene: str, ): print("Reading in {}".format(input_data)) @@ -39,7 +39,6 @@ def input_to_adata( adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample) # open gene information - txp2gene = "{}/geneInfo.tab".format(star_index) print("Reading in {}".format(txp2gene)) t2g = pd.read_table("{}".format(txp2gene), header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1]) t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") @@ -67,7 +66,7 @@ def dump_versions(task_process): parser.add_argument("-s", "--sample", dest="sample", help="Sample name") parser.add_argument("-o", "--out", dest="out", help="Output path.") parser.add_argument("--task_process", dest="task_process", help="Task process name.") - parser.add_argument("--star_index", dest="star_index", help="Star index folder containing geneInfo.tab.", nargs="?", const="") + parser.add_argument("--txp2gene", dest="txp2gene", help="Star index folder containing geneInfo.tab.", nargs="?", const="") args = vars(parser.parse_args()) @@ -79,7 +78,7 @@ def dump_versions(task_process): barcode_file=args["barcode"], feature_file=args["feature"], sample=args["sample"], - star_index=args["star_index"] + txp2gene=args["txp2gene"] ) adata.write_h5ad(args["out"], compression="gzip") diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf index ae42262f..35d6f2ef 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/mtx_to_h5ad_star.nf @@ -29,7 +29,6 @@ process MTX_TO_H5AD_STAR { barcodes_tsv = "${input_type}/barcodes.tsv.gz" features_tsv = "${input_type}/features.tsv.gz" - """ # convert file types mtx_to_h5ad_star.py \\ @@ -38,7 +37,7 @@ process MTX_TO_H5AD_STAR { --input $mtx_matrix \\ --barcode $barcodes_tsv \\ --feature $features_tsv \\ - --star_index ${star_index} \\ + --txp2gene ${star_index}/geneInfo.tab \\ --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad """ From 4771ed1fa1dc7301a856bfe8edcc04a0f2d9c342 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 13:02:50 +0200 Subject: [PATCH 006/147] simplify module lines --- modules/local/mtx_to_h5ad_star.nf | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf index 35d6f2ef..2ad9d4f0 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/mtx_to_h5ad_star.nf @@ -25,18 +25,15 @@ process MTX_TO_H5AD_STAR { // check input type of inputs input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' meta2 = meta + [input_type: input_type] - mtx_matrix = "${input_type}/matrix.mtx.gz" - barcodes_tsv = "${input_type}/barcodes.tsv.gz" - features_tsv = "${input_type}/features.tsv.gz" """ # convert file types mtx_to_h5ad_star.py \\ --task_process ${task.process} \\ --sample ${meta.id} \\ - --input $mtx_matrix \\ - --barcode $barcodes_tsv \\ - --feature $features_tsv \\ + --input ${input_type}/matrix.mtx.gz \\ + --barcode ${input_type}/barcodes.tsv.gz \\ + --feature ${input_type}/features.tsv.gz \\ --txp2gene ${star_index}/geneInfo.tab \\ --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad """ From af85f87512f06a1320db8083f111c23054ef473f Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 13:03:53 +0200 Subject: [PATCH 007/147] emit h5ad on starsolo --- subworkflows/local/starsolo.nf | 1 + 1 file changed, 1 insertion(+) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 19dc9221..1226ce5d 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -68,5 +68,6 @@ workflow STARSOLO { // get rid of meta for star index star_result = STAR_ALIGN.out.tab star_counts = STAR_ALIGN.out.counts + star_h5ad = MTX_TO_H5AD.out.h5ad for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } } From d2a53861a73114c2656cad6999f08d74eb3a9e8b Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 13:04:41 +0200 Subject: [PATCH 008/147] add versions emition --- subworkflows/local/starsolo.nf | 1 + 1 file changed, 1 insertion(+) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 1226ce5d..8236632d 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -61,6 +61,7 @@ workflow STARSOLO { STAR_ALIGN.out.raw_counts.mix( STAR_ALIGN.out.filtered_counts ), star_index.map{ meta, index -> index } ) + ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) emit: From 0e5505197854b2da1271bf90bd2a5cf89357c7b8 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 19 Aug 2024 13:35:07 +0200 Subject: [PATCH 009/147] update module to use templates and cleanup way of priting versions --- bin/mtx_to_h5ad_star.py | 88 ----------------- modules/local/mtx_to_h5ad_star.nf | 12 +-- modules/local/templates/mtx_to_h5ad_star.py | 104 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 99 deletions(-) delete mode 100755 bin/mtx_to_h5ad_star.py create mode 100755 modules/local/templates/mtx_to_h5ad_star.py diff --git a/bin/mtx_to_h5ad_star.py b/bin/mtx_to_h5ad_star.py deleted file mode 100755 index 0e388e61..00000000 --- a/bin/mtx_to_h5ad_star.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python - -# Set numba chache dir to current working directory (which is a writable mount also in containers) -import os - -os.environ["NUMBA_CACHE_DIR"] = "." - -import scanpy as sc -import pandas as pd -import argparse -from scipy import io -from anndata import AnnData - -def _mtx_to_adata( - mtx_file: str, - barcode_file: str, - feature_file: str, - sample: str, -): - adata = sc.read_mtx(mtx_file) - adata = adata.transpose() - adata.obs_names = pd.read_csv(barcode_file, header=None, sep="\t")[0].values - adata.var_names = pd.read_csv(feature_file, header=None, sep="\t")[0].values - adata.obs["sample"] = sample - - return adata - - -def input_to_adata( - input_data: str, - barcode_file: str, - feature_file: str, - sample: str, - txp2gene: str, -): - print("Reading in {}".format(input_data)) - - # open main data - adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample) - - # open gene information - print("Reading in {}".format(txp2gene)) - t2g = pd.read_table("{}".format(txp2gene), header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1]) - t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") - - # standard format - adata.var.index = t2g.index.values.tolist() - adata.var["gene_symbol"] = t2g["gene_symbol"] - - return adata - -def dump_versions(task_process): - import pkg_resources - - with open("versions.yml", "w") as f: - f.write(f"{task_process}:\n\t") - f.write("\n\t".join([f"{pkg.key}: {pkg.version}" for pkg in pkg_resources.working_set])) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Converts mtx output to h5ad.") - - parser.add_argument("-i", "--input_data", dest="input_data", help="Path to either mtx or mtx h5 file.") - parser.add_argument("-f", "--feature", dest="feature", help="Path to feature file.", nargs="?", const="") - parser.add_argument("-b", "--barcode", dest="barcode", help="Path to barcode file.", nargs="?", const="") - parser.add_argument("-s", "--sample", dest="sample", help="Sample name") - parser.add_argument("-o", "--out", dest="out", help="Output path.") - parser.add_argument("--task_process", dest="task_process", help="Task process name.") - parser.add_argument("--txp2gene", dest="txp2gene", help="Star index folder containing geneInfo.tab.", nargs="?", const="") - - args = vars(parser.parse_args()) - - # create the directory with the sample name - os.makedirs(os.path.dirname(args["out"]), exist_ok=True) - - adata = input_to_adata( - input_data=args["input_data"], - barcode_file=args["barcode"], - feature_file=args["feature"], - sample=args["sample"], - txp2gene=args["txp2gene"] - ) - - adata.write_h5ad(args["out"], compression="gzip") - - print("Wrote h5ad file to {}".format(args["out"])) - - dump_versions(task_process=args["task_process"]) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf index 2ad9d4f0..ded91538 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/mtx_to_h5ad_star.nf @@ -26,17 +26,7 @@ process MTX_TO_H5AD_STAR { input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' meta2 = meta + [input_type: input_type] - """ - # convert file types - mtx_to_h5ad_star.py \\ - --task_process ${task.process} \\ - --sample ${meta.id} \\ - --input ${input_type}/matrix.mtx.gz \\ - --barcode ${input_type}/barcodes.tsv.gz \\ - --feature ${input_type}/features.tsv.gz \\ - --txp2gene ${star_index}/geneInfo.tab \\ - --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad - """ + template 'mtx_to_h5ad_star.py' stub: """ diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py new file mode 100755 index 00000000..88bd91ec --- /dev/null +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +# Set numba chache dir to current working directory (which is a writable mount also in containers) +import os + +os.environ["NUMBA_CACHE_DIR"] = "." + +import scanpy as sc +import pandas as pd +import argparse +from anndata import AnnData +import platform + +def _mtx_to_adata( + mtx_file: str, + barcode_file: str, + feature_file: str, + sample: str, +): + adata = sc.read_mtx(mtx_file) + adata = adata.transpose() + adata.obs_names = pd.read_csv(barcode_file, header=None, sep="\t")[0].values + adata.var_names = pd.read_csv(feature_file, header=None, sep="\t")[0].values + adata.obs["sample"] = sample + + return adata + + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +def dump_versions(): + versions = { + "${task.process}": { + "python": platform.python_version(), + "scanpy": sc.__version__, + "pandas": pd.__version__ + } + } + + with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) + +def input_to_adata( + input_data: str, + output: str, + barcode_file: str, + feature_file: str, + sample: str, + txp2gene: str, +): + print(f"Reading in {input_data}") + + # open main data + adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample) + + # open gene information + print(f"Reading in {txp2gene}") + t2g = pd.read_table(f"{txp2gene}", header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1]) + t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") + + # standard format + adata.var.index = t2g.index.values.tolist() + adata.var["gene_symbol"] = t2g["gene_symbol"] + + # write results + adata.write_h5ad(f"{output}", compression="gzip") + print(f"Wrote h5ad file to {output}") + + # dump versions + dump_versions() + + return adata + +# +# Run main script +# + +# create the directory with the sample name +os.makedirs("${meta.id}", exist_ok=True) + +# input_type comes from NF module +adata = input_to_adata( + input_data="${input_type}/matrix.mtx.gz", + output="${meta.id}/${meta.id}_${input_type}_matrix.h5ad", + barcode_file="${input_type}/barcodes.tsv.gz", + feature_file="${input_type}/features.tsv.gz", + sample="${meta.id}", + txp2gene="${star_index}/geneInfo.tab" +) From 994d5a530d969c864c9c9465ef2d09cd083d9f49 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 20 Aug 2024 12:41:36 +0200 Subject: [PATCH 010/147] fix h5ad generator script --- modules/local/mtx_to_h5ad_star.nf | 6 ++-- modules/local/templates/mtx_to_h5ad_star.py | 33 +++++++-------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf index ded91538..1895cb87 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/mtx_to_h5ad_star.nf @@ -2,10 +2,8 @@ process MTX_TO_H5AD_STAR { tag "$meta.id" label 'process_medium' - conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : - 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" + container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" input: tuple val(meta), path(inputs) diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index 88bd91ec..d6889671 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -12,15 +12,10 @@ import platform def _mtx_to_adata( - mtx_file: str, - barcode_file: str, - feature_file: str, + input: str, sample: str, ): - adata = sc.read_mtx(mtx_file) - adata = adata.transpose() - adata.obs_names = pd.read_csv(barcode_file, header=None, sep="\t")[0].values - adata.var_names = pd.read_csv(feature_file, header=None, sep="\t")[0].values + adata = sc.read_10x_mtx(input) adata.obs["sample"] = sample return adata @@ -58,24 +53,19 @@ def dump_versions(): def input_to_adata( input_data: str, output: str, - barcode_file: str, - feature_file: str, sample: str, - txp2gene: str, ): print(f"Reading in {input_data}") # open main data - adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample) - - # open gene information - print(f"Reading in {txp2gene}") - t2g = pd.read_table(f"{txp2gene}", header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1]) - t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") + adata = _mtx_to_adata(input_data, sample) # standard format - adata.var.index = t2g.index.values.tolist() - adata.var["gene_symbol"] = t2g["gene_symbol"] + # index are gene IDs and symbols are a column + adata.var["gene_symbol"] = adata.var.index + adata.var[['gene_ids', 'gene_versions']] = adata.var["gene_ids"].apply(lambda x: pd.Series(str(x).split("."))) + adata.var.index = adata.var["gene_ids"].values + adata.var = adata.var.drop("gene_ids", axis=1) # write results adata.write_h5ad(f"{output}", compression="gzip") @@ -95,10 +85,7 @@ def input_to_adata( # input_type comes from NF module adata = input_to_adata( - input_data="${input_type}/matrix.mtx.gz", + input_data="${input_type}", output="${meta.id}/${meta.id}_${input_type}_matrix.h5ad", - barcode_file="${input_type}/barcodes.tsv.gz", - feature_file="${input_type}/features.tsv.gz", - sample="${meta.id}", - txp2gene="${star_index}/geneInfo.tab" + sample="${meta.id}" ) From b317165c43e47999f78198df51f37059e8fd1388 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 20 Aug 2024 12:43:01 +0200 Subject: [PATCH 011/147] simplify check --- modules/local/mtx_to_h5ad_star.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/mtx_to_h5ad_star.nf index 1895cb87..84474ed0 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/mtx_to_h5ad_star.nf @@ -21,7 +21,7 @@ process MTX_TO_H5AD_STAR { def input_to_check = (inputs instanceof String) ? inputs : inputs[0] // check input type of inputs - input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' + input_type = (input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' meta2 = meta + [input_type: input_type] template 'mtx_to_h5ad_star.py' From 72e9d50f91e8fb8fc870b9ed223f4bcc8fc77211 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Wed, 21 Aug 2024 09:23:17 +0200 Subject: [PATCH 012/147] Fix h5ad structure --- modules/local/templates/mtx_to_h5ad_star.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index d6889671..48749114 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -63,7 +63,8 @@ def input_to_adata( # standard format # index are gene IDs and symbols are a column adata.var["gene_symbol"] = adata.var.index - adata.var[['gene_ids', 'gene_versions']] = adata.var["gene_ids"].apply(lambda x: pd.Series(str(x).split("."))) + adata.var['gene_versions'] = adata.var["gene_ids"] + adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] adata.var.index = adata.var["gene_ids"].values adata.var = adata.var.drop("gene_ids", axis=1) From 882908024464e740fa5dd271ff78b9574450f76e Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 30 Aug 2024 10:49:57 +0200 Subject: [PATCH 013/147] updated concat module --- modules/local/concat_h5ad.nf | 15 ++----- .../local/templates}/concat_h5ad.py | 25 +++-------- subworkflows/local/mtx_conversion.nf | 31 +++---------- workflows/scrnaseq.nf | 45 +++++++------------ 4 files changed, 30 insertions(+), 86 deletions(-) rename {bin => modules/local/templates}/concat_h5ad.py (53%) diff --git a/modules/local/concat_h5ad.nf b/modules/local/concat_h5ad.nf index cd08cbbe..37b14156 100644 --- a/modules/local/concat_h5ad.nf +++ b/modules/local/concat_h5ad.nf @@ -1,13 +1,11 @@ process CONCAT_H5AD { label 'process_medium' - conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : - 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" + container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" input: - tuple val(input_type), path(h5ad) + tuple val(meta), path(h5ad) path samplesheet output: @@ -17,12 +15,7 @@ process CONCAT_H5AD { task.ext.when == null || task.ext.when script: - """ - concat_h5ad.py \\ - --input $samplesheet \\ - --out combined_${input_type}_matrix.h5ad \\ - --suffix "_matrix.h5ad" - """ + template 'concat_h5ad.py' stub: """ diff --git a/bin/concat_h5ad.py b/modules/local/templates/concat_h5ad.py similarity index 53% rename from bin/concat_h5ad.py rename to modules/local/templates/concat_h5ad.py index 43ea071a..033bc89a 100755 --- a/bin/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -7,7 +7,6 @@ import scanpy as sc, anndata as ad, pandas as pd from pathlib import Path -import argparse def read_samplesheet(samplesheet): @@ -17,36 +16,24 @@ def read_samplesheet(samplesheet): # samplesheet may contain replicates, when it has, # group information from replicates and collapse with commas # only keep unique values using set() - df = df.groupby(["sample"]).agg(lambda column: ",".join(set(column))) + df = df.groupby(["sample"]).agg(lambda column: ",".join(set(str(column)))) return df if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Concatenates h5ad files and merge metadata from samplesheet") - - parser.add_argument("-i", "--input", dest="input", help="Path to samplesheet.csv") - parser.add_argument("-o", "--out", dest="out", help="Output path.") - parser.add_argument( - "-s", - "--suffix", - dest="suffix", - help="Suffix of matrices to remove and get sample name", - ) - - args = vars(parser.parse_args()) # Open samplesheet as dataframe - df_samplesheet = read_samplesheet(args["input"]) + df_samplesheet = read_samplesheet("${samplesheet}") # find all h5ad and append to dict - dict_of_h5ad = {str(path).replace(args["suffix"], ""): sc.read_h5ad(path) for path in Path(".").rglob("*.h5ad")} + dict_of_h5ad = {str(path).replace("_matrix.h5ad", ""): sc.read_h5ad(path) for path in Path(".").rglob("*.h5ad")} # concat h5ad files adata = ad.concat(dict_of_h5ad, label="sample", merge="unique", index_unique="_") # merge with data.frame, on sample information - adata.obs = adata.obs.join(df_samplesheet, on="sample") - adata.write_h5ad(args["out"], compression="gzip") + adata.obs = adata.obs.join(df_samplesheet, on="sample").astype(str) + adata.write_h5ad("combined_${meta.input_type}_matrix.h5ad", compression="gzip") - print("Wrote h5ad file to {}".format(args["out"])) + print("Wrote h5ad file to {}".format("combined_${meta.input_type}_matrix.h5ad")) diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 98e49a2e..0d076db0 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -8,35 +8,14 @@ workflow MTX_CONVERSION { take: mtx_matrices samplesheet - txp2gene - star_index main: ch_versions = Channel.empty() - // Cellranger module output contains too many files which cause path collisions, we filter to the ones we need. - // Keeping backwards compatibility with cellranger-arc. - // TODO: Adapt cellranger-arc subworkflow like cellranger to remove this snippet here. - if (params.aligner in [ 'cellrangerarc' ]) { - mtx_matrices = mtx_matrices.map { meta, mtx_files -> - [ meta, mtx_files.findAll { it.toString().contains("filtered_feature_bc_matrix") } ] - } - .filter { meta, mtx_files -> mtx_files } // Remove any that are missing the relevant files - } - - // - // Convert matrix to h5ad - // - MTX_TO_H5AD ( - mtx_matrices, - txp2gene, - star_index - ) - // // Concat sample-specific h5ad in one // - ch_concat_h5ad_input = MTX_TO_H5AD.out.h5ad.groupTuple() // gather all sample-specific files / per type + ch_concat_h5ad_input = mtx_matrices.groupTuple() // gather all sample-specific files / per type if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) // which nextflow break because it is not a valid 'path' thus, we have to remove one level @@ -51,12 +30,12 @@ workflow MTX_CONVERSION { // // Convert matrix do seurat // - MTX_TO_SEURAT ( - mtx_matrices - ) + // MTX_TO_SEURAT ( + // mtx_matrices + // ) //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output - ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) + // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) emit: ch_versions diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index e58978da..333699a7 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -86,8 +86,8 @@ workflow SCRNASEQ { ch_multi_samplesheet = params.cellranger_multi_barcodes ? file(params.cellranger_multi_barcodes, checkIfExists: true) : [] empty_file = file("$projectDir/assets/EMPTY", checkIfExists: true) - ch_versions = Channel.empty() - ch_mtx_matrices = Channel.empty() + ch_versions = Channel.empty() + ch_h5ad_matrices = Channel.empty() // Run FastQC if (!params.skip_fastqc) { @@ -137,7 +137,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(KALLISTO_BUSTOOLS.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(KALLISTO_BUSTOOLS.out.raw_counts, KALLISTO_BUSTOOLS.out.filtered_counts) + ch_h5ad_matrices = ch_h5ad_matrices.mix(KALLISTO_BUSTOOLS.out.raw_counts, KALLISTO_BUSTOOLS.out.filtered_counts) ch_txp2gene = KALLISTO_BUSTOOLS.out.txp2gene } @@ -155,7 +155,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(SCRNASEQ_ALEVIN.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(SCRNASEQ_ALEVIN.out.alevin_results.map{ meta, it -> it }) - ch_mtx_matrices = ch_mtx_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_results) + ch_h5ad_matrices = ch_h5ad_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_results) } // Run STARSolo pipeline @@ -172,6 +172,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) + ch_h5ad_matrices = STARSOLO.out.star_h5ad } // Run cellranger pipeline @@ -200,7 +201,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(UNIVERSC_ALIGN.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(UNIVERSC_ALIGN.out.universc_out) + ch_h5ad_matrices = ch_h5ad_matrices.mix(UNIVERSC_ALIGN.out.universc_out) } // Run cellrangerarc pipeline @@ -214,7 +215,7 @@ workflow SCRNASEQ { ch_cellrangerarc_config ) ch_versions = ch_versions.mix(CELLRANGERARC_ALIGN.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGERARC_ALIGN.out.cellranger_arc_out) + ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGERARC_ALIGN.out.cellranger_arc_out) } // Run cellrangermulti pipeline @@ -282,39 +283,23 @@ workflow SCRNASEQ { ch_multiqc_files = ch_multiqc_files.mix( CELLRANGER_MULTI_ALIGN.out.cellrangermulti_out.map{ meta, outs -> outs.findAll{ it -> it.name == "web_summary.html" } }) - ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx) + ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx) } // Run emptydrops calling module // if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - // // - // // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it - // // - // if ( params.aligner in [ 'cellranger', 'cellrangermulti', 'kallisto', 'star' ] ) { - // ch_mtx_matrices_for_emptydrops = - // ch_mtx_matrices.filter { meta, mtx_files -> - // mtx_files.toString().contains("raw_feature_bc_matrix") || // cellranger - // mtx_files.toString().contains("counts_unfiltered") || // kallisto - // mtx_files.toString().contains("raw") // star - // } - // } else { - // ch_mtx_matrices_for_emptydrops = ch_mtx_matrices - // } - - // EMPTYDROPS_CELL_CALLING( ch_mtx_matrices_for_emptydrops ) - // ch_mtx_matrices = ch_mtx_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) + // EMPTYDROPS_CELL_CALLING( ch_h5ad_matrices.filter{ it[0].input_type == 'raw' } ) + // ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) // } - // // Run mtx to h5ad conversion subworkflow - // MTX_CONVERSION ( - // ch_mtx_matrices, - // ch_input, - // ch_txp2gene, - // ch_star_index - // ) + // Run mtx to h5ad conversion subworkflow + MTX_CONVERSION ( + ch_h5ad_matrices, + ch_input + ) // //Add Versions from MTX Conversion workflow too // ch_versions.mix(MTX_CONVERSION.out.ch_versions) From 9fc75b482b6142a6e76eeb8d82bb11814024d059 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 30 Aug 2024 10:59:25 +0200 Subject: [PATCH 014/147] workflow misses emptydrops and seurat & mtx conversion modules --- workflows/scrnaseq.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 333699a7..10569c35 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -301,8 +301,8 @@ workflow SCRNASEQ { ch_input ) - // //Add Versions from MTX Conversion workflow too - // ch_versions.mix(MTX_CONVERSION.out.ch_versions) + //Add Versions from MTX Conversion workflow too + ch_versions.mix(MTX_CONVERSION.out.ch_versions) // // Collate and save software versions From dc69f47f1ae8c080fdad3084ebc9cedea5a13ede Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 12 Sep 2024 13:55:14 +0200 Subject: [PATCH 015/147] start emptydrops cellbender subworkflow --- modules.json | 5 + modules/adata_barcodes.nf | 23 ++ modules/local/templates/barcodes.py | 44 ++++ .../removebackground/environment.yml | 5 + .../cellbender/removebackground/main.nf | 65 ++++++ .../cellbender/removebackground/meta.yml | 75 +++++++ .../removebackground/tests/epochs.config | 6 + .../removebackground/tests/main.nf.test | 66 ++++++ .../removebackground/tests/main.nf.test.snap | 196 ++++++++++++++++++ .../removebackground/tests/tags.yml | 2 + subworkflows/local/emptydrops_removal.nf | 25 +++ subworkflows/local/mtx_conversion.nf | 7 +- workflows/scrnaseq.nf | 9 - 13 files changed, 516 insertions(+), 12 deletions(-) create mode 100644 modules/adata_barcodes.nf create mode 100644 modules/local/templates/barcodes.py create mode 100644 modules/nf-core/cellbender/removebackground/environment.yml create mode 100644 modules/nf-core/cellbender/removebackground/main.nf create mode 100644 modules/nf-core/cellbender/removebackground/meta.yml create mode 100644 modules/nf-core/cellbender/removebackground/tests/epochs.config create mode 100644 modules/nf-core/cellbender/removebackground/tests/main.nf.test create mode 100644 modules/nf-core/cellbender/removebackground/tests/main.nf.test.snap create mode 100644 modules/nf-core/cellbender/removebackground/tests/tags.yml create mode 100644 subworkflows/local/emptydrops_removal.nf diff --git a/modules.json b/modules.json index aa186d98..b9680cfe 100644 --- a/modules.json +++ b/modules.json @@ -5,6 +5,11 @@ "https://github.com/nf-core/modules.git": { "modules": { "nf-core": { + "cellbender/removebackground": { + "branch": "master", + "git_sha": "06c8865e36741e05ad32ef70ab3fac127486af48", + "installed_by": ["modules"] + }, "cellranger/count": { "branch": "master", "git_sha": "90dad5491658049282ceb287a3d7732c1ce39837", diff --git a/modules/adata_barcodes.nf b/modules/adata_barcodes.nf new file mode 100644 index 00000000..2aef8ec9 --- /dev/null +++ b/modules/adata_barcodes.nf @@ -0,0 +1,23 @@ +process ADATA_BARCODES { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'oras://community.wave.seqera.io/library/anndata:0.10.7--e9840a94592528c8': + 'community.wave.seqera.io/library/anndata:0.10.7--336c6c1921a0632b' }" + + input: + tuple val(meta), path(h5ad), path(barcodes_csv) + + output: + tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + prefix = task.ext.prefix ?: "${meta.id}" + template 'barcodes.py' +} diff --git a/modules/local/templates/barcodes.py b/modules/local/templates/barcodes.py new file mode 100644 index 00000000..8a9b10a7 --- /dev/null +++ b/modules/local/templates/barcodes.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import platform +import anndata as ad +import pandas as pd + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +df = pd.read_csv("${barcodes_csv}", header=None) +adata = ad.read_h5ad("${h5ad}") + +adata = adata[df[0].values] + +adata.write_h5ad("${prefix}.h5ad") + +# Versions + +versions = { + "${task.process}": { + "python": platform.python_version(), + "anndata": ad.__version__, + "pandas": pd.__version__ + } +} + +with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) diff --git a/modules/nf-core/cellbender/removebackground/environment.yml b/modules/nf-core/cellbender/removebackground/environment.yml new file mode 100644 index 00000000..a157c522 --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/environment.yml @@ -0,0 +1,5 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::cellbender=0.3.0 diff --git a/modules/nf-core/cellbender/removebackground/main.nf b/modules/nf-core/cellbender/removebackground/main.nf new file mode 100644 index 00000000..f3cfd1ff --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/main.nf @@ -0,0 +1,65 @@ +process CELLBENDER_REMOVEBACKGROUND { + tag "$meta.id" + label 'process_medium' + label 'process_gpu' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'oras://community.wave.seqera.io/library/cellbender:0.3.0--c4addb97ab2d83fe': + 'community.wave.seqera.io/library/cellbender:0.3.0--41318a055fc3aacb' }" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("${prefix}.h5") , emit: h5 + tuple val(meta), path("${prefix}_filtered.h5") , emit: filtered_h5 + tuple val(meta), path("${prefix}_posterior.h5") , emit: posterior_h5 + tuple val(meta), path("${prefix}_cell_barcodes.csv"), emit: barcodes + tuple val(meta), path("${prefix}_metrics.csv") , emit: metrics + tuple val(meta), path("${prefix}_report.html") , emit: report + tuple val(meta), path("${prefix}.pdf") , emit: pdf + tuple val(meta), path("${prefix}.log") , emit: log + tuple val(meta), path("ckpt.tar.gz") , emit: checkpoint + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + prefix = task.ext.prefix ?: "${meta.id}" + args = task.ext.args ?: "" + use_gpu = task.ext.use_gpu ? "--cuda" : "" + """ + TMPDIR=. cellbender remove-background \ + ${args} \ + --cpu-threads ${task.cpus} \ + ${use_gpu} \ + --input ${h5ad} \ + --output ${prefix}.h5 + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + cellbender: \$(cellbender --version) + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + """ + touch "${prefix}.h5" + touch "${prefix}_filtered.h5" + touch "${prefix}_posterior.h5" + touch "${prefix}_cell_barcodes.csv" + touch "${prefix}_metrics.csv" + touch "${prefix}_report.html" + touch "${prefix}.pdf" + touch "${prefix}.log" + touch "ckpt.tar.gz" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + cellbender: \$(cellbender --version) + END_VERSIONS + """ +} diff --git a/modules/nf-core/cellbender/removebackground/meta.yml b/modules/nf-core/cellbender/removebackground/meta.yml new file mode 100644 index 00000000..d70fa3fd --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/meta.yml @@ -0,0 +1,75 @@ +name: cellbender_removebackground +description: Module to use CellBender to estimate ambient RNA from single-cell RNA-seq data +keywords: + - single-cell + - scRNA-seq + - ambient RNA removal +tools: + - cellbender: + description: CellBender is a software package for eliminating technical artifacts from high-throughput single-cell RNA sequencing (scRNA-seq) data. + documentation: https://cellbender.readthedocs.io/en/latest/ + tool_dev_url: https://github.com/broadinstitute/CellBender + licence: ["BSD-3-Clause"] +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - h5ad: + type: file + description: AnnData file containing unfiltered data (with empty droplets) + pattern: "*.h5ad" +output: + - h5: + type: file + description: Full count matrix as an h5 file, with background RNA removed. This file contains all the original droplet barcodes. + pattern: "*.h5" + - filtered_h5: + type: file + description: | + Full count matrix as an h5 file, with background RNA removed. This file contains only the droplet barcodes which were determined to have a > 50% posterior probability of containing cells. + pattern: "*.h5" + - posterior_h5: + type: file + description: | + The full posterior probability of noise counts. This is not normally used downstream. + pattern: "*.h5" + - barcodes: + type: file + description: | + CSV file containing all the droplet barcodes which were determined to have a > 50% posterior probability of containing cells. | + Barcodes are written in plain text. This information is also contained in each of the above outputs, | + but is included as a separate output for convenient use in certain downstream applications. + pattern: "*.csv" + - metrics: + type: file + description: | + Metrics describing the run, potentially to be used to flag problematic runs | + when using CellBender as part of a large-scale automated pipeline. + pattern: "*.csv" + - report: + type: file + description: | + HTML report including plots and commentary, along with any warnings or suggestions for improved parameter settings. + pattern: "*.html" + - pdf: + type: file + description: PDF file that provides a standard graphical summary of the inference procedure. + pattern: "*.pdf" + - log: + type: file + description: Log file produced by the cellbender remove-background run. + pattern: "*.log" + - checkpoint: + type: file + description: Checkpoint file which contains the trained model and the full posterior. + pattern: "*.ckpt" + - versions: + type: file + description: File containing software version + pattern: "versions.yml" +authors: + - "@nictru" +maintainers: + - "@nictru" diff --git a/modules/nf-core/cellbender/removebackground/tests/epochs.config b/modules/nf-core/cellbender/removebackground/tests/epochs.config new file mode 100644 index 00000000..96282b07 --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/tests/epochs.config @@ -0,0 +1,6 @@ + +process { + withName: CELLBENDER_REMOVEBACKGROUND { + ext.args = '--epochs 20' + } +} diff --git a/modules/nf-core/cellbender/removebackground/tests/main.nf.test b/modules/nf-core/cellbender/removebackground/tests/main.nf.test new file mode 100644 index 00000000..1afa6f3b --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/tests/main.nf.test @@ -0,0 +1,66 @@ +nextflow_process { + name 'Test Process CELLBENDER_REMOVEBACKGROUND' + script '../main.nf' + process 'CELLBENDER_REMOVEBACKGROUND' + + tag "modules" + tag "modules_nfcore" + tag "cellbender/removebackground" + tag "cellbender" + + test("test_cellbender_removebackground") { + config './epochs.config' + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file("https://raw.githubusercontent.com/nf-core/test-datasets/scdownstream/samples/SAMN14430799_raw_matrix_5k.h5ad", checkIfExists: true) + ] + """ + } + } + then { + assertAll( + {assert process.success}, + {assert file(process.out.h5.get(0).get(1)).exists()}, + {assert file(process.out.filtered_h5.get(0).get(1)).exists()}, + {assert file(process.out.posterior_h5.get(0).get(1)).exists()}, + {assert snapshot(process.out.barcodes).match("cellbender_removebackground_barcodes")}, + {assert snapshot(process.out.metrics).match("cellbender_removebackground_metrics")}, + {assert file(process.out.report.get(0).get(1)).exists()}, + {assert file(process.out.pdf.get(0).get(1)).exists()}, + {assert file(process.out.log.get(0).get(1)).exists()}, + {assert snapshot(process.out.versions).match("cellbender_removebackground_versions")} + ) + } + } + + test("test_cellbender_removebackground - stub") { + options '-stub' + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file("https://raw.githubusercontent.com/nf-core/test-datasets/scdownstream/samples/SAMN14430799_raw_matrix_5k.h5ad", checkIfExists: true) + ] + """ + } + } + then { + assertAll( + {assert process.success}, + {assert snapshot(process.out.h5).match("cellbender_removebackground_h5_stub")}, + {assert snapshot(process.out.filtered_h5).match("cellbender_removebackground_filtered_h5_stub")}, + {assert snapshot(process.out.posterior_h5).match("cellbender_removebackground_posterior_h5_stub")}, + {assert snapshot(process.out.barcodes).match("cellbender_removebackground_barcodes_stub")}, + {assert snapshot(process.out.metrics).match("cellbender_removebackground_metrics_stub")}, + {assert snapshot(process.out.report).match("cellbender_removebackground_report_stub")}, + {assert snapshot(process.out.pdf).match("cellbender_removebackground_pdf_stub")}, + {assert snapshot(process.out.log).match("cellbender_removebackground_log_stub")}, + {assert snapshot(process.out.versions).match("cellbender_removebackground_versions_stub")} + ) + } + } +} diff --git a/modules/nf-core/cellbender/removebackground/tests/main.nf.test.snap b/modules/nf-core/cellbender/removebackground/tests/main.nf.test.snap new file mode 100644 index 00000000..fdb51d66 --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/tests/main.nf.test.snap @@ -0,0 +1,196 @@ +{ + "cellbender_removebackground_versions": { + "content": [ + [ + "versions.yml:md5,b236ac7595dfa6cd4d51ac73e51cb05a" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:09.33127881" + }, + "cellbender_removebackground_filtered_h5_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_filtered.h5:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.833598082" + }, + "cellbender_removebackground_pdf_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test.pdf:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.891829278" + }, + "cellbender_removebackground_metrics": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_metrics.csv:md5,88272bde1c157528b0b0ab2abe5ad26f" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:09.327155805" + }, + "cellbender_removebackground_versions_stub": { + "content": [ + [ + "versions.yml:md5,b236ac7595dfa6cd4d51ac73e51cb05a" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.904614838" + }, + "cellbender_removebackground_h5_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test.h5:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.829304361" + }, + "cellbender_removebackground_metrics_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_metrics.csv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.870469733" + }, + "cellbender_removebackground_log_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.899293304" + }, + "cellbender_removebackground_barcodes": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_cell_barcodes.csv:md5,c8e8df9d0f9aea976d6f6aa36d329429" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:09.316098811" + }, + "cellbender_removebackground_report_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.885307244" + }, + "cellbender_removebackground_posterior_h5_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_posterior.h5:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.838032754" + }, + "cellbender_removebackground_barcodes_stub": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_cell_barcodes.csv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-08-12T13:41:20.861284979" + } +} \ No newline at end of file diff --git a/modules/nf-core/cellbender/removebackground/tests/tags.yml b/modules/nf-core/cellbender/removebackground/tests/tags.yml new file mode 100644 index 00000000..d935083b --- /dev/null +++ b/modules/nf-core/cellbender/removebackground/tests/tags.yml @@ -0,0 +1,2 @@ +cellbender/removebackground: + - modules/nf-core/cellbender/removebackground/** diff --git a/subworkflows/local/emptydrops_removal.nf b/subworkflows/local/emptydrops_removal.nf new file mode 100644 index 00000000..202612d6 --- /dev/null +++ b/subworkflows/local/emptydrops_removal.nf @@ -0,0 +1,25 @@ +include { CELLBENDER_REMOVEBACKGROUND } from '../../modules/nf-core/cellbender/removebackground' +include { ADATA_BARCODES } from '../../modules/local/adata_barcodes' + +workflow EMPTY_DROPLET_REMOVAL { + take: + ch_unfiltered + + main: + ch_versions = Channel.empty() + + CELLBENDER_REMOVEBACKGROUND(ch_unfiltered) + ch_versions = ch_versions.mix(CELLBENDER_REMOVEBACKGROUND.out.versions) + + ch_combined = ch_unfiltered.join(CELLBENDER_REMOVEBACKGROUND.out.barcodes) + + ADATA_BARCODES(ch_combined) + ch_versions = ch_versions.mix(ADATA_BARCODES.out.versions) + + ch_h5ad = ADATA_BARCODES.out.h5ad + + emit: + h5ad = ch_h5ad + + versions = ch_versions +} diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 0d076db0..7cca836d 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -1,7 +1,8 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad.nf' -include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' -include { MTX_TO_SEURAT } from '../../modules/local/mtx_to_seurat.nf' +include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad.nf' +include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' +include { MTX_TO_SEURAT } from '../../modules/local/mtx_to_seurat.nf' +include { EMPTY_DROPLET_REMOVAL } from '../subworkflows/local/emptydrops_removal' workflow MTX_CONVERSION { diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 10569c35..bc5eaef6 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -9,7 +9,6 @@ include { CELLRANGERARC_ALIGN } from "../subworkflows/local/align include { UNIVERSC_ALIGN } from "../subworkflows/local/align_universc" include { MTX_CONVERSION } from "../subworkflows/local/mtx_conversion" include { GTF_GENE_FILTER } from '../modules/local/gtf_gene_filter' -include { EMPTYDROPS_CELL_CALLING } from '../modules/local/emptydrops' include { GUNZIP as GUNZIP_FASTA } from '../modules/nf-core/gunzip/main' include { GUNZIP as GUNZIP_GTF } from '../modules/nf-core/gunzip/main' include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' @@ -287,14 +286,6 @@ workflow SCRNASEQ { } - // Run emptydrops calling module - // if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - - // EMPTYDROPS_CELL_CALLING( ch_h5ad_matrices.filter{ it[0].input_type == 'raw' } ) - // ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) - - // } - // Run mtx to h5ad conversion subworkflow MTX_CONVERSION ( ch_h5ad_matrices, From 9c204200d0f86f7bc716343404bce2ad9e1c1636 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 10:57:43 +0200 Subject: [PATCH 016/147] fix paths --- modules/{ => local}/adata_barcodes.nf | 0 subworkflows/local/mtx_conversion.nf | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename modules/{ => local}/adata_barcodes.nf (100%) diff --git a/modules/adata_barcodes.nf b/modules/local/adata_barcodes.nf similarity index 100% rename from modules/adata_barcodes.nf rename to modules/local/adata_barcodes.nf diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 7cca836d..4d3ba721 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -2,7 +2,7 @@ include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad.nf' include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' include { MTX_TO_SEURAT } from '../../modules/local/mtx_to_seurat.nf' -include { EMPTY_DROPLET_REMOVAL } from '../subworkflows/local/emptydrops_removal' +include { EMPTY_DROPLET_REMOVAL } from './emptydrops_removal' workflow MTX_CONVERSION { From 683119598e67e5073f43b488e457a8f4d868f14f Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 12:46:51 +0200 Subject: [PATCH 017/147] started the anndatar standardization module --- modules/local/anndatar_convert.nf | 24 ++++++++++++++++++++++ modules/local/templates/anndatar_convert.R | 20 ++++++++++++++++++ subworkflows/local/mtx_conversion.nf | 9 ++++++++ 3 files changed, 53 insertions(+) create mode 100644 modules/local/anndatar_convert.nf create mode 100755 modules/local/templates/anndatar_convert.R diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf new file mode 100644 index 00000000..f3552ef0 --- /dev/null +++ b/modules/local/anndatar_convert.nf @@ -0,0 +1,24 @@ +process ANNDATAR_CONVERT { + label 'process_medium' + + container "fmalmeida/anndatar:dev" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("${meta.id}_standardized.h5ad"), emit: h5ad + tuple val(meta), path("${meta.id}_standardized.Rds"), emit: rds + + when: + task.ext.when == null || task.ext.when + + script: + template 'anndatar_convert.R' + + stub: + """ + touch ${meta.id}_standardized.h5ad + touch ${meta.id}_standardized.Rds + """ +} diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R new file mode 100755 index 00000000..016f8bb4 --- /dev/null +++ b/modules/local/templates/anndatar_convert.R @@ -0,0 +1,20 @@ +#!/usr/bin/env Rscript + +# to use nf variables: "${meta.id}" + +# load libraries +library(anndataR) + +# read input +adata <- read_h5ad("${h5ad}") + +# +# scope to perform standardization options +# + +# convert to Rds +obj <- adata\$to_Seurat() + +# save files +write_h5ad(adata, "${meta.id}_standardized.h5ad") +saveRDS(obj, file = "${meta.id}_standardized.Rds") diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 4d3ba721..6fc0ffb9 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -1,4 +1,5 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ +include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad.nf' include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' include { MTX_TO_SEURAT } from '../../modules/local/mtx_to_seurat.nf' @@ -13,6 +14,13 @@ workflow MTX_CONVERSION { main: ch_versions = Channel.empty() + // + // MODULE: Standardize h5ad and convert with AnndataR package + // + ANNDATAR_CONVERT ( + mtx_matrices + ) + // // Concat sample-specific h5ad in one // @@ -23,6 +31,7 @@ workflow MTX_CONVERSION { // making it as [ mtx_1, mtx_2 ] ch_concat_h5ad_input = ch_concat_h5ad_input.map{ type, matrices -> [ type, matrices.flatten().toList() ] } } + CONCAT_H5AD ( ch_concat_h5ad_input, samplesheet From 237d1ca3c65f1fc3b236e8d7ab8ac552e447779c Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 12:49:09 +0200 Subject: [PATCH 018/147] concat h5ad with anndatar h5ad --- subworkflows/local/mtx_conversion.nf | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 6fc0ffb9..0178868f 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -24,7 +24,7 @@ workflow MTX_CONVERSION { // // Concat sample-specific h5ad in one // - ch_concat_h5ad_input = mtx_matrices.groupTuple() // gather all sample-specific files / per type + ch_concat_h5ad_input = ANNDATAR_CONVERT.out.h5ad.groupTuple() // gather all sample-specific files / per type if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) // which nextflow break because it is not a valid 'path' thus, we have to remove one level @@ -37,13 +37,6 @@ workflow MTX_CONVERSION { samplesheet ) - // - // Convert matrix do seurat - // - // MTX_TO_SEURAT ( - // mtx_matrices - // ) - //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) From f5fb4a27cb3b70211403e62fbc7ffc485ad9f64a Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 12:51:03 +0200 Subject: [PATCH 019/147] update tag information --- modules/local/anndatar_convert.nf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index f3552ef0..3f985b64 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -1,4 +1,6 @@ process ANNDATAR_CONVERT { + tag "${meta.id}" + label 'process_medium' container "fmalmeida/anndatar:dev" From 752666bff34142a2bc2b6a947c5bad4a3c3ba236 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 12:58:07 +0200 Subject: [PATCH 020/147] update tags --- modules/local/concat_h5ad.nf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/local/concat_h5ad.nf b/modules/local/concat_h5ad.nf index 37b14156..41310553 100644 --- a/modules/local/concat_h5ad.nf +++ b/modules/local/concat_h5ad.nf @@ -1,4 +1,6 @@ process CONCAT_H5AD { + tag "${meta.id}" + label 'process_medium' conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" From d0ad7f6a90056903f2b7ee473232dcfc202523ff Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 12:59:19 +0200 Subject: [PATCH 021/147] module is only to convert to rds --- modules/local/anndatar_convert.nf | 4 +--- modules/local/templates/anndatar_convert.R | 5 ----- subworkflows/local/mtx_conversion.nf | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index 3f985b64..dfe5fce9 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -3,13 +3,12 @@ process ANNDATAR_CONVERT { label 'process_medium' - container "fmalmeida/anndatar:dev" + container "fmalmeida/anndatar:dev" // TODO: Fix input: tuple val(meta), path(h5ad) output: - tuple val(meta), path("${meta.id}_standardized.h5ad"), emit: h5ad tuple val(meta), path("${meta.id}_standardized.Rds"), emit: rds when: @@ -20,7 +19,6 @@ process ANNDATAR_CONVERT { stub: """ - touch ${meta.id}_standardized.h5ad touch ${meta.id}_standardized.Rds """ } diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R index 016f8bb4..479ac912 100755 --- a/modules/local/templates/anndatar_convert.R +++ b/modules/local/templates/anndatar_convert.R @@ -8,13 +8,8 @@ library(anndataR) # read input adata <- read_h5ad("${h5ad}") -# -# scope to perform standardization options -# - # convert to Rds obj <- adata\$to_Seurat() # save files -write_h5ad(adata, "${meta.id}_standardized.h5ad") saveRDS(obj, file = "${meta.id}_standardized.Rds") diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 0178868f..4e85a1a7 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -24,7 +24,7 @@ workflow MTX_CONVERSION { // // Concat sample-specific h5ad in one // - ch_concat_h5ad_input = ANNDATAR_CONVERT.out.h5ad.groupTuple() // gather all sample-specific files / per type + ch_concat_h5ad_input = mtx_matrices.groupTuple() // gather all sample-specific files / per type if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) // which nextflow break because it is not a valid 'path' thus, we have to remove one level From 20ac4ae8a5eea676428c28945a684b55444cd8f9 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 13:37:19 +0200 Subject: [PATCH 022/147] update directives --- conf/modules.config | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index 81395a1d..1f83f2a6 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -45,12 +45,13 @@ process { } } - withName: 'MTX_TO_H5AD|CONCAT_H5AD|MTX_TO_SEURAT' { + withName: 'MTX_TO_H5AD*|CONCAT_H5AD|ANNDATAR_CONVERT' { publishDir = [ path: { "${params.outdir}/${params.aligner}/mtx_conversions" }, mode: params.publish_dir_mode ] } + withName: 'GTF_GENE_FILTER' { publishDir = [ path: { "${params.outdir}/gtf_filter" }, From da5b0363dcfd972b317056cd9eae1fd431926658 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 13:39:05 +0200 Subject: [PATCH 023/147] update comments --- subworkflows/local/mtx_conversion.nf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 4e85a1a7..a2ff58c7 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -1,8 +1,6 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' -include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad.nf' include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' -include { MTX_TO_SEURAT } from '../../modules/local/mtx_to_seurat.nf' +include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' include { EMPTY_DROPLET_REMOVAL } from './emptydrops_removal' workflow MTX_CONVERSION { @@ -15,7 +13,7 @@ workflow MTX_CONVERSION { ch_versions = Channel.empty() // - // MODULE: Standardize h5ad and convert with AnndataR package + // MODULE: Convert to Rds with AnndataR package // ANNDATAR_CONVERT ( mtx_matrices From b90f3884770bddcea033d9e59829c9179d7ff391 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 14:28:05 +0200 Subject: [PATCH 024/147] add cellbender to workflow --- subworkflows/local/mtx_conversion.nf | 1 - workflows/scrnaseq.nf | 31 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index a2ff58c7..6435966d 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -1,7 +1,6 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' -include { EMPTY_DROPLET_REMOVAL } from './emptydrops_removal' workflow MTX_CONVERSION { diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index bc5eaef6..1e7ea9ef 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -3,11 +3,12 @@ include { FASTQC_CHECK } from '../subworkflows/local/fastq include { KALLISTO_BUSTOOLS } from '../subworkflows/local/kallisto_bustools' include { SCRNASEQ_ALEVIN } from '../subworkflows/local/alevin' include { STARSOLO } from '../subworkflows/local/starsolo' -include { CELLRANGER_ALIGN } from "../subworkflows/local/align_cellranger" -include { CELLRANGER_MULTI_ALIGN } from "../subworkflows/local/align_cellrangermulti" -include { CELLRANGERARC_ALIGN } from "../subworkflows/local/align_cellrangerarc" -include { UNIVERSC_ALIGN } from "../subworkflows/local/align_universc" -include { MTX_CONVERSION } from "../subworkflows/local/mtx_conversion" +include { CELLRANGER_ALIGN } from '../subworkflows/local/align_cellranger' +include { CELLRANGER_MULTI_ALIGN } from '../subworkflows/local/align_cellrangermulti' +include { CELLRANGERARC_ALIGN } from '../subworkflows/local/align_cellrangerarc' +include { UNIVERSC_ALIGN } from '../subworkflows/local/align_universc' +include { EMPTY_DROPLET_REMOVAL } from '../subworkflows/local/emptydrops_removal' +include { MTX_CONVERSION } from '../subworkflows/local/mtx_conversion' include { GTF_GENE_FILTER } from '../modules/local/gtf_gene_filter' include { GUNZIP as GUNZIP_FASTA } from '../modules/nf-core/gunzip/main' include { GUNZIP as GUNZIP_GTF } from '../modules/nf-core/gunzip/main' @@ -286,6 +287,26 @@ workflow SCRNASEQ { } + // SUBWORKFLOW: Run cellbender emptydrops filter + if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { + + // + // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it + // + if ( params.aligner in [ 'cellranger', 'cellrangermulti', 'kallisto', 'star' ] ) { + ch_h5ad_matrices_for_emptydrops = + ch_h5ad_matrices.filter { meta, mtx_files -> meta.input_type == 'raw' } + } else { + ch_h5ad_matrices_for_emptydrops = ch_h5ad_matrices + } + + EMPTY_DROPLET_REMOVAL ( + ch_h5ad_matrices_for_emptydrops + ) + // ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) + + } + // Run mtx to h5ad conversion subworkflow MTX_CONVERSION ( ch_h5ad_matrices, From 7c304cc2c259709f5fd09921cafb630280fd9661 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 13 Sep 2024 14:41:58 +0200 Subject: [PATCH 025/147] start organisation of files --- conf/modules.config | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 1f83f2a6..8a53c285 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -32,15 +32,17 @@ process { } if (!params.skip_emptydrops) { - withName: EMPTYDROPS_CELL_CALLING { + withName: 'CELLBENDER_REMOVEBACKGROUND' { publishDir = [ - path: { "${params.outdir}/${params.aligner}" }, - mode: params.publish_dir_mode, - saveAs: { filename -> - if ( params.aligner == 'cellranger' ) "count/${meta.id}/${filename}" - else if ( params.aligner == 'kallisto' ) "${meta.id}.count/${filename}" - else "${meta.id}/${filename}" - } + path: { "${params.outdir}/${params.aligner}/${meta.id}/emptydrops_filter" }, + mode: params.publish_dir_mode + ] + } + withName: 'ADATA_BARCODES' { + ext.prefix = { "${meta.id}_custom_emptydrops_filter_matrix" } + publishDir = [ + path: { "${params.outdir}/${params.aligner}/mtx_conversions/${meta.id}" }, + mode: params.publish_dir_mode ] } } From 012f6083801dd71693ee53c7d239b0fbd915c6c9 Mon Sep 17 00:00:00 2001 From: an-altosian Date: Mon, 16 Sep 2024 03:10:46 +0000 Subject: [PATCH 026/147] update doc related to alevin-fry, simpleaf and salmon --- CITATIONS.md | 8 ++++++++ bin/emptydrops_cell_calling.R | 4 ++-- docs/output.md | 20 +++++++++++--------- docs/usage.md | 10 +++++----- nextflow.config | 2 +- nextflow_schema.json | 7 +++---- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index 867bde34..9a6c64de 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -18,6 +18,14 @@ > Ewels P, Magnusson M, Lundin S, Käller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. +- [Simpleaf](https://doi.org/10.1093/bioinformatics/btad614) + + > Dongze He, Rob Patro, simpleaf: a simple, flexible, and scalable framework for single-cell data processing using alevin-fry, Bioinformatics 39, 10 (2023). + +* [Alevin-fry](https://doi.org/10.1038/s41592-022-01408-3) + + > He, D., Zakeri, M., Sarkar, H. et al. Alevin-fry unlocks rapid, accurate and memory-frugal quantification of single-cell RNA-seq data. Nat Methods 19, 316–322 (2022). + * [Alevin](https://doi.org/10.1186/s13059-019-1670-y) > Srivastava, A., Malik, L., Smith, T. et al. Alevin efficiently estimates accurate gene abundances from dscRNA-seq data. Genome Biol 20, 65 (2019). diff --git a/bin/emptydrops_cell_calling.R b/bin/emptydrops_cell_calling.R index 23a45267..d21cba61 100755 --- a/bin/emptydrops_cell_calling.R +++ b/bin/emptydrops_cell_calling.R @@ -22,8 +22,8 @@ get_name <- function(file) { } # transpose matrices when required -# based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin would require transposition -print("Only kallisto and alevin have transposed matrices.") +# based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin-fry would require transposition +print("Only kallisto and alevin-fry have transposed matrices.") if (aligner %in% c( "kallisto", "alevin" )) { is_transposed <- TRUE mtx<-t(mtx) diff --git a/docs/output.md b/docs/output.md index 3ab87625..a5292336 100644 --- a/docs/output.md +++ b/docs/output.md @@ -9,20 +9,20 @@ This document describes the output produced by the pipeline. Most of the plots a The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: - [nf-core/scrnaseq: Output](#nf-corescrnaseq-output) - - [:warning: Please read this documentation on the nf-core website: https://nf-co.re/scrnaseq/output](#warning-please-read-this-documentation-on-the-nf-core-website-httpsnf-corescrnaseqoutput) - [Introduction](#introduction) - [Pipeline overview](#pipeline-overview) - [FastQC](#fastqc) - - [Kallisto & Bustools Results](#kallisto--bustools-results) + - [Kallisto \& Bustools Results](#kallisto--bustools-results) - [STARsolo](#starsolo) - - [Salmon Alevin & AlevinQC](#salmon-alevin--alevinqc) + - [Salmon \& Alevin-fry \& AlevinQC](#salmon--alevin-fry--alevinqc) - [Cellranger](#cellranger) - [Cellranger ARC](#cellranger-arc) + - [Cellranger multi](#cellranger-multi) - [UniverSC](#universc) - [Custom emptydrops filter](#custom-emptydrops-filter) - [Other output data](#other-output-data) - [MultiQC](#multiqc) - - [Pipeline information](#pipeline-information) + - [Pipeline information](#pipeline-information) ## FastQC @@ -81,21 +81,23 @@ For details on how to load these into R and perform further downstream analysis, - `star_index` - Contains the index of the supplied genome fasta file -## Salmon Alevin & AlevinQC +## Salmon & Alevin-fry & AlevinQC + +This pipeline uses the simplified and flexible modules in [Simpleaf](https://simpleaf.readthedocs.io/en/latest/) for processing single-cell data with [Salmon](https://salmon.readthedocs.io/en/latest/) as the underlying mapper and [Alevin-fry](https://alevin-fry.readthedocs.io/en/latest/) as the quantification tool. For detailed examples of using the quantification results generated by Alevin-fry in downstream analyses, such as RNA-velocity, please refer to [Alevin-fry/simpleaf tutorials](https://combine-lab.github.io/alevin-fry-tutorials/#blog). **Output directory: `results/alevin`** - `alevin` - - Contains the created Salmon Alevin pseudo-aligned output + - Contains the count matrix created by Alevin-fry - `alevinqc` - - Contains the QC report for the aforementioned Salmon Alevin output data + - Contains the QC report for the aforementioned Alevin-fry output data **Output directory: `results/reference_genome`** - `salmon_index` - - Contains the indexed reference transcriptome for Salmon Alevin + - Contains the indexed reference transcriptome for the Salmon mapper - `alevin/txp2gene.tsv` - - The transcriptome to gene mapping TSV file utilized by Salmon Alevin + - The transcriptome to gene mapping TSV file utilized by Alevin-fry ## Cellranger diff --git a/docs/usage.md b/docs/usage.md index 499e404d..1c8be75b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -39,7 +39,7 @@ An [example samplesheet](../assets/samplesheet.csv) has been provided with the p This parameter is currently supported by -- [Salmon Alevin](https://salmon.readthedocs.io/en/latest/alevin.html#expectcells) +- [Alevin-fry](https://alevin-fry.readthedocs.io/en/latest/generate_permit_list.html#:~:text=procedure%20described%20above.-,%2D%2Dexpect%2Dcells,-%3Cncells%3E%3A%20This) - [STARsolo](https://github.com/alexdobin/STAR/blob/master/docs/STARsolo.md) - [Cellranger](https://support.10xgenomics.com/single-cell-gene-expression/software/pipelines/latest/what-is-cell-ranger) @@ -47,7 +47,7 @@ Note that since cellranger v7, it is **not recommended** anymore to supply the ` ## Aligning options -By default, the pipeline uses [Salmon Alevin](https://salmon.readthedocs.io/en/latest/alevin.html) (i.e. --aligner alevin) to perform pseudo-alignment of reads to the reference genome and to perform the downstream BAM-level quantification. Then QC reports are generated with AlevinQC. +By default (i.e. `--aligner alevin`), the pipeline uses [Salmon](https://salmon.readthedocs.io/en/latest/) to perform pseudo-alignment of reads to the reference genome and [Alevin-fry](https://alevin-fry.readthedocs.io/en/latest/) to perform the downstream BAM-level quantification. Then QC reports are generated with AlevinQC. Other aligner options for running the pipeline are: @@ -100,11 +100,11 @@ The command `kb --list` shows all supported, preconfigured protocols. Additional For more details, please refer to the [Kallisto/bustools documentation](https://pachterlab.github.io/kallisto/manual#bus). -#### Alevin/fry +#### Alevin-fry -Alevin/fry also supports custom chemistries in a slighly different format, e.g. `1{b[16]u[12]x:}2{r:}`. +Simpleaf has the ability to pass custom chemistries to Alevin-fry, in a slighly different format, e.g. `1{b[16]u[12]x:}2{r:}`. -For more details, see the [simpleaf documentation](https://simpleaf.readthedocs.io/en/latest/quant-command.html#a-note-on-the-chemistry-flag) +For more details, see Simpleaf's paper, [He _et al._ 2023](https://doi.org/10.1093/bioinformatics/btad614). #### UniverSC diff --git a/nextflow.config b/nextflow.config index 70753bea..c130fcc7 100644 --- a/nextflow.config +++ b/nextflow.config @@ -22,7 +22,7 @@ params { fasta = null gtf = null - // salmon alevin parameters (simpleaf) + // alevin-fry parameters (simpleaf) simpleaf_rlen = 91 barcode_whitelist = null salmon_index = null diff --git a/nextflow_schema.json b/nextflow_schema.json index e5fb71b5..811efdde 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -59,7 +59,7 @@ "type": "string", "description": "Name of the tool to use for scRNA (pseudo-) alignment.", "default": "alevin", - "help_text": "The workflow can handle three types of methods:\n\n- Kallisto/Bustools\n- Salmon Alevin + AlevinQC\n- STARsolo\n\nTo choose which one to use, please specify either `alevin`, `star` or `kallisto` as a parameter option for `--aligner`. By default, the pipeline runs the `alevin` option. Note that specifying another aligner option also requires choosing appropriate parameters (see below) for the selected option.", + "help_text": "The workflow can handle three types of methods:\n\n- Kallisto/Bustools\n- Salmon + Alevin-fry + AlevinQC\n- STARsolo\n\nTo choose which one to use, please specify either `alevin`, `star` or `kallisto` as a parameter option for `--aligner`. By default, the pipeline runs the `alevin` option. Note that specifying another aligner option also requires choosing appropriate parameters (see below) for the selected option.", "fa_icon": "fas fa-align-center", "enum": ["kallisto", "star", "alevin", "cellranger", "universc", "cellrangerarc", "cellrangermulti"] }, @@ -153,7 +153,7 @@ } }, "alevin_options": { - "title": "Alevin Options", + "title": "Alevin-fry Options", "type": "object", "description": "", "default": "", @@ -167,8 +167,7 @@ }, "txp2gene": { "type": "string", - "description": "Path to transcript to gene mapping file. This allows the specification of a transcript to gene mapping file for Salmon Alevin and AlevinQC.", - "help_text": "> This is only used by the Salmon Alevin workflow.", + "description": "Path to transcript to gene mapping file. This allows the specification of a transcript to gene mapping file for Alevin-fry and AlevinQC.", "fa_icon": "fas fa-map-marked-alt", "format": "file-path", "exists": true From c02a0545f13c6c579f1e2b3716999458a3d1e252 Mon Sep 17 00:00:00 2001 From: an-altosian Date: Mon, 16 Sep 2024 03:22:40 +0000 Subject: [PATCH 027/147] update simpleaf reference --- CITATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATIONS.md b/CITATIONS.md index 9a6c64de..8c2045bf 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -20,7 +20,7 @@ - [Simpleaf](https://doi.org/10.1093/bioinformatics/btad614) - > Dongze He, Rob Patro, simpleaf: a simple, flexible, and scalable framework for single-cell data processing using alevin-fry, Bioinformatics 39, 10 (2023). + > He, D., Patro, R. simpleaf: a simple, flexible, and scalable framework for single-cell data processing using alevin-fry, Bioinformatics 39, 10 (2023). * [Alevin-fry](https://doi.org/10.1038/s41592-022-01408-3) From f2c1a242d76902caad33fe629c02d15fdbf464e9 Mon Sep 17 00:00:00 2001 From: an-altosian Date: Mon, 16 Sep 2024 03:28:12 +0000 Subject: [PATCH 028/147] change aligner name back to alevin --- bin/emptydrops_cell_calling.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/emptydrops_cell_calling.R b/bin/emptydrops_cell_calling.R index d21cba61..5c406369 100755 --- a/bin/emptydrops_cell_calling.R +++ b/bin/emptydrops_cell_calling.R @@ -23,7 +23,7 @@ get_name <- function(file) { # transpose matrices when required # based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin-fry would require transposition -print("Only kallisto and alevin-fry have transposed matrices.") +print("Only kallisto and alevin have transposed matrices.") if (aligner %in% c( "kallisto", "alevin" )) { is_transposed <- TRUE mtx<-t(mtx) From 77f91f770fe7585593ef9367d74d664a8192229e Mon Sep 17 00:00:00 2001 From: an-altosian Date: Mon, 16 Sep 2024 03:28:40 +0000 Subject: [PATCH 029/147] change aligner name back to alevin --- bin/emptydrops_cell_calling.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/emptydrops_cell_calling.R b/bin/emptydrops_cell_calling.R index 5c406369..23a45267 100755 --- a/bin/emptydrops_cell_calling.R +++ b/bin/emptydrops_cell_calling.R @@ -22,7 +22,7 @@ get_name <- function(file) { } # transpose matrices when required -# based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin-fry would require transposition +# based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin would require transposition print("Only kallisto and alevin have transposed matrices.") if (aligner %in% c( "kallisto", "alevin" )) { is_transposed <- TRUE From 973508b2abec263d43006ac9e6574a43f65ef124 Mon Sep 17 00:00:00 2001 From: an-altosian Date: Mon, 16 Sep 2024 03:35:26 +0000 Subject: [PATCH 030/147] make prettier happy --- CITATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATIONS.md b/CITATIONS.md index 8c2045bf..a9fa4e2f 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -19,7 +19,7 @@ > Ewels P, Magnusson M, Lundin S, Käller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. - [Simpleaf](https://doi.org/10.1093/bioinformatics/btad614) - + > He, D., Patro, R. simpleaf: a simple, flexible, and scalable framework for single-cell data processing using alevin-fry, Bioinformatics 39, 10 (2023). * [Alevin-fry](https://doi.org/10.1038/s41592-022-01408-3) From 83204317227513880fc330429658e256761b93df Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 09:25:25 +0200 Subject: [PATCH 031/147] fix file naming --- modules/local/anndatar_convert.nf | 4 ++-- modules/local/templates/anndatar_convert.R | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index dfe5fce9..a3a67a08 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -9,7 +9,7 @@ process ANNDATAR_CONVERT { tuple val(meta), path(h5ad) output: - tuple val(meta), path("${meta.id}_standardized.Rds"), emit: rds + tuple val(meta), path("${meta.id}/${meta.id}_${meta.input_type}_matrix.Rds"), emit: rds when: task.ext.when == null || task.ext.when @@ -19,6 +19,6 @@ process ANNDATAR_CONVERT { stub: """ - touch ${meta.id}_standardized.Rds + touch ${meta.id}.Rds """ } diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R index 479ac912..06a723d7 100755 --- a/modules/local/templates/anndatar_convert.R +++ b/modules/local/templates/anndatar_convert.R @@ -12,4 +12,5 @@ adata <- read_h5ad("${h5ad}") obj <- adata\$to_Seurat() # save files -saveRDS(obj, file = "${meta.id}_standardized.Rds") +dir.create(file.path("$meta.id"), showWarnings = FALSE) +saveRDS(obj, file = "${meta.id}/${meta.id}_${meta.input_type}_matrix.Rds") From 1c95e858d822dde703b4e25106dd3cae1226fb89 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 09:36:57 +0200 Subject: [PATCH 032/147] resolve emptydrops naming --- conf/modules.config | 2 +- subworkflows/local/emptydrops_removal.nf | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 8a53c285..ac26f731 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -39,7 +39,7 @@ process { ] } withName: 'ADATA_BARCODES' { - ext.prefix = { "${meta.id}_custom_emptydrops_filter_matrix" } + ext.prefix = { "${meta.id}_${meta.input_type}_matrix" } publishDir = [ path: { "${params.outdir}/${params.aligner}/mtx_conversions/${meta.id}" }, mode: params.publish_dir_mode diff --git a/subworkflows/local/emptydrops_removal.nf b/subworkflows/local/emptydrops_removal.nf index 202612d6..7171c3f3 100644 --- a/subworkflows/local/emptydrops_removal.nf +++ b/subworkflows/local/emptydrops_removal.nf @@ -11,7 +11,15 @@ workflow EMPTY_DROPLET_REMOVAL { CELLBENDER_REMOVEBACKGROUND(ch_unfiltered) ch_versions = ch_versions.mix(CELLBENDER_REMOVEBACKGROUND.out.versions) - ch_combined = ch_unfiltered.join(CELLBENDER_REMOVEBACKGROUND.out.barcodes) + ch_combined = + ch_unfiltered + .join(CELLBENDER_REMOVEBACKGROUND.out.barcodes) + .map { meta, h5ad, csv -> + def meta_clone = meta.clone() + meta_clone.input_type = 'emptydrops_filter' + + [ meta_clone, h5ad, csv ] + } ADATA_BARCODES(ch_combined) ch_versions = ch_versions.mix(ADATA_BARCODES.out.versions) From e6fff346698a321e8305d0ae6f78e58adcfd4b41 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 09:38:17 +0200 Subject: [PATCH 033/147] also convert emptydrops filter matrices --- workflows/scrnaseq.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 1e7ea9ef..a0e7c958 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -303,7 +303,7 @@ workflow SCRNASEQ { EMPTY_DROPLET_REMOVAL ( ch_h5ad_matrices_for_emptydrops ) - // ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTYDROPS_CELL_CALLING.out.filtered_matrices ) + ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTY_DROPLET_REMOVAL.out.h5ad ) } From e9c88e89891a9858bed3fea721432c56d6ea6301 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 09:40:03 +0200 Subject: [PATCH 034/147] move files to BKP since they will be replaced --- modules/local/{ => BKP}/emptydrops.nf | 0 modules/local/{ => BKP}/mtx_to_h5ad.nf | 0 modules/local/{ => BKP}/mtx_to_seurat.nf | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename modules/local/{ => BKP}/emptydrops.nf (100%) rename modules/local/{ => BKP}/mtx_to_h5ad.nf (100%) rename modules/local/{ => BKP}/mtx_to_seurat.nf (100%) diff --git a/modules/local/emptydrops.nf b/modules/local/BKP/emptydrops.nf similarity index 100% rename from modules/local/emptydrops.nf rename to modules/local/BKP/emptydrops.nf diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/BKP/mtx_to_h5ad.nf similarity index 100% rename from modules/local/mtx_to_h5ad.nf rename to modules/local/BKP/mtx_to_h5ad.nf diff --git a/modules/local/mtx_to_seurat.nf b/modules/local/BKP/mtx_to_seurat.nf similarity index 100% rename from modules/local/mtx_to_seurat.nf rename to modules/local/BKP/mtx_to_seurat.nf From f7f5fa520bb016f99de2bf8e0872aff7bf2eb902 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 12:16:11 +0200 Subject: [PATCH 035/147] add conversion for alevin --- modules/local/{ => alevin}/alevinqc.nf | 0 modules/local/alevin/mtx_to_h5ad.nf | 34 +++++++ modules/local/{ => alevin}/simpleaf_index.nf | 0 modules/local/{ => alevin}/simpleaf_quant.nf | 0 modules/local/alevin/templates/mtx_to_h5ad.py | 96 +++++++++++++++++++ subworkflows/local/alevin.nf | 25 +++-- workflows/scrnaseq.nf | 2 +- 7 files changed, 148 insertions(+), 9 deletions(-) rename modules/local/{ => alevin}/alevinqc.nf (100%) create mode 100644 modules/local/alevin/mtx_to_h5ad.nf rename modules/local/{ => alevin}/simpleaf_index.nf (100%) rename modules/local/{ => alevin}/simpleaf_quant.nf (100%) create mode 100755 modules/local/alevin/templates/mtx_to_h5ad.py diff --git a/modules/local/alevinqc.nf b/modules/local/alevin/alevinqc.nf similarity index 100% rename from modules/local/alevinqc.nf rename to modules/local/alevin/alevinqc.nf diff --git a/modules/local/alevin/mtx_to_h5ad.nf b/modules/local/alevin/mtx_to_h5ad.nf new file mode 100644 index 00000000..0c4fc844 --- /dev/null +++ b/modules/local/alevin/mtx_to_h5ad.nf @@ -0,0 +1,34 @@ +process MTX_TO_H5AD { + tag "$meta.id" + label 'process_medium' + + conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" + container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" + + input: + tuple val(meta), path(inputs) + path star_index + + output: + tuple val(meta2), path("${meta.id}/*h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Get a file to check input type. Some aligners bring arrays instead of a single file. + def input_to_check = (inputs instanceof String) ? inputs : inputs[0] + + // alevin has a single output + meta2 = meta + [input_type: 'raw'] + + template 'mtx_to_h5ad.py' + + stub: + """ + mkdir ${meta.id} + touch ${meta.id}/${meta.id}_matrix.h5ad + touch versions.yml + """ +} diff --git a/modules/local/simpleaf_index.nf b/modules/local/alevin/simpleaf_index.nf similarity index 100% rename from modules/local/simpleaf_index.nf rename to modules/local/alevin/simpleaf_index.nf diff --git a/modules/local/simpleaf_quant.nf b/modules/local/alevin/simpleaf_quant.nf similarity index 100% rename from modules/local/simpleaf_quant.nf rename to modules/local/alevin/simpleaf_quant.nf diff --git a/modules/local/alevin/templates/mtx_to_h5ad.py b/modules/local/alevin/templates/mtx_to_h5ad.py new file mode 100755 index 00000000..fb784fde --- /dev/null +++ b/modules/local/alevin/templates/mtx_to_h5ad.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Set numba chache dir to current working directory (which is a writable mount also in containers) +import os + +os.environ["NUMBA_CACHE_DIR"] = "." + +import scanpy as sc +import pandas as pd +import argparse +from anndata import AnnData +import platform + +def _mtx_to_adata( + input: str, + sample: str, +): + + adata = sc.read_mtx(f"{input}/quants_mat.mtx") + adata.obs_names = pd.read_csv(f"{input}/quants_mat_rows.txt", header=None, sep="\t")[0].values + adata.var_names = pd.read_csv(f"{input}/quants_mat_cols.txt", header=None, sep="\t")[0].values + adata.obs["sample"] = sample + + return adata + + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +def dump_versions(): + versions = { + "${task.process}": { + "python": platform.python_version(), + "scanpy": sc.__version__, + "pandas": pd.__version__ + } + } + + with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) + +def input_to_adata( + input_data: str, + output: str, + sample: str, +): + print(f"Reading in {input_data}") + + # open main data + adata = _mtx_to_adata(input_data, sample) + + # standard format + # index are gene IDs and symbols are a column + # TODO: how to get gene_symbols for alevin? + adata.var['gene_versions'] = adata.var.index + adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var["gene_ids"].values + adata.var = adata.var.drop("gene_ids", axis=1) + adata.var_names_make_unique() + + # write results + adata.write_h5ad(f"{output}", compression="gzip") + print(f"Wrote h5ad file to {output}") + + # dump versions + dump_versions() + + return adata + +# +# Run main script +# + +# create the directory with the sample name +os.makedirs("${meta2.id}", exist_ok=True) + +# input_type comes from NF module +adata = input_to_adata( + input_data="${meta2.id}_alevin_results/af_quant/alevin/", + output="${meta2.id}/${meta2.id}_${meta2.input_type}_matrix.h5ad", + sample="${meta2.id}" +) diff --git a/subworkflows/local/alevin.nf b/subworkflows/local/alevin.nf index 764c08f8..d8848dc4 100644 --- a/subworkflows/local/alevin.nf +++ b/subworkflows/local/alevin.nf @@ -1,11 +1,12 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { GFFREAD_TRANSCRIPTOME } from '../../modules/local/gffread_transcriptome' -include { ALEVINQC } from '../../modules/local/alevinqc' -include { SIMPLEAF_INDEX } from '../../modules/local/simpleaf_index' -include { SIMPLEAF_QUANT } from '../../modules/local/simpleaf_quant' +include { GFFREAD_TRANSCRIPTOME } from '../../modules/local/gffread_transcriptome' +include { ALEVINQC } from '../../modules/local/alevin/alevinqc' +include { SIMPLEAF_INDEX } from '../../modules/local/alevin/simpleaf_index' +include { SIMPLEAF_QUANT } from '../../modules/local/alevin/simpleaf_quant' +include { MTX_TO_H5AD } from '../../modules/local/alevin/mtx_to_h5ad' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ -include { GUNZIP } from '../../modules/nf-core/gunzip/main' +include { GUNZIP } from '../../modules/nf-core/gunzip/main' include { GFFREAD as GFFREAD_TXP2GENE } from '../../modules/nf-core/gffread/main' def multiqc_report = [] @@ -44,8 +45,6 @@ workflow SCRNASEQ_ALEVIN { } } - - /* * Perform quantification with salmon alevin */ @@ -58,6 +57,15 @@ workflow SCRNASEQ_ALEVIN { ) ch_versions = ch_versions.mix(SIMPLEAF_QUANT.out.versions) + /* + * Perform h5ad conversion + */ + MTX_TO_H5AD ( + SIMPLEAF_QUANT.out.alevin_results, + [] + ) + ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) + /* * Run alevinQC */ @@ -67,5 +75,6 @@ workflow SCRNASEQ_ALEVIN { emit: ch_versions alevin_results = SIMPLEAF_QUANT.out.alevin_results - alevinqc = ALEVINQC.out.report + alevin_h5ad = MTX_TO_H5AD.out.h5ad + alevinqc = ALEVINQC.out.report } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index a0e7c958..c4a63d7d 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -155,7 +155,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(SCRNASEQ_ALEVIN.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(SCRNASEQ_ALEVIN.out.alevin_results.map{ meta, it -> it }) - ch_h5ad_matrices = ch_h5ad_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_results) + ch_h5ad_matrices = ch_h5ad_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_h5ad) } // Run STARSolo pipeline From 73c91c27e0e6798f46d44ce05fdf5155821b68a8 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 26 Sep 2024 12:28:43 +0200 Subject: [PATCH 036/147] re-organise star modules --- .../local/{mtx_to_h5ad_star.nf => star/mtx_to_h5ad.nf} | 8 ++++---- modules/local/{ => star}/star_align.nf | 0 .../mtx_to_h5ad_star.py => star/templates/mtx_to_h5ad.py} | 8 ++++---- subworkflows/local/starsolo.nf | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename modules/local/{mtx_to_h5ad_star.nf => star/mtx_to_h5ad.nf} (80%) rename modules/local/{ => star}/star_align.nf (100%) rename modules/local/{templates/mtx_to_h5ad_star.py => star/templates/mtx_to_h5ad.py} (92%) diff --git a/modules/local/mtx_to_h5ad_star.nf b/modules/local/star/mtx_to_h5ad.nf similarity index 80% rename from modules/local/mtx_to_h5ad_star.nf rename to modules/local/star/mtx_to_h5ad.nf index 84474ed0..8e77749d 100644 --- a/modules/local/mtx_to_h5ad_star.nf +++ b/modules/local/star/mtx_to_h5ad.nf @@ -1,4 +1,4 @@ -process MTX_TO_H5AD_STAR { +process MTX_TO_H5AD { tag "$meta.id" label 'process_medium' @@ -21,10 +21,10 @@ process MTX_TO_H5AD_STAR { def input_to_check = (inputs instanceof String) ? inputs : inputs[0] // check input type of inputs - input_type = (input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' - meta2 = meta + [input_type: input_type] + input_type = (input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' + meta2 = meta + [input_type: input_type] - template 'mtx_to_h5ad_star.py' + template 'mtx_to_h5ad.py' stub: """ diff --git a/modules/local/star_align.nf b/modules/local/star/star_align.nf similarity index 100% rename from modules/local/star_align.nf rename to modules/local/star/star_align.nf diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/star/templates/mtx_to_h5ad.py similarity index 92% rename from modules/local/templates/mtx_to_h5ad_star.py rename to modules/local/star/templates/mtx_to_h5ad.py index 48749114..3a84295d 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/star/templates/mtx_to_h5ad.py @@ -82,11 +82,11 @@ def input_to_adata( # # create the directory with the sample name -os.makedirs("${meta.id}", exist_ok=True) +os.makedirs("${meta2.id}", exist_ok=True) # input_type comes from NF module adata = input_to_adata( - input_data="${input_type}", - output="${meta.id}/${meta.id}_${input_type}_matrix.h5ad", - sample="${meta.id}" + input_data="${meta2.input_type}", + output="${meta2.id}/${meta2.id}_${meta2.input_type}_matrix.h5ad", + sample="${meta2.id}" ) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 8236632d..3499f707 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -1,6 +1,6 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { STAR_ALIGN } from '../../modules/local/star_align' -include { MTX_TO_H5AD_STAR as MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad_star' +include { STAR_ALIGN } from '../../modules/local/star/star_align' +include { MTX_TO_H5AD } from '../../modules/local/star/mtx_to_h5ad' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ include { GUNZIP } from '../../modules/nf-core/gunzip/main' From 417bf698f44419f7ac23a806608738f8ba822de9 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 27 Sep 2024 09:52:37 +0200 Subject: [PATCH 037/147] fixed publishDir directives --- conf/modules.config | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index ac26f731..a6fc3411 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -35,22 +35,25 @@ process { withName: 'CELLBENDER_REMOVEBACKGROUND' { publishDir = [ path: { "${params.outdir}/${params.aligner}/${meta.id}/emptydrops_filter" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } withName: 'ADATA_BARCODES' { ext.prefix = { "${meta.id}_${meta.input_type}_matrix" } publishDir = [ path: { "${params.outdir}/${params.aligner}/mtx_conversions/${meta.id}" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } } - withName: 'MTX_TO_H5AD*|CONCAT_H5AD|ANNDATAR_CONVERT' { + withName: 'MTX_TO_H5AD|CONCAT_H5AD|ANNDATAR_CONVERT' { publishDir = [ path: { "${params.outdir}/${params.aligner}/mtx_conversions" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } @@ -164,8 +167,9 @@ if (params.aligner == "alevin") { } withName: 'SIMPLEAF_QUANT' { publishDir = [ - path: { "${params.outdir}/${params.aligner}" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/${params.aligner}/${meta.id}" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] ext.args = "-r cr-like" } From 94bf9c11ed6909d6615f059e3aebcc2ac12d4566 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 30 Sep 2024 13:47:53 +0200 Subject: [PATCH 038/147] add h5ad conversion module --- modules/local/mtx_to_h5ad.nf | 35 ++++++ .../local/templates/mtx_to_h5ad_cellranger.py | 101 ++++++++++++++++++ subworkflows/local/align_cellranger.nf | 21 +++- 3 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 modules/local/mtx_to_h5ad.nf create mode 100755 modules/local/templates/mtx_to_h5ad_cellranger.py diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf new file mode 100644 index 00000000..cb3a0a4f --- /dev/null +++ b/modules/local/mtx_to_h5ad.nf @@ -0,0 +1,35 @@ +process MTX_TO_H5AD { + tag "$meta.id" + label 'process_medium' + + conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : + 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + + input: + // inputs from cellranger nf-core module does not come in a single sample dir + // for each sample, the sub-folders and files come directly in array. + tuple val(meta), path(inputs) + path txp2gene + path star_index + + output: + tuple val(input_type), path("${meta.id}/*h5ad") , emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def aligner = (params.aligner in [ 'cellranger', 'cellrangerarc', 'cellrangermulti' ]) ? 'cellranger' : params.aligner + + template "mtx_to_h5ad_${aligner}.py" + + stub: + """ + mkdir ${meta.id} + touch ${meta.id}/${meta.id}_matrix.h5ad + touch versions.yml + """ +} diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py new file mode 100755 index 00000000..a91fe0cf --- /dev/null +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +# Set numba chache dir to current working directory (which is a writable mount also in containers) +import os + +os.environ["NUMBA_CACHE_DIR"] = "." + +import scanpy as sc +import pandas as pd +import argparse +from anndata import AnnData +import platform + +def _mtx_to_adata( + input: str, + sample: str, +): + + adata = sc.read_10x_h5(input) + adata.var["gene_symbols"] = adata.var_names + adata.var.set_index("gene_ids", inplace=True) + adata.obs["sample"] = sample + + # reorder columns for 10x mtx files + adata.var = adata.var[["gene_symbols", "feature_types", "genome"]] + + return adata + + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +def dump_versions(): + versions = { + "${task.process}": { + "python": platform.python_version(), + "scanpy": sc.__version__, + "pandas": pd.__version__ + } + } + + with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) + +def input_to_adata( + input_data: str, + output: str, + sample: str, +): + print(f"Reading in {input_data}") + + # open main data + adata = _mtx_to_adata(input_data, sample) + + # standard format + # index are gene IDs and symbols are a column + adata.var['gene_versions'] = adata.var.index + adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var["gene_ids"].values + adata.var = adata.var.drop("gene_ids", axis=1) + adata.var_names_make_unique() + + print(adata) + print(adata.var) + + # write results + adata.write_h5ad(f"{output}", compression="gzip") + print(f"Wrote h5ad file to {output}") + + # dump versions + dump_versions() + + return adata + +# +# Run main script +# + +# create the directory with the sample name +os.makedirs("${meta.id}", exist_ok=True) + +# input_type comes from NF module +adata = input_to_adata( + input_data="${meta.input_type}_feature_bc_matrix.h5", + output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + sample="${meta.id}" +) diff --git a/subworkflows/local/align_cellranger.nf b/subworkflows/local/align_cellranger.nf index 2461373b..413efc02 100644 --- a/subworkflows/local/align_cellranger.nf +++ b/subworkflows/local/align_cellranger.nf @@ -2,9 +2,10 @@ * Alignment with Cellranger */ -include {CELLRANGER_MKGTF} from "../../modules/nf-core/cellranger/mkgtf/main.nf" -include {CELLRANGER_MKREF} from "../../modules/nf-core/cellranger/mkref/main.nf" -include {CELLRANGER_COUNT} from "../../modules/nf-core/cellranger/count/main.nf" +include { CELLRANGER_MKGTF } from "../../modules/nf-core/cellranger/mkgtf/main.nf" +include { CELLRANGER_MKREF } from "../../modules/nf-core/cellranger/mkref/main.nf" +include { CELLRANGER_COUNT } from "../../modules/nf-core/cellranger/count/main.nf" +include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' // Define workflow to subset and index a genome region fasta file workflow CELLRANGER_ALIGN { @@ -49,7 +50,7 @@ workflow CELLRANGER_ALIGN { mtx_files.each{ if ( it.toString().contains("raw_feature_bc_matrix") ) { desired_files.add( it ) } } - [ meta, desired_files ] + [ meta + [input_type: 'raw'], desired_files ] } ch_matrices_filtered = @@ -58,9 +59,19 @@ workflow CELLRANGER_ALIGN { mtx_files.each{ if ( it.toString().contains("filtered_feature_bc_matrix") ) { desired_files.add( it ) } } - [ meta, desired_files ] + [ meta + [input_type: 'filtered'], desired_files ] } + /* + * Perform h5ad conversion + */ + MTX_TO_H5AD ( + ch_matrices_raw.mix( ch_matrices_filtered ), + [], + [] + ) + ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) + emit: ch_versions cellranger_out = CELLRANGER_COUNT.out.outs From 43a29c22e632dcdbfbe452debd836b750efcb54b Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 30 Sep 2024 14:32:39 +0200 Subject: [PATCH 039/147] integrate to mtx_conversion module currently receiving the error: 'H5AD encoding information is missing. This file may have been created with Python anndata<0.8.0.' --- modules/local/anndatar_convert.nf | 2 +- modules/local/mtx_to_h5ad.nf | 4 ++-- modules/local/templates/mtx_to_h5ad_cellranger.py | 3 --- subworkflows/local/align_cellranger.nf | 1 + workflows/scrnaseq.nf | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index a3a67a08..d6a8271d 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -3,7 +3,7 @@ process ANNDATAR_CONVERT { label 'process_medium' - container "fmalmeida/anndatar:dev" // TODO: Fix + container "docker://fmalmeida/anndatar:dev" // TODO: Fix input: tuple val(meta), path(h5ad) diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index cb3a0a4f..f708162d 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -15,8 +15,8 @@ process MTX_TO_H5AD { path star_index output: - tuple val(input_type), path("${meta.id}/*h5ad") , emit: h5ad - path "versions.yml" , emit: versions + tuple val(meta), path("${meta.id}/*h5ad"), emit: h5ad + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index a91fe0cf..294a15e7 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -74,9 +74,6 @@ def input_to_adata( adata.var = adata.var.drop("gene_ids", axis=1) adata.var_names_make_unique() - print(adata) - print(adata.var) - # write results adata.write_h5ad(f"{output}", compression="gzip") print(f"Wrote h5ad file to {output}") diff --git a/subworkflows/local/align_cellranger.nf b/subworkflows/local/align_cellranger.nf index 413efc02..09442313 100644 --- a/subworkflows/local/align_cellranger.nf +++ b/subworkflows/local/align_cellranger.nf @@ -76,5 +76,6 @@ workflow CELLRANGER_ALIGN { ch_versions cellranger_out = CELLRANGER_COUNT.out.outs cellranger_matrices = ch_matrices_raw.mix( ch_matrices_filtered ) + cellranger_h5ad = MTX_TO_H5AD.out.h5ad star_index = cellranger_index } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index c4a63d7d..4b6323ee 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -185,7 +185,7 @@ workflow SCRNASEQ { protocol_config['protocol'] ) ch_versions = ch_versions.mix(CELLRANGER_ALIGN.out.ch_versions) - ch_star_index = CELLRANGER_ALIGN.out.star_index + ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGER_ALIGN.out.cellranger_h5ad) ch_multiqc_files = ch_multiqc_files.mix(CELLRANGER_ALIGN.out.cellranger_out.map{ meta, outs -> outs.findAll{ it -> it.name == "web_summary.html"} }) From 7fc32f0a7bd6b701739156bd16c15fa6394a37ab Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 30 Sep 2024 14:48:11 +0200 Subject: [PATCH 040/147] reorganize module levels and fix docker image --- modules/local/alevin/mtx_to_h5ad.nf | 34 ------------------- modules/local/{alevin => }/alevinqc.nf | 0 modules/local/mtx_to_h5ad.nf | 6 ++-- modules/local/{alevin => }/simpleaf_index.nf | 0 modules/local/{alevin => }/simpleaf_quant.nf | 0 .../mtx_to_h5ad_alevin.py} | 8 ++--- subworkflows/local/alevin.nf | 11 +++--- 7 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 modules/local/alevin/mtx_to_h5ad.nf rename modules/local/{alevin => }/alevinqc.nf (100%) rename modules/local/{alevin => }/simpleaf_index.nf (100%) rename modules/local/{alevin => }/simpleaf_quant.nf (100%) rename modules/local/{alevin/templates/mtx_to_h5ad.py => templates/mtx_to_h5ad_alevin.py} (92%) diff --git a/modules/local/alevin/mtx_to_h5ad.nf b/modules/local/alevin/mtx_to_h5ad.nf deleted file mode 100644 index 0c4fc844..00000000 --- a/modules/local/alevin/mtx_to_h5ad.nf +++ /dev/null @@ -1,34 +0,0 @@ -process MTX_TO_H5AD { - tag "$meta.id" - label 'process_medium' - - conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" - container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" - - input: - tuple val(meta), path(inputs) - path star_index - - output: - tuple val(meta2), path("${meta.id}/*h5ad"), emit: h5ad - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - // Get a file to check input type. Some aligners bring arrays instead of a single file. - def input_to_check = (inputs instanceof String) ? inputs : inputs[0] - - // alevin has a single output - meta2 = meta + [input_type: 'raw'] - - template 'mtx_to_h5ad.py' - - stub: - """ - mkdir ${meta.id} - touch ${meta.id}/${meta.id}_matrix.h5ad - touch versions.yml - """ -} diff --git a/modules/local/alevin/alevinqc.nf b/modules/local/alevinqc.nf similarity index 100% rename from modules/local/alevin/alevinqc.nf rename to modules/local/alevinqc.nf diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index f708162d..751cf9a5 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -2,10 +2,8 @@ process MTX_TO_H5AD { tag "$meta.id" label 'process_medium' - conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : - 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" + container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" input: // inputs from cellranger nf-core module does not come in a single sample dir diff --git a/modules/local/alevin/simpleaf_index.nf b/modules/local/simpleaf_index.nf similarity index 100% rename from modules/local/alevin/simpleaf_index.nf rename to modules/local/simpleaf_index.nf diff --git a/modules/local/alevin/simpleaf_quant.nf b/modules/local/simpleaf_quant.nf similarity index 100% rename from modules/local/alevin/simpleaf_quant.nf rename to modules/local/simpleaf_quant.nf diff --git a/modules/local/alevin/templates/mtx_to_h5ad.py b/modules/local/templates/mtx_to_h5ad_alevin.py similarity index 92% rename from modules/local/alevin/templates/mtx_to_h5ad.py rename to modules/local/templates/mtx_to_h5ad_alevin.py index fb784fde..fba5a34f 100755 --- a/modules/local/alevin/templates/mtx_to_h5ad.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -86,11 +86,11 @@ def input_to_adata( # # create the directory with the sample name -os.makedirs("${meta2.id}", exist_ok=True) +os.makedirs("${meta.id}", exist_ok=True) # input_type comes from NF module adata = input_to_adata( - input_data="${meta2.id}_alevin_results/af_quant/alevin/", - output="${meta2.id}/${meta2.id}_${meta2.input_type}_matrix.h5ad", - sample="${meta2.id}" + input_data="${meta.id}_alevin_results/af_quant/alevin/", + output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + sample="${meta.id}" ) diff --git a/subworkflows/local/alevin.nf b/subworkflows/local/alevin.nf index d8848dc4..0aa2483f 100644 --- a/subworkflows/local/alevin.nf +++ b/subworkflows/local/alevin.nf @@ -1,9 +1,9 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ include { GFFREAD_TRANSCRIPTOME } from '../../modules/local/gffread_transcriptome' -include { ALEVINQC } from '../../modules/local/alevin/alevinqc' -include { SIMPLEAF_INDEX } from '../../modules/local/alevin/simpleaf_index' -include { SIMPLEAF_QUANT } from '../../modules/local/alevin/simpleaf_quant' -include { MTX_TO_H5AD } from '../../modules/local/alevin/mtx_to_h5ad' +include { ALEVINQC } from '../../modules/local/alevinqc' +include { SIMPLEAF_INDEX } from '../../modules/local/simpleaf_index' +include { SIMPLEAF_QUANT } from '../../modules/local/simpleaf_quant' +include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ include { GUNZIP } from '../../modules/nf-core/gunzip/main' @@ -61,7 +61,8 @@ workflow SCRNASEQ_ALEVIN { * Perform h5ad conversion */ MTX_TO_H5AD ( - SIMPLEAF_QUANT.out.alevin_results, + SIMPLEAF_QUANT.out.alevin_results.map{ meta, files -> [meta + [input_type: 'raw'], files] }, + [], [] ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From e36639681a00c8f959671cad2d85d2e9b19c5a48 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 30 Sep 2024 15:04:22 +0200 Subject: [PATCH 041/147] reorganize star modules --- modules/local/star/mtx_to_h5ad.nf | 35 ------------------- modules/local/{star => }/star_align.nf | 0 .../mtx_to_h5ad_star.py} | 8 ++--- subworkflows/local/starsolo.nf | 9 +++-- 4 files changed, 10 insertions(+), 42 deletions(-) delete mode 100644 modules/local/star/mtx_to_h5ad.nf rename modules/local/{star => }/star_align.nf (100%) rename modules/local/{star/templates/mtx_to_h5ad.py => templates/mtx_to_h5ad_star.py} (92%) diff --git a/modules/local/star/mtx_to_h5ad.nf b/modules/local/star/mtx_to_h5ad.nf deleted file mode 100644 index 8e77749d..00000000 --- a/modules/local/star/mtx_to_h5ad.nf +++ /dev/null @@ -1,35 +0,0 @@ -process MTX_TO_H5AD { - tag "$meta.id" - label 'process_medium' - - conda "conda-forge::scanpy==1.10.2 conda-forge::python-igraph conda-forge::leidenalg" - container "community.wave.seqera.io/library/scanpy:1.10.2--e83da2205b92a538" - - input: - tuple val(meta), path(inputs) - path star_index - - output: - tuple val(meta2), path("${meta.id}/*h5ad"), emit: h5ad - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - // Get a file to check input type. Some aligners bring arrays instead of a single file. - def input_to_check = (inputs instanceof String) ? inputs : inputs[0] - - // check input type of inputs - input_type = (input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' - meta2 = meta + [input_type: input_type] - - template 'mtx_to_h5ad.py' - - stub: - """ - mkdir ${meta.id} - touch ${meta.id}/${meta.id}_matrix.h5ad - touch versions.yml - """ -} diff --git a/modules/local/star/star_align.nf b/modules/local/star_align.nf similarity index 100% rename from modules/local/star/star_align.nf rename to modules/local/star_align.nf diff --git a/modules/local/star/templates/mtx_to_h5ad.py b/modules/local/templates/mtx_to_h5ad_star.py similarity index 92% rename from modules/local/star/templates/mtx_to_h5ad.py rename to modules/local/templates/mtx_to_h5ad_star.py index 3a84295d..49a1837e 100755 --- a/modules/local/star/templates/mtx_to_h5ad.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -82,11 +82,11 @@ def input_to_adata( # # create the directory with the sample name -os.makedirs("${meta2.id}", exist_ok=True) +os.makedirs("${meta.id}", exist_ok=True) # input_type comes from NF module adata = input_to_adata( - input_data="${meta2.input_type}", - output="${meta2.id}/${meta2.id}_${meta2.input_type}_matrix.h5ad", - sample="${meta2.id}" + input_data="${meta.input_type}", + output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + sample="${meta.id}" ) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 3499f707..6b71dbdd 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -1,6 +1,6 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { STAR_ALIGN } from '../../modules/local/star/star_align' -include { MTX_TO_H5AD } from '../../modules/local/star/mtx_to_h5ad' +include { STAR_ALIGN } from '../../modules/local/star_align' +include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ include { GUNZIP } from '../../modules/nf-core/gunzip/main' @@ -58,7 +58,10 @@ workflow STARSOLO { * Perform h5ad conversion */ MTX_TO_H5AD ( - STAR_ALIGN.out.raw_counts.mix( STAR_ALIGN.out.filtered_counts ), + STAR_ALIGN.out.raw_counts + .map{ meta, files -> [meta + [input_type: 'raw'], files] } + .mix( STAR_ALIGN.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ), + [], star_index.map{ meta, index -> index } ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From 76f126ebc0d804266bcaa49cce5ca71718e37080 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 09:36:44 +0200 Subject: [PATCH 042/147] re-structure and include cellranger --- subworkflows/local/align_cellranger.nf | 12 ------- subworkflows/local/mtx_conversion.nf | 33 ++++++++++++++++++-- workflows/scrnaseq.nf | 43 +++++++------------------- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/subworkflows/local/align_cellranger.nf b/subworkflows/local/align_cellranger.nf index 09442313..86b69a38 100644 --- a/subworkflows/local/align_cellranger.nf +++ b/subworkflows/local/align_cellranger.nf @@ -5,7 +5,6 @@ include { CELLRANGER_MKGTF } from "../../modules/nf-core/cellranger/mkgtf/main.nf" include { CELLRANGER_MKREF } from "../../modules/nf-core/cellranger/mkref/main.nf" include { CELLRANGER_COUNT } from "../../modules/nf-core/cellranger/count/main.nf" -include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' // Define workflow to subset and index a genome region fasta file workflow CELLRANGER_ALIGN { @@ -62,20 +61,9 @@ workflow CELLRANGER_ALIGN { [ meta + [input_type: 'filtered'], desired_files ] } - /* - * Perform h5ad conversion - */ - MTX_TO_H5AD ( - ch_matrices_raw.mix( ch_matrices_filtered ), - [], - [] - ) - ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) - emit: ch_versions cellranger_out = CELLRANGER_COUNT.out.outs cellranger_matrices = ch_matrices_raw.mix( ch_matrices_filtered ) - cellranger_h5ad = MTX_TO_H5AD.out.h5ad star_index = cellranger_index } diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 6435966d..3ecd1b36 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -1,27 +1,56 @@ /* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ +include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' +include { EMPTY_DROPLET_REMOVAL } from '../../subworkflows/local/emptydrops_removal' workflow MTX_CONVERSION { take: mtx_matrices + txp2gene + star_index samplesheet main: ch_versions = Channel.empty() + ch_h5ads = Channel.empty() + + // + // MODULE: Convert matrices to h5ad + // + MTX_TO_H5AD ( + mtx_matrices, + txp2gene, + star_index + ) + ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) + ch_h5ads = MTX_TO_H5AD.out.h5ad + + // + // SUBWORKFLOW: Run cellbender emptydrops filter + // + if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { + + // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it + EMPTY_DROPLET_REMOVAL ( + MTX_TO_H5AD.out.h5ad.filter { meta, mtx_files -> meta.input_type == 'raw' } + ) + ch_h5ads = ch_h5ads.mix( EMPTY_DROPLET_REMOVAL.out.h5ad ) + + } // // MODULE: Convert to Rds with AnndataR package // ANNDATAR_CONVERT ( - mtx_matrices + ch_h5ads ) // // Concat sample-specific h5ad in one // - ch_concat_h5ad_input = mtx_matrices.groupTuple() // gather all sample-specific files / per type + ch_concat_h5ad_input = ch_h5ads.groupTuple() // gather all sample-specific files / per type if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) // which nextflow break because it is not a valid 'path' thus, we have to remove one level diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 4b6323ee..282a7ab9 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -7,7 +7,6 @@ include { CELLRANGER_ALIGN } from '../subworkflows/local/align include { CELLRANGER_MULTI_ALIGN } from '../subworkflows/local/align_cellrangermulti' include { CELLRANGERARC_ALIGN } from '../subworkflows/local/align_cellrangerarc' include { UNIVERSC_ALIGN } from '../subworkflows/local/align_universc' -include { EMPTY_DROPLET_REMOVAL } from '../subworkflows/local/emptydrops_removal' include { MTX_CONVERSION } from '../subworkflows/local/mtx_conversion' include { GTF_GENE_FILTER } from '../modules/local/gtf_gene_filter' include { GUNZIP as GUNZIP_FASTA } from '../modules/nf-core/gunzip/main' @@ -87,7 +86,7 @@ workflow SCRNASEQ { empty_file = file("$projectDir/assets/EMPTY", checkIfExists: true) ch_versions = Channel.empty() - ch_h5ad_matrices = Channel.empty() + ch_mtx_matrices = Channel.empty() // Run FastQC if (!params.skip_fastqc) { @@ -137,7 +136,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(KALLISTO_BUSTOOLS.out.ch_versions) - ch_h5ad_matrices = ch_h5ad_matrices.mix(KALLISTO_BUSTOOLS.out.raw_counts, KALLISTO_BUSTOOLS.out.filtered_counts) + ch_mtx_matrices = ch_mtx_matrices.mix(KALLISTO_BUSTOOLS.out.raw_counts, KALLISTO_BUSTOOLS.out.filtered_counts) ch_txp2gene = KALLISTO_BUSTOOLS.out.txp2gene } @@ -155,7 +154,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(SCRNASEQ_ALEVIN.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(SCRNASEQ_ALEVIN.out.alevin_results.map{ meta, it -> it }) - ch_h5ad_matrices = ch_h5ad_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_h5ad) + ch_mtx_matrices = ch_mtx_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_h5ad) } // Run STARSolo pipeline @@ -172,7 +171,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) - ch_h5ad_matrices = STARSOLO.out.star_h5ad + ch_mtx_matrices = STARSOLO.out.star_h5ad } // Run cellranger pipeline @@ -185,8 +184,8 @@ workflow SCRNASEQ { protocol_config['protocol'] ) ch_versions = ch_versions.mix(CELLRANGER_ALIGN.out.ch_versions) - ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGER_ALIGN.out.cellranger_h5ad) - ch_multiqc_files = ch_multiqc_files.mix(CELLRANGER_ALIGN.out.cellranger_out.map{ + ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_ALIGN.out.cellranger_matrices) + ch_multiqc_files = ch_multiqc_files.mix(CELLRANGER_ALIGN.out.cellranger_out.map { meta, outs -> outs.findAll{ it -> it.name == "web_summary.html"} }) } @@ -201,7 +200,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(UNIVERSC_ALIGN.out.ch_versions) - ch_h5ad_matrices = ch_h5ad_matrices.mix(UNIVERSC_ALIGN.out.universc_out) + ch_mtx_matrices = ch_mtx_matrices.mix(UNIVERSC_ALIGN.out.universc_out) } // Run cellrangerarc pipeline @@ -215,7 +214,7 @@ workflow SCRNASEQ { ch_cellrangerarc_config ) ch_versions = ch_versions.mix(CELLRANGERARC_ALIGN.out.ch_versions) - ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGERARC_ALIGN.out.cellranger_arc_out) + ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGERARC_ALIGN.out.cellranger_arc_out) } // Run cellrangermulti pipeline @@ -283,33 +282,15 @@ workflow SCRNASEQ { ch_multiqc_files = ch_multiqc_files.mix( CELLRANGER_MULTI_ALIGN.out.cellrangermulti_out.map{ meta, outs -> outs.findAll{ it -> it.name == "web_summary.html" } }) - ch_h5ad_matrices = ch_h5ad_matrices.mix(CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx) - - } - - // SUBWORKFLOW: Run cellbender emptydrops filter - if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - - // - // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it - // - if ( params.aligner in [ 'cellranger', 'cellrangermulti', 'kallisto', 'star' ] ) { - ch_h5ad_matrices_for_emptydrops = - ch_h5ad_matrices.filter { meta, mtx_files -> meta.input_type == 'raw' } - } else { - ch_h5ad_matrices_for_emptydrops = ch_h5ad_matrices - } - - EMPTY_DROPLET_REMOVAL ( - ch_h5ad_matrices_for_emptydrops - ) - ch_h5ad_matrices = ch_h5ad_matrices.mix( EMPTY_DROPLET_REMOVAL.out.h5ad ) + ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx) } // Run mtx to h5ad conversion subworkflow MTX_CONVERSION ( - ch_h5ad_matrices, + ch_mtx_matrices, + ch_txp2gene, + ch_star_index, ch_input ) From b92af8b43e9716b9d1e90e29441f7a531170e564 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 09:47:58 +0200 Subject: [PATCH 043/147] add alevin to new structure --- subworkflows/local/alevin.nf | 14 +------------- workflows/scrnaseq.nf | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/subworkflows/local/alevin.nf b/subworkflows/local/alevin.nf index 0aa2483f..ae98cc85 100644 --- a/subworkflows/local/alevin.nf +++ b/subworkflows/local/alevin.nf @@ -3,7 +3,6 @@ include { GFFREAD_TRANSCRIPTOME } from '../../modules/local/gffread_transcriptom include { ALEVINQC } from '../../modules/local/alevinqc' include { SIMPLEAF_INDEX } from '../../modules/local/simpleaf_index' include { SIMPLEAF_QUANT } from '../../modules/local/simpleaf_quant' -include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' /* -- IMPORT NF-CORE MODULES/SUBWORKFLOWS -- */ include { GUNZIP } from '../../modules/nf-core/gunzip/main' @@ -57,16 +56,6 @@ workflow SCRNASEQ_ALEVIN { ) ch_versions = ch_versions.mix(SIMPLEAF_QUANT.out.versions) - /* - * Perform h5ad conversion - */ - MTX_TO_H5AD ( - SIMPLEAF_QUANT.out.alevin_results.map{ meta, files -> [meta + [input_type: 'raw'], files] }, - [], - [] - ) - ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) - /* * Run alevinQC */ @@ -75,7 +64,6 @@ workflow SCRNASEQ_ALEVIN { emit: ch_versions - alevin_results = SIMPLEAF_QUANT.out.alevin_results - alevin_h5ad = MTX_TO_H5AD.out.h5ad + alevin_results = SIMPLEAF_QUANT.out.alevin_results.map{ meta, files -> [meta + [input_type: 'raw'], files] } alevinqc = ALEVINQC.out.report } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 282a7ab9..a55029ea 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -154,7 +154,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(SCRNASEQ_ALEVIN.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(SCRNASEQ_ALEVIN.out.alevin_results.map{ meta, it -> it }) - ch_mtx_matrices = ch_mtx_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_h5ad) + ch_mtx_matrices = ch_mtx_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_results) } // Run STARSolo pipeline From c57b09fc98fe0b21c54234dcf3db729f1ab04f60 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 10:29:37 +0200 Subject: [PATCH 044/147] add star to new structure --- subworkflows/local/starsolo.nf | 18 +++++------------- workflows/scrnaseq.nf | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 6b71dbdd..d69ce9d9 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -54,24 +54,16 @@ workflow STARSOLO { ) ch_versions = ch_versions.mix(STAR_ALIGN.out.versions) - /* - * Perform h5ad conversion - */ - MTX_TO_H5AD ( - STAR_ALIGN.out.raw_counts - .map{ meta, files -> [meta + [input_type: 'raw'], files] } - .mix( STAR_ALIGN.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ), - [], - star_index.map{ meta, index -> index } - ) - ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) + // generate channel of star counts with correct metadata + ch_star_counts = + STAR_ALIGN.out.raw_counts.map{ meta, files -> [meta + [input_type: 'raw'], files] } + .mix( STAR_ALIGN.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ) emit: ch_versions // get rid of meta for star index star_result = STAR_ALIGN.out.tab - star_counts = STAR_ALIGN.out.counts - star_h5ad = MTX_TO_H5AD.out.h5ad + star_counts = ch_star_counts for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index a55029ea..2f2301c1 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -171,7 +171,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) - ch_mtx_matrices = STARSOLO.out.star_h5ad + ch_mtx_matrices = STARSOLO.out.star_counts } // Run cellranger pipeline From d0d1f814b56971d1757e681a6a29f022a80080fd Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 13:04:06 +0200 Subject: [PATCH 045/147] Add kallisto standard workflow to structure --- modules/local/templates/mtx_to_h5ad_alevin.py | 4 +- .../local/templates/mtx_to_h5ad_kallisto.py | 103 ++++++++++++++++++ subworkflows/local/kallisto_bustools.nf | 8 +- workflows/scrnaseq.nf | 2 +- 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100755 modules/local/templates/mtx_to_h5ad_kallisto.py diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index fba5a34f..50107e0c 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -17,8 +17,8 @@ def _mtx_to_adata( ): adata = sc.read_mtx(f"{input}/quants_mat.mtx") - adata.obs_names = pd.read_csv(f"{input}/quants_mat_rows.txt", header=None, sep="\t")[0].values - adata.var_names = pd.read_csv(f"{input}/quants_mat_cols.txt", header=None, sep="\t")[0].values + adata.obs_names = pd.read_csv(f"{input}/quants_mat_rows.txt", header=None, sep="\\t")[0].values + adata.var_names = pd.read_csv(f"{input}/quants_mat_cols.txt", header=None, sep="\\t")[0].values adata.obs["sample"] = sample return adata diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py new file mode 100755 index 00000000..cf27f13c --- /dev/null +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +# Set numba chache dir to current working directory (which is a writable mount also in containers) +import os + +os.environ["NUMBA_CACHE_DIR"] = "." + +import scanpy as sc +import pandas as pd +import argparse +from anndata import AnnData +import platform +import glob + +def _mtx_to_adata( + input: str, + sample: str, + t2g: str +): + + adata = sc.read_mtx(glob.glob(f"{input}/*.mtx")[0]) + adata.obs_names = pd.read_csv(glob.glob(f"{input}/*.barcodes.txt")[0], header=None, sep="\\t")[0].values + adata.var_names = pd.read_csv(glob.glob(f"{input}/*.genes.txt")[0], header=None, sep="\\t")[0].values + adata.obs["sample"] = sample + + txp2gene = pd.read_table(glob.glob(f"{t2g}")[0], header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) + txp2gene = txp2gene.drop_duplicates(subset="gene_id").set_index("gene_id") + adata.var = pd.merge(adata.var, txp2gene, left_index=True, right_index=True) + + return adata + + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +def dump_versions(): + versions = { + "${task.process}": { + "python": platform.python_version(), + "scanpy": sc.__version__, + "pandas": pd.__version__ + } + } + + with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) + +def input_to_adata( + input_data: str, + output: str, + sample: str, + t2g: str +): + print(f"Reading in {input_data}") + + # open main data + adata = _mtx_to_adata(input_data, sample, t2g) + + # standard format + # index are gene IDs and symbols are a column + adata.var['gene_versions'] = adata.var.index + adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var["gene_ids"].values + adata.var = adata.var.drop("gene_ids", axis=1) + adata.var_names_make_unique() + + # write results + adata.write_h5ad(f"{output}", compression="gzip") + print(f"Wrote h5ad file to {output}") + + # dump versions + dump_versions() + + return adata + +# +# Run main script +# + +# create the directory with the sample name +os.makedirs("${meta.id}", exist_ok=True) + +# input_type comes from NF module +adata = input_to_adata( + input_data="${inputs}", + output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + sample="${meta.id}", + t2g="${txp2gene}" +) diff --git a/subworkflows/local/kallisto_bustools.nf b/subworkflows/local/kallisto_bustools.nf index 3deee2c5..d6b171a6 100644 --- a/subworkflows/local/kallisto_bustools.nf +++ b/subworkflows/local/kallisto_bustools.nf @@ -55,20 +55,18 @@ workflow KALLISTO_BUSTOOLS { // get raw/filtered counts ch_raw_counts = KALLISTOBUSTOOLS_COUNT.out.count.map{ meta, kb_dir -> if (file("${kb_dir.toUriString()}/counts_unfiltered").exists()) { - [meta, file("${kb_dir.toUriString()}/counts_unfiltered")] + [meta + [input_type: 'raw'], file("${kb_dir.toUriString()}/counts_unfiltered")] } } ch_filtered_counts = KALLISTOBUSTOOLS_COUNT.out.count.map{ meta, kb_dir -> if (file("${kb_dir.toUriString()}/counts_filtered").exists()) { - [meta, file("${kb_dir.toUriString()}/counts_filtered")] + [meta + [input_type: 'filtered'], file("${kb_dir.toUriString()}/counts_filtered")] } } emit: ch_versions - counts = KALLISTOBUSTOOLS_COUNT.out.count - raw_counts = ch_raw_counts - filtered_counts = ch_filtered_counts + counts = ch_raw_counts.mix (ch_filtered_counts) txp2gene = txp2gene.collect() } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 2f2301c1..3f3c1017 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -136,7 +136,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(KALLISTO_BUSTOOLS.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(KALLISTO_BUSTOOLS.out.raw_counts, KALLISTO_BUSTOOLS.out.filtered_counts) + ch_mtx_matrices = KALLISTO_BUSTOOLS.out.counts ch_txp2gene = KALLISTO_BUSTOOLS.out.txp2gene } From aa99cd17eac212e350d1d20fe251ccd187c9343c Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 16:16:11 +0200 Subject: [PATCH 046/147] Account for non-standard kallisto workflows --- .../local/templates/mtx_to_h5ad_kallisto.py | 58 ++++++++++++------- subworkflows/local/emptydrops_removal.nf | 2 +- subworkflows/local/mtx_conversion.nf | 20 ++++++- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index cf27f13c..24efe7e0 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -13,14 +13,16 @@ import glob def _mtx_to_adata( - input: str, - sample: str, - t2g: str + matrix: str, + barcodes: str, + features: str, + t2g: str, + sample: str ): - adata = sc.read_mtx(glob.glob(f"{input}/*.mtx")[0]) - adata.obs_names = pd.read_csv(glob.glob(f"{input}/*.barcodes.txt")[0], header=None, sep="\\t")[0].values - adata.var_names = pd.read_csv(glob.glob(f"{input}/*.genes.txt")[0], header=None, sep="\\t")[0].values + adata = sc.read_mtx(matrix) + adata.obs_names = pd.read_csv(barcodes, header=None, sep="\\t")[0].values + adata.var_names = pd.read_csv(features, header=None, sep="\\t")[0].values adata.obs["sample"] = sample txp2gene = pd.read_table(glob.glob(f"{t2g}")[0], header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) @@ -60,15 +62,17 @@ def dump_versions(): f.write(format_yaml_like(versions)) def input_to_adata( - input_data: str, + matrix: str, + barcodes: str, + features: str, + t2g: str, output: str, sample: str, - t2g: str ): - print(f"Reading in {input_data}") + print(f"Reading in {matrix}") # open main data - adata = _mtx_to_adata(input_data, sample, t2g) + adata = _mtx_to_adata(matrix=matrix, barcodes=barcodes, features=features, sample=sample, t2g=t2g) # standard format # index are gene IDs and symbols are a column @@ -82,11 +86,6 @@ def input_to_adata( adata.write_h5ad(f"{output}", compression="gzip") print(f"Wrote h5ad file to {output}") - # dump versions - dump_versions() - - return adata - # # Run main script # @@ -95,9 +94,26 @@ def input_to_adata( os.makedirs("${meta.id}", exist_ok=True) # input_type comes from NF module -adata = input_to_adata( - input_data="${inputs}", - output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", - sample="${meta.id}", - t2g="${txp2gene}" -) +if "${params.kb_workflow}" == "standard": + input_to_adata( + matrix=glob.glob("${inputs}/*.mtx")[0], + barcodes=glob.glob("${inputs}/*.barcodes.txt")[0], + features=glob.glob("${inputs}/*.genes.txt")[0], + output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + sample="${meta.id}", + t2g="${txp2gene}" + ) + +else: + for type in ['spliced', 'unspliced']: + input_to_adata( + matrix=glob.glob("${inputs}/" + f"{type}*.mtx")[0], + barcodes=glob.glob("${inputs}/" + f"{type}*.barcodes.txt")[0], + features=glob.glob("${inputs}/" + f"{type}*.genes.txt")[0], + output="${meta.id}/${meta.id}_${meta.input_type}" + f"_{type}_matrix.h5ad", + sample="${meta.id}", + t2g="${txp2gene}" + ) + +# dump versions +dump_versions() diff --git a/subworkflows/local/emptydrops_removal.nf b/subworkflows/local/emptydrops_removal.nf index 7171c3f3..2ccacc26 100644 --- a/subworkflows/local/emptydrops_removal.nf +++ b/subworkflows/local/emptydrops_removal.nf @@ -16,7 +16,7 @@ workflow EMPTY_DROPLET_REMOVAL { .join(CELLBENDER_REMOVEBACKGROUND.out.barcodes) .map { meta, h5ad, csv -> def meta_clone = meta.clone() - meta_clone.input_type = 'emptydrops_filter' + meta_clone.input_type = meta['input_type'].toString().replaceAll('raw', 'emptydrops_filter') [ meta_clone, h5ad, csv ] } diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 3ecd1b36..55bd68dc 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -25,7 +25,23 @@ workflow MTX_CONVERSION { star_index ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) - ch_h5ads = MTX_TO_H5AD.out.h5ad + + // fix channel size when kallisto non-standard workflow + if (params.aligner == 'kallisto' && !(params.kb_workflow == 'standard')) { + ch_h5ads = + MTX_TO_H5AD.out.h5ad + .transpose() + .map { meta, h5ad -> + def meta_clone = meta.clone() + def spc_prefix = h5ad.toString().contains('unspliced') ? 'un' : '' + + meta_clone["input_type"] = "${meta.input_type}_${spc_prefix}spliced" + + [ meta_clone, h5ad ] + } + } else { + ch_h5ads = MTX_TO_H5AD.out.h5ad + } // // SUBWORKFLOW: Run cellbender emptydrops filter @@ -34,7 +50,7 @@ workflow MTX_CONVERSION { // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it EMPTY_DROPLET_REMOVAL ( - MTX_TO_H5AD.out.h5ad.filter { meta, mtx_files -> meta.input_type == 'raw' } + ch_h5ads.filter { meta, mtx_files -> meta.input_type.contains('raw') } ) ch_h5ads = ch_h5ads.mix( EMPTY_DROPLET_REMOVAL.out.h5ad ) From 0ca1161dbac0722cd0e46afe270cb9cc40260d90 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 4 Oct 2024 16:19:31 +0200 Subject: [PATCH 047/147] Simplify alevin template --- modules/local/templates/mtx_to_h5ad_alevin.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index 50107e0c..db295f1b 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -76,11 +76,6 @@ def input_to_adata( adata.write_h5ad(f"{output}", compression="gzip") print(f"Wrote h5ad file to {output}") - # dump versions - dump_versions() - - return adata - # # Run main script # @@ -89,8 +84,11 @@ def input_to_adata( os.makedirs("${meta.id}", exist_ok=True) # input_type comes from NF module -adata = input_to_adata( +input_to_adata( input_data="${meta.id}_alevin_results/af_quant/alevin/", output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) + +# dump versions +dump_versions() From 28251b068a5df1f12661195fbb0b60eb29656337 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 7 Oct 2024 09:21:35 +0200 Subject: [PATCH 048/147] reorganise star template --- modules/local/templates/mtx_to_h5ad_star.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index 49a1837e..0cc9a969 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -72,11 +72,6 @@ def input_to_adata( adata.write_h5ad(f"{output}", compression="gzip") print(f"Wrote h5ad file to {output}") - # dump versions - dump_versions() - - return adata - # # Run main script # @@ -85,8 +80,11 @@ def input_to_adata( os.makedirs("${meta.id}", exist_ok=True) # input_type comes from NF module -adata = input_to_adata( +input_to_adata( input_data="${meta.input_type}", output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) + +# dump versions +dump_versions() From c785b971828b077f39b1ec18425a00c5995b32c6 Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Tue, 8 Oct 2024 12:32:06 +0000 Subject: [PATCH 049/147] Template update for nf-core/tools version 3.0.0 --- .editorconfig | 4 + .github/CONTRIBUTING.md | 10 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/awsfulltest.yml | 23 +- .github/workflows/ci.yml | 17 +- .github/workflows/download_pipeline.yml | 53 ++- .github/workflows/linting.yml | 23 +- .github/workflows/linting_comment.yml | 2 +- .github/workflows/release-announcements.yml | 2 +- .../workflows/template_version_comment.yml | 43 ++ .gitpod.yml | 7 +- .nf-core.yml | 22 +- .pre-commit-config.yaml | 2 +- .prettierignore | 1 + CHANGELOG.md | 2 +- CITATIONS.md | 4 +- README.md | 5 +- assets/multiqc_config.yml | 6 +- assets/schema_input.json | 2 +- conf/base.config | 34 +- conf/igenomes_ignored.config | 9 + conf/modules.config | 1 - conf/test.config | 13 +- docs/images/mqc_fastqc_adapter.png | Bin 23458 -> 0 bytes docs/images/mqc_fastqc_counts.png | Bin 33918 -> 0 bytes docs/images/mqc_fastqc_quality.png | Bin 55769 -> 0 bytes docs/output.md | 11 +- docs/usage.md | 12 +- main.nf | 10 +- modules.json | 12 +- modules/nf-core/fastqc/environment.yml | 2 - modules/nf-core/fastqc/main.nf | 5 +- modules/nf-core/fastqc/meta.yml | 57 +-- modules/nf-core/fastqc/tests/main.nf.test | 225 ++++++++--- .../nf-core/fastqc/tests/main.nf.test.snap | 370 ++++++++++++++++-- modules/nf-core/multiqc/environment.yml | 4 +- modules/nf-core/multiqc/main.nf | 14 +- modules/nf-core/multiqc/meta.yml | 78 ++-- modules/nf-core/multiqc/tests/main.nf.test | 8 + .../nf-core/multiqc/tests/main.nf.test.snap | 20 +- modules/nf-core/multiqc/tests/nextflow.config | 5 + nextflow.config | 148 ++++--- nextflow_schema.json | 85 +--- .../utils_nfcore_scrnaseq_pipeline/main.nf | 56 +-- .../nf-core/utils_nextflow_pipeline/main.nf | 24 +- .../tests/nextflow.config | 2 +- .../nf-core/utils_nfcore_pipeline/main.nf | 45 ++- .../nf-core/utils_nfschema_plugin/main.nf | 46 +++ .../nf-core/utils_nfschema_plugin/meta.yml | 35 ++ .../utils_nfschema_plugin/tests/main.nf.test | 117 ++++++ .../tests/nextflow.config | 8 + .../tests/nextflow_schema.json | 8 +- .../nf-core/utils_nfvalidation_plugin/main.nf | 62 --- .../utils_nfvalidation_plugin/meta.yml | 44 --- .../tests/main.nf.test | 200 ---------- .../utils_nfvalidation_plugin/tests/tags.yml | 2 - workflows/scrnaseq.nf | 23 +- 57 files changed, 1213 insertions(+), 812 deletions(-) create mode 100644 .github/workflows/template_version_comment.yml create mode 100644 conf/igenomes_ignored.config delete mode 100755 docs/images/mqc_fastqc_adapter.png delete mode 100755 docs/images/mqc_fastqc_counts.png delete mode 100755 docs/images/mqc_fastqc_quality.png create mode 100644 modules/nf-core/multiqc/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/main.nf create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/meta.yml create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config rename subworkflows/nf-core/{utils_nfvalidation_plugin => utils_nfschema_plugin}/tests/nextflow_schema.json (95%) delete mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/main.nf delete mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml delete mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test delete mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml diff --git a/.editorconfig b/.editorconfig index 72dda289..e1058815 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,7 @@ indent_style = space [*.{md,yml,yaml,html,css,scss,js}] indent_size = 2 + # These files are edited and tested upstream in nf-core/modules [/modules/nf-core/**] charset = unset @@ -25,9 +26,12 @@ insert_final_newline = unset trim_trailing_whitespace = unset indent_style = unset + + [/assets/email*] indent_size = unset + # ignore python and markdown [*.{py,md}] indent_style = unset diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d2dc65c3..ba739ae1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,7 @@ If you'd like to write some code for nf-core/scrnaseq, the standard workflow is 1. Check that there isn't already an issue about your idea in the [nf-core/scrnaseq issues](https://github.com/nf-core/scrnaseq/issues) to avoid duplicating work. If there isn't one already, please create one so that others know you're working on this 2. [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the [nf-core/scrnaseq repository](https://github.com/nf-core/scrnaseq) to your GitHub account 3. Make the necessary changes / additions within your forked repository following [Pipeline conventions](#pipeline-contribution-conventions) -4. Use `nf-core schema build` and add any new parameters to the pipeline JSON schema (requires [nf-core tools](https://github.com/nf-core/tools) >= 1.10). +4. Use `nf-core pipelines schema build` and add any new parameters to the pipeline JSON schema (requires [nf-core tools](https://github.com/nf-core/tools) >= 1.10). 5. Submit a Pull Request against the `dev` branch and wait for the code to be reviewed and merged If you're not used to this workflow with git, you can start with some [docs from GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or even their [excellent `git` resources](https://try.github.io/). @@ -40,7 +40,7 @@ There are typically two types of tests that run: ### Lint tests `nf-core` has a [set of guidelines](https://nf-co.re/developers/guidelines) which all pipelines must adhere to. -To enforce these and ensure that all pipelines stay in sync, we have developed a helper tool which runs checks on the pipeline code. This is in the [nf-core/tools repository](https://github.com/nf-core/tools) and once installed can be run locally with the `nf-core lint ` command. +To enforce these and ensure that all pipelines stay in sync, we have developed a helper tool which runs checks on the pipeline code. This is in the [nf-core/tools repository](https://github.com/nf-core/tools) and once installed can be run locally with the `nf-core pipelines lint ` command. If any failures or warnings are encountered, please follow the listed URL for more documentation. @@ -75,7 +75,7 @@ If you wish to contribute a new step, please use the following coding standards: 2. Write the process block (see below). 3. Define the output channel if needed (see below). 4. Add any new parameters to `nextflow.config` with a default (see below). -5. Add any new parameters to `nextflow_schema.json` with help text (via the `nf-core schema build` tool). +5. Add any new parameters to `nextflow_schema.json` with help text (via the `nf-core pipelines schema build` tool). 6. Add sanity checks and validation for all relevant parameters. 7. Perform local tests to validate that the new code works as expected. 8. If applicable, add a new test command in `.github/workflow/ci.yml`. @@ -86,7 +86,7 @@ If you wish to contribute a new step, please use the following coding standards: Parameters should be initialised / defined with default values in `nextflow.config` under the `params` scope. -Once there, use `nf-core schema build` to add to `nextflow_schema.json`. +Once there, use `nf-core pipelines schema build` to add to `nextflow_schema.json`. ### Default processes resource requirements @@ -103,7 +103,7 @@ Please use the following naming schemes, to make it easy to understand what is g ### Nextflow version bumping -If you are using a new feature from core Nextflow, you may bump the minimum required version of nextflow in the pipeline with: `nf-core bump-version --nextflow . [min-nf-version]` +If you are using a new feature from core Nextflow, you may bump the minimum required version of nextflow in the pipeline with: `nf-core pipelines bump-version --nextflow . [min-nf-version]` ### Images and figures diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 073b2953..4a1fd411 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,7 +17,7 @@ Learn more about contributing: [CONTRIBUTING.md](https://github.com/nf-core/scrn - [ ] If you've fixed a bug or added code that should be tested, add tests! - [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/nf-core/scrnaseq/tree/master/.github/CONTRIBUTING.md) - [ ] If necessary, also make a PR on the nf-core/scrnaseq _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. -- [ ] Make sure your code lints (`nf-core lint`). +- [ ] Make sure your code lints (`nf-core pipelines lint`). - [ ] Ensure the test suite passes (`nextflow run . -profile test,docker --outdir `). - [ ] Check for unexpected warnings in debug mode (`nextflow run . -profile debug,test,docker --outdir `). - [ ] Usage Documentation in `docs/usage.md` is updated. diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index c890dcd6..886e3814 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -1,18 +1,33 @@ name: nf-core AWS full size tests -# This workflow is triggered on published releases. +# This workflow is triggered on PRs opened against the master branch. # It can be additionally triggered manually with GitHub actions workflow dispatch button. # It runs the -profile 'test_full' on AWS batch on: - release: - types: [published] + pull_request: + branches: + - master workflow_dispatch: + pull_request_review: + types: [submitted] + jobs: run-platform: name: Run AWS full tests - if: github.repository == 'nf-core/scrnaseq' + if: github.repository == 'nf-core/scrnaseq' && github.event.review.state == 'approved' runs-on: ubuntu-latest steps: + - uses: octokit/request-action@v2.x + id: check_approvals + with: + route: GET /repos/${{ github.repository }}/pulls/${{ github.event.review.number }}/reviews + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: test_variables + run: | + JSON_RESPONSE='${{ steps.check_approvals.outputs.data }}' + CURRENT_APPROVALS_COUNT=$(echo $JSON_RESPONSE | jq -c '[.[] | select(.state | contains("APPROVED")) ] | length') + test $CURRENT_APPROVALS_COUNT -ge 2 || exit 1 # At least 2 approvals are required - name: Launch workflow via Seqera Platform uses: seqeralabs/action-tower-launch@v2 # TODO nf-core: You can customise AWS full pipeline tests as required diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db462ba7..37227a13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: release: types: [published] + workflow_dispatch: env: NXF_ANSI_LOG: false @@ -24,7 +25,7 @@ jobs: strategy: matrix: NXF_VER: - - "23.04.0" + - "24.04.2" - "latest-everything" steps: - name: Check out pipeline code @@ -38,9 +39,21 @@ jobs: - name: Disk space cleanup uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - name: Run pipeline with test data + - name: Run pipeline with test data (docker) # TODO nf-core: You can customise CI pipeline run tests as required # For example: adding multiple test runs with different parameters # Remember that you can parallelise this by using strategy.matrix run: | nextflow run ${GITHUB_WORKSPACE} -profile test,docker --outdir ./results + + - name: Run pipeline with test data (singularity) + # TODO nf-core: You can customise CI pipeline run tests as required + run: | + nextflow run ${GITHUB_WORKSPACE} -profile test,singularity --outdir ./results + if: "${{ github.base_ref == 'master' }}" + + - name: Run pipeline with test data (conda) + # TODO nf-core: You can customise CI pipeline run tests as required + run: | + nextflow run ${GITHUB_WORKSPACE} -profile test,conda --outdir ./results + if: "${{ github.base_ref == 'master' }}" diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index 2d20d644..713dc3e7 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -1,4 +1,4 @@ -name: Test successful pipeline download with 'nf-core download' +name: Test successful pipeline download with 'nf-core pipelines download' # Run the workflow when: # - dispatched manually @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: testbranch: - description: "The specific branch you wish to utilize for the test execution of nf-core download." + description: "The specific branch you wish to utilize for the test execution of nf-core pipelines download." required: true default: "dev" pull_request: @@ -39,9 +39,11 @@ jobs: with: python-version: "3.12" architecture: "x64" - - uses: eWaterCycle/setup-singularity@931d4e31109e875b13309ae1d07c70ca8fbc8537 # v7 + + - name: Setup Apptainer + uses: eWaterCycle/setup-apptainer@4bb22c52d4f63406c49e94c804632975787312b3 # v2.0.0 with: - singularity-version: 3.8.3 + apptainer-version: 1.3.4 - name: Install dependencies run: | @@ -54,33 +56,64 @@ jobs: echo "REPOTITLE_LOWERCASE=$(basename ${GITHUB_REPOSITORY,,})" >> ${GITHUB_ENV} echo "REPO_BRANCH=${{ github.event.inputs.testbranch || 'dev' }}" >> ${GITHUB_ENV} + - name: Make a cache directory for the container images + run: | + mkdir -p ./singularity_container_images + - name: Download the pipeline env: - NXF_SINGULARITY_CACHEDIR: ./ + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images run: | - nf-core download ${{ env.REPO_LOWERCASE }} \ + nf-core pipelines download ${{ env.REPO_LOWERCASE }} \ --revision ${{ env.REPO_BRANCH }} \ --outdir ./${{ env.REPOTITLE_LOWERCASE }} \ --compress "none" \ --container-system 'singularity' \ - --container-library "quay.io" -l "docker.io" -l "ghcr.io" \ + --container-library "quay.io" -l "docker.io" -l "community.wave.seqera.io" \ --container-cache-utilisation 'amend' \ - --download-configuration + --download-configuration 'yes' - name: Inspect download run: tree ./${{ env.REPOTITLE_LOWERCASE }} + - name: Count the downloaded number of container images + id: count_initial + run: | + image_count=$(ls -1 ./singularity_container_images | wc -l | xargs) + echo "Initial container image count: $image_count" + echo "IMAGE_COUNT_INITIAL=$image_count" >> ${GITHUB_ENV} + - name: Run the downloaded pipeline (stub) id: stub_run_pipeline continue-on-error: true env: - NXF_SINGULARITY_CACHEDIR: ./ + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images NXF_SINGULARITY_HOME_MOUNT: true run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -stub -profile test,singularity --outdir ./results - name: Run the downloaded pipeline (stub run not supported) id: run_pipeline if: ${{ job.steps.stub_run_pipeline.status == failure() }} env: - NXF_SINGULARITY_CACHEDIR: ./ + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images NXF_SINGULARITY_HOME_MOUNT: true run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -profile test,singularity --outdir ./results + + - name: Count the downloaded number of container images + id: count_afterwards + run: | + image_count=$(ls -1 ./singularity_container_images | wc -l | xargs) + echo "Post-pipeline run container image count: $image_count" + echo "IMAGE_COUNT_AFTER=$image_count" >> ${GITHUB_ENV} + + - name: Compare container image counts + run: | + if [ "${{ env.IMAGE_COUNT_INITIAL }}" -ne "${{ env.IMAGE_COUNT_AFTER }}" ]; then + initial_count=${{ env.IMAGE_COUNT_INITIAL }} + final_count=${{ env.IMAGE_COUNT_AFTER }} + difference=$((final_count - initial_count)) + echo "$difference additional container images were \n downloaded at runtime . The pipeline has no support for offline runs!" + tree ./singularity_container_images + exit 1 + else + echo "The pipeline can be downloaded successfully!" + fi diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 1fcafe88..b882838a 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,6 +1,6 @@ name: nf-core linting # This workflow is triggered on pushes and PRs to the repository. -# It runs the `nf-core lint` and markdown lint tests to ensure +# It runs the `nf-core pipelines lint` and markdown lint tests to ensure # that the code meets the nf-core guidelines. on: push: @@ -41,17 +41,32 @@ jobs: python-version: "3.12" architecture: "x64" + - name: read .nf-core.yml + uses: pietrobolcato/action-read-yaml@1.0.0 + id: read_yml + with: + config: ${{ github.workspace }}/.nf-core.yaml + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install nf-core + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + + - name: Run nf-core pipelines lint + if: ${{ github.base_ref != 'master' }} + env: + GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_COMMIT: ${{ github.event.pull_request.head.sha }} + run: nf-core -l lint_log.txt pipelines lint --dir ${GITHUB_WORKSPACE} --markdown lint_results.md - - name: Run nf-core lint + - name: Run nf-core pipelines lint --release + if: ${{ github.base_ref == 'master' }} env: GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PR_COMMIT: ${{ github.event.pull_request.head.sha }} - run: nf-core -l lint_log.txt lint --dir ${GITHUB_WORKSPACE} --markdown lint_results.md + run: nf-core -l lint_log.txt pipelines lint --release --dir ${GITHUB_WORKSPACE} --markdown lint_results.md - name: Save PR number if: ${{ always() }} diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml index 40acc23f..42e519bf 100644 --- a/.github/workflows/linting_comment.yml +++ b/.github/workflows/linting_comment.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download lint results - uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3 + uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 with: workflow: linting.yml workflow_conclusion: completed diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 03ecfcf7..c6ba35df 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -12,7 +12,7 @@ jobs: - name: get topics and convert to hashtags id: get_topics run: | - echo "topics=$(curl -s https://nf-co.re/pipelines.json | jq -r '.remote_workflows[] | select(.full_name == "${{ github.repository }}") | .topics[]' | awk '{print "#"$0}' | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "topics=$(curl -s https://nf-co.re/pipelines.json | jq -r '.remote_workflows[] | select(.full_name == "${{ github.repository }}") | .topics[]' | awk '{print "#"$0}' | tr '\n' ' ')" | sed 's/-//g' >> $GITHUB_OUTPUT - uses: rzr/fediverse-action@master with: diff --git a/.github/workflows/template_version_comment.yml b/.github/workflows/template_version_comment.yml new file mode 100644 index 00000000..9dea41f0 --- /dev/null +++ b/.github/workflows/template_version_comment.yml @@ -0,0 +1,43 @@ +name: nf-core template version comment +# This workflow is triggered on PRs to check if the pipeline template version matches the latest nf-core version. +# It posts a comment to the PR, even if it comes from a fork. + +on: pull_request_target + +jobs: + template_version: + runs-on: ubuntu-latest + steps: + - name: Check out pipeline code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - name: Read template version from .nf-core.yml + uses: pietrobolcato/action-read-yaml@1.0.0 + id: read_yml + with: + config: ${{ github.workspace }}/.nf-core.yml + + - name: Install nf-core + run: | + python -m pip install --upgrade pip + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + + - name: Check nf-core outdated + id: nf_core_outdated + run: pip list --outdated | grep nf-core + + - name: Post nf-core template version comment + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 + if: | + ${{ steps.nf_core_outdated.outputs.stdout }} =~ 'nf-core' + with: + repo-token: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} + allow-repeats: false + message: | + ## :warning: Newer version of the nf-core template is available. + + Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. + Please update your pipeline to the latest version. + + For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). + # diff --git a/.gitpod.yml b/.gitpod.yml index 105a1821..46118637 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -4,17 +4,14 @@ tasks: command: | pre-commit install --install-hooks nextflow self-update - - name: unset JAVA_TOOL_OPTIONS - command: | - unset JAVA_TOOL_OPTIONS vscode: extensions: # based on nf-core.nf-core-extensionpack - - esbenp.prettier-vscode # Markdown/CommonMark linting and style checking for Visual Studio Code + #- esbenp.prettier-vscode # Markdown/CommonMark linting and style checking for Visual Studio Code - EditorConfig.EditorConfig # override user/workspace settings with settings found in .editorconfig files - Gruntfuggly.todo-tree # Display TODO and FIXME in a tree view in the activity bar - mechatroner.rainbow-csv # Highlight columns in csv files in different colors - # - nextflow.nextflow # Nextflow syntax highlighting + - nextflow.nextflow # Nextflow syntax highlighting - oderwat.indent-rainbow # Highlight indentation level - streetsidesoftware.code-spell-checker # Spelling checker for source code - charliermarsh.ruff # Code linter Ruff diff --git a/.nf-core.yml b/.nf-core.yml index e0b85a77..d8d61e07 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,2 +1,22 @@ +bump_version: null +lint: + files_exist: + - lib/Utils.groovy + files_unchanged: + - .github/ISSUE_TEMPLATE/bug_report.yml + schema_params: false + template_strings: false +nf_core_version: 3.0.0 +org_path: null repository_type: pipeline -nf_core_version: "2.14.1" +template: + author: Bailey PJ, Botvinnik O, Marques de Almeida F, Peltzer A, Sturm G + description: Pipeline for processing 10x Genomics single cell rnaseq data + force: false + is_nfcore: true + name: scrnaseq + org: nf-core + outdir: . + skip_features: null + version: 2.8.0dev +update: null diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4dc0f1dc..9e9f0e1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - prettier@3.2.5 - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: "2.7.3" + rev: "3.0.3" hooks: - id: editorconfig-checker alias: ec diff --git a/.prettierignore b/.prettierignore index 437d763d..610e5069 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ + email_template.html adaptivecard.json slackreport.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca79396..f8117c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ 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). -## v2.6.0 - [date] +## v2.8.0dev - [date] Initial release of nf-core/scrnaseq, created with the [nf-core](https://nf-co.re/) template. diff --git a/CITATIONS.md b/CITATIONS.md index bd70fa4e..1fd8ce72 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -12,11 +12,11 @@ - [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) - > Andrews, S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. +> Andrews, S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. - [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) - > Ewels P, Magnusson M, Lundin S, Käller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. +> Ewels P, Magnusson M, Lundin S, Käller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. ## Software packaging/containerisation tools diff --git a/README.md b/README.md index f069633b..5ec36240 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![GitHub Actions Linting Status](https://github.com/nf-core/scrnaseq/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/scrnaseq/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/scrnaseq/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/nextflow%20DSL2-%E2%89%A523.04.0-23aa62.svg)](https://www.nextflow.io/) +[![Nextflow](https://img.shields.io/badge/nextflow%20DSL2-%E2%89%A524.04.2-23aa62.svg)](https://www.nextflow.io/) [![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/) @@ -67,8 +67,7 @@ nextflow run nf-core/scrnaseq \ ``` > [!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/usage/configuration#custom-configuration-files). +> 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](https://nf-co.re/scrnaseq/usage) and the [parameter documentation](https://nf-co.re/scrnaseq/parameters). diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 2b66f5b2..584bae69 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -1,9 +1,7 @@ report_comment: > - - This report has been generated by the nf-core/scrnaseq + This report has been generated by the nf-core/scrnaseq analysis pipeline. For information about how to interpret these results, please see the - documentation. - + documentation. report_section_order: "nf-core-scrnaseq-methods-description": order: -1000 diff --git a/assets/schema_input.json b/assets/schema_input.json index dab0450a..8313fe42 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/scrnaseq/master/assets/schema_input.json", "title": "nf-core/scrnaseq pipeline - params.input schema", "description": "Schema for the file provided with params.input", diff --git a/conf/base.config b/conf/base.config index 7013df7d..a5341ecc 100644 --- a/conf/base.config +++ b/conf/base.config @@ -11,9 +11,9 @@ process { // TODO nf-core: Check the defaults for all processes - cpus = { check_max( 1 * task.attempt, 'cpus' ) } - memory = { check_max( 6.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } + cpus = { 1 * task.attempt } + memory = { 6.GB * task.attempt } + time = { 4.h * task.attempt } errorStrategy = { task.exitStatus in ((130..145) + 104) ? 'retry' : 'finish' } maxRetries = 1 @@ -27,30 +27,30 @@ process { // TODO nf-core: Customise requirements for specific processes. // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors withLabel:process_single { - cpus = { check_max( 1 , 'cpus' ) } - memory = { check_max( 6.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } + cpus = { 1 } + memory = { 6.GB * task.attempt } + time = { 4.h * task.attempt } } withLabel:process_low { - cpus = { check_max( 2 * task.attempt, 'cpus' ) } - memory = { check_max( 12.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } + cpus = { 2 * task.attempt } + memory = { 12.GB * task.attempt } + time = { 4.h * task.attempt } } withLabel:process_medium { - cpus = { check_max( 6 * task.attempt, 'cpus' ) } - memory = { check_max( 36.GB * task.attempt, 'memory' ) } - time = { check_max( 8.h * task.attempt, 'time' ) } + cpus = { 6 * task.attempt } + memory = { 36.GB * task.attempt } + time = { 8.h * task.attempt } } withLabel:process_high { - cpus = { check_max( 12 * task.attempt, 'cpus' ) } - memory = { check_max( 72.GB * task.attempt, 'memory' ) } - time = { check_max( 16.h * task.attempt, 'time' ) } + cpus = { 12 * task.attempt } + memory = { 72.GB * task.attempt } + time = { 16.h * task.attempt } } withLabel:process_long { - time = { check_max( 20.h * task.attempt, 'time' ) } + time = { 20.h * task.attempt } } withLabel:process_high_memory { - memory = { check_max( 200.GB * task.attempt, 'memory' ) } + memory = { 200.GB * task.attempt } } withLabel:error_ignore { errorStrategy = 'ignore' diff --git a/conf/igenomes_ignored.config b/conf/igenomes_ignored.config new file mode 100644 index 00000000..b4034d82 --- /dev/null +++ b/conf/igenomes_ignored.config @@ -0,0 +1,9 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for iGenomes paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Empty genomes dictionary to use when igenomes is ignored. +---------------------------------------------------------------------------------------- +*/ + +params.genomes = [:] diff --git a/conf/modules.config b/conf/modules.config index d203d2b6..d266a387 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -21,7 +21,6 @@ process { withName: FASTQC { ext.args = '--quiet' } - withName: 'MULTIQC' { ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' } publishDir = [ diff --git a/conf/test.config b/conf/test.config index edb53f78..52e2151e 100644 --- a/conf/test.config +++ b/conf/test.config @@ -10,15 +10,18 @@ ---------------------------------------------------------------------------------------- */ +process { + resourceLimits = [ + cpus: 4, + memory: '15.GB', + time: '1.h' + ] +} + params { config_profile_name = 'Test profile' config_profile_description = 'Minimal test dataset to check pipeline function' - // Limit resources so that this can run on GitHub Actions - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' - // Input data // 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 diff --git a/docs/images/mqc_fastqc_adapter.png b/docs/images/mqc_fastqc_adapter.png deleted file mode 100755 index 361d0e47acfb424dea1f326590d1eb2f6dfa26b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23458 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 diff --git a/docs/images/mqc_fastqc_quality.png b/docs/images/mqc_fastqc_quality.png deleted file mode 100755 index a4b89bf56ab2ba88cab87841916eb680a816deae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55769 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�)v~C(0C)c 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 M)78&qol`;+0Fu7JivR!s diff --git a/docs/output.md b/docs/output.md index 22a1a79f..c5a4bb83 100644 --- a/docs/output.md +++ b/docs/output.md @@ -14,6 +14,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [FastQC](#fastqc) - Raw read QC - [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline + - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution ### FastQC @@ -29,16 +30,6 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d [FastQC](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/) gives general quality metrics about your sequenced reads. It provides information about the quality score distribution across your reads, per base sequence content (%A/T/G/C), adapter contamination and overrepresented sequences. For further reading and documentation see the [FastQC help pages](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/). -![MultiQC - FastQC sequence counts plot](images/mqc_fastqc_counts.png) - -![MultiQC - FastQC mean quality scores plot](images/mqc_fastqc_quality.png) - -![MultiQC - FastQC adapter content plot](images/mqc_fastqc_adapter.png) - -:::note -The FastQC plots displayed in the MultiQC report shows _untrimmed_ reads. They may contain adapter sequence and potentially regions with low quality. -::: - ### MultiQC
diff --git a/docs/usage.md b/docs/usage.md index dbc1aa81..f904b0b5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -85,9 +85,9 @@ The above pipeline run specified with a params file in yaml format: nextflow run nf-core/scrnaseq -profile docker -params-file params.yaml ``` -with `params.yaml` containing: +with: -```yaml +```yaml title="params.yaml" input: './samplesheet.csv' outdir: './results/' genome: 'GRCh37' @@ -199,14 +199,6 @@ See the main [Nextflow documentation](https://www.nextflow.io/docs/latest/config If you have any questions or issues please send us a message on [Slack](https://nf-co.re/join/slack) on the [`#configs` channel](https://nfcore.slack.com/channels/configs). -## Azure Resource Requests - -To be used with the `azurebatch` profile by specifying the `-profile azurebatch`. -We recommend providing a compute `params.vm_type` of `Standard_D16_v3` VMs by default but these options can be changed if required. - -Note that the choice of VM size depends on your quota and the overall workload during the analysis. -For a thorough list, please refer the [Azure Sizes for virtual machines in Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/sizes). - ## Running in the background Nextflow handles job submissions and supervises the running jobs. The Nextflow process must run until the pipeline is finished. diff --git a/main.nf b/main.nf index fe1b3bad..a20c1ec6 100644 --- a/main.nf +++ b/main.nf @@ -9,8 +9,6 @@ ---------------------------------------------------------------------------------------- */ -nextflow.enable.dsl = 2 - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS / WORKFLOWS @@ -20,7 +18,6 @@ nextflow.enable.dsl = 2 include { SCRNASEQ } from './workflows/scrnaseq' include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_scrnaseq_pipeline' include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_scrnaseq_pipeline' - include { getGenomeAttribute } from './subworkflows/local/utils_nfcore_scrnaseq_pipeline' /* @@ -56,10 +53,8 @@ workflow NFCORE_SCRNASEQ { SCRNASEQ ( samplesheet ) - emit: multiqc_report = SCRNASEQ.out.multiqc_report // channel: /path/to/multiqc_report.html - } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -70,27 +65,24 @@ workflow NFCORE_SCRNASEQ { workflow { main: - // // SUBWORKFLOW: Run initialisation tasks // PIPELINE_INITIALISATION ( params.version, - params.help, params.validate_params, params.monochrome_logs, args, params.outdir, params.input ) - + // // WORKFLOW: Run main workflow // NFCORE_SCRNASEQ ( PIPELINE_INITIALISATION.out.samplesheet ) - // // SUBWORKFLOW: Run completion tasks // diff --git a/modules.json b/modules.json index 198bbc97..0c633d31 100644 --- a/modules.json +++ b/modules.json @@ -7,12 +7,12 @@ "nf-core": { "fastqc": { "branch": "master", - "git_sha": "285a50500f9e02578d90b3ce6382ea3c30216acd", + "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", "installed_by": ["modules"] }, "multiqc": { "branch": "master", - "git_sha": "b7ebe95761cd389603f9cc0e0dc384c0f663815a", + "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", "installed_by": ["modules"] } } @@ -21,17 +21,17 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "git_sha": "d20fb2a9cc3e2835e9d067d1046a63252eb17352", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "92de218a329bfc9a9033116eb5f65fd270e72ba3", + "git_sha": "2fdce49d30c0254f76bc0f13c55c17455c1251ab", "installed_by": ["subworkflows"] }, - "utils_nfvalidation_plugin": { + "utils_nfschema_plugin": { "branch": "master", - "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "git_sha": "bbd5a41f4535a8defafe6080e00ea74c45f4f96c", "installed_by": ["subworkflows"] } } diff --git a/modules/nf-core/fastqc/environment.yml b/modules/nf-core/fastqc/environment.yml index 1787b38a..691d4c76 100644 --- a/modules/nf-core/fastqc/environment.yml +++ b/modules/nf-core/fastqc/environment.yml @@ -1,7 +1,5 @@ -name: fastqc channels: - conda-forge - bioconda - - defaults dependencies: - bioconda::fastqc=0.12.1 diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf index d79f1c86..d8989f48 100644 --- a/modules/nf-core/fastqc/main.nf +++ b/modules/nf-core/fastqc/main.nf @@ -26,7 +26,10 @@ process FASTQC { def rename_to = old_new_pairs*.join(' ').join(' ') def renamed_files = old_new_pairs.collect{ old_name, new_name -> new_name }.join(' ') - def memory_in_mb = MemoryUnit.of("${task.memory}").toUnit('MB') + // The total amount of allocated RAM by FastQC is equal to the number of threads defined (--threads) time the amount of RAM defined (--memory) + // https://github.com/s-andrews/FastQC/blob/1faeea0412093224d7f6a07f777fad60a5650795/fastqc#L211-L222 + // Dividing the task.memory by task.cpu allows to stick to requested amount of RAM in the label + def memory_in_mb = MemoryUnit.of("${task.memory}").toUnit('MB') / task.cpus // FastQC memory value allowed range (100 - 10000) def fastqc_memory = memory_in_mb > 10000 ? 10000 : (memory_in_mb < 100 ? 100 : memory_in_mb) diff --git a/modules/nf-core/fastqc/meta.yml b/modules/nf-core/fastqc/meta.yml index ee5507e0..4827da7a 100644 --- a/modules/nf-core/fastqc/meta.yml +++ b/modules/nf-core/fastqc/meta.yml @@ -16,35 +16,44 @@ tools: homepage: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/ documentation: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/ licence: ["GPL-2.0-only"] + identifier: biotools:fastqc input: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test', single_end:false ] - - reads: - type: file - description: | - List of input FastQ files of size 1 and 2 for single-end and paired-end data, - respectively. + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - reads: + type: file + description: | + List of input FastQ files of size 1 and 2 for single-end and paired-end data, + respectively. output: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test', single_end:false ] - html: - type: file - description: FastQC report - pattern: "*_{fastqc.html}" + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.html": + type: file + description: FastQC report + pattern: "*_{fastqc.html}" - zip: - type: file - description: FastQC report archive - pattern: "*_{fastqc.zip}" + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.zip": + type: file + description: FastQC report archive + pattern: "*_{fastqc.zip}" - versions: - type: file - description: File containing software versions - pattern: "versions.yml" + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" authors: - "@drpatelh" - "@grst" diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index 70edae4d..e9d79a07 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -23,17 +23,14 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - // NOTE The report contains the date inside it, which means that the md5sum is stable per day, but not longer than that. So you can't md5sum it. - // looks like this:
Mon 2 Oct 2023
test.gz
- // https://github.com/nf-core/modules/pull/3903#issuecomment-1743620039 - - { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, - { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, - { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_single") } + { assert process.success }, + // NOTE The report contains the date inside it, which means that the md5sum is stable per day, but not longer than that. So you can't md5sum it. + // looks like this:
Mon 2 Oct 2023
test.gz
+ // https://github.com/nf-core/modules/pull/3903#issuecomment-1743620039 + { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, + { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, + { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } @@ -54,16 +51,14 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, - { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, - { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, - { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, - { assert path(process.out.html[0][1][0]).text.contains("File typeConventional base calls") }, - { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_paired") } + { assert process.success }, + { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, + { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, + { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, + { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, + { assert path(process.out.html[0][1][0]).text.contains("File typeConventional base calls") }, + { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } @@ -83,13 +78,11 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, - { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, - { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_interleaved") } + { assert process.success }, + { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, + { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, + { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } @@ -109,13 +102,11 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, - { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, - { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_bam") } + { assert process.success }, + { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, + { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, + { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } @@ -138,22 +129,20 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, - { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, - { assert process.out.html[0][1][2] ==~ ".*/test_3_fastqc.html" }, - { assert process.out.html[0][1][3] ==~ ".*/test_4_fastqc.html" }, - { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, - { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, - { assert process.out.zip[0][1][2] ==~ ".*/test_3_fastqc.zip" }, - { assert process.out.zip[0][1][3] ==~ ".*/test_4_fastqc.zip" }, - { assert path(process.out.html[0][1][0]).text.contains("File typeConventional base calls") }, - { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, - { assert path(process.out.html[0][1][2]).text.contains("File typeConventional base calls") }, - { assert path(process.out.html[0][1][3]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_multiple") } + { assert process.success }, + { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, + { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, + { assert process.out.html[0][1][2] ==~ ".*/test_3_fastqc.html" }, + { assert process.out.html[0][1][3] ==~ ".*/test_4_fastqc.html" }, + { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, + { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, + { assert process.out.zip[0][1][2] ==~ ".*/test_3_fastqc.zip" }, + { assert process.out.zip[0][1][3] ==~ ".*/test_4_fastqc.zip" }, + { assert path(process.out.html[0][1][0]).text.contains("File typeConventional base calls") }, + { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, + { assert path(process.out.html[0][1][2]).text.contains("File typeConventional base calls") }, + { assert path(process.out.html[0][1][3]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } @@ -173,21 +162,18 @@ nextflow_process { then { assertAll ( - { assert process.success }, - - { assert process.out.html[0][1] ==~ ".*/mysample_fastqc.html" }, - { assert process.out.zip[0][1] ==~ ".*/mysample_fastqc.zip" }, - { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - - { assert snapshot(process.out.versions).match("fastqc_versions_custom_prefix") } + { assert process.success }, + { assert process.out.html[0][1] ==~ ".*/mysample_fastqc.html" }, + { assert process.out.zip[0][1] ==~ ".*/mysample_fastqc.zip" }, + { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match() } ) } } test("sarscov2 single-end [fastq] - stub") { - options "-stub" - + options "-stub" when { process { """ @@ -201,12 +187,123 @@ nextflow_process { then { assertAll ( - { assert process.success }, - { assert snapshot(process.out.html.collect { file(it[1]).getName() } + - process.out.zip.collect { file(it[1]).getName() } + - process.out.versions ).match("fastqc_stub") } + { assert process.success }, + { assert snapshot(process.out).match() } ) } } + test("sarscov2 paired-end [fastq] - stub") { + + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: 'test', single_end: false], // meta map + [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) ] + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("sarscov2 interleaved [fastq] - stub") { + + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: 'test', single_end: false], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_interleaved.fastq.gz', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("sarscov2 paired-end [bam] - stub") { + + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [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() } + ) + } + } + + test("sarscov2 multiple [fastq] - stub") { + + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: 'test', single_end: false], // meta map + [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_2.fastq.gz', checkIfExists: true) ] + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("sarscov2 custom_prefix - stub") { + + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [ id:'mysample', single_end:true ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } } diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap index 86f7c311..d5db3092 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -1,88 +1,392 @@ { - "fastqc_versions_interleaved": { + "sarscov2 custom_prefix": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:40:07.293713" + "timestamp": "2024-07-22T11:02:16.374038" }, - "fastqc_stub": { + "sarscov2 single-end [fastq] - stub": { "content": [ - [ - "test.html", - "test.zip", - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "0": [ + [ + { + "id": "test", + "single_end": true + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": true + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "test", + "single_end": true + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "test", + "single_end": true + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-07-22T11:02:24.993809" + }, + "sarscov2 custom_prefix - stub": { + "content": [ + { + "0": [ + [ + { + "id": "mysample", + "single_end": true + }, + "mysample.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "mysample", + "single_end": true + }, + "mysample.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "mysample", + "single_end": true + }, + "mysample.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "mysample", + "single_end": true + }, + "mysample.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:31:01.425198" + "timestamp": "2024-07-22T11:03:10.93942" }, - "fastqc_versions_multiple": { + "sarscov2 interleaved [fastq]": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:40:55.797907" + "timestamp": "2024-07-22T11:01:42.355718" }, - "fastqc_versions_bam": { + "sarscov2 paired-end [bam]": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:40:26.795862" + "timestamp": "2024-07-22T11:01:53.276274" }, - "fastqc_versions_single": { + "sarscov2 multiple [fastq]": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:39:27.043675" + "timestamp": "2024-07-22T11:02:05.527626" }, - "fastqc_versions_paired": { + "sarscov2 paired-end [fastq]": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-07-22T11:01:31.188871" + }, + "sarscov2 paired-end [fastq] - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-07-22T11:02:34.273566" + }, + "sarscov2 multiple [fastq] - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:39:47.584191" + "timestamp": "2024-07-22T11:03:02.304411" }, - "fastqc_versions_custom_prefix": { + "sarscov2 single-end [fastq]": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-07-22T11:01:19.095607" + }, + "sarscov2 interleaved [fastq] - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" + }, + "timestamp": "2024-07-22T11:02:44.640184" + }, + "sarscov2 paired-end [bam] - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "html": [ + [ + { + "id": "test", + "single_end": false + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ], + "zip": [ + [ + { + "id": "test", + "single_end": false + }, + "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.3" }, - "timestamp": "2024-01-31T17:41:14.576531" + "timestamp": "2024-07-22T11:02:53.550742" } } \ No newline at end of file diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index ca39fb67..f1cd99b0 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -1,7 +1,5 @@ -name: multiqc channels: - conda-forge - bioconda - - defaults dependencies: - - bioconda::multiqc=1.21 + - bioconda::multiqc=1.24.1 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 47ac352f..b9ccebdb 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,14 +3,16 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.21--pyhdfd78af_0' : - 'biocontainers/multiqc:1.21--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.25--pyhdfd78af_0' : + 'biocontainers/multiqc:1.25--pyhdfd78af_0' }" input: path multiqc_files, stageAs: "?/*" path(multiqc_config) path(extra_multiqc_config) path(multiqc_logo) + path(replace_names) + path(sample_names) output: path "*multiqc_report.html", emit: report @@ -23,16 +25,22 @@ process MULTIQC { script: def args = task.ext.args ?: '' + def prefix = task.ext.prefix ? "--filename ${task.ext.prefix}.html" : '' def config = multiqc_config ? "--config $multiqc_config" : '' def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' - def logo = multiqc_logo ? /--cl-config 'custom_logo: "${multiqc_logo}"'/ : '' + def logo = multiqc_logo ? "--cl-config 'custom_logo: \"${multiqc_logo}\"'" : '' + def replace = replace_names ? "--replace-names ${replace_names}" : '' + def samples = sample_names ? "--sample-names ${sample_names}" : '' """ multiqc \\ --force \\ $args \\ $config \\ + $prefix \\ $extra_config \\ $logo \\ + $replace \\ + $samples \\ . cat <<-END_VERSIONS > versions.yml diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index 45a9bc35..b16c1879 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -1,5 +1,6 @@ name: multiqc -description: Aggregate results from bioinformatics analyses across many samples into a single report +description: Aggregate results from bioinformatics analyses across many samples into + a single report keywords: - QC - bioinformatics tools @@ -12,40 +13,59 @@ tools: homepage: https://multiqc.info/ documentation: https://multiqc.info/docs/ licence: ["GPL-3.0-or-later"] + identifier: biotools:multiqc input: - - multiqc_files: - type: file - description: | - List of reports / files recognised by MultiQC, for example the html and zip output of FastQC - - multiqc_config: - type: file - description: Optional config yml for MultiQC - pattern: "*.{yml,yaml}" - - extra_multiqc_config: - type: file - description: Second optional config yml for MultiQC. Will override common sections in multiqc_config. - pattern: "*.{yml,yaml}" - - multiqc_logo: - type: file - description: Optional logo file for MultiQC - pattern: "*.{png}" + - - multiqc_files: + type: file + description: | + List of reports / files recognised by MultiQC, for example the html and zip output of FastQC + - - multiqc_config: + type: file + description: Optional config yml for MultiQC + pattern: "*.{yml,yaml}" + - - extra_multiqc_config: + type: file + description: Second optional config yml for MultiQC. Will override common sections + in multiqc_config. + pattern: "*.{yml,yaml}" + - - multiqc_logo: + type: file + description: Optional logo file for MultiQC + pattern: "*.{png}" + - - replace_names: + type: file + description: | + Optional two-column sample renaming file. First column a set of + patterns, second column a set of corresponding replacements. Passed via + MultiQC's `--replace-names` option. + pattern: "*.{tsv}" + - - sample_names: + type: file + description: | + Optional TSV file with headers, passed to the MultiQC --sample_names + argument. + pattern: "*.{tsv}" output: - report: - type: file - description: MultiQC report file - pattern: "multiqc_report.html" + - "*multiqc_report.html": + type: file + description: MultiQC report file + pattern: "multiqc_report.html" - data: - type: directory - description: MultiQC data dir - pattern: "multiqc_data" + - "*_data": + type: directory + description: MultiQC data dir + pattern: "multiqc_data" - plots: - type: file - description: Plots created by MultiQC - pattern: "*_data" + - "*_plots": + type: file + description: Plots created by MultiQC + pattern: "*_data" - versions: - type: file - description: File containing software versions - pattern: "versions.yml" + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" authors: - "@abhi18av" - "@bunop" diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test index f1c4242e..33316a7d 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -8,6 +8,8 @@ nextflow_process { tag "modules_nfcore" tag "multiqc" + config "./nextflow.config" + test("sarscov2 single-end [fastqc]") { when { @@ -17,6 +19,8 @@ nextflow_process { input[1] = [] input[2] = [] input[3] = [] + input[4] = [] + input[5] = [] """ } } @@ -41,6 +45,8 @@ nextflow_process { input[1] = Channel.of(file("https://github.com/nf-core/tools/raw/dev/nf_core/pipeline-template/assets/multiqc_config.yml", checkIfExists: true)) input[2] = [] input[3] = [] + input[4] = [] + input[5] = [] """ } } @@ -66,6 +72,8 @@ nextflow_process { input[1] = [] input[2] = [] input[3] = [] + input[4] = [] + input[5] = [] """ } } diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index bfebd802..b779e469 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" + "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" ] ], "meta": { "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nextflow": "24.04.2" }, - "timestamp": "2024-02-29T08:48:55.657331" + "timestamp": "2024-07-10T12:41:34.562023" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" + "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" ] ], "meta": { "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nextflow": "24.04.2" }, - "timestamp": "2024-02-29T08:49:49.071937" + "timestamp": "2024-07-10T11:27:11.933869532" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" + "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" ] ], "meta": { "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nextflow": "24.04.2" }, - "timestamp": "2024-02-29T08:49:25.457567" + "timestamp": "2024-07-10T11:26:56.709849369" } -} \ No newline at end of file +} diff --git a/modules/nf-core/multiqc/tests/nextflow.config b/modules/nf-core/multiqc/tests/nextflow.config new file mode 100644 index 00000000..c537a6a3 --- /dev/null +++ b/modules/nf-core/multiqc/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: 'MULTIQC' { + ext.prefix = null + } +} diff --git a/nextflow.config b/nextflow.config index 703872c5..ff6d17ef 100644 --- a/nextflow.config +++ b/nextflow.config @@ -16,7 +16,6 @@ params { genome = null igenomes_base = 's3://ngi-igenomes/igenomes/' igenomes_ignore = false - // MultiQC options multiqc_config = null multiqc_title = null @@ -33,48 +32,26 @@ params { monochrome_logs = false hook_url = null help = false + help_full = false + show_hidden = false version = false pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' - // Config options config_profile_name = null config_profile_description = null + custom_config_version = 'master' custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" config_profile_contact = null config_profile_url = null - - // Max resource options - // Defaults only, expecting to be overwritten - max_memory = '128.GB' - max_cpus = 16 - max_time = '240.h' - // Schema validation default options - validationFailUnrecognisedParams = false - validationLenientMode = false - validationSchemaIgnoreParams = 'genomes,igenomes_base' - validationShowHiddenParams = false - validate_params = true - + validate_params = true + } // Load base.config by default for all pipelines includeConfig 'conf/base.config' -// Load nf-core custom profiles from different Institutions -try { - includeConfig "${params.custom_config_base}/nfcore_custom.config" -} catch (Exception e) { - System.err.println("WARNING: Could not load nf-core/config profiles: ${params.custom_config_base}/nfcore_custom.config") -} - -// Load nf-core/scrnaseq custom profiles from different institutions. -try { - includeConfig "${params.custom_config_base}/pipeline/scrnaseq.config" -} catch (Exception e) { - System.err.println("WARNING: Could not load nf-core/config/scrnaseq profiles: ${params.custom_config_base}/pipeline/scrnaseq.config") -} profiles { debug { dumpHashes = true @@ -89,7 +66,7 @@ profiles { podman.enabled = false shifter.enabled = false charliecloud.enabled = false - conda.channels = ['conda-forge', 'bioconda', 'defaults'] + conda.channels = ['conda-forge', 'bioconda'] apptainer.enabled = false } mamba { @@ -178,25 +155,23 @@ profiles { test_full { includeConfig 'conf/test_full.config' } } -// Set default registry for Apptainer, Docker, Podman and Singularity independent of -profile -// Will not be used unless Apptainer / Docker / Podman / Singularity are enabled -// Set to your registry if you have a mirror of containers -apptainer.registry = 'quay.io' -docker.registry = 'quay.io' -podman.registry = 'quay.io' -singularity.registry = 'quay.io' +// Load nf-core custom profiles from different Institutions +includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/nfcore_custom.config" : "/dev/null" -// Nextflow plugins -plugins { - id 'nf-validation@1.1.3' // Validation of pipeline parameters and creation of an input channel from a sample sheet -} +// Load nf-core/scrnaseq 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 !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/pipeline/scrnaseq.config" : "/dev/null" +// Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile +// Will not be used unless Apptainer / Docker / Podman / Charliecloud / Singularity are enabled +// Set to your registry if you have a mirror of containers +apptainer.registry = 'quay.io' +docker.registry = 'quay.io' +podman.registry = 'quay.io' +singularity.registry = 'quay.io' +charliecloud.registry = 'quay.io' // Load igenomes.config if required -if (!params.igenomes_ignore) { - includeConfig 'conf/igenomes.config' -} else { - params.genomes = [:] -} +includeConfig !params.igenomes_ignore ? 'conf/igenomes.config' : 'conf/igenomes_ignored.config' // Export these variables to prevent local Python/R libraries from conflicting with those in the container // The JULIA depot path has been adjusted to a fixed path `/usr/local/share/julia` that needs to be used for packages in the container. // See https://apeltzer.github.io/post/03-julia-lang-nextflow/ for details on that. Once we have a common agreement on where to keep Julia packages, this is adjustable. @@ -208,8 +183,15 @@ env { JULIA_DEPOT_PATH = "/usr/local/share/julia" } -// Capture exit codes from upstream processes when piping -process.shell = ['/bin/bash', '-euo', 'pipefail'] +// Set bash options +process.shell = """\ +bash + +set -e # Exit if a tool returns a non-zero status/exit code +set -u # Treat unset variables and parameters as an error +set -o pipefail # Returns the status of the last command to exit with a non-zero status or zero if all successfully execute +set -C # No clobber - prevent output redirection from overwriting files. +""" // Disable process selector warnings by default. Use debug profile to enable warnings. nextflow.enable.configProcessNamesValidation = false @@ -238,43 +220,47 @@ manifest { homePage = 'https://github.com/nf-core/scrnaseq' description = """Pipeline for processing 10x Genomics single cell rnaseq data""" mainScript = 'main.nf' - nextflowVersion = '!>=23.04.0' - version = '2.6.0' + nextflowVersion = '!>=24.04.2' + version = '2.8.0dev' doi = '' } -// Load modules.config for DSL2 module specific options -includeConfig 'conf/modules.config' +// Nextflow plugins +plugins { + id 'nf-schema@2.1.1' // Validation of pipeline parameters and creation of an input channel from a sample sheet +} + +validation { + defaultIgnoreParams = ["genomes"] + help { + enabled = true + command = "nextflow run $manifest.name -profile --input samplesheet.csv --outdir " + fullParameter = "help_full" + showHiddenParameter = "show_hidden" + beforeText = """ +-\033[2m----------------------------------------------------\033[0m- + \033[0;32m,--.\033[0;30m/\033[0;32m,-.\033[0m +\033[0;34m ___ __ __ __ ___ \033[0;32m/,-._.--~\'\033[0m +\033[0;34m |\\ | |__ __ / ` / \\ |__) |__ \033[0;33m} {\033[0m +\033[0;34m | \\| | \\__, \\__/ | \\ |___ \033[0;32m\\`-._,-`-,\033[0m + \033[0;32m`._,._,\'\033[0m +\033[0;35m ${manifest.name} ${manifest.version}\033[0m +-\033[2m----------------------------------------------------\033[0m- +""" + afterText = """${manifest.doi ? "* The pipeline\n" : ""}${manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${manifest.doi ? "\n" : ""} +* The nf-core framework + https://doi.org/10.1038/s41587-020-0439-x -// Function to ensure that resource requirements don't go beyond -// a maximum limit -def check_max(obj, type) { - if (type == 'memory') { - try { - if (obj.compareTo(params.max_memory as nextflow.util.MemoryUnit) == 1) - return params.max_memory as nextflow.util.MemoryUnit - else - return obj - } catch (all) { - println " ### ERROR ### Max memory '${params.max_memory}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'time') { - try { - if (obj.compareTo(params.max_time as nextflow.util.Duration) == 1) - return params.max_time as nextflow.util.Duration - else - return obj - } catch (all) { - println " ### ERROR ### Max time '${params.max_time}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'cpus') { - try { - return Math.min( obj, params.max_cpus as int ) - } catch (all) { - println " ### ERROR ### Max cpus '${params.max_cpus}' is not valid! Using default value: $obj" - return obj - } +* Software dependencies + https://github.com/${manifest.name}/blob/master/CITATIONS.md +""" + } + summary { + beforeText = validation.help.beforeText + afterText = validation.help.afterText } } + +// Load modules.config for DSL2 module specific options +includeConfig 'conf/modules.config' + diff --git a/nextflow_schema.json b/nextflow_schema.json index 3a722aba..bce091dc 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,10 +1,10 @@ { - "$schema": "http://json-schema.org/draft-07/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/scrnaseq/master/nextflow_schema.json", "title": "nf-core/scrnaseq pipeline parameters", "description": "Pipeline for processing 10x Genomics single cell rnaseq data", "type": "object", - "definitions": { + "$defs": { "input_output_options": { "title": "Input/output options", "type": "object", @@ -71,6 +71,14 @@ "fa_icon": "fas fa-ban", "hidden": true, "help_text": "Do not load `igenomes.config` when running the pipeline. You may choose this option if you observe clashes between custom parameters and those supplied in `igenomes.config`." + }, + "igenomes_base": { + "type": "string", + "format": "directory-path", + "description": "The base path to the igenomes reference files", + "fa_icon": "fas fa-ban", + "hidden": true, + "default": "s3://ngi-igenomes/igenomes/" } } }, @@ -122,41 +130,6 @@ } } }, - "max_job_request_options": { - "title": "Max job request options", - "type": "object", - "fa_icon": "fab fa-acquisitions-incorporated", - "description": "Set the top limit for requested resources for any single job.", - "help_text": "If you are running on a smaller system, a pipeline step requesting more resources than are available may cause the Nextflow to stop the run with an error. These options allow you to cap the maximum resources requested by any single job so that the pipeline will run on your system.\n\nNote that you can not _increase_ the resources requested by any job using these options. For that you will need your own configuration file. See [the nf-core website](https://nf-co.re/usage/configuration) for details.", - "properties": { - "max_cpus": { - "type": "integer", - "description": "Maximum number of CPUs that can be requested for any single job.", - "default": 16, - "fa_icon": "fas fa-microchip", - "hidden": true, - "help_text": "Use to set an upper-limit for the CPU requirement for each process. Should be an integer e.g. `--max_cpus 1`" - }, - "max_memory": { - "type": "string", - "description": "Maximum amount of memory that can be requested for any single job.", - "default": "128.GB", - "fa_icon": "fas fa-memory", - "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", - "hidden": true, - "help_text": "Use to set an upper-limit for the memory requirement for each process. Should be a string in the format integer-unit e.g. `--max_memory '8.GB'`" - }, - "max_time": { - "type": "string", - "description": "Maximum amount of time that can be requested for any single job.", - "default": "240.h", - "fa_icon": "far fa-clock", - "pattern": "^(\\d+\\.?\\s*(s|m|h|d|day)\\s*)+$", - "hidden": true, - "help_text": "Use to set an upper-limit for the time requirement for each process. Should be a string in the format integer-unit e.g. `--max_time '2.h'`" - } - } - }, "generic_options": { "title": "Generic options", "type": "object", @@ -164,12 +137,6 @@ "description": "Less common options for the pipeline, typically set in a config file.", "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", "properties": { - "help": { - "type": "boolean", - "description": "Display help text.", - "fa_icon": "fas fa-question-circle", - "hidden": true - }, "version": { "type": "boolean", "description": "Display version and exit.", @@ -245,27 +212,6 @@ "fa_icon": "fas fa-check-square", "hidden": true }, - "validationShowHiddenParams": { - "type": "boolean", - "fa_icon": "far fa-eye-slash", - "description": "Show all params when using `--help`", - "hidden": true, - "help_text": "By default, parameters set as _hidden_ in the schema are not shown on the command line when a user runs with `--help`. Specifying this option will tell the pipeline to show all parameters." - }, - "validationFailUnrecognisedParams": { - "type": "boolean", - "fa_icon": "far fa-check-circle", - "description": "Validation of parameters fails when an unrecognised parameter is found.", - "hidden": true, - "help_text": "By default, when an unrecognised parameter is found, it returns a warinig." - }, - "validationLenientMode": { - "type": "boolean", - "fa_icon": "far fa-check-circle", - "description": "Validation of parameters in lenient more.", - "hidden": true, - "help_text": "Allows string values that are parseable as numbers or booleans. For further information see [JSONSchema docs](https://github.com/everit-org/json-schema#lenient-mode)." - }, "pipelines_testdata_base_path": { "type": "string", "fa_icon": "far fa-check-circle", @@ -278,19 +224,16 @@ }, "allOf": [ { - "$ref": "#/definitions/input_output_options" - }, - { - "$ref": "#/definitions/reference_genome_options" + "$ref": "#/$defs/input_output_options" }, { - "$ref": "#/definitions/institutional_config_options" + "$ref": "#/$defs/reference_genome_options" }, { - "$ref": "#/definitions/max_job_request_options" + "$ref": "#/$defs/institutional_config_options" }, { - "$ref": "#/definitions/generic_options" + "$ref": "#/$defs/generic_options" } ] } diff --git a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf index 5c50fc86..1894cc1e 100644 --- a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf @@ -8,17 +8,14 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { UTILS_NFVALIDATION_PLUGIN } from '../../nf-core/utils_nfvalidation_plugin' -include { paramsSummaryMap } from 'plugin/nf-validation' -include { fromSamplesheet } from 'plugin/nf-validation' -include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' +include { UTILS_NFSCHEMA_PLUGIN } from '../../nf-core/utils_nfschema_plugin' +include { paramsSummaryMap } from 'plugin/nf-schema' +include { samplesheetToList } from 'plugin/nf-schema' include { completionEmail } from '../../nf-core/utils_nfcore_pipeline' include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' -include { dashedLine } from '../../nf-core/utils_nfcore_pipeline' -include { nfCoreLogo } from '../../nf-core/utils_nfcore_pipeline' include { imNotification } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' -include { workflowCitation } from '../../nf-core/utils_nfcore_pipeline' +include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' /* ======================================================================================== @@ -30,7 +27,6 @@ workflow PIPELINE_INITIALISATION { take: version // boolean: Display version and exit - help // boolean: Display help text validate_params // boolean: Boolean whether to validate parameters against the schema at runtime monochrome_logs // boolean: Do not use coloured log outputs nextflow_cli_args // array: List of positional nextflow CLI args @@ -51,20 +47,16 @@ workflow PIPELINE_INITIALISATION { workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 ) + // // Validate parameters and generate parameter summary to stdout // - pre_help_text = nfCoreLogo(monochrome_logs) - post_help_text = '\n' + workflowCitation() + '\n' + dashedLine(monochrome_logs) - def String workflow_command = "nextflow run ${workflow.manifest.name} -profile --input samplesheet.csv --outdir " - UTILS_NFVALIDATION_PLUGIN ( - help, - workflow_command, - pre_help_text, - post_help_text, + UTILS_NFSCHEMA_PLUGIN ( + workflow, validate_params, - "nextflow_schema.json" + null ) + // // Check config provided to the pipeline @@ -80,8 +72,9 @@ workflow PIPELINE_INITIALISATION { // // Create channel from input file provided through params.input // + Channel - .fromSamplesheet("input") + .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) .map { meta, fastq_1, fastq_2 -> if (!fastq_2) { @@ -91,8 +84,8 @@ workflow PIPELINE_INITIALISATION { } } .groupTuple() - .map { - validateInputSamplesheet(it) + .map { samplesheet -> + validateInputSamplesheet(samplesheet) } .map { meta, fastqs -> @@ -117,13 +110,13 @@ workflow PIPELINE_COMPLETION { email // string: email address email_on_fail // string: email address sent on pipeline failure plaintext_email // boolean: Send plain-text email instead of HTML + outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output hook_url // string: hook URL for notifications multiqc_report // string: Path to MultiQC report main: - summary_params = paramsSummaryMap(workflow, parameters_schema: "nextflow_schema.json") // @@ -131,11 +124,18 @@ workflow PIPELINE_COMPLETION { // workflow.onComplete { if (email || email_on_fail) { - completionEmail(summary_params, email, email_on_fail, plaintext_email, outdir, monochrome_logs, multiqc_report.toList()) + completionEmail( + summary_params, + email, + email_on_fail, + plaintext_email, + outdir, + monochrome_logs, + multiqc_report.toList() + ) } completionSummary(monochrome_logs) - if (hook_url) { imNotification(summary_params, hook_url) } @@ -165,7 +165,7 @@ def validateInputSamplesheet(input) { def (metas, fastqs) = input[1..2] // Check that multiple runs of the same sample are of the same datatype i.e. single-end / paired-end - def endedness_ok = metas.collect{ it.single_end }.unique().size == 1 + def endedness_ok = metas.collect{ meta -> meta.single_end }.unique().size == 1 if (!endedness_ok) { error("Please check input samplesheet -> Multiple runs of a sample must be of the same datatype i.e. single-end or paired-end: ${metas[0].id}") } @@ -197,7 +197,6 @@ def genomeExistsError() { error(error_string) } } - // // Generate methods description for MultiQC // @@ -239,8 +238,10 @@ def methodsDescriptionText(mqc_methods_yaml) { // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers // Removing ` ` since the manifest.doi is a string and not a proper list def temp_doi_ref = "" - String[] manifest_doi = meta.manifest_map.doi.tokenize(",") - for (String doi_ref: manifest_doi) temp_doi_ref += "(doi:
${doi_ref.replace("https://doi.org/", "").replace(" ", "")}), " + def manifest_doi = meta.manifest_map.doi.tokenize(",") + manifest_doi.each { doi_ref -> + temp_doi_ref += "(doi: ${doi_ref.replace("https://doi.org/", "").replace(" ", "")}), " + } meta["doi_text"] = temp_doi_ref.substring(0, temp_doi_ref.length() - 2) } else meta["doi_text"] = "" 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.
  • " @@ -261,3 +262,4 @@ def methodsDescriptionText(mqc_methods_yaml) { return description_html.toString() } + diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index ac31f28f..28e32b20 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -2,10 +2,6 @@ // Subworkflow with functionality that may be useful for any Nextflow pipeline // -import org.yaml.snakeyaml.Yaml -import groovy.json.JsonOutput -import nextflow.extension.FilesEx - /* ======================================================================================== SUBWORKFLOW DEFINITION @@ -58,7 +54,7 @@ workflow UTILS_NEXTFLOW_PIPELINE { // Generate version string // def getWorkflowVersion() { - String version_string = "" + def version_string = "" as String if (workflow.manifest.version) { def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' version_string += "${prefix_v}${workflow.manifest.version}" @@ -79,10 +75,10 @@ def dumpParametersToJSON(outdir) { def timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') def filename = "params_${timestamp}.json" def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") - def jsonStr = JsonOutput.toJson(params) - temp_pf.text = JsonOutput.prettyPrint(jsonStr) + def jsonStr = groovy.json.JsonOutput.toJson(params) + temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) - FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") + nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") temp_pf.delete() } @@ -90,7 +86,7 @@ def dumpParametersToJSON(outdir) { // When running with -profile conda, warn if channels have not been set-up appropriately // def checkCondaChannels() { - Yaml parser = new Yaml() + def parser = new org.yaml.snakeyaml.Yaml() def channels = [] try { def config = parser.load("conda config --show channels".execute().text) @@ -102,14 +98,16 @@ def checkCondaChannels() { // Check that all channels are present // This channel list is ordered by required channel priority. - def required_channels_in_order = ['conda-forge', 'bioconda', 'defaults'] + def required_channels_in_order = ['conda-forge', 'bioconda'] def channels_missing = ((required_channels_in_order as Set) - (channels as Set)) as Boolean // Check that they are in the right order def channel_priority_violation = false - def n = required_channels_in_order.size() - for (int i = 0; i < n - 1; i++) { - channel_priority_violation |= !(channels.indexOf(required_channels_in_order[i]) < channels.indexOf(required_channels_in_order[i+1])) + + required_channels_in_order.eachWithIndex { channel, index -> + if (index < required_channels_in_order.size() - 1) { + channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index+1])) + } } if (channels_missing | channel_priority_violation) { diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config index d0a926bf..a09572e5 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config @@ -3,7 +3,7 @@ manifest { author = """nf-core""" homePage = 'https://127.0.0.1' description = """Dummy pipeline""" - nextflowVersion = '!>=23.04.0' + nextflowVersion = '!>=23.04.0' version = '9.9.9' doi = 'https://doi.org/10.5281/zenodo.5070524' } diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index 14558c39..cbd8495b 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -2,9 +2,6 @@ // Subworkflow with utility functions specific to the nf-core pipeline template // -import org.yaml.snakeyaml.Yaml -import nextflow.extension.FilesEx - /* ======================================================================================== SUBWORKFLOW DEFINITION @@ -34,7 +31,7 @@ workflow UTILS_NFCORE_PIPELINE { // Warn if a -profile or Nextflow config has not been provided to run the pipeline // def checkConfigProvided() { - valid_config = true + def valid_config = true as Boolean if (workflow.profile == 'standard' && workflow.configFiles.size() <= 1) { log.warn "[$workflow.manifest.name] You are attempting to run the pipeline without any custom configuration!\n\n" + "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + @@ -66,11 +63,13 @@ def checkProfileProvided(nextflow_cli_args) { // def workflowCitation() { def temp_doi_ref = "" - String[] manifest_doi = workflow.manifest.doi.tokenize(",") + def manifest_doi = workflow.manifest.doi.tokenize(",") // Using a loop to handle multiple DOIs // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers // Removing ` ` since the manifest.doi is a string and not a proper list - for (String doi_ref: manifest_doi) temp_doi_ref += " https://doi.org/${doi_ref.replace('https://doi.org/', '').replace(' ', '')}\n" + manifest_doi.each { doi_ref -> + temp_doi_ref += " https://doi.org/${doi_ref.replace('https://doi.org/', '').replace(' ', '')}\n" + } return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + "* The pipeline\n" + temp_doi_ref + "\n" + @@ -84,7 +83,7 @@ def workflowCitation() { // Generate workflow version string // def getWorkflowVersion() { - String version_string = "" + def version_string = "" as String if (workflow.manifest.version) { def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' version_string += "${prefix_v}${workflow.manifest.version}" @@ -102,8 +101,8 @@ def getWorkflowVersion() { // Get software versions for pipeline // def processVersionsFromYAML(yaml_file) { - Yaml yaml = new Yaml() - versions = yaml.load(yaml_file).collectEntries { k, v -> [ k.tokenize(':')[-1], v ] } + def yaml = new org.yaml.snakeyaml.Yaml() + def versions = yaml.load(yaml_file).collectEntries { k, v -> [ k.tokenize(':')[-1], v ] } return yaml.dumpAsMap(versions).trim() } @@ -124,7 +123,7 @@ def workflowVersionToYAML() { def softwareVersionsToYAML(ch_versions) { return ch_versions .unique() - .map { processVersionsFromYAML(it) } + .map { version -> processVersionsFromYAML(version) } .unique() .mix(Channel.of(workflowVersionToYAML())) } @@ -134,19 +133,19 @@ def softwareVersionsToYAML(ch_versions) { // def paramsSummaryMultiqc(summary_params) { def summary_section = '' - for (group in summary_params.keySet()) { + summary_params.keySet().each { group -> def group_params = summary_params.get(group) // This gets the parameters of that particular group if (group_params) { summary_section += "

    $group

    \n" summary_section += "
    \n" - for (param in group_params.keySet()) { + group_params.keySet().sort().each { param -> summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" } summary_section += "
    \n" } } - String yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" + def yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" as String yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" @@ -161,7 +160,7 @@ def paramsSummaryMultiqc(summary_params) { // nf-core logo // def nfCoreLogo(monochrome_logs=true) { - Map colors = logColours(monochrome_logs) + def colors = logColours(monochrome_logs) as Map String.format( """\n ${dashedLine(monochrome_logs)} @@ -180,7 +179,7 @@ def nfCoreLogo(monochrome_logs=true) { // Return dashed line // def dashedLine(monochrome_logs=true) { - Map colors = logColours(monochrome_logs) + def colors = logColours(monochrome_logs) as Map return "-${colors.dim}----------------------------------------------------${colors.reset}-" } @@ -188,7 +187,7 @@ def dashedLine(monochrome_logs=true) { // ANSII colours used for terminal logging // def logColours(monochrome_logs=true) { - Map colorcodes = [:] + def colorcodes = [:] as Map // Reset / Meta colorcodes['reset'] = monochrome_logs ? '' : "\033[0m" @@ -287,7 +286,7 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi } def summary = [:] - for (group in summary_params.keySet()) { + summary_params.keySet().sort().each { group -> summary << summary_params[group] } @@ -344,10 +343,10 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi def sendmail_html = sendmail_template.toString() // Send the HTML e-mail - Map colors = logColours(monochrome_logs) + def colors = logColours(monochrome_logs) as Map if (email_address) { try { - if (plaintext_email) { throw GroovyException('Send plaintext e-mail, not HTML') } + if (plaintext_email) { throw new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } // Try to send HTML e-mail using sendmail def sendmail_tf = new File(workflow.launchDir.toString(), ".sendmail_tmp.html") sendmail_tf.withWriter { w -> w << sendmail_html } @@ -364,13 +363,13 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi // Write summary e-mail HTML to a file def output_hf = new File(workflow.launchDir.toString(), ".pipeline_report.html") output_hf.withWriter { w -> w << email_html } - FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html"); + nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html"); output_hf.delete() // Write summary e-mail TXT to a file def output_tf = new File(workflow.launchDir.toString(), ".pipeline_report.txt") output_tf.withWriter { w -> w << email_txt } - FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt"); + nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt"); output_tf.delete() } @@ -378,7 +377,7 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi // Print pipeline summary on completion // def completionSummary(monochrome_logs=true) { - Map colors = logColours(monochrome_logs) + def colors = logColours(monochrome_logs) as Map if (workflow.success) { if (workflow.stats.ignoredCount == 0) { log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Pipeline completed successfully${colors.reset}-" @@ -395,7 +394,7 @@ def completionSummary(monochrome_logs=true) { // def imNotification(summary_params, hook_url) { def summary = [:] - for (group in summary_params.keySet()) { + summary_params.keySet().sort().each { group -> summary << summary_params[group] } diff --git a/subworkflows/nf-core/utils_nfschema_plugin/main.nf b/subworkflows/nf-core/utils_nfschema_plugin/main.nf new file mode 100644 index 00000000..4994303e --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/main.nf @@ -0,0 +1,46 @@ +// +// Subworkflow that uses the nf-schema plugin to validate parameters and render the parameter summary +// + +include { paramsSummaryLog } from 'plugin/nf-schema' +include { validateParameters } from 'plugin/nf-schema' + +workflow UTILS_NFSCHEMA_PLUGIN { + + take: + input_workflow // workflow: the workflow object used by nf-schema to get metadata from the workflow + validate_params // boolean: validate the parameters + parameters_schema // string: path to the parameters JSON schema. + // this has to be the same as the schema given to `validation.parametersSchema` + // when this input is empty it will automatically use the configured schema or + // "${projectDir}/nextflow_schema.json" as default. This input should not be empty + // for meta pipelines + + main: + + // + // Print parameter summary to stdout. This will display the parameters + // that differ from the default given in the JSON schema + // + if(parameters_schema) { + log.info paramsSummaryLog(input_workflow, parameters_schema:parameters_schema) + } else { + log.info paramsSummaryLog(input_workflow) + } + + // + // Validate the parameters using nextflow_schema.json or the schema + // given via the validation.parametersSchema configuration option + // + if(validate_params) { + if(parameters_schema) { + validateParameters(parameters_schema:parameters_schema) + } else { + validateParameters() + } + } + + emit: + dummy_emit = true +} + diff --git a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml new file mode 100644 index 00000000..f7d9f028 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "utils_nfschema_plugin" +description: Run nf-schema to validate parameters and create a summary of changed parameters +keywords: + - validation + - JSON schema + - plugin + - parameters + - summary +components: [] +input: + - input_workflow: + type: object + description: | + The workflow object of the used pipeline. + This object contains meta data used to create the params summary log + - validate_params: + type: boolean + description: Validate the parameters and error if invalid. + - parameters_schema: + type: string + description: | + Path to the parameters JSON schema. + This has to be the same as the schema given to the `validation.parametersSchema` config + option. When this input is empty it will automatically use the configured schema or + "${projectDir}/nextflow_schema.json" as default. The schema should not be given in this way + for meta pipelines. +output: + - dummy_emit: + type: boolean + description: Dummy emit to make nf-core subworkflows lint happy +authors: + - "@nvnieuwk" +maintainers: + - "@nvnieuwk" diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test new file mode 100644 index 00000000..842dc432 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test @@ -0,0 +1,117 @@ +nextflow_workflow { + + name "Test Subworkflow UTILS_NFSCHEMA_PLUGIN" + script "../main.nf" + workflow "UTILS_NFSCHEMA_PLUGIN" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/utils_nfschema_plugin" + tag "plugin/nf-schema" + + config "./nextflow.config" + + test("Should run nothing") { + + when { + + params { + test_data = '' + } + + workflow { + """ + validate_params = false + input[0] = workflow + input[1] = validate_params + input[2] = "" + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should validate params") { + + when { + + params { + test_data = '' + outdir = 1 + } + + workflow { + """ + validate_params = true + input[0] = workflow + input[1] = validate_params + input[2] = "" + """ + } + } + + then { + assertAll( + { assert workflow.failed }, + { assert workflow.stdout.any { it.contains('ERROR ~ Validation of pipeline parameters failed!') } } + ) + } + } + + test("Should run nothing - custom schema") { + + when { + + params { + test_data = '' + } + + workflow { + """ + validate_params = false + input[0] = workflow + input[1] = validate_params + input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should validate params - custom schema") { + + when { + + params { + test_data = '' + outdir = 1 + } + + workflow { + """ + validate_params = true + input[0] = workflow + input[1] = validate_params + input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + """ + } + } + + then { + assertAll( + { assert workflow.failed }, + { assert workflow.stdout.any { it.contains('ERROR ~ Validation of pipeline parameters failed!') } } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config new file mode 100644 index 00000000..0907ac58 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config @@ -0,0 +1,8 @@ +plugins { + id "nf-schema@2.1.0" +} + +validation { + parametersSchema = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + monochromeLogs = true +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json similarity index 95% rename from subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json rename to subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json index 7626c1c9..331e0d2f 100644 --- a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json @@ -1,10 +1,10 @@ { - "$schema": "http://json-schema.org/draft-07/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/./master/nextflow_schema.json", "title": ". pipeline parameters", "description": "", "type": "object", - "definitions": { + "$defs": { "input_output_options": { "title": "Input/output options", "type": "object", @@ -87,10 +87,10 @@ }, "allOf": [ { - "$ref": "#/definitions/input_output_options" + "$ref": "#/$defs/input_output_options" }, { - "$ref": "#/definitions/generic_options" + "$ref": "#/$defs/generic_options" } ] } diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf b/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf deleted file mode 100644 index 2585b65d..00000000 --- a/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf +++ /dev/null @@ -1,62 +0,0 @@ -// -// Subworkflow that uses the nf-validation plugin to render help text and parameter summary -// - -/* -======================================================================================== - IMPORT NF-VALIDATION PLUGIN -======================================================================================== -*/ - -include { paramsHelp } from 'plugin/nf-validation' -include { paramsSummaryLog } from 'plugin/nf-validation' -include { validateParameters } from 'plugin/nf-validation' - -/* -======================================================================================== - SUBWORKFLOW DEFINITION -======================================================================================== -*/ - -workflow UTILS_NFVALIDATION_PLUGIN { - - take: - print_help // boolean: print help - workflow_command // string: default commmand used to run pipeline - pre_help_text // string: string to be printed before help text and summary log - post_help_text // string: string to be printed after help text and summary log - validate_params // boolean: validate parameters - schema_filename // path: JSON schema file, null to use default value - - main: - - log.debug "Using schema file: ${schema_filename}" - - // Default values for strings - pre_help_text = pre_help_text ?: '' - post_help_text = post_help_text ?: '' - workflow_command = workflow_command ?: '' - - // - // Print help message if needed - // - if (print_help) { - log.info pre_help_text + paramsHelp(workflow_command, parameters_schema: schema_filename) + post_help_text - System.exit(0) - } - - // - // Print parameter summary to stdout - // - log.info pre_help_text + paramsSummaryLog(workflow, parameters_schema: schema_filename) + post_help_text - - // - // Validate parameters relative to the parameter JSON schema - // - if (validate_params){ - validateParameters(parameters_schema: schema_filename) - } - - emit: - dummy_emit = true -} diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml b/subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml deleted file mode 100644 index 3d4a6b04..00000000 --- a/subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml +++ /dev/null @@ -1,44 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json -name: "UTILS_NFVALIDATION_PLUGIN" -description: Use nf-validation to initiate and validate a pipeline -keywords: - - utility - - pipeline - - initialise - - validation -components: [] -input: - - print_help: - type: boolean - description: | - Print help message and exit - - workflow_command: - type: string - description: | - The command to run the workflow e.g. "nextflow run main.nf" - - pre_help_text: - type: string - description: | - Text to print before the help message - - post_help_text: - type: string - description: | - Text to print after the help message - - validate_params: - type: boolean - description: | - Validate the parameters and error if invalid. - - schema_filename: - type: string - description: | - The filename of the schema to validate against. -output: - - dummy_emit: - type: boolean - description: | - Dummy emit to make nf-core subworkflows lint happy -authors: - - "@adamrtalbot" -maintainers: - - "@adamrtalbot" - - "@maxulysse" diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test deleted file mode 100644 index 5784a33f..00000000 --- a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test +++ /dev/null @@ -1,200 +0,0 @@ -nextflow_workflow { - - name "Test Workflow UTILS_NFVALIDATION_PLUGIN" - script "../main.nf" - workflow "UTILS_NFVALIDATION_PLUGIN" - tag "subworkflows" - tag "subworkflows_nfcore" - tag "plugin/nf-validation" - tag "'plugin/nf-validation'" - tag "utils_nfvalidation_plugin" - tag "subworkflows/utils_nfvalidation_plugin" - - test("Should run nothing") { - - when { - - params { - monochrome_logs = true - test_data = '' - } - - workflow { - """ - help = false - workflow_command = null - pre_help_text = null - post_help_text = null - validate_params = false - schema_filename = "$moduleTestDir/nextflow_schema.json" - - input[0] = help - input[1] = workflow_command - input[2] = pre_help_text - input[3] = post_help_text - input[4] = validate_params - input[5] = schema_filename - """ - } - } - - then { - assertAll( - { assert workflow.success } - ) - } - } - - test("Should run help") { - - - when { - - params { - monochrome_logs = true - test_data = '' - } - workflow { - """ - help = true - workflow_command = null - pre_help_text = null - post_help_text = null - validate_params = false - schema_filename = "$moduleTestDir/nextflow_schema.json" - - input[0] = help - input[1] = workflow_command - input[2] = pre_help_text - input[3] = post_help_text - input[4] = validate_params - input[5] = schema_filename - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.exitStatus == 0 }, - { assert workflow.stdout.any { it.contains('Input/output options') } }, - { assert workflow.stdout.any { it.contains('--outdir') } } - ) - } - } - - test("Should run help with command") { - - when { - - params { - monochrome_logs = true - test_data = '' - } - workflow { - """ - help = true - workflow_command = "nextflow run noorg/doesntexist" - pre_help_text = null - post_help_text = null - validate_params = false - schema_filename = "$moduleTestDir/nextflow_schema.json" - - input[0] = help - input[1] = workflow_command - input[2] = pre_help_text - input[3] = post_help_text - input[4] = validate_params - input[5] = schema_filename - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.exitStatus == 0 }, - { assert workflow.stdout.any { it.contains('nextflow run noorg/doesntexist') } }, - { assert workflow.stdout.any { it.contains('Input/output options') } }, - { assert workflow.stdout.any { it.contains('--outdir') } } - ) - } - } - - test("Should run help with extra text") { - - - when { - - params { - monochrome_logs = true - test_data = '' - } - workflow { - """ - help = true - workflow_command = "nextflow run noorg/doesntexist" - pre_help_text = "pre-help-text" - post_help_text = "post-help-text" - validate_params = false - schema_filename = "$moduleTestDir/nextflow_schema.json" - - input[0] = help - input[1] = workflow_command - input[2] = pre_help_text - input[3] = post_help_text - input[4] = validate_params - input[5] = schema_filename - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.exitStatus == 0 }, - { assert workflow.stdout.any { it.contains('pre-help-text') } }, - { assert workflow.stdout.any { it.contains('nextflow run noorg/doesntexist') } }, - { assert workflow.stdout.any { it.contains('Input/output options') } }, - { assert workflow.stdout.any { it.contains('--outdir') } }, - { assert workflow.stdout.any { it.contains('post-help-text') } } - ) - } - } - - test("Should validate params") { - - when { - - params { - monochrome_logs = true - test_data = '' - outdir = 1 - } - workflow { - """ - help = false - workflow_command = null - pre_help_text = null - post_help_text = null - validate_params = true - schema_filename = "$moduleTestDir/nextflow_schema.json" - - input[0] = help - input[1] = workflow_command - input[2] = pre_help_text - input[3] = post_help_text - input[4] = validate_params - input[5] = schema_filename - """ - } - } - - then { - assertAll( - { assert workflow.failed }, - { assert workflow.stdout.any { it.contains('ERROR ~ ERROR: Validation of pipeline parameters failed!') } } - ) - } - } -} diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml deleted file mode 100644 index 60b1cfff..00000000 --- a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/utils_nfvalidation_plugin: - - subworkflows/nf-core/utils_nfvalidation_plugin/** diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index b70276b2..d0d0d5d6 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -3,10 +3,9 @@ IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - include { FASTQC } from '../modules/nf-core/fastqc/main' include { MULTIQC } from '../modules/nf-core/multiqc/main' -include { paramsSummaryMap } from 'plugin/nf-validation' +include { paramsSummaryMap } from 'plugin/nf-schema' include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scrnaseq_pipeline' @@ -21,12 +20,10 @@ workflow SCRNASEQ { take: ch_samplesheet // channel: samplesheet read in from --input - main: ch_versions = Channel.empty() ch_multiqc_files = Channel.empty() - // // MODULE: Run FastQC // @@ -42,11 +39,12 @@ workflow SCRNASEQ { softwareVersionsToYAML(ch_versions) .collectFile( storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_pipeline_software_mqc_versions.yml', + name: 'nf_core_' + 'pipeline_software_' + 'mqc_' + 'versions.yml', sort: true, newLine: true ).set { ch_collated_versions } + // // MODULE: MultiQC // @@ -59,18 +57,19 @@ workflow SCRNASEQ { Channel.fromPath(params.multiqc_logo, checkIfExists: true) : Channel.empty() + summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) - + ch_multiqc_files = ch_multiqc_files.mix( + ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml')) + ch_multiqc_custom_methods_description = params.multiqc_methods_description ? file(params.multiqc_methods_description, checkIfExists: true) : file("$projectDir/assets/methods_description_template.yml", checkIfExists: true) ch_methods_description = Channel.value( methodsDescriptionText(ch_multiqc_custom_methods_description)) - ch_multiqc_files = ch_multiqc_files.mix( - ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml')) ch_multiqc_files = ch_multiqc_files.mix(ch_collated_versions) ch_multiqc_files = ch_multiqc_files.mix( ch_methods_description.collectFile( @@ -83,12 +82,14 @@ workflow SCRNASEQ { ch_multiqc_files.collect(), ch_multiqc_config.toList(), ch_multiqc_custom_config.toList(), - ch_multiqc_logo.toList() + ch_multiqc_logo.toList(), + [], + [] ) - emit: - multiqc_report = MULTIQC.out.report.toList() // channel: /path/to/multiqc_report.html + emit:multiqc_report = MULTIQC.out.report.toList() // channel: /path/to/multiqc_report.html versions = ch_versions // channel: [ path(versions.yml) ] + } /* From 3958cc260531000bddaf4d5da88231c0fd9e668b Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Wed, 9 Oct 2024 11:07:42 +0000 Subject: [PATCH 050/147] Template update for nf-core/tools version 3.0.1 --- .editorconfig | 4 - .github/CONTRIBUTING.md | 2 +- .github/workflows/awsfulltest.yml | 6 +- .github/workflows/linting.yml | 4 +- .nf-core.yml | 2 +- .prettierignore | 1 - docs/output.md | 1 - modules.json | 6 +- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 4 +- .../nf-core/multiqc/tests/main.nf.test.snap | 26 +- nextflow.config | 8 +- .../utils_nfcore_scrnaseq_pipeline/main.nf | 12 +- .../nf-core/utils_nextflow_pipeline/main.nf | 46 ++- .../nf-core/utils_nfcore_pipeline/main.nf | 279 ++++++++++-------- 15 files changed, 209 insertions(+), 194 deletions(-) diff --git a/.editorconfig b/.editorconfig index e1058815..72dda289 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ indent_style = space [*.{md,yml,yaml,html,css,scss,js}] indent_size = 2 - # These files are edited and tested upstream in nf-core/modules [/modules/nf-core/**] charset = unset @@ -26,12 +25,9 @@ insert_final_newline = unset trim_trailing_whitespace = unset indent_style = unset - - [/assets/email*] indent_size = unset - # ignore python and markdown [*.{py,md}] indent_style = unset diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ba739ae1..9b48f356 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -90,7 +90,7 @@ Once there, use `nf-core pipelines schema build` to add to `nextflow_schema.json ### Default processes resource requirements -Sensible defaults for process resource requirements (CPUs / memory / time) for a process should be defined in `conf/base.config`. These should generally be specified generic with `withLabel:` selectors so they can be shared across multiple processes/steps of the pipeline. A nf-core standard set of labels that should be followed where possible can be seen in the [nf-core pipeline template](https://github.com/nf-core/tools/blob/master/nf_core/pipeline-template/conf/base.config), which has the default process as a single core-process, and then different levels of multi-core configurations for increasingly large memory requirements defined with standardised labels. +Sensible defaults for process resource requirements (CPUs / memory / time) for a process should be defined in `conf/base.config`. These should generally be specified generic with `withLabel:` selectors so they can be shared across multiple processes/steps of the pipeline. A nf-core standard set of labels that should be followed where possible can be seen in the [nf-core pipeline template](https://github.com/nf-core/tools/blob/main/nf_core/pipeline-template/conf/base.config), which has the default process as a single core-process, and then different levels of multi-core configurations for increasingly large memory requirements defined with standardised labels. The process resources can be passed on to the tool dynamically within the process with the `${task.cpus}` and `${task.memory}` variables in the `script:` block. diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index 886e3814..48bec286 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -14,16 +14,18 @@ on: jobs: run-platform: name: Run AWS full tests - if: github.repository == 'nf-core/scrnaseq' && github.event.review.state == 'approved' + # run only if the PR is approved by at least 2 reviewers and against the master branch or manually triggered + if: github.repository == 'nf-core/scrnaseq' && github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'master' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - uses: octokit/request-action@v2.x id: check_approvals with: - route: GET /repos/${{ github.repository }}/pulls/${{ github.event.review.number }}/reviews + route: GET /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - id: test_variables + if: github.event_name != 'workflow_dispatch' run: | JSON_RESPONSE='${{ steps.check_approvals.outputs.data }}' CURRENT_APPROVALS_COUNT=$(echo $JSON_RESPONSE | jq -c '[.[] | select(.state | contains("APPROVED")) ] | length') diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b882838a..a502573c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -42,10 +42,10 @@ jobs: architecture: "x64" - name: read .nf-core.yml - uses: pietrobolcato/action-read-yaml@1.0.0 + uses: pietrobolcato/action-read-yaml@1.1.0 id: read_yml with: - config: ${{ github.workspace }}/.nf-core.yaml + config: ${{ github.workspace }}/.nf-core.yml - name: Install dependencies run: | diff --git a/.nf-core.yml b/.nf-core.yml index d8d61e07..684de437 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -6,7 +6,7 @@ lint: - .github/ISSUE_TEMPLATE/bug_report.yml schema_params: false template_strings: false -nf_core_version: 3.0.0 +nf_core_version: 3.0.1 org_path: null repository_type: pipeline template: diff --git a/.prettierignore b/.prettierignore index 610e5069..437d763d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,3 @@ - email_template.html adaptivecard.json slackreport.json diff --git a/docs/output.md b/docs/output.md index c5a4bb83..cba487c3 100644 --- a/docs/output.md +++ b/docs/output.md @@ -14,7 +14,6 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [FastQC](#fastqc) - Raw read QC - [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline - - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution ### FastQC diff --git a/modules.json b/modules.json index 0c633d31..cc391d5e 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", + "git_sha": "b8d36829fa84b6e404364abff787e8b07f6d058c", "installed_by": ["modules"] } } @@ -21,12 +21,12 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "d20fb2a9cc3e2835e9d067d1046a63252eb17352", + "git_sha": "9d05360da397692321d377b6102d2fb22507c6ef", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "2fdce49d30c0254f76bc0f13c55c17455c1251ab", + "git_sha": "772684d9d66f37b650c8ba5146ac1ee3ecba2acb", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index f1cd99b0..6f5b867b 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -2,4 +2,4 @@ channels: - conda-forge - bioconda dependencies: - - bioconda::multiqc=1.24.1 + - bioconda::multiqc=1.25.1 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index b9ccebdb..9724d2f3 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,8 +3,8 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.25--pyhdfd78af_0' : - 'biocontainers/multiqc:1.25--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.25.1--pyhdfd78af_0' : + 'biocontainers/multiqc:1.25.1--pyhdfd78af_0' }" input: path multiqc_files, stageAs: "?/*" diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index b779e469..2fcbb5ff 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T12:41:34.562023" + "timestamp": "2024-10-02T17:51:46.317523" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T11:27:11.933869532" + "timestamp": "2024-10-02T17:52:20.680978" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,8c8724363a5efe0c6f43ab34faa57efd" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T11:26:56.709849369" + "timestamp": "2024-10-02T17:52:09.185842" } -} +} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index ff6d17ef..7ed2e084 100644 --- a/nextflow.config +++ b/nextflow.config @@ -12,10 +12,12 @@ params { // TODO nf-core: Specify your pipeline's command line flags // Input options input = null + // References genome = null igenomes_base = 's3://ngi-igenomes/igenomes/' igenomes_ignore = false + // MultiQC options multiqc_config = null multiqc_title = null @@ -36,6 +38,7 @@ params { show_hidden = false version = false pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' + // Config options config_profile_name = null config_profile_description = null @@ -44,9 +47,9 @@ params { custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" config_profile_contact = null config_profile_url = null + // Schema validation default options validate_params = true - } // Load base.config by default for all pipelines @@ -161,6 +164,7 @@ includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${pa // Load nf-core/scrnaseq 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 !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/pipeline/scrnaseq.config" : "/dev/null" + // Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile // Will not be used unless Apptainer / Docker / Podman / Charliecloud / Singularity are enabled // Set to your registry if you have a mirror of containers @@ -172,6 +176,7 @@ charliecloud.registry = 'quay.io' // Load igenomes.config if required includeConfig !params.igenomes_ignore ? 'conf/igenomes.config' : 'conf/igenomes_ignored.config' + // Export these variables to prevent local Python/R libraries from conflicting with those in the container // The JULIA depot path has been adjusted to a fixed path `/usr/local/share/julia` that needs to be used for packages in the container. // See https://apeltzer.github.io/post/03-julia-lang-nextflow/ for details on that. Once we have a common agreement on where to keep Julia packages, this is adjustable. @@ -263,4 +268,3 @@ validation { // Load modules.config for DSL2 module specific options includeConfig 'conf/modules.config' - diff --git a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf index 1894cc1e..975d4034 100644 --- a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf @@ -18,9 +18,9 @@ include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW TO INITIALISE PIPELINE -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow PIPELINE_INITIALISATION { @@ -99,9 +99,9 @@ workflow PIPELINE_INITIALISATION { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW FOR PIPELINE COMPLETION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow PIPELINE_COMPLETION { @@ -147,9 +147,9 @@ workflow PIPELINE_COMPLETION { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // // Check and validate pipeline parameters diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index 28e32b20..2b0dc67a 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -3,13 +3,12 @@ // /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NEXTFLOW_PIPELINE { - take: print_version // boolean: print version dump_parameters // boolean: dump parameters @@ -22,7 +21,7 @@ workflow UTILS_NEXTFLOW_PIPELINE { // Print workflow version and exit on --version // if (print_version) { - log.info "${workflow.manifest.name} ${getWorkflowVersion()}" + log.info("${workflow.manifest.name} ${getWorkflowVersion()}") System.exit(0) } @@ -45,9 +44,9 @@ workflow UTILS_NEXTFLOW_PIPELINE { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -72,11 +71,11 @@ def getWorkflowVersion() { // Dump pipeline parameters to a JSON file // def dumpParametersToJSON(outdir) { - def timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') - def filename = "params_${timestamp}.json" - def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") - def jsonStr = groovy.json.JsonOutput.toJson(params) - temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) + def timestamp = new java.util.Date().format('yyyy-MM-dd_HH-mm-ss') + def filename = "params_${timestamp}.json" + def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") + def jsonStr = groovy.json.JsonOutput.toJson(params) + temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") temp_pf.delete() @@ -91,9 +90,14 @@ def checkCondaChannels() { try { def config = parser.load("conda config --show channels".execute().text) channels = config.channels - } catch(NullPointerException | IOException e) { - log.warn "Could not verify conda channel configuration." - return + } + catch (NullPointerException e) { + log.warn("Could not verify conda channel configuration.") + return null + } + catch (IOException e) { + log.warn("Could not verify conda channel configuration.") + return null } // Check that all channels are present @@ -106,19 +110,13 @@ def checkCondaChannels() { required_channels_in_order.eachWithIndex { channel, index -> if (index < required_channels_in_order.size() - 1) { - channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index+1])) + channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index + 1])) } } if (channels_missing | channel_priority_violation) { - log.warn "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - " There is a problem with your Conda configuration!\n\n" + - " You will need to set-up the conda-forge and bioconda channels correctly.\n" + - " Please refer to https://bioconda.github.io/\n" + - " The observed channel order is \n" + - " ${channels}\n" + - " but the following channel order is required:\n" + - " ${required_channels_in_order}\n" + - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + log.warn( + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + " There is a problem with your Conda configuration!\n\n" + " You will need to set-up the conda-forge and bioconda channels correctly.\n" + " Please refer to https://bioconda.github.io/\n" + " The observed channel order is \n" + " ${channels}\n" + " but the following channel order is required:\n" + " ${required_channels_in_order}\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + ) } } diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index cbd8495b..b78273ca 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -3,13 +3,12 @@ // /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NFCORE_PIPELINE { - take: nextflow_cli_args @@ -22,9 +21,9 @@ workflow UTILS_NFCORE_PIPELINE { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -33,12 +32,9 @@ workflow UTILS_NFCORE_PIPELINE { def checkConfigProvided() { def valid_config = true as Boolean if (workflow.profile == 'standard' && workflow.configFiles.size() <= 1) { - log.warn "[$workflow.manifest.name] You are attempting to run the pipeline without any custom configuration!\n\n" + - "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + - " (1) Using an existing pipeline profile e.g. `-profile docker` or `-profile singularity`\n" + - " (2) Using an existing nf-core/configs for your Institution e.g. `-profile crick` or `-profile uppmax`\n" + - " (3) Using your own local custom config e.g. `-c /path/to/your/custom.config`\n\n" + - "Please refer to the quick start section and usage docs for the pipeline.\n " + log.warn( + "[${workflow.manifest.name}] You are attempting to run the pipeline without any custom configuration!\n\n" + "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + " (1) Using an existing pipeline profile e.g. `-profile docker` or `-profile singularity`\n" + " (2) Using an existing nf-core/configs for your Institution e.g. `-profile crick` or `-profile uppmax`\n" + " (3) Using your own local custom config e.g. `-c /path/to/your/custom.config`\n\n" + "Please refer to the quick start section and usage docs for the pipeline.\n " + ) valid_config = false } return valid_config @@ -49,12 +45,14 @@ def checkConfigProvided() { // def checkProfileProvided(nextflow_cli_args) { if (workflow.profile.endsWith(',')) { - error "The `-profile` option cannot end with a trailing comma, please remove it and re-run the pipeline!\n" + - "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + error( + "The `-profile` option cannot end with a trailing comma, please remove it and re-run the pipeline!\n" + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + ) } if (nextflow_cli_args[0]) { - log.warn "nf-core pipelines do not accept positional arguments. The positional argument `${nextflow_cli_args[0]}` has been detected.\n" + - "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + log.warn( + "nf-core pipelines do not accept positional arguments. The positional argument `${nextflow_cli_args[0]}` has been detected.\n" + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + ) } } @@ -70,13 +68,7 @@ def workflowCitation() { manifest_doi.each { doi_ref -> temp_doi_ref += " https://doi.org/${doi_ref.replace('https://doi.org/', '').replace(' ', '')}\n" } - return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + - "* The pipeline\n" + - temp_doi_ref + "\n" + - "* The nf-core framework\n" + - " https://doi.org/10.1038/s41587-020-0439-x\n\n" + - "* Software dependencies\n" + - " https://github.com/${workflow.manifest.name}/blob/master/CITATIONS.md" + return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + "* The pipeline\n" + temp_doi_ref + "\n" + "* The nf-core framework\n" + " https://doi.org/10.1038/s41587-020-0439-x\n\n" + "* Software dependencies\n" + " https://github.com/${workflow.manifest.name}/blob/master/CITATIONS.md" } // @@ -102,7 +94,7 @@ def getWorkflowVersion() { // def processVersionsFromYAML(yaml_file) { def yaml = new org.yaml.snakeyaml.Yaml() - def versions = yaml.load(yaml_file).collectEntries { k, v -> [ k.tokenize(':')[-1], v ] } + def versions = yaml.load(yaml_file).collectEntries { k, v -> [k.tokenize(':')[-1], v] } return yaml.dumpAsMap(versions).trim() } @@ -112,8 +104,8 @@ def processVersionsFromYAML(yaml_file) { def workflowVersionToYAML() { return """ Workflow: - $workflow.manifest.name: ${getWorkflowVersion()} - Nextflow: $workflow.nextflow.version + ${workflow.manifest.name}: ${getWorkflowVersion()} + Nextflow: ${workflow.nextflow.version} """.stripIndent().trim() } @@ -121,11 +113,7 @@ def workflowVersionToYAML() { // Get channel of software versions used in pipeline in YAML format // def softwareVersionsToYAML(ch_versions) { - return ch_versions - .unique() - .map { version -> processVersionsFromYAML(version) } - .unique() - .mix(Channel.of(workflowVersionToYAML())) + return ch_versions.unique().map { version -> processVersionsFromYAML(version) }.unique().mix(Channel.of(workflowVersionToYAML())) } // @@ -133,25 +121,31 @@ def softwareVersionsToYAML(ch_versions) { // def paramsSummaryMultiqc(summary_params) { def summary_section = '' - summary_params.keySet().each { group -> - def group_params = summary_params.get(group) // This gets the parameters of that particular group - if (group_params) { - summary_section += "

    $group

    \n" - summary_section += "
    \n" - group_params.keySet().sort().each { param -> - summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" + summary_params + .keySet() + .each { group -> + def group_params = summary_params.get(group) + // This gets the parameters of that particular group + if (group_params) { + summary_section += "

    ${group}

    \n" + summary_section += "
    \n" + group_params + .keySet() + .sort() + .each { param -> + summary_section += "
    ${param}
    ${group_params.get(param) ?: 'N/A'}
    \n" + } + summary_section += "
    \n" } - summary_section += "
    \n" } - } - def yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" as String - yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" - yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" - yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" - yaml_file_text += "plot_type: 'html'\n" - yaml_file_text += "data: |\n" - yaml_file_text += "${summary_section}" + def yaml_file_text = "id: '${workflow.manifest.name.replace('/', '-')}-summary'\n" as String + yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" + yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" + yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" + yaml_file_text += "plot_type: 'html'\n" + yaml_file_text += "data: |\n" + yaml_file_text += "${summary_section}" return yaml_file_text } @@ -199,54 +193,54 @@ def logColours(monochrome_logs=true) { colorcodes['hidden'] = monochrome_logs ? '' : "\033[8m" // Regular Colors - colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m" - colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m" - colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m" - colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m" - colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m" - colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m" - colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m" - colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m" + colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m" + colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m" + colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m" + colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m" + colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m" + colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m" + colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m" + colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m" // Bold - colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m" - colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m" - colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m" - colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m" - colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m" - colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m" - colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m" - colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m" + colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m" + colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m" + colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m" + colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m" + colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m" + colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m" + colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m" + colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m" // Underline - colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m" - colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m" - colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m" - colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m" - colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m" - colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m" - colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m" - colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m" + colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m" + colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m" + colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m" + colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m" + colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m" + colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m" + colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m" + colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m" // High Intensity - colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m" - colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m" - colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m" - colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m" - colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m" - colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m" - colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m" - colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m" + colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m" + colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m" + colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m" + colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m" + colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m" + colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m" + colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m" + colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m" // Bold High Intensity - colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m" - colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m" - colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m" - colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m" - colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m" - colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m" - colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m" - colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m" + colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m" + colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m" + colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m" + colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m" + colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m" + colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m" + colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m" + colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m" return colorcodes } @@ -261,14 +255,15 @@ def attachMultiqcReport(multiqc_report) { mqc_report = multiqc_report.getVal() if (mqc_report.getClass() == ArrayList && mqc_report.size() >= 1) { if (mqc_report.size() > 1) { - log.warn "[$workflow.manifest.name] Found multiple reports from process 'MULTIQC', will use only one" + log.warn("[${workflow.manifest.name}] Found multiple reports from process 'MULTIQC', will use only one") } mqc_report = mqc_report[0] } } - } catch (all) { + } + catch (Exception all) { if (multiqc_report) { - log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" + log.warn("[${workflow.manifest.name}] Could not attach MultiQC report to summary email") } } return mqc_report @@ -280,26 +275,35 @@ def attachMultiqcReport(multiqc_report) { def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdir, monochrome_logs=true, multiqc_report=null) { // Set up the e-mail variables - def subject = "[$workflow.manifest.name] Successful: $workflow.runName" + def subject = "[${workflow.manifest.name}] Successful: ${workflow.runName}" if (!workflow.success) { - subject = "[$workflow.manifest.name] FAILED: $workflow.runName" + subject = "[${workflow.manifest.name}] FAILED: ${workflow.runName}" } def summary = [:] - summary_params.keySet().sort().each { group -> - summary << summary_params[group] - } + summary_params + .keySet() + .sort() + .each { group -> + summary << summary_params[group] + } def misc_fields = [:] misc_fields['Date Started'] = workflow.start misc_fields['Date Completed'] = workflow.complete misc_fields['Pipeline script file path'] = workflow.scriptFile misc_fields['Pipeline script hash ID'] = workflow.scriptId - if (workflow.repository) misc_fields['Pipeline repository Git URL'] = workflow.repository - if (workflow.commitId) misc_fields['Pipeline repository Git Commit'] = workflow.commitId - if (workflow.revision) misc_fields['Pipeline Git branch/tag'] = workflow.revision - misc_fields['Nextflow Version'] = workflow.nextflow.version - misc_fields['Nextflow Build'] = workflow.nextflow.build + if (workflow.repository) { + misc_fields['Pipeline repository Git URL'] = workflow.repository + } + if (workflow.commitId) { + misc_fields['Pipeline repository Git Commit'] = workflow.commitId + } + if (workflow.revision) { + misc_fields['Pipeline Git branch/tag'] = workflow.revision + } + misc_fields['Nextflow Version'] = workflow.nextflow.version + misc_fields['Nextflow Build'] = workflow.nextflow.build misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp def email_fields = [:] @@ -337,7 +341,7 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi // Render the sendmail template def max_multiqc_email_size = (params.containsKey('max_multiqc_email_size') ? params.max_multiqc_email_size : 0) as nextflow.util.MemoryUnit - def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "${workflow.projectDir}", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ] + def smail_fields = [email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "${workflow.projectDir}", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes()] def sf = new File("${workflow.projectDir}/assets/sendmail_template.txt") def sendmail_template = engine.createTemplate(sf).make(smail_fields) def sendmail_html = sendmail_template.toString() @@ -346,30 +350,32 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi def colors = logColours(monochrome_logs) as Map if (email_address) { try { - if (plaintext_email) { throw new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } + if (plaintext_email) { +new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } // Try to send HTML e-mail using sendmail def sendmail_tf = new File(workflow.launchDir.toString(), ".sendmail_tmp.html") sendmail_tf.withWriter { w -> w << sendmail_html } - [ 'sendmail', '-t' ].execute() << sendmail_html - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (sendmail)-" - } catch (all) { + ['sendmail', '-t'].execute() << sendmail_html + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Sent summary e-mail to ${email_address} (sendmail)-") + } + catch (Exception all) { // Catch failures and try with plaintext - def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] + def mail_cmd = ['mail', '-s', subject, '--content-type=text/html', email_address] mail_cmd.execute() << email_html - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-" + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Sent summary e-mail to ${email_address} (mail)-") } } // Write summary e-mail HTML to a file def output_hf = new File(workflow.launchDir.toString(), ".pipeline_report.html") output_hf.withWriter { w -> w << email_html } - nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html"); + nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html") output_hf.delete() // Write summary e-mail TXT to a file def output_tf = new File(workflow.launchDir.toString(), ".pipeline_report.txt") output_tf.withWriter { w -> w << email_txt } - nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt"); + nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt") output_tf.delete() } @@ -380,12 +386,14 @@ def completionSummary(monochrome_logs=true) { def colors = logColours(monochrome_logs) as Map if (workflow.success) { if (workflow.stats.ignoredCount == 0) { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Pipeline completed successfully${colors.reset}-" - } else { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.yellow} Pipeline completed successfully, but with errored process(es) ${colors.reset}-" + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Pipeline completed successfully${colors.reset}-") + } + else { + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.yellow} Pipeline completed successfully, but with errored process(es) ${colors.reset}-") } - } else { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-" + } + else { + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.red} Pipeline completed with errors${colors.reset}-") } } @@ -394,21 +402,30 @@ def completionSummary(monochrome_logs=true) { // def imNotification(summary_params, hook_url) { def summary = [:] - summary_params.keySet().sort().each { group -> - summary << summary_params[group] - } + summary_params + .keySet() + .sort() + .each { group -> + summary << summary_params[group] + } def misc_fields = [:] - misc_fields['start'] = workflow.start - misc_fields['complete'] = workflow.complete - misc_fields['scriptfile'] = workflow.scriptFile - misc_fields['scriptid'] = workflow.scriptId - if (workflow.repository) misc_fields['repository'] = workflow.repository - if (workflow.commitId) misc_fields['commitid'] = workflow.commitId - if (workflow.revision) misc_fields['revision'] = workflow.revision - misc_fields['nxf_version'] = workflow.nextflow.version - misc_fields['nxf_build'] = workflow.nextflow.build - misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp + misc_fields['start'] = workflow.start + misc_fields['complete'] = workflow.complete + misc_fields['scriptfile'] = workflow.scriptFile + misc_fields['scriptid'] = workflow.scriptId + if (workflow.repository) { + misc_fields['repository'] = workflow.repository + } + if (workflow.commitId) { + misc_fields['commitid'] = workflow.commitId + } + if (workflow.revision) { + misc_fields['revision'] = workflow.revision + } + misc_fields['nxf_version'] = workflow.nextflow.version + misc_fields['nxf_build'] = workflow.nextflow.build + misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp def msg_fields = [:] msg_fields['version'] = getWorkflowVersion() @@ -433,13 +450,13 @@ def imNotification(summary_params, hook_url) { def json_message = json_template.toString() // POST - def post = new URL(hook_url).openConnection(); + def post = new URL(hook_url).openConnection() post.setRequestMethod("POST") post.setDoOutput(true) post.setRequestProperty("Content-Type", "application/json") - post.getOutputStream().write(json_message.getBytes("UTF-8")); - def postRC = post.getResponseCode(); - if (! postRC.equals(200)) { - log.warn(post.getErrorStream().getText()); + post.getOutputStream().write(json_message.getBytes("UTF-8")) + def postRC = post.getResponseCode() + if (!postRC.equals(200)) { + log.warn(post.getErrorStream().getText()) } } From d88d6697ad4086a1b230e7df9ce868ffcdde22ec Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Fri, 11 Oct 2024 12:35:10 +0000 Subject: [PATCH 051/147] Template update for nf-core/tools version 3.0.2 --- .github/workflows/ci.yml | 60 +++++++++++++------ .../workflows/template_version_comment.yml | 21 ++++--- .gitignore | 1 + .nf-core.yml | 2 +- main.nf | 2 +- modules.json | 6 +- modules/nf-core/multiqc/main.nf | 2 +- nextflow.config | 4 +- .../utils_nfcore_scrnaseq_pipeline/main.nf | 4 +- .../nf-core/utils_nextflow_pipeline/main.nf | 30 +++++----- .../nf-core/utils_nfcore_pipeline/main.nf | 10 ++-- workflows/scrnaseq.nf | 2 - 12 files changed, 86 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37227a13..57b74922 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ on: env: NXF_ANSI_LOG: false + NXF_SINGULARITY_CACHEDIR: ${{ github.workspace }}/.singularity + NXF_SINGULARITY_LIBRARYDIR: ${{ github.workspace }}/.singularity concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" @@ -18,7 +20,7 @@ concurrency: jobs: test: - name: Run pipeline with test data + name: "Run pipeline with test data (${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }})" # Only run on push if this is the nf-core dev branch (merged PRs) if: "${{ github.event_name != 'push' || (github.event_name == 'push' && github.repository == 'nf-core/scrnaseq') }}" runs-on: ubuntu-latest @@ -27,33 +29,57 @@ jobs: NXF_VER: - "24.04.2" - "latest-everything" + profile: + - "conda" + - "docker" + - "singularity" + test_name: + - "test" + isMaster: + - ${{ github.base_ref == 'master' }} + # Exclude conda and singularity on dev + exclude: + - isMaster: false + profile: "conda" + - isMaster: false + profile: "singularity" steps: - name: Check out pipeline code uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - - name: Install Nextflow + - name: Set up Nextflow uses: nf-core/setup-nextflow@v2 with: version: "${{ matrix.NXF_VER }}" - - name: Disk space cleanup - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + - name: Set up Apptainer + if: matrix.profile == 'singularity' + uses: eWaterCycle/setup-apptainer@main - - name: Run pipeline with test data (docker) - # TODO nf-core: You can customise CI pipeline run tests as required - # For example: adding multiple test runs with different parameters - # Remember that you can parallelise this by using strategy.matrix + - name: Set up Singularity + if: matrix.profile == 'singularity' run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,docker --outdir ./results + mkdir -p $NXF_SINGULARITY_CACHEDIR + mkdir -p $NXF_SINGULARITY_LIBRARYDIR + + - name: Set up Miniconda + if: matrix.profile == 'conda' + uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca # v3 + with: + miniconda-version: "latest" + auto-update-conda: true + conda-solver: libmamba + channels: conda-forge,bioconda - - name: Run pipeline with test data (singularity) - # TODO nf-core: You can customise CI pipeline run tests as required + - name: Set up Conda + if: matrix.profile == 'conda' run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,singularity --outdir ./results - if: "${{ github.base_ref == 'master' }}" + echo $(realpath $CONDA)/condabin >> $GITHUB_PATH + echo $(realpath python) >> $GITHUB_PATH + + - name: Clean up Disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - name: Run pipeline with test data (conda) - # TODO nf-core: You can customise CI pipeline run tests as required + - name: "Run pipeline with test data ${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }}" run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,conda --outdir ./results - if: "${{ github.base_ref == 'master' }}" + nextflow run ${GITHUB_WORKSPACE} -profile ${{ matrix.test_name }},${{ matrix.profile }} --outdir ./results diff --git a/.github/workflows/template_version_comment.yml b/.github/workflows/template_version_comment.yml index 9dea41f0..e8aafe44 100644 --- a/.github/workflows/template_version_comment.yml +++ b/.github/workflows/template_version_comment.yml @@ -10,9 +10,11 @@ jobs: steps: - name: Check out pipeline code uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Read template version from .nf-core.yml - uses: pietrobolcato/action-read-yaml@1.0.0 + uses: nichmor/minimal-read-yaml@v0.0.2 id: read_yml with: config: ${{ github.workspace }}/.nf-core.yml @@ -24,20 +26,21 @@ jobs: - name: Check nf-core outdated id: nf_core_outdated - run: pip list --outdated | grep nf-core + run: echo "OUTPUT=$(pip list --outdated | grep nf-core)" >> ${GITHUB_ENV} - name: Post nf-core template version comment uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 if: | - ${{ steps.nf_core_outdated.outputs.stdout }} =~ 'nf-core' + contains(env.OUTPUT, 'nf-core') with: repo-token: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} allow-repeats: false message: | - ## :warning: Newer version of the nf-core template is available. - - Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. - Please update your pipeline to the latest version. - - For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). + > [!WARNING] + > Newer version of the nf-core template is available. + > + > Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. + > Please update your pipeline to the latest version. + > + > For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). # diff --git a/.gitignore b/.gitignore index 5124c9ac..a42ce016 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ results/ testing/ testing* *.pyc +null/ diff --git a/.nf-core.yml b/.nf-core.yml index 684de437..48d46d3b 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -6,7 +6,7 @@ lint: - .github/ISSUE_TEMPLATE/bug_report.yml schema_params: false template_strings: false -nf_core_version: 3.0.1 +nf_core_version: 3.0.2 org_path: null repository_type: pipeline template: diff --git a/main.nf b/main.nf index a20c1ec6..58baa655 100644 --- a/main.nf +++ b/main.nf @@ -76,7 +76,7 @@ workflow { params.outdir, params.input ) - + // // WORKFLOW: Run main workflow // diff --git a/modules.json b/modules.json index cc391d5e..585a1c84 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "b8d36829fa84b6e404364abff787e8b07f6d058c", + "git_sha": "cf17ca47590cc578dfb47db1c2a44ef86f89976d", "installed_by": ["modules"] } } @@ -21,12 +21,12 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "9d05360da397692321d377b6102d2fb22507c6ef", + "git_sha": "3aa0aec1d52d492fe241919f0c6100ebf0074082", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "772684d9d66f37b650c8ba5146ac1ee3ecba2acb", + "git_sha": "1b6b9a3338d011367137808b49b923515080e3ba", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 9724d2f3..cc0643e1 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -52,7 +52,7 @@ process MULTIQC { stub: """ mkdir multiqc_data - touch multiqc_plots + mkdir multiqc_plots touch multiqc_report.html cat <<-END_VERSIONS > versions.yml diff --git a/nextflow.config b/nextflow.config index 7ed2e084..915a2473 100644 --- a/nextflow.config +++ b/nextflow.config @@ -254,10 +254,10 @@ validation { """ afterText = """${manifest.doi ? "* The pipeline\n" : ""}${manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${manifest.doi ? "\n" : ""} * The nf-core framework - https://doi.org/10.1038/s41587-020-0439-x + https://doi.org/10.1038/s41587-020-0439-x * Software dependencies - https://github.com/${manifest.name}/blob/master/CITATIONS.md + https://github.com/${manifest.name}/blob/master/CITATIONS.md """ } summary { diff --git a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf index 975d4034..94355f06 100644 --- a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf @@ -47,7 +47,6 @@ workflow PIPELINE_INITIALISATION { workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 ) - // // Validate parameters and generate parameter summary to stdout // @@ -56,7 +55,6 @@ workflow PIPELINE_INITIALISATION { validate_params, null ) - // // Check config provided to the pipeline @@ -64,6 +62,7 @@ workflow PIPELINE_INITIALISATION { UTILS_NFCORE_PIPELINE ( nextflow_cli_args ) + // // Custom validation for pipeline parameters // @@ -110,7 +109,6 @@ workflow PIPELINE_COMPLETION { email // string: email address email_on_fail // string: email address sent on pipeline failure plaintext_email // boolean: Send plain-text email instead of HTML - outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output hook_url // string: hook URL for notifications diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index 2b0dc67a..0fcbf7b3 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -3,9 +3,9 @@ // /* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NEXTFLOW_PIPELINE { @@ -44,9 +44,9 @@ workflow UTILS_NEXTFLOW_PIPELINE { } /* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -106,17 +106,19 @@ def checkCondaChannels() { def channels_missing = ((required_channels_in_order as Set) - (channels as Set)) as Boolean // Check that they are in the right order - def channel_priority_violation = false - - required_channels_in_order.eachWithIndex { channel, index -> - if (index < required_channels_in_order.size() - 1) { - channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index + 1])) - } - } + def channel_priority_violation = required_channels_in_order != channels.findAll { ch -> ch in required_channels_in_order } if (channels_missing | channel_priority_violation) { - log.warn( - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + " There is a problem with your Conda configuration!\n\n" + " You will need to set-up the conda-forge and bioconda channels correctly.\n" + " Please refer to https://bioconda.github.io/\n" + " The observed channel order is \n" + " ${channels}\n" + " but the following channel order is required:\n" + " ${required_channels_in_order}\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" - ) + log.warn """\ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + There is a problem with your Conda configuration! + You will need to set-up the conda-forge and bioconda channels correctly. + Please refer to https://bioconda.github.io/ + The observed channel order is + ${channels} + but the following channel order is required: + ${required_channels_in_order} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + """.stripIndent(true) } } diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index b78273ca..5cb7bafe 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -3,9 +3,9 @@ // /* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NFCORE_PIPELINE { @@ -21,9 +21,9 @@ workflow UTILS_NFCORE_PIPELINE { } /* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -62,7 +62,7 @@ def checkProfileProvided(nextflow_cli_args) { def workflowCitation() { def temp_doi_ref = "" def manifest_doi = workflow.manifest.doi.tokenize(",") - // Using a loop to handle multiple DOIs + // Handling multiple DOIs // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers // Removing ` ` since the manifest.doi is a string and not a proper list manifest_doi.each { doi_ref -> diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index d0d0d5d6..b994c0c9 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -57,13 +57,11 @@ workflow SCRNASEQ { Channel.fromPath(params.multiqc_logo, checkIfExists: true) : Channel.empty() - summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) ch_multiqc_files = ch_multiqc_files.mix( ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml')) - ch_multiqc_custom_methods_description = params.multiqc_methods_description ? file(params.multiqc_methods_description, checkIfExists: true) : file("$projectDir/assets/methods_description_template.yml", checkIfExists: true) From 81ba00167925a5606fe3e7222d63b17a1e687e07 Mon Sep 17 00:00:00 2001 From: "Edward S. Rice" Date: Fri, 11 Oct 2024 14:54:11 +0200 Subject: [PATCH 052/147] Fix #380 --- modules/local/star_align.nf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/local/star_align.nf b/modules/local/star_align.nf index 4b3df1e1..b7e2bf58 100644 --- a/modules/local/star_align.nf +++ b/modules/local/star_align.nf @@ -54,12 +54,18 @@ process STAR_ALIGN { // separate forward from reverse pairs def (forward, reverse) = reads.collate(2).transpose() """ + if [[ $whitelist == *.gz ]]; then + gzip -cdf $whitelist > whitelist.txt + else + cp $whitelist whitelist.txt + fi + STAR \\ --genomeDir $index \\ --readFilesIn ${reverse.join( "," )} ${forward.join( "," )} \\ --runThreadN $task.cpus \\ --outFileNamePrefix $prefix. \\ - --soloCBwhitelist <(gzip -cdf $whitelist) \\ + --soloCBwhitelist whitelist.txt \\ --soloType $protocol \\ --soloFeatures $star_feature \\ $other_10x_parameters \\ From 017e0d3ef954b0e6b4b347a76d217f291ae5f1af Mon Sep 17 00:00:00 2001 From: "Edward S. Rice" Date: Fri, 11 Oct 2024 15:26:40 +0200 Subject: [PATCH 053/147] Fix same error as last commit, but for alevin The alevin-fry workflow for this pipeline has the same issue as the STAR workflow: it assumes the whitelist is gzipped, and crashes if it is not. This commit fixes that by only trying to unzip the whitelist if it is in fact gzipped. --- modules/local/simpleaf_quant.nf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/local/simpleaf_quant.nf b/modules/local/simpleaf_quant.nf index abb58404..53e0ccb2 100644 --- a/modules/local/simpleaf_quant.nf +++ b/modules/local/simpleaf_quant.nf @@ -39,7 +39,11 @@ process SIMPLEAF_QUANT { save_whitelist = "" if (!(args_list.any { it in ['-k', '--knee', '-e', '--expect-cells', '-f', '--forced-cells']} || meta.expected_cells)) { if (whitelist) { - unzip_whitelist = "gzip -dcf $whitelist > whitelist.uncompressed.txt" + if (whitelist.name.endsWith('.gz')) { + unzip_whitelist = "gzip -dcf $whitelist > whitelist.uncompressed.txt" + } else { + unzip_whitelist = "cp $whitelist whitelist.uncompressed.txt" + } unfiltered_command = "-u whitelist.uncompressed.txt" save_whitelist = "mv whitelist.uncompressed.txt ${prefix}_alevin_results/" } From 6f9dc79b22d7f13ce29ccf06ee8a8b9444573da9 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Thu, 17 Oct 2024 10:53:52 +0200 Subject: [PATCH 054/147] Update fastqc and multiqc --- modules.json | 4 +- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 6 +- modules/nf-core/multiqc/meta.yml | 59 ++++++++++--------- .../nf-core/multiqc/tests/main.nf.test.snap | 24 ++++---- 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/modules.json b/modules.json index e7463012..3caa39e7 100644 --- a/modules.json +++ b/modules.json @@ -47,7 +47,7 @@ }, "fastqc": { "branch": "master", - "git_sha": "46eca555142d6e597729fcb682adcc791796f514", + "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", "installed_by": ["modules"] }, "gffread": { @@ -72,7 +72,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "b80f5fd12ff7c43938f424dd76392a2704fa2396", + "git_sha": "cf17ca47590cc578dfb47db1c2a44ef86f89976d", "installed_by": ["modules"] }, "star/genomegenerate": { diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index 3c1bb83a..6f5b867b 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -2,4 +2,4 @@ channels: - conda-forge - bioconda dependencies: - - bioconda::multiqc=1.23 + - bioconda::multiqc=1.25.1 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 73f0c996..cc0643e1 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,8 +3,8 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.23--pyhdfd78af_0' : - 'biocontainers/multiqc:1.23--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.25.1--pyhdfd78af_0' : + 'biocontainers/multiqc:1.25.1--pyhdfd78af_0' }" input: path multiqc_files, stageAs: "?/*" @@ -28,7 +28,7 @@ process MULTIQC { def prefix = task.ext.prefix ? "--filename ${task.ext.prefix}.html" : '' def config = multiqc_config ? "--config $multiqc_config" : '' def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' - def logo = multiqc_logo ? /--cl-config 'custom_logo: "${multiqc_logo}"'/ : '' + def logo = multiqc_logo ? "--cl-config 'custom_logo: \"${multiqc_logo}\"'" : '' def replace = replace_names ? "--replace-names ${replace_names}" : '' def samples = sample_names ? "--sample-names ${sample_names}" : '' """ diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index 6f64c06f..b16c1879 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -15,35 +15,36 @@ tools: licence: ["GPL-3.0-or-later"] identifier: biotools:multiqc input: - - multiqc_files: - type: file - description: | - List of reports / files recognised by MultiQC, for example the html and zip output of FastQC - - multiqc_config: - type: file - description: Optional config yml for MultiQC - pattern: "*.{yml,yaml}" - - extra_multiqc_config: - type: file - description: Second optional config yml for MultiQC. Will override common sections in multiqc_config. - pattern: "*.{yml,yaml}" - - multiqc_logo: - type: file - description: Optional logo file for MultiQC - pattern: "*.{png}" - - replace_names: - type: file - description: | - Optional two-column sample renaming file. First column a set of - patterns, second column a set of corresponding replacements. Passed via - MultiQC's `--replace-names` option. - pattern: "*.{tsv}" - - sample_names: - type: file - description: | - Optional TSV file with headers, passed to the MultiQC --sample_names - argument. - pattern: "*.{tsv}" + - - multiqc_files: + type: file + description: | + List of reports / files recognised by MultiQC, for example the html and zip output of FastQC + - - multiqc_config: + type: file + description: Optional config yml for MultiQC + pattern: "*.{yml,yaml}" + - - extra_multiqc_config: + type: file + description: Second optional config yml for MultiQC. Will override common sections + in multiqc_config. + pattern: "*.{yml,yaml}" + - - multiqc_logo: + type: file + description: Optional logo file for MultiQC + pattern: "*.{png}" + - - replace_names: + type: file + description: | + Optional two-column sample renaming file. First column a set of + patterns, second column a set of corresponding replacements. Passed via + MultiQC's `--replace-names` option. + pattern: "*.{tsv}" + - - sample_names: + type: file + description: | + Optional TSV file with headers, passed to the MultiQC --sample_names + argument. + pattern: "*.{tsv}" output: - report: - "*multiqc_report.html": diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index 45e95e5d..2fcbb5ff 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,87904cd321df21fac35d18f0fc01bb19" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T12:41:34.562023" + "timestamp": "2024-10-02T17:51:46.317523" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,87904cd321df21fac35d18f0fc01bb19" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T11:27:11.933869532" + "timestamp": "2024-10-02T17:52:20.680978" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,87904cd321df21fac35d18f0fc01bb19" + "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" ] ], "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-07-10T11:26:56.709849369" + "timestamp": "2024-10-02T17:52:09.185842" } } \ No newline at end of file From 2ed44fed24e43ab43ccc188fbff1fee2d0af3fee Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Thu, 17 Oct 2024 11:03:39 +0200 Subject: [PATCH 055/147] Remove check_max usages --- conf/modules.config | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 0f74a93c..ff24d6bb 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -19,7 +19,7 @@ process { withName: FASTQC { ext.args = '--quiet' - time = { check_max( 120.h * task.attempt, 'time' ) } + time = { 120.h * task.attempt } } withName: 'MULTIQC' { ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' } @@ -82,7 +82,7 @@ if(params.aligner == "cellranger") { mode: params.publish_dir_mode ] ext.args = {"--chemistry ${meta.chemistry} --create-bam true " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} - time = { check_max( 240.h * task.attempt, 'time' ) } + time = { 240.h * task.attempt } } } } @@ -109,7 +109,7 @@ if(params.aligner == "cellrangerarc") { mode: params.publish_dir_mode ] ext.args = {meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : ''} - time = { check_max( 240.h * task.attempt, 'time' ) } + time = { 240.h * task.attempt } } } } @@ -139,7 +139,7 @@ if(params.aligner == "universc") { path: "${params.outdir}/universc", mode: params.publish_dir_mode ] - time = { check_max( 240.h * task.attempt, 'time' ) } + time = { 240.h * task.attempt } } } } @@ -168,7 +168,7 @@ if (params.aligner == "alevin") { // Fix for issue 196 // Modified for issue 334 withName: 'ALEVINQC' { - time = { check_max( 120.h, 'time' ) } + time = { 120.h } } } } From ecba92d03c8edf0e2e8feecdc42614c5a7a70e2f Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Thu, 17 Oct 2024 11:22:10 +0200 Subject: [PATCH 056/147] Remove old resource limits from tests --- tests/main_pipeline_alevin.nf.test | 5 ----- tests/main_pipeline_cellranger.nf.test | 5 ----- tests/main_pipeline_cellrangermulti.nf.test | 5 ----- tests/main_pipeline_kallisto.nf.test | 5 ----- tests/main_pipeline_star.nf.test | 5 ----- 5 files changed, 25 deletions(-) diff --git a/tests/main_pipeline_alevin.nf.test b/tests/main_pipeline_alevin.nf.test index dc6c081a..06d31f9e 100644 --- a/tests/main_pipeline_alevin.nf.test +++ b/tests/main_pipeline_alevin.nf.test @@ -10,11 +10,6 @@ nextflow_pipeline { params { aligner = 'alevin' outdir = "${outputDir}/results_alevin" - - // Limit resources so that this can run on GitHub Actions -- for some reason it had not been taken from shared config - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' } } diff --git a/tests/main_pipeline_cellranger.nf.test b/tests/main_pipeline_cellranger.nf.test index ea68eca6..b62e615b 100644 --- a/tests/main_pipeline_cellranger.nf.test +++ b/tests/main_pipeline_cellranger.nf.test @@ -10,11 +10,6 @@ nextflow_pipeline { params { aligner = 'cellranger' outdir = "${outputDir}/results_cellranger" - - // Limit resources so that this can run on GitHub Actions -- for some reason it had not been taken from shared config - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' } } diff --git a/tests/main_pipeline_cellrangermulti.nf.test b/tests/main_pipeline_cellrangermulti.nf.test index 42e927ea..9785ee75 100644 --- a/tests/main_pipeline_cellrangermulti.nf.test +++ b/tests/main_pipeline_cellrangermulti.nf.test @@ -18,11 +18,6 @@ nextflow_pipeline { gtf = 'https://ftp.ensembl.org/pub/release-110/gtf/homo_sapiens/Homo_sapiens.GRCh38.110.gtf.gz' aligner = 'cellrangermulti' protocol = 'auto' - - // Limit resources so that this can run on GitHub Actions -- for some reason it had not been taken from shared config - max_cpus = 2 - max_memory = '8.GB' - max_time = '6.h' } } diff --git a/tests/main_pipeline_kallisto.nf.test b/tests/main_pipeline_kallisto.nf.test index 12e78144..76c5e479 100644 --- a/tests/main_pipeline_kallisto.nf.test +++ b/tests/main_pipeline_kallisto.nf.test @@ -10,11 +10,6 @@ nextflow_pipeline { params { aligner = 'kallisto' outdir = "${outputDir}/results_kallisto" - - // Limit resources so that this can run on GitHub Actions -- for some reason it had not been taken from shared config - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' } } diff --git a/tests/main_pipeline_star.nf.test b/tests/main_pipeline_star.nf.test index 37c54d57..11b52efd 100644 --- a/tests/main_pipeline_star.nf.test +++ b/tests/main_pipeline_star.nf.test @@ -10,11 +10,6 @@ nextflow_pipeline { params { aligner = 'star' outdir = "${outputDir}/results_star" - - // Limit resources so that this can run on GitHub Actions -- for some reason it had not been taken from shared config - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' } } From 42c5aa37340bf0f9156962a40ee92530ecd5f7f7 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Thu, 17 Oct 2024 11:25:55 +0200 Subject: [PATCH 057/147] Set global resource limits --- tests/nextflow.config | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/nextflow.config b/tests/nextflow.config index 7efd4642..b2d67a36 100644 --- a/tests/nextflow.config +++ b/tests/nextflow.config @@ -32,4 +32,10 @@ process { withName: '.*:CELLRANGER_COUNT' { maxForks = 1 } + + resourceLimits = [ + cpus: 4, + memory: '10.GB', + time: '1.h' + ] } From a6ed15567a475ce7be153d9deb5e43462916171c Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Thu, 17 Oct 2024 11:37:09 +0200 Subject: [PATCH 058/147] Align test resource limits with previous implementation --- tests/nextflow.config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nextflow.config b/tests/nextflow.config index b2d67a36..db3051a1 100644 --- a/tests/nextflow.config +++ b/tests/nextflow.config @@ -34,8 +34,8 @@ process { } resourceLimits = [ - cpus: 4, - memory: '10.GB', - time: '1.h' + cpus: 2, + memory: '6.GB', + time: '6.h' ] } From 4a7160333192b5cb4a86f5ccd985e266bde33109 Mon Sep 17 00:00:00 2001 From: Nico Trummer <52698566+nictru@users.noreply.github.com> Date: Fri, 18 Oct 2024 05:52:36 +0000 Subject: [PATCH 059/147] Remove problematic workflow snapshotting --- tests/main_pipeline_alevin.nf.test | 1 - tests/main_pipeline_alevin.nf.test.snap | 19 ----------------- tests/main_pipeline_cellranger.nf.test | 1 - tests/main_pipeline_cellranger.nf.test.snap | 21 +------------------ tests/main_pipeline_cellrangermulti.nf.test | 2 -- ...main_pipeline_cellrangermulti.nf.test.snap | 21 +------------------ tests/main_pipeline_kallisto.nf.test | 1 - tests/main_pipeline_kallisto.nf.test.snap | 21 +------------------ tests/main_pipeline_star.nf.test | 1 - tests/main_pipeline_star.nf.test.snap | 19 ----------------- 10 files changed, 3 insertions(+), 104 deletions(-) diff --git a/tests/main_pipeline_alevin.nf.test b/tests/main_pipeline_alevin.nf.test index 06d31f9e..be04fb76 100644 --- a/tests/main_pipeline_alevin.nf.test +++ b/tests/main_pipeline_alevin.nf.test @@ -45,7 +45,6 @@ nextflow_pipeline { // Check if files are the same // {assert snapshot( - workflow, path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat.mtx" ), path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), diff --git a/tests/main_pipeline_alevin.nf.test.snap b/tests/main_pipeline_alevin.nf.test.snap index e3d19f72..58976c6a 100644 --- a/tests/main_pipeline_alevin.nf.test.snap +++ b/tests/main_pipeline_alevin.nf.test.snap @@ -1,25 +1,6 @@ { "test-dataset_alevin_aligner": { "content": [ - { - "stderr": [ - - ], - "errorReport": "", - "exitStatus": 0, - "failed": false, - "stdout": [ - - ], - "errorMessage": "", - "trace": { - "tasksFailed": 0, - "tasksCount": 14, - "tasksSucceeded": 14 - }, - "name": "workflow", - "success": true - }, "quants_mat_cols.txt:md5,e9868982c17a330392e38c2a5933cf97", "quants_mat.mtx:md5,b8aa7b3c488fd8923de50a3621d4991f", "quants_mat_rows.txt:md5,6227df5a13127b71c71fb18cd8574857", diff --git a/tests/main_pipeline_cellranger.nf.test b/tests/main_pipeline_cellranger.nf.test index b62e615b..d1eb4191 100644 --- a/tests/main_pipeline_cellranger.nf.test +++ b/tests/main_pipeline_cellranger.nf.test @@ -47,7 +47,6 @@ nextflow_pipeline { // Check if files are the same // {assert snapshot( - workflow, path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/features.tsv.gz" ), path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/matrix.mtx.gz" ), diff --git a/tests/main_pipeline_cellranger.nf.test.snap b/tests/main_pipeline_cellranger.nf.test.snap index ef8874f8..6d276d82 100644 --- a/tests/main_pipeline_cellranger.nf.test.snap +++ b/tests/main_pipeline_cellranger.nf.test.snap @@ -1,25 +1,6 @@ { "test-dataset_cellranger_aligner": { "content": [ - { - "stderr": [ - - ], - "errorReport": "", - "exitStatus": 0, - "failed": false, - "stdout": [ - - ], - "errorMessage": "", - "trace": { - "tasksFailed": 0, - "tasksCount": 18, - "tasksSucceeded": 18 - }, - "name": "workflow", - "success": true - }, "barcodes.tsv.gz:md5,fe6e51564b4405b37ca8604a844b1f2e", "features.tsv.gz:md5,99e453cb1443a3e43e99405184e51a5e", "matrix.mtx.gz:md5,1528b9b0fccc78dec95695928e42e710", @@ -43,4 +24,4 @@ }, "timestamp": "2024-04-16T09:43:45.32298954" } -} \ No newline at end of file +} diff --git a/tests/main_pipeline_cellrangermulti.nf.test b/tests/main_pipeline_cellrangermulti.nf.test index 9785ee75..9cf4b413 100644 --- a/tests/main_pipeline_cellrangermulti.nf.test +++ b/tests/main_pipeline_cellrangermulti.nf.test @@ -69,8 +69,6 @@ nextflow_pipeline { // Check if files are the same // {assert snapshot( - workflow, - // barcodes.tsv.gz files path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), diff --git a/tests/main_pipeline_cellrangermulti.nf.test.snap b/tests/main_pipeline_cellrangermulti.nf.test.snap index d8cf3ae4..1594f01e 100644 --- a/tests/main_pipeline_cellrangermulti.nf.test.snap +++ b/tests/main_pipeline_cellrangermulti.nf.test.snap @@ -1,25 +1,6 @@ { "test-dataset_cellrangermulti_aligner": { "content": [ - { - "stderr": [ - - ], - "errorReport": "", - "exitStatus": 0, - "failed": false, - "stdout": [ - - ], - "errorMessage": "", - "trace": { - "tasksFailed": 0, - "tasksCount": 55, - "tasksSucceeded": 55 - }, - "name": "workflow", - "success": true - }, "barcodes.tsv.gz:md5,a3937e73aa76a2acff554ca2f81a108d", "barcodes.tsv.gz:md5,60c25e3ecc0d185048bad6ee78b03ec5", "barcodes.tsv.gz:md5,74ccf55411109ed9c3f2b6f73ae91ddc", @@ -99,4 +80,4 @@ }, "timestamp": "2024-05-06T11:40:54.060867657" } -} \ No newline at end of file +} diff --git a/tests/main_pipeline_kallisto.nf.test b/tests/main_pipeline_kallisto.nf.test index 76c5e479..cb331986 100644 --- a/tests/main_pipeline_kallisto.nf.test +++ b/tests/main_pipeline_kallisto.nf.test @@ -46,7 +46,6 @@ nextflow_pipeline { // Check if files are the same // {assert snapshot( - workflow, path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.barcodes.txt" ), path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.genes.txt" ), path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.mtx" ), diff --git a/tests/main_pipeline_kallisto.nf.test.snap b/tests/main_pipeline_kallisto.nf.test.snap index f9b9c96a..82580178 100644 --- a/tests/main_pipeline_kallisto.nf.test.snap +++ b/tests/main_pipeline_kallisto.nf.test.snap @@ -1,25 +1,6 @@ { "test-dataset_kallisto_aligner": { "content": [ - { - "stderr": [ - - ], - "errorReport": "", - "exitStatus": 0, - "failed": false, - "stdout": [ - - ], - "errorMessage": "", - "trace": { - "tasksFailed": 0, - "tasksCount": 12, - "tasksSucceeded": 12 - }, - "name": "workflow", - "success": true - }, "cells_x_genes.barcodes.txt:md5,72d78bb1c1ee7cb174520b30f695aa48", "cells_x_genes.genes.txt:md5,acd9d00120f52031974b2add3e7521b6", "cells_x_genes.mtx:md5,894d60da192e3788de11fa8fc1fa711d", @@ -35,4 +16,4 @@ }, "timestamp": "2024-03-18T14:51:42.040931572" } -} \ No newline at end of file +} diff --git a/tests/main_pipeline_star.nf.test b/tests/main_pipeline_star.nf.test index 11b52efd..21eb1955 100644 --- a/tests/main_pipeline_star.nf.test +++ b/tests/main_pipeline_star.nf.test @@ -46,7 +46,6 @@ nextflow_pipeline { // Check if files are the same // {assert snapshot( - workflow, path( "${outputDir}/results_star/star/Sample_X/Sample_X.SJ.out.tab" ), path( "${outputDir}/results_star/star/Sample_X/Sample_X.Solo.out/Barcodes.stats" ), path( "${outputDir}/results_star/star/Sample_X/Sample_X.Solo.out/Gene/filtered/matrix.mtx.gz" ), diff --git a/tests/main_pipeline_star.nf.test.snap b/tests/main_pipeline_star.nf.test.snap index 0aae74cf..88739a7b 100644 --- a/tests/main_pipeline_star.nf.test.snap +++ b/tests/main_pipeline_star.nf.test.snap @@ -1,25 +1,6 @@ { "test-dataset_star_aligner": { "content": [ - { - "stderr": [ - - ], - "errorReport": "", - "exitStatus": 0, - "failed": false, - "stdout": [ - - ], - "errorMessage": "", - "trace": { - "tasksFailed": 0, - "tasksCount": 17, - "tasksSucceeded": 17 - }, - "name": "workflow", - "success": true - }, "Sample_X.SJ.out.tab:md5,d2d7f0abe38029012571bdf6622fc6eb", "Barcodes.stats:md5,7f99dc8aa5e074fbe5779ea7712c0886", "matrix.mtx.gz:md5,6a923393343aa1a69b0cf1bd998c9285", From 9d13a7e1e113c51428db41a9d912d531f8ccaa09 Mon Sep 17 00:00:00 2001 From: "Edward S. Rice" Date: Mon, 28 Oct 2024 11:41:36 +0100 Subject: [PATCH 060/147] Fix potential filename conflict Call the uncompressed whitelist whitelist.uncompressed.txt instead of whitelist.txt. --- modules/local/star_align.nf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/star_align.nf b/modules/local/star_align.nf index b7e2bf58..0b26e037 100644 --- a/modules/local/star_align.nf +++ b/modules/local/star_align.nf @@ -55,9 +55,9 @@ process STAR_ALIGN { def (forward, reverse) = reads.collate(2).transpose() """ if [[ $whitelist == *.gz ]]; then - gzip -cdf $whitelist > whitelist.txt + gzip -cdf $whitelist > whitelist.uncompressed.txt else - cp $whitelist whitelist.txt + cp $whitelist whitelist.uncompressed.txt fi STAR \\ @@ -65,7 +65,7 @@ process STAR_ALIGN { --readFilesIn ${reverse.join( "," )} ${forward.join( "," )} \\ --runThreadN $task.cpus \\ --outFileNamePrefix $prefix. \\ - --soloCBwhitelist whitelist.txt \\ + --soloCBwhitelist whitelist.uncompressed.txt \\ --soloType $protocol \\ --soloFeatures $star_feature \\ $other_10x_parameters \\ From 0098906f37370bd630bd0beca2e4edcebf9df8d9 Mon Sep 17 00:00:00 2001 From: kopichris Date: Tue, 29 Oct 2024 18:18:05 +0800 Subject: [PATCH 061/147] feat(cellranger count and multi): Add create-bam option to cellranger count and multi pipelines --- conf/modules.config | 2 +- nextflow.config | 2 ++ nextflow_schema.json | 11 +++++++++++ workflows/scrnaseq.nf | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 81395a1d..15073a21 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -82,7 +82,7 @@ if(params.aligner == "cellranger") { path: "${params.outdir}/${params.aligner}/count", mode: params.publish_dir_mode ] - ext.args = {"--chemistry ${meta.chemistry} --create-bam true " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} + ext.args = {"--chemistry ${meta.chemistry} --create-bam ${params.cellranger_create_bam}" + " " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} time = { check_max( 240.h * task.attempt, 'time' ) } } } diff --git a/nextflow.config b/nextflow.config index e1b608d2..7a5a6c76 100644 --- a/nextflow.config +++ b/nextflow.config @@ -43,6 +43,7 @@ params { // Cellranger parameters skip_cellranger_renaming = false cellranger_index = null + cellranger_create_bam = true // Cellranger ARC parameters motifs = null @@ -65,6 +66,7 @@ params { vdj_inner_enrichment_primers = null gex_barcode_sample_assignment = null cellranger_multi_barcodes = null + cellranger_multi_create_bam = true // Template Boilerplate options diff --git a/nextflow_schema.json b/nextflow_schema.json index e5fb71b5..f582e934 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -270,6 +270,11 @@ "skip_cellranger_renaming": { "type": "boolean", "description": "Should it skip the automatic renaming included in cellranger-related modules?" + }, + "cellranger_create_bam": { + "type": "boolean", + "description": "Create BAM alignment file. Note: Using the default value of true will increase computational resources and disk space. Please see the [10x documentation about create-bam parameter](https://kb.10xgenomics.com/hc/en-us/articles/24292856143885-Understanding-the-create-bam-Parameter-in-Cell-Ranger-v8-0) for more information.", + "default": true } } }, @@ -366,6 +371,11 @@ "exists": true, "mimetype": "text/csv", "description": "Additional samplesheet to provide information about multiplexed samples. See the 'Usage' section for more details." + }, + "cellranger_multi_create_bam": { + "type": "boolean", + "description": "Create BAM alignment file. Note: Using the default value of true will increase computational resources and disk space. Please see the [10x documentation about create-bam parameter](https://kb.10xgenomics.com/hc/en-us/articles/24292856143885-Understanding-the-create-bam-Parameter-in-Cell-Ranger-v8-0) for more information.", + "default": true } } }, @@ -617,3 +627,4 @@ } ] } + diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 10ced221..94bbb844 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -237,7 +237,7 @@ workflow SCRNASEQ { // add cellranger options that are currently handled by pipeline, coming from samplesheet // the module parses them from the 'gex' options if (meta.feature_type.toString() == 'gex') { - parsed_meta.options['create-bam'] = true // force bam creation -- param required by cellranger multi + parsed_meta.options['create-bam'] = params.cellranger_multi_create_bam // force bam creation -- param required by cellranger multi if (meta.expected_cells) { parsed_meta.options['expected-cells'] = meta.expected_cells } } From c749bf2aee0ea769b77ad44e51c2c04b83a456a2 Mon Sep 17 00:00:00 2001 From: kopichris Date: Tue, 29 Oct 2024 18:54:40 +0800 Subject: [PATCH 062/147] chore: Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1b6e41..239b53b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ 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). +## [Unreleased] +- Add --create-bam parameter to cellranger count and multi pipelines ([#384](https://github.com/nf-core/scrnaseq/issues/384)) + ## v2.7.1 - 2024-08-13 - Fix that tests have not been executed with nf-test v0.9 ([#359](https://github.com/nf-core/scrnaseq/pull/359)) From c1e8357112b8e389bab9e5a6503b052b6147594c Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:02:53 +0100 Subject: [PATCH 063/147] write uncompressed Co-authored-by: Gregor Sturm --- modules/local/templates/concat_h5ad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/concat_h5ad.py b/modules/local/templates/concat_h5ad.py index 033bc89a..2a483e7b 100755 --- a/modules/local/templates/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -34,6 +34,6 @@ def read_samplesheet(samplesheet): # merge with data.frame, on sample information adata.obs = adata.obs.join(df_samplesheet, on="sample").astype(str) - adata.write_h5ad("combined_${meta.input_type}_matrix.h5ad", compression="gzip") + adata.write_h5ad("combined_${meta.input_type}_matrix.h5ad") print("Wrote h5ad file to {}".format("combined_${meta.input_type}_matrix.h5ad")) From ae857102b56cc8f68def3bb45b1f167edb672c07 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:04:52 +0100 Subject: [PATCH 064/147] use .astype(str) Co-authored-by: Gregor Sturm --- modules/local/templates/concat_h5ad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/concat_h5ad.py b/modules/local/templates/concat_h5ad.py index 2a483e7b..e2cce156 100755 --- a/modules/local/templates/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -16,7 +16,7 @@ def read_samplesheet(samplesheet): # samplesheet may contain replicates, when it has, # group information from replicates and collapse with commas # only keep unique values using set() - df = df.groupby(["sample"]).agg(lambda column: ",".join(set(str(column)))) + df = df.groupby(["sample"]).agg(lambda column: ",".join(set(column.astype(str))) return df From ae8809a828b5543dc1c5ed968fa057e962fb13bc Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:05:14 +0100 Subject: [PATCH 065/147] simplify iteration Co-authored-by: Gregor Sturm --- modules/local/templates/mtx_to_h5ad_cellranger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index 294a15e7..be870829 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -69,9 +69,7 @@ def input_to_adata( # standard format # index are gene IDs and symbols are a column adata.var['gene_versions'] = adata.var.index - adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] - adata.var.index = adata.var["gene_ids"].values - adata.var = adata.var.drop("gene_ids", axis=1) + adata.var.index = adata.var['gene_versions'].str.split('.').str[0] adata.var_names_make_unique() # write results From 68464de7d8d0a71a827379f2d15d1f8f4b310cfc Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:05:35 +0100 Subject: [PATCH 066/147] perform join left operation Co-authored-by: Gregor Sturm --- modules/local/templates/mtx_to_h5ad_kallisto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index 24efe7e0..87511823 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -27,7 +27,7 @@ def _mtx_to_adata( txp2gene = pd.read_table(glob.glob(f"{t2g}")[0], header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) txp2gene = txp2gene.drop_duplicates(subset="gene_id").set_index("gene_id") - adata.var = pd.merge(adata.var, txp2gene, left_index=True, right_index=True) + adata.var = adata.var.join(txp2gene, how="left") return adata From fdedc4d3385e59b2f2670a7d16525928f0dc384f Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:05:51 +0100 Subject: [PATCH 067/147] do not compress output h5ad Co-authored-by: Gregor Sturm --- modules/local/templates/mtx_to_h5ad_kallisto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index 87511823..fab07f59 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -83,7 +83,7 @@ def input_to_adata( adata.var_names_make_unique() # write results - adata.write_h5ad(f"{output}", compression="gzip") + adata.write_h5ad(f"{output}") print(f"Wrote h5ad file to {output}") # From 187dbf6d04d8fe73a4b4cabbca2b957656035ed6 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:06:26 +0100 Subject: [PATCH 068/147] perform join left operation Co-authored-by: Gregor Sturm --- modules/local/templates/concat_h5ad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/concat_h5ad.py b/modules/local/templates/concat_h5ad.py index e2cce156..ac5da7f6 100755 --- a/modules/local/templates/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -33,7 +33,7 @@ def read_samplesheet(samplesheet): adata = ad.concat(dict_of_h5ad, label="sample", merge="unique", index_unique="_") # merge with data.frame, on sample information - adata.obs = adata.obs.join(df_samplesheet, on="sample").astype(str) + adata.obs = adata.obs.join(df_samplesheet, on="sample", how="left").astype(str) adata.write_h5ad("combined_${meta.input_type}_matrix.h5ad") print("Wrote h5ad file to {}".format("combined_${meta.input_type}_matrix.h5ad")) From 98af608abbc3aaa562b052ccecebb0030c9ef0b6 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 08:21:15 +0000 Subject: [PATCH 069/147] not compress h5ad output --- modules/local/templates/mtx_to_h5ad_alevin.py | 2 +- modules/local/templates/mtx_to_h5ad_cellranger.py | 2 +- modules/local/templates/mtx_to_h5ad_star.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index db295f1b..a4563d6b 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -73,7 +73,7 @@ def input_to_adata( adata.var_names_make_unique() # write results - adata.write_h5ad(f"{output}", compression="gzip") + adata.write_h5ad(f"{output}") print(f"Wrote h5ad file to {output}") # diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index be870829..4a40201d 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -73,7 +73,7 @@ def input_to_adata( adata.var_names_make_unique() # write results - adata.write_h5ad(f"{output}", compression="gzip") + adata.write_h5ad(f"{output}") print(f"Wrote h5ad file to {output}") # dump versions diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index 0cc9a969..5614e698 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -69,7 +69,7 @@ def input_to_adata( adata.var = adata.var.drop("gene_ids", axis=1) # write results - adata.write_h5ad(f"{output}", compression="gzip") + adata.write_h5ad(f"{output}") print(f"Wrote h5ad file to {output}") # From efd6299bd54cee59d68568f6fab20f1d9339eb7b Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 08:22:03 +0000 Subject: [PATCH 070/147] simplify iteration --- modules/local/templates/mtx_to_h5ad_alevin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index a4563d6b..d36d8797 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -67,9 +67,7 @@ def input_to_adata( # index are gene IDs and symbols are a column # TODO: how to get gene_symbols for alevin? adata.var['gene_versions'] = adata.var.index - adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] - adata.var.index = adata.var["gene_ids"].values - adata.var = adata.var.drop("gene_ids", axis=1) + adata.var.index = adata.var['gene_versions'].str.split('.').str[0] adata.var_names_make_unique() # write results From f357fd7ccbed89b715d1f0e1fa7b7b9ca9efeda1 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 08:23:53 +0000 Subject: [PATCH 071/147] simplify index iteration --- modules/local/templates/mtx_to_h5ad_kallisto.py | 4 +--- modules/local/templates/mtx_to_h5ad_star.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index fab07f59..ac71fc42 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -77,9 +77,7 @@ def input_to_adata( # standard format # index are gene IDs and symbols are a column adata.var['gene_versions'] = adata.var.index - adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] - adata.var.index = adata.var["gene_ids"].values - adata.var = adata.var.drop("gene_ids", axis=1) + adata.var.index = adata.var['gene_versions'].str.split('.').str[0] adata.var_names_make_unique() # write results diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index 5614e698..aa0cd02f 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -64,9 +64,7 @@ def input_to_adata( # index are gene IDs and symbols are a column adata.var["gene_symbol"] = adata.var.index adata.var['gene_versions'] = adata.var["gene_ids"] - adata.var['gene_ids'] = adata.var['gene_versions'].str.split('.').str[0] - adata.var.index = adata.var["gene_ids"].values - adata.var = adata.var.drop("gene_ids", axis=1) + adata.var.index = adata.var['gene_versions'].str.split('.').str[0] # write results adata.write_h5ad(f"{output}") From bd1a74cebe0fd8bcf725f2f8be2a461cedb598ea Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 08:24:22 +0000 Subject: [PATCH 072/147] fix unmatched parenthesis --- modules/local/templates/concat_h5ad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/concat_h5ad.py b/modules/local/templates/concat_h5ad.py index ac5da7f6..087f7fde 100755 --- a/modules/local/templates/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -16,7 +16,7 @@ def read_samplesheet(samplesheet): # samplesheet may contain replicates, when it has, # group information from replicates and collapse with commas # only keep unique values using set() - df = df.groupby(["sample"]).agg(lambda column: ",".join(set(column.astype(str))) + df = df.groupby(["sample"]).agg(lambda column: ",".join(set(column.astype(str)))) return df From 4731e009f2264b64356f490f58aa1479ac3dbc31 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 08:50:33 +0000 Subject: [PATCH 073/147] fix use of igenomes ... pipeline was not properly selecting igenomes options --- .../utils_nfcore_scrnaseq_pipeline/main.nf | 5 +++- workflows/scrnaseq.nf | 29 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf index dd54d352..16569d5a 100644 --- a/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_scrnaseq_pipeline/main.nf @@ -202,9 +202,12 @@ def getGenomeAttribute(attribute) { if (params.genomes && params.genome && params.genomes.containsKey(params.genome)) { if (params.genomes[ params.genome ].containsKey(attribute)) { return params.genomes[ params.genome ][ attribute ] + } else { + return null } + } else { + return null } - return null } // diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 3f3c1017..61401185 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -31,12 +31,11 @@ workflow SCRNASEQ { error "Only cellranger supports `protocol = 'auto'`. Please specify the protocol manually!" } - params.fasta = getGenomeAttribute('fasta') - params.gtf = getGenomeAttribute('gtf') - params.star_index = getGenomeAttribute('star') - - ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] - ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] + // search igenomes, but overwrite with user paths + // cannot use 'params. = ' in workflow, it does not overwrite parameter + def fasta_file = params.fasta ? params.fasta : getGenomeAttribute('fasta') + def gtf_file = params.gtf ? params.gtf : getGenomeAttribute('gtf') + def star_index = params.star_index ? params.star_index : getGenomeAttribute('star') // general input and params ch_transcript_fasta = params.transcript_fasta ? file(params.transcript_fasta): [] @@ -70,7 +69,7 @@ workflow SCRNASEQ { ch_salmon_index = params.salmon_index ? file(params.salmon_index) : [] //star params - star_index = params.star_index ? file(params.star_index, checkIfExists: true) : null + star_index = star_index ? file(star_index, checkIfExists: true) : null ch_star_index = star_index ? [[id: star_index.baseName], star_index] : [] star_feature = params.star_feature @@ -98,24 +97,24 @@ workflow SCRNASEQ { // // Uncompress genome fasta file if required // - if (params.fasta) { - if (params.fasta.endsWith('.gz')) { - ch_genome_fasta = GUNZIP_FASTA ( [ [:], file(params.fasta) ] ).gunzip.map { it[1] } + if (fasta_file) { + if (fasta_file.endsWith('.gz')) { + ch_genome_fasta = GUNZIP_FASTA ( [ [:], file(fasta_file) ] ).gunzip.map { it[1] } ch_versions = ch_versions.mix(GUNZIP_FASTA.out.versions) } else { - ch_genome_fasta = Channel.value( file(params.fasta) ) + ch_genome_fasta = Channel.value( file(fasta_file) ) } } // // Uncompress GTF annotation file or create from GFF3 if required // - if (params.gtf) { - if (params.gtf.endsWith('.gz')) { - ch_gtf = GUNZIP_GTF ( [ [:], file(params.gtf) ] ).gunzip.map { it[1] } + if (gtf_file) { + if (gtf_file.endsWith('.gz')) { + ch_gtf = GUNZIP_GTF ( [ [:], file(gtf_file) ] ).gunzip.map { it[1] } ch_versions = ch_versions.mix(GUNZIP_GTF.out.versions) } else { - ch_gtf = Channel.value( file(params.gtf) ) + ch_gtf = Channel.value( file(gtf_file) ) } } From a71348fe887a3798d07a391a7c42f53973e308d8 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:44:43 +0000 Subject: [PATCH 074/147] correct values parsing --- modules/local/templates/mtx_to_h5ad_alevin.py | 2 +- modules/local/templates/mtx_to_h5ad_cellranger.py | 2 +- modules/local/templates/mtx_to_h5ad_kallisto.py | 2 +- modules/local/templates/mtx_to_h5ad_star.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index d36d8797..83cbd946 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -67,7 +67,7 @@ def input_to_adata( # index are gene IDs and symbols are a column # TODO: how to get gene_symbols for alevin? adata.var['gene_versions'] = adata.var.index - adata.var.index = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values adata.var_names_make_unique() # write results diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index 4a40201d..76ec4998 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -69,7 +69,7 @@ def input_to_adata( # standard format # index are gene IDs and symbols are a column adata.var['gene_versions'] = adata.var.index - adata.var.index = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values adata.var_names_make_unique() # write results diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index ac71fc42..0fc606ba 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -77,7 +77,7 @@ def input_to_adata( # standard format # index are gene IDs and symbols are a column adata.var['gene_versions'] = adata.var.index - adata.var.index = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values adata.var_names_make_unique() # write results diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index aa0cd02f..b4c71e9e 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -64,7 +64,7 @@ def input_to_adata( # index are gene IDs and symbols are a column adata.var["gene_symbol"] = adata.var.index adata.var['gene_versions'] = adata.var["gene_ids"] - adata.var.index = adata.var['gene_versions'].str.split('.').str[0] + adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values # write results adata.write_h5ad(f"{output}") From d2f9cfdfa0c1af8e0c14f1b99dd7a9e071ff7452 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 09:50:20 +0000 Subject: [PATCH 075/147] fix container registry --- modules/local/anndatar_convert.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index d6a8271d..8fb6dac3 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -3,7 +3,7 @@ process ANNDATAR_CONVERT { label 'process_medium' - container "docker://fmalmeida/anndatar:dev" // TODO: Fix + container "docker.io/fmalmeida/anndatar:dev" // TODO: Fix input: tuple val(meta), path(h5ad) From 5c31226a18f9d57d9f6d5971b480095f81d66a4c Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:13:49 +0000 Subject: [PATCH 076/147] do not save versions files --- conf/modules.config | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index a6fc3411..4a230680 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -80,13 +80,15 @@ if(params.aligner == "cellranger") { withName: CELLRANGER_MKREF { publishDir = [ path: "${params.outdir}/${params.aligner}/mkref", - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } withName: CELLRANGER_COUNT { publishDir = [ path: "${params.outdir}/${params.aligner}/count", - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] ext.args = {"--chemistry ${meta.chemistry} --create-bam true " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} time = { check_max( 240.h * task.attempt, 'time' ) } @@ -183,20 +185,20 @@ if (params.aligner == "alevin") { if (params.aligner == "star") { process { - withName: STAR_ALIGN { - ext.args = "--readFilesCommand zcat --runDirPerm All_RWX --outWigType bedGraph --twopassMode Basic --outSAMtype BAM SortedByCoordinate" - } withName: STAR_GENOMEGENERATE { publishDir = [ path: { "${params.outdir}/${params.aligner}/genome_generate" }, mode: params.publish_dir_mode, - enabled: params.save_reference + enabled: params.save_reference, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } withName: STAR_ALIGN { + ext.args = "--readFilesCommand zcat --runDirPerm All_RWX --outWigType bedGraph --twopassMode Basic --outSAMtype BAM SortedByCoordinate" publishDir = [ path: { "${params.outdir}/${params.aligner}/${meta.id}" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } } @@ -208,14 +210,16 @@ if (params.aligner == 'kallisto') { publishDir = [ path: { "${params.outdir}/${params.aligner}" }, mode: params.publish_dir_mode, - enabled: params.save_reference + enabled: params.save_reference, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } withName: KALLISTOBUSTOOLS_COUNT { def kb_filter = (params.kb_filter) ? '--filter' : '' publishDir = [ path: { "${params.outdir}/${params.aligner}" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] ext.args = "--workflow ${params.kb_workflow} ${kb_filter}" } @@ -254,7 +258,8 @@ if (params.aligner == 'cellrangermulti') { withName: CELLRANGER_MKVDJREF { publishDir = [ path: "${params.outdir}/${params.aligner}/mkvdjref", - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } } From 82784295364ffc436afd2cb7ac35258f59763efa Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:13:59 +0000 Subject: [PATCH 077/147] also convert concat h5ads --- modules/local/concat_h5ad.nf | 2 +- subworkflows/local/mtx_conversion.nf | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/local/concat_h5ad.nf b/modules/local/concat_h5ad.nf index 41310553..393a7353 100644 --- a/modules/local/concat_h5ad.nf +++ b/modules/local/concat_h5ad.nf @@ -11,7 +11,7 @@ process CONCAT_H5AD { path samplesheet output: - path "*.h5ad", emit: h5ad + tuple val(meta), path("*.h5ad"), emit: h5ad when: task.ext.when == null || task.ext.when diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf index 55bd68dc..9891536d 100644 --- a/subworkflows/local/mtx_conversion.nf +++ b/subworkflows/local/mtx_conversion.nf @@ -56,13 +56,6 @@ workflow MTX_CONVERSION { } - // - // MODULE: Convert to Rds with AnndataR package - // - ANNDATAR_CONVERT ( - ch_h5ads - ) - // // Concat sample-specific h5ad in one // @@ -78,6 +71,18 @@ workflow MTX_CONVERSION { ch_concat_h5ad_input, samplesheet ) + ch_h5ad_concat = CONCAT_H5AD.out.h5ad.map{ meta, file -> + def meta_clone = meta.clone() + meta_clone.id = 'combined' // maintain output prefix + [ meta_clone, file ] + } + + // + // MODULE: Convert to Rds with AnndataR package + // + ANNDATAR_CONVERT ( + ch_h5ads.mix( ch_h5ad_concat ) + ) //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) From 2b199d053d011c785b71d45cb1d04ba8eaf2f440 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:23:02 +0000 Subject: [PATCH 078/147] manage subdirectory in publishDir --- conf/modules.config | 6 +++++- modules/local/anndatar_convert.nf | 2 +- modules/local/mtx_to_h5ad.nf | 4 ++-- modules/local/templates/anndatar_convert.R | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 4a230680..ed0c1b92 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -53,7 +53,11 @@ process { publishDir = [ path: { "${params.outdir}/${params.aligner}/mtx_conversions" }, mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + saveAs: { filename -> + if (!filename.contains('combined_')) { "${meta.id}/${filename}" } + else if (filename.equals('versions.yml')) { null } + else filename + } ] } diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index 8fb6dac3..12a2b1b4 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -9,7 +9,7 @@ process ANNDATAR_CONVERT { tuple val(meta), path(h5ad) output: - tuple val(meta), path("${meta.id}/${meta.id}_${meta.input_type}_matrix.Rds"), emit: rds + tuple val(meta), path("${meta.id}_${meta.input_type}_matrix.Rds"), emit: rds when: task.ext.when == null || task.ext.when diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index 751cf9a5..ea56c7ae 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -13,8 +13,8 @@ process MTX_TO_H5AD { path star_index output: - tuple val(meta), path("${meta.id}/*h5ad"), emit: h5ad - path "versions.yml" , emit: versions + tuple val(meta), path("${meta.id}_${meta.input_type}_matrix.h5ad"), emit: h5ad + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R index 06a723d7..5be46163 100755 --- a/modules/local/templates/anndatar_convert.R +++ b/modules/local/templates/anndatar_convert.R @@ -13,4 +13,4 @@ obj <- adata\$to_Seurat() # save files dir.create(file.path("$meta.id"), showWarnings = FALSE) -saveRDS(obj, file = "${meta.id}/${meta.id}_${meta.input_type}_matrix.Rds") +saveRDS(obj, file = "${meta.id}_${meta.input_type}_matrix.Rds") From cb677974e480ef386349efc70f7f4ac79b7bb75b Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:24:35 +0000 Subject: [PATCH 079/147] match template scripts with new publishDir --- modules/local/templates/mtx_to_h5ad_alevin.py | 2 +- modules/local/templates/mtx_to_h5ad_cellranger.py | 2 +- modules/local/templates/mtx_to_h5ad_kallisto.py | 4 ++-- modules/local/templates/mtx_to_h5ad_star.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index 83cbd946..d54a1667 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -84,7 +84,7 @@ def input_to_adata( # input_type comes from NF module input_to_adata( input_data="${meta.id}_alevin_results/af_quant/alevin/", - output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + output="${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index 76ec4998..ecc5c077 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -91,6 +91,6 @@ def input_to_adata( # input_type comes from NF module adata = input_to_adata( input_data="${meta.input_type}_feature_bc_matrix.h5", - output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + output="${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index 0fc606ba..37f66c5f 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -97,7 +97,7 @@ def input_to_adata( matrix=glob.glob("${inputs}/*.mtx")[0], barcodes=glob.glob("${inputs}/*.barcodes.txt")[0], features=glob.glob("${inputs}/*.genes.txt")[0], - output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + output="${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}", t2g="${txp2gene}" ) @@ -108,7 +108,7 @@ def input_to_adata( matrix=glob.glob("${inputs}/" + f"{type}*.mtx")[0], barcodes=glob.glob("${inputs}/" + f"{type}*.barcodes.txt")[0], features=glob.glob("${inputs}/" + f"{type}*.genes.txt")[0], - output="${meta.id}/${meta.id}_${meta.input_type}" + f"_{type}_matrix.h5ad", + output="${meta.id}_${meta.input_type}" + f"_{type}_matrix.h5ad", sample="${meta.id}", t2g="${txp2gene}" ) diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index b4c71e9e..e44d2478 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -80,7 +80,7 @@ def input_to_adata( # input_type comes from NF module input_to_adata( input_data="${meta.input_type}", - output="${meta.id}/${meta.id}_${meta.input_type}_matrix.h5ad", + output="${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) From 7230095677ba87f5325ed7bd4d853b00ee4255fe Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:31:25 +0000 Subject: [PATCH 080/147] have outputs separated --- subworkflows/local/starsolo.nf | 14 +++++--------- workflows/scrnaseq.nf | 4 +++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index d69ce9d9..4cbf7aea 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -54,16 +54,12 @@ workflow STARSOLO { ) ch_versions = ch_versions.mix(STAR_ALIGN.out.versions) - // generate channel of star counts with correct metadata - ch_star_counts = - STAR_ALIGN.out.raw_counts.map{ meta, files -> [meta + [input_type: 'raw'], files] } - .mix( STAR_ALIGN.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ) - - emit: ch_versions // get rid of meta for star index - star_result = STAR_ALIGN.out.tab - star_counts = ch_star_counts - for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } + star_result = STAR_ALIGN.out.tab + star_counts = STAR_ALIGN.out.counts + raw_counts = STAR_ALIGN.out.raw_counts + filtered_counts = STAR_ALIGN.out.filtered_counts + for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 61401185..f7bbe692 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -170,7 +170,9 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) - ch_mtx_matrices = STARSOLO.out.star_counts + ch_mtx_matrices = + STARSOLO.out.raw_counts.map{ meta, files -> [meta + [input_type: 'raw'], files] } + .mix( STARSOLO.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ) } // Run cellranger pipeline From 206a7c1d8f50914d70a873bf66dfd184219d17f5 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:36:48 +0000 Subject: [PATCH 081/147] make parsing inside sub-workflow --- subworkflows/local/starsolo.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subworkflows/local/starsolo.nf b/subworkflows/local/starsolo.nf index 4cbf7aea..aadda6b6 100644 --- a/subworkflows/local/starsolo.nf +++ b/subworkflows/local/starsolo.nf @@ -59,7 +59,7 @@ workflow STARSOLO { // get rid of meta for star index star_result = STAR_ALIGN.out.tab star_counts = STAR_ALIGN.out.counts - raw_counts = STAR_ALIGN.out.raw_counts - filtered_counts = STAR_ALIGN.out.filtered_counts + raw_counts = STAR_ALIGN.out.raw_counts.map{ meta, files -> [meta + [input_type: 'raw'], files] } + filtered_counts = STAR_ALIGN.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } for_multiqc = STAR_ALIGN.out.log_final.map{ meta, it -> it } } From c4a09e07baba8b2a1176e6e8429a3f133128cd55 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:43:28 +0000 Subject: [PATCH 082/147] added kallisto to correct structure --- subworkflows/local/kallisto_bustools.nf | 4 +++- workflows/scrnaseq.nf | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/subworkflows/local/kallisto_bustools.nf b/subworkflows/local/kallisto_bustools.nf index d6b171a6..5f8d9bcc 100644 --- a/subworkflows/local/kallisto_bustools.nf +++ b/subworkflows/local/kallisto_bustools.nf @@ -66,7 +66,9 @@ workflow KALLISTO_BUSTOOLS { emit: ch_versions - counts = ch_raw_counts.mix (ch_filtered_counts) + counts = KALLISTOBUSTOOLS_COUNT.out.count + counts_raw = ch_raw_counts + counts_filtered = ch_filtered_counts txp2gene = txp2gene.collect() } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index f7bbe692..070565f4 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -135,7 +135,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(KALLISTO_BUSTOOLS.out.ch_versions) - ch_mtx_matrices = KALLISTO_BUSTOOLS.out.counts + ch_mtx_matrices = KALLISTO_BUSTOOLS.out.counts_raw.mix( KALLISTO_BUSTOOLS.out.counts_filtered ) ch_txp2gene = KALLISTO_BUSTOOLS.out.txp2gene } @@ -170,9 +170,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) - ch_mtx_matrices = - STARSOLO.out.raw_counts.map{ meta, files -> [meta + [input_type: 'raw'], files] } - .mix( STARSOLO.out.filtered_counts.map{ meta, files -> [meta + [input_type: 'filtered'], files] } ) + ch_mtx_matrices = STARSOLO.out.raw_counts.mix( STARSOLO.out.filtered_counts ) } // Run cellranger pipeline From 971667b454be7960ccbc57a9eb5bef7239fa6beb Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:47:33 +0000 Subject: [PATCH 083/147] correct mix of channels --- subworkflows/local/align_cellranger.nf | 7 ++++--- workflows/scrnaseq.nf | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/subworkflows/local/align_cellranger.nf b/subworkflows/local/align_cellranger.nf index 86b69a38..d787e0f0 100644 --- a/subworkflows/local/align_cellranger.nf +++ b/subworkflows/local/align_cellranger.nf @@ -63,7 +63,8 @@ workflow CELLRANGER_ALIGN { emit: ch_versions - cellranger_out = CELLRANGER_COUNT.out.outs - cellranger_matrices = ch_matrices_raw.mix( ch_matrices_filtered ) - star_index = cellranger_index + cellranger_out = CELLRANGER_COUNT.out.outs + cellranger_matrices_raw = ch_matrices_raw + cellranger_matrices_filtered = ch_matrices_filtered + star_index = cellranger_index } diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 070565f4..20776d6b 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -135,7 +135,7 @@ workflow SCRNASEQ { ch_fastq ) ch_versions = ch_versions.mix(KALLISTO_BUSTOOLS.out.ch_versions) - ch_mtx_matrices = KALLISTO_BUSTOOLS.out.counts_raw.mix( KALLISTO_BUSTOOLS.out.counts_filtered ) + ch_mtx_matrices = ch_mtx_matrices.mix( KALLISTO_BUSTOOLS.out.counts_raw, KALLISTO_BUSTOOLS.out.counts_filtered ) ch_txp2gene = KALLISTO_BUSTOOLS.out.txp2gene } @@ -153,7 +153,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(SCRNASEQ_ALEVIN.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(SCRNASEQ_ALEVIN.out.alevin_results.map{ meta, it -> it }) - ch_mtx_matrices = ch_mtx_matrices.mix(SCRNASEQ_ALEVIN.out.alevin_results) + ch_mtx_matrices = ch_mtx_matrices.mix( SCRNASEQ_ALEVIN.out.alevin_results ) } // Run STARSolo pipeline @@ -170,7 +170,7 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(STARSOLO.out.ch_versions) ch_multiqc_files = ch_multiqc_files.mix(STARSOLO.out.for_multiqc) - ch_mtx_matrices = STARSOLO.out.raw_counts.mix( STARSOLO.out.filtered_counts ) + ch_mtx_matrices = ch_mtx_matrices.mix( STARSOLO.out.raw_counts, STARSOLO.out.filtered_counts ) } // Run cellranger pipeline @@ -183,7 +183,7 @@ workflow SCRNASEQ { protocol_config['protocol'] ) ch_versions = ch_versions.mix(CELLRANGER_ALIGN.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_ALIGN.out.cellranger_matrices) + ch_mtx_matrices = ch_mtx_matrices.mix( CELLRANGER_ALIGN.out.cellranger_matrices_raw, CELLRANGER_ALIGN.out.cellranger_matrices_filtered ) ch_multiqc_files = ch_multiqc_files.mix(CELLRANGER_ALIGN.out.cellranger_out.map { meta, outs -> outs.findAll{ it -> it.name == "web_summary.html"} }) From 81221135336857206c7eaa24cd58dfc1c1b17810 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 10:49:17 +0000 Subject: [PATCH 084/147] correct for cellranger multi --- subworkflows/local/align_cellrangermulti.nf | 5 +++-- workflows/scrnaseq.nf | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/subworkflows/local/align_cellrangermulti.nf b/subworkflows/local/align_cellrangermulti.nf index 977bf478..f13c7bf1 100644 --- a/subworkflows/local/align_cellrangermulti.nf +++ b/subworkflows/local/align_cellrangermulti.nf @@ -204,8 +204,9 @@ workflow CELLRANGER_MULTI_ALIGN { emit: ch_versions - cellrangermulti_out = CELLRANGER_MULTI.out.outs - cellrangermulti_mtx = ch_matrices_raw.mix( ch_matrices_filtered ) + cellrangermulti_out = CELLRANGER_MULTI.out.outs + cellrangermulti_mtx_raw = ch_matrices_raw + cellrangermulti_mtx_filtered = ch_matrices_filtered } def parse_demultiplexed_output_channels(in_ch, pattern) { diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 20776d6b..418f6711 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -281,7 +281,7 @@ workflow SCRNASEQ { ch_multiqc_files = ch_multiqc_files.mix( CELLRANGER_MULTI_ALIGN.out.cellrangermulti_out.map{ meta, outs -> outs.findAll{ it -> it.name == "web_summary.html" } }) - ch_mtx_matrices = ch_mtx_matrices.mix(CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx) + ch_mtx_matrices = ch_mtx_matrices.mix( CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx_raw, CELLRANGER_MULTI_ALIGN.out.cellrangermulti_mtx_filtered ) } From 44d1cb745c0d2907a97b992df0f217313ad8333b Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 11:04:24 +0000 Subject: [PATCH 085/147] correct stub --- modules/local/mtx_to_h5ad.nf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index ea56c7ae..27e1070a 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -26,8 +26,7 @@ process MTX_TO_H5AD { stub: """ - mkdir ${meta.id} - touch ${meta.id}/${meta.id}_matrix.h5ad + touch ${meta.id}_raw_matrix.h5ad touch versions.yml """ } From 18cfdd7571ddb80234ff7e6ed2c58f6360605292 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 11:04:53 +0000 Subject: [PATCH 086/147] remove glob in txp2gene --- modules/local/templates/mtx_to_h5ad_kallisto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index 37f66c5f..8d0f0909 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -25,7 +25,7 @@ def _mtx_to_adata( adata.var_names = pd.read_csv(features, header=None, sep="\\t")[0].values adata.obs["sample"] = sample - txp2gene = pd.read_table(glob.glob(f"{t2g}")[0], header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) + txp2gene = pd.read_table(f"{t2g}", header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) txp2gene = txp2gene.drop_duplicates(subset="gene_id").set_index("gene_id") adata.var = adata.var.join(txp2gene, how="left") From 079bb7ec7a7dd5f4a2009463106e785fabbdd178 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Wed, 30 Oct 2024 11:15:13 +0000 Subject: [PATCH 087/147] added small comment to local modules --- modules/local/adata_barcodes.nf | 6 ++++++ modules/local/alevinqc.nf | 5 +++++ modules/local/anndatar_convert.nf | 5 +++++ modules/local/concat_h5ad.nf | 6 ++++++ modules/local/gffread_transcriptome.nf | 5 +++++ modules/local/gtf_gene_filter.nf | 5 +++++ modules/local/mtx_to_h5ad.nf | 5 +++++ modules/local/parse_cellrangermulti_samplesheet.nf | 5 +++++ modules/local/simpleaf_index.nf | 5 +++++ modules/local/simpleaf_quant.nf | 5 +++++ modules/local/star_align.nf | 5 +++++ 11 files changed, 57 insertions(+) diff --git a/modules/local/adata_barcodes.nf b/modules/local/adata_barcodes.nf index 2aef8ec9..630d90ae 100644 --- a/modules/local/adata_barcodes.nf +++ b/modules/local/adata_barcodes.nf @@ -1,4 +1,10 @@ process ADATA_BARCODES { + + // + // Module from nf-core/scdownstream. + // This module performs the subset of the h5ad file to only contain barcodes that passed emptydrops filter with cellbender + // + tag "$meta.id" label 'process_single' diff --git a/modules/local/alevinqc.nf b/modules/local/alevinqc.nf index 9000d79e..777a1371 100644 --- a/modules/local/alevinqc.nf +++ b/modules/local/alevinqc.nf @@ -1,4 +1,9 @@ process ALEVINQC { + + // + // This module executes alevinfry QC reporting tool on alevin results + // + tag "$meta.id" label 'process_low' diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index 12a2b1b4..8e4c242e 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -1,4 +1,9 @@ process ANNDATAR_CONVERT { + + // + // This module uses the anndata R package to convert h5ad files in different formats + // + tag "${meta.id}" label 'process_medium' diff --git a/modules/local/concat_h5ad.nf b/modules/local/concat_h5ad.nf index 393a7353..c875ba3c 100644 --- a/modules/local/concat_h5ad.nf +++ b/modules/local/concat_h5ad.nf @@ -1,4 +1,10 @@ process CONCAT_H5AD { + + // + // This module concatenates all h5ad, per type (raw, filtered, etc.) files generated during pipeline execution + // + + tag "${meta.id}" label 'process_medium' diff --git a/modules/local/gffread_transcriptome.nf b/modules/local/gffread_transcriptome.nf index ab573b07..671b6726 100644 --- a/modules/local/gffread_transcriptome.nf +++ b/modules/local/gffread_transcriptome.nf @@ -1,4 +1,9 @@ process GFFREAD_TRANSCRIPTOME { + + // + // This module uses gffread to filter input to generate a transcripts fasta + // + tag "${genome_fasta}" label 'process_low' diff --git a/modules/local/gtf_gene_filter.nf b/modules/local/gtf_gene_filter.nf index 063bd228..10af352b 100644 --- a/modules/local/gtf_gene_filter.nf +++ b/modules/local/gtf_gene_filter.nf @@ -1,4 +1,9 @@ process GTF_GENE_FILTER { + + // + // This module executes a custom script to filter input gtf to contain only annotations present in input genome + // + tag "$fasta" label 'process_low' diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index 27e1070a..f54318f1 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -1,4 +1,9 @@ process MTX_TO_H5AD { + + // + // This module executes different conversion template scripts (per aligner) for converting output mtx files into h5ad files + // + tag "$meta.id" label 'process_medium' diff --git a/modules/local/parse_cellrangermulti_samplesheet.nf b/modules/local/parse_cellrangermulti_samplesheet.nf index df616995..e8f56b67 100644 --- a/modules/local/parse_cellrangermulti_samplesheet.nf +++ b/modules/local/parse_cellrangermulti_samplesheet.nf @@ -1,4 +1,9 @@ process PARSE_CELLRANGERMULTI_SAMPLESHEET { + + // + // This module contains a custom script for checking special cellranger multi samplesheet + // + label 'process_low' publishDir = [ enabled: false ] diff --git a/modules/local/simpleaf_index.nf b/modules/local/simpleaf_index.nf index 8e8bd519..5c362c99 100644 --- a/modules/local/simpleaf_index.nf +++ b/modules/local/simpleaf_index.nf @@ -1,4 +1,9 @@ process SIMPLEAF_INDEX { + + // + // This module executes simpleaf to generate alevin genome index + // + tag "$transcript_gtf" label "process_medium" diff --git a/modules/local/simpleaf_quant.nf b/modules/local/simpleaf_quant.nf index abb58404..9241b210 100644 --- a/modules/local/simpleaf_quant.nf +++ b/modules/local/simpleaf_quant.nf @@ -1,4 +1,9 @@ process SIMPLEAF_QUANT { + + // + // This module executes simpleaf to perform quantification with alevin + // + tag "$meta.id" label 'process_high' diff --git a/modules/local/star_align.nf b/modules/local/star_align.nf index 4b3df1e1..70d6770c 100644 --- a/modules/local/star_align.nf +++ b/modules/local/star_align.nf @@ -1,4 +1,9 @@ process STAR_ALIGN { + + // + // This module executes STAR align quantification + // + tag "$meta.id" label 'process_high' From 36eeee39339446e84a4d5362ff47562ff9fe5441 Mon Sep 17 00:00:00 2001 From: kopichris Date: Sat, 2 Nov 2024 01:17:38 +0800 Subject: [PATCH 088/147] feat: Create single boolean parameter allowing users to control how alignment files are saved for CellRanger and STAR --- conf/modules.config | 25 +++++++++++++++++++------ nextflow.config | 3 +-- nextflow_schema.json | 15 +++++---------- workflows/scrnaseq.nf | 2 +- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 15073a21..502f00ff 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -82,7 +82,7 @@ if(params.aligner == "cellranger") { path: "${params.outdir}/${params.aligner}/count", mode: params.publish_dir_mode ] - ext.args = {"--chemistry ${meta.chemistry} --create-bam ${params.cellranger_create_bam}" + " " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} + ext.args = {"--chemistry ${meta.chemistry} --create-bam ${params.save_align_intermeds}" + " " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} time = { check_max( 240.h * task.attempt, 'time' ) } } } @@ -186,11 +186,24 @@ if (params.aligner == "star") { enabled: params.save_reference ] } - withName: STAR_ALIGN { - publishDir = [ - path: { "${params.outdir}/${params.aligner}/${meta.id}" }, - mode: params.publish_dir_mode - ] + + if(params.save_align_intermeds) { + withName: 'STAR_ALIGN' { + publishDir = [ + path: { "${params.outdir}/${params.aligner}/${meta.id}" }, + mode: params.publish_dir_mode + ] + } + } + else { + withName: 'STAR_ALIGN' { + publishDir = [ + path: { "${params.outdir}/${params.aligner}/${meta.id}" }, + mode: params.publish_dir_mode, + pattern: '*', + saveAs: { it.endsWith('.bam') ? null : it } + ] + } } } } diff --git a/nextflow.config b/nextflow.config index 7a5a6c76..0e8f3399 100644 --- a/nextflow.config +++ b/nextflow.config @@ -13,6 +13,7 @@ params { aligner = 'alevin' input = null save_reference = false + save_align_intermeds = true protocol = 'auto' // reference files @@ -43,7 +44,6 @@ params { // Cellranger parameters skip_cellranger_renaming = false cellranger_index = null - cellranger_create_bam = true // Cellranger ARC parameters motifs = null @@ -66,7 +66,6 @@ params { vdj_inner_enrichment_primers = null gex_barcode_sample_assignment = null cellranger_multi_barcodes = null - cellranger_multi_create_bam = true // Template Boilerplate options diff --git a/nextflow_schema.json b/nextflow_schema.json index f582e934..07e53e4c 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -142,6 +142,11 @@ "description": "Specify this parameter to save the indices created (STAR, Kallisto, Salmon) to the results.", "fa_icon": "fas fa-bookmark" }, + "save_align_intermeds": { + "type": "boolean", + "description": "Specify this parameter to save the intermediate alignment files (STAR, CellRanger) to the results.", + "fa_icon": "fas fa-bookmark" + }, "igenomes_base": { "type": "string", "format": "directory-path", @@ -270,11 +275,6 @@ "skip_cellranger_renaming": { "type": "boolean", "description": "Should it skip the automatic renaming included in cellranger-related modules?" - }, - "cellranger_create_bam": { - "type": "boolean", - "description": "Create BAM alignment file. Note: Using the default value of true will increase computational resources and disk space. Please see the [10x documentation about create-bam parameter](https://kb.10xgenomics.com/hc/en-us/articles/24292856143885-Understanding-the-create-bam-Parameter-in-Cell-Ranger-v8-0) for more information.", - "default": true } } }, @@ -371,11 +371,6 @@ "exists": true, "mimetype": "text/csv", "description": "Additional samplesheet to provide information about multiplexed samples. See the 'Usage' section for more details." - }, - "cellranger_multi_create_bam": { - "type": "boolean", - "description": "Create BAM alignment file. Note: Using the default value of true will increase computational resources and disk space. Please see the [10x documentation about create-bam parameter](https://kb.10xgenomics.com/hc/en-us/articles/24292856143885-Understanding-the-create-bam-Parameter-in-Cell-Ranger-v8-0) for more information.", - "default": true } } }, diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 94bbb844..2a0ea5d2 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -237,7 +237,7 @@ workflow SCRNASEQ { // add cellranger options that are currently handled by pipeline, coming from samplesheet // the module parses them from the 'gex' options if (meta.feature_type.toString() == 'gex') { - parsed_meta.options['create-bam'] = params.cellranger_multi_create_bam // force bam creation -- param required by cellranger multi + parsed_meta.options['create-bam'] = params.save_align_intermeds // force bam creation -- param required by cellranger multi if (meta.expected_cells) { parsed_meta.options['expected-cells'] = meta.expected_cells } } From 128b18333c22ac2f5cad61d36d203b93a1312814 Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Sat, 2 Nov 2024 15:23:22 +0000 Subject: [PATCH 089/147] Add pre-built cellranger, cellranger-arc simpleaf --- workflows/scrnaseq.nf | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 120a19d5..f41752ec 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -37,9 +37,14 @@ workflow SCRNASEQ { error "Only cellranger supports `protocol = 'auto'`. Please specify the protocol manually!" } - params.fasta = getGenomeAttribute('fasta') - params.gtf = getGenomeAttribute('gtf') - params.star_index = getGenomeAttribute('star') + // collecting paths from genome attributes (optional) + params.fasta = getGenomeAttribute('fasta') + params.gtf = getGenomeAttribute('gtf') + params.star_index = getGenomeAttribute('star') + params.salmon_index = getGenomeAttribute('simpleaf') + params.txp2gene = getGenomeAttribute('simpleaf_tx2pgene') + params.cellranger_index = getGenomeAttribute('cellranger') + params.cellranger_index = getGenomeAttribute('cellrangerarc') ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] From dd566c96819f7c2123665d0cb4dc46a967ec2b27 Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Sat, 2 Nov 2024 15:39:17 +0000 Subject: [PATCH 090/147] Added pre-build iundex support to Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e6450ec..888e8852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Added support for pre-built indexes in `genomes.config` file for `cellranger`, `cellranger-arc`, `simpleaf` and `simpleaf txp2gene` ([#371](https://github.com/nf-core/scrnaseq/issues/371)) + ## v2.7.1 - 2024-08-13 - Fix that tests have not been executed with nf-test v0.9 ([#359](https://github.com/nf-core/scrnaseq/pull/359)) From 952a24c038d016d364833abe629a15bc457bda80 Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Sat, 2 Nov 2024 15:40:56 +0000 Subject: [PATCH 091/147] better comment --- workflows/scrnaseq.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index f41752ec..12f6e3b8 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -37,7 +37,7 @@ workflow SCRNASEQ { error "Only cellranger supports `protocol = 'auto'`. Please specify the protocol manually!" } - // collecting paths from genome attributes (optional) + // collect paths from genome attributes file (e.g. iGenomes.config; optional) params.fasta = getGenomeAttribute('fasta') params.gtf = getGenomeAttribute('gtf') params.star_index = getGenomeAttribute('star') From d2478c1cf45dd03850ab245763e5c445e865c9f4 Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Mon, 4 Nov 2024 20:21:09 +0000 Subject: [PATCH 092/147] make cellranger arc index conditional --- workflows/scrnaseq.nf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 12f6e3b8..1e30b890 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -44,7 +44,10 @@ workflow SCRNASEQ { params.salmon_index = getGenomeAttribute('simpleaf') params.txp2gene = getGenomeAttribute('simpleaf_tx2pgene') params.cellranger_index = getGenomeAttribute('cellranger') - params.cellranger_index = getGenomeAttribute('cellrangerarc') + // Make cellranger-arc index overwriting conditional + if (params.aligner == "cellrangerarc") { + params.cellranger_index = getGenomeAttribute('cellrangerarc') + } ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] From 613a4c5ebbc3eb1c4f187fb202af49c460ca168b Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Tue, 5 Nov 2024 09:28:57 +0000 Subject: [PATCH 093/147] cleaner cellranger index declaration --- workflows/scrnaseq.nf | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 1e30b890..ceb60b7e 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -43,11 +43,14 @@ workflow SCRNASEQ { params.star_index = getGenomeAttribute('star') params.salmon_index = getGenomeAttribute('simpleaf') params.txp2gene = getGenomeAttribute('simpleaf_tx2pgene') - params.cellranger_index = getGenomeAttribute('cellranger') - // Make cellranger-arc index overwriting conditional - if (params.aligner == "cellrangerarc") { - params.cellranger_index = getGenomeAttribute('cellrangerarc') + + // Make cellranger or cellranger-arc index conditional + if (params.aligner in ["cellranger", "cellrangermulti"]){ + params.cellranger_index = getGenomeAttribute('cellranger') } + else if (params.aligner == "cellrangerarc") { + params.cellranger_index = getGenomeAttribute('cellrangerarc') + } ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] From 8c439095ee12c2f04f13aaaa277514954da56eca Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Tue, 5 Nov 2024 16:26:22 +0000 Subject: [PATCH 094/147] removed trailing whitespace --- workflows/scrnaseq.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index ceb60b7e..8e83d8ff 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -50,7 +50,7 @@ workflow SCRNASEQ { } else if (params.aligner == "cellrangerarc") { params.cellranger_index = getGenomeAttribute('cellrangerarc') - } + } ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] From d68c88337b86c4ce1ba13df9fda9cd8804e78fdc Mon Sep 17 00:00:00 2001 From: Felix Krueger Date: Tue, 5 Nov 2024 19:39:07 +0000 Subject: [PATCH 095/147] fixed space --- workflows/scrnaseq.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 8e83d8ff..ae2249c7 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -43,7 +43,7 @@ workflow SCRNASEQ { params.star_index = getGenomeAttribute('star') params.salmon_index = getGenomeAttribute('simpleaf') params.txp2gene = getGenomeAttribute('simpleaf_tx2pgene') - + // Make cellranger or cellranger-arc index conditional if (params.aligner in ["cellranger", "cellrangermulti"]){ params.cellranger_index = getGenomeAttribute('cellranger') From bf1a9df2004bac856008a0a073e054bc83b2aeae Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 6 Nov 2024 19:09:32 +0800 Subject: [PATCH 096/147] Remove check_max function in cellranger_count block Co-authored-by: Gregor Sturm --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index dd65957a..c049e5b3 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -82,7 +82,7 @@ if(params.aligner == "cellranger") { mode: params.publish_dir_mode ] ext.args = {"--chemistry ${meta.chemistry} --create-bam ${params.save_align_intermeds}" + " " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} - time = { check_max( 240.h * task.attempt, 'time' ) } + time = { 240.h * task.attempt, 'time' } } } } From 2cfe0df9fdff0f84abc45ad3643572f171654da5 Mon Sep 17 00:00:00 2001 From: kopichris Date: Fri, 8 Nov 2024 12:59:12 +0800 Subject: [PATCH 097/147] chore: remove string from time property in cellranger count module --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index c049e5b3..d00278a6 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -82,7 +82,7 @@ if(params.aligner == "cellranger") { mode: params.publish_dir_mode ] ext.args = {"--chemistry ${meta.chemistry} --create-bam ${params.save_align_intermeds}" + " " + (meta.expected_cells ? "--expect-cells ${meta.expected_cells}" : '')} - time = { 240.h * task.attempt, 'time' } + time = { 240.h * task.attempt } } } } From a0bf7ab527d1009f6c3c9f6999b16ec44c3ec16a Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 15 Nov 2024 09:16:59 +0000 Subject: [PATCH 098/147] split mtx to h5ad conversion and general conversion, so h5ad is generated separately for all aligners, and then passed for format conversion, custom filter and concat. --- subworkflows/local/h5ad_conversion.nf | 49 ++++++++++++++ subworkflows/local/mtx_conversion.nf | 94 --------------------------- workflows/scrnaseq.nf | 91 +++++++++++++++++++------- 3 files changed, 117 insertions(+), 117 deletions(-) create mode 100644 subworkflows/local/h5ad_conversion.nf delete mode 100644 subworkflows/local/mtx_conversion.nf diff --git a/subworkflows/local/h5ad_conversion.nf b/subworkflows/local/h5ad_conversion.nf new file mode 100644 index 00000000..2e7888ba --- /dev/null +++ b/subworkflows/local/h5ad_conversion.nf @@ -0,0 +1,49 @@ +/* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ +include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' +include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' + +workflow H5AD_CONVERSION { + + take: + ch_h5ads + samplesheet + + main: + ch_versions = Channel.empty() + + // + // Concat sample-specific h5ad in one + // + ch_concat_h5ad_input = ch_h5ads.groupTuple() // gather all sample-specific files / per type + if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { + // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) + // which nextflow break because it is not a valid 'path' thus, we have to remove one level + // making it as [ mtx_1, mtx_2 ] + ch_concat_h5ad_input = ch_concat_h5ad_input.map{ type, matrices -> [ type, matrices.flatten().toList() ] } + } + + CONCAT_H5AD ( + ch_concat_h5ad_input, + samplesheet + ) + ch_h5ad_concat = CONCAT_H5AD.out.h5ad.map{ meta, file -> + def meta_clone = meta.clone() + meta_clone.id = 'combined' // maintain output prefix + [ meta_clone, file ] + } + + // + // MODULE: Convert to Rds with AnndataR package + // + ANNDATAR_CONVERT ( + ch_h5ads.mix( ch_h5ad_concat ) + ) + + //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output + // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) + + emit: + ch_versions + h5ads = ch_h5ads + +} diff --git a/subworkflows/local/mtx_conversion.nf b/subworkflows/local/mtx_conversion.nf deleted file mode 100644 index 9891536d..00000000 --- a/subworkflows/local/mtx_conversion.nf +++ /dev/null @@ -1,94 +0,0 @@ -/* -- IMPORT LOCAL MODULES/SUBWORKFLOWS -- */ -include { MTX_TO_H5AD } from '../../modules/local/mtx_to_h5ad' -include { CONCAT_H5AD } from '../../modules/local/concat_h5ad.nf' -include { ANNDATAR_CONVERT } from '../../modules/local/anndatar_convert' -include { EMPTY_DROPLET_REMOVAL } from '../../subworkflows/local/emptydrops_removal' - -workflow MTX_CONVERSION { - - take: - mtx_matrices - txp2gene - star_index - samplesheet - - main: - ch_versions = Channel.empty() - ch_h5ads = Channel.empty() - - // - // MODULE: Convert matrices to h5ad - // - MTX_TO_H5AD ( - mtx_matrices, - txp2gene, - star_index - ) - ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) - - // fix channel size when kallisto non-standard workflow - if (params.aligner == 'kallisto' && !(params.kb_workflow == 'standard')) { - ch_h5ads = - MTX_TO_H5AD.out.h5ad - .transpose() - .map { meta, h5ad -> - def meta_clone = meta.clone() - def spc_prefix = h5ad.toString().contains('unspliced') ? 'un' : '' - - meta_clone["input_type"] = "${meta.input_type}_${spc_prefix}spliced" - - [ meta_clone, h5ad ] - } - } else { - ch_h5ads = MTX_TO_H5AD.out.h5ad - } - - // - // SUBWORKFLOW: Run cellbender emptydrops filter - // - if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - - // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it - EMPTY_DROPLET_REMOVAL ( - ch_h5ads.filter { meta, mtx_files -> meta.input_type.contains('raw') } - ) - ch_h5ads = ch_h5ads.mix( EMPTY_DROPLET_REMOVAL.out.h5ad ) - - } - - // - // Concat sample-specific h5ad in one - // - ch_concat_h5ad_input = ch_h5ads.groupTuple() // gather all sample-specific files / per type - if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') { - // when having spliced / unspliced matrices, the collected tuple has two levels ( [[mtx_1, mtx_2]] ) - // which nextflow break because it is not a valid 'path' thus, we have to remove one level - // making it as [ mtx_1, mtx_2 ] - ch_concat_h5ad_input = ch_concat_h5ad_input.map{ type, matrices -> [ type, matrices.flatten().toList() ] } - } - - CONCAT_H5AD ( - ch_concat_h5ad_input, - samplesheet - ) - ch_h5ad_concat = CONCAT_H5AD.out.h5ad.map{ meta, file -> - def meta_clone = meta.clone() - meta_clone.id = 'combined' // maintain output prefix - [ meta_clone, file ] - } - - // - // MODULE: Convert to Rds with AnndataR package - // - ANNDATAR_CONVERT ( - ch_h5ads.mix( ch_h5ad_concat ) - ) - - //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output - // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) - - emit: - ch_versions - // counts = MTX_TO_H5AD.out.counts was this ever used? - -} diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 418f6711..2d48dbfb 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -1,21 +1,24 @@ -include { MULTIQC } from '../modules/nf-core/multiqc/main' -include { FASTQC_CHECK } from '../subworkflows/local/fastqc' -include { KALLISTO_BUSTOOLS } from '../subworkflows/local/kallisto_bustools' -include { SCRNASEQ_ALEVIN } from '../subworkflows/local/alevin' -include { STARSOLO } from '../subworkflows/local/starsolo' -include { CELLRANGER_ALIGN } from '../subworkflows/local/align_cellranger' -include { CELLRANGER_MULTI_ALIGN } from '../subworkflows/local/align_cellrangermulti' -include { CELLRANGERARC_ALIGN } from '../subworkflows/local/align_cellrangerarc' -include { UNIVERSC_ALIGN } from '../subworkflows/local/align_universc' -include { MTX_CONVERSION } from '../subworkflows/local/mtx_conversion' -include { GTF_GENE_FILTER } from '../modules/local/gtf_gene_filter' -include { GUNZIP as GUNZIP_FASTA } from '../modules/nf-core/gunzip/main' -include { GUNZIP as GUNZIP_GTF } from '../modules/nf-core/gunzip/main' -include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scrnaseq_pipeline' -include { paramsSummaryLog; paramsSummaryMap } from 'plugin/nf-validation' -include { getGenomeAttribute } from '../subworkflows/local/utils_nfcore_scrnaseq_pipeline' +include { MULTIQC } from '../modules/nf-core/multiqc/main' +include { FASTQC_CHECK } from '../subworkflows/local/fastqc' +include { KALLISTO_BUSTOOLS } from '../subworkflows/local/kallisto_bustools' +include { SCRNASEQ_ALEVIN } from '../subworkflows/local/alevin' +include { STARSOLO } from '../subworkflows/local/starsolo' +include { CELLRANGER_ALIGN } from '../subworkflows/local/align_cellranger' +include { CELLRANGER_MULTI_ALIGN } from '../subworkflows/local/align_cellrangermulti' +include { CELLRANGERARC_ALIGN } from '../subworkflows/local/align_cellrangerarc' +include { UNIVERSC_ALIGN } from '../subworkflows/local/align_universc' +include { MTX_TO_H5AD } from '../modules/local/mtx_to_h5ad' +include { H5AD_CONVERSION } from '../subworkflows/local/h5ad_conversion' +include { H5AD_CONVERSION as EMPTYDROPS_H5AD_CONVERSION } from '../subworkflows/local/h5ad_conversion' +include { EMPTY_DROPLET_REMOVAL } from '../subworkflows/local/emptydrops_removal.nf' +include { GTF_GENE_FILTER } from '../modules/local/gtf_gene_filter' +include { GUNZIP as GUNZIP_FASTA } from '../modules/nf-core/gunzip/main' +include { GUNZIP as GUNZIP_GTF } from '../modules/nf-core/gunzip/main' +include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scrnaseq_pipeline' +include { paramsSummaryLog; paramsSummaryMap } from 'plugin/nf-validation' +include { getGenomeAttribute } from '../subworkflows/local/utils_nfcore_scrnaseq_pipeline' @@ -285,16 +288,58 @@ workflow SCRNASEQ { } - // Run mtx to h5ad conversion subworkflow - MTX_CONVERSION ( + // + // MODULE: Convert mtx matrices to h5ad + // + MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - ch_star_index, + ch_star_index + ) + ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) + + // fix channel size when kallisto non-standard workflow + if (params.aligner == 'kallisto' && !(params.kb_workflow == 'standard')) { + ch_h5ads = + MTX_TO_H5AD.out.h5ad + .transpose() + .map { meta, h5ad -> + def meta_clone = meta.clone() + def spc_prefix = h5ad.toString().contains('unspliced') ? 'un' : '' + + meta_clone["input_type"] = "${meta.input_type}_${spc_prefix}spliced" + + [ meta_clone, h5ad ] + } + } else { + ch_h5ads = MTX_TO_H5AD.out.h5ad + } + + // + // SUBWORKFLOW: Run h5ad conversion and concatenation + // + ch_emptydrops = Channel.empty() + H5AD_CONVERSION ( + ch_h5ads, ch_input ) + ch_versions.mix(H5AD_CONVERSION.out.ch_versions) + + // + // SUBWORKFLOW: Run cellbender emptydrops filter + // + if ( !params.skip_emptydrops && !(params.aligner in ['cellrangerarc']) ) { - //Add Versions from MTX Conversion workflow too - ch_versions.mix(MTX_CONVERSION.out.ch_versions) + // emptydrops should only run on the raw matrices thus, filter-out the filtered result of the aligners that can produce it + EMPTY_DROPLET_REMOVAL ( + H5AD_CONVERSION.out.h5ads.filter { meta, mtx_files -> meta.input_type.contains('raw') } + ) + EMPTYDROPS_H5AD_CONVERSION ( + EMPTY_DROPLET_REMOVAL.out.h5ad, + ch_input + ) + + } // // Collate and save software versions From 1a18dc836accfc210fb69de86ba98ff49401048d Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Mon, 18 Nov 2024 13:37:40 +0000 Subject: [PATCH 099/147] [automated] Fix code linting --- CHANGELOG.md | 1 + nextflow_schema.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 239b53b9..cd013afa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 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). ## [Unreleased] + - Add --create-bam parameter to cellranger count and multi pipelines ([#384](https://github.com/nf-core/scrnaseq/issues/384)) ## v2.7.1 - 2024-08-13 diff --git a/nextflow_schema.json b/nextflow_schema.json index c60361f6..935d4277 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -556,4 +556,3 @@ } ] } - From 2fd1d4104b3f345e995fdb68efe3651851516a25 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 25 Nov 2024 08:36:34 +0100 Subject: [PATCH 100/147] fix star index channel generation --- workflows/scrnaseq.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 2d48dbfb..3c98a7e9 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -73,7 +73,7 @@ workflow SCRNASEQ { //star params star_index = star_index ? file(star_index, checkIfExists: true) : null - ch_star_index = star_index ? [[id: star_index.baseName], star_index] : [] + ch_star_index = star_index ? Channel.of( [[id: star_index.baseName], star_index] ) : [] star_feature = params.star_feature //cellranger params @@ -294,7 +294,7 @@ workflow SCRNASEQ { MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - ch_star_index + ch_star_index.map{it[1]} ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From b615e01fef659fa8baaf323097ab815196b2f304 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 12:55:03 +0000 Subject: [PATCH 101/147] fixed tuple size --- modules/local/mtx_to_h5ad.nf | 2 +- workflows/scrnaseq.nf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index f54318f1..eab69cf4 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -15,7 +15,7 @@ process MTX_TO_H5AD { // for each sample, the sub-folders and files come directly in array. tuple val(meta), path(inputs) path txp2gene - path star_index + tuple val(meta2), path(star_index) output: tuple val(meta), path("${meta.id}_${meta.input_type}_matrix.h5ad"), emit: h5ad diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 3c98a7e9..c118f3b6 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -73,7 +73,7 @@ workflow SCRNASEQ { //star params star_index = star_index ? file(star_index, checkIfExists: true) : null - ch_star_index = star_index ? Channel.of( [[id: star_index.baseName], star_index] ) : [] + ch_star_index = star_index ? Channel.of( [[id: star_index.baseName], star_index] ) : [[], []] star_feature = params.star_feature //cellranger params @@ -294,7 +294,7 @@ workflow SCRNASEQ { MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - ch_star_index.map{it[1]} + ch_star_index ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From ae744813b1bfaed0b60acac565701b933ce11da5 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 25 Nov 2024 14:19:17 +0100 Subject: [PATCH 102/147] Update kallisto script --- .../local/templates/mtx_to_h5ad_kallisto.py | 111 +++++++++--------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index 8d0f0909..a25289e5 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -7,29 +7,38 @@ import scanpy as sc import pandas as pd -import argparse -from anndata import AnnData +from anndata import AnnData, concat as concat_ad import platform import glob + def _mtx_to_adata( matrix: str, barcodes: str, features: str, - t2g: str, - sample: str ): - + """Load kallisto-formatted mtx files into AnnData""" adata = sc.read_mtx(matrix) adata.obs_names = pd.read_csv(barcodes, header=None, sep="\\t")[0].values adata.var_names = pd.read_csv(features, header=None, sep="\\t")[0].values + return adata + + +def _add_metadata(adata: AnnData, t2g: str, sample: str): + """Add var and obs metadata""" adata.obs["sample"] = sample - txp2gene = pd.read_table(f"{t2g}", header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) + txp2gene = pd.read_table( + t2g, header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2] + ) txp2gene = txp2gene.drop_duplicates(subset="gene_id").set_index("gene_id") adata.var = adata.var.join(txp2gene, how="left") - return adata + # sanitize gene IDs into standard format + # index are gene IDs and symbols are a column + adata.var["gene_versions"] = adata.var.index + adata.var.index = adata.var["gene_versions"].str.split(".").str[0].values + assert adata.var_names.is_unique, "Gene IDs are expected to be unique" def format_yaml_like(data: dict, indent: int = 0) -> str: @@ -49,69 +58,57 @@ def format_yaml_like(data: dict, indent: int = 0) -> str: yaml_str += f"{spaces}{key}: {value}\\n" return yaml_str + def dump_versions(): versions = { "${task.process}": { "python": platform.python_version(), "scanpy": sc.__version__, - "pandas": pd.__version__ + "pandas": pd.__version__, } } with open("versions.yml", "w") as f: f.write(format_yaml_like(versions)) -def input_to_adata( - matrix: str, - barcodes: str, - features: str, - t2g: str, - output: str, - sample: str, -): - print(f"Reading in {matrix}") - # open main data - adata = _mtx_to_adata(matrix=matrix, barcodes=barcodes, features=features, sample=sample, t2g=t2g) +if __name__ == "__main__": + # create the directory with the sample name + os.makedirs("${meta.id}", exist_ok=True) - # standard format - # index are gene IDs and symbols are a column - adata.var['gene_versions'] = adata.var.index - adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values - adata.var_names_make_unique() - - # write results - adata.write_h5ad(f"{output}") - print(f"Wrote h5ad file to {output}") - -# -# Run main script -# - -# create the directory with the sample name -os.makedirs("${meta.id}", exist_ok=True) - -# input_type comes from NF module -if "${params.kb_workflow}" == "standard": - input_to_adata( - matrix=glob.glob("${inputs}/*.mtx")[0], - barcodes=glob.glob("${inputs}/*.barcodes.txt")[0], - features=glob.glob("${inputs}/*.genes.txt")[0], - output="${meta.id}_${meta.input_type}_matrix.h5ad", - sample="${meta.id}", - t2g="${txp2gene}" - ) + # input_type comes from NF module + if "${params.kb_workflow}" == "standard": + adata = _mtx_to_adata( + matrix=glob.glob("${inputs}/*.mtx")[0], + barcodes=glob.glob("${inputs}/*.barcodes.txt")[0], + features=glob.glob("${inputs}/*.genes.txt")[0], + ) -else: - for type in ['spliced', 'unspliced']: - input_to_adata( - matrix=glob.glob("${inputs}/" + f"{type}*.mtx")[0], - barcodes=glob.glob("${inputs}/" + f"{type}*.barcodes.txt")[0], - features=glob.glob("${inputs}/" + f"{type}*.genes.txt")[0], - output="${meta.id}_${meta.input_type}" + f"_{type}_matrix.h5ad", - sample="${meta.id}", - t2g="${txp2gene}" + else: + spliced = _mtx_to_adata( + matrix=glob.glob("${inputs}/spliced*.mtx")[0], + barcodes=glob.glob("${inputs}/spliced*.barcodes.txt")[0], + features=glob.glob("${inputs}/spliced*.genes.txt")[0], + ) + unspliced = _mtx_to_adata( + matrix=glob.glob("${inputs}/unspliced*.mtx")[0], + barcodes=glob.glob("${inputs}/unspliced*.barcodes.txt")[0], + features=glob.glob("${inputs}/unspliced*.genes.txt")[0], ) -# dump versions -dump_versions() + # move X into layers + spliced.layers["spliced"] = spliced.X + spliced.X = None + unspliced.layers["unspliced"] = unspliced.X + unspliced.X = None + + # outer-join will fill missing values with 0s in case of sparse matrices. + adata = concat_ad([spliced, unspliced], join="outer") + # X as the sum of spliced and unspliced counts + adata.X = adata.layers["spliced"] + adata.layers["unspliced"] + + _add_metadata(adata, t2g="${txp2gene}", sample="${meta.id}") + adata.write_h5ad("${meta.id}_${meta.input_type}_matrix.h5ad") + + # dump versions + dump_versions() From bf78578c47d51c2188899b726c954d3f0602a6d5 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 13:50:08 +0000 Subject: [PATCH 103/147] update alevin nf-test and snaps --- tests/main_pipeline_alevin.nf.test | 20 ++++++++++---------- tests/main_pipeline_alevin.nf.test.snap | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/main_pipeline_alevin.nf.test b/tests/main_pipeline_alevin.nf.test index dc6c081a..e46a5102 100644 --- a/tests/main_pipeline_alevin.nf.test +++ b/tests/main_pipeline_alevin.nf.test @@ -30,11 +30,11 @@ nextflow_pipeline { {assert workflow.success}, // How many tasks were executed? - {assert workflow.trace.tasks().size() == 14}, + {assert workflow.trace.tasks().size() == 17}, // How many results were produced? {assert path("${outputDir}/results_alevin").list().size() == 5}, - {assert path("${outputDir}/results_alevin/alevin").list().size() == 4}, + {assert path("${outputDir}/results_alevin/alevin").list().size() == 3}, {assert path("${outputDir}/results_alevin/alevin/mtx_conversions").list().size() == 4}, {assert path("${outputDir}/results_alevin/alevinqc").list().size() == 2}, {assert path("${outputDir}/results_alevin/fastqc").list().size() == 12}, @@ -51,14 +51,14 @@ nextflow_pipeline { // {assert snapshot( workflow, - path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), - path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat.mtx" ), - path( "${outputDir}/results_alevin/alevin/Sample_X_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), - path( "${outputDir}/results_alevin/alevin/Sample_Y_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), - path( "${outputDir}/results_alevin/alevin/Sample_Y_alevin_results/af_quant/alevin/quants_mat.mtx" ), - path( "${outputDir}/results_alevin/alevin/Sample_Y_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), - path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_X/Sample_X_raw_matrix.rds" ), - path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.rds" ) + path( "${outputDir}/results_alevin/alevin/Sample_X/Sample_X_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), + path( "${outputDir}/results_alevin/alevin/Sample_X/Sample_X_alevin_results/af_quant/alevin/quants_mat.mtx" ), + path( "${outputDir}/results_alevin/alevin/Sample_X/Sample_X_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), + path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), + path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat.mtx" ), + path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_X/Sample_X_raw_matrix.Rds" ), + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.Rds" ) ).match()} ) // end of assertAll() diff --git a/tests/main_pipeline_alevin.nf.test.snap b/tests/main_pipeline_alevin.nf.test.snap index e3d19f72..5b48ad45 100644 --- a/tests/main_pipeline_alevin.nf.test.snap +++ b/tests/main_pipeline_alevin.nf.test.snap @@ -3,19 +3,19 @@ "content": [ { "stderr": [ - + ], "errorReport": "", "exitStatus": 0, "failed": false, "stdout": [ - + ], "errorMessage": "", "trace": { "tasksFailed": 0, - "tasksCount": 14, - "tasksSucceeded": 14 + "tasksCount": 17, + "tasksSucceeded": 17 }, "name": "workflow", "success": true @@ -26,13 +26,13 @@ "quants_mat_cols.txt:md5,e9868982c17a330392e38c2a5933cf97", "quants_mat.mtx:md5,54cd12666016adce94c025b2e07f4b02", "quants_mat_rows.txt:md5,6b458a7777260ba90eccbe7919df934b", - "Sample_X_raw_matrix.rds:md5,ad35ee66bf2fc3d5d4656c19a7e64e2b", - "Sample_Y_raw_matrix.rds:md5,baf584142205b1d42bb6fdab1f22a06a" + "Sample_X_raw_matrix.Rds:md5,8bc70642a88e5025da198d5463faffbe", + "Sample_Y_raw_matrix.Rds:md5,1797b215c555d9eeb58d0896529efbff" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.1" }, - "timestamp": "2024-02-14T14:49:46.831540515" + "timestamp": "2024-11-25T13:46:14.542051299" } -} +} \ No newline at end of file From 8f3f1872e0cfd2933357ed5d7805623e6ecce8d4 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 14:04:56 +0000 Subject: [PATCH 104/147] added singlecellobject export --- modules/local/anndatar_convert.nf | 2 +- modules/local/templates/anndatar_convert.R | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index 8e4c242e..6e1150fa 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -14,7 +14,7 @@ process ANNDATAR_CONVERT { tuple val(meta), path(h5ad) output: - tuple val(meta), path("${meta.id}_${meta.input_type}_matrix.Rds"), emit: rds + tuple val(meta), path("${meta.id}_${meta.input_type}_matrix*.rds"), emit: rds when: task.ext.when == null || task.ext.when diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R index 5be46163..f27b2027 100755 --- a/modules/local/templates/anndatar_convert.R +++ b/modules/local/templates/anndatar_convert.R @@ -4,13 +4,22 @@ # load libraries library(anndataR) +library(SeuratObject) +library(SingleCellExperiment) # read input adata <- read_h5ad("${h5ad}") -# convert to Rds +# convert to Seurat obj <- adata\$to_Seurat() # save files dir.create(file.path("$meta.id"), showWarnings = FALSE) -saveRDS(obj, file = "${meta.id}_${meta.input_type}_matrix.Rds") +saveRDS(obj, file = "${meta.id}_${meta.input_type}_matrix.seurat.rds") + +# convert to SingleCellExperiment +obj <- adata\$to_SingleCellExperiment() + +# save files +dir.create(file.path("$meta.id"), showWarnings = FALSE) +saveRDS(obj, file = "${meta.id}_${meta.input_type}_matrix.sce.rds") From fae209917affaa1e085ed99495c931ed5b03cc7a Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 14:12:52 +0000 Subject: [PATCH 105/147] include singlecellexperiment object in nf-test and update namings --- tests/main_pipeline_alevin.nf.test | 8 +++++--- tests/main_pipeline_alevin.nf.test.snap | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/main_pipeline_alevin.nf.test b/tests/main_pipeline_alevin.nf.test index e46a5102..0016b33d 100644 --- a/tests/main_pipeline_alevin.nf.test +++ b/tests/main_pipeline_alevin.nf.test @@ -35,7 +35,7 @@ nextflow_pipeline { // How many results were produced? {assert path("${outputDir}/results_alevin").list().size() == 5}, {assert path("${outputDir}/results_alevin/alevin").list().size() == 3}, - {assert path("${outputDir}/results_alevin/alevin/mtx_conversions").list().size() == 4}, + {assert path("${outputDir}/results_alevin/alevin/mtx_conversions").list().size() == 5}, {assert path("${outputDir}/results_alevin/alevinqc").list().size() == 2}, {assert path("${outputDir}/results_alevin/fastqc").list().size() == 12}, {assert path("${outputDir}/results_alevin/multiqc").list().size() == 3}, @@ -57,8 +57,10 @@ nextflow_pipeline { path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat_cols.txt" ), path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat.mtx" ), path( "${outputDir}/results_alevin/alevin/Sample_Y/Sample_Y_alevin_results/af_quant/alevin/quants_mat_rows.txt" ), - path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_X/Sample_X_raw_matrix.Rds" ), - path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.Rds" ) + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_X/Sample_X_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_X/Sample_X_raw_matrix.sce.rds" ), + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_alevin/alevin/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.sce.rds" ) ).match()} ) // end of assertAll() diff --git a/tests/main_pipeline_alevin.nf.test.snap b/tests/main_pipeline_alevin.nf.test.snap index 5b48ad45..b9e7a7e8 100644 --- a/tests/main_pipeline_alevin.nf.test.snap +++ b/tests/main_pipeline_alevin.nf.test.snap @@ -26,13 +26,15 @@ "quants_mat_cols.txt:md5,e9868982c17a330392e38c2a5933cf97", "quants_mat.mtx:md5,54cd12666016adce94c025b2e07f4b02", "quants_mat_rows.txt:md5,6b458a7777260ba90eccbe7919df934b", - "Sample_X_raw_matrix.Rds:md5,8bc70642a88e5025da198d5463faffbe", - "Sample_Y_raw_matrix.Rds:md5,1797b215c555d9eeb58d0896529efbff" + "Sample_X_raw_matrix.seurat.rds:md5,8bc70642a88e5025da198d5463faffbe", + "Sample_X_raw_matrix.sce.rds:md5,2236117afee08cdb102e30bf02a51e7b", + "Sample_Y_raw_matrix.seurat.rds:md5,1797b215c555d9eeb58d0896529efbff", + "Sample_Y_raw_matrix.sce.rds:md5,d2a20b7902feefb7b4899caea4328fd9" ], "meta": { "nf-test": "0.9.0", "nextflow": "24.10.1" }, - "timestamp": "2024-11-25T13:46:14.542051299" + "timestamp": "2024-11-25T14:08:32.729548697" } } \ No newline at end of file From 9d8577bd21d48f56f40db79e63fb874f02933a04 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 14:19:41 +0000 Subject: [PATCH 106/147] avoind assessing params from modules --- modules/local/mtx_to_h5ad.nf | 3 ++- workflows/scrnaseq.nf | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index eab69cf4..64707099 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -16,6 +16,7 @@ process MTX_TO_H5AD { tuple val(meta), path(inputs) path txp2gene tuple val(meta2), path(star_index) + val input_aligner output: tuple val(meta), path("${meta.id}_${meta.input_type}_matrix.h5ad"), emit: h5ad @@ -25,7 +26,7 @@ process MTX_TO_H5AD { task.ext.when == null || task.ext.when script: - def aligner = (params.aligner in [ 'cellranger', 'cellrangerarc', 'cellrangermulti' ]) ? 'cellranger' : params.aligner + def aligner = (input_aligner in [ 'cellranger', 'cellrangerarc', 'cellrangermulti' ]) ? 'cellranger' : input_aligner template "mtx_to_h5ad_${aligner}.py" diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index c118f3b6..56ebced9 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -294,7 +294,8 @@ workflow SCRNASEQ { MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - ch_star_index + ch_star_index, + params.aligner ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From 705df8108d0b1c2cbcd67135fd1fdb83824f9f6d Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 25 Nov 2024 15:32:08 +0100 Subject: [PATCH 107/147] add snippet to make values unique --- modules/local/templates/mtx_to_h5ad_kallisto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index a25289e5..ffbd9f73 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -38,7 +38,7 @@ def _add_metadata(adata: AnnData, t2g: str, sample: str): # index are gene IDs and symbols are a column adata.var["gene_versions"] = adata.var.index adata.var.index = adata.var["gene_versions"].str.split(".").str[0].values - assert adata.var_names.is_unique, "Gene IDs are expected to be unique" + adata.var_names_make_unique() # in case user does not use ensembl references, names might not be unique def format_yaml_like(data: dict, indent: int = 0) -> str: From 804d5cd905ebb28b15e8cbf3d6d90b5b4cf7a36a Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 14:42:26 +0000 Subject: [PATCH 108/147] fix star channel parsing --- modules/local/mtx_to_h5ad.nf | 2 +- workflows/scrnaseq.nf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index 64707099..1dc920a5 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -15,7 +15,7 @@ process MTX_TO_H5AD { // for each sample, the sub-folders and files come directly in array. tuple val(meta), path(inputs) path txp2gene - tuple val(meta2), path(star_index) + path star_index val input_aligner output: diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 56ebced9..3795332d 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -73,7 +73,7 @@ workflow SCRNASEQ { //star params star_index = star_index ? file(star_index, checkIfExists: true) : null - ch_star_index = star_index ? Channel.of( [[id: star_index.baseName], star_index] ) : [[], []] + ch_star_index = star_index ? Channel.of( [[id: star_index.baseName], star_index] ) : [] star_feature = params.star_feature //cellranger params @@ -294,7 +294,7 @@ workflow SCRNASEQ { MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - ch_star_index, + star_index ? ch_star_index : [], params.aligner ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From 41731ac8f26b1a7bba4d328226fb1cec38f7cd0f Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 25 Nov 2024 15:59:39 +0100 Subject: [PATCH 109/147] simplified kallisto so non-standardo workflow have spliced and unspliced as layers in the same object --- workflows/scrnaseq.nf | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 56ebced9..895b0684 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -299,29 +299,12 @@ workflow SCRNASEQ { ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) - // fix channel size when kallisto non-standard workflow - if (params.aligner == 'kallisto' && !(params.kb_workflow == 'standard')) { - ch_h5ads = - MTX_TO_H5AD.out.h5ad - .transpose() - .map { meta, h5ad -> - def meta_clone = meta.clone() - def spc_prefix = h5ad.toString().contains('unspliced') ? 'un' : '' - - meta_clone["input_type"] = "${meta.input_type}_${spc_prefix}spliced" - - [ meta_clone, h5ad ] - } - } else { - ch_h5ads = MTX_TO_H5AD.out.h5ad - } - // // SUBWORKFLOW: Run h5ad conversion and concatenation // ch_emptydrops = Channel.empty() H5AD_CONVERSION ( - ch_h5ads, + MTX_TO_H5AD.out.h5ad, ch_input ) ch_versions.mix(H5AD_CONVERSION.out.ch_versions) From 298c49147fcf0ce29cb966b3b7227f1a09c05d7b Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Mon, 25 Nov 2024 15:20:44 +0000 Subject: [PATCH 110/147] update star nf-test, snaps and file namings --- tests/main_pipeline_star.nf.test | 16 +++++++++------ tests/main_pipeline_star.nf.test.snap | 28 +++++++++++++++------------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/main_pipeline_star.nf.test b/tests/main_pipeline_star.nf.test index 37c54d57..41c68d35 100644 --- a/tests/main_pipeline_star.nf.test +++ b/tests/main_pipeline_star.nf.test @@ -30,12 +30,12 @@ nextflow_pipeline { {assert workflow.success}, // How many tasks were executed? - {assert workflow.trace.tasks().size() == 17}, + {assert workflow.trace.tasks().size() == 23}, // How many results were produced? {assert path("${outputDir}/results_star").list().size() == 4}, {assert path("${outputDir}/results_star/star").list().size() == 3}, - {assert path("${outputDir}/results_star/star/mtx_conversions").list().size() == 5}, + {assert path("${outputDir}/results_star/star/mtx_conversions").list().size() == 8}, {assert path("${outputDir}/results_star/fastqc").list().size() == 12}, {assert path("${outputDir}/results_star/multiqc").list().size() == 3}, @@ -62,10 +62,14 @@ nextflow_pipeline { path( "${outputDir}/results_star/star/Sample_Y/Sample_Y.Solo.out/Gene/filtered/matrix.mtx.gz" ), path( "${outputDir}/results_star/star/Sample_Y/Sample_Y.Solo.out/Gene/filtered/features.tsv.gz" ), path( "${outputDir}/results_star/star/Sample_Y/Sample_Y.Solo.out/Gene/filtered/barcodes.tsv.gz" ), - path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_raw_matrix.rds" ), - path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.rds" ), - path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_filtered_matrix.rds" ), - path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_raw_matrix.sce.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.sce.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_X/Sample_X_filtered_matrix.sce.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_star/star/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.sce.rds" ) ).match()} ) // end of assertAll() diff --git a/tests/main_pipeline_star.nf.test.snap b/tests/main_pipeline_star.nf.test.snap index 0aae74cf..80cccf74 100644 --- a/tests/main_pipeline_star.nf.test.snap +++ b/tests/main_pipeline_star.nf.test.snap @@ -3,19 +3,19 @@ "content": [ { "stderr": [ - + ], "errorReport": "", "exitStatus": 0, "failed": false, "stdout": [ - + ], "errorMessage": "", "trace": { "tasksFailed": 0, - "tasksCount": 17, - "tasksSucceeded": 17 + "tasksCount": 23, + "tasksSucceeded": 23 }, "name": "workflow", "success": true @@ -30,15 +30,19 @@ "matrix.mtx.gz:md5,0ae080bd0002e350531a5816e159345e", "features.tsv.gz:md5,99e453cb1443a3e43e99405184e51a5e", "barcodes.tsv.gz:md5,9b695b0b91bcb146ec9c4688ca10a690", - "Sample_X_raw_matrix.rds:md5,31604db3e7846acc8d9a60b1a171ce78", - "Sample_Y_raw_matrix.rds:md5,1a52c823e91acce2b29621c8c99c8c72", - "Sample_X_filtered_matrix.rds:md5,aa2d36dd8507aba864347c88e4ce0d27", - "Sample_Y_filtered_matrix.rds:md5,d459af8f99258bcc88b80b2f7c58e911" + "Sample_X_raw_matrix.seurat.rds:md5,db2aa4392b23bd2a0cf8261d9fa5ac1e", + "Sample_X_raw_matrix.sce.rds:md5,67894a2ca56c5d8f21097e7c93a0c61b", + "Sample_Y_raw_matrix.seurat.rds:md5,ea5351c9f871d535c9363aac56fb3e6f", + "Sample_Y_raw_matrix.sce.rds:md5,bf0dcb00f76fc190e0cdeac23128bdc4", + "Sample_X_filtered_matrix.seurat.rds:md5,97d5b28505fafe07bdec0ff942a782a7", + "Sample_X_filtered_matrix.sce.rds:md5,fb72a298bae9dd56bf19f2cf8b56fad8", + "Sample_Y_filtered_matrix.seurat.rds:md5,5f1bb403d80a8c2afd099373d20bfba7", + "Sample_Y_filtered_matrix.sce.rds:md5,a6887a7577a6121ca0a37f382beae820" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.1" }, - "timestamp": "2024-02-14T16:30:25.7971791" + "timestamp": "2024-11-25T15:19:07.532050762" } -} +} \ No newline at end of file From 208c11b87f2fb971608981c981fc5a3091c0bdcf Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 26 Nov 2024 08:51:57 +0100 Subject: [PATCH 111/147] fix channel size --- workflows/scrnaseq.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 24a94fed..b805895e 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -294,7 +294,7 @@ workflow SCRNASEQ { MTX_TO_H5AD ( ch_mtx_matrices, ch_txp2gene, - star_index ? ch_star_index : [], + star_index ? ch_star_index.map{it[1]} : [], params.aligner ) ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions.first()) From f120824965590048ea94d1ce4ed4d7c9d0e6deae Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 26 Nov 2024 09:21:12 +0100 Subject: [PATCH 112/147] Fix kallisto mtx script --- .../local/templates/mtx_to_h5ad_kallisto.py | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index ffbd9f73..db93523f 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -8,6 +8,7 @@ import scanpy as sc import pandas as pd from anndata import AnnData, concat as concat_ad +from scipy.sparse import csr_matrix import platform import glob @@ -38,7 +39,7 @@ def _add_metadata(adata: AnnData, t2g: str, sample: str): # index are gene IDs and symbols are a column adata.var["gene_versions"] = adata.var.index adata.var.index = adata.var["gene_versions"].str.split(".").str[0].values - adata.var_names_make_unique() # in case user does not use ensembl references, names might not be unique + adata.var_names_make_unique() # in case user does not use ensembl references, names might not be unique def format_yaml_like(data: dict, indent: int = 0) -> str: @@ -96,16 +97,32 @@ def dump_versions(): features=glob.glob("${inputs}/unspliced*.genes.txt")[0], ) - # move X into layers - spliced.layers["spliced"] = spliced.X - spliced.X = None - unspliced.layers["unspliced"] = unspliced.X - unspliced.X = None + # The barcodes of spliced / non-spliced are not necessarily the same. + # We fill the missing barcodes with zeros + all_barcodes = list(set(unspliced.obs_names) | set(spliced.obs_names)) + missing_spliced = list(set(unspliced.obs_names) - set(spliced.obs_names)) + missing_unspliced = list(set(spliced.obs_names) - set(unspliced.obs_names)) + ad_missing_spliced = AnnData( + X=csr_matrix((len(missing_spliced), spliced.shape[1])), + obs=pd.DataFrame(index=missing_spliced), + ) + ad_missing_unspliced = AnnData( + X=csr_matrix((len(missing_unspliced), spliced.shape[1])), + obs=pd.DataFrame(index=missing_unspliced), + ) - # outer-join will fill missing values with 0s in case of sparse matrices. - adata = concat_ad([spliced, unspliced], join="outer") - # X as the sum of spliced and unspliced counts - adata.X = adata.layers["spliced"] + adata.layers["unspliced"] + spliced = concat_ad([spliced, ad_missing_spliced], join="outer")[ + all_barcodes, : + ] + unspliced = concat_ad([unspliced, ad_missing_unspliced], join="outer")[ + all_barcodes, : + ] + + adata = AnnData( + X=spliced.X + unspliced.X, + layers={"unspliced": unspliced.X, "spliced": spliced.X}, + obs=pd.DataFrame(index=all_barcodes), + ) _add_metadata(adata, t2g="${txp2gene}", sample="${meta.id}") adata.write_h5ad("${meta.id}_${meta.input_type}_matrix.h5ad") From 8e883848d0c15b0a5da44c0d6dd3d8d3ebc115b4 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 26 Nov 2024 09:50:10 +0100 Subject: [PATCH 113/147] add a little comment line --- modules/local/templates/mtx_to_h5ad_kallisto.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index db93523f..c491dfaa 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -124,6 +124,9 @@ def dump_versions(): obs=pd.DataFrame(index=all_barcodes), ) + # out of the conditional: snippet for both standard and non-standard workflows + + # finalize generated adata object _add_metadata(adata, t2g="${txp2gene}", sample="${meta.id}") adata.write_h5ad("${meta.id}_${meta.input_type}_matrix.h5ad") From a15a28bb6164463ce4a0c3cd7cde46920fc16454 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Tue, 26 Nov 2024 10:00:40 +0000 Subject: [PATCH 114/147] updated cellranger nf-test sizes, namings and snaps --- tests/main_pipeline_cellranger.nf.test | 34 ++++++++++++--------- tests/main_pipeline_cellranger.nf.test.snap | 22 +++++++------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/tests/main_pipeline_cellranger.nf.test b/tests/main_pipeline_cellranger.nf.test index ea68eca6..977f6721 100644 --- a/tests/main_pipeline_cellranger.nf.test +++ b/tests/main_pipeline_cellranger.nf.test @@ -30,13 +30,13 @@ nextflow_pipeline { {assert workflow.success}, // How many tasks were executed? - {assert workflow.trace.tasks().size() == 18}, + {assert workflow.trace.tasks().size() == 24}, // How many results were produced? {assert path("${outputDir}/results_cellranger").list().size() == 4}, {assert path("${outputDir}/results_cellranger/cellranger").list().size() == 4}, - {assert path("${outputDir}/results_cellranger/cellranger/mtx_conversions").list().size() == 5}, - {assert path("${outputDir}/results_cellranger/cellranger/count").list().size() == 3}, + {assert path("${outputDir}/results_cellranger/cellranger/mtx_conversions").list().size() == 8}, + {assert path("${outputDir}/results_cellranger/cellranger/count").list().size() == 2}, {assert path("${outputDir}/results_cellranger/fastqc").list().size() == 12}, {assert path("${outputDir}/results_cellranger/multiqc").list().size() == 3}, @@ -55,20 +55,24 @@ nextflow_pipeline { workflow, path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/filtered_feature_bc_matrix/matrix.mtx.gz" ), path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/filtered_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_raw_matrix.rds" ), - path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.rds" ), - path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_filtered_matrix.rds" ), - path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.rds" ) + path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_X/outs/raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellranger/cellranger/count/Sample_Y/outs/raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_X/Sample_X_filtered_matrix.sce.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellranger/cellranger/mtx_conversions/Sample_Y/Sample_Y_filtered_matrix.sce.rds" ) ).match()} ) // end of assertAll() diff --git a/tests/main_pipeline_cellranger.nf.test.snap b/tests/main_pipeline_cellranger.nf.test.snap index ef8874f8..33709ae8 100644 --- a/tests/main_pipeline_cellranger.nf.test.snap +++ b/tests/main_pipeline_cellranger.nf.test.snap @@ -14,8 +14,8 @@ "errorMessage": "", "trace": { "tasksFailed": 0, - "tasksCount": 18, - "tasksSucceeded": 18 + "tasksCount": 24, + "tasksSucceeded": 24 }, "name": "workflow", "success": true @@ -32,15 +32,19 @@ "barcodes.tsv.gz:md5,081f72b5252ccaf5ffd535ffbd235c4c", "features.tsv.gz:md5,99e453cb1443a3e43e99405184e51a5e", "matrix.mtx.gz:md5,58182db2706d532ec970526de3d3b70f", - "Sample_X_raw_matrix.rds:md5,306a5477ace4d43d851b8389fdfeaf1f", - "Sample_Y_raw_matrix.rds:md5,74b31532da4cae5a8197d690021d77fc", - "Sample_X_filtered_matrix.rds:md5,f9191ba575a3ab79ada4807715f18573", - "Sample_Y_filtered_matrix.rds:md5,7be3f7b29d668dcf7e951b9f4d371a5e" + "Sample_X_raw_matrix.seurat.rds:md5,9313e78e79afc6003c75033529d1a9dd", + "Sample_X_raw_matrix.sce.rds:md5,e834517ca64a5c3f84265a6aa287086b", + "Sample_Y_raw_matrix.seurat.rds:md5,e6a9898842cd9090c2608795a8e266ee", + "Sample_Y_raw_matrix.sce.rds:md5,d8aaa5c6ffd2092b27716c6106b047e9", + "Sample_X_filtered_matrix.seurat.rds:md5,68c0f673aeb8b8391930e534ca0b2653", + "Sample_X_filtered_matrix.sce.rds:md5,52619bbf52478c4fdca8cb62606a567c", + "Sample_Y_filtered_matrix.seurat.rds:md5,bbb40897f4fabf9db1124e4fc28b624e", + "Sample_Y_filtered_matrix.sce.rds:md5,aa47704a2d43addbe37cb63cf977ac7b" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.1" }, - "timestamp": "2024-04-16T09:43:45.32298954" + "timestamp": "2024-11-26T09:27:10.525261276" } } \ No newline at end of file From 245ae5b4a520c3d01d2ca92cf276c87f32403005 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 26 Nov 2024 11:33:25 +0100 Subject: [PATCH 115/147] Update kallisto script (again) --- modules/local/templates/mtx_to_h5ad_kallisto.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index c491dfaa..bacb97a7 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -11,6 +11,7 @@ from scipy.sparse import csr_matrix import platform import glob +import numpy as np def _mtx_to_adata( @@ -105,10 +106,12 @@ def dump_versions(): ad_missing_spliced = AnnData( X=csr_matrix((len(missing_spliced), spliced.shape[1])), obs=pd.DataFrame(index=missing_spliced), + var=spliced.var, ) ad_missing_unspliced = AnnData( X=csr_matrix((len(missing_unspliced), spliced.shape[1])), obs=pd.DataFrame(index=missing_unspliced), + var=unspliced.var, ) spliced = concat_ad([spliced, ad_missing_spliced], join="outer")[ @@ -118,10 +121,13 @@ def dump_versions(): all_barcodes, : ] + assert np.all(spliced.var_names == unspliced.var_names) + adata = AnnData( X=spliced.X + unspliced.X, layers={"unspliced": unspliced.X, "spliced": spliced.X}, obs=pd.DataFrame(index=all_barcodes), + var=pd.DataFrame(index=spliced.var_names), ) # out of the conditional: snippet for both standard and non-standard workflows From bee74f6325bd9e6bde3893320fe845a84e89b736 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Tue, 26 Nov 2024 11:01:01 +0000 Subject: [PATCH 116/147] update kallisto nf-test size, namings and snaps --- tests/main_pipeline_kallisto.nf.test | 20 +++++++++++--------- tests/main_pipeline_kallisto.nf.test.snap | 16 +++++++++------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/main_pipeline_kallisto.nf.test b/tests/main_pipeline_kallisto.nf.test index 12e78144..95493f58 100644 --- a/tests/main_pipeline_kallisto.nf.test +++ b/tests/main_pipeline_kallisto.nf.test @@ -30,12 +30,12 @@ nextflow_pipeline { {assert workflow.success}, // How many tasks were executed? - {assert workflow.trace.tasks().size() == 12}, + {assert workflow.trace.tasks().size() == 15}, // How many results were produced? {assert path("${outputDir}/results_kallisto").list().size() == 4}, - {assert path("${outputDir}/results_kallisto/kallisto").list().size() == 4}, - {assert path("${outputDir}/results_kallisto/kallisto/mtx_conversions").list().size() == 4}, + {assert path("${outputDir}/results_kallisto/kallisto").list().size() == 3}, + {assert path("${outputDir}/results_kallisto/kallisto/mtx_conversions").list().size() == 5}, {assert path("${outputDir}/results_kallisto/kallisto/Sample_X.count").list().size() == 9}, {assert path("${outputDir}/results_kallisto/kallisto/Sample_Y.count").list().size() == 9}, {assert path("${outputDir}/results_kallisto/fastqc").list().size() == 12}, @@ -53,13 +53,15 @@ nextflow_pipeline { {assert snapshot( workflow, path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.barcodes.txt" ), - path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.genes.txt" ), - path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.mtx" ), + path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.genes.txt" ), + path( "${outputDir}/results_kallisto/kallisto/Sample_X.count/counts_unfiltered/cells_x_genes.mtx" ), path( "${outputDir}/results_kallisto/kallisto/Sample_Y.count/counts_unfiltered/cells_x_genes.barcodes.txt" ), - path( "${outputDir}/results_kallisto/kallisto/Sample_Y.count/counts_unfiltered/cells_x_genes.genes.txt" ), - path( "${outputDir}/results_kallisto/kallisto/Sample_Y.count/counts_unfiltered/cells_x_genes.mtx" ), - path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_X/Sample_X_raw_matrix.rds" ), - path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.rds" ) + path( "${outputDir}/results_kallisto/kallisto/Sample_Y.count/counts_unfiltered/cells_x_genes.genes.txt" ), + path( "${outputDir}/results_kallisto/kallisto/Sample_Y.count/counts_unfiltered/cells_x_genes.mtx" ), + path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_X/Sample_X_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_X/Sample_X_raw_matrix.sce.rds" ), + path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_kallisto/kallisto/mtx_conversions/Sample_Y/Sample_Y_raw_matrix.sce.rds" ) ).match()} ) // end of assertAll() diff --git a/tests/main_pipeline_kallisto.nf.test.snap b/tests/main_pipeline_kallisto.nf.test.snap index f9b9c96a..e5153968 100644 --- a/tests/main_pipeline_kallisto.nf.test.snap +++ b/tests/main_pipeline_kallisto.nf.test.snap @@ -14,8 +14,8 @@ "errorMessage": "", "trace": { "tasksFailed": 0, - "tasksCount": 12, - "tasksSucceeded": 12 + "tasksCount": 15, + "tasksSucceeded": 15 }, "name": "workflow", "success": true @@ -26,13 +26,15 @@ "cells_x_genes.barcodes.txt:md5,a8cf7ea4b2d075296a94bf066a64b7a4", "cells_x_genes.genes.txt:md5,acd9d00120f52031974b2add3e7521b6", "cells_x_genes.mtx:md5,abd83de117204d0a77df3c92d00cc025", - "Sample_X_raw_matrix.rds:md5,0938f4189b7a7fd1030abfcee798741c", - "Sample_Y_raw_matrix.rds:md5,93c12abe283ab37c5f37e5cd3cb25302" + "Sample_X_raw_matrix.seurat.rds:md5,2e93382c175b6b4f4a94e3a88f8a7190", + "Sample_X_raw_matrix.sce.rds:md5,27069a3772fbe4f6577d7ec0ddf29666", + "Sample_Y_raw_matrix.seurat.rds:md5,74f16124176dee919ca91d9890254b39", + "Sample_Y_raw_matrix.sce.rds:md5,584e0a2dc64798bc8a4e5a070d8a90bc" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.1" }, - "timestamp": "2024-03-18T14:51:42.040931572" + "timestamp": "2024-11-26T11:00:26.349961551" } } \ No newline at end of file From dfb439abd85d2539fe0179d7223918b917a25dbe Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Tue, 26 Nov 2024 11:48:02 +0000 Subject: [PATCH 117/147] adding versions.yml output for modules CONCAT_H5AD, MTX_TO_H5AD and ANNDATAR_CONVERT --- modules/local/anndatar_convert.nf | 4 ++- modules/local/concat_h5ad.nf | 2 ++ modules/local/mtx_to_h5ad.nf | 2 +- modules/local/templates/anndatar_convert.R | 17 ++++++++++ modules/local/templates/concat_h5ad.py | 31 +++++++++++++++++++ modules/local/templates/mtx_to_h5ad_alevin.py | 4 ++- .../local/templates/mtx_to_h5ad_cellranger.py | 4 ++- .../local/templates/mtx_to_h5ad_kallisto.py | 2 ++ modules/local/templates/mtx_to_h5ad_star.py | 4 ++- subworkflows/local/h5ad_conversion.nf | 5 ++- workflows/scrnaseq.nf | 2 +- 11 files changed, 68 insertions(+), 9 deletions(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index 6e1150fa..c84d92dd 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -15,6 +15,7 @@ process ANNDATAR_CONVERT { output: tuple val(meta), path("${meta.id}_${meta.input_type}_matrix*.rds"), emit: rds + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -24,6 +25,7 @@ process ANNDATAR_CONVERT { stub: """ - touch ${meta.id}.Rds + touch ${meta.id}_${meta.input_type}_matrix.Rds + touch versions.yml """ } diff --git a/modules/local/concat_h5ad.nf b/modules/local/concat_h5ad.nf index c875ba3c..17c8d4e1 100644 --- a/modules/local/concat_h5ad.nf +++ b/modules/local/concat_h5ad.nf @@ -18,6 +18,7 @@ process CONCAT_H5AD { output: tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -28,5 +29,6 @@ process CONCAT_H5AD { stub: """ touch combined_matrix.h5ad + touch versions.yml """ } diff --git a/modules/local/mtx_to_h5ad.nf b/modules/local/mtx_to_h5ad.nf index 1dc920a5..424580f6 100644 --- a/modules/local/mtx_to_h5ad.nf +++ b/modules/local/mtx_to_h5ad.nf @@ -32,7 +32,7 @@ process MTX_TO_H5AD { stub: """ - touch ${meta.id}_raw_matrix.h5ad + touch ${meta.id}_${meta.input_type}_matrix.h5ad touch versions.yml """ } diff --git a/modules/local/templates/anndatar_convert.R b/modules/local/templates/anndatar_convert.R index f27b2027..6f78e282 100755 --- a/modules/local/templates/anndatar_convert.R +++ b/modules/local/templates/anndatar_convert.R @@ -23,3 +23,20 @@ obj <- adata\$to_SingleCellExperiment() # save files dir.create(file.path("$meta.id"), showWarnings = FALSE) saveRDS(obj, file = "${meta.id}_${meta.input_type}_matrix.sce.rds") + +# +# save versions file +# +versions_file <- file("versions.yml") +write( + paste( + '${task.process}:', + paste0(' r-base: "', R.Version()\$version.string, '"'), + paste0(' anndataR: "', as.character(packageVersion("anndataR")), '"'), + paste0(' SeuratObject: "', as.character(packageVersion("SeuratObject")), '"'), + paste0(' SingleCellExperiment: "', as.character(packageVersion("SingleCellExperiment")), '"'), + sep = "\\n" + ), + versions_file +) +close(versions_file) diff --git a/modules/local/templates/concat_h5ad.py b/modules/local/templates/concat_h5ad.py index 087f7fde..9eddfa46 100755 --- a/modules/local/templates/concat_h5ad.py +++ b/modules/local/templates/concat_h5ad.py @@ -7,6 +7,7 @@ import scanpy as sc, anndata as ad, pandas as pd from pathlib import Path +import platform def read_samplesheet(samplesheet): @@ -20,6 +21,33 @@ def read_samplesheet(samplesheet): return df +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +def dump_versions(): + versions = { + "${task.process}": { + "python": platform.python_version(), + "scanpy": sc.__version__, + } + } + + with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) if __name__ == "__main__": @@ -37,3 +65,6 @@ def read_samplesheet(samplesheet): adata.write_h5ad("combined_${meta.input_type}_matrix.h5ad") print("Wrote h5ad file to {}".format("combined_${meta.input_type}_matrix.h5ad")) + + # dump versions + dump_versions() diff --git a/modules/local/templates/mtx_to_h5ad_alevin.py b/modules/local/templates/mtx_to_h5ad_alevin.py index d54a1667..492defd3 100755 --- a/modules/local/templates/mtx_to_h5ad_alevin.py +++ b/modules/local/templates/mtx_to_h5ad_alevin.py @@ -8,6 +8,7 @@ import scanpy as sc import pandas as pd import argparse +import anndata from anndata import AnnData import platform @@ -46,7 +47,8 @@ def dump_versions(): "${task.process}": { "python": platform.python_version(), "scanpy": sc.__version__, - "pandas": pd.__version__ + "pandas": pd.__version__, + "anndata": anndata.__version__, } } diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index ecc5c077..3fe7d287 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -8,6 +8,7 @@ import scanpy as sc import pandas as pd import argparse +import anndata from anndata import AnnData import platform @@ -49,7 +50,8 @@ def dump_versions(): "${task.process}": { "python": platform.python_version(), "scanpy": sc.__version__, - "pandas": pd.__version__ + "pandas": pd.__version__, + "anndata": anndata.__version__, } } diff --git a/modules/local/templates/mtx_to_h5ad_kallisto.py b/modules/local/templates/mtx_to_h5ad_kallisto.py index bacb97a7..905f3d8a 100755 --- a/modules/local/templates/mtx_to_h5ad_kallisto.py +++ b/modules/local/templates/mtx_to_h5ad_kallisto.py @@ -7,6 +7,7 @@ import scanpy as sc import pandas as pd +import anndata from anndata import AnnData, concat as concat_ad from scipy.sparse import csr_matrix import platform @@ -67,6 +68,7 @@ def dump_versions(): "python": platform.python_version(), "scanpy": sc.__version__, "pandas": pd.__version__, + "anndata": anndata.__version__, } } diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index e44d2478..a9a80372 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -8,6 +8,7 @@ import scanpy as sc import pandas as pd import argparse +import anndata from anndata import AnnData import platform @@ -43,7 +44,8 @@ def dump_versions(): "${task.process}": { "python": platform.python_version(), "scanpy": sc.__version__, - "pandas": pd.__version__ + "pandas": pd.__version__, + "anndata": anndata.__version__, } } diff --git a/subworkflows/local/h5ad_conversion.nf b/subworkflows/local/h5ad_conversion.nf index 2e7888ba..f832a7cf 100644 --- a/subworkflows/local/h5ad_conversion.nf +++ b/subworkflows/local/h5ad_conversion.nf @@ -31,6 +31,7 @@ workflow H5AD_CONVERSION { meta_clone.id = 'combined' // maintain output prefix [ meta_clone, file ] } + ch_versions = ch_versions.mix(CONCAT_H5AD.out.versions.first()) // // MODULE: Convert to Rds with AnndataR package @@ -38,9 +39,7 @@ workflow H5AD_CONVERSION { ANNDATAR_CONVERT ( ch_h5ads.mix( ch_h5ad_concat ) ) - - //TODO CONCAT h5ad and MTX to h5ad should also have versions.yaml output - // ch_versions = ch_versions.mix(MTX_TO_H5AD.out.versions, MTX_TO_SEURAT.out.versions) + ch_versions = ch_versions.mix(ANNDATAR_CONVERT.out.versions.first()) emit: ch_versions diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index b805895e..6ba48e5d 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -307,7 +307,7 @@ workflow SCRNASEQ { MTX_TO_H5AD.out.h5ad, ch_input ) - ch_versions.mix(H5AD_CONVERSION.out.ch_versions) + ch_versions = ch_versions.mix(H5AD_CONVERSION.out.ch_versions) // // SUBWORKFLOW: Run cellbender emptydrops filter From eed41c01b6e1f1d2a940431eeed16edc6ad4b823 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Tue, 26 Nov 2024 11:55:23 +0000 Subject: [PATCH 118/147] correct ifelse order for file renames --- conf/modules.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index ed0c1b92..a9b92b5d 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -54,8 +54,8 @@ process { path: { "${params.outdir}/${params.aligner}/mtx_conversions" }, mode: params.publish_dir_mode, saveAs: { filename -> - if (!filename.contains('combined_')) { "${meta.id}/${filename}" } - else if (filename.equals('versions.yml')) { null } + if (filename.equals('versions.yml')) { null } + else if (!filename.contains('combined_')) { "${meta.id}/${filename}" } else filename } ] From b245be2473fd69fdf7d71c9928862557ffcff19a Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 28 Nov 2024 13:27:37 +0100 Subject: [PATCH 119/147] re-add missing star_align ext.args --- conf/modules.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/modules.config b/conf/modules.config index d48cfb0c..7c09d8df 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -188,6 +188,9 @@ if (params.aligner == "alevin") { if (params.aligner == "star") { process { + withName: STAR_ALIGN { + ext.args = "--readFilesCommand zcat --runDirPerm All_RWX --outWigType bedGraph --twopassMode Basic --outSAMtype BAM SortedByCoordinate" + } withName: STAR_GENOMEGENERATE { publishDir = [ path: { "${params.outdir}/${params.aligner}/genome_generate" }, From 80e78503fb22f744f1d56ff91e93a4352355a44a Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 28 Nov 2024 13:28:33 +0100 Subject: [PATCH 120/147] correct the parsing of getGenomeAttribute as a nextflow variable, since it is not possible to overwrite params. in the workflow scope --- workflows/scrnaseq.nf | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 38c30cf3..353c1a4f 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -40,28 +40,30 @@ workflow SCRNASEQ { } // collect paths from genome attributes file (e.g. iGenomes.config; optional) - params.fasta = getGenomeAttribute('fasta') - params.gtf = getGenomeAttribute('gtf') - params.star_index = getGenomeAttribute('star') - params.salmon_index = getGenomeAttribute('simpleaf') - params.txp2gene = getGenomeAttribute('simpleaf_tx2pgene') + // we cannot overwrite params in the workflow (they stay null as coming from the config file) + def genome_fasta = params.fasta ?: getGenomeAttribute('fasta') + def gtf = params.gtf ?: getGenomeAttribute('gtf') + def star_index = params.star_index ?: getGenomeAttribute('star') + def salmon_index = params.salmon_index ?: getGenomeAttribute('simpleaf') + def txp2gene = params.txp2gene ?: getGenomeAttribute('simpleaf_tx2pgene') // Make cellranger or cellranger-arc index conditional + def cellranger_index = [] if (params.aligner in ["cellranger", "cellrangermulti"]){ - params.cellranger_index = getGenomeAttribute('cellranger') + cellranger_index = params.cellranger_index ?: getGenomeAttribute('cellranger') } else if (params.aligner == "cellrangerarc") { - params.cellranger_index = getGenomeAttribute('cellrangerarc') + cellranger_index = params.cellranger_index ?: getGenomeAttribute('cellrangerarc') } - ch_genome_fasta = params.fasta ? file(params.fasta, checkIfExists: true) : [] - ch_gtf = params.gtf ? file(params.gtf, checkIfExists: true) : [] + ch_genome_fasta = genome_fasta ? file(genome_fasta, checkIfExists: true) : [] + ch_gtf = gtf ? file(gtf, checkIfExists: true) : [] // general input and params ch_transcript_fasta = params.transcript_fasta ? file(params.transcript_fasta): [] ch_motifs = params.motifs ? file(params.motifs) : [] ch_cellrangerarc_config = params.cellrangerarc_config ? file(params.cellrangerarc_config) : [] - ch_txp2gene = params.txp2gene ? file(params.txp2gene) : [] + ch_txp2gene = txp2gene ? file(txp2gene) : [] ch_multiqc_files = Channel.empty() if (params.barcode_whitelist) { ch_barcode_whitelist = file(params.barcode_whitelist) @@ -71,12 +73,6 @@ workflow SCRNASEQ { ch_barcode_whitelist = [] } - //kallisto params - ch_kallisto_index = params.kallisto_index ? file(params.kallisto_index) : [] - kb_workflow = params.kb_workflow - kb_t1c = params.kb_t1c ? file(params.kb_t1c) : [] - kb_t2c = params.kb_t2c ? file(params.kb_t2c) : [] - // samplesheet - this is passed to the MTX conversion functions to add metadata to the // AnnData objects. ch_input = file(params.input) @@ -84,9 +80,11 @@ workflow SCRNASEQ { //kallisto params ch_kallisto_index = params.kallisto_index ? file(params.kallisto_index) : [] kb_workflow = params.kb_workflow + kb_t1c = params.kb_t1c ? file(params.kb_t1c) : [] + kb_t2c = params.kb_t2c ? file(params.kb_t2c) : [] //salmon params - ch_salmon_index = params.salmon_index ? file(params.salmon_index) : [] + ch_salmon_index = salmon_index ? file(salmon_index) : [] //star params star_index = star_index ? file(star_index, checkIfExists: true) : null @@ -94,7 +92,7 @@ workflow SCRNASEQ { star_feature = params.star_feature //cellranger params - ch_cellranger_index = params.cellranger_index ? file(params.cellranger_index) : [] + ch_cellranger_index = cellranger_index ? file(cellranger_index) : [] //universc params ch_universc_index = params.universc_index ? file(params.universc_index) : [] @@ -117,8 +115,8 @@ workflow SCRNASEQ { // // Uncompress genome fasta file if required // - if (ch_genome_fasta) { - if (ch_genome_fasta.endsWith('.gz')) { + if (genome_fasta) { + if (genome_fasta.endsWith('.gz')) { ch_genome_fasta = GUNZIP_FASTA ( [ [:], ch_genome_fasta ] ).gunzip.map { it[1] } ch_versions = ch_versions.mix(GUNZIP_FASTA.out.versions) } else { @@ -129,8 +127,8 @@ workflow SCRNASEQ { // // Uncompress GTF annotation file or create from GFF3 if required // - if (ch_gtf) { - if (ch_gtf.endsWith('.gz')) { + if (gtf) { + if (gtf.endsWith('.gz')) { ch_gtf = GUNZIP_GTF ( [ [:], ch_gtf ] ).gunzip.map { it[1] } ch_versions = ch_versions.mix(GUNZIP_GTF.out.versions) } else { From 395a3e87c424164847aad7b1b47212953978879f Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 28 Nov 2024 13:29:31 +0100 Subject: [PATCH 121/147] current version of star is incompatible with the star index available in igenomes --- conf/test_full.config | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conf/test_full.config b/conf/test_full.config index dd838978..cd215505 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -18,7 +18,9 @@ params { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/scrnaseq/samplesheet_2.0_full.csv' // Genome references - genome = 'GRCh38' + // genome = 'GRCh38' // TODO: Fix: incompatible with new star version + fasta = 'https://ftp.ensembl.org/pub/release-110/fasta/homo_sapiens/dna/Homo_sapiens.GRCh38.dna.primary_assembly.fa.gz' + gtf = 'https://ftp.ensembl.org/pub/release-110/gtf/homo_sapiens/Homo_sapiens.GRCh38.110.gtf.gz' aligner = 'star' protocol = '10XV2' From 6ddcf1545c7c93513e7ec788104b1501437dccc8 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 28 Nov 2024 13:32:30 +0100 Subject: [PATCH 122/147] add checkIfExists: true to file loading calls --- workflows/scrnaseq.nf | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 353c1a4f..07fb2517 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -63,12 +63,12 @@ workflow SCRNASEQ { ch_transcript_fasta = params.transcript_fasta ? file(params.transcript_fasta): [] ch_motifs = params.motifs ? file(params.motifs) : [] ch_cellrangerarc_config = params.cellrangerarc_config ? file(params.cellrangerarc_config) : [] - ch_txp2gene = txp2gene ? file(txp2gene) : [] + ch_txp2gene = txp2gene ? file(txp2gene, checkIfExists: true) : [] ch_multiqc_files = Channel.empty() if (params.barcode_whitelist) { - ch_barcode_whitelist = file(params.barcode_whitelist) + ch_barcode_whitelist = file(params.barcode_whitelist, checkIfExists: true) } else if (protocol_config.containsKey("whitelist")) { - ch_barcode_whitelist = file("$projectDir/${protocol_config['whitelist']}") + ch_barcode_whitelist = file("$projectDir/${protocol_config['whitelist']}", checkIfExists: true) } else { ch_barcode_whitelist = [] } @@ -78,13 +78,13 @@ workflow SCRNASEQ { ch_input = file(params.input) //kallisto params - ch_kallisto_index = params.kallisto_index ? file(params.kallisto_index) : [] + ch_kallisto_index = params.kallisto_index ? file(params.kallisto_index, checkIfExists: true) : [] kb_workflow = params.kb_workflow - kb_t1c = params.kb_t1c ? file(params.kb_t1c) : [] - kb_t2c = params.kb_t2c ? file(params.kb_t2c) : [] + kb_t1c = params.kb_t1c ? file(params.kb_t1c, checkIfExists: true) : [] + kb_t2c = params.kb_t2c ? file(params.kb_t2c, checkIfExists: true) : [] //salmon params - ch_salmon_index = salmon_index ? file(salmon_index) : [] + ch_salmon_index = salmon_index ? file(salmon_index, checkIfExists: true) : [] //star params star_index = star_index ? file(star_index, checkIfExists: true) : null @@ -92,10 +92,10 @@ workflow SCRNASEQ { star_feature = params.star_feature //cellranger params - ch_cellranger_index = cellranger_index ? file(cellranger_index) : [] + ch_cellranger_index = cellranger_index ? file(cellranger_index, checkIfExists: true) : [] //universc params - ch_universc_index = params.universc_index ? file(params.universc_index) : [] + ch_universc_index = params.universc_index ? file(params.universc_index, checkIfExists: true) : [] //cellrangermulti params cellranger_vdj_index = params.cellranger_vdj_index ? file(params.cellranger_vdj_index, checkIfExists: true) : [] From 72c2242636518258084ed5811257fc82401e6dce Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Thu, 28 Nov 2024 13:42:17 +0100 Subject: [PATCH 123/147] remove old conversion scripts --- bin/emptydrops_cell_calling.R | 52 ---------- bin/mtx_to_h5ad.py | 160 ----------------------------- bin/mtx_to_seurat.R | 54 ---------- modules/local/BKP/emptydrops.nf | 101 ------------------ modules/local/BKP/mtx_to_h5ad.nf | 139 ------------------------- modules/local/BKP/mtx_to_seurat.nf | 127 ----------------------- 6 files changed, 633 deletions(-) delete mode 100755 bin/emptydrops_cell_calling.R delete mode 100755 bin/mtx_to_h5ad.py delete mode 100755 bin/mtx_to_seurat.R delete mode 100644 modules/local/BKP/emptydrops.nf delete mode 100644 modules/local/BKP/mtx_to_h5ad.nf delete mode 100644 modules/local/BKP/mtx_to_seurat.nf diff --git a/bin/emptydrops_cell_calling.R b/bin/emptydrops_cell_calling.R deleted file mode 100755 index 23a45267..00000000 --- a/bin/emptydrops_cell_calling.R +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env Rscript -library("DropletUtils") -library("Matrix") - -args <- commandArgs(trailingOnly=TRUE) - -fn_mtx <- args[1] -fn_barcodes <- args[2] -fn_genes <- args[3] -outdir <- args[4] -aligner <- args[5] - -# Read matrix/barcodes/genes -genes <- read.table(fn_genes,sep='\t') -barcodes <- read.table(fn_barcodes,sep='\t') -mtx <- readMM(fn_mtx) - -get_name <- function(file) { - name <- as.character(basename(file)) - name <- gsub('\\.gz$', '', name) - return(name) -} - -# transpose matrices when required -# based on code of 'mtx_to_seurat.R', only the data from kallisto and alevin would require transposition -print("Only kallisto and alevin have transposed matrices.") -if (aligner %in% c( "kallisto", "alevin" )) { - is_transposed <- TRUE - mtx<-t(mtx) -} else { - is_transposed <- FALSE -} - - -# Call empty drops -e.out <- emptyDrops(mtx) -is.cell <- e.out$FDR <= 0.01 - -# Slice matrix and barcodes -mtx_filtered <-mtx[,which(is.cell),drop=FALSE] -barcodes_filtered<-barcodes[which(is.cell),] - -# If matrix was transposed early, need to transpose back -if (is_transposed){ - mtx_filtered<-t(mtx_filtered) - print('Transposing back matrix.') -} - -# Write output -writeMM(mtx_filtered,file.path(outdir,get_name(fn_mtx))) -write.table(barcodes_filtered,file=file.path(outdir,get_name(fn_barcodes)),col.names=FALSE,row.names=FALSE,sep='\t',quote=FALSE) -write.table(genes,file=file.path(outdir,get_name(fn_genes)),col.names=FALSE,row.names=FALSE,sep='\t',quote=FALSE) diff --git a/bin/mtx_to_h5ad.py b/bin/mtx_to_h5ad.py deleted file mode 100755 index 2190245d..00000000 --- a/bin/mtx_to_h5ad.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python - -# Set numba chache dir to current working directory (which is a writable mount also in containers) -import os - -os.environ["NUMBA_CACHE_DIR"] = "." - -import scanpy as sc -import pandas as pd -import argparse -from scipy import io -from anndata import AnnData - - -def _10x_h5_to_adata(mtx_h5: str, sample: str): - adata = sc.read_10x_h5(mtx_h5) - adata.var["gene_symbols"] = adata.var_names - adata.var.set_index("gene_ids", inplace=True) - adata.obs["sample"] = sample - - # reorder columns for 10x mtx files - adata.var = adata.var[["gene_symbols", "feature_types", "genome"]] - - return adata - - -def _mtx_to_adata( - mtx_file: str, - barcode_file: str, - feature_file: str, - sample: str, - aligner: str, -): - adata = sc.read_mtx(mtx_file) - # for some reason star matrix comes transposed and doesn't fit when values are appended directly - # also true for cellranger files ( this is only used when running with the custom emptydrops_filtered files ) - # otherwise, it uses the cellranger .h5 files - if aligner in [ - "cellranger", - "cellrangermulti", - "star", - ]: - adata = adata.transpose() - - adata.obs_names = pd.read_csv(barcode_file, header=None, sep="\t")[0].values - adata.var_names = pd.read_csv(feature_file, header=None, sep="\t")[0].values - adata.obs["sample"] = sample - - return adata - - -def input_to_adata( - input_data: str, - barcode_file: str, - feature_file: str, - sample: str, - aligner: str, - txp2gene: str, - star_index: str, - verbose: bool = True, -): - if verbose and (txp2gene or star_index): - print("Reading in {}".format(input_data)) - - # - # open main data - # - if aligner == "cellranger" and input_data.lower().endswith('.h5'): - adata = _10x_h5_to_adata(input_data, sample) - else: - adata = _mtx_to_adata(input_data, barcode_file, feature_file, sample, aligner) - - # - # open gene information - # - if verbose and (txp2gene or star_index): - print("Reading in {}".format(txp2gene)) - - if aligner == "cellranger" and not input_data.lower().endswith('.h5'): - # - # for cellranger workflow, we do not have a txp2gene file, so, when using this normal/manual function for empty drops - # we need to provide this information coming directly from the features.tsv file - # by not using the .h5 file for conversion, we loose the two col information: feature_types and genome - # - t2g = pd.read_table(feature_file, header=None, names=["gene_id", "gene_symbol", "feature_types"], usecols=[0, 1, 2]) - else: - if txp2gene: - t2g = pd.read_table(txp2gene, header=None, names=["gene_id", "gene_symbol"], usecols=[1, 2]) - elif star_index: - t2g = pd.read_table( - f"{star_index}/geneInfo.tab", header=None, skiprows=1, names=["gene_id", "gene_symbol"], usecols=[0, 1] - ) - - if txp2gene or star_index or (aligner == "cellranger" and not input_data.lower().endswith('.h5')): - t2g = t2g.drop_duplicates(subset="gene_id").set_index("gene_id") - adata.var["gene_symbol"] = t2g["gene_symbol"] - - return adata - - -def write_counts( - adata: AnnData, - out: str, - verbose: bool = False, -): - pd.DataFrame(adata.obs.index).to_csv(os.path.join(out, "barcodes.tsv"), sep="\t", index=False, header=None) - pd.DataFrame(adata.var).to_csv(os.path.join(out, "features.tsv"), sep="\t", index=True, header=None) - io.mmwrite(os.path.join(out, "matrix.mtx"), adata.X.T, field="integer") - - if verbose: - print("Wrote features.tsv, barcodes.tsv, and matrix.mtx files to {}".format(args["out"])) - - -def dump_versions(task_process): - import pkg_resources - - with open("versions.yml", "w") as f: - f.write(f"{task_process}:\n\t") - f.write("\n\t".join([f"{pkg.key}: {pkg.version}" for pkg in pkg_resources.working_set])) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Converts mtx output to h5ad.") - - parser.add_argument("-i", "--input_data", dest="input_data", help="Path to either mtx or mtx h5 file.") - parser.add_argument("-v", "--verbose", dest="verbose", help="Toggle verbose messages", default=False) - parser.add_argument("-f", "--feature", dest="feature", help="Path to feature file.", nargs="?", const="") - parser.add_argument("-b", "--barcode", dest="barcode", help="Path to barcode file.", nargs="?", const="") - parser.add_argument("-s", "--sample", dest="sample", help="Sample name") - parser.add_argument("-o", "--out", dest="out", help="Output path.") - parser.add_argument("-a", "--aligner", dest="aligner", help="Which aligner has been used?") - parser.add_argument("--task_process", dest="task_process", help="Task process name.") - parser.add_argument("--txp2gene", dest="txp2gene", help="Transcript to gene (t2g) file.", nargs="?", const="") - parser.add_argument( - "--star_index", dest="star_index", help="Star index folder containing geneInfo.tab.", nargs="?", const="" - ) - - args = vars(parser.parse_args()) - - # create the directory with the sample name - os.makedirs(os.path.dirname(args["out"]), exist_ok=True) - - adata = input_to_adata( - input_data=args["input_data"], - barcode_file=args["barcode"], - feature_file=args["feature"], - sample=args["sample"], - aligner=args["aligner"], - txp2gene=args["txp2gene"], - star_index=args["star_index"], - verbose=args["verbose"], - ) - - write_counts(adata=adata, out=args["sample"], verbose=args["verbose"]) - - adata.write_h5ad(args["out"], compression="gzip") - - print("Wrote h5ad file to {}".format(args["out"])) - - dump_versions(task_process=args["task_process"]) diff --git a/bin/mtx_to_seurat.R b/bin/mtx_to_seurat.R deleted file mode 100755 index 7cacccf7..00000000 --- a/bin/mtx_to_seurat.R +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env Rscript -library(Seurat) - -args <- commandArgs(trailingOnly=TRUE) - -mtx_file <- args[1] -barcode_file <- args[2] -feature_file <- args[3] -out.file <- args[4] -aligner <- args[5] -is_emptydrops <- args[6] - -if (is_emptydrops == "--is_emptydrops") { - is_emptydrops <- TRUE -} else{ - is_emptydrops <- FALSE -} - -if (aligner %in% c( "kallisto", "alevin" )) { - print("1") - # for kallisto and alevin, the features file contains only one column and matrix needs to be transposed - expression.matrix <- ReadMtx( - mtx = mtx_file, features = feature_file, cells = barcode_file, feature.column = 1, mtx.transpose = TRUE - ) -} else { - if (aligner %in% c( "cellranger", "cellrangermulti", "star" ) && is_emptydrops) { - print("2") - expression.matrix <- ReadMtx( - mtx = mtx_file, features = feature_file, cells = barcode_file, feature.column = 1 - ) - } else{ - print("3") - expression.matrix <- ReadMtx( - mtx = mtx_file, features = feature_file, cells = barcode_file - ) - } -} - - -seurat.object <- CreateSeuratObject(counts = expression.matrix) - -dir.create(basename(dirname(out.file)), showWarnings = FALSE) - -saveRDS(seurat.object, file = out.file) - - -yaml::write_yaml( -list( - 'MTX_TO_SEURAT'=list( - 'Seurat' = paste(packageVersion('Seurat'), collapse='.') - ) -), -"versions.yml" -) diff --git a/modules/local/BKP/emptydrops.nf b/modules/local/BKP/emptydrops.nf deleted file mode 100644 index 9457fc09..00000000 --- a/modules/local/BKP/emptydrops.nf +++ /dev/null @@ -1,101 +0,0 @@ -process EMPTYDROPS_CELL_CALLING { - tag "$meta.id" - label 'process_medium' - - conda "bioconda::bioconductor-dropletutils" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/bioconductor-dropletutils:1.18.0--r42hf17093f_1' : - 'quay.io/biocontainers/bioconductor-dropletutils:1.18.0--r42hf17093f_1' }" - - input: - // inputs from cellranger nf-core module does not come in a single sample dir - // for each sample, the sub-folders and files come directly in array. - tuple val(meta), path(inputs) - - output: - tuple val(meta), path("emptydrops_filtered"), emit: filtered_matrices - - when: - task.ext.when == null || task.ext.when - - script: - if (params.aligner in ["cellranger", "cellrangermulti"]) { - - matrix = "matrix.mtx.gz" - barcodes = "barcodes.tsv.gz" - features = "features.tsv.gz" - - } else if (params.aligner == "kallisto") { - - matrix = "counts_unfiltered/*.mtx" - barcodes = "counts_unfiltered/*.barcodes.txt" - features = "counts_unfiltered/*.genes.names.txt" - - // kallisto allows the following workflows: ["standard", "lamanno", "nac"] - // lamanno creates "spliced" and "unspliced" - // nac creates "nascent", "ambiguous" "mature" - // also, lamanno produces a barcodes and genes file for both spliced and unspliced - // while nac keep only one for all the different .mtx files produced - kb_non_standard_files = "" - if (params.kb_workflow == "lamanno") { - kb_non_standard_files = "spliced unspliced" - matrix = "counts_unfiltered/\${input_type}.mtx" - barcodes = "counts_unfiltered/\${input_type}.barcodes.txt" - features = "counts_unfiltered/\${input_type}.genes.txt" - } - if (params.kb_workflow == "nac") { - kb_non_standard_files = "nascent ambiguous mature" - matrix = "counts_unfiltered/*\${input_type}.mtx" - features = "counts_unfiltered/*.genes.txt" - } // barcodes tsv has same pattern as standard workflow - - } else if (params.aligner == "alevin") { - - matrix = "*_alevin_results/af_quant/alevin/quants_mat.mtx" - barcodes = "*_alevin_results/af_quant/alevin/quants_mat_rows.txt" - features = "*_alevin_results/af_quant/alevin/quants_mat_cols.txt" - - } else if (params.aligner == 'star') { - - matrix = "raw/matrix.mtx.gz" - barcodes = "raw/barcodes.tsv.gz" - features = "raw/features.tsv.gz" - - } - - // - // run script - // - if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') - """ - # convert file types - for input_type in ${kb_non_standard_files} ; do - mkdir -p emptydrops_filtered/\${input_type} - emptydrops_cell_calling.R \\ - ${matrix} \\ - ${barcodes} \\ - ${features} \\ - emptydrops_filtered/\${input_type} \\ - ${params.aligner} \\ - 0 - done - """ - - else - """ - mkdir emptydrops_filtered/ - emptydrops_cell_calling.R \\ - $matrix \\ - $barcodes \\ - $features \\ - emptydrops_filtered \\ - ${params.aligner} \\ - 0 - """ - - stub: - """ - mkdir emptydrops_filtered - touch emptydrops_filtered/empty_file - """ -} diff --git a/modules/local/BKP/mtx_to_h5ad.nf b/modules/local/BKP/mtx_to_h5ad.nf deleted file mode 100644 index 61e06e91..00000000 --- a/modules/local/BKP/mtx_to_h5ad.nf +++ /dev/null @@ -1,139 +0,0 @@ -process MTX_TO_H5AD { - tag "$meta.id" - label 'process_medium' - - conda "conda-forge::scanpy conda-forge::python-igraph conda-forge::leidenalg" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : - 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" - - input: - // inputs from cellranger nf-core module does not come in a single sample dir - // for each sample, the sub-folders and files come directly in array. - tuple val(meta), path(inputs) - path txp2gene - path star_index - - output: - tuple val(input_type), path("${meta.id}/*h5ad") , emit: h5ad - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - // Get a file to check input type. Some aligners bring arrays instead of a single file. - def input_to_check = (inputs instanceof String) ? inputs : inputs[0] - - // check input type of inputs - input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' - if ( params.aligner == 'alevin' ) { input_type = 'raw' } // alevin has its own filtering methods and mostly output a single mtx, 'raw' here means, the base tool output - if (input_to_check.toUriString().contains('emptydrops')) { input_type = 'custom_emptydrops_filter' } - - // def file paths for aligners. Cellranger is normally converted with the .h5 files - // However, the emptydrops call, always generate .mtx files, thus, cellranger 'emptydrops' required a parsing - if (params.aligner in [ 'cellranger', 'cellrangerarc', 'cellrangermulti' ] && input_type == 'custom_emptydrops_filter') { - - aligner = 'cellranger' - txp2gene = '' - star_index = '' - mtx_matrix = "emptydrops_filtered/matrix.mtx" - barcodes_tsv = "emptydrops_filtered/barcodes.tsv" - features_tsv = "emptydrops_filtered/features.tsv" - - } else if (params.aligner == 'kallisto') { - - kb_pattern = (input_type == 'raw') ? 'un' : '' - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : "counts_${kb_pattern}filtered" - if ((input_type == 'custom_emptydrops_filter') && (params.kb_workflow != 'standard')) { mtx_dir = 'emptydrops_filtered/\${input_type}' } // dir has subdirs for non-standard workflows - mtx_matrix = "${mtx_dir}/*.mtx" - barcodes_tsv = "${mtx_dir}/*.barcodes.txt" - features_tsv = "${mtx_dir}/*.genes.names.txt" - - // kallisto allows the following workflows: ["standard", "lamanno", "nac"] - // lamanno creates "spliced" and "unspliced" - // nac creates "nascent", "ambiguous" "mature" - // also, lamanno produces a barcodes and genes file for both spliced and unspliced - // while nac keep only one for all the different .mtx files produced - kb_non_standard_files = "" - if (params.kb_workflow == "lamanno") { - kb_non_standard_files = "spliced unspliced" - matrix = "${mtx_dir}/\${input_type}.mtx" - barcodes_tsv = "${mtx_dir}/\${input_type}.barcodes.txt" - features_tsv = "${mtx_dir}/\${input_type}.genes.txt" - } - if (params.kb_workflow == "nac") { - kb_non_standard_files = "nascent ambiguous mature" - matrix = "${mtx_dir}/*\${input_type}.mtx" - features_tsv = "${mtx_dir}/*.genes.txt" - } // barcodes tsv has same pattern as standard workflow - - } else if (params.aligner == 'alevin') { - - // alevin does not have filtered/unfiltered results - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : '*_alevin_results/af_quant/alevin' - mtx_matrix = "${mtx_dir}/quants_mat.mtx" - barcodes_tsv = "${mtx_dir}/quants_mat_rows.txt" - features_tsv = "${mtx_dir}/quants_mat_cols.txt" - - } else if (params.aligner == 'star') { - - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : "${input_type}" - suffix = (input_type == 'custom_emptydrops_filter') ? '' : '.gz' - mtx_matrix = "${mtx_dir}/matrix.mtx${suffix}" - barcodes_tsv = "${mtx_dir}/barcodes.tsv${suffix}" - features_tsv = "${mtx_dir}/features.tsv${suffix}" - - } - - // - // run script - // - if (params.aligner in [ "cellranger", "cellrangerarc", "cellrangermulti"] && input_type != 'custom_emptydrops_filter') - """ - # convert file types - mtx_to_h5ad.py \\ - --aligner cellranger \\ - --input *${input_type}_feature_bc_matrix.h5 \\ - --sample ${meta.id} \\ - --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad - """ - - else if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') - """ - # convert file types - for input_type in ${kb_non_standard_files} ; do - mtx_to_h5ad.py \\ - --aligner ${params.aligner} \\ - --sample ${meta.id} \\ - --input ${matrix} \\ - --barcode ${barcodes_tsv} \\ - --feature ${features_tsv} \\ - --txp2gene ${txp2gene} \\ - --star_index ${star_index} \\ - --out ${meta.id}/${meta.id}_\${input_type}_matrix.h5ad ; - done - """ - - else - """ - # convert file types - mtx_to_h5ad.py \\ - --task_process ${task.process} \\ - --aligner ${params.aligner} \\ - --sample ${meta.id} \\ - --input $mtx_matrix \\ - --barcode $barcodes_tsv \\ - --feature $features_tsv \\ - --txp2gene ${txp2gene} \\ - --star_index ${star_index} \\ - --out ${meta.id}/${meta.id}_${input_type}_matrix.h5ad - """ - - stub: - """ - mkdir ${meta.id} - touch ${meta.id}/${meta.id}_matrix.h5ad - touch versions.yml - """ -} diff --git a/modules/local/BKP/mtx_to_seurat.nf b/modules/local/BKP/mtx_to_seurat.nf deleted file mode 100644 index 21dea175..00000000 --- a/modules/local/BKP/mtx_to_seurat.nf +++ /dev/null @@ -1,127 +0,0 @@ -process MTX_TO_SEURAT { - tag "$meta.id" - label 'process_medium' - - conda "r-seurat" - container "nf-core/seurat:4.3.0" - - input: - // inputs from cellranger nf-core module does not come in a single sample dir - // for each sample, the sub-folders and files come directly in array. - tuple val(meta), path(inputs) - - output: - path "${meta.id}/*.rds", emit: seuratObjects - path "versions.yml", emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def aligner = params.aligner - - - // Get a file to check input type. Some aligners bring arrays instead of a single file. - def input_to_check = (inputs instanceof String) ? inputs : inputs[0] - - // check input type of inputs - def is_emptydrops = '0' - input_type = (input_to_check.toUriString().contains('unfiltered') || input_to_check.toUriString().contains('raw')) ? 'raw' : 'filtered' - if ( params.aligner == 'alevin' ) { input_type = 'raw' } // alevin has its own filtering methods and mostly output a single mtx, raw here means, the base tool output - if (input_to_check.toUriString().contains('emptydrops')) { - input_type = 'custom_emptydrops_filter' - is_emptydrops = '--is_emptydrops' - } - - // def file paths for aligners. Cellranger is normally converted with the .h5 files - // However, the emptydrops call, always generate .mtx files, thus, cellranger 'emptydrops' required a parsing - if (params.aligner in [ "cellranger", "cellrangerarc", "cellrangermulti" ]) { - - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered/' : '' - matrix = "${mtx_dir}matrix.mtx*" - barcodes = "${mtx_dir}barcodes.tsv*" - features = "${mtx_dir}features.tsv*" - - } else if (params.aligner == 'kallisto') { - - kb_pattern = (input_type == 'raw') ? 'un' : '' - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : "counts_${kb_pattern}filtered" - if ((input_type == 'custom_emptydrops_filter') && (params.kb_workflow != 'standard')) { mtx_dir = 'emptydrops_filtered/\${input_type}' } // dir has subdirs for non-standard workflows - matrix = "${mtx_dir}/*.mtx" - barcodes = "${mtx_dir}/*.barcodes.txt" - features = "${mtx_dir}/*.genes.names.txt" - - // kallisto allows the following workflows: ["standard", "lamanno", "nac"] - // lamanno creates "spliced" and "unspliced" - // nac creates "nascent", "ambiguous" "mature" - // also, lamanno produces a barcodes and genes file for both spliced and unspliced - // while nac keep only one for all the different .mtx files produced - kb_non_standard_files = "" - if (params.kb_workflow == "lamanno") { - kb_non_standard_files = "spliced unspliced" - matrix = "${mtx_dir}/\${input_type}.mtx" - barcodes = "${mtx_dir}/\${input_type}.barcodes.txt" - features = "${mtx_dir}/\${input_type}.genes.txt" - } - if (params.kb_workflow == "nac") { - kb_non_standard_files = "nascent ambiguous mature" - matrix = "${mtx_dir}/*\${input_type}.mtx" - features = "${mtx_dir}/*.genes.txt" - } // barcodes tsv has same pattern as standard workflow - - } else if (params.aligner == "alevin") { - - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : '*_alevin_results/af_quant/alevin' - matrix = "${mtx_dir}/quants_mat.mtx" - barcodes = "${mtx_dir}/quants_mat_rows.txt" - features = "${mtx_dir}/quants_mat_cols.txt" - - } else if (params.aligner == 'star') { - - mtx_dir = (input_type == 'custom_emptydrops_filter') ? 'emptydrops_filtered' : "${input_type}" - suffix = (input_type == 'custom_emptydrops_filter') ? '' : '.gz' - matrix = "${mtx_dir}/matrix.mtx${suffix}" - barcodes = "${mtx_dir}/barcodes.tsv${suffix}" - features = "${mtx_dir}/features.tsv${suffix}" - - } - - // - // run script - // - """ - mkdir ${meta.id} - """ - - if (params.aligner == 'kallisto' && params.kb_workflow != 'standard') - """ - # convert file types - for input_type in ${kb_non_standard_files} ; do - mtx_to_seurat.R \\ - ${matrix} \\ - ${barcodes} \\ - ${features} \\ - ${meta.id}/${meta.id}_\${input_type}_matrix.rds \\ - ${aligner} \\ - ${is_emptydrops} - done - """ - - else - """ - mtx_to_seurat.R \\ - $matrix \\ - $barcodes \\ - $features \\ - ${meta.id}/${meta.id}_${input_type}_matrix.rds \\ - ${aligner} \\ - ${is_emptydrops} - """ - - stub: - """ - mkdir ${meta.id} - touch ${meta.id}/${meta.id}_matrix.rds - touch versions.yml - """ -} From dd41a8ba8eb9f461c70332fa00873f36632cdc6a Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 29 Nov 2024 08:49:57 +0100 Subject: [PATCH 124/147] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d169db..750a7028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--save_align_intermeds` parameter that publishes BAM files to the output directory (for `starsolo`, `cellranger` and `cellranger multi`) ([#384](https://github.com/nf-core/scrnaseq/issues/384)) - Added support for pre-built indexes in `genomes.config` file for `cellranger`, `cellranger-arc`, `simpleaf` and `simpleaf txp2gene` ([#371](https://github.com/nf-core/scrnaseq/issues/371)) +- Cleanup and fix bugs in matrix conversion code, and change to use anndataR for conversions, and cellbender for emptydrops call. ([#369](https://github.com/nf-core/scrnaseq/pull/369)) +- Fix problem with `test_full` that was not running out of the box, since code was trying to overwrite parameters in the workflow, which is not possible ([#366](https://github.com/nf-core/scrnaseq/issues/366)) ## v2.7.1 - 2024-08-13 From 9c6d4054dbe52022adda56371d6befde7ec59e0d Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 29 Nov 2024 08:51:32 +0100 Subject: [PATCH 125/147] add TODO --- subworkflows/local/emptydrops_removal.nf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/subworkflows/local/emptydrops_removal.nf b/subworkflows/local/emptydrops_removal.nf index 2ccacc26..7d63e86f 100644 --- a/subworkflows/local/emptydrops_removal.nf +++ b/subworkflows/local/emptydrops_removal.nf @@ -1,6 +1,10 @@ include { CELLBENDER_REMOVEBACKGROUND } from '../../modules/nf-core/cellbender/removebackground' include { ADATA_BARCODES } from '../../modules/local/adata_barcodes' +// +// TODO: Make it a nf-core subworkflow to be shared by scrnaseq and scdownstream pipelines. +// + workflow EMPTY_DROPLET_REMOVAL { take: ch_unfiltered From f07cac6c6c77a7dcf231fea0b5410b38aa6c72aa Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 29 Nov 2024 08:58:08 +0100 Subject: [PATCH 126/147] update emptydrops documentation --- docs/output.md | 16 ++++++++-------- nextflow_schema.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/output.md b/docs/output.md index a5292336..38dbda10 100644 --- a/docs/output.md +++ b/docs/output.md @@ -19,7 +19,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [Cellranger ARC](#cellranger-arc) - [Cellranger multi](#cellranger-multi) - [UniverSC](#universc) - - [Custom emptydrops filter](#custom-emptydrops-filter) + - [Cellbender emptydrops filter](#cellbender-emptydrops-filter) - [Other output data](#other-output-data) - [MultiQC](#multiqc) - [Pipeline information](#pipeline-information) @@ -141,15 +141,15 @@ Battenberg, K., Kelly, S.T., Ras, R.A., Hetherington, N.A., Hayashi, K., and Min - Contains the mapped BAM files, filtered and unfiltered HDF5 matrices and output metrics created by the open-source implementation of Cell Ranger run via UniverSC -## Custom emptydrops filter +## Cellbender emptydrops filter -The pipeline also possess a module to perform empty-drops calling and filtering with a custom-made script that uses a library called `bioconductor-dropletutils` that is available in `bioconda`. The process is simple, it takes a raw/unfiltered matrix file, and performs the empty-drops calling and filtering on it, generating another matrix file. +The pipeline also possess a subworkflow imported from scdownstream to perform emptydrops calling and filtering using [cellbender](https://github.com/broadinstitute/CellBender). The process is simple, it takes a raw/unfiltered matrix file, and performs the emptydrops calling and filtering on it, generating another matrix file. > Users can turn it of with `--skip_emptydrops`. -**Output directory: `results/${params.aligner}/emptydrops_filtered`** +**Output directory: `results/${params.aligner}/${meta.id}/emptydrops_filter`** -- Contains the empty-drops filtered matrices results generated by the `bioconductor-dropletutils` custom script +- Contains the emptydrops filtered matrices results generated by the cellbender subworkflow. ## Other output data @@ -170,15 +170,15 @@ The pipeline also possess a module to perform empty-drops calling and filtering - `.mtx` files converted to R native data format, rds, using the [Seurat package](https://github.com/satijalab/seurat) - One per sample -Because the pipeline has both the data directly from the aligners, and from the custom empty-drops filtering module the conversion modules were modified to understand the difference between raw/filtered from the aligners itself and filtered from the custom empty-drops module. So, to try to avoid confusion by the user, we added "suffixes" to the generated converted files so that we have provenance from what input it came from. +Because the pipeline has both the data directly from the aligners, and from the cellbender empty-drops filtering module, the conversion modules were modified to understand the difference between raw/filtered from the aligners itself and filtered from the empty-drops module. So, to try to avoid confusion by the user, we added "suffixes" to the generated converted files so that we have provenance from what input it came from. -So, the conversion modules generate data with the following syntax: **`*_{raw,filtered,custom_emptydrops_filter}_matrix.{h5ad,rds}`**. With the following meanings: +So, the conversion modules generate data with the following syntax: **`*_{raw,filtered,emptydrops_filter}_matrix.{h5ad,rds}`**. With the following meanings: | suffix | meaning | | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | | raw | Conversion of the raw/unprocessed matrix generated by the tool. It is also used for tools that generate only one matrix, such as alevin. | | filtered | Conversion of the filtered/processed matrix generated by the tool | -| custom_emptydrops_filter | Conversion of the matrix that was generated by the new custom empty drops filter module | +| emptydrops_filter | Conversion of the matrix that was generated by the cellbender empty drops filter module | > Some aligners, like `alevin` do not produce both raw&filtered matrices. When aligners give only one output, they are treated with the `raw` suffix. Some aligners may have an option to give both raw&filtered and only one, like `kallisto`. Be aware when using the tools. diff --git a/nextflow_schema.json b/nextflow_schema.json index 935d4277..c09875be 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -90,7 +90,7 @@ }, "skip_emptydrops": { "type": "boolean", - "description": "Skip custom empty drops filter module" + "description": "Skip cellbender empty drops filter subworkflow" } } }, From d12f64defb16aad6ce2c2e6344ed228389d05af9 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 09:51:44 +0000 Subject: [PATCH 127/147] update cellranger snaps --- tests/main_pipeline_cellranger.nf.test.snap | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/main_pipeline_cellranger.nf.test.snap b/tests/main_pipeline_cellranger.nf.test.snap index a4cb0d14..1379aff8 100644 --- a/tests/main_pipeline_cellranger.nf.test.snap +++ b/tests/main_pipeline_cellranger.nf.test.snap @@ -13,19 +13,19 @@ "barcodes.tsv.gz:md5,081f72b5252ccaf5ffd535ffbd235c4c", "features.tsv.gz:md5,99e453cb1443a3e43e99405184e51a5e", "matrix.mtx.gz:md5,58182db2706d532ec970526de3d3b70f", - "Sample_X_raw_matrix.seurat.rds:md5,9313e78e79afc6003c75033529d1a9dd", - "Sample_X_raw_matrix.sce.rds:md5,e834517ca64a5c3f84265a6aa287086b", - "Sample_Y_raw_matrix.seurat.rds:md5,e6a9898842cd9090c2608795a8e266ee", - "Sample_Y_raw_matrix.sce.rds:md5,d8aaa5c6ffd2092b27716c6106b047e9", - "Sample_X_filtered_matrix.seurat.rds:md5,68c0f673aeb8b8391930e534ca0b2653", - "Sample_X_filtered_matrix.sce.rds:md5,52619bbf52478c4fdca8cb62606a567c", - "Sample_Y_filtered_matrix.seurat.rds:md5,bbb40897f4fabf9db1124e4fc28b624e", - "Sample_Y_filtered_matrix.sce.rds:md5,aa47704a2d43addbe37cb63cf977ac7b" + "Sample_X_raw_matrix.seurat.rds:md5,155faccf5164a5c56819b267dee0ebb1", + "Sample_X_raw_matrix.sce.rds:md5,4bfef42037307e73f0135abb2373a21e", + "Sample_Y_raw_matrix.seurat.rds:md5,3f4a3e6529b10c646fd08173d5baa339", + "Sample_Y_raw_matrix.sce.rds:md5,061ab8ba3ed28e6312c0367a2a9dfeb3", + "Sample_X_filtered_matrix.seurat.rds:md5,847448239100d08e3ca44017f93ca05d", + "Sample_X_filtered_matrix.sce.rds:md5,797244c2cd63f2b814f2a0cd6c3f080e", + "Sample_Y_filtered_matrix.seurat.rds:md5,50e765e4559c94edd23b123f9232075f", + "Sample_Y_filtered_matrix.sce.rds:md5,dcf9ce35fba58c2b04ca72703b483804" ], "meta": { "nf-test": "0.9.0", - "nextflow": "24.10.1" + "nextflow": "24.10.2" }, - "timestamp": "2024-11-26T09:27:10.525261276" + "timestamp": "2024-11-29T07:53:03.653246538" } -} +} \ No newline at end of file From a6f47f5b59c56b80407d9ad316405ffc8abd47a8 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 10:14:23 +0000 Subject: [PATCH 128/147] update kallisto snaps --- tests/main_pipeline_kallisto.nf.test.snap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/main_pipeline_kallisto.nf.test.snap b/tests/main_pipeline_kallisto.nf.test.snap index ae83ace4..6629f459 100644 --- a/tests/main_pipeline_kallisto.nf.test.snap +++ b/tests/main_pipeline_kallisto.nf.test.snap @@ -7,15 +7,15 @@ "cells_x_genes.barcodes.txt:md5,a8cf7ea4b2d075296a94bf066a64b7a4", "cells_x_genes.genes.txt:md5,acd9d00120f52031974b2add3e7521b6", "cells_x_genes.mtx:md5,abd83de117204d0a77df3c92d00cc025", - "Sample_X_raw_matrix.seurat.rds:md5,2e93382c175b6b4f4a94e3a88f8a7190", - "Sample_X_raw_matrix.sce.rds:md5,27069a3772fbe4f6577d7ec0ddf29666", - "Sample_Y_raw_matrix.seurat.rds:md5,74f16124176dee919ca91d9890254b39", - "Sample_Y_raw_matrix.sce.rds:md5,584e0a2dc64798bc8a4e5a070d8a90bc" + "Sample_X_raw_matrix.seurat.rds:md5,6dba7ab652441df6a2b0712c7529053b", + "Sample_X_raw_matrix.sce.rds:md5,3fef29fea599561551a06caa82811e2b", + "Sample_Y_raw_matrix.seurat.rds:md5,a9e9ac3d1bf83f4e791d6f0c3f6540de", + "Sample_Y_raw_matrix.sce.rds:md5,6818392c6b8b65d762521406aa963b2a" ], "meta": { "nf-test": "0.9.0", - "nextflow": "24.10.1" + "nextflow": "24.10.2" }, - "timestamp": "2024-11-26T11:00:26.349961551" + "timestamp": "2024-11-29T09:54:53.033167911" } -} +} \ No newline at end of file From a2c1a11e654f50cb328c0b7c7b5fbd44231985ab Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 10:21:25 +0000 Subject: [PATCH 129/147] update alevin snaps --- tests/main_pipeline_alevin.nf.test.snap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/main_pipeline_alevin.nf.test.snap b/tests/main_pipeline_alevin.nf.test.snap index deb810d8..4138b7e5 100644 --- a/tests/main_pipeline_alevin.nf.test.snap +++ b/tests/main_pipeline_alevin.nf.test.snap @@ -7,15 +7,15 @@ "quants_mat_cols.txt:md5,e9868982c17a330392e38c2a5933cf97", "quants_mat.mtx:md5,54cd12666016adce94c025b2e07f4b02", "quants_mat_rows.txt:md5,6b458a7777260ba90eccbe7919df934b", - "Sample_X_raw_matrix.seurat.rds:md5,8bc70642a88e5025da198d5463faffbe", - "Sample_X_raw_matrix.sce.rds:md5,2236117afee08cdb102e30bf02a51e7b", - "Sample_Y_raw_matrix.seurat.rds:md5,1797b215c555d9eeb58d0896529efbff", - "Sample_Y_raw_matrix.sce.rds:md5,d2a20b7902feefb7b4899caea4328fd9" + "Sample_X_raw_matrix.seurat.rds:md5,708ec66ee15c31c1a09cbaee035a6508", + "Sample_X_raw_matrix.sce.rds:md5,3bed89cd187a3f5385636fd5196ef42d", + "Sample_Y_raw_matrix.seurat.rds:md5,3f031ff7c50ee2a2e13ea86319892ee5", + "Sample_Y_raw_matrix.sce.rds:md5,c83e8e04b8fd4bb4d03313a1348686dc" ], "meta": { "nf-test": "0.9.0", - "nextflow": "24.10.1" + "nextflow": "24.10.2" }, - "timestamp": "2024-11-25T14:08:32.729548697" + "timestamp": "2024-11-29T10:17:18.33882659" } -} +} \ No newline at end of file From d106e6e46e94003d48c5b4a37e475da5105d3a0d Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 14:33:02 +0100 Subject: [PATCH 130/147] Update modules/local/templates/barcodes.py Co-authored-by: Gregor Sturm --- modules/local/templates/barcodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/local/templates/barcodes.py b/modules/local/templates/barcodes.py index 8a9b10a7..73b9a32a 100644 --- a/modules/local/templates/barcodes.py +++ b/modules/local/templates/barcodes.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +"""Subset h5ad to a predefined set of barcodes""" import platform import anndata as ad From b84770eb6b0fee8167faa899a08f943b4b4411f4 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 13:38:42 +0000 Subject: [PATCH 131/147] fix anndatar to nf-core container --- modules/local/anndatar_convert.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/anndatar_convert.nf b/modules/local/anndatar_convert.nf index c84d92dd..f17e0483 100644 --- a/modules/local/anndatar_convert.nf +++ b/modules/local/anndatar_convert.nf @@ -8,7 +8,7 @@ process ANNDATAR_CONVERT { label 'process_medium' - container "docker.io/fmalmeida/anndatar:dev" // TODO: Fix + container "docker.io/nfcore/anndatar:20241129" input: tuple val(meta), path(h5ad) From a2fc47619f8f28fb284260edd4ac4e204226bc79 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 14:03:33 +0000 Subject: [PATCH 132/147] add resources control to cellrangermulti testing profile --- conf/test_cellranger_multi.config | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/conf/test_cellranger_multi.config b/conf/test_cellranger_multi.config index f10550ae..0a229f9c 100644 --- a/conf/test_cellranger_multi.config +++ b/conf/test_cellranger_multi.config @@ -10,6 +10,14 @@ ---------------------------------------------------------------------------------------- */ +process { + resourceLimits = [ + cpus: 4, + memory: '15.GB', + time: '1.h' + ] +} + // shared across profiles params { config_profile_name = 'Test profile (Cellranger Multi)' From d194bfa3c00fbbde9377f2dffccaf41c8e681f92 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 14:20:46 +0000 Subject: [PATCH 133/147] add metadata --- subworkflows/local/align_cellrangermulti.nf | 1 + 1 file changed, 1 insertion(+) diff --git a/subworkflows/local/align_cellrangermulti.nf b/subworkflows/local/align_cellrangermulti.nf index f13c7bf1..53313b2f 100644 --- a/subworkflows/local/align_cellrangermulti.nf +++ b/subworkflows/local/align_cellrangermulti.nf @@ -219,6 +219,7 @@ def parse_demultiplexed_output_channels(in_ch, pattern) { .transpose() // transpose for handling one meta/file pair at a time .map { meta, mtx_files -> def meta_clone = meta.clone() + meta_clone.input_type = pattern.contains('raw_') ? 'raw' : 'filtered' // add metadata for conversion workflow if ( mtx_files.toString().contains("per_sample_outs") ) { def demultiplexed_sample_id = mtx_files.toString().split('/per_sample_outs/')[1].split('/')[0] meta_clone.id = demultiplexed_sample_id.toString() From e9836fdd816869e74887af246fe7694da5a333e1 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 14:29:40 +0000 Subject: [PATCH 134/147] allow regex to also find cellrangermulti results --- modules/local/templates/mtx_to_h5ad_cellranger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/local/templates/mtx_to_h5ad_cellranger.py b/modules/local/templates/mtx_to_h5ad_cellranger.py index 3fe7d287..44d587e6 100755 --- a/modules/local/templates/mtx_to_h5ad_cellranger.py +++ b/modules/local/templates/mtx_to_h5ad_cellranger.py @@ -11,6 +11,7 @@ import anndata from anndata import AnnData import platform +import glob def _mtx_to_adata( input: str, @@ -92,7 +93,7 @@ def input_to_adata( # input_type comes from NF module adata = input_to_adata( - input_data="${meta.input_type}_feature_bc_matrix.h5", + input_data=glob.glob("*${meta.input_type}_feature_bc_matrix.h5")[0], # cellrangermulti has 'sample_' as prefix output="${meta.id}_${meta.input_type}_matrix.h5ad", sample="${meta.id}" ) From 3e3608c704b71c7aa031471cfdf4d3e88e4210ae Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 14:58:14 +0000 Subject: [PATCH 135/147] update cellranger multi nf-test snaps --- tests/main_pipeline_cellrangermulti.nf.test | 154 ++++++++++-------- ...main_pipeline_cellrangermulti.nf.test.snap | 56 ++++--- 2 files changed, 126 insertions(+), 84 deletions(-) diff --git a/tests/main_pipeline_cellrangermulti.nf.test b/tests/main_pipeline_cellrangermulti.nf.test index 9cf4b413..263e486a 100644 --- a/tests/main_pipeline_cellrangermulti.nf.test +++ b/tests/main_pipeline_cellrangermulti.nf.test @@ -18,6 +18,7 @@ nextflow_pipeline { gtf = 'https://ftp.ensembl.org/pub/release-110/gtf/homo_sapiens/Homo_sapiens.GRCh38.110.gtf.gz' aligner = 'cellrangermulti' protocol = 'auto' + skip_emptydrops = true } } @@ -33,12 +34,12 @@ nextflow_pipeline { {assert workflow.success}, // How many tasks were executed? - {assert workflow.trace.tasks().size() == 55}, + {assert workflow.trace.tasks().size() == 85}, // How many results were produced? {assert path("${outputDir}/results_cellrangermulti").list().size() == 4}, {assert path("${outputDir}/results_cellrangermulti/cellrangermulti").list().size() == 5}, - {assert path("${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions").list().size() == 13}, + {assert path("${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions").list().size() == 16}, {assert path("${outputDir}/results_cellrangermulti/cellrangermulti/count").list().size() == 4}, {assert path("${outputDir}/results_cellrangermulti/fastqc").list().size() == 48}, {assert path("${outputDir}/results_cellrangermulti/multiqc").list().size() == 3}, @@ -70,86 +71,111 @@ nextflow_pipeline { // {assert snapshot( // barcodes.tsv.gz files - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_1/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_2/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/barcodes.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/barcodes.tsv.gz" ), // features.tsv.gz files path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_1/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_2/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/features.tsv.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/features.tsv.gz" ), // matrix.mtx.gz files path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/count/sample_raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_1/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_2/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/multi/count/raw_feature_bc_matrix/matrix.mtx.gz" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/count/sample_filtered_feature_bc_matrix/matrix.mtx.gz" ), // metrics_summary.csv files - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/metrics_summary.csv" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/metrics_summary.csv" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/metrics_summary.csv" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/metrics_summary.csv" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Colorectal_BC3/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Liver_BC1/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Ovarian_BC2/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/4PLEX_HUMAN/outs/per_sample_outs/Pancreas_BC4/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K/outs/per_sample_outs/PBMC_10K/metrics_summary.csv" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_1/metrics_summary.csv" ), path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMO/outs/per_sample_outs/PBMC_10K_CMO_PBMCs_human_2/metrics_summary.csv" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/metrics_summary.csv" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/count/PBMC_10K_CMV/outs/per_sample_outs/PBMC_10K_CMV/metrics_summary.csv" ), // .rds files - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/4PLEX_HUMAN/4PLEX_HUMAN_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO/PBMC_10K_CMO_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_1/PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_2/PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_filtered_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_raw_matrix.rds" ), - path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_filtered_matrix.rds" ) + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/4PLEX_HUMAN/4PLEX_HUMAN_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/4PLEX_HUMAN/4PLEX_HUMAN_raw_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Colorectal_BC3/Colorectal_BC3_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Liver_BC1/Liver_BC1_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Ovarian_BC2/Ovarian_BC2_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K/PBMC_10K_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO/PBMC_10K_CMO_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO/PBMC_10K_CMO_raw_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_1/PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_1/PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_2/PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMO_PBMCs_human_2/PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/PBMC_10K_CMV/PBMC_10K_CMV_filtered_matrix.sce.rds" ), + + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_raw_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_raw_matrix.sce.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_filtered_matrix.seurat.rds" ), + path( "${outputDir}/results_cellrangermulti/cellrangermulti/mtx_conversions/Pancreas_BC4/Pancreas_BC4_filtered_matrix.sce.rds" ) ).match()} diff --git a/tests/main_pipeline_cellrangermulti.nf.test.snap b/tests/main_pipeline_cellrangermulti.nf.test.snap index 1594f01e..1b2f1bdd 100644 --- a/tests/main_pipeline_cellrangermulti.nf.test.snap +++ b/tests/main_pipeline_cellrangermulti.nf.test.snap @@ -57,27 +57,43 @@ "metrics_summary.csv:md5,d14d385df3e8e61c924a30cd5b959b86", "metrics_summary.csv:md5,bcd7e1d1854e31a4968d5876eab7d64a", "metrics_summary.csv:md5,98d9d9617c2ca4f614614568e4371d44", - "4PLEX_HUMAN_raw_matrix.rds:md5,16fbbce025edf14080d9652f0b3b7994", - "Colorectal_BC3_raw_matrix.rds:md5,bb6da39a69fac14757be2ef9c1759b24", - "Colorectal_BC3_filtered_matrix.rds:md5,92f5b4a8340a5180dc7ad232a4994ace", - "Liver_BC1_raw_matrix.rds:md5,def4320e149344bf1852304feae7a137", - "Liver_BC1_filtered_matrix.rds:md5,573da7ab560533d6e0930c3b3822f90f", - "Ovarian_BC2_raw_matrix.rds:md5,e43ac66c2bce57d94152e30e5a1af721", - "Ovarian_BC2_filtered_matrix.rds:md5,4368fe37be8ca85850689e70a0a9a5f6", - "PBMC_10K_raw_matrix.rds:md5,d9e9a5d35e6ead35ba5a0d2a9bfdb06d", - "PBMC_10K_filtered_matrix.rds:md5,1866ab65fdfb8dd704485bae222823d2", - "PBMC_10K_CMO_raw_matrix.rds:md5,fb5bc19943ebaf58ffcfbbe796c4baf2", - "PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.rds:md5,725b5310cc5ea04bcce6fd69d92ec36a", - "PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.rds:md5,5a1c0412f9058487f5f0798c28b87d39", - "PBMC_10K_CMV_raw_matrix.rds:md5,a7ee013dcf87bea1e6d589f51ace64ff", - "PBMC_10K_CMV_filtered_matrix.rds:md5,c0b74395b5ecc54dcb321a12f0601777", - "Pancreas_BC4_raw_matrix.rds:md5,0d9640458250fd53c54031382b3b19dc", - "Pancreas_BC4_filtered_matrix.rds:md5,1df6ddb6fc29427fc6240c197447d146" + "4PLEX_HUMAN_raw_matrix.seurat.rds:md5,acc22e948a2250907897f79e008ff3ea", + "4PLEX_HUMAN_raw_matrix.sce.rds:md5,084ac812ebb69ca3152abd2c7226d739", + "Colorectal_BC3_raw_matrix.seurat.rds:md5,0d6a6222daf2b03cba426cb80b86914c", + "Colorectal_BC3_raw_matrix.sce.rds:md5,b00d8244f06eef3e5d0fae39c9e14196", + "Colorectal_BC3_filtered_matrix.seurat.rds:md5,554e8c02aabc9935f7f60b4ccd95790d", + "Colorectal_BC3_filtered_matrix.sce.rds:md5,d18364082406728404deefd7e163e758", + "Liver_BC1_raw_matrix.seurat.rds:md5,826973ce82225e8942a823e18dbe01fd", + "Liver_BC1_raw_matrix.sce.rds:md5,4806e7f7e00af77c9eb0e2666cedc96c", + "Liver_BC1_filtered_matrix.seurat.rds:md5,958cb86208fa8241f84452b92061b534", + "Liver_BC1_filtered_matrix.sce.rds:md5,315bee1c2cfbebd58fbe070756fa578e", + "Ovarian_BC2_raw_matrix.seurat.rds:md5,f107fd3e315a04aec3e6e53650c456f8", + "Ovarian_BC2_raw_matrix.sce.rds:md5,e30869371d77941b1d009fd6983b5c43", + "Ovarian_BC2_filtered_matrix.seurat.rds:md5,c2f00ae90a958938197666f2642697f1", + "Ovarian_BC2_filtered_matrix.sce.rds:md5,0852d41a76b05cae1b6ee8359b7fcb15", + "PBMC_10K_raw_matrix.seurat.rds:md5,97d5d0bc88db1df05b7effcb9dbb31cc", + "PBMC_10K_raw_matrix.sce.rds:md5,4b3fe0fbda8a80eafc372bd895c26ec0", + "PBMC_10K_filtered_matrix.seurat.rds:md5,dc780d878e2abae61b10eba5218116b8", + "PBMC_10K_filtered_matrix.sce.rds:md5,1f2f3072ede853dd5f6c47821fc39543", + "PBMC_10K_CMO_raw_matrix.seurat.rds:md5,079695fa7ca1190a8467c40ea906ab55", + "PBMC_10K_CMO_raw_matrix.sce.rds:md5,1b5f531b29b6f35a2fc17e8e679b1f38", + "PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.seurat.rds:md5,8dc6983ed48e114e997111bb9a3cb08d", + "PBMC_10K_CMO_PBMCs_human_1_filtered_matrix.sce.rds:md5,343dff9ef666aceafc5bc3f5da4dfb67", + "PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.seurat.rds:md5,d1d8205885be044a721295205e34aeea", + "PBMC_10K_CMO_PBMCs_human_2_filtered_matrix.sce.rds:md5,a6598288ca1afd8590e25ea3095de929", + "PBMC_10K_CMV_raw_matrix.seurat.rds:md5,7b731e32655cace681ace140e3ef9af3", + "PBMC_10K_CMV_raw_matrix.sce.rds:md5,75baf36779b04ed941b97b644125a2ff", + "PBMC_10K_CMV_filtered_matrix.seurat.rds:md5,4dfa3f7aa87706e23a04248349292dc1", + "PBMC_10K_CMV_filtered_matrix.sce.rds:md5,cb78864bde8833c2e6323ff01eef3c15", + "Pancreas_BC4_raw_matrix.seurat.rds:md5,7f972f40b05824a3cff5449a8f9f8b61", + "Pancreas_BC4_raw_matrix.sce.rds:md5,4af114f068fc9bab8cceb3f25a9b6d9a", + "Pancreas_BC4_filtered_matrix.seurat.rds:md5,83b0c41b147c45ce0204eb14e6bca9d9", + "Pancreas_BC4_filtered_matrix.sce.rds:md5,b22101dc4bd007f03d5254e0211961fd" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.2" }, - "timestamp": "2024-05-06T11:40:54.060867657" + "timestamp": "2024-11-29T14:57:39.288233009" } -} +} \ No newline at end of file From 2830093d4488ee1ee1b832cc6c4b3f1d0c554599 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 15:35:11 +0000 Subject: [PATCH 136/147] update star nf-test snaps --- tests/main_pipeline_star.nf.test.snap | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/main_pipeline_star.nf.test.snap b/tests/main_pipeline_star.nf.test.snap index ac351097..e13a570a 100644 --- a/tests/main_pipeline_star.nf.test.snap +++ b/tests/main_pipeline_star.nf.test.snap @@ -11,19 +11,19 @@ "matrix.mtx.gz:md5,0ae080bd0002e350531a5816e159345e", "features.tsv.gz:md5,99e453cb1443a3e43e99405184e51a5e", "barcodes.tsv.gz:md5,9b695b0b91bcb146ec9c4688ca10a690", - "Sample_X_raw_matrix.seurat.rds:md5,db2aa4392b23bd2a0cf8261d9fa5ac1e", - "Sample_X_raw_matrix.sce.rds:md5,67894a2ca56c5d8f21097e7c93a0c61b", - "Sample_Y_raw_matrix.seurat.rds:md5,ea5351c9f871d535c9363aac56fb3e6f", - "Sample_Y_raw_matrix.sce.rds:md5,bf0dcb00f76fc190e0cdeac23128bdc4", - "Sample_X_filtered_matrix.seurat.rds:md5,97d5b28505fafe07bdec0ff942a782a7", - "Sample_X_filtered_matrix.sce.rds:md5,fb72a298bae9dd56bf19f2cf8b56fad8", - "Sample_Y_filtered_matrix.seurat.rds:md5,5f1bb403d80a8c2afd099373d20bfba7", - "Sample_Y_filtered_matrix.sce.rds:md5,a6887a7577a6121ca0a37f382beae820" + "Sample_X_raw_matrix.seurat.rds:md5,51a863d56f6d4c9df7161d574fecfd33", + "Sample_X_raw_matrix.sce.rds:md5,2135e075bfb5043b78841de9bd261a3c", + "Sample_Y_raw_matrix.seurat.rds:md5,f177854d779169f6f0e1c628f154a656", + "Sample_Y_raw_matrix.sce.rds:md5,86fc59316bc9083c510aa0d3a0a24ffb", + "Sample_X_filtered_matrix.seurat.rds:md5,a035c9ead72baa36f2ef298dc02d5e1b", + "Sample_X_filtered_matrix.sce.rds:md5,8fd8b3a602a00578e5a72f1fd6792e05", + "Sample_Y_filtered_matrix.seurat.rds:md5,42823b941a1c375473f454980eafbf0b", + "Sample_Y_filtered_matrix.sce.rds:md5,254fd8a73aa0e0ca4bee57855c9cde30" ], "meta": { "nf-test": "0.9.0", - "nextflow": "24.10.1" + "nextflow": "24.10.2" }, - "timestamp": "2024-11-25T15:19:07.532050762" + "timestamp": "2024-11-29T15:32:12.524479228" } -} +} \ No newline at end of file From fae53a2c72b0fae494a8cc447013df79f9c31930 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Fri, 29 Nov 2024 15:53:31 +0000 Subject: [PATCH 137/147] prettier fix --- docs/output.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/output.md b/docs/output.md index 38dbda10..8be54c40 100644 --- a/docs/output.md +++ b/docs/output.md @@ -174,10 +174,10 @@ Because the pipeline has both the data directly from the aligners, and from the So, the conversion modules generate data with the following syntax: **`*_{raw,filtered,emptydrops_filter}_matrix.{h5ad,rds}`**. With the following meanings: -| suffix | meaning | -| :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | -| raw | Conversion of the raw/unprocessed matrix generated by the tool. It is also used for tools that generate only one matrix, such as alevin. | -| filtered | Conversion of the filtered/processed matrix generated by the tool | +| suffix | meaning | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | +| raw | Conversion of the raw/unprocessed matrix generated by the tool. It is also used for tools that generate only one matrix, such as alevin. | +| filtered | Conversion of the filtered/processed matrix generated by the tool | | emptydrops_filter | Conversion of the matrix that was generated by the cellbender empty drops filter module | > Some aligners, like `alevin` do not produce both raw&filtered matrices. When aligners give only one output, they are treated with the `raw` suffix. Some aligners may have an option to give both raw&filtered and only one, like `kallisto`. Be aware when using the tools. From 84a83c29cac1f0f62191945fce7e41c6bbf74e41 Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Fri, 29 Nov 2024 18:07:16 +0100 Subject: [PATCH 138/147] avoid fetching star index from iGenomes due version incompatibility --- conf/test_full.config | 4 +--- workflows/scrnaseq.nf | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/conf/test_full.config b/conf/test_full.config index cd215505..dd838978 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -18,9 +18,7 @@ params { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/scrnaseq/samplesheet_2.0_full.csv' // Genome references - // genome = 'GRCh38' // TODO: Fix: incompatible with new star version - fasta = 'https://ftp.ensembl.org/pub/release-110/fasta/homo_sapiens/dna/Homo_sapiens.GRCh38.dna.primary_assembly.fa.gz' - gtf = 'https://ftp.ensembl.org/pub/release-110/gtf/homo_sapiens/Homo_sapiens.GRCh38.110.gtf.gz' + genome = 'GRCh38' aligner = 'star' protocol = '10XV2' diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 07fb2517..375ad79d 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -43,7 +43,7 @@ workflow SCRNASEQ { // we cannot overwrite params in the workflow (they stay null as coming from the config file) def genome_fasta = params.fasta ?: getGenomeAttribute('fasta') def gtf = params.gtf ?: getGenomeAttribute('gtf') - def star_index = params.star_index ?: getGenomeAttribute('star') + def star_index = params.star_index // ?: getGenomeAttribute('star') TODO: Currently not fetching iGenomes star index due version incompatibility def salmon_index = params.salmon_index ?: getGenomeAttribute('simpleaf') def txp2gene = params.txp2gene ?: getGenomeAttribute('simpleaf_tx2pgene') From b498ddc23f10f82f2c2efed511a5066d1e8c932d Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Mon, 2 Dec 2024 14:57:25 +0100 Subject: [PATCH 139/147] add .var_names_make_unique() to star code which was missing --- modules/local/templates/mtx_to_h5ad_star.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/local/templates/mtx_to_h5ad_star.py b/modules/local/templates/mtx_to_h5ad_star.py index a9a80372..d90a7d50 100755 --- a/modules/local/templates/mtx_to_h5ad_star.py +++ b/modules/local/templates/mtx_to_h5ad_star.py @@ -67,6 +67,7 @@ def input_to_adata( adata.var["gene_symbol"] = adata.var.index adata.var['gene_versions'] = adata.var["gene_ids"] adata.var.index = adata.var['gene_versions'].str.split('.').str[0].values + adata.var_names_make_unique() # in case user does not use ensembl references, names might not be unique # write results adata.write_h5ad(f"{output}") From ba38a9c89256a44c37b70f720b29567a246795cb Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 3 Dec 2024 08:59:20 +0100 Subject: [PATCH 140/147] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 750a7028..9b81ce8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Remove universc workflow from pipeline ([#289](https://github.com/nf-core/scrnaseq/issues/289)) - Add `--save_align_intermeds` parameter that publishes BAM files to the output directory (for `starsolo`, `cellranger` and `cellranger multi`) ([#384](https://github.com/nf-core/scrnaseq/issues/384)) - Added support for pre-built indexes in `genomes.config` file for `cellranger`, `cellranger-arc`, `simpleaf` and `simpleaf txp2gene` ([#371](https://github.com/nf-core/scrnaseq/issues/371)) - Cleanup and fix bugs in matrix conversion code, and change to use anndataR for conversions, and cellbender for emptydrops call. ([#369](https://github.com/nf-core/scrnaseq/pull/369)) From 0a481579973c0c31e0e347496e8efa8ddfe11f3d Mon Sep 17 00:00:00 2001 From: "zxBIB Almeida,Felipe (GCBDS) EXTERNAL" Date: Tue, 3 Dec 2024 09:07:14 +0100 Subject: [PATCH 141/147] remove all universc things --- .github/workflows/awsfulltest.yml | 2 +- .github/workflows/awstest.yml | 2 +- README.md | 4 +- assets/protocols.json | 20 ---- conf/modules.config | 30 ------ docs/output.md | 17 ---- docs/usage.md | 11 +-- modules.json | 5 - modules/nf-core/universc/CITATION.cff | 51 ---------- modules/nf-core/universc/CITATION.md | 37 -------- modules/nf-core/universc/README.md | 116 ----------------------- modules/nf-core/universc/environment.yml | 5 - modules/nf-core/universc/main.nf | 80 ---------------- modules/nf-core/universc/meta.yml | 42 -------- nextflow.config | 3 - nextflow_schema.json | 21 +--- subworkflows/local/align_universc.nf | 52 ---------- workflows/scrnaseq.nf | 17 ---- 18 files changed, 6 insertions(+), 509 deletions(-) delete mode 100644 modules/nf-core/universc/CITATION.cff delete mode 100644 modules/nf-core/universc/CITATION.md delete mode 100644 modules/nf-core/universc/README.md delete mode 100644 modules/nf-core/universc/environment.yml delete mode 100644 modules/nf-core/universc/main.nf delete mode 100644 modules/nf-core/universc/meta.yml delete mode 100644 subworkflows/local/align_universc.nf diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index a0105036..665c7198 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - aligner: ["alevin", "kallisto", "star", "cellranger", "universc"] + aligner: ["alevin", "kallisto", "star", "cellranger"] steps: - uses: octokit/request-action@v2.x id: check_approvals diff --git a/.github/workflows/awstest.yml b/.github/workflows/awstest.yml index d04c973b..fccb7fc0 100644 --- a/.github/workflows/awstest.yml +++ b/.github/workflows/awstest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - aligner: ["alevin", "kallisto", "star", "cellranger", "universc"] + aligner: ["alevin", "kallisto", "star", "cellranger"] steps: # Launch workflow using Seqera Platform CLI tool action - name: Launch workflow via Seqera Platform diff --git a/README.md b/README.md index 59b8ddb4..cee8bd67 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ This is a community effort in building a pipeline capable to support: - STARSolo - Kallisto + BUStools - Cellranger -- UniverSC ## Documentation @@ -63,7 +62,7 @@ nextflow run nf-core/scrnaseq \ --genome_fasta GRCm38.p6.genome.chr19.fa \ --gtf gencode.vM19.annotation.chr19.gtf \ --protocol 10XV2 \ - --aligner \ + --aligner \ --outdir ``` @@ -82,7 +81,6 @@ graph TD A[sc RNA] -->|CellRanger| B(h5ad/seurat/mtx matrices) A[sc RNA] -->|kbpython| B(h5ad/seurat/mtx matrices) A[sc RNA] -->|STARsolo| B(h5ad/seurat/mtx matrices) - A[sc RNA] -->|Universc| B(h5ad/seurat/mtx matrices) ``` Options for the respective alignment method can be found [here](https://github.com/nf-core/scrnaseq/blob/dev/docs/usage.md#aligning-options) to choose between methods. diff --git a/assets/protocols.json b/assets/protocols.json index 0552f8d5..613e9773 100644 --- a/assets/protocols.json +++ b/assets/protocols.json @@ -89,25 +89,5 @@ "smartseq": { "protocol": "SMARTSEQ" } - }, - "universc": { - "auto": { - "protocol": "10x" - }, - "10XV1": { - "protocol": "10x-v1" - }, - "10XV2": { - "protocol": "10x-v2" - }, - "10XV3": { - "protocol": "10x-v3" - }, - "10XV4": { - "protocol": "10x-v4" - }, - "dropseq": { - "protocol": "dropseq" - } } } diff --git a/conf/modules.config b/conf/modules.config index 7c09d8df..a1bfb4af 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -126,36 +126,6 @@ if(params.aligner == "cellrangerarc") { } } -if(params.aligner == "universc") { - process { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - - withName: CELLRANGER_MKGTF { - publishDir = [ - path: "${params.outdir}/cellranger/mkgtf", - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } - ] - ext.args = "--attribute=gene_biotype:protein_coding --attribute=gene_biotype:lncRNA --attribute=gene_biotype:pseudogene" - container = "nf-core/universc:1.2.5.1" - } - withName: CELLRANGER_MKREF { - publishDir = [ - path: "${params.outdir}/cellranger/mkref", - mode: params.publish_dir_mode - ] - container = "nf-core/universc:1.2.5.1" - } - withName: UNIVERSC { - publishDir = [ - path: "${params.outdir}/universc", - mode: params.publish_dir_mode - ] - time = { 240.h * task.attempt } - } - } -} - if (params.aligner == "alevin") { process { withName: GFFREAD_TXP2GENE { diff --git a/docs/output.md b/docs/output.md index 8be54c40..cc3abf08 100644 --- a/docs/output.md +++ b/docs/output.md @@ -18,7 +18,6 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [Cellranger](#cellranger) - [Cellranger ARC](#cellranger-arc) - [Cellranger multi](#cellranger-multi) - - [UniverSC](#universc) - [Cellbender emptydrops filter](#cellbender-emptydrops-filter) - [Other output data](#other-output-data) - [MultiQC](#multiqc) @@ -125,22 +124,6 @@ for the corresponding documentation. - Overall same output structure as cellranger. In case of multiplexed samples there will be one ouput folder for each demultiplexed sample, and one containing all (non-demultiplexed) cells. -## UniverSC - -UniverSC is a wrapper that calls an open-source implementation of Cell Ranger v3.0.2 and adjusts run parameters for compatibility with a wide ranger of technologies. -Custom inputs and at least 40 preset technologies are supported. UniverSC is developed independently from 10X Genomics and all software are not subject -to the 10X Genomics End User License Agreement which restricts usage on other platforms. Therefore in principle UniverSC can be run on any scRNA-Seq technology -without restrictions to align reads, generate feature-barcode matrices, perform clustering and other secondary analysis. -See [UniverSC](https://github.com/minoda-lab/universc) for more information on UniverSC. - -UniverSC has been published in _Nature Communications_. - -Battenberg, K., Kelly, S.T., Ras, R.A., Hetherington, N.A., Hayashi, K., and Minoda, A. (2022) A flexible cross-platform single-cell data processing pipeline. Nat Commun 13(1): 1-7. https://doi.org/10.1038/s41467-022-34681-z - -**Output directory: `results/universc`** - -- Contains the mapped BAM files, filtered and unfiltered HDF5 matrices and output metrics created by the open-source implementation of Cell Ranger run via UniverSC - ## Cellbender emptydrops filter The pipeline also possess a subworkflow imported from scdownstream to perform emptydrops calling and filtering using [cellbender](https://github.com/broadinstitute/CellBender). The process is simple, it takes a raw/unfiltered matrix file, and performs the emptydrops calling and filtering on it, generating another matrix file. diff --git a/docs/usage.md b/docs/usage.md index 0d841854..4040423b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -58,10 +58,8 @@ Other aligner options for running the pipeline are: - [Cellranger](https://support.10xgenomics.com/single-cell-gene-expression/software/pipelines/latest/what-is-cell-ranger) to perform both alignment and downstream analysis. - `--aligner cellranger` - [Cellranger Multi](https://www.10xgenomics.com/support/software/cell-ranger/latest/analysis/running-pipelines/cr-5p-multi#what) to perform the alignment and downstream analysis of samples with multiple libraries at the same time using Feature Barcode technology that enables simultaneous profiling of the V(D)J repertoire, cell surface protein, antigen, and gene expression (GEX) data. -- [UniverSC](https://github.com/minoda-lab/universc) to run an open-source version of Cell Ranger on any technology - - '--aligner universc` -### If using cellranger or universc +### If using cellranger This pipeline automatically renames input FASTQ files to follow the [naming convention by 10x](https://support.10xgenomics.com/single-cell-gene-expression/software/pipelines/latest/using/fastq-input): @@ -75,7 +73,6 @@ For more details, see - [this issue](https://github.com/nf-core/scrnaseq/issues/241), discussing various mechanisms to deal with non-conformant filenames - [the README of the cellranger/count module](https://github.com/nf-core/modules/blob/master/modules/nf-core/cellranger/count/README.md) which demonstrates that renaming files does not affect the results. - [the code for renaming files in the cellranger/count module](https://github.com/nf-core/modules/blob/master/modules/nf-core/cellranger/count/templates/cellranger_count.py) -- [the code for renaming files in UniverSC](https://github.com/minoda-lab/universc/blob/99a20652430c1dc9f962536a2793536f643810b7/launch_universc.sh#L1411-L1609) As a sanity check, we verify that filenames of a pair of FASTQ files only differ by `R1`/`R2`. @@ -106,12 +103,6 @@ Simpleaf has the ability to pass custom chemistries to Alevin-fry, in a slighly For more details, see Simpleaf's paper, [He _et al._ 2023](https://doi.org/10.1093/bioinformatics/btad614). -#### UniverSC - -See the [UniverSC GitHub page](https://github.com/minoda-lab/universc#pre-set-configurations) for all supported protocols. - -Currently only 3\' scRNA-Seq parameters are supported in nextflow, although chemistry parameters for 5\' scRNA-Seq and full-length scRNA-Seq libraries are supported by teh container. - ### If using cellranger-arc #### Automatic file name detection diff --git a/modules.json b/modules.json index ad713c9f..d4d95cab 100644 --- a/modules.json +++ b/modules.json @@ -85,11 +85,6 @@ "git_sha": "46eca555142d6e597729fcb682adcc791796f514", "installed_by": ["modules"] }, - "universc": { - "branch": "master", - "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", - "installed_by": ["modules"] - }, "unzip": { "branch": "master", "git_sha": "b0c3ff2485534c09d9debbf20125d9c6b72ce118", diff --git a/modules/nf-core/universc/CITATION.cff b/modules/nf-core/universc/CITATION.cff deleted file mode 100644 index 35e281e6..00000000 --- a/modules/nf-core/universc/CITATION.cff +++ /dev/null @@ -1,51 +0,0 @@ -cff-version: 1.2.0 -message: "If you use this software, please cite it as below." -authors: - - given-names: "S. Thomas" - family-names: "Kelly" - email: "tom.kelly@riken.jp" - affiliation: "Center for Integrative Medical Sciences, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "https://orcid.org/0000-0003-3904-6690" - - family-names: "Battenberg" - given-names: "Kai" - email: "kai.battenberg@riken.jp" - affiliation: "Center for Sustainable Resource Science, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "http://orcid.org/0000-0001-7517-2657" -version: 1.2.5.1 -doi: 10.1101/2021.01.19.427209 -date-released: 2021-02-14 -url: "https://github.com/minoda-lab/universc" -preferred-citation: - type: article - authors: - - given-names: "S. Thomas" - family-names: "Kelly" - email: "tom.kelly@riken.jp" - affiliation: "Center for Integrative Medical Sciences, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "https://orcid.org/0000-0003-3904-6690" - - family-names: "Battenberg" - given-names: "Kai" - email: "kai.battenberg@riken.jp" - affiliation: "Center for Sustainable Resource Science, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "https://orcid.org/http://orcid.org/0000-0001-7517-2657" - - family-names: "Hetherington" - given-names: "Nicola A." - affiliation: "Center for Integrative Medical Sciences, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "http://orcid.org/0000-0001-8802-2906" - - family-names: "Hayashi" - given-names: "Makoto" - affiliation: "Center for Sustainable Resource Science, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "http://orcid.org/0000-0001-6389-4265" - - given-names: "Aki" - family-names: "Minoda" - email: "akiko.minoda@riken.jp" - affiliation: Center for Integrative Medical Sciences, RIKEN, Suehiro-cho-1-7-22, Tsurumi Ward, Yokohama, Japan" - orcid: "http://orcid.org/0000-0002-2927-5791" - doi: "10.1101/2021.01.19.427209" - title: "UniverSC: a flexible cross-platform single-cell data processing pipeline" - year: "2021" - journal: "bioRxiv" - start: 2021.01.19.427209 - volume: - issue: - month: 1 diff --git a/modules/nf-core/universc/CITATION.md b/modules/nf-core/universc/CITATION.md deleted file mode 100644 index 4f420bb8..00000000 --- a/modules/nf-core/universc/CITATION.md +++ /dev/null @@ -1,37 +0,0 @@ -### Citation - -A submission to a journal and biorXiv is in progress. Please cite these when -they are available. Currently, the package can be cited -as follows: - -Kelly, S.T., Battenberg, Hetherington, N.A., K., Hayashi, K., and Minoda, A. (2021) -UniverSC: a flexible cross-platform single-cell data processing pipeline. -bioRxiv 2021.01.19.427209; doi: [https://doi.org/10.1101/2021.01.19.427209](https://doi.org/10.1101/2021.01.19.427209) -package version 1.2.5.1. [https://github.com/minoda-lab/universc](https://github.com/minoda-lab/universc) - -``` -@article {Kelly2021.01.19.427209, - author = {Kelly, S. Thomas and Battenberg, Kai and Hetherington, Nicola A. and Hayashi, Makoto and Minoda, Aki}, - title = {{UniverSC}: a flexible cross-platform single-cell data processing pipeline}, - elocation-id = {2021.01.19.427209}, - year = {2021}, - doi = {10.1101/2021.01.19.427209}, - publisher = {Cold Spring Harbor Laboratory}, - abstract = {Single-cell RNA-sequencing analysis to quantify RNA molecules in individual cells has become popular owing to the large amount of information one can obtain from each experiment. We have developed UniverSC (https://github.com/minoda-lab/universc), a universal single-cell processing tool that supports any UMI-based platform. Our command-line tool enables consistent and comprehensive integration, comparison, and evaluation across data generated from a wide range of platforms.Competing Interest StatementThe authors have declared no competing interest.}, - eprint = {https://www.biorxiv.org/content/early/2021/01/19/2021.01.19.427209.full.pdf}, - journal = {{bioRxiv}}, - note = {package version 1.2.5.1}, - URL = {https://github.com/minoda-lab/universc}, -} - -``` - -``` -@Manual{, - title = {{UniverSC}: a flexible cross-platform single-cell data processing pipeline}, - author = {S. Thomas Kelly, Kai Battenberg, Nicola A. Hetherington, Makoto Hayashi, and Aki Minoda}, - year = {2021}, - note = {package version 1.2.5.1}, - url = {https://github.com/minoda-lab/universc}, - } -``` diff --git a/modules/nf-core/universc/README.md b/modules/nf-core/universc/README.md deleted file mode 100644 index 793f4f13..00000000 --- a/modules/nf-core/universc/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# UniverSC - -## Single-cell processing across technologies - -UniverSC is an open-source single-cell pipeline that runs across platforms on various technologies. - -## Maintainers - -Tom Kelly (RIKEN, IMS) - -Kai Battenberg (RIKEN CSRS/IMS) - -Contact: .[at]riken.jp - -## Implementation - -This container runs Cell Ranger v3.0.2 installed from source on MIT License on GitHub with -modifications for compatibility with updated dependencies. All software is installed from -open-source repositories and available for reuse. - -It is _not_ subject to the 10X Genomics End User License Agreement (EULA). -This version allows running Cell Ranger v3.0.2 on data generated from any experimental platform -without restrictions. However, updating to newer versions on Cell Ranger subject to the -10X EULA is not possible without the agreement of 10X Genomics. - -To comply with licensing and respect 10X Genomics Trademarks, the 10X Genomics logo -has been removed from HTML reports, the tool has been renamed, and proprietary -closed-source tools to build Cloupe files are disabled. - -It is still suffient to generate summary reports and count matrices compatible with -single-cell analysis tools available for 10X Genomics and Cell Ranger output format -in Python and R packages. - -## Usage - -### Generating References - -The Cell Ranger modules can be used to generate reference indexes to run UniverSC. -Note that UniverSC requires the Open Source version v3.0.2 of Cell Ranger included -in the nf-core/universc Docker image. The same module parameters can be run provided -that the container is changed in process configurations (modify nextflow.config). - -``` -process { - -... - withName: CELLRANGER_MKGTF { - container = "nf-core/universc:1.2.5.1" - } - withName: CELLRANGER_MKREF{ - container = "nf-core/universc:1.2.5.1" - } -... -} -``` - -This will generate a compatible index for UniverSC using the same version of the -STAR aligner and a permissive software license without and EULA. - -### Container settings - -The cellranger install directory must have write permissions to run UniverSC. -To run in docker or podman use the `--user root` option in container parameters -and for singularity use the `--writeable` parameter. - -These are set as default in universc/main.nf: - -``` - container "nf-core/universc:1.2.5.1" - if (workflow.containerEngine == 'docker'){ - containerOptions = "--privileged" - } - if (workflow.containerEngine == 'podman'){ - containerOptions = "--runtime /usr/bin/crun --userns=keep-id --user root --systemd=always" - } - if (workflow.containerEngine == 'singularity'){ - containerOptions = "--writable" - } -``` - -Select the container engine with `nextflow --profile "docker"` or set the environment variable -as one of the following before running nextflow. - -``` -export PROFILE="docker" -export PROFILE="podman" -export PROFILE="singularity" -``` - -Note that due to dependencies installed in a docker image, it is not possible to use conda environments. - -## Disclaimer - -We are third party developers not affiliated with 10X Genomics or any other vendor of -single-cell technologies. We are releasing this code on an open-source license which calls Cell Ranger -as an external dependency. - -## Licensing - -This package is provided open-source on a GPL-3 license. This means that you are free to use and -modify this code provided that they also contain this license. - -## Updating the package - -The tomkellygenetics/universc: container is automatically updated with tomkellygenetics/universc:latest. - -A stable release is mirrored at nf-core/universc:1.2.5.1 and will be updated as needed. - -To build an updated container use the Dockerfile provided here: - -[https://github.com/minoda-lab/universc/blob/master/Dockerfile](https://github.com/minoda-lab/universc/blob/master/Dockerfile) - -Note that this uses a custom base image which is built with an open-source implementation of -Cell Ranger v3.0.2 on MIT License and relies of Python 2. The build file can be found here: - -[https://github.com/TomKellyGenetics/cellranger_clean/blob/master/Dockerfile](https://github.com/TomKellyGenetics/cellranger_clean/blob/master/Dockerfile) diff --git a/modules/nf-core/universc/environment.yml b/modules/nf-core/universc/environment.yml deleted file mode 100644 index e9cdf650..00000000 --- a/modules/nf-core/universc/environment.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: universc -channels: - - conda-forge - - bioconda - - defaults diff --git a/modules/nf-core/universc/main.nf b/modules/nf-core/universc/main.nf deleted file mode 100644 index de8394a0..00000000 --- a/modules/nf-core/universc/main.nf +++ /dev/null @@ -1,80 +0,0 @@ -process UNIVERSC { - tag "$meta.id" - label 'process_medium' - - container "nf-core/universc:1.2.5.1" - if (workflow.containerEngine == 'docker'){ - containerOptions = "--privileged" - } - if ( workflow.containerEngine == 'podman'){ - containerOptions = "--runtime crun --userns=keep-id --systemd=always" - } - if (workflow.containerEngine == 'singularity'){ - containerOptions = "-B /var/tmp --writable-tmpfs" - params.singularity_autoMounts = true - } - - input: - tuple val(meta), path(reads) - path reference - - - output: - tuple val(meta), path("sample-${meta.id}/outs/*"), emit: outs - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "UNIVERSC module does not support Conda. Please use Docker / Singularity / Podman instead." - } - def args = task.ext.args ?: '' - def sample_arg = meta.samples.unique().join(",") - def reference_name = reference.name - def input_reads = meta.single_end ? "--file $reads" : "-R1 ${reads[0]} -R2 ${reads[1]}" - """ - universc \\ - --id 'sample-${meta.id}' \\ - ${input_reads} \\ - --technology '${meta.technology}' \\ - --chemistry '${meta.chemistry}' \\ - --reference ${reference_name} \\ - --description ${sample_arg} \\ - --jobmode "local" \\ - --localcores ${task.cpus} \\ - --localmem ${task.memory.toGiga()} \\ - --per-cell-data \\ - $args 1> _log 2> _err - - # save log files - echo !! > sample-${meta.id}/outs/_invocation - cp _log sample-${meta.id}/outs/_log - cp _err sample-${meta.id}/outs/_err - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - cellranger: \$(echo \$(cellranger count --version 2>&1 | head -n 2 | tail -n 1 | sed 's/^.* //g' | sed 's/(//g' | sed 's/)//g' )) - universc: \$(echo \$(bash /universc/launch_universc.sh --version | grep version | grep universc | sed 's/^.* //g' )) - END_VERSIONS - """ - - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "UNIVERSC module does not support Conda. Please use Docker / Singularity / Podman instead." - } - """ - mkdir -p "sample-${meta.id}/outs/" - touch sample-${meta.id}/outs/fake_file.txt - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - cellranger: \$(echo \$(cellranger count --version 2>&1 | head -n 2 | tail -n 1 | sed 's/^.* //g' | sed 's/(//g' | sed 's/)//g' )) - universc: \$(echo \$(bash /universc/launch_universc.sh --version | grep version | grep universc | sed 's/^.* //g' )) - END_VERSIONS - """ -} diff --git a/modules/nf-core/universc/meta.yml b/modules/nf-core/universc/meta.yml deleted file mode 100644 index 92a46bc6..00000000 --- a/modules/nf-core/universc/meta.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "universc" -description: Module to run UniverSC an open-source pipeline to demultiplex and process single-cell RNA-Seq data -keywords: - - demultiplex - - align - - single-cell - - scRNA-Seq - - count - - umi -tools: - - "universc": - description: "UniverSC: a flexible cross-platform single-cell data processing pipeline" - homepage: "https://hub.docker.com/r/tomkellygenetics/universc" - documentation: "https://raw.githubusercontent.com/minoda-lab/universc/master/man/launch_universc.sh" - tool_dev_url: "https://github.com/minoda-lab/universc" - doi: "10.1101/2021.01.19.427209" - licence: ["GPL-3.0-or-later"] -input: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test', single_end:false ] - - reads: - type: file - description: FASTQ or FASTQ.GZ file, list of 2 files for paired-end data - pattern: "*.{fastq,fq,fastq.gz,fq.gz}" -output: - - outs: - type: file - description: Files containing the outputs of Cell Ranger - pattern: "sample-${meta.id}/outs/*" - - versions: - type: file - description: File containing software version - pattern: "versions.yml" -authors: - - "@kbattenb" - - "@tomkellygenetics" -maintainers: - - "@kbattenb" - - "@tomkellygenetics" diff --git a/nextflow.config b/nextflow.config index 9b45c657..97df756b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -50,9 +50,6 @@ params { cellrangerarc_config = null cellrangerarc_reference = null - // UniverSC parameters - universc_index = null - // Emptydrops parameters skip_emptydrops = false diff --git a/nextflow_schema.json b/nextflow_schema.json index c09875be..e36f93c9 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -61,12 +61,12 @@ "default": "alevin", "help_text": "The workflow can handle three types of methods:\n\n- Kallisto/Bustools\n- Salmon + Alevin-fry + AlevinQC\n- STARsolo\n\nTo choose which one to use, please specify either `alevin`, `star` or `kallisto` as a parameter option for `--aligner`. By default, the pipeline runs the `alevin` option. Note that specifying another aligner option also requires choosing appropriate parameters (see below) for the selected option.", "fa_icon": "fas fa-align-center", - "enum": ["kallisto", "star", "alevin", "cellranger", "universc", "cellrangerarc", "cellrangermulti"] + "enum": ["kallisto", "star", "alevin", "cellranger", "cellrangerarc", "cellrangermulti"] }, "protocol": { "type": "string", "description": "The protocol that was used to generate the single cell data, e.g. 10x Genomics v2 Chemistry.\n\n Can be 'auto' (cellranger only), '10XV1', '10XV2', '10XV3', '10XV4', or any other protocol string that will get directly passed the respective aligner.", - "help_text": "The default is to auto-detect the protocol when running cellranger. For all other aligners the protocol MUST be manually specified. \n\n The following protocols are recognized by the pipeline and mapped to the corresponding protocol name of the respective aligner: '10XV1', '10XV2', '10XV3', '10XV4'. \n\nAny other protocol value is passed to the aligner in verbatim to support other sequencing platforms. See the [kallisto](https://pachterlab.github.io/kallisto/manual#bus), [simpleaf](https://simpleaf.readthedocs.io/en/latest/quant-command.html#a-note-on-the-chemistry-flag), [starsolo](https://gensoft.pasteur.fr/docs/STAR/2.7.9a/STARsolo.html), and [universc](https://github.com/minoda-lab/universc#pre-set-configurations) documentations for more details.", + "help_text": "The default is to auto-detect the protocol when running cellranger. For all other aligners the protocol MUST be manually specified. \n\n The following protocols are recognized by the pipeline and mapped to the corresponding protocol name of the respective aligner: '10XV1', '10XV2', '10XV3', '10XV4'. \n\nAny other protocol value is passed to the aligner in verbatim to support other sequencing platforms. See the [kallisto](https://pachterlab.github.io/kallisto/manual#bus), [simpleaf](https://simpleaf.readthedocs.io/en/latest/quant-command.html#a-note-on-the-chemistry-flag), [starsolo](https://gensoft.pasteur.fr/docs/STAR/2.7.9a/STARsolo.html)", "default": "auto", "fa_icon": "fas fa-cogs" } @@ -297,20 +297,6 @@ } } }, - "universc_options": { - "title": "UniverSC Options", - "type": "object", - "description": "Params related to the Cellranger pipeline", - "default": "", - "properties": { - "universc_index": { - "type": "string", - "description": "Specify a pre-calculated cellranger index. Readily prepared indexes can be obtained from the 10x Genomics website.", - "format": "path", - "exists": true - } - } - }, "cellranger_multi_options": { "title": "Cellranger Multi options", "type": "object", @@ -542,9 +528,6 @@ { "$ref": "#/$defs/cellrangerarc_options" }, - { - "$ref": "#/$defs/universc_options" - }, { "$ref": "#/$defs/cellranger_multi_options" }, diff --git a/subworkflows/local/align_universc.nf b/subworkflows/local/align_universc.nf deleted file mode 100644 index cf16985e..00000000 --- a/subworkflows/local/align_universc.nf +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Alignment with Cellranger open-source implementation called by UniverSC - */ - -include {CELLRANGER_MKGTF} from "../../modules/nf-core/cellranger/mkgtf/main.nf" -include {CELLRANGER_MKREF} from "../../modules/nf-core/cellranger/mkref/main.nf" -include {UNIVERSC} from "../../modules/nf-core/universc/main.nf" - -// Define workflow to subset and index a genome region fasta file -workflow UNIVERSC_ALIGN { - take: - fasta - gtf - universc_index - universc_technology - ch_fastq - - main: - ch_versions = Channel.empty() - - assert universc_index || (fasta && gtf): - "Must provide either a cellranger index or both a fasta file ('--fasta') and a gtf file ('--gtf')." - - if (!universc_index) { - // Filter GTF based on gene biotypes passed in params.modules - CELLRANGER_MKGTF( gtf ) - ch_versions = ch_versions.mix(CELLRANGER_MKGTF.out.versions) - - // Make reference genome - CELLRANGER_MKREF( fasta, CELLRANGER_MKGTF.out.gtf, "cellranger_reference" ) - ch_versions = ch_versions.mix(CELLRANGER_MKREF.out.versions) - universc_index = CELLRANGER_MKREF.out.reference - } - - // Obtain read counts - UNIVERSC ( - ch_fastq.map{ - meta, reads -> [ - // defaults - ["samples": [meta.id], "technology": universc_technology, "chemistry": "auto", "single_end": false, "strandedness": "forward"] + meta, // + meta overrides defaults with information already in meta - reads - ] - }, - universc_index - ) - ch_versions = ch_versions.mix(UNIVERSC.out.versions) - - emit: - ch_versions - universc_out = UNIVERSC.out.outs - star_index = universc_index -} diff --git a/workflows/scrnaseq.nf b/workflows/scrnaseq.nf index 375ad79d..0481b988 100644 --- a/workflows/scrnaseq.nf +++ b/workflows/scrnaseq.nf @@ -16,7 +16,6 @@ include { STARSOLO } from '../subworkflows/ include { CELLRANGER_ALIGN } from "../subworkflows/local/align_cellranger" include { CELLRANGER_MULTI_ALIGN } from "../subworkflows/local/align_cellrangermulti" include { CELLRANGERARC_ALIGN } from "../subworkflows/local/align_cellrangerarc" -include { UNIVERSC_ALIGN } from "../subworkflows/local/align_universc" include { MTX_TO_H5AD } from '../modules/local/mtx_to_h5ad' include { H5AD_CONVERSION } from '../subworkflows/local/h5ad_conversion' include { H5AD_CONVERSION as EMPTYDROPS_H5AD_CONVERSION } from '../subworkflows/local/h5ad_conversion' @@ -94,9 +93,6 @@ workflow SCRNASEQ { //cellranger params ch_cellranger_index = cellranger_index ? file(cellranger_index, checkIfExists: true) : [] - //universc params - ch_universc_index = params.universc_index ? file(params.universc_index, checkIfExists: true) : [] - //cellrangermulti params cellranger_vdj_index = params.cellranger_vdj_index ? file(params.cellranger_vdj_index, checkIfExists: true) : [] ch_multi_samplesheet = params.cellranger_multi_barcodes ? file(params.cellranger_multi_barcodes, checkIfExists: true) : [] @@ -207,19 +203,6 @@ workflow SCRNASEQ { }) } - // Run universc pipeline - if (params.aligner == "universc") { - UNIVERSC_ALIGN( - ch_genome_fasta, - ch_filter_gtf, - ch_universc_index, - protocol_config['protocol'], - ch_fastq - ) - ch_versions = ch_versions.mix(UNIVERSC_ALIGN.out.ch_versions) - ch_mtx_matrices = ch_mtx_matrices.mix(UNIVERSC_ALIGN.out.universc_out) - } - // Run cellrangerarc pipeline if (params.aligner == "cellrangerarc") { CELLRANGERARC_ALIGN( From 8a98f31e787dcc2df9c8378279c93df7cee02768 Mon Sep 17 00:00:00 2001 From: Felipe Marques de Almeida Date: Tue, 3 Dec 2024 11:52:37 +0000 Subject: [PATCH 142/147] update images --- README.md | 2 +- .../scrnaseq_pipeline_V3.0-metro_clean.png | Bin 0 -> 807828 bytes ...=> scrnaseq_pipeline_V3.0-metro_clean.svg} | 552 ++++++++++++++---- .../scrnaseq_pipeline_v1.0_metro_clean.png | Bin 711723 -> 0 bytes .../scrnaseq_pipeline_v2.0_metro_clean.png | Bin 952584 -> 0 bytes 5 files changed, 426 insertions(+), 128 deletions(-) create mode 100644 docs/images/scrnaseq_pipeline_V3.0-metro_clean.png rename docs/images/{rnaseq_pipeline_V1.0-metro_clean.svg => scrnaseq_pipeline_V3.0-metro_clean.svg} (93%) delete mode 100644 docs/images/scrnaseq_pipeline_v1.0_metro_clean.png delete mode 100644 docs/images/scrnaseq_pipeline_v2.0_metro_clean.png diff --git a/README.md b/README.md index cee8bd67..17a91823 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This is a community effort in building a pipeline capable to support: The nf-core/scrnaseq pipeline comes with documentation about the pipeline [usage](https://nf-co.re/scrnaseq/usage), [parameters](https://nf-co.re/scrnaseq/parameters) and [output](https://nf-co.re/scrnaseq/output). -![scrnaseq workflow](docs/images/scrnaseq_pipeline_v1.0_metro_clean.png) +![scrnaseq workflow](docs/images/scrnaseq_pipeline_V3.0-metro_clean.png) ## Usage diff --git a/docs/images/scrnaseq_pipeline_V3.0-metro_clean.png b/docs/images/scrnaseq_pipeline_V3.0-metro_clean.png new file mode 100644 index 0000000000000000000000000000000000000000..42c542c5436aadb7f311d869784fc471ac9a5a0c GIT binary patch literal 807828 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¥2|j!Ag18;SqoemZ>D4nOBqUf*o;*peee~Ed zUSJED*sFAIc%Q>Fu1p!@@&-sV;G7&d7zz8VU@4&jhQH)fv4~xuhT+J8X5xip8pde> z(d)5UCdu#&6G+mU;!*IyLv{Pew)JSizaC52&J`PQ@7|S1k2a!+IH#qB4a%Oo|XH}lo4HvR5&1NUUifa(tLOS(!sJJ5MA*13)ivWJ2rP3K>@&fY@+gMu3_s7!r+MHtvo8V?xFxPuv$?ku#1so0Kb2d?^09g)HW3^b93lU}#0M zc9t_40sXe6B{)F>ZjM*nw9MxPE`XQU3L2Vx0@Fe4^pU3QLD!4OJ^kMH6T_RkyN%DD zRi>?4yJ5o>I2&yc1C^eKetsSZ;Gp=wAoWOscBt<%bO!b7S_hZ{W58|bxr>X7-+`Aw z)_ zL>Ws<9*ek4WB{5kU%>Iq8K_V2_h`n_y;vK*KM{4To zC@uzW&9&k&5U1iGF$TBU$(^e=iLU_CpWAPp<}k7Y?!ZdKssnH78f?MSz!W*p5my<1 zI2L-Sf<~Xu#e~OlisF(eGELs|vaEL>D1rp~Sd#;nOFD*0N~%rVFd}^}NzI|t02<2Z zAvOW<^t_><6Mktd*h23L3aGRX!jHn;V0r^4~Wu1Y#Ohtf{8G`&6|8d6`F>X%G!&&&_5OmBu01 z5kC{uNMYQmohh7U*$DE?YjgPx_4V}#2s=#saV`+Qh-C9K3z{t5vu96TU7clfi`&HT z{(c)3S-2?P(q0hFLTVFGU=~@}-vL5^M5qUXH}*v2)LT&~`AG0Qz0J=&h5g*pmwT@& z(mxG1xt<~f(L-m2EA`DVsKm3tPChuT4V&PM!fav7RtnddZwE{IpF`(Cc|BqL8h#W- zurPbeIXOAgIuS2e+S%m;FSQ!&Iay){EAcY+)r8F}_=AQDZ_)m{EZfa3sY|m79(Al` zTi#QrN?Zz<6B{Uf);}>08PjP@x`fHZdxRL}S?`7#LF%N!UmuA(@S$fz3F8N93Lup4 za6+={ev1D;(*;-W3r?A_FF-WoTpf3s*qj)dm_l_E3=M?0E&wusYXwQH0zlq>5F`02 zQ#ZbW2SB_8AZt*)p%;gmIT%u$5Zw~11dNJ``^FTaC^#GR01)5fNvr+S15rJ9bCU!U zw0t}Dxz>hRM4R^BdH3%J1<@c>M386WDPVc0@GS8P0cVz|@6%F2Iw%)suJcCWL(+lE z!vH0OIY^j?2t2z4@MjSL`fy|ctQ_wgt40>x1JJY{{OG{VG@1bFg|12h}~2~<=`NZTZJU9kJi&x0m#T*!}~TNEP=D+W6w z4Oh~EY6TVA7)7jgRa{?dMM(jlb0e?b(u6v?hu7Ay2WiJhVsKA zUFf$u0Z!0578WjhX2jXBv+iMGVMX0{Kn*)HpUmJ8!xQ6(6<}G`)6r34HJ%3>IM^XF ztgElL!m6yKrhW%7Y#m(D3Ly-qE@rGyxU0;O4^_^K{*1RuU*O6_sX!SBA7D~LYU48E z!!How%%7TCS`INbtqDxx)v5Oihv{i zk1m7%i@x!vhm+zXC!*<~sKU$B;t-J;*G30HF99-c>*4=v?R;y4#zz%&d!#dMRZ;1h@Ci6bj_}=eM;S8Gq8uT z>UL1j>uR%ed3ALgK{&sD<3=7z)=4S?S%<}-Q0L$Q>|@w4J%Mi3X9lz=QaX2R(i>(~ zFWJG#iN6uCw0~|Z-sq0Eyte_K*kW9?B|;b0wzxkC>h%jmaux3GSg<53pcdWaV7Lef z#1+q*BaG7jnvv5TBK83Afnvta`Np&ao!4{^LAnUOY|PpwrE(uLw?3O^iJ#Kq` zL1ed*Aj4}3jE2lfl|9|x!0m#r_-^+!WZ8mt*nY2YI8i3|a*@hzg@SN%#!uJ(g<_mv zqukYjYLW*mNrD>95Ma9z3dTd&0r7E zKn}LXJc3{LtdWsE%+~}`Gu0Yw6Pic+;Na=e-{bB;IQJtVCT0(U>=(8Y22aDnFz`a) z)+%Z$l~kPw#j=c4*yp;&#;u?*e&pxW0+Mwm8K7(^V!6zI*y434eU~vb4A*E}vtmUK z>{OOoTRhQ5;Fb@@dH|(Ie34gMt2!uxK%YY*8dfN4Zq~`86Dm|W<95Q)9d7~W`1o1<{89OnbY>0AOGFbb&&pVPRpZT5Yw?J0Alr?pDN7 zW{tUlxQJmU*Xv)v<0dIK+@eti?yKUt{2egc%f^v*`tI2Qx283~BI~3>vy7ngA|Bvr z9M`U&YlL><0U;G0?RkL`cm%}#PO8;GlQ4~1$k50vERFp%&~Prz)V1()6wWhyJbn6< z$92X&sgxON?#PuUz&stRYTxJQf1{69CUm1ackez;`j(0%(}M@ZbghP*A*s2Uga?LW zb71Qc=gZIuhBhhdSSX@bA2@;Mo!yo=X9WW`&kTvp{{H^+XI5=(ZIjnPtm42k*wg=^*B`Y0m1bG9m6HlYntfJ^k9XIb2tV5UM(}?KmFz!8!WrYJ<|qtasVG_nS7g zBz!{TP9Xdv1?s7mn@1SmD08Uvqe0hR3m+`a$oM%V#i^;O9}Vh=I;ROEJCN%#H`Tw# zun{0hIa)?M*C*H${8FRjsA=lE|DL8lf69_`{9pR3=mEsUc$6 zzvCX=82jv;R=Tb1p6zZ``>P&44DeRf4F&qX*MwTanZ>NZD|3$jz&U;?6TM5~7e%_xGs zkCg^XvDMVnTwYaje-z1U_T8G8(8CEmo88k5Ur@KV;<*NAGTr+1i{0GZV8hNCh;!pa zK0@B=eZEzDdpp{npVdsWrtIqK(s-=ZI|)tfK=m4~-#U0l#pBqR;$SJA!7t;4>crzT zZptF%su`7^mI|Z16nqb+Tbt>$#t%3t6ZlR{#4&Alh2B@p;ITgp_SQalC*`ly`Af!0)C4q` zDL4W6FG-Kjpr(T9*2wmb?rx%}F6Yswf}fGw*vJfI?94cN=ez|MN@%`fgk90~;{>239+xRwr1342Ve8vdT3qjOjxulDcpazbUYZsF zD-2#%3`^UfI}z=0I>}^=(&*_S4nI8@?9AHjXuPryHsFk|?j^(sBv*l9ifscKU|~_S z2rIlkpNoK_=w}ns8;R#@SHtohL^KUmZ%<{WgLgyU%D^B9HzHkg6X7s_ZOqBAW%Jpe ztjT4+{O5OAQ{mEjH0h{|H=rW<{7W%k=T@dg?_oUJ1$>6_{bFbCLyhoLol!;*q4<0i zqTSno3ejBSU4Fh*H24K;u=gb)JIu%dv6;^4cFBfl#3HED)MVk-$-0Sv#d`cF^cD{& z7tix0RPH=_^nqEMpoXu_?pwwFwLmEOIcp4 z&?XNn=#6}SC}=%|wn7L(;tY#|mt^BC7H}DAK0%8KM#S<;Y9(|ZNU{@&wl3km25?hA zz7uJLHYJEBK2FZ`q2tHp^z^pO{QT*!OAYV^I9dTtYxR?R=~W^_k>`=4Np94hv+2iZ;<%Lorv*tTaax=PYcp`8!W z9!Qm5dGf?0d-k7a6mErj@x+m$ALA1edJm>x8MwdGB5Z z2oam4a}L10wFn;sR$Ps_?S|+D3C?`3eNh;052D0VBzwSK+vn^oUJ@+jiID=OCC+j&9`!lP?qas!2vEW(!VKkQK;(@A4Oq?kOqX)UY@X2%Sp zK!M~XKtUylihsT(%5;cjFHyJ)yc51X^LnqVHdS6kkaoof1h!m4*4#P{Q zTSuJ4z*7Lw!3vN$4=#@-uvkD~n-pIvzhPL`3honn*h)Pb6cQ4GW3`^-#qS)tihUwL zl05;#b&$`2*SHx>v9=jS(hHv?myxeD$^a5PFgREaGlxvc8Hi=^kSLO_fPoL^L~UzW z%f?n}8U?;RL!j4F0Cw#NRpyCqbpUI@?B_^vMnnSc9J5jRMFcA#meIKV9DCSc6A9%j zE-qdMR&ZJ;4D)4@eOYvkUf5UHG(+*#u?zmv1rkprWXKVClNV@YEhTi z6cc&`xY|!a$&uE1d>Kzl!ZFlPh?0CW!d$}O>p`O6WH8dMGoT3v_9KD(B_@0J3-x;> z#0lK|4Gaa#Xl8o)9T#cHKDHrcOChyOC%ZT~TXxZV<}*^)6} z_pu1FZ?cM=GM@yStPPnJ5D%=LWWc8kuMPnkAvv7vK%!#+g^{9lAI(zWM*1-HEQj?H_{OOa$IvL{jSQ#g=pDyOi8{nr56>4Bdg=n)RN zCeB;Zl3kD*4|r%{NQ_v2yap;5mO~$?bg`ZKej4yNAe%kC zTSv!LIAR>HMi5JoBs9)F?e}6 zfaMLtdsmqOx587o6mZGRA)S;K{QA|aj#bwi0SVG;z5)pa3Ff@%bTHTbJuVQ5K3~fX zjjd_!01JtbU=A|Y9RPFFeKju8I(UY2$QV=LCI={Akh zOydK@;0emrhuv&^XICJHbORxuwJWIseL!Urp%8kxNeG$O2!%)Kx8J;x0BZgzFebje zsJwg~n8CNjqJapnBCwdKs5O1@>NRVwfkZBsZ|5}Ha|z_R5E#7cw{GP_iXe#3hed(G z-D+BY>5~T>;45II6$XW6Pk$A|v82cKa-faqY6?GoyaF}m`g2S*saw)<)EJ6h_UjB8 zcnh+HED@EW_hzPkbC0%uks z)Oeg^5Cdi zGKUeK4DE=pk%v1)4%X*Ue$Jv01Zu*dJz(}Fb6O|V!Xxxi2JBSD{iGZ2C$%*38?b1* zRacbO)X>8~y+@VAb+|wruQnjH4zEBToiYRUPemv!0yQ5^W+}RPIp(>f#OKwA4@XE< z-NKHJj&)F`swxrR&ljwkU38U!`cq+Hn}iX2CHS5s&9CcFm{*&RMy?itEgn--QzT!A zteEAOcPl-{A-C-iWjLtGMI#{aZz0sj5=*uX8}2jR-JX3Tqb zGqNqZGYGRl0oT2qoY}_1vli(g>q}N($jX4q(ya#Ep<-(0h-bCmJI5VL8mZFzsRWS| zdme{ghD+h5mi!b$s!&8k1Z1A|;@aOV&q_9AZmN9nrPRc)yvrygD#{zMUOQaVz(K%< z3tISJ&HU@9$>4pzGq@dY6*@b8+9A^>F)`Rh^@J+U%loc6&5gg#i#Ms>{qf9>e<8e% z^GJ~8n<_oG9|jnF8-*M-EQD=<{24n}Hzzl#egY0CjVRI`L@lHJZxFMPP#h_GC5I=3 zn*Z(FB>?EFUEx$I20A93A&UP5_w3$RK=`S0H*dmU2AzM5>ia&j7>+SWzX|+F+&T(%NeyA;JH@xX=dLiiDn>F znJyDP1<3!Cx1(T8>QuC*7qlZsA{q{E78WM;1gigsvG0z@dhh?&pt__XyGi!mp;CmT zWhcojTZKwiL?m0Ktddps$Sxyfm8?W1Tc{MFtnhoj&$-WipZohgkH`1tkGt;ba9yA4 zJzlTpdf8Pj2VZmZWJC(|fipiljATR7fJpa4I5qM%joE^?7X0_yb=Kr?0nhv?I1Ppo zV`#A*YB4^BBRAU`mE{Gv`5Sj^#TTA04O-F|>NqUA07!A_7&Dktp~! zXd0XwfDq05vOp%X^Yc?kexfDq+O^uxSmy2x_4ZORGcz|9K<#Oa506FGfz!g(#YGOC zI8j>p6ae-VZAv3QX=1{(Y#%mO4M+h|Krh#_`j_@z8TVw}(2DI(mIFpp5-~PPc$olI z7iVWNZ#JCf6F?`u83lAN6x{IfF~QYRbag?_F%Y7%1fp@g9Pgg4zKqIiMG2a7*?vc#Z0kFt) zp7fDD*=GoP&!#=SPspH$sI$IzMLR|le2`iy0#kC)P@E{*Mihjo!3OW*I4S2{&Bw#D z3cNlk(s6x@nK!U>eLaB20DIol zNQN_&>m-dQ7i4ELdNxg)vP+xz|dIzTav{l9TY;8@%b+$K-FN5llfhscs8 z#0HRyp@roF4?o!$+t|Efop8dHSp3izn)k+^yW#EpRY2rySOLqZ0QLto?j{lhFnm`! z@}E5Mf;4m3$;j%y({=SvC24f9JwC^E5vjmNR1=O}AjnKX1sLfhQ`oO(k2A_bFF=sy zwslg%!jh7ArhC}5`3ekGRo{ilU87W^nWDwIu10At$+n7WIJYM2enP@d3w3X8ejhZ| zFuy;JKZ_WI7&8_&HgfOk$429zJI~vW9yh!@>qq_T*FG={^jf62Ej?nFk#JQ zx$zzx6VH_xTwQ%Q39J{UQ4#w5z`Bd14o1p|59IQbjTYe3EOAy?Y)(^67bm!!uO#rw z255)^-Z%2oJA(NilEdoh7aE7ni3<%^{v_ic27v@v$*EPx)+jLl&n=m=_8ynqcl z&KBp-hoDRyX9|D|(1a$Gz;UPnN;(NmYhmy7aH@M(`|w(-#>U2yS{=AkBImPE=IO^t zqRfD0BjMq|7r=%T>O$J15B9LWP0L-{yPuQZo~DOtKQuUS9mG|pFZG~rYdDpd2j(C< z@60N-T*b;PSepziuwT`x)KEy;N@ zqj1`<#H?MPPvF)p8e{rS7w&0>5(&Z;=XoObhIXyta(nSCV5^2pcxaEO93id{!Hx2= z^9F6t^ziT5NoU&6tgNgi9+0Bvx>zfDba}!nDH)@gS}Ae#VlZN`jH~Ei(9~R z6qk|p>B(RgF|X;ET2r)Xq(z0k-D`O$9LzH1`#f8p4bf33rZgi!`KeH!l#r%Z+?JQoN|5Ac|_&PKJVx2P&WbZho zKlJhR^o(CRe)zE2gC4!<;M7$0xaDOFzfR$%q`|qR!Im0%>(+7vzcv=M%8jj#`|f<= zw49tA!2r%*J1vaUjoehzrwONS;*+*Kw#rj)nO`**H#a}sJm2C%T@q*owL&fW6VJ~d zidrd!f^s=pLi#PO#?bEKQmuUX(m!{hoTW|FZRWy|Q%Yf>Q_B)4*9$ZT%br7&5LJsl12w z2cy6MEe{{@bDD4bg=RhJYnr!|1upK37?JRKE`2~vkZAyFRQ{&Nf5LMc+II2s`n|7_ z%VO+!baXQg5ue}K0^eez}5iN9J;0vN! zMQ`~VKorI2)ocORpnHCHwlUK#!HfmHXvikPRlM#*-?s`arE!6ydobouo{Z7sq&LBN z?KQLu#HzxAznS|ZEUOU=DQo>XEAMo+gYt!GTx!=YVjR_`RB?O&RQJ+W;8pxvw_!K- zK5D9n;SnHr;lo;Zr`^U+6x}-f4Tk-Ja7qNEThGZY`)Qblr}gsdYPg+l;^H#Ky~DES ziYRo7uS!$|Gj=Su8$E$13DZ~{!qhUH!_Qb+)^$EW?ppY_)jvmCb`DvbH8b-Dg1%;3 zz}S3<$lCZl{j}zMFPx6eh|oy6&5SQY7XUa(_Yfz&k34FMGrk9{PXXB?S~X}zWn^R? zqE#pGy+x16R!>a>+>5i&r&ppgNV)_tzchA5b6lE?bdm&RMj!!D zt0lM%!5GjRGq|t1JNWr?y+P0O=MUSw*xDA(L4RC@<#L5KCpseHp~uqVyAo%K@7<*V zm^q1xa1B0|;n5|dwbr8Oc0jMnc9OpYyFwM~EvMJ99hH<|{wzv}o9~B)haXECTx$XZ zMEnzwxzR#Ws)Dc!qNm@o=gJ0HyeJE=8yX_aau$zzDf5R<$fv3QbiVV%vtr^%c(Ki- z1l9A+?~cS7e&|SbC>8H4C@#wu{kQ+=zjodtbBmE8{lxa2D?)eHAIMY+G(c*=&D=1p z&>Q&OL=tuLn--s(%w-Kzf$Hy{Gx*BQiMC33J{T#Ve$`685i{`274v%~*%AQtvg|bv zylbwcH&H!X*QhI~y5shtSH{x+`FVpjwI?2y3VO56P&hp>{Lrh$nDGe0LaGqruJNqE z+NbC`Lvp%y3Vm*nz#U>`0;JtLdj9NLuH;b1QkT(wE|m6Olu+gl$e_dLXxYP5Vbu~K z8rO=7ikab9j|iV=Dih_Rwz!}Lf1MWw*jhNliPlu#ah{$?0kC}IfmZS}((}I_5QFL% zcpI)lui*sT$7_M&%XFrvr=x>PySTR9ZP^Yg zDU_nU&J6vu5BV}3KD}&^&YU#T)ny`zulHCO`_(Ju&D^`_sXAKAK+b(f}Hb0!H?6o??|`DP(dfk3?fsl)|h%V z5`XYpg>n1UR9Hv!7Z`1CPJbBWqmaa%OvCa;n@+ZKh}+XCGsLd%t_lJA^d_W8+E8a5y;gn&U$&-bkpCnCPt9BqR?@X;7t zrE&0CHF)}9Lm6Y@SHtpQ6vWqYAj2U7A-5-LS0W}r?PJzbN@QANrH64zgws?zY@ArQ z3%$)m5d_nEPtL5#EyalpgY`xzxn9pU-1<4HUnfzU1vSJb=wlh~9!}DnnVB&LfPjX! z&!7W527nro{9>G}t3i46+rEN+36jzvq=zK$Ifn)hg$+-kAov=P;LowaMRD5tbTmx6 zY1BDB>L*L5Bnxu?36EcM(EH9v5T?u0fcsI(BIB3Bkv~Lsj9-qLVOLXdQo44HcWo&uzk@A{fKcQ54g`{NK znvHo&wf5Mfs?Nj-wvwj1-E!@!8)F)2x#UPc=00kbN8`sIR8O&@lD{DnxRE6| zEg_+5Hcb zP1zSAAQ9AGArhqCKAax9CaxzI#X%*Sv)<32Q}V_@D?oa>!_pw$9e%kNIOJtIaX$U> zv6x(%$rJ!bmj!6)#}9tS^^)S^t=8p=ZdsGipk4XV@1r&l{=(_#YYxf88?Vq?!O@7- zBs_lgynvQI^_$u5F~Sf*@y8algaSNcr4;Y8nkx%)`7f7ki2`nKUH~gtisW55!2$yV zLy5kJn<*gd?%kTr6OV=CdJ+^rfBj1Plw0ek{EUq$;9CHg{DS3hSOOxwFfTnlrcYud z({PES?1f1bkd5gya`;W&61)5*oH*-|z7i#G!d{UeK-QsNw^j+~vG8 z@KcMg*!JjnY%uoPq)R%zpZ6ROgz*EUxkSDJ(#O}a3+S=+ z0;`cdkRwIVfe}rQfgFVZ>`8W8{|q@FLlQfwVw#8`u4Z%zN~}}{-BH3{=jP`#N~$== zecr1ELBq;vWJ!tvl9Y{QEpwe-M_5iec@be5nF!}~=%sZd4LkoVUjHL){quKq_mr?h z-F!I3`5w5t1iZ2jxggQU8ZX|jv14E{!B+nLtvw~L=KP#aZr^z4_bhYaC1p8Ft>SIL zmrkzUdAhHBZfWn`7TzbY#Ynd{yt(qIPsL_IMMdkGa2$n<8r&XlN?FKP$ODHrw5k#Pd1i%Lm-+Iux% zV1Cc!#Dt`LqmHaDN?;$m0PYzp0EH_}I`|>mtYDb#R_UR>u+iCj8rD$lGwO$2h7VJjocUJ6)0{gg%%>(sBdg zY({s|-zAU@E(UWt8+k1%%E zk+88SUZf&fX}Da6u1hx(w9(d-fY2hJoEZmh2^gPVSVa|b|REh7-tH!~}$)W&d za+bl?vHz5efB&6k6w+rxvs85zgoUg3R{j_{wR&M{-E8afjPnOLZga@)TcH_gDn^SP z2Z$P7^7->3K{X@Y39GQJGm}_1%HQ%@KIr({-hHAre>5&buO@>S3h_ zF~|rNIly=D5m>=^$;(sr8gbNh(52R&=+)oc#uu-jq{bND;f%cmhTuBtl;jfOd#(Jz zdfR%=+lh;bUCYgt0IG5WWC-vZ`(K?f!+_5r-WCOFpO>9Kl8-q5j`7)cB5A=87W_dB zcVS4~k$0gyb+{a0f%z$`m=!MvuKwU!v(Nx2NQdp-*rhc6>;}kf1Qh5#JbXEf^34-~ z<2a2JuG>C<9;C*Vesr*nSkAkD0i>&FX$dO~Au`>D$g0j^U`YJbQg?LY78Rkt;{SO< zDqT6bRoLyce#ftB<%0*!@G&A(XJcErNtN%&$6^UU@M3^68<)=|>AcIyqg6?LYLtU{ zun&Q(uBK4>ZPz25(egL3THu_)rXXHIz&w6vwI#f-2L=U2nx$;=d|qgj%BIF8x$=0Y z0-a(=_>x0dD@J)=5C-gAi+}p3=NojjWyv>eQ(_=NRl(fWQ&U4jkS@Fu!X`F))t_Zl zdf4cEA{ugT=YVg^V*^$>hxvmebbZJ(L!9u>OD$-&9KGG8%SrI~(b z-mmndjQMg9IHl{MiFt+y3wGMr{Ri z40AIZccP&vW*fGx!9T>2Hp7yM}jyj{R` zP$gYu1xg&#yT!e$2l#Hhn1{!bqLjI7ctF76u%YsoJv~NGG!CGHg&u;E{|gI|>bKMB z9E@uw_%c-BMA?K;mTP!yCDMjpuD;1rsHKIMFaBuCphIzc$TjV_cPBnSmB}uPuD?U` z;K2jX$c(m?J9Sw;G&N<_jaljNGnUQt)7Ep-0Y$EYS#>SMmk19*hSATnqfpY*$CxFn z(D~ZB5%nX9R%ja^1i%`x>pZE}4A(|(+{l;(iGnH4SFa#9&!s5Wb(^Kv0u8N&bxj?C zu~bV^E@**dxsN{xgoL~wby)kACa~@-c)oYH?9|D-3Z@zMGp~{P5qu}6)Z%+>7Td8* z<9)`%)wl?i-%`w#qT$_UbKWE>Qf~L|1Z)>zqaCNja%_mV!>07ONzv1%{RkcJ>O&QzR!oO3rdUoordi8Jg+ z!){tQN7iW$DW)u*-^$n~dUtb0&Aa6V+;_93a`vB;IHg^1`P#i8ftS&pRV@un1>8)w zH46XQs#EK5wJCs`R-#?IUFHH6ZhBr=MskL@bQulLWpirXNcEi22^SF_qF zO()G4s#!)Wce{YeC=Ra7izqRQ0qY1@XMxJzFOX~t-~TrvsMQl zPxKQ3c~^mK_)8kyBkKXuK~(Z87aO2%C#@+7`E?LVnYTl33TQ=viw~&$4Ps`GYR(eJ zAwG4X);S-(bLpR*&)V716&nc8xKaUhtyer9)mosMT1J_=%crFxc^6UYlMDEi!$L!M zuxyqvKN1!Y7r>||z&G_%j$5Zqe{_-QR&1<@ezU$`oAdXfA>Bc*`6y{Gq)t=-l9yz2 zMbAq#hv<)l)vUEup*tWfD3-1;3n`f3DevL`n0a1P`4E7-uN+3vcNjGHS>P%9FO)PF zh`oo3hxE-AdJr0reubD{r;2scjcrU~qu)~hE=_D(*lYVv6gkgN)aSn(0Aj%;VkpR> zmG+hhr*U&8{(OpJkNEU@p6*3_8hnvHj~@@88g@meqtx-t z%VTU}UWBEUcC)=<3?c~x_3r48+FRjP37g*IFt{2Eaitmfn~2>`!q7eOxCk;%Paac% z5&vxx20{0t(q{`XLMct$x3_D<{E|8OP8A5k22H{^4;xCrM?&xqMLh#kd-tHgCq zI*~(%u7!ndyrXvZ+Ptic3?Y@N4X0JzAGjBn%Z!eWrcKg3$q{w>(1PI(IXSi-_vx*~eTQ5P zP_YakB(dWH2BXmZ(c8A|c!8h|L<+R<562VH56RI2TI3g8>^3Wo_qz(Q~Vs# z#vJJ7Tk?T=AGVd?OBt{OP?W5n&kv!cJ%o(}$R+e}=a?sYYEjT;f_mBeMYlm)1pDCz zJemW*r8K~^Cu{SAKcWTnupbrbh-1tVZk$)4h7Dl30aQ(G{$i7cgQrY*~-SSL{;Wh1?e?#%ZDP0onV z9Nu#&S2-|pc_e?I$Z4BbFW;@b%zZc6bim)!;Zh4if{IScRUXMa&Cc`J(ZyekJBIy3 zvO=7?F*M)t)e=9e-@iX8i801Ik)c@5lH0N!ih`C12-1*Zc{1?*?}O3H{jp-JQTbDw zLD&8qg)>)x-KU`;O1I%VQ(bDtkUMgjU)nQlLg!Bz0j$V|76~VZAJe`Q{NW_b0}q`m zVCZnyc_h>*1u$`?MdoUkVfoTH$4J=)@3{AhX#4=w6`CR>ieN4=_>kd7s+g3P zlF}LAR)GJ~OEJ!~u(0sVn+-72z<9ByhGZvC`m&GFrkM7^v*f9k)SA3w9NnH2zu&Z1 zn=>?2hJwSh0mIFx+JlB6V|fT|?+otxse3Fp5wWu!jVYH)spzFI+E5IrPRt$YAVEvS zz*+;;v^!FT4x$c*AbiU#7Kz!A0_rMo!Ms%+ zKxiOpY&grT?jU+A!ZIBpm0pF1x86hNNd4W)&oL!AG1xI@+TV9ad$K}cY^+h(G=ppR zvPBzCk}Om|i&o#OkolMMEWUQo=q(Q9w7y1;T zYRr6|16-y(wdHj*gej6P=NUI_phC=8OVdg)Gk%~ADgacYY9ePU3M(n=GhAR&q~qzB zAz$>vt!#q4^W+m*+wQ1wVQ6mRM|eoB~TywP`8%tQxq zB86YuIKdZmQAKsN^0?!xUC7N^v%1RjFD*bxyTO4Ttfz5^KB}6Uj!#XNGb7!MXp}8W z52CM-oGbYZ+s6T5Rm%HrBbA~W|1tz8yWv3TS#ehMAsXPif`%D*Qfk4KvBA)C^;-dQ z+Lg0j_W@4y!7bE>^WV$fNAVhQzZ1_=2AsH4^wZBa{IzBN^Q*hSOlO>u_UMH6?9BI< zu`|CWw%fMuS5it`!5>a3UnqF;yi!M#BoSn|uHS0@^J?7*z8{bp{-u=ZQpa~7pZD}U^;e-|GNhi034q*Z;=oySQor4Mg7H@;|8dX%j0epKPHdsBXn;%GR6c=|!PAuNO=D=9aFRUYA_sKj0Gt)X2SP|`q6R?3 z4$SOdIw&LI1p`HS&_sHcmX;a|LP3YU)@*qnvFcX&SvZkE*;1Q7jK3$bsIeQ?xb021TAzQ&Y=3h+v=GoOl)Nh>=m9wd|ZyO)oNo z4vaHel4?Jq%`A_6IWLu%c>^#F=QIvOSkgIUr_`ZNhf}~Amu(dMOAR<`F45tN) zyjzjwwUg{{n}xcacGeg-nR`t=pE6rbhLueMt*L6BF^F>m_XC6Vp%UgTOmcRxHZ z&zeHxPKsygAZBo(8i1-f#`u1f3?LoB__i;i16pj@Q--&?%Ww0wl)e`g6g9I~NZx0R1NXvn_^Pzz4F2G(xIup2Y zoWD8I0ak+zqK&^0t`{}wUW9B_quOgG3a!!io8Sv?nQ`>J)wbtr)%tyHSJJsVq6u}6 zm@bvCtnn?Ep`Y8hZJS}QYhESM=D^S=$#x1)nnuN}SF^LH&g{!AZ;fs_?nT`zju-;t~D!Qsv@q+3j@ejwuD~lxY6+;yBCi zv8;%h@=Fm;j=S5tcL(l0F1%yUqFf_)mx|KFUW4O76{SJ`?beVasov|PbDR5F7ukymsT&QZ_<1CV6Y5GJw@j6h3$&H4qyTvpzrqMwzkGJh$sPL*C=MVk{!L zxQo9hk7rCXxgKGp_zXIJpoR6T3UfGoAp9Wd8i;(PE?X=i|2=%r>XO1boO|Wvw?SOq zZn?d+6w(4!ro(_+gb`U80)E3L?}1|6b8I4F69d6xz2MdfIb{nC4N0j$qro{$A_m~e z7bCSXNM{n>iIaz@(a9#vaA@HH)Y`($iqAqX-#UEoHzaay(_Kz*%E}Qy05W+p!Hl$| zkB&roLFjB99wM!5g=R@P>y)Ik2m8-$m^gl1lKWB&leTE-)rC1kmv0Qb4>>ucskbJH zG8LLjV+{M4ffc>)+qWRX!a}495}@o%+;k;WBs`FiaP<0^jK-Dp_MpyIkU zfZ|zX&TulfNq*D)LR9+8dt$L-jzmeW0TJtE&%?umcyfJF?zhQ(I7n~J^D|Juk!m^| zImCG>hIy(xQwC|V@j&F+f__d;ZrWRx$NtMucIVE7=N)V~uu_WW-1bbJUTfgj_SGAN z7d*i1@280F5hcrwveum(;-I2c*pXI_g3hIj2cl}QNc-{GacxG`vg`PhYr!mkN^O;= z9QTU>fYEz;(JIc6a1tBP708{qU1M3D1elC1i#-8Te z1WYOGBj^W5SS>CQIwfntu$CieHle)U_su`TYDr;QEbySJ-1WaNT@YFU7VGi#m3 z5W7*vb+uO8dyBO+f;nhCBr+{{(7s>v)5HNe?pzNrqsyZNg88-pU^{~b>Nx`K zE{p;xBqqlCQaFjOXJ?BF8kKFqlQ;m74$F-WC{06@)M1D3&xGMvlq>*TvphP1^cke4 zcQHzdcZJrKe|-8O!M|a)5e0;?mQ{QuY^BhIE5i6r)a)(J=!#kPy5x=65D_X-u+-0= z1vLbfRPLFpiQ54<&S2ybS1qjFCZHlLVeKJR5eSES9ld7>db$ge1m0iWn5_cz-=UoB9nZQ{=G- zJtg-kjxm0j6B)W@z><*y6K1>z(+NOWSDh-{2RRZdKxJFo^ke?bX=&k7BRV_8lw(I(0y`g! zz;aS6EBg`+VhKTwHxXU1>HGKZK8BBh%s{`hlg!J&S(Y)39)up1WSELGR;aTH_o@y>2Lbp%)wB9Huu5h+cj;jfD6KfjLA zOIz>YSNJ7g?00E8gjJ3XN2$|G_++qzdzAGPbiTGHfBBY?5U93Ks;wyU*J>?cyXUnx z9-Gw_pM5twvn5`lKkV_#j#m$6#dmb+1}9H&7iqm>l%=z&%!}WAK9CHos{){MT!lGehJd%qoYNG&NGRiOm>sUpHX!89+yh zDDah}Sw>7v%wAU}6jbEA!)18``YldwZYH_M5M14?F>iVhgt)hZqIXf`fZ|bS0_0 z-++vXf+cLZq7WpRNZGkS=1I*iQGEI{;{7#i-y)(l8*8gV4=PM>0X%w^WI_jOcVy9q zH*~54_#*eGEH|`%=o~i%!hBorr3o7+>hbHwg3{8ud4BG&p+j%QImw@n>Mmd#4`LSn z0PmxvNRm^wo;Fl>;O#UJa(;hk?h%l;0rUwBf!HD@fQRFcYC?{*GNccYw19Quumt2@ z=O-gbMfg3ZCH5~IZ|-R+0jRW~94C2REyXzGUBC5^8-Q*PV285b6Ng)t*TSI1U`zQC zzr%$ftCLr@ljQuV+2cUFfcC@X_2QHm3L9WReIP{m+sYtHidW7ULpU((_Y@}2m4rYX zzzaOz^#J1&a7H}@JRU!fF|i@>wf;bTa6B6${1_$bA*N?B5d1^Dx`T~pfpxEoBs|-+ zTbMD%0diqv9g;(n#G7O4gOT*2{s`kIBpE3wO=+h z)#AS+BPQS%K7Qc=R0<^H-2PlOVVSnOPlXu^?%TH=R7(wPM~1sa$b!NeIV8=-CT4ow zke2)9CkLpJl_llP8zKtXT2TQL+Y>q|6=YhD7^H9;fuf9mU5~&EgbeeL1P#)UAQ1kB4L3zR=H(?|zQPGsb#D&XY#fg*VT zR`g2LRm5ydLhn(e#XA!33}~T%8boj@nc-WiAD zEqIGwVrWEA+L7o8lDyUexN3RNn~~d(g4iUtp4f2yW_j%#%$x|{Yw0-m3f_0@L}G8; zomZ$@j)V!W@JtVjD0)nHs~zyS4%bVBAVt5TaP>a=YV~oR^)GqX2pe zMzQNak+49R*gVdiLs9~{<#A^rzS{qcL?bhP2+v$M1URb*9Z+OHz}a*71k9;;{#B5~ zkd`dSfpHSU(kiC@nWIG3h3jT&!+uV<2q?G8s8N z#>_y9&YesQg}U-kP0#Nmjq37Cza6okNI^tG9aY9Q2L%NYvnPtRemv9>K#^e%Fd~z* z$Uq1?$X&@x@;Byp*|e|4AJk$u{HW)NWw2W6Zv&u2(%O*?cWVHM&LbVIyYXZ&1Ms*& zcq>WI0`xp}@CriQVD>_K&vuBB!rh3`?|*zv$v5{D7Wk(T6pYNhF(s5?>Ep|m3Z#<_+b9Tq#=VxDR~{A@NI$O^q2h ztT&!AZ4GWzYWU3HH~)S1S{BhYA*F;U85QF;oZf}lS^z`b+?Cs{0b_E=%NvTeBMd;& z?gInBB#{8tQvIR=tU8j<2{@o9DovZ8xZse|K}9w{$@vH63uK7{#{qCT`j1*jjk?m! zqK{XS4H5*^_Q8uspe;tT$*sqE83i8BT*i`~H?6HChJyrE!SsC{{wop&j0=tAMaPGP zgFET=SMB`pfq2PgW`|>`2S{s=Z6HeCyJa&Kl%C|hL*b(o{hW#zRE#m`@?MP5&u@i2 z@z_mXK?=khPXa4&k&$o)SjNW(>o?&=tpfkDNr>YMax>9&xIfMeXC4mLQy4^)1Bx6* z)YKSzDTYfb6m=T*YQVM=RO3jtYwUfB>e(-ALKN!Kyrb^+z=` zswj4#!$d1_P-J-e}Mx>^1*bxhN8)@c|;eJ zM8%3Cd?G4=EZ6Yquc;a6?}xh1}ZXH473fecG(;UbT67AVFN^=JbpV_S^N8lBRt#y(>r@nh{?)0}BhU%T^W)>Oqwkk4q3et_t$Ifs2km zS>|gh+Sr%tSl=&=W6ikB)PH0 z{%19_k*c&Fe=1bhz0ebq3s90xby#Q`(BTJ7z#r`KZP8r|Nh{Za*bQ}10^!nA zd9ptfYE(keV41D1$wf>jNnwtZICzl`e*5LyYi~UQKmh?QQy^TwBxF`(>D%YeCy$&g z{idv8&IcOf79CR_SqSjk>x-g-@CBwBj~$qdtQ1IrNi=ke`ME4ix>P)OE^{PDPk-Bb zF(CZ7sy5oGlF0{n+6uz=%1a2hO_Bpn9d$C4aU5Dp49AeHj&NCU#NT>O;`R>)yS*PD zhkcIBJ^=~`_ti~=-~eH+dijz93k(sy1O8glmw1YcodXH~uqv|zPta5- znjS(1Mwt9llSGE$26qSB*d)&O)mIx&gpx-9KuEvIl_Z~&p>`zwkR10oE)=2ehA#=B z>=Y*_Cz33LP0g0Y*M5D&OqQ6!A!}h{Q&TT=LS$G)9b%VJbP~#(*gkMpBHi8>;5~t? zp$kAp)p3#71-Obg;oh&poj*5GY5-vj9MJ2aP?XudyWCAMG0N=lETXB%v@9mRrd`q3 ztX`oAViqxcft&II0tfcE&oQVa(PN>J{360rZ{~t5gjTx#Ef>5*gjpk*>SR6!vG*V% z#N++-PNdtxeuoKsA{TewL`Ti=WzWC4(SH*?{Pi=h5608)PtrA;y}zK^DD?530kBL} zJs2)=QyL^n0nH2uk({BQcqS~iYwO{2W7X{uZ|bDgy?uA3EISiH#d=OU+c9tt7X)5^DzH_+T*a>jpto24~@Cf1EM2eVc!Xm4jhU=7kAVaq_uX5 zi&w%>szfV@Fgr73DiDg?SQrbDC~^QyNOe0x{pBJgtgmO}{*w{2JcFTZqtpOSBvn^X z2Vne40B0A&A0&hN?#DgN5SRke5o?7EPf~~bl{Z`sp??8PVIJXXNs6fBAx~O40Kc^t z-5x4TxtY&boQG#UtJ;WR%{dG$fESaxgX9~-siAKCKm`8m`Lib9ZwaDZ#}EfbphVcNG}f&;9Vq!;~e#u-g4zl!0;wJ|M#V7Ws7q*2-YB8VlVrA{UF$MN#Jw5 z4R$A<;ej-FSBy6!N-|^`dHsNj)KW*L1(+97JU0d=B_nHML7S+_lH0@afAgLDg8b3 zIwJ&geF!qECNa__TH4CXi~EGD8Bap_G0thG8LZgpgQrH!2-2T~fxu=5JTtZw~JS zy4J5}GJsmP=z*K;wocU0#Jm(T+=i%jD;z>!eL`A`Fwr<;O_9$rQ0AHk*K`( zJ_&&M?=OuMb@3(s(2>24!_I;Qd-vFkNCH{ymYtveR^=WRuM{=sFp@p}bR-av?~m)7 zXQtkT=y@#Z-Lz^QeBBQ##gWxC($2Y zs=MZ<$3g&1;f^po@7k7mdNa0v4cJspTJpJV0NCYy%zrD*%9Y0OQz;uB+>W6I8xhEP z@L(-R)>#{j)0a5Rq5QFV2|Ni=omaiQA|fyRBP$`};ZM#?ZIK=KvkBi7_)U z_;{*N2gEIym$pMK$wUZ4R_KIWHT29IAudI+cltZ4Y`G!*eD8i?Dk%L#F9@zq_2%1ICz zv=U@O_;8RnmgNnU*#nrXM=fndwP>Rb%v&xG)>GAaWT&9~U_|T-f2-Ua(opvyz`szH zjtQEg9H`5KC_P`ja2aAX>n6vba}7e`&jHD+h^cOgknT`Z>?pzIj&vds2ZeS>$98aX zChBE+4?4(RMW8%t(!Ay@zN{zt`PDcxNVAbKB#Y5lKqn1|`*!`$Z(4T&yAv11(JM7w8JecaBF9M3J2lXBSC{P0 zhP5XnOo{M@Rl@$lR&}O+p9Vj-|%E;R|KkALGsU;BE5i8^8PA)5um+i7~p_ zR?=YFb?;w$#Lr(C;XQ!Osw`sN9|2AiA=`wMyhPaooXJX(!^QNanvSqs390IYDUsK-s;di9CWirC91Mx?rx$xITsABEmu zKd6d{7bK*y-kgR=ScuRakLsEFB)AEOB`9oh0A?Dyp(Doa?>X`tv_yYGh&+8@@v~=# znGzcSUZK_kw8fBWOUhz`;^p;5Ij65h$O_g5fKrVdwOsxqs6O& zmLTt(IP$-55dAS;%6X}^d^c&ClW6MrrrS6b7~MkF-pQ;T&_O{+lF=9_edV&dBqXW;E^#)c#T~ezRwmq4?6?Xe zp!ggzm6XN;0C%y&Elr?T2-EV)n38S{HO_V9lUthdzrYslD3tUhxaS}eRLIGUi(u#O-KV}@B5E)& z;-v6JvBIw{j3*}xVKIqh-Xia&H{eW~OQ^t!XXd_iOfxeQLWy*hjQ<6o5E_|sa+5Zq zHjogx0d14#{rmSH8^U{_vf3#qnEw0+NaG0cvy`V7KQG5J0v!TmVK~DcC1*8y z%9S45@PYnDNkJfa_?@|?wRjnbLqL$`7db}>VL_o(qD4{)&?!s7X-ZmK66K%hOYKXK zWJy$x7~ZELiYx>lDE8nu(-xH{lbf(4alY^T!zfyY`==TKgg0>ZBle;)6-raU-b5`) zE|vk)5ugdgR0iv``e^k{BQnbjcnpcwgz%IK6I5ERoI}q<0zV1N8^Zb}7?&Lx^VK9j zzm}kgxV0stpm-qqR}wJ6w(AY9qDpM94bUe*8oWbXTytR0SMUzlXz?hm*_xbV0o|;? zVy?u)zdpb74-+`POO5JQo30IQ@IP7N7Q$7Dty zn&S#tJO~&TZ=k=GpBvc;VYTLfil!-y>M)@b5Y8BqhIi#2`|w;vBD}GI2#`je0YcDQ zX4slrTQ&2pg*@`FScA7wffOj5&4eSa zkp+U@vgK8{z~dIS^VZ!Zve;EVs8B{fyVpNB^F;|gXHAH%XMMXvt5hhTo z5hpvgsEe}!ws;}j3k7R51)vi>+4h0=fSv<3^x4xY7}+=Br6KH2>^UV z0Yh0qv(_^tr0G@XUbKNB^IqM#B(Dg}?fI_7FJgk#jg7~q+|tnHUz^0M4}g8y3^fwT za#K;Mv9U++sv9Q2P-s>Zj6$)E+nKlvg`sT6Bt1@hdt z5U>z08LV|sVbo68xHpj}+gUhfGvIjQ4i|tuk%6^|y2JuiyH6AIZ>V_Mq0{aQPfEG7xe3)A|FCe?+OZ z3%{GLzo^F=z4?#IMG?FoOEX6F*!CEXNZH!j*)LWqL;b38BQkv5tTm&#QKuJ3N<}+Q z-&ILn4IfHH>ja)RTWvV>^zIzh>EI@@y6*X?rt5_n(F(sFwF&^|9aw(r&r|d%9N^yg zTn7M<6(L%aWRqZJbx`D@A|jqc)Q;-R7sW2wFCcp>p(3_$MrUsZWSgWglLSP(^)f+2 z8!M|du=bJAd{hf$RmQ*Vh6ZCd1Eu;@;VFH6Q+zhmjmOp0DnYt(v5-Ot5in`?!btqD zMIUI7MGZ!W1E?OewmIQ< zaQlxVGInri2(NJ^(3|m}Kb7Gp0d;0_ZgI(D7)6;5WXqZth~Pn$Nvq-IOGN~+A=~%WK+S6pDp_ry47KZ0c%k3aP>oX5JMmOcqiWEYInT(uudoHvmh0y>e6Q2ayj za*s}hpkSXqQG?xZLy{U$VQ&NB4w+im7;HNWj*1RJ|C6kkA<~!zyFxFN1o6WJGX6<~s^7_$I;l7Lg3>~+YpL%1C&Gk^qD zy(_xH_Lk7(?Ci^MfnX}~k5>!%&ZmXwU=jqaVOUo7{=*02Dp5W+6fc5d6@DQh2hGj7 zQU0l;jlz`H5xa6w4}=)R-=ePf&CPWTi#mjGvK@eB;kUwVs~9PM7Er0aZS5r!E*_qp zz+&-Q2nY(91YV2QNX=@WFjNX;^fs0=igmJ_p>6DLtS5hl?*v=~K=lPjS0|6Gbah_B zhG51@CGtz~$JAv!&auG7$p?o+&l7JE-K7awmBNLYE9zEg+9w)MZ_@Zt{CiaXX>5c| zXz6%YIk;@~F@yY^Eycf(_P4!Zj_HZZgJ`Kdhv{U@1hv>w%}k70sSto)4oTkEan zI&#TzH6L6LiVn}|^1c=HJI!+TNJjSiC7ZTv*QF>o_0JV5blM*v;{D^NgcCB0d}L4s z9NR2zIiMWpaNwJf0aPp)*m2l4=fkeE78YOg^}awtEjbRo^#G|1w0`%%4NtGTvI%+Zn{BMzr0 zhYoB^96D>NFerTB@xOKZE)W;2Lu`OIQ|>M)DR&Y7&#%rr(R`D*Y<~`OS;yJiG`;0C zbYoTaNj6wFNskV#psM>cq1lhSH`+o*w19PsWuKrBK@I*uIQ=_H3U#|0%ld?{qwG z{w~)tqL!j}Y`q}f$9;LPe8`)dVS+D@z*1s}_#EklS&OiC%h9p*Jv4L|?`V7u%dD$x zd$l<NJsTGP7QnG1R9PN^^Hx+7gb8S+m~KG6Z%jzXSuhjt91<_EZ)_ru0M(A|G5LI zrj4x5xy_TX*_6$gu150Rr3jt+$G&XfLX^#~2FGbRKORiGGc#GS1{lvjJwkp3Pz}9( zeI|c2n}0p2zkaQIKInVQbHqJ7GS<8HWWCGD&c+w??)>g+Sc?QZ{dDLK9{A1JR{YLD)sFCb>crb6^f zz3fIKq6w|+CsLjdMQD+QkffPrKl54VZ>Pt9ErNggKUsf##5X(45`5Q>@{icr0dTv; zi}BOXoW2RRvb~OS4_q&woC3ftMB-3kXw3Y_W~-7n$GART*US*V$o5a)@*n?f%7bjp z{q&A+rV0v5Q}VmpVTI(sS+KOQ3kg#CiBi6F+t&dUB9Cq~WMO-o`ewNxCw<$?l>R5N zf2jEWY0dn@pUv&FZQFll2W(L;Cp+}aS32(0DosBM4>mmvib!qg3CL4HT)~e+lS6VZ zlcyjG>4S&_)w^L)#Fn!o41X=S|MS<8C0tZt*Uk(Ne?EKKtayux_=68hN>huZcG;$M zklCdSqu6>yXsS?p-#8h*ZoS0q4bk^)M2;`iSx|Dw zsEXcll_bUIAJrSoOsy@`vNViL87vx->HDbQ!0P+_)oD~pltX&uwF~D;I zo_D3NbM;*jo-@*c9S?!eZ`R|5&mo`K|T-3}3aR4-FdPMvG zT;qTJgKk%BDmkpCICX)rq=xFM22Q14J__CMH8WcCa|{0hi9eC|7hhU2V_|M1ZFf6Vk=`YgyL1i3PjCOmS1A@E z?~a$oJ{~^#{YCD_`wzNu12h>{Ze*aPXzo-!I${+{!-fSMDn4*+Ek7 zMnk23N^8aK`jv&xKE>^b=T*$=*eTDx4BGO#x)G?X*RExep{lwOvLEF4otKT1LN5k> zJdBTDzPUZCHcBcjEO*~3M#h6CqAbBecX}x1R6063fS!y>zkw)2iwQiY8ZxyD^-1^o zf71$k8fNXY@!I_H+^#QQzCera^R2Uh%<|+F5>h^TiLSw1D{Z?Ym7jmGilW<|r*LcC zIxJkB_v$f}6N}B3k_x}q>eqg9U`L?&zKofT2ydvk=CvF;`sV{b@{)t8_a8k<=O5so z4N2ea&YcPI%?YmSiOq}oXrvb(vr!-bj@4cpJ|0iJJ$vz&?TlN-zCt3t$|&q+dJ!zQ zK30&uN1Lf&mT|*F9Y0m=)Q3z;TYeVx-a1qCNn%nkrTwhW>#t|j|18JL%K5G~AEZRwO1~tMmI#S@!MOh5UNE+WrHZR~UMhG&@Lgx?efF#^~;%zl_eAG@BQx zmu3zW+NMr_knL?0EYBW9%)Us_eS1VY*Z4ZV-@??(P(%JCyG3?h+-G29fUW zE@|oRkOrme-#qVozfauH_{M*YBPU=S&vl)>SIjlnT=yQ-Y9z)q!B z>A>K8Tw2-#P+nmIc(X(ApI;4+w}dQ48{!3OMe!rcDv3!+8_nB8i)x!yg{mJX>h0or z_jT#hTv!@{u39OT_0%lorw>?pB*UC2Yn6YHK(LvL1iVN_Kah82Eab@gF?pVq9XovN z)VXb?Wsm&-&#;Ns5?AkjuLY>-chi zBAh;2cA!H+fOf6xn-gYfqgdXjeFH(HB6G1Z|JWRGv&|5{bTFnYjr~m+q zwe@P<>%+K-U%+}hoKYtm?hH3OtTZ@)jk0cA_|999^m#QnOiJEYIZ^|(2qt#|A(Pqr zBw&J-)&0*z`x@Z?NSdmtN$)yx6AcV6`J=D+k+#2o-?i-+;@tKfa9>b>zKs(94HZ+{ zb%NsESIZa;eP4X$wo~uoI9wRWVqdKohs}QY%HmNIIR!}Dg=+$;nz1UkVp2;u)inG4B_MZGCj8B(3EjN7!fD@+_<`J#< zf$dAhpM&zsdqB1B3`TU{FU)7Py?3X>inOnvVbn@8VkbE3zFGwsvcz1Q4WTusPQxPa zpfTrWFK$M^KO{8EW>K*yTNsth^?d&?-oLm29B*EZjdKT)9!4_6Pz!@@^yl|`h6kbyf)h<+P}nr92Y zlz?JDNb)APd9&rif61f%`+iFw!J87My)i`uhH#){ArAH3-A5a`U>&4~oQ7-)u|AO@ z75)qX_)P4w&L}avzVL>X5BFWu&hH&~(>4EWCtsg1Fh46=t=r~n(R{*OeQgkXlEf?p zn0a-!tB|ewq~{)7HcR0jp3d9=6#06~2{vl>p!XJ(kBlBXb$LwS-j8^qJ;0+Nxas%; zu^sD$228mm?~v_PU*zg}qT{5mTdrX=Y6aiE!7#U8L$&Ew!RXud-#zaAzB#pePgB<5 zO+eyx@4$bX+>ZfF<#CzD92I1wb2OzO@V zRVDiNq+v5;m3rl*eyrJaJNX96VmRnKqm)I)bo$ex0;iU)8 zWb05LcV+Mn5_X5JMbMar0)IxB`19nip49U8*!mUE>d8>_ny`%=iY2cT0pKv58gvkm zf;#>CAJ1`1bn4OUS|89rhk4gb)P#A@p^{+d5>I9}sHj={IK!ZYK?AGIFL=KO-LQOY zU;mOtE2$FnE$IrFz~L}yM+53vNONA+0+4a6vz`C>oq%iA5!u+-Bxp)yS8azSxTH>7 zZDUYmcB5=XH4CF>)s2oKTbWjvcpg_U4)d}}!CvcfLsMDQ#8iIP%DyKtdj1`D>9Gt1 z3nwp*8}&Xbn=GtI5(LbPU@BoXNijkJyrS?GWy=Jza7!vE_fkUp9{s2|w2~f;FwU-D zR=io#=A0>k|89`~AI}z`g9t|1Tx=QI1gfx|kY;nH)Ul6U7t&Gig%UsHj+*s`nLGt^ zyO@FnTa*tVd;ZgohdE$T*KPB}hCpy(Q@<1qNRT1TiCGC|_|Jx+D=0*>!k(P%_R?nd z=3sySbj9n$_!3a&fINWS?s2%Km6gL{X+4=!cc^{+PfIXNsll@k#hd@`rLOM{_PQ-C zovBt|7-M)Q6eJ&}vT@oYGKPk{@Bzji+U+fPPF?NMr$r~9XT z4j%i`2!Z2!+##}Yd*2wcW-bD1Jz-4?&(>HV*^Xr*#oJWl3`Y9=Z@J%n)sB*CrZIAi zMuIXGMx)At#7L|h|J5)0QUE5f;GB;}FWDbAY$ck{Q>ic2W7SB)1BYt<$s`Zyl$Gkg zt;qlOTF$t!a<|q_Vw#zzS{3HGR-5!;(gGBJ^_^1-&xWlz_9N~xwWS?s-0aqKTZ#qa zfxv~x2HL9xZ%$fX*4Eap+fthX*{QJN_qq``28>`?9sI(RHIPOR5N46{6CiEzx}G9+7MjMzICU} z3vyK2d)sHz6(*tQ|+)LSFEF^HJ3eX+k9Zm!r* zo*vj+VXi4cT`$*E#M2`eS-%2e0~}g}L-_y;*i3QD&~p9hmnA9>;sX7#8a-ZZ)tcM#H~N zIFt17sOG2*45yu(WYH@RKzOyL52F$@z?3Zd(#1B)fL2zdXSbd-6TSk9#H5 zgKR6z*NC|JD7_U8`s=n~qsFU@I-z&Rt?+1JluM<~<^Z*AxY$Wm*3%;f%S=H_3*WZw zhUUIugBDbVLaj3Zmy`9|I(PAOazE+yex5 zA8d+P9ez@Q;FnRF*hSxV5IKJgHH6IA@!ar!bLn7$fxdBmb1LV>n>*cT%^p# ztGaLy&udhvPcXO)(*amVLcSMF2fprli3+jJ2q{C`maBIi|K_oIR^9z_4U%06QvchEfd3Mjf%e=D)}m=r5y}+NrrKi zc#4_L^Jh=^zO>)J$6AJsj(VpSIi2ps#t~SzGrCaellW@j@&4TgN~yoX$Jc3 zP~7-Fu$Rw9`3YVT`Hl>uxG|6e&TrK2dx5Acp=%7J5DKt7=Vs;FB^4ADisLlsuo|w~ z?gHmjcFoS`#`)-Bl~5W8lAVc>66dN&kwNU=oA#Te(`Ru-3fBcipU{E!MQx?cz6{3a z@m@U$r)@6(CHICv+4xgGy=KE55eEMJn3oW`MRDD$Z(n9tjF8$YIO;VWA%;`}`I zVum_C@WLORcI%&P7L+P1C2)Cd^%pbv-`7~z-U;6>`553q+l*^<(8|r}X#^kD!WP(1 zb*|M3lheh2i;(zQhRh^Q5DXGGkxY#Qv%^A$zT+keqMFa$8!jD`HyC+S6g;AFzO2hE z6m95Dl0EP~wtu_gIkEmCsLnHsDqa|b^jAif`{DNWq94A19>dL1KHIQfYcw|oV&I9| zqF$$Rg*C@i+<-v-q~$kfhVShc`+7&oJMTw+Z{E94_Vt*Ka-fL{^mFwmz9~a;V|cyc z%*%RJJw`U)F392(f%LJkl( z2B$D2Bez(?7XK9x-6t{~psOK4bt7~Rt@cPyF{puyl6zz1xJW>JC3(#DCsMX=IC{w5 zW8XYHaQwT+2nin!R(W@Q7+3BO3v0&`2*}-iueH96Pb?fRSZ(wpJyP(#qTk*Ih282! zSQO?iO#MAoSK($Y2pIKbGxgSOKmyhwcBGgul%D-BO#W^cGE{!v=}&z-H(PbSI%oG8uz2{H0aT@J?^^%wq7c3da* zd*Ules@`>R1$>Q`iycDuUj|R1WC+jcSQX>ur#PWN#(Hna$Cu!uIvkOfAscI8(SIFy zw4bsGU~En$npJ&mqOA+1J~h4Z_!(%!yfD3 z_f<}{UUut?Z6@fFl^xu&v?T5vVqOZ%R%0pbAG&aw{Fm?h6%~lkAc>!vszwdt^03TC z7vL79dM-t(csWss!PQrhd$5`#cISHX0@pw?eNo#kvwJrbKy!s}_J>;%(Cv17qC$EP zv|a8+p%^cqt1GT=$Ml?8Zgg{e$5(eCh6FL zZ{x$~@?^%&ABjT1B)DGP+}|{uwB1eQ0&Z6Q&l13hE76;6BE(Q^xI~{N2SakCiB;%c zsW~W1eCxJhwsbvQj;O>V#t*RY&{enLYy;^wbQI=uiG|txt z(=&~1dXG7$9b`%FO=kJ=merSjS~*s4`c$FEnfV09yt`XuFBht{l^eJEJMK`6yP~4t zyri@}?`}Cx3bLGM^INgMZ^{4r&tyE+t7ki5TTfWtdQ0yuXfWrA0RxH2OHARyJ5Ys^ zIs%tJiC-6wDwA*u$l~k!#pM7v+ik08$t@@>>}=QftX$5R1mYDyBb2l|G)@@6R4-mO z3zyDxn6q~o7D&j*AU-RAh~?#-y7QAC^%}CQ0noWC)vgu$r$?aC=!eWsON)n)J3wp! zY&0R56@eul1d6GqHb_{GZ^dTcir)z{h!Mbu1T*Z?ZC@8GauDw9ydl{0?i&m9o zj_8*&iNcpo%l8L7Lm1@|eL#*?Vf4;(lq)8b9RY)dA{(7ankZPjl*H483WW(dvIKih zH$BMPY4WL^EaaCU!@qB2@GQr0jd&UhhXN)%2SFx*tk%(J*^tk=Tm(4p*4zXTNvC(d z_q%6(9=EkD`P}gxNY-(15!#&i2}B<~Nh7I?HiAIB0#G>CyO3#3{-m%z_r-0z+{m7k z5G|e-O)_gpQ`EFkpHiD811iIyc+dwl-c@vT7H!uFVQ+4LB-Sh2_RUz=FDLT#_MK3G zV$J={2_euQu(Z~c&;_EOsmregh%IZICeCoG@q4LzQ-tgUaTu?maH@GsTVLN;vObrU>3${bYt{%C9AjK0z*RT z{yL%N7Vhr&&m^)3#=K#+FL?)y5bWpPX)Nt$!G(}fC}JaK z63rPO(ZmU?RlX({6^@pLSHaNM6DyyAG=DhTG5|Y75Hw^}j4&fWfu>v_4PcK#!iI;4 zsFyW+fLR?Mg+mg%t#1vwuXVy30ld|6Tejt{-3`>9P_@t=yk==h{ep#7+)X-KLqE|) zf=M&Nva%W=9U#lILITbzJ0{#;vNS-w7~Dd%OL=Z?-1$k|TlL+2^sig=xzm{d3YIcV zqdAPDS%QOy0tcjAa;kEmBy%g@#WhbGcY2*XGJhNUBAMIy($DJ!`r|K=o<7bN&6Ui( z6g(g^g;n1A$V=jTZNz`E^$GOjo{0U-1n^E(p4{2oW?a}eUlt&<2O(*q2jXY?#Dm?5IR51*3@t#-|EuVkxr<_<(YWl8as0l48e+Wnl)0&f79C2|h zqV60o3W;2)kRgzWhc8%5JEMQI+)z1Y3vLE|MHD&l4!27MPC7=;i&>b7;c60OGK9$M zD6L#Swt(uJS%BO*nIdrrw+j|X-Cm+%H_wL9=VPK}60*`0zhIvjr=NOVrh3G_KC)y- zXpKv@Rv9q6HlvTr;c*||pnrWINP9Xw+Ni0jB1ARw%H#Cs3iH{o1}VapW3vHwkJT6L zSC)eAqu34QCpGWANbwelPhveVPIi3a#*j2P_%i;!Cf}@MyJM-*t3(8_{z^$sSMmkLjnBk&K1=hai>&(H%#mDzd0vTG6}a&X^(EOyInNJ4 z#${GMt`58*wV2jb_*E3u&-F4hP@i~yp8yJ`z$3tRy!H3xB3NlQvpmn@0Y)2CU_b}4 z$rv9xsE>j&3kDR*IS7_W;@~+rI6yfSzRvm)6;x1+fijqV^D_Ct9pD+wRrvZ0>}RqV zb_loJE?A{-Tf7iwt+*Mza~MfNmKEB%VdH%`Y~+d1VsTElhlD{E?or#;*~%XNeLIJ_F@ z?)9JpCQer95s;cUmakU&w>5MzPiL2Yd0y^^H2&V*%)>S4;InHdPB=|~MQ-a5Ll;^i z4-z`$B;4`l1O)*&bduSytuQ0&I|^w&$U0VkkVdjA3^*TW!Jcky*o9jZ{b|T@DHCk^;GnrOs}J# z&U!5578o?t=zV*^PFVe*d96Ba3-`C&FJt|aGLneM&W zk)@8{+|3MfIda}%;uW35d_=^Li5)Bw2p8xxP7%0#@72z4!+tB7&&fARnt9m$HFBA* zO09$1WB*8QX5YbNOoqkdQ1k8Ah-oRe4gWl%1fbG(jTIO!a!Znuz=p#0Ck>&r0LgrX zC#1b&--UgSL>d4>mMRe%lEXa))$dJwwlh5MTnUiQthRmSRP`9L9g1u6$vb z?1#hpX&(pyiGJ1o;>)``f+^&!3!tI(AIFr61GOs>5+qcaENRf9mr@^w4|;)i<3Kxv zaIUhErYU zt42VhjExRO2t)zwM*e3z?qT(;ZeYXLA!R(pN!}Qk`qZ{ z)nuWH`Q7(p?H$1pFBf&?il;YjaP`Fj7?k9F@eZS@m{|xoeoni-Z^+Cjo=0^H@r{r` zM#`qhVR!foxd-aoy;mHi%?>omU-z8I5%>-M#iqW=gn%5oY$R!%q2%5u37N1eu7CP{ zj(W;o>5ro#_s!}h3uDuVprbPoP1(^5A4@l5eAeMfBx~T>>htI1%3|6kJn)JbsOPfgdjr{;-4hj$7i#}amf32z9 zf(DyZ<=>~%ie_Cr(qFgW-C}SGoq=}^FRyUnr#PiD7+?8koW|SL9y^v7MIrWe^{k=F zARKV_{s6LQs6S!XU2qZqguFMl@Br(}UB$8H%Op<{$R)Y#b>7AoG!&T1*%nBj&$>o% zWvUwr((QS}yXKqK&B-7J=(0Ctbvng?1m0EXSJobc%_tHvGt;lw-KjrBtU|<_a8Nyi zAsJ1^-YP9PW9Y4@n@xZ09Q9LYBka&@EGr&1J{v)Xv8QEWi1Q2mQD15~!~Q&fu5iWXRAy}$&2kLbk&D5f=qPx6pAPlL;%Aroq~;7Z&lOX zRy;HtPKUBW*wZY>vz*RG>W7{w2jfA$Gown5+Lh<)N@0Juq`1lD6ATWLMeuUL! zF%ng_YCB)ma8PXb6)E;8l89OY+|0onG%O8fBc?f71qo4_c!o9C zR_u{{41aYN{SEc3vc*#Hy&RZx<${M02#Zw+JfWT9|GLg#ez3y`;mID!2n8s*xrM+ zPY0`%WP@xtIOTRK=`#y<{2*0;hQLZ0I$g?OCP1~|)~lTima6md&-~%W&SPuSYs;N? zd&O8JPW{Nc*W|y$qZci^%XMn}VOc~i<2;Sq+t#{p(2C{wqwkw6i|rGJd}m@;wi0M= z92740Y^JBB@XiU)jKn zgr%nrXc2T78PFUwsU-Ar>MO1FLJi{k>Q_{?I#$oEMmCe*Ij0+b?WBs`x=3eJ(_dip z%=igeM85q5rBaA41L&sz&t3$A1S9f^%xz_$JrC8yVoDhxczmw z9%t<|gJjjRIO_QajwKhi|KbAV-|JXQK!#+Q+o#bXD2H8VbiSNIGW&F=g#HmBSm|;- zL;%@g{O!tK3UG#~bqaEXA*MD5h0v@O~plJ*E zfg)6e{e&@~E3tk1%`S-n)6>(p0Wgx@3$7)9fTEf_E;T9_Proj!wOLf>CW^&y_89)8 zBoG30rNT-c5kU#C8$6AneYzBv-j_=V9v((b@t%+%FZ83S)*RMsJl~C%DJuGOAzH@$m+_mB%es?=kpt zE@{H)>C_zJz3Z}O)g}IflGTij@c+vuet!6YK6Na@TZiWvoN7hmrb4F>=lD}3{mtry zy`maDyQn?$yURhr&%9R3+icmRCcw*ti8)egXG?q*jcWT?xQG7{VH@9G&tK`%C_fLz zV}{M0in;0Fg+p5#X^rv2!FkaVjp-L8l_OTM;BYDzl+fNkfJalO@LR4b8bD%b3vpd* zl|+8N@4{6j*V78r{!Gz;;S@8%9@M1gN85BY8Xa2#uEZ)OZ^PJapx~rj!n6C};kQyGWb;DnkI|;Ix*k@ONX9Ipe40grgxXO12;M%& zK#kyq+Qho;S@gyypMmTDEN;jtwqiRXQr1QuFVfAl`~$1Vq}l6U9RAo*se1}Ub$M#VuIAw`FwMzDYge! z!b-)m7-`R41>M)T?AzGpRx1cuTPX{1=f6 zp5av!K{UvcuCvypS_IGa+c_=?6#GfqEUlc+dGmuW0`_V2ODvP@gH3ve4 zTog+JA=0EDf%fuMkBMLGlMar=01~Ew(i82^+@_ksE7el%#uxhKT;>;w`Lq4&2WG2) z7Feg3MaT$HC=giLUIday+mhFFYC>Za(SD&m2X&&AFuo>VRFF zXTo_KF^;dnSm~(qs;c;@Hsf1opld=wRSQYRMvwI7!%9qk#=Va6!L7Go$>Vl1+RG_r z9i1rVcHOz}1RC<0v7oOXWEx1gAx+5qqm-Sm>BE(!%wXzi{?CYDx_Am|i3XGs!CpEN zIZk!#J9X{52ImtF2JCK%lGqq)XU~>lP=OYpVpZx;`otP%WWtWnae~OO+py0 zPk1PZ>x_g7_S&Cl>8`8+XXiL+l)ij?N5OgcFtw+zXqZso*Vclx>NAI3z6VAPRLnnX z5gTfTl>fv^3-YLgnokuL<&bW;qXscVRunx!w~42Tyu4&8iCy(O>x3g&lFuj8zN=l4b-W|SJH08tzjFggjVqa-U zD2+XH-NYTGae2|GK}A&6_E9@SI;8J}!us{Z(x&bat4Y`y8QKDa;h%-IPt-j{6d#Ks zy2Dde_FjH`QPV%a;Y2ay?9CL1`aTo-V-6hRzBx+terTu4GG=Ks!Vlei-paXS61i7J zOW#e(X$QdG1Yk@^bU0S@_?5+E*Pi}7{H}$;P<#MgUfsM&dIJ0kO$H^!{yhlz2vK;h z8(nEHwa~bl@aQ1uU|~bWUHW_VLMg`7Lzk0Ypu`+UG*5nqX&Go!N~#!o8))scUfJ7c zBekB8+D5|sHoG(AY74m+Nav=X zgAN2Z8DrynGTn4`xK{)|9T$E@^AQ)JJ_Qas1dLstI{V_$5l2zw3|yQ2`;2qmsJOzg z5HST^65vB6O+}<$Jc_Q2u%Y6@cQV++7d!h5N| zXzElpcUI1a2BoN8BOf2})vjDV!-r+kgfaZTF{ii{u1`B6;dWUmH{P%a*r0eyQk$6T zo_OAY{Q26kOLb2DQ=E1({>hGCT+<{JTayIM2Kw@t6s`he=$==O&bOykm(@9^7l6sp zb=+jE)+`^ty*f6m{@JD)d#`2Niism^rvF1Cc{Bc%oidR5#; z2Fd7|^Am=dWC*m-1X{5ObbfD4lqUsi?VW3k`1hylHnt-tI zbRhF7T1e>LzBQ^R$m}FT0HB~Cf422yN&nnv1kIl@4)eI=G}fw2w@`&R!RnfcJVal=vHLZ>;DTMZV$YdXmiT)QdLQ`*)T)t?fpM4KYW!LFmV&%_tA5^Y zL2EAToDTJwy}&Q|L*sdCJD(BpaA%pb&W>!t8ZoNlO8ShXP`^zkaUOwWC9FxIcSvGL zdABxwDZ`AZ$L-ysy~|I{rF22qyOOZe&h%Gh zDYJWrPCsUHk~tCIG%Iu#qzDFhYZG@#nvm>}Xhk_DXMd5pOeqh0!2SrySm(uy&$g3X z+ONdH_aHd>Y^&s&QM#9^T;?>B7x#Yi*IN^NTE2A9xRtB_s68;eCY3QXEQfs$%Q?()pK$^2+RIZd$q{QwWs zRI9{HWcsukGCQ5~LM+E~E&F(K|MkF80{`{tDZMFyqha#>6;AP#k#;g>gMiK3RA|0% zI`2rLMEBflFDzbqD1wtjpMg3o%Eh!sVd5pUj;#J(#a~1*cF*7&3;RS|+aK%MZH^e(&Bb+x=NMeY^fbUIs7F;J|U+ z()6Pre#dRo1D&h-di(BDC|+LKd>e3=JIQt(=V)~g9I^Z-sDTE(1PyqIo;&AEA*%6uMlmqW&_;jZnV$|2|H09$e0b1~F)|dRG5cJEETk}5*FL_T5>xUF=pK`pR+lsDVXN~18nRVz7 zkqajgwvX!#ELsprGg3H~9vdnc&J`JuN%^L!WMHZCu$FP~DMCJ!P|Cm!j$TnL0)e96x0 z?$Wc2M{@Gy76!0tR>TJunru=|^`m}OzTJ+U(x>KK4~b$2pE%?!4XNqH49xV%=slBm zr&9YVBL|E3tq&42Nb(AV5eOg}SOTo6X8<#!X#^ZS|7EtW+Sll1SG zO&_gziE6S!UrP3T09_}$%Wl;vP@fC5 z_~FP6*+QYgM$Jcq5kxhGeW#d|ES~a)Tu#dUH1vU_Q(05rAfWtE{+W8`m_%u;)IW(a za(~4?-DrPbPQ0pSBazgI!R8NrnVX_fCzxg8rWNV1xZ-xYzW#224Hu3GoG^%R+_TLQ z^%?Zh%D0fO8!mwfI{Vp44XoIm9$D=Vv#wQi#u8kB+@h?d7L{H*9b5D3orbK&hrWz$ z+CNq6+@-64uOrh)TnyetW9n0dH82coWXbB)`E6}>?KbiDp66=Pe@9ET8Q$RC;|>=; z1Vq({ggc!Zn7f1d(_XZ)8mStMvFrZ)!WVbo#Jou0eaqWFs6U`kEX<>7Lw$gODV*r{ z$j`&``*DN!#f4`?aEyA?lHbjqbtkFR>TL5c(dXLW7ZQKM9b(-VG!G2OzT9$URyo#gHwS8r}0(b`+!PBy_}zL z$k5!L$!1c^oDowGA@%C);-ky<`+ME$>IvBX0Fx8!D0)ugQX_-vRt%N?&t^_X)t90tUNtf$A9(3&-|olK}V_0Lm3j zsQ_^t1W9g}B{81rGrj3ce^CNOYLPDVX)SlMT}q-Fu7}$>vsV_5e(&`ZqcWVrw)#q- zqvCDQMqI)=*~_hcq5%2@8a~`l#+9zWJ6d5hBM}2b*c|5Dg)AWl1*n4QdX7e&6Y~5? zT=0pupH^ADhVvlPcQY+_Ey@%1b`o%|c1~QyQ+I|#HWOlL4hfharTBmuzdh#e>& z$f^XaJD_HMvi#8Pq>)^{?vVqES#>mVQGBo6p&<@eR{b(QZ-wp-A?eF0{RcDwT7uEb zi*gh{C|h&wy+dWDK*1?rs>EUEUX)hHwLUmse{#D=|*&O!KPsw`$NCJdx&(TejfxFR{JIo0DnWTwN0h+-z( zt^=B8(2pNK;-`)$X`vtP&=F*gzB~W=2@7_(%)tK1Vm~a3c9(6v$sNdc3k4p2;Dcft z*w*wS820m1LxK9UmbDbj%tCuEI{V26z7|dpgV5;w2({Y8-(u;~#LZ5}ZayzeWzGJR z51lhg3oJa?9+s0`?X+83aWL$*9^0-B)OM*m+9q>RTypJpzJ`4CZUk!ygikGL3C;6X ztdT_TAYYg7f97|ub@!;gxYU+Avp=@BenX6qWhoXCiv-pKE`w!NDv`#%HNPelcco@A z-+)=av#L0M}VjGLqHfQC4?*?z5$<>t^!!^ppI59RcXobc;aK!CT< zA(AH-lYCyD`pC50)La?N_ei8f`uO9qByfP55S zaG)|Z47DOY#X8+Ea;-G+lcs&QK6rv~E~kL=)u~}|-$4A?;KymmkeQlfXB((MuRP;P zPQu?RTjuwzO>_DY{l9l}e%e@1^4F3eKYr!H<(cV@iTzb}^z%E<{YSF(>4fbHc#QX0 zia6zzbS}>+nb_#ptSKaHQ0?-IOoFAR+6JJ`6L$p$r;v!-GKKb|r>y$Hflpe_1Q%PC z!}5Jg?BtXK#=DX?@1GmzbSQl>2P5xrtip}&wE`ElI#eXA{)8E;r|UrS>k zur|<|`E#RHr5?vH*~dQj=yA3{KbINq7xS6 z$XXXaB3o#z{v|=Uuu)VDv@=&)#ngp}K&gy;D7$gXXJLDi4DRLzb&5J!Hxd-JLcU!- zKA>2dM+62!HRyJ>lWSnW$hYm~WX;+P?@gfE`I$09fG)VdLH7c=vVd-*-gbf=OR0wUBTi)@M(ea)B%dQ3zx{SYVP#X=qXU zKy)lpIIAu|@!}arSm>%A;n^tZeQyFu(}Q~2?ALz5k|9u0Fdem!2|$Vaa%WfoRFx@` z9Jmc<4cKZ%zP9<=pUM1#Yt7q7=oxJBJTm-+{V^Cp1yzcn3trn&*_4YBQDhl*`wJfF zB6_Q{!qd}7x%0Kgps0@@7?4G96^0qSSPwdk4{MX^8k0tLyV{Z3`v@P7t^fQ!%9jff z#aRE8>SxE#Rpiwg7Y0y?_a?r=ttgR)IOE(D`CC&1Z$@eQIJGZjk+0saA|MVy{}ulOL6Du%=}4-GSi04Eb#G7h!* zbslP0gKh#mLs7MZ+wEun9n@N|lSqE~^6Y#rw>l@1_4jA3#NQ*F$MzZtt60?jxi^Ou zL-O(hdOawZw78mpF{f$T9efA^U-XLWH22FjiuN&P2vGJ8?fWC#L<2eT10jXqRjnyz zfD`u)mE3Vq|Ni%yaf{HNud@nA%kk#^H&zfm7`2kqX8k@Z`^t?HcN_xa!VcgpdcFRg zI=PEG4o_=Ml}|AMYv9m%qf66^E?!n@G?1z|m1SqDL1pzP{M|lA;dlqY+qWuBlJmZG z9?Ub`otn7dkQ};ddDX5M17=wG>Q`>AJ>rkS8JIGAIk5q<$~g-rk|D)tccozu8Bs(I zGaI!DCoIKd7Z+U^lD&UAV_xB|Ef$rIz_}q}#AnM{oHi`a9=%X&9vP@k6tR6j_#V*Z z1AQUNx0$`kvlX50Z+?|=7Rr)*DopOKJ zh}JbC%uI^nGYZoB7Ayp$HtAy&%>dWAP%_|%MM|O>;F9RImUtf)uFc;+TKNFW?rho| z)9dS}?%BQu@2z}l5_ND>{BFHO>|-rdYwlYD0o(U|d78ea4Bq*{H8V}a*G+#kmKKJd z1YTwm-8XjDm0_MWuq{8GyyIhw5yiHLNN!ho9DS9Wh>2QN$0)6wk4_tRyuB$ga@^+a?A za&rep22r(+keSnnsuo7@fz~M~Sf(j{_#LD}_u-Q^Jr0kFvP{GRGZBW-8vP`^ztSLca8 zaup`WI3}HB^U5`C*G;c&qviJ~U%8}-2jd9;)i*wzjl9ECBbcDdR}F7cB#V2eUa1-j z+H!|TgaCm%R`m3_YAdd?*2|`pSe_T4@w8`@)E93OPeb1W`wyI0^n39>VoIr#*Nm)4 zXGWS8Rl#%75(wnM7^46CW2@o9q2G2er%GEfq8E{xPC1`Kr7TF~dnoN**!*NjP^{!( z?W`b4^K38`r`&N;>rZuBT8$B$qBI<{V`ni+XV|q1iU_L*( zSN(F@vU0><{Y28WSyj3E9*=?m^B}+B}GD?W_N5VC2bLg=sGjm-_N!Z0D0_hxY!cPyEagw3rXi4)>al1B=nLMQhlgd7LHVjR zv9x>rq{XW}xs!Imp{kKdABqxA7rW$6wGZmKH!LNDJNJ0Z)-1obc)?NL4`l*%q(8*vh}i&zEijA4Mwx% z7IMM-lSXhyd?Ee$SA0Haw)Cd}>vwPGHhD0%&Q@JcjH)l}rSHJfGz@%CdbD5IiJSD0 zFqDL!zxK66^Jtm>QL7$&{r(sdCKd4azQE^sz7Cyt;pOSFHg;c=6Le7%jYRp|o%J$h`TdZi`Q@Np%*xh3h^0~@fP7X~qP`aV2 zQ-L{NCQK_)?K7=WM|DN5ar{X0k zdP(6Ts9FRsXG~xBz$+|@Ef^Hsyhz?E3r;|OxXUWa$6u|99D6Ezt&NI5)cgKtKToYZ zWT`ythqb*`#Q?H6Pbt20mycqmVXB)LVT1(O3jrtCCD%mmr~LnFE&gdYf|Zq_c@(K% zId2NUz7P`$!yPUEfq97NbjAYB!U$c_8i0f{OIR>Y*f5UU(ZNa%iP}nz>SGhs20tr= z7DhIEXLpi9)KO_@rfWwhj&jN$_i%UFYg&>GIHi?fut7t#KnB5n9x_UFqGYa!t-ii) zUu@_rYFI%1D~Rjfw6d}a>bA}A`lGVYE+i=Q>HC)H7H&9lb&s5!gm)Lv*G-U zVGNGn)X1xrm~z$rDZiA_3}>l`=_-)A8o{+#w{cWA4)u=r3vbcU=rr!ygawyzW-D5R zBXL!t%)qkcdMr-=%-CL0umUMPmViY zhh<{Co^_(Zg+7#j`%D|*&;0ILN}jlW zLLo7w4r)uKgo9Iq#@#h4lR{I>B~*L{v&)2aU>(xBPF z)%`oH6&9R_-Vy_wc?x>|WoD_wXak%Qu?QNmn!~C;+wK!Bh~+ibh!(5!lZ3%9%DB(V zG6<}zNM6)d8#*Y=ywXf=(mZcyp!u!lZ6XjXbs!YVz;>7HpJG>W3<%!(3(6%j5~vnod?;-uHju%Z zQu;_}?>7jyRjArEs?+(y-glpZku_;U1DiQAkj(W$ZPP!Oq}uuC?2!Wse4aQ23>M3Uz}at0*drn&6XDTYY+9Qg8@x6uB|k3B+GcD z*%SmA1oxIRRTVyHxcaTMh~KJ|23KFnCf9CDCMyS&qwiu{n0Pb2+it8F73g65D{@>= z*-(MuGyKkk3r~znJ|wum6!S*GiPBbQaNty>nfsJX?n`jB!f&d2{McAkk$z$U4P>NB zL>9tGleqx@r0lBCGVbO--mwhk`NguQy>Qzq>vUaw3ay!KTExI`AE3BTmj(8M$O;Mu z=-k@0yuG_{SE4RsTrqKqoyG2TEn^l$Cgpg-c)`20{;Q8KM#sU!ckfPCiTFkhYwl|i z+eTno1YwSA39BK?bbOGWUiL}mQH5G8Pl1tC{OZ|`TVcklOjJp3`4PPxZ@&vDgxo#(g^&A$4Qzo6yQ z#EmbTG(%bRSPQQWdo6|z%kl;>mY}fi7lK!dx27-QfDzlG`{lw4pSaCR!0xwi6ioFZ z5jw~nFuKs4w0U2Yeb%@5qyL?!fqRs5SWMHIP|4{1iVpx!@j6^Cqmph6RfY>RM5!N{ z!;x-tt~)Vb;jFWhQmgTY6I&4ILe`fBj^Lg}!>!T~;xZo4HJXrTrKi46mspRI37ENQ zjEV|;@Dja-J+@cdH|xMHxH@g+cSIzmU4;7n^29hl`=<;)|2g5^uZ`Cr?guiOpE|C6 z9}?)mjf-U!jrRj-lfOVSKFf4e8K!l$C)o+H#+{SHb4|%8Q}M<@tWkDLPEy(#C=?nd zrv+=#Zp6l!dYlJgiWSZlEG@wx0}2ypm=vo!kqIV!?s-)Y0$hB=4y#Z@{G>Q`EUKT# z^IF$17GB2F%^y>&aINFv{M+JIC~X3}tFO1UI$Xo0l+Zohh;o7s>e z3{!0OCf0=YsAaquV$GlYu|4f)v?H-|$KA|0Z@Sj?(<*JlE9Z0m6c^=N3Wj{)N)gS@X_ zWkEwt(<1e?%r%Ri?G)+7x}=>Ug3MMS{9BG>YlmV!lrfbGiBp6(FQC5!u`Qzz8L=x|!;xBQ6A@>usRv^RK-1Yh!)KvrF>ewU za-rbPIP%d?Z#NbHF5$7T=Sqhp;lL!Iks73fr{%w-+=P2lOC*46QKhVFxLR4dYt^In z#l1iqW6)gngKXz%4Pq`q!+ur9MgLg*huVAoBw}UO2TePw=njAOGwHb9^!Q(TXv9r{ z8c-wq&H*S=PZ6`b<8)F-<|YdMEpO**Qhv#NR2wygV^o#8S;=W8&} zqbh>aW2RoU@_{2DmkjSvi%hx#>r_k_<&ev&BiofhLLjGW&hL;HHFOq_iVWqRVItHI zvqqzH1@)gM{;x1?_cD`VvB#EfHnFFt6W76QWmXAcaR+fONRPmWhj46!ti81q*$3%u0CG&#~zX_PU z)L#oIq9ePI!<;v9h6OPG6Fc~OrQ8~f<4y18b*%bhV&{)X6|e@pzViCe=Ekr8fH*mXD>Jno_G^TN z8FqAQ``z~%qzfOSA)n`r9U_B$-^j2(zylBftF)Z4EonZDs9d}0BsAS@g(&p>_WuU~fO=~!884?Q|P*;LqO4Pv}4wZHu8 zN0?()arCwqJ}>avL!_-QDqr#~m<2z!h-ITFFN&RG!gj3Ni_;NU)Ps+@EtQ!!Qk(2d zNw8{;n6G7-cU~DfYu(So@cG+Po1IecmF!B_m0{PZg7=Q{z8Tga1{BkK9Zn0$4qcE! zJ;-|ySn=JxO1c}JLi;ch6v=m~E7~3MbvN;k+uiVl`El;gn$n8gPn8}8MKDvuS3(L3 z2VU1ZN_Xj3tXcG!87XWO2CJW<0f1YRwhu;4O&j2*o{NX^TFA^1G%T>3T& zQlqsRRZvi&B1SSc!mM5lk>x2kw%K>5*l(yWSXpht@QVrLg|J&sN5Q9Y{g51_e73XT`8}t+=MXF zCpiMl2`DHp3iL?Rr~Q=qao41yg|Wo&pc%LNS*2oy z7t2lXd^#?3#rvt13~||@H2$gU4Vjnsz!fx~30O|!GMN+LY+R~D!P~rOE;*AqH_ou1 zG02AAJ$>17=ae5qbMRF8w}KZoNvd+aCOrz~ak)ijapXsu^0=IWscBm@%)d9{VW zOuvK*qb|d}<3Fd@BZgI|OSae{A)#%ynGF;iDmk)53jyODK<7}ZZfQ&KmnCoQ><}KB z%9wbykjcu1gIPq~Zx)It&AR%To4Iq|s1#}jgTdNeCgKLG=sql58IkbMaHD8%-u=Bi z`qNp?YD?H$x`$?pMXuXWgg9rZ{LGTNj+BQU%WjgcM=@77gcIvJUkQKx(^FcnU@b)Z zV%oB#Er_I{v?fBYI8S!WZ#K zIG+4Jd61vhw8Weso=3-q&ceWuoma~loSwk}z~t;Z+iL&1i=;Pc)u0NU8N|-r4z0?<{z9g! z&o;vJA+MW{$`(Gw^afMQ3-UHmXIWw0tc}?7K`n>(m)bol|L-fh=BhX`oFsDgEm3^z=6$ISmS14n~ILBg5Urs0-r=k3h#6n)E^aF38~OWoR)yHwu05CGQ6ZMPzl_ zQR9q0QbP1Q$bHFjRcwo|LORi?i+g?k@yEfbj0@=#mB6G9YT8OQv!5X#+6A56YRt^J z*v>Gg{SklEbi7TPw)*w9y4!ynp)Qj&=l)OWAJ(l@Gby^_mJ z8{gJtO#q?GrF?GxsJ4R)OaB{~-4kys@sdx&W7LBY$Z)}+22hgE0R5%f8a6y@0yyJ= z^fWCHjmzqtsAxytlrcU$*P_FC?ep>|m*p>cR??BE9RMKsw{1TyTh~?u0hbDU?Rl_B zlAq(v{?^%C8AO|JBxh6UNA_@sdQP2Mj6AZD{~;eJCV*pY$g>?c_9&b9v+XN@e&bpZ z{cWW(OFfgYL_Rstw4RaJi9Mh)p6{nP07fFLafXZ(<_LN##P96UpbBK|Gf>S~-0R<2 zf+UUk2UoaD!-^8G20Auu>A+EBfWF#%oI5ALx~`nZ@m`>wUM~=idw{oaxdR1Ts0>&Y z%IxmSuI%O&uQ3aujVE?p1E4N=c1nj8fk8ceY{ZpW~}f{kRI~Veg9}X5&e324kPTeEmyLiG;lUjQRCi=iloJ zEfQ-8H=hNHYH8mXP)O?|BE&Akkw(U}1WDj7z(nXpUME>G@;uk6IzZx4HM4bI@yprg zHYs=}^vZ-&IK3J(8#^!!qk;%;#0LsXqFk zYKv&mqeHdht^UxPkrtSrVl>}y>3V1Q;F!rcND_L&58MCIhAdvE2Rv7yhmqQY{QKI>OwkvVK( zbgm@pVAbV@hjC|55?;oC`e2gs@ zzw{>GGVC3?Ft^L)5y^A)_-a^HqZHz@VY*;88;M0iL{oBu=sv~VJzWoJ|ukp$TuE^bPA7Iz`>M@$dW z9-3kYD54mLP$1Fd4q{gcDV$>nnk-V7wUjTj=kcuPZMeSVe(j06L4y-I>-;I-=CoSk zRpe#5lQT(MmByn)`C*k$Y zpw39K79M)8k|IjU&6kipovK9f4-?|h+a>*w6#D6Qfp+`h;5XQ%W5nZ z);%KD=%UuGFdb#Ox6MDAn`Lx*L=W8nA}I`f;`swo#4eJ<76}(A;C78HF5<@9Dw}as z)*qvhe&uLcLQC0PW35&pYKihU*V#2WClT zYNzVq&)amM05Qy)B%B1=tG9)rBmb8pjM6rf`y%w=8l$kr3-3Iyj^ zA-@)xIzqUeiLah5JG@_8CY$m4T|TpZ<=Vol+O8|}^vc>)%HaIK@(^I$+ev|A5JgV3 zV4H1VAYT@e7eUd2o@!1LSbj>1Z}2nk*4w(Mb@%m>*j~kq8@}RPg^lpeYMERlI;-%u zq1jWPgv%iH3z=KQa#|B~5Q(hGg_GF5dwIKQwcmfj@Wz)|Aa&@{zheoy8A*rxtHpvxK}%3&>?QgOl7 z!!D_cpT2@^bD~}ACkR3~cM!s>A10W82-)2;(JF{u3x>25%CW}1)JYDhrx11pKw@|5 zxhm@2<7Aw*SSD4kBljFTGp-`lQ{={{qIa-q5FKU)Nhh-UtU*=*rr7=KMN*4>+75Tw zCHF#cmSX^CS~-AM^Kg2L9y$4EF9yE>vDoK|U#{Cm zQjzAf_D2lt!n4=Os<E*Qi z?*)7(A+Gw)OJ9pa+Mo_*ug79x-sOnpI1^nRO94{eWy zL@M*nYt#_TDd(>=?rpPum15lO(VQ-`A$Vw+y+GQ`-0#_?+ zwJ$J#1i~^Y%?!F^Uzj&e)ykgKro=1x?}iRXdo)5FC8`sZ>h_B@qH zif@A5++^%h60J6PQ2@G6DaB@u<3}Wd)-#l!hLD6^T*=5IAMVBf?acpecuy610xbZl z(CuVkleM|Il*I|gMu_@G9<9A?{9505?#o!yKI``Z=-PcVS0S{g9t!dt(HOang^rr& z;WV(z@@DeqoN{z1Q1it-k&cy0QdB=!@V<}@U%iF$U;)V5Lq$9Yff!AXS9t6>nJy4 zSN9yfi)NgXu3pcJ)~_ca2o!?owl1Fl_&@I=IqIV6f;WceCyQ)+xKSirVo{LTZt({V z!fF@RCyyOb^nKiDrK(<*!g9%xxO=w5D{>|nu1ASRxR-8Zy()8YCh(JlhSFN|cqz^y zy9FGSjC4<%qV%D!IPQ)1NcRSb%W`YM(FtY4@B970!z0f2AT6W3??-3vohd|L3#zJs zT+QkNr}i|6rWFmL*xb%>74251+wZxK=r<-3&<{hlLxB+{Ox4YkPTMynKjMf}cOB-(`Ivhz3PFX}mIHD1^kr$;-b`dvV*6IYWiI z4dpYeaj|?FCqO%4ygc*W3$Y_=*_Tn4^&r)r!>jC;4%Rn)7)af^DAx~HMnll#^M>En z(_@e>KChVbHHxR&)0b5yI9{GNf7y%|4OE|%^52y* z*kK?AZlf4L^}pRh`m}{+1?A4}@nsvyM)$eraA{K;l3xp6phx;)D1g$4k!+4p_>Nq< zxPEu}e&pdn61uwgI^q}dfGCXQadbF`uqAVP1z_{1ToIdp51$hiMj0*r6saXID(Wd9 zIA3ckqpxYVZOD?4WvxUFY0<@oD-{<(7gy!}N3qlMXBFW_Vc#qOm=Ntz;x8$Y9C`%f zpBt!W_NRiCSB0K!C|XaX17UYc-xnxp)&QPfxF7*=w1}PiFqbz<2|1H)3}}}tClr*j zn)1fTTuHlVHsT4*KBBq!W}>yE?3Kr^wwF{Lpt8F$X3`@Pp}SaA#f8q=Pts-nB;ygJ z>O1#o5XdT;p9vFjoc#*PcSy)&DoVTC1H8G5a= z2z|995&4M+uUj;e^I4k@D*>7G7XZ~ztG`2?*SV+eOCB5nZQfj6feix0clXgaZm$^5 zKmkNzf&}ZjxA&|2k;4NybcuBAk*#05Hw_-N91j~9#P-E@8~{pACxa;3c}?BxsL<4% zycNQ}EnkM{0uP7a>g&}wUk*2AoGiF{ylA!rpD1+3ix!~=;3JNWI?`d{#^)M^tG?9$ zDVx_Rho%nJ^=kt=CJaK`Nr!8+drC!ico@nITm#wZb)jTZ;9lII$%#L1m1njg5&#B$ zxMrL2;1sBO*txk3+scxtpLWyB+MF$93yWC_M6%@o@(TxLeYAW?V|e+2_!DXrzgdE4 z`e=N8ONom1RXnr-iFv{!_>pT3vv<8;F=s6$A%6ACvl1yLG+0-qGxp4kTvw&Jnp-VO z?M)EMDB(Fw45Ty_Y`p~%X1Y!xg{pOB?yDyXA~7#rME}YlQTgAqPJfwKPyP5{q)d^} z&z|ru-cWHQL3&XHJ`<8$k{1E2HWt=u&!+GOTF(R6Z3adkF9Tk)Fd{k0w>+f5+cZ6* zNx^ck5I1hn4@x(b zr7}N|VB1}K8wq|>d>La@8yMN>G-&>F#RAYLErIl|2SJOafQs*%;a5Lqr^^_2nop4f zTna2TBon(1TN8TiqXa^hzm1+C@js~C3UaEWy=O|zA8c18g7A>3%=a+#^qXZqKPc*}%JuCxTQ$Pe4FQ--L-!zEx~KZ(S0E?)Yc?>XR8` zR|}yaLP811g&%)gb}}Oo;)HYjiwp2O{EFDDkUOwzeY^kIX5p4%pU*wNtm_js?*sI5 zdnMHxCcW^Jw>I6*f5*>#NqG19Qc(eKiVTJ85Ua|SYIe1*sg0>m3l%4~)$h4BN%f&* zR(_aT0E>u5jInVYoqMHgp;Q#vQa+ZqllztVgDyzdp5k6_(C^TF zwONSo3I2i4(&RhOR;Mpa-MW5-f@ZF<=$^@`V|l0kibFjel6<|nTJIMbSjpwe7=1se zU640Y3W!KGVxs}c&LgyTRDqsU^w#;teeMS5DT>9Ra#qbQ|M}hdBd;OKVK;`Nxf|)3h}u zn!JASzY_uK=tR5+!^SNk6T!ODvg-+nJ>?zMN~*@zQNIXkbVGuAP`MNYWG&6OX8zCQ67uLrR=249 zb9b>@NqA69azb<$-vx(ieYF)5lVXMY>4LOsK(Uxuq;Z@KRFjIKNZ&Ohm4nBR2cyz* ztqX&eG424pX*Tci0>ID4K98REMv$LSnZj$cT8kGjS6=Pw zm@Q$D)PQ!#=|@~~0nWct$8eB3u6`4%S*~%{d7E2f{BBo{<4-Fyth}W)!kh%N>RRCd zT*ifCUH_5CIUZ%op$&InNB!Q)aP08lCx24}o~uLA$F}OBvSh5B{4j1Bdx~Nve$R5C7QgrS>rn3W~963`n3XEh~3!o-$cj4Cq;Ae zWm#HYdwJ&${#`N1#H)sgD<-1#c6nE zOdj%nsxOx3iaT5i9Er>X_B)0`q0AL=my4<`OP|kE?58>$2q#Vjrs5=_eb*#&^5i<~ zupiV}^NUE%_m&^LhL8|!0*lAc0_GSyvbgCUK;20JM*hkr0~d3-GbNz8b4^Zk&Dc*@861LI)!#PUy}PbAE@6s z-DRx0-sLZg0M^bs4r@@Ev7R|IKmlG;JD{Exat^kEu566Sm}p1|pfIw#GjeZPbZ#c! z%CBSMiJ{;vN2jxd%vwOx92B|X#Ah*g$Y`O)c&4KOMjyQ@(#6~^;A|4397L2xQi20? za!;sU^*Pc`Wa(ML+J7xc{pDD-V(oaSA6$mrAFc?mZJ*s}vgp4(Ymt;%|DreKXp`4)i25$mi!zQV z_p^e?8z^!Bwb(1->gq#^MP7NyErqmE;5Cvnc)#*N*G`QTdno@xQ)h%XGW` zLi+Y<>V1T>*3kAenayq&W0J%ovmf0$I|t-nQ0F_nKiIU1 z#|m;^0D1ef5~5(t+q&^7KTyhNbeW3a3sN{j@4}`!o@wDK79U-jrgMy zf?Dz$w7S`eyx(*a*=uD~HETA`&mP8)UOPFcWXwzw*pdL=n(B977%g5S8rH_HvD1M)9pGVZDaNOvxxzvkTuBw`P8!U`0uriXpXCaMj{Pf zRB*};rc=w=63Sa^?v9KRjTZ`;O-djD%Z8`b-zIPj%^6n9jwfqed|y?yl5 zZou4rU>kWe`kf2)_g?a~)l>7{Z3pBfx}m*`t3lyx!a2C#G>FM^H{J4>A!x9HhwS2D z-r5Jj<3B&T!2~o?xlh%M;%qOm6j-(bTN^&}<)QnKyXf=%RsH-c1BzM;QOx*Zo(b{Q zQR0vMHOt7z5tpI#J@{Q6u9+Ny@QZeWvkX9_7mX0NIN(Lrc&piCSFg?cheE>AH*7@& zG5$G5=ZC(O?%9|C&@U4G+4mJh%lM@TGiM@TPNfwMgvB6eRC;?HrAn&cN0msm;Sex< z%f-b@&UdO=#W}?V5Z2a-w71bV!N)6XsA8U@552yv@Coqh`L8s8HY2k+)=AW?nRQ5S z$mTiL1phX@P_x-hr(qFFM^`l}48fN_R_ma-%G1YV8dk$7L-(x9BzSbZde10F+Z7mw zBVWpD!h>Vg!%N~_vP8Sn0?`gNN4q0ap_c0q5yd#+_%%{0<>Kd6>1H{-py*dpk1C0) z5AOpAg>PizUz?TK)~tw~QnT0dQ6VKY&s9H5tVg9#P4+EdRZV|O^2ukg%l$aomF?ne zB5l+hqsV*d+mu3@T-T&}ZkqgStpPWF;)1n&9pJ{9t1ZO+8bnVv51TH?N@qR=YE~hD z)nnp3IGqZDx|aYjFh>92JP&wG-$B7P8@Dm+rU$k!8n+PT34g%o&YPu~2nGFUdP}PF z>0%9eoEX$32!y|H-atoN^GD&I>mu+!V7<@*+<5)wzIRcP^OUwOpC@3U&Lb+fcbbp$G-5y;K zXQis~OvdE%*A_q1`zGbj@tDgb~DDR9+ZPNt)Fv9_@{35dF{ z_G$rZlzYphm3o(gUvSN+_Xq)M>xAzJQ1m@!>+o9WYtaj$ji8{cdep-^BE&}@;C*mQ za3Z<9;L~cBi{cd9d)))W$@=7}S1%elGMrjk*W%ZQGY4t3l<=Sk=ShG9LpQVv`F8Kz zCGV^+YW;EQ>W$$3JdxA={(|!KqE>)qHT|9EP8CJVZntP0J%M7CI#6H-0Zuj7vQJZu zT4|i3FP?Ao=7(>4_7l)h&1IzR&ee8z!!aX}-9G}g75sO$xP#zs^=5*=-69vg!+6B5 zgBI!+LPMKue9f!EFYMj33lAxkuRyGIA@fClP9u5!`xCtNh!FZTX82?dk4x3MhQpP6 z`-MUUA_$t?J8y1zU`XecANKMt?Ac3*B4u0XUh(_lx!mQSP_auVOHtk+`^dr9mRH#3nh~=LboG(&-^v-tbXG_`EW*} z$PNJ<5ClD9c{@ZCL#eHGvsZM;J)=L!_18{sZ!hRJyF6sz2Y`b=jSgX9{$(<;Yn=bx z#&UG$$KWQQ>j4sp4RR=ldT8lOM|ebsZ9r%Uxq zSK8+dYguY87sH&ub&KU{VYF*q$tqcK0P?&`7pzp0@2f zK>%In)%X53TMNx@CtiKcEq83w%QZb^^`M)OxRo(H&R0x!fBYV}^hehFfcQDFE7_W4~SYxUhaU<*J`Zgs-~ zq9=Ww4CI%M-734sq$5jKQ~zNr(uTEoV^LdDBi>N6NcD5D8I6AL%cIb%0DGv-U8S1t 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