From 5dcb302e8224e298b05a74dec48b55361403bc12 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 20 Feb 2023 14:18:12 +0100 Subject: [PATCH 001/410] Black formatting --- bin/check_samplesheet.py | 2 -- bin/read_visium_mtx.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index b7ddce0..491dc9c 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -215,7 +215,6 @@ def check_samplesheet(file_in, file_out): sample_mapping_dict = {} with open(file_in, "r") as fin: - ## Check header MIN_COLS = 7 HEADER = [ @@ -308,7 +307,6 @@ def check_samplesheet(file_in, file_out): + "\n" ) for sample in sorted(sample_mapping_dict.keys()): - for idx, val in enumerate(sample_mapping_dict[sample]): fout.write(",".join(["{}_T{}".format(sample, idx + 1)] + val) + "\n") else: diff --git a/bin/read_visium_mtx.py b/bin/read_visium_mtx.py index bb22de9..6c85ebf 100644 --- a/bin/read_visium_mtx.py +++ b/bin/read_visium_mtx.py @@ -14,6 +14,7 @@ import anndata from anndata import AnnData, read_csv + # Function to read MTX def read_visium_mtx( path: Union[str, Path], From c93cd418937a31345205c86c50be4c114ab4ec52 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 7 Mar 2023 09:37:28 +0100 Subject: [PATCH 002/410] Fix erroneous schema/config entries --- nextflow.config | 78 ++++++++++++++++++++++---------------------- nextflow_schema.json | 25 +++++++++++--- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/nextflow.config b/nextflow.config index 4c06b38..cb1503d 100644 --- a/nextflow.config +++ b/nextflow.config @@ -12,59 +12,59 @@ params { // TODO nf-core: Specify your pipeline's command line flags // Input options - input = null - input_spaceranger = null + input = null + spaceranger_input = null // Spaceranger options - run_spaceranger = false - spaceranger_reference = null - spaceranger_probeset = null + run_spaceranger = false + spaceranger_reference = null + spaceranger_probeset = null + spaceranger_manual_alignment = null // Single cell options - single_cell = false - + single_cell = false // References - genome = null - igenomes_base = 's3://ngi-igenomes/igenomes' - igenomes_ignore = false + genome = null + igenomes_base = 's3://ngi-igenomes/igenomes' + igenomes_ignore = false + // MultiQC options - multiqc_config = null - multiqc_title = null - multiqc_logo = null - max_multiqc_email_size = '25.MB' - multiqc_methods_description = null + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' + multiqc_methods_description = null // Boilerplate options - outdir = null - tracedir = "${params.outdir}/pipeline_info" - publish_dir_mode = 'copy' - email = null - email_on_fail = null - plaintext_email = false - monochrome_logs = false - hook_url = null - help = false - version = false - validate_params = true - show_hidden_params = false - schema_ignore_params = 'genomes' - + outdir = null + tracedir = "${params.outdir}/pipeline_info" + publish_dir_mode = 'copy' + email = null + email_on_fail = null + plaintext_email = false + monochrome_logs = false + hook_url = null + help = false + version = false + validate_params = true + show_hidden_params = false + schema_ignore_params = 'genomes' + enable_conda = false // Config options - custom_config_version = 'master' - custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" - config_profile_description = null - config_profile_contact = null - config_profile_url = null - config_profile_name = null - + custom_config_version = 'master' + custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" + config_profile_description = null + config_profile_contact = null + config_profile_url = null + config_profile_name = null // Max resource options // Defaults only, expecting to be overwritten - max_memory = '128.GB' - max_cpus = 64 - max_time = '10.h' + max_memory = '128.GB' + max_cpus = 64 + max_time = '10.h' } diff --git a/nextflow_schema.json b/nextflow_schema.json index 2f942ad..89c9256 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -39,7 +39,7 @@ "help_text": "Use this when your data is raw spatial data that needs to be processed by SpaceRanger before downstream analyses can be executed.", "default": "false" }, - "input_spaceranger": { + "spaceranger_input": { "type": "string", "format": "file-path", "mimetype": "text/csv", @@ -47,6 +47,12 @@ "description": "Path to comma-separated file containing information about the raw spatial samples in the experiment.", "fa_icon": "fas fa-file-csv" }, + "spaceranger_reference": { + "type": "string", + "format": "file-path", + "description": "Location of SpaceRanger reference directory", + "fa_icon": "fas fa-folder-open" + }, "spaceranger_probeset": { "type": "string", "format": "file-path", @@ -55,12 +61,12 @@ "description": "Location of SpaceRanger probeset file", "fa_icon": "fas fa-file-csv" }, - "spaceranger_reference": { + "spaceranger_manual_alignment": { "type": "string", "format": "file-path", "mimetype": "text/csv", - "pattern": "^\\S+\\.csv$", - "description": "Location of SpaceRanger reference file", + "pattern": "^\\S+\\.json$", + "description": "Location of SpaceRanger manual alignment file", "fa_icon": "fas fa-file-csv" }, "email": { @@ -115,6 +121,11 @@ "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`." + }, + "save_reference": { + "type": "boolean", + "description": "Temporary param to be removed.", + "fa_icon": "fas fa-save" } } }, @@ -523,6 +534,12 @@ "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." + }, + "enable_conda": { + "type": "boolean", + "description": "Run this workflow with Conda. You can also use '-profile conda' instead of providing this parameter.", + "hidden": true, + "fa_icon": "fas fa-bacon" } } } From cc4a30373fa2e6dcff64a6e31e6364e3ea012421 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 7 Mar 2023 11:15:15 +0100 Subject: [PATCH 003/410] Allow usage of manual alignment in SpaceRanger --- modules/local/spaceranger_count.nf | 8 ++++++-- subworkflows/local/spaceranger.nf | 22 ++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index 188fa12..bf6f466 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -13,6 +13,7 @@ process SPACERANGER_COUNT { tuple val(meta), path(fastq_dir), path(image), val(slide), val(area) path(reference) path(probeset) + path(manual_alignment) output: path "spaceranger-${meta.id}", type: "dir" , emit: sr_dir @@ -27,6 +28,8 @@ process SPACERANGER_COUNT { path "versions.yml" , emit: versions script: + def probeset = probeset.name != 'EMPTY' ? "--probe-set=${probeset}" : '' + def manual_alignment = manual_alignment.name != 'EMPTY' ? "--loupe-alignment=${manual_alignment}" : '' """ spaceranger count \ --id=spaceranger-${meta.id} \ @@ -36,8 +39,9 @@ process SPACERANGER_COUNT { --slide=${slide} \ --area=${area} \ --transcriptome=${reference} \ - --probe-set=${probeset} \ - --localcores=${task.cpus} + --localcores=${task.cpus} \ + ${probeset} \ + ${manual_alignment} cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index cd4a68f..56f7dea 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -24,7 +24,7 @@ workflow SPACERANGER { .map { create_spaceranger_channels(it) } // - // Use a user-provided reference or a default value + // Reference files // ch_reference = Channel.empty() if (params.spaceranger_reference) { @@ -36,16 +36,25 @@ workflow SPACERANGER { } // - // Use a user-provided probe set or a default value + // Optional: probe set // ch_probeset = Channel.empty() if (params.spaceranger_probeset) { ch_probeset = Channel .fromPath ( params.spaceranger_probeset, checkIfExists: true ) } else { - address = "https://cf.10xgenomics.com/supp/spatial-exp/probeset/Visium_Mouse_Transcriptome_Probe_Set_v1.0_mm10-2020-A.csv" - ch_probeset = Channel - .fromPath( address ) + ch_probeset = file ( 'EMPTY' ) + } + + // + // Optional: manual alignment file + // + ch_manual_alignment = Channel.empty() + if (params.spaceranger_manual_alignment) { + ch_manual_alignment = Channel + .fromPath ( params.spaceranger_manual_alignment, checkIfExists: true ) + } else { + ch_manual_alignment = file ( 'EMPTY' ) } // @@ -54,7 +63,8 @@ workflow SPACERANGER { SPACERANGER_COUNT ( ch_input, ch_reference, - ch_probeset + ch_probeset, + ch_manual_alignment ) ch_versions = ch_versions.mix(SPACERANGER_COUNT.out.versions.first()) From aa6980ceeee507bf7e68827697ef61629d9e5113 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 7 Mar 2023 14:41:45 +0100 Subject: [PATCH 004/410] Fix input file name duplication --- modules/local/spaceranger_count.nf | 4 ++-- subworkflows/local/spaceranger.nf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index bf6f466..68f5db6 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -28,8 +28,8 @@ process SPACERANGER_COUNT { path "versions.yml" , emit: versions script: - def probeset = probeset.name != 'EMPTY' ? "--probe-set=${probeset}" : '' - def manual_alignment = manual_alignment.name != 'EMPTY' ? "--loupe-alignment=${manual_alignment}" : '' + def probeset = probeset.name != 'EMPTY_PROBESET' ? "--probe-set=${probeset}" : '' + def manual_alignment = manual_alignment.name != 'EMPTY_ALIGNMENT' ? "--loupe-alignment=${manual_alignment}" : '' """ spaceranger count \ --id=spaceranger-${meta.id} \ diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 56f7dea..5a55ca4 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -43,7 +43,7 @@ workflow SPACERANGER { ch_probeset = Channel .fromPath ( params.spaceranger_probeset, checkIfExists: true ) } else { - ch_probeset = file ( 'EMPTY' ) + ch_probeset = file ( 'EMPTY_PROBESET' ) } // @@ -54,7 +54,7 @@ workflow SPACERANGER { ch_manual_alignment = Channel .fromPath ( params.spaceranger_manual_alignment, checkIfExists: true ) } else { - ch_manual_alignment = file ( 'EMPTY' ) + ch_manual_alignment = file ( 'EMPTY_ALIGNMENT' ) } // From df5ad72417da03d1eb80ab3c538fc3f0cb2da1d1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 7 Mar 2023 15:32:57 +0100 Subject: [PATCH 005/410] Fix schema, config and old Conda specifications --- modules/local/download_probeset.nf | 2 +- modules/local/download_reference.nf | 2 +- modules/local/read_st_and_sc_data.nf | 2 +- nextflow.config | 1 - nextflow_schema.json | 11 ----------- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/modules/local/download_probeset.nf b/modules/local/download_probeset.nf index 3539731..36d5908 100644 --- a/modules/local/download_probeset.nf +++ b/modules/local/download_probeset.nf @@ -7,7 +7,7 @@ process DOWNLOAD_PROBESET { tag "${name}" label "process_low" - conda (params.enable_conda ? "conda-forge::gnu-wget=1.18" : null) + conda "conda-forge::gnu-wget=1.18" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/gnu-wget:1.18--hed695b0_4' : 'quay.io/biocontainers/gnu-wget:1.18--hed695b0_4' }" diff --git a/modules/local/download_reference.nf b/modules/local/download_reference.nf index d7ac7a1..8c1a623 100644 --- a/modules/local/download_reference.nf +++ b/modules/local/download_reference.nf @@ -7,7 +7,7 @@ process DOWNLOAD_REFERENCE { tag "${name}" label "process_low" - conda (params.enable_conda ? "conda-forge::gnu-wget=1.18" : null) + conda "conda-forge::gnu-wget=1.18" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/gnu-wget:1.18--hed695b0_4' : 'quay.io/biocontainers/gnu-wget:1.18--hed695b0_4' }" diff --git a/modules/local/read_st_and_sc_data.nf b/modules/local/read_st_and_sc_data.nf index f451f7d..9602dff 100644 --- a/modules/local/read_st_and_sc_data.nf +++ b/modules/local/read_st_and_sc_data.nf @@ -6,7 +6,7 @@ process READ_ST_AND_SC_DATA { tag "${meta.id}" label "process_low" - conda (params.enable_conda ? "conda-forge::scanpy=1.7.2" : null) + conda "conda-forge::scanpy=1.7.2" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" diff --git a/nextflow.config b/nextflow.config index cb1503d..2b56dbc 100644 --- a/nextflow.config +++ b/nextflow.config @@ -50,7 +50,6 @@ params { validate_params = true show_hidden_params = false schema_ignore_params = 'genomes' - enable_conda = false // Config options custom_config_version = 'master' diff --git a/nextflow_schema.json b/nextflow_schema.json index 89c9256..c68f590 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -121,11 +121,6 @@ "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`." - }, - "save_reference": { - "type": "boolean", - "description": "Temporary param to be removed.", - "fa_icon": "fas fa-save" } } }, @@ -534,12 +529,6 @@ "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." - }, - "enable_conda": { - "type": "boolean", - "description": "Run this workflow with Conda. You can also use '-profile conda' instead of providing this parameter.", - "hidden": true, - "fa_icon": "fas fa-bacon" } } } From bf9a5d7e9c7adf74df9bc6cbb3846fb10ca33295 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 24 Mar 2023 13:16:40 +0100 Subject: [PATCH 006/410] Update resource specs --- conf/base.config | 5 +++++ modules/local/spaceranger_count.nf | 2 +- modules/local/st_spatial_de.nf | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/conf/base.config b/conf/base.config index df36283..7b766b9 100644 --- a/conf/base.config +++ b/conf/base.config @@ -49,6 +49,11 @@ process { withLabel:process_high_memory { memory = { check_max( 200.GB * task.attempt, 'memory' ) } } + withLabel:process_spaceranger { + cpus = { check_max( 8 * task.attempt, 'cpus' ) } + memory = { check_max( 64.GB * task.attempt, 'memory' ) } + time = { check_max( 20.h * task.attempt, 'time' ) } + } withLabel:error_ignore { errorStrategy = 'ignore' } diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index 68f5db6..f26482e 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -5,7 +5,7 @@ process SPACERANGER_COUNT { tag "${meta.id}" - label "process_high" + label "process_spaceranger" container "nfcore/spaceranger:1.3.0" diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 81cffd0..4b0eedb 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -7,7 +7,7 @@ process ST_SPATIAL_DE { // TODO: Export versions tag "${sample_id}" - label "process_low" + label "process_medium" container "erikfas/spatialtranscriptomics" From a9996fc7f8b32206eee223c510031c38ae9d1430 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Sun, 26 Mar 2023 21:12:38 +0200 Subject: [PATCH 007/410] Add clustering report --- bin/stClustering.qmd | 114 ++++++++++++++++++++++++ modules/local/st_clustering.nf | 23 ++--- subworkflows/local/st_postprocessing.nf | 16 +++- 3 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 bin/stClustering.qmd diff --git a/bin/stClustering.qmd b/bin/stClustering.qmd new file mode 100644 index 0000000..028ab39 --- /dev/null +++ b/bin/stClustering.qmd @@ -0,0 +1,114 @@ +--- +title: "Clustering Spatial Transcriptomics data" +format: + html: + code-fold: true +jupyter: python3 +--- + +```{python} +#| tags: [parameters] +#| echo: false + +fileNameST = None +resolution = 1 +saveFileST = None +``` + +```{python} +#| echo: false +# Hide warnings in output html. +import scanpy as sc +import warnings +warnings.filterwarnings("ignore") +sc.settings.verbosity = 0 +``` + +```{python} +# Load packages +import scanpy as sc +import numpy as np +import pandas as pd +from umap import UMAP +from matplotlib import pyplot as plt +import seaborn as sns + +sc.set_figure_params(dpi_save=300, facecolor="white") +``` +## Reading the data +The data has already been filtered and is saved in AnnData format. +```{python} +#| echo: true +#| code-fold: false +st_adata = sc.read(str(fileNameST)) +st_adata +``` + +## Manifold embedding and clustering based on transcriptional similarity + +We embed and cluster the manifold encoded by transcriptional similarity, using Leiden clustering [REF]. + +```{python} +sc.pp.pca(st_adata) +sc.pp.neighbors(st_adata) +sc.tl.umap(st_adata) +sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) +``` + +We plot some covariates to check if there is any particular structure in the UMAP associated with total counts and detected genes. + +```{python} +# Make plots of UMAP of ST spots clusters +plt.rcParams["figure.figsize"] = (4, 4) +sc.pl.umap( + st_adata, color=["total_counts", "n_genes_by_counts", "clusters"], wspace=0.4 +) +sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") +sc.pl.embedding_density(st_adata, groupby="clusters", ncols=4) +``` + +## Visualization in spatial coordinates +We now take a look at how `total_counts` and `n_genes_by_counts` behave in spatial coordinates. We will overlay the circular spots on top of the Hematoxylin and eosin stain (H&E) image provided. + +```{python} +plt.rcParams["figure.figsize"] = (10, 10) +sc.pl.spatial( + st_adata, img_key="hires", color=["total_counts", "n_genes_by_counts"] +) +``` + +Before, we performed clustering in gene expression space, and visualized the results with UMAP. By visualizing clustered samples in spatial dimensions, we can gain insights into tissue organization and, potentially, into inter-cellular communication. + +```{python} +plt.rcParams["figure.figsize"] = (10, 10) +sc.pl.spatial( + st_adata, img_key="hires", color=["clusters"] +) +``` + +Spots belonging to the same cluster in gene expression space often co-occur in spatial dimensions. + +```{python} +#| echo: false +# Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 +st_adata.uns['log1p']['base'] = None +``` + +## Differential Gene Expression + +```{python} +plt.rcParams["figure.figsize"] = (5, 5) +sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +``` + +```{python} +sc.tl.rank_genes_groups(st_adata, 'clusters', method='wilcoxon') +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +``` + +## Saving anndata file for future use. +```{python} +if saveFileST is not None: + st_adata.write(saveFileST) +``` \ No newline at end of file diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index a228c4d..d604b2c 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -9,24 +9,25 @@ process ST_CLUSTERING { label "process_medium" - container "erikfas/spatialtranscriptomics" + container "cavenel/spatialtranscriptomics" input: - tuple val(sample_id), path(st_adata_norm), path(sc_adata_norm) + path(report_template_summary) + tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample), path("*.st_*.h5ad"), emit: st_adata_processed - tuple val(sample), path("*.sc_*.h5ad"), emit: sc_adata_processed - path("*.png") , emit: figures, optional: true + tuple val(sample_id), path("*.st_*.h5ad"), emit: st_adata_processed + tuple val(sample_id), path("*.stClustering.html") , emit: report + tuple val(sample_id), path("stClustering_files/*") , emit: report_files // path("versions.yml") , emit: versions script: """ - stClusteringWorkflow.py \ - --fileNameST ${st_adata_norm} \ - --fileNameSC ${sc_adata_norm} \ - --resolution=$params.Clustering_resolution \ - --saveFileST ${sample_id}.st_adata_processed.h5ad \ - --saveFileSC ${sample_id}.sc_adata_processed.h5ad + quarto render "${report_template_summary}" --output "${sample_id}.stClustering.html" \ + -P fileNameST:${st_adata_norm} \ + -P resolution:$params.Clustering_resolution \ + -P saveFileST:st_adata_processed.h5ad + + mv st_adata_processed.h5ad "${sample_id}.st_adata_processed.h5ad" """ } diff --git a/subworkflows/local/st_postprocessing.nf b/subworkflows/local/st_postprocessing.nf index 2b6170c..c75380a 100644 --- a/subworkflows/local/st_postprocessing.nf +++ b/subworkflows/local/st_postprocessing.nf @@ -3,22 +3,34 @@ // include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' +include { ST_CLUSTERING } from '../../modules/local/st_clustering' include { REPORT_ALL } from '../../modules/local/report_all' workflow ST_POSTPROCESSING { take: - st_data_norm + st_adata_norm main: ch_versions = Channel.empty() + // + // Report files + // + report_template_summary = file("${projectDir}/bin/stClustering.qmd") + + // Clustering + ST_CLUSTERING ( + report_template_summary, + st_adata_norm + ) + // // Spatial differential expression // ST_SPATIAL_DE ( - st_data_norm + st_adata_norm ) // TODO: Add reporting From 0abad0bbe021fc2bb4245dab5d51a10c2c2e779f Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Sun, 26 Mar 2023 21:13:03 +0200 Subject: [PATCH 008/410] Add temporary Dockerfile with quarto --- env/Dockerfile | 26 +++++++++++++++++--------- env/environment.yml | 28 ++-------------------------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/env/Dockerfile b/env/Dockerfile index 52b5427..b13d94e 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -1,18 +1,26 @@ FROM nfcore/base:2.1 LABEL \ - author = "Erik Fasterius" \ + author = "Christophe Avenel" \ description = "Temporary Dockerfile for nf-core/spatialtranscriptomics" \ - maintainer = "erik.fasterius@nbis.se" + maintainer = "christophe.avenel@it.uu.se" + +RUN apt-get -y update; apt-get -y install -y --no-install-recommends \ + pandoc \ + pandoc-citeproc \ + curl \ + gdebi-core \ + && rm -rf /var/lib/apt/lists/* + +# Install quarto +ARG QUARTO_VERSION="0.9.522" +RUN curl -o quarto-linux-amd64.deb -L https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb +RUN gdebi --non-interactive quarto-linux-amd64.deb # Copy and install the Conda environment COPY environment.yml ./ -RUN conda config --set channel_priority strict \ - && conda env update --name base --file environment.yml \ - && conda clean --all --force-pkgs-dirs --yes - -# Installation of non-Conda packages -RUN Rscript -e 'devtools::install_github("MarcElosua/SPOTlight", ref="1f364a965ab275ac54a711e28c67554db25d547e")' -RUN Rscript -e 'remotes::install_github("JEFworks-Lab/STdeconvolve", ref="648d2303bb38002dc6e9a5c9f59487b44d8161d4")' +RUN conda install -c conda-forge mamba \ + && mamba env update --name base --file environment.yml \ + && mamba clean --all --force-pkgs-dirs --yes # Start Bash shell by default CMD /bin/bash diff --git a/env/environment.yml b/env/environment.yml index 9751c27..6b99dd2 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -5,7 +5,6 @@ channels: dependencies: # Python packages - bbknn=1.5.1 - - jinja2=3.1.1 - leidenalg=0.8.9 - matplotlib=3.5.1 - scikit-learn=1.0.2 @@ -14,28 +13,5 @@ dependencies: - scipy=1.8.0 - seaborn=0.11.2 - umap-learn=0.5.2 - - # Pip packages - - pip=22.0.04 - - pip: - - spatialde=1.1.3 - - # R packages - - bioconductor-bluster=1.4.0 - - bioconductor-bayesspace=1.4.1 - - bioconductor-deseq2=1.34.0 - - bioconductor-scran=1.22.1 - - bioconductor-singlecellexperiment=1.16.0 - - bioconductor-spatialde=1.0.0 - - bioconductor-spatialexperiment=1.4.0 - - r-argparse=2.1.3 - - r-corrplot=0.92 - - r-devtools=2.4.3 - - r-fastmap=1.1.0 - - r-ggcorrplot=0.1.3 - - r-gt=0.4.0 - - r-hdf5r=1.3.5 - - r-rsvd=1.0.5 - - r-reticulate=1.24 - - r-seurat=4.1.0 - - r-tidyverse=1.3.1 + - papermill=2.3.4 + - jupyter=1.0.0 From 75541581415811a7293f89e90119d93ab44e97bd Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 31 Mar 2023 10:34:05 +0200 Subject: [PATCH 009/410] Update credits --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 599618f..35d1db2 100644 --- a/README.md +++ b/README.md @@ -82,15 +82,19 @@ The nf-core/spatialtranscriptomics pipeline comes with documentation about the p ## Credits -nf-core/spatialtranscriptomics was originally developed by The Jackson Laboratory. This project has been supported by grants from the US National Institutes of Health [U24CA224067](https://reporter.nih.gov/project-details/10261367) and [U54AG075941](https://reporter.nih.gov/project-details/10376627). Original authors: - -- [Dr. Sergii Domanskyi](https://github.com/sdomanskyi) -- Prof. Jeffrey Chuang -- Dr. Anuj Srivastava - -The pipeline is being further developed in collaboration with the [National Genomics Infastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/). - - +nf-core/spatialtranscriptomics was originally developed by the Jackson +Laboratory1, up to the [0.1.0](https://github.com/nf-core/spatialtranscriptomics/releases/tag/0.1.0) +tag. It was further developed in a collaboration between the [National +Bioinformatics Infrastructure Sweden](https://nbis.se/) and [National Genomics +Infastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/); +it is currently developed and maintained by [Erik Fasterius](https://github.com/fasterius) +and [Christophe Avenel](https://github.com/cavenel). + +1 Supported by grants from the US National Institutes of Health +[U24CA224067](https://reporter.nih.gov/project-details/10261367) and +[U54AG075941](https://reporter.nih.gov/project-details/10376627). Original +authors [Dr. Sergii Domanskyi](https://github.com/sdomanskyi), Prof. Jeffrey +Chuang and Dr. Anuj Srivastava. ## Contributions and Support From 4b5ff0de2c060b1c02a3a3385b0a2f3120f71d26 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 31 Mar 2023 10:46:05 +0200 Subject: [PATCH 010/410] Fix redundant whitespace --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35d1db2..69e7549 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ The nf-core/spatialtranscriptomics pipeline comes with documentation about the p nf-core/spatialtranscriptomics was originally developed by the Jackson Laboratory1, up to the [0.1.0](https://github.com/nf-core/spatialtranscriptomics/releases/tag/0.1.0) -tag. It was further developed in a collaboration between the [National +tag. It was further developed in a collaboration between the [National Bioinformatics Infrastructure Sweden](https://nbis.se/) and [National Genomics Infastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/); it is currently developed and maintained by [Erik Fasterius](https://github.com/fasterius) From df121d21846f64ff8dcd9efe21bb694ee307a0c0 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 31 Mar 2023 11:29:51 +0200 Subject: [PATCH 011/410] Add `--outdir` flag to test profile --- conf/test.config | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/conf/test.config b/conf/test.config index 0d381b8..95c2110 100644 --- a/conf/test.config +++ b/conf/test.config @@ -19,9 +19,7 @@ params { max_memory = '6.GB' max_time = '1.h' - // Input data + // Input and output input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv' - - // TODO nf-core: Give any required params for the test so that command line flags are not needed - // ... + outdir = 'results' } From a3e9cd328d746b2b6a7c6a7fdbbf106403606c8f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 31 Mar 2023 15:01:58 +0200 Subject: [PATCH 012/410] Update Dockerfile for AMD64-emulation Update the Dockerfile to explicitly build with the `--platform=linux/amd64` flag, which will allow it to be executed on both AMD64-architecture and ARM64-architecture (the new Apple chips) through Rosetta-emulation. --- env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/Dockerfile b/env/Dockerfile index b13d94e..366a910 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -1,4 +1,4 @@ -FROM nfcore/base:2.1 +FROM --platform=linux/amd64 nfcore/base:2.1 LABEL \ author = "Christophe Avenel" \ description = "Temporary Dockerfile for nf-core/spatialtranscriptomics" \ From 0304726984975362ac9928c1f69e7dcc22710b6d Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 31 Mar 2023 20:22:53 +0200 Subject: [PATCH 013/410] Remove unused bin scripts --- bin/calculateSumFactors.R | 27 -- bin/characterization_BayesSpace.R | 186 -------------- bin/characterization_SPOTlight.R | 182 -------------- bin/characterization_STdeconvolve.R | 207 ---------------- bin/scPreprocess.py | 163 ------------ bin/script_read_sc_data.py | 49 ---- bin/stClusteringWorkflow.py | 289 ---------------------- bin/{stPreprocess.py => stPreprocess.qmd} | 0 bin/{stSpatialDE.py => stSpatialDE.qmd} | 0 9 files changed, 1103 deletions(-) delete mode 100755 bin/calculateSumFactors.R delete mode 100755 bin/characterization_BayesSpace.R delete mode 100644 bin/characterization_SPOTlight.R delete mode 100644 bin/characterization_STdeconvolve.R delete mode 100755 bin/scPreprocess.py delete mode 100755 bin/script_read_sc_data.py delete mode 100755 bin/stClusteringWorkflow.py rename bin/{stPreprocess.py => stPreprocess.qmd} (100%) mode change 100755 => 100644 rename bin/{stSpatialDE.py => stSpatialDE.qmd} (100%) mode change 100755 => 100644 diff --git a/bin/calculateSumFactors.R b/bin/calculateSumFactors.R deleted file mode 100755 index 47b24b8..0000000 --- a/bin/calculateSumFactors.R +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env Rscript - -# Load packages -library(argparse) -library(reticulate) -library(SpatialExperiment) -library(scran) - -# Parse command-line arguments -parser <- ArgumentParser() - -args <- parser$add_argument_group("Agruments", "required and optional arguments") -args$add_argument("--npCountsOutputName", required = TRUE, metavar = "file", help = "Name of npz counts file") -args$add_argument("--npFactorsOutputName", required = TRUE, metavar = "file", help = "Name of npz factors file") -args <- parser$parse_args() - -# Main script -np <- import("numpy") -matrix_st <- np$load(args$npCountsOutputName)[['arr_0']] -print(dim(matrix_st)) - -spe <- SpatialExperiment(list(counts=matrix_st)) -sfs <- calculateSumFactors(spe, cluster=quickCluster(spe)) -print(length(sfs)) - -# Save to file -np$savez_compressed(args$npFactorsOutputName, sfs) diff --git a/bin/characterization_BayesSpace.R b/bin/characterization_BayesSpace.R deleted file mode 100755 index 1280106..0000000 --- a/bin/characterization_BayesSpace.R +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env Rscript - -# Load packages -library(argparse) -library(SingleCellExperiment) -library(ggplot2) -library(BayesSpace) -library(Matrix) -library(reticulate) - -# Parse command-line arguments -parser <- ArgumentParser() -args <- parser$add_argument_group("Arguments", "required and optional arguments") -args$add_argument("--nameX", default = "st_adata_X.npz", help = "Path to X", metavar = "file", required = FALSE) -args$add_argument("--nameVar", default = "st_adata.var.csv", help = "Path to features metadata", metavar = "file", required = FALSE) -args$add_argument("--nameObs", default = "st_adata.obs.csv", help = "Path to observation metadata", metavar = "file", required = FALSE) -args$add_argument("--countsFactor", default = 100, help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--numberHVG", default = 2000, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--numberPCs", default = 7, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--minClusters", default = 2, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--maxClusters", default = 10, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--optimalQ", default = 5, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--STplatform", default = "Visium", help = "Technology grid", metavar = "factor", required = FALSE) -args$add_argument("--qtuneSaveName", default = "st_bayes_qtune.png", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesClustersName", default = "st_bayes_clusters.png", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesClustersEnhancedName", default = "st_bayes_clusters_enhanced.png", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesOriginalEnhancedFeatures", default = "st_bayes_original_and_enhanced.png", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesEnhancedMarkers", default = "bayes_enhanced_markers.csv", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesSubspotCoord", default = "bayes_subspot_cluster_and_coord.csv", help = "file name", metavar = "file", required = FALSE) -args$add_argument("--bayesSpotCluster", default = "bayes_spot_cluster.csv", help = "file name", metavar = "file", required = FALSE) -args <- parser$parse_args() - -# #### Spatial clustering: -# The latent cluster is modeled to depend on three parameters: cluster mean, -# fixed precision matrix and scaling factor. The parameters are set to follow -# the priors. Clusters are initialized using a non-spatial clustering method -# (mclust). Iteratively and sequentially, each of the three parameters is -# updated via MCMC (Gibbs sampling). The cluster membership is updated via MCMC -# (Metropolis–Hastings algorithm) by taking into account both the likelihood -# and spatial prior information. -# -# #### Enhancement of clustering resolution: -# The procedure reassigns the total expression (in the PC space) within a spot -# to its constituent subspots by leveraging spatial information. The latent -# expression of each subspot is initialized to be expression of the original -# spot, and updated via MCMC (the Metropolis–Hastings algorithm). The cluster -# membership is then determined similarly to the spatial clustering above. -# -# #### Mapping gene expression: -# A regression model is trained for each gene of interest, and used to predict -# gene expression from the high-resolution PCs estimated using -# enhanced-resolution clustering. -# -# #### Tuning number of clusters: -# Elbow of negative pseudo-log-likelihood curve, that estimates how well the -# model fit the data for each number of clusters. - -# Main script -set.seed(123) -np <- import("numpy") - -# Load gene names -rowData <- read.csv(paste0(args$nameVar))$X -row_df <- as.data.frame(rowData) -row.names(row_df) <- rowData - -# Load coordinates of spots -st_obs_all <- read.csv(paste0(args$nameObs)) -col_df <- as.data.frame(st_obs_all$X) -row.names(col_df) <- st_obs_all$X -col_df$imagerow <- st_obs_all$array_row -col_df$imagecol <- st_obs_all$array_col -colnames(col_df) <- c('id', 'imagerow', 'imagecol') -col_df$row <- st_obs_all$array_row -col_df$col <- st_obs_all$array_col - -# Load counts data -count.data <- np$load(paste0(args$nameX))[['arr_0']] * args$countsFactor -colnames(count.data) <- st_obs_all$X -rownames(count.data) <- rowData - -print(args$countsFactor) -print(args$STplatform) -print(args$numberPCs) -print(args$numberHVG) -print(args$minClusters) - -dsp <- SingleCellExperiment(assays = list(counts=count.data), rowData = row_df, colData = col_df) -dsp <- spatialPreprocess(dsp, - platform = args$STplatform, - n.PCs = args$numberPCs, - n.HVGs = args$numberHVG, - log.normalize = TRUE -) - -dsp <- qTune(dsp, - qs = seq(args$minClusters, args$maxClusters), - d = args$numberPCs, - platform = "Visium" -) -qPlot(dsp) -ggsave(paste0(args$qtuneSaveName), - dpi = 600, - scale = 0.55, - width = 8, - height = 8, - units = "in" -) - -# TODO: Need to determine optimal q! -dsp <- spatialCluster(dsp, - q = args$optimalQ, - platform = args$STplatform, - d = args$numberPCs, - init.method = "mclust", - model = "t", - gamma = 2, - nrep = 1000, - burn.in = 100, - save.chain = FALSE -) - -clusterPlot(dsp, palette = c("purple", "cyan", "blue", "yellow", "red"), color = NA) + - theme_bw() + - xlab("Column") + - ylab("Row") + - labs(fill = "BayesSpace\ncluster", title = "Spatial clustering") -ggsave(paste0(args$bayesClustersName), - dpi = 600, - scale = 0.75, - width = 8, - height = 8, - units = "in" -) - -dsp.enhanced <- spatialEnhance(dsp, - q = args$optimalQ, - platform = "Visium", - d = args$numberPCs, - model = "t", - gamma = 2, - jitter_prior = 0.3, - jitter_scale = 3.5, - nrep = 1000, - burn.in = 100 -) - -clusterPlot(dsp.enhanced, palette = c("purple", "cyan", "blue", "yellow", "red"), color = NA) + - theme_bw() + - xlab("Column") + - ylab("Row") + - labs(fill = "BayesSpace\ncluster", title = "Spatial clustering") -ggsave(paste0(args$bayesClustersEnhancedName), - dpi = 600, - scale = 0.75, - width = 8, - height = 8, - units = "in" -) - -# As of current implementation: take first 6 genes and enhance/plot them -ncol <- 6 -markers <- dsp@rowRanges@partitioning@NAMES[0:ncol] - -dsp.enhanced <- enhanceFeatures(dsp.enhanced, dsp, feature_names = markers, nrounds = 0 -) -enhanced.plots <- purrr::map(markers, function(x) featurePlot(dsp.enhanced, x)) -spot.plots <- purrr::map(markers, function(x) featurePlot(dsp, x, color = NA)) -patchwork::wrap_plots(c(spot.plots, enhanced.plots), ncol = ncol) -ggsave(paste0(args$bayesOriginalEnhancedFeatures), - dpi = 600, - scale = 1.25, - width = 2.3 * ncol, - height = 4.5, - limitsize = FALSE, - units = "in" -) - -df_subspot_cluster_and_coord <- subset(as.data.frame(dsp.enhanced@colData), select = -c(imagecol, imagerow)) -write.csv(df_subspot_cluster_and_coord, file = paste0(args$bayesSubspotCoord)) - -df_enhanced_markers <- as.data.frame(dsp.enhanced@assays@data@listData[["logcounts"]][markers,]) -write.csv(df_enhanced_markers, file = paste0(args$bayesEnhancedMarkers)) - -df_spot_cluster <- subset(as.data.frame(dsp@colData), select = -c(imagecol, imagerow)) -write.csv(df_spot_cluster, file = paste0(args$bayesSpotCluster)) diff --git a/bin/characterization_SPOTlight.R b/bin/characterization_SPOTlight.R deleted file mode 100644 index ebe80a8..0000000 --- a/bin/characterization_SPOTlight.R +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env Rscript - -# Load packages -library(argparse) -library(Matrix) -library(data.table) -library(Seurat) -library(dplyr) -library(gt) -library(SPOTlight) -library(igraph) -library(RColorBrewer) -library(DESeq2) -library(ggplot2) -library(reticulate) - -# Parse command-line arguments -parser <- ArgumentParser() - -args <- parser$add_argument_group("Agruments", "required and optional arguments") - -args$add_argument("--filePath", help="Path to npz counts file", metavar="dir", required=TRUE) -args$add_argument("--outsPath", help="Path to data", metavar="dir", required=TRUE) -args$add_argument("--nameX", default="st_adata_X.npz", help="Path to X", metavar="file", required=FALSE) -args$add_argument("--nameVar", default="st_adata.var.csv", help="Path to features metadata", metavar="file", required=FALSE) -args$add_argument("--nameObs", default="st_adata.obs.csv", help="Path to observation metadata", metavar="file", required=FALSE) -args$add_argument("--SCnameX", default="sc_adata_X.npz", help="Path to X", metavar="file", required=FALSE) -args$add_argument("--SCnameVar", default="sc_adata.var.csv", help="Path to features metadata", metavar="file", required=FALSE) -args$add_argument("--SCnameObs", default="sc_adata.obs.csv", help="Path to observation metadata", metavar="file", required=FALSE) -args$add_argument("--fileh5", default="raw_feature_bc_matrix.h5", help="File HDF5", metavar="file", required=FALSE) # "filtered_feature_bc_matrix.h5" -args$add_argument("--outsSubDir", default="raw_feature_bc_matrix/", help="dir", metavar="file", required=FALSE) -args$add_argument("--mtxGeneColumn", default=2, type="integer", help="columns index", metavar="col", required=FALSE) -args$add_argument("--countsFactor", default=100, type="integer", help="factor", metavar="factor", required=FALSE) -args$add_argument("--clusterResolution", default=0.3, type="double", help="factor", metavar="factor", required=FALSE) -args$add_argument("--numberHVG", default=3000, type="integer", help="factor", metavar="factor", required=FALSE) -args$add_argument("--numberCellsPerCelltype", default=100, type="integer", help="factor", metavar="factor", required=FALSE) -args$add_argument("--NMFsaveFile", default="SPOTlight_ls_mk_normed.rds", help="File to save NMF RDS", metavar="file", required=FALSE) -args$add_argument("--SPOTlightScatterpiesName", default="SPOTlight_st_scatterpies.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightScatterpiesSize", default=0.35, type="double", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightSCclustersName", default="SPOTlight_sc_clusters.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightTopicsName", default="SPOTlight_st_topic_profiles.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightFeaturesName", default="SPOTlight_st_prop.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--imagePath", default="spatial/tissue_lowres_image.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightCorrName", default="SPOTlight_st_prop_corr.png", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightPropNorm", default="SPOTlight_prop_norm.csv", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightBetaNorm", default="SPOTlight_beta_norm.csv", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightSCclusterIds", default="SPOTlight_sc_cluster_ids.csv", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightSCpca", default="SPOTlight_sc_pca.csv", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightSCloadings", default="SPOTlight_sc_pca_feature_loadings.csv", help="dir", metavar="file", required=FALSE) -args$add_argument("--SPOTlightSCclusterMarkers", default="SPOTlight_sc_cluster_markers.csv", help="dir", metavar="file", required=FALSE) -args <- parser$parse_args() - - -# Main script -set.seed(123) -np <- import("numpy") -normDataDir <- args$filePath - -filename <- list.files(path=args$outsPath, pattern=args$fileh5)[1] -print(args$outsPath) - -if (!is.na(filename)) { - print(filename) - se_st <- Seurat::Load10X_Spatial(data.dir = args$outsPath, filename = filename) -} else { - image <- Read10X_Image(image.dir=file.path(args$outsPath, 'spatial'), filter.matrix=TRUE) - m <- Read10X(paste0(args$outsPath, args$outsSubDir), gene.column=args$mtxGeneColumn) - m <- m[,row.names(image@coordinates)] - m <- m[,colSums(m)>0] - se_st <- CreateSeuratObject(counts=m, assay="Spatial") - image <- image[Cells(x=se_st)] - DefaultAssay(object=image) <- "Spatial" - se_st[["slice1"]] <- image -} - -matrix_st <- np$load(paste0(normDataDir, args$nameX))[['arr_0']] -st_genes <- read.csv(paste0(normDataDir, args$nameVar))$X -st_obs <- read.csv(paste0(normDataDir, args$nameObs))$X -rownames(matrix_st) <- st_genes -colnames(matrix_st) <- st_obs -se_st@assays$Spatial@counts <- as((args$countsFactor)*matrix_st, "sparseMatrix") -se_st@assays$Spatial@data <- as((args$countsFactor)*matrix_st, "sparseMatrix") - - -matrix_sc <- np$load(paste0(normDataDir, args$SCnameX))[['arr_0']] -sc_genes <- read.csv(paste0(normDataDir, args$SCnameVar)) -sc_obs <- read.csv(paste0(normDataDir, args$SCnameObs)) -rownames(matrix_sc) <- get(colnames(sc_genes)[1], sc_genes) -colnames(matrix_sc) <- get(colnames(sc_obs)[1], sc_obs) -se_sc <- Seurat::CreateSeuratObject(counts = as((args$countsFactor)*matrix_sc, "sparseMatrix")) - -# Cluster sc data -se_sc <- Seurat::FindVariableFeatures(se_sc, verbose = FALSE) -se_sc <- Seurat::ScaleData(se_sc, verbose = FALSE) -se_sc <- Seurat::RunPCA(se_sc, verbose = FALSE) -se_sc <- Seurat::RunUMAP(se_sc, dims = 1:30, verbose = FALSE) -se_sc <- Seurat::FindNeighbors(se_sc) -se_sc <- Seurat::FindClusters(se_sc, resolution=args$clusterResolution) -Seurat::DimPlot(se_sc, group.by = "seurat_clusters", label = TRUE) + Seurat::NoLegend() -ggsave(paste0(args$filePath, args$SPOTlightSCclustersName), dpi=600, scale=0.5, width=8, height=8, units='in') - -cluster_markers_all <- Seurat::FindAllMarkers(object = se_sc, assay = NULL, slot = "data", verbose = TRUE, test.use = "wilcox", only.pos = TRUE) - -spotlight_ls <- SPOTlight::spotlight_deconvolution( - se_sc = se_sc, # Single cell dataset - counts_spatial = se_st@assays$Spatial@counts, # Spatial dataset count - clust_vr = "seurat_clusters", # Variable in sc_sc containing the cell-type annotation - cluster_markers = cluster_markers_all, # Dataframe with the marker genes - cl_n = args$numberCellsPerCelltype, # Number of cells per cell type to use - hvg = args$numberHVG, # Number of HVG to use - ntop = NULL, # Number of top marker genes to use (by default all) - transf = "uv", # Perform unit-variance scaling per cell and spot prior to factorzation and NLS - method = "nsNMF", # Factorization method - min_cont = 0 # Remove those cells contributing to a spot below a certain threshold -) - -saveRDS(object = spotlight_ls, file = paste0(args$filePath, args$NMFsaveFile)) - -# spotlight_ls <- readRDS(file = paste0(args$filePath, args$NMFsaveFile)) - -# NMF topic profiles -nmf_mod <- spotlight_ls[[1]] -h <- NMF::coef(nmf_mod[[1]]) -rownames(h) <- paste("Topic", 1:nrow(h), sep = "_") -topic_profile_plts <- SPOTlight::dot_plot_profiles_fun(h = h, train_cell_clust = nmf_mod[[2]]) -topic_profile_plts[[2]] + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 0), axis.text = ggplot2::element_text(size = 12)) -ggsave(paste0(args$filePath, args$SPOTlightTopicsName), dpi=300, scale=0.5, width=10, height=8, units='in') - -# Add deconvolution results to se_st meta.data -decon_mtrx <- spotlight_ls[[2]] -decon_mtrx_sub <- decon_mtrx[, colnames(decon_mtrx)!="res_ss"] -decon_mtrx_sub[decon_mtrx_sub < 0.08] <- 0 -decon_mtrx <- cbind(decon_mtrx_sub, "res_ss" = decon_mtrx[, "res_ss"]) -rm(decon_mtrx_sub) -rownames(decon_mtrx) <- colnames(se_st) -decon_df <- decon_mtrx %>% data.frame() %>% tibble::rownames_to_column("barcodes") -se_st@meta.data <- se_st@meta.data %>% tibble::rownames_to_column("barcodes") %>% dplyr::left_join(decon_df, by = "barcodes") %>% tibble::column_to_rownames("barcodes") - -# Individual cell types on image -Seurat::SpatialFeaturePlot(object = se_st, features = colnames(decon_df)[-1][-length(colnames(decon_df))+1], alpha = c(0.1, 1), min.cutoff=0, max.cutoff=0.3, crop = FALSE, pt.size.factor=1.0) -ggsave(paste0(args$filePath, args$SPOTlightFeaturesName), dpi=500, scale=1.25, width=8, height=8, units='in') - - -cell_types_all <- colnames(decon_mtrx)[which(colnames(decon_mtrx) != "res_ss")] -cell_types_all <- paste("X", cell_types_all, sep = "") -SPOTlight::spatial_scatterpie(se_obj = se_st, cell_types_all = cell_types_all, - img_path = paste0(args$outsPath, args$imagePath), - cell_types_interest = NULL, - slice = NULL, - scatterpie_alpha = 1, - pie_scale = args$SPOTlightScatterpiesSize -) -ggsave(paste0(args$filePath, args$SPOTlightScatterpiesName), dpi=600, scale=1.0, width=8, height=8, units='in') - -# Remove cell types not predicted to be on the tissue -decon_mtrx_sub <- decon_mtrx[, colnames(decon_mtrx)[which(colnames(decon_mtrx) != "res_ss")]] -decon_mtrx_sub <- decon_mtrx_sub[, colSums(decon_mtrx_sub) > 0] -colnames(decon_mtrx_sub) <- paste("X", colnames(decon_mtrx_sub), sep = "") -decon_cor <- cor(decon_mtrx_sub) -p.mat <- corrplot::cor.mtest(mat = decon_mtrx_sub, conf.level = 0.95) - -# Visualize -ggcorrplot::ggcorrplot(corr = decon_cor, p.mat = p.mat[[1]], hc.order = TRUE, type = "full", insig = "blank", - lab = TRUE, outline.col = "lightgrey", method = "square", colors = c("#6D9EC1", "white", "#E46726"), - title = "Cell type proportions correlation", - legend.title = "Correlation\n(Pearson)") + - ggplot2::theme(plot.title = ggplot2::element_text(size = 22, hjust = 0.5, face = "bold"), - legend.text = ggplot2::element_text(size = 12), legend.title = ggplot2::element_text(size = 15), - axis.text.x = ggplot2::element_text(angle = 90), axis.text = ggplot2::element_text(size = 18, vjust = 0.5) - ) -ggsave(paste0(args$filePath, args$SPOTlightCorrName), dpi=600, scale=0.75, width=8, height=8, units='in') - -write.csv(decon_df, file=paste0(args$filePath, args$SPOTlightPropNorm)) -# There was some error need to test this output -write.csv(cbind(cluster_id=nmf_mod[[2]], h), file=paste0(args$filePath, args$SPOTlightBetaNorm)) - -write.csv(se_sc@active.ident, file=paste0(args$filePath, args$SPOTlightSCclusterIds)) -write.csv(se_sc@reductions[["pca"]]@cell.embeddings, file=paste0(args$filePath, args$SPOTlightSCpca)) -write.csv(se_sc@reductions[["pca"]]@feature.loadings, file=paste0(args$filePath, args$SPOTlightSCloadings)) -write.csv(cluster_markers_all, file=paste0(args$filePath, args$SPOTlightSCclusterMarkers)) - -quit(status=0) diff --git a/bin/characterization_STdeconvolve.R b/bin/characterization_STdeconvolve.R deleted file mode 100644 index 9664a47..0000000 --- a/bin/characterization_STdeconvolve.R +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/local/bin/Rscript - -# Load packages -library(argparse) -library(dplyr) -library(ggplot2) -library(Seurat) -library(STdeconvolve) -library(corrplot) -library(reticulate) - -# Parse command-line arguments -parser <- ArgumentParser() -args <- parser$add_argument_group("Agruments", "required and optional arguments") -args$add_argument("--outsPath", help = "Path to data", metavar = "dir", required = TRUE) -args$add_argument("--nameX", default = "st_adata_X.npz", help = "Path to X", metavar = "file", required = FALSE) -args$add_argument("--nameVar", default = "st_adata.var.csv", help = "Path to features metadata", metavar = "file", required = FALSE) -args$add_argument("--nameObs", default = "st_adata.obs.csv", help = "Path to observation metadata", metavar = "file", required = FALSE) -args$add_argument("--SCnameX", default = "sc_adata_X.npz", help = "Path to X", metavar = "file", required = FALSE) -args$add_argument("--SCnameVar", default = "sc_adata.var.csv", help = "Path to features metadata", metavar = "file", required = FALSE) -args$add_argument("--SCnameObs", default = "sc_adata.obs.csv", help = "Path to observation metadata", metavar = "file", required = FALSE) -args$add_argument("--fileh5", default = "raw_feature_bc_matrix.h5", help = "File HDF5", metavar = "file", required = FALSE) -args$add_argument("--outsSubDir", default = "raw_feature_bc_matrix/", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--mtxGeneColumn", default = 2, type = "integer", help = "columns index", metavar = "col", required = FALSE) -args$add_argument("--countsFactor", default = 100, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--corpusRemoveAbove", default = 1.0, type = "double", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--corpusRemoveBelow", default = 0.05, type = "double", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--LDAminTopics", default = 8, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--LDAmaxTopics", default = 9, type = "integer", help = "factor", metavar = "factor", required = FALSE) -args$add_argument("--LDAsaveFile", default = "STdeconvolve_optLDA.rds", help = "File to save LDA RDS", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveScatterpiesName", default = "STdeconvolve_st_scatterpies.png", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveScatterpiesSize", default = 2.85, type = "double", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveFeaturesSizeFactor", default = 1.0, type = "double", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveFeaturesName", default = "STdeconvolve_st_prop.png", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveCorrName", default = "STdeconvolve_st_prop_corr.png", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolvePropNormName", default = "STdeconvolve_prop_norm.csv", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveBetaNormName", default = "STdeconvolve_beta_norm.csv", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveSCclustersName", default = "STdeconvolve_sc_clusters.png", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveSCclusterIds", default = "STdeconvolve_sc_cluster_ids.csv", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveSCpca", default = "STdeconvolve_sc_pca.csv", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveSCloadings", default = "STdeconvolve_sc_pca_feature_loadings.csv", help = "dir", metavar = "file", required = FALSE) -args$add_argument("--STdeconvolveSCclusterMarkers", default = "STdeconvolve_sc_cluster_markers.csv", help = "dir", metavar = "file", required = FALSE) -args <- parser$parse_args() - -# Main script -set.seed(123) -np <- import("numpy") - -filename <- list.files(path=args$outsPath, pattern = args$fileh5)[1] -print(args$outsPath) - -if (!is.na(filename)) { - print(filename) - se_st <- Seurat::Load10X_Spatial(data.dir = args$outsPath, filename = filename) -} else { - image <- Read10X_Image(image.dir = file.path(args$outsPath, 'spatial'), filter.matrix = TRUE) - m <- Read10X(paste0(args$outsPath, args$outsSubDir), gene.column = args$mtxGeneColumn) - m <- m[, row.names(image@coordinates)] - m <- m[, colSums(m) > 0] - se_st <- CreateSeuratObject(counts = m, assay = "Spatial") - image <- image[Cells(x = se_st)] - DefaultAssay(object = image) <- "Spatial" - se_st[["slice1"]] <- image -} - -matrix_st <- np$load(paste0(args$nameX))[['arr_0']] -st_genes <- read.csv(paste0(args$nameVar))$X -st_obs <- read.csv(paste0(args$nameObs))$X -rownames(matrix_st) <- st_genes -colnames(matrix_st) <- st_obs -se_st@assays$Spatial@counts <- as(matrix_st, "sparseMatrix") -se_st@assays$Spatial@data <- as(matrix_st, "sparseMatrix") -se_st@assays$Spatial@counts <- round((args$countsFactor) * se_st@assays$Spatial@counts) - -corpus <- restrictCorpus(se_st@assays$Spatial@counts, - removeAbove = args$corpusRemoveAbove, - removeBelow = args$corpusRemoveBelow -) -corpus <- corpus + 1 -print(dim(as.matrix(corpus))) -print(sum(colSums(as.matrix(corpus)) == 0)) - -ldas <- fitLDA(t(as.matrix(corpus)), Ks = seq(args$LDAminTopics, args$LDAmaxTopics, by = 1)) -optLDA <- optimalModel(models = ldas, opt = "min") -saveRDS(object = optLDA, file = args$LDAsaveFile) - -optLDA <- readRDS(file = args$LDAsaveFile) - -results <- getBetaTheta(optLDA, t(as.matrix(corpus))) -deconProp <- results$theta -posk <- se_st@images[["slice1"]]@coordinates[c("imagerow", "imagecol")] -names(posk) <- c('y', 'x') -posk$x <- posk$x * se_st@images[["slice1"]]@scale.factors[["lowres"]] -posk$y <- dim(se_st@images[["slice1"]])[1] - - posk$y * se_st@images[["slice1"]]@scale.factors[["lowres"]] -vizAllTopics(deconProp, - posk, - lwd = 0, - overlay = se_st@images[["slice1"]]@image, - r = args$STdeconvolveScatterpiesSize -) -ggsave(args$STdeconvolveScatterpiesName, - dpi = 600, - scale = 1.0, - width = 8, - height = 8, - units = 'in' -) - -# Individual topics proportions spatial overlay -decon_df <- deconProp %>% - data.frame() %>% - tibble::rownames_to_column("barcodes") - -print(colnames(decon_df)) -print(dim(decon_df)) - -se_st@meta.data <- se_st@meta.data %>% - tibble::rownames_to_column("barcodes") %>% - dplyr::left_join(decon_df, by = "barcodes") %>% - tibble::column_to_rownames("barcodes") -Seurat::SpatialFeaturePlot(object = se_st, - features = colnames(decon_df)[-1], - alpha = c(0.1, 1), - min.cutoff = 0, - max.cutoff = 0.3, - crop = FALSE, - pt.size.factor = args$STdeconvolveFeaturesSizeFactor -) -ggsave(args$STdeconvolveFeaturesName, - dpi = 300, - scale = 2.0, - width = 8, - height = 8, - units = 'in' -) - -# Cell type proportions correlation -decon_mtrx_sub <- deconProp[, colnames(deconProp)[which(colnames(deconProp) != "res_ss")]] -decon_mtrx_sub <- decon_mtrx_sub[, colSums(decon_mtrx_sub) > 0] -colnames(decon_mtrx_sub) <- paste("X", colnames(decon_mtrx_sub), sep = "") -decon_cor <- cor(decon_mtrx_sub) -p.mat <- corrplot::cor.mtest(mat = decon_mtrx_sub, conf.level = 0.95) -ggcorrplot::ggcorrplot( - corr = decon_cor, - p.mat = p.mat[[1]], - hc.order = TRUE, - type = "full", - insig = "blank", - lab = TRUE, - outline.col = "lightgrey", - method = "square", - colors = c("#6D9EC1", "white", "#E46726"), - title = "Cell type proportions correlation", - legend.title = "Correlation\n(Pearson)") + - ggplot2::theme(plot.title = ggplot2::element_text(size = 22, hjust = 0.5, face = "bold")) + - ggplot2::theme(legend.text = ggplot2::element_text(size = 12)) + - ggplot2::theme(legend.title = ggplot2::element_text(size = 15)) + - ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 90)) + - ggplot2::theme(axis.text = ggplot2::element_text(size = 18, vjust = 0.5)) -ggsave(args$STdeconvolveCorrName, - dpi = 600, - scale = 0.75, - width = 8, - height = 8, - units = 'in' -) - -write.csv(results$theta, file = args$STdeconvolvePropNormName) -write.csv(t(results$beta), file = args$STdeconvolveBetaNormName) - -##### For PCA and clustering only -matrix_sc <- np$load(paste0(args$SCnameX))[['arr_0']] -sc_genes <- read.csv(paste0(args$SCnameVar)) -sc_obs <- read.csv(paste0(args$SCnameObs)) -rownames(matrix_sc) <- get(colnames(sc_genes)[1], sc_genes) -colnames(matrix_sc) <- get(colnames(sc_obs)[1], sc_obs) -se_sc <- Seurat::CreateSeuratObject(counts = as((args$countsFactor) * matrix_sc, "sparseMatrix")) - -se_sc <- Seurat::FindVariableFeatures(se_sc, verbose = FALSE) -se_sc <- Seurat::ScaleData(se_sc, verbose = FALSE) -se_sc <- Seurat::RunPCA(se_sc, verbose = FALSE) -se_sc <- Seurat::RunUMAP(se_sc, dims = 1:30, verbose = FALSE) -se_sc <- Seurat::FindNeighbors(se_sc) -se_sc <- Seurat::FindClusters(se_sc, resolution = 0.3) -Seurat::DimPlot(se_sc, group.by = "seurat_clusters", label = TRUE) + - Seurat::NoLegend() -ggsave(args$STdeconvolveSCclustersName, - dpi = 600, - scale = 0.5, - width = 8, - height = 8, - units = 'in' -) -cluster_markers_all <- Seurat::FindAllMarkers( - object = se_sc, - assay = NULL, - verbose = TRUE, - only.pos = TRUE, - slot = "data", - test.use = "wilcox" -) - -write.csv(se_sc@active.ident, file = args$STdeconvolveSCclusterIds) -write.csv(se_sc@reductions[["pca"]]@cell.embeddings, file = args$STdeconvolveSCpca) -write.csv(se_sc@reductions[["pca"]]@feature.loadings, file = args$STdeconvolveSCloadings) -write.csv(cluster_markers_all, file = args$STdeconvolveSCclusterMarkers) diff --git a/bin/scPreprocess.py b/bin/scPreprocess.py deleted file mode 100755 index e8c2527..0000000 --- a/bin/scPreprocess.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python - -# Load packages -import argparse -import scanpy as sc -import numpy as np -import pandas as pd -import scipy.stats -from matplotlib import pyplot as plt -from scipy.sparse import csr_matrix - - -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Preprocess single cell transcriptomics data.") -parser.add_argument( - "--npFactorsOutputName", metavar="filename", type=str, default=None, help="Name of files with counts." -) -parser.add_argument("--rawAdata", metavar="h5file", type=str, default=None, help="Name of the h5ad file.") -parser.add_argument("--mitoFile", metavar="file", type=str, default=None, help="Path and name of the mito file.") -parser.add_argument("--pltFigSize", metavar="figsize", type=int, default=6, help="Figure size.") -parser.add_argument("--minCounts", metavar="cutoff", type=int, default=500, help="Min counts per spot.") -parser.add_argument("--minGenes", metavar="cutoff", type=int, default=250, help="Min genes per spot.") -parser.add_argument("--minCells", metavar="cutoff", type=int, default=1, help="Min cells per gene.") -parser.add_argument("--histplotQCmaxTotalCounts", metavar="cutoff", type=int, default=5000, help="Max total counts.") -parser.add_argument("--histplotQCminGeneCounts", metavar="cutoff", type=int, default=2000, help="Min gene counts.") -parser.add_argument("--histplotQCbins", metavar="number", type=int, default=40, help="Number of bins.") -parser.add_argument( - "--histogramPlotAllName", metavar="name", type=str, default="sc_histogrtam_all.png", help="Figure name." -) -parser.add_argument( - "--histogramPlotFilteredName", metavar="name", type=str, default="sc_histogrtam_filtered.png", help="Figure name." -) -parser.add_argument( - "--histWithWithoutNorm", - metavar="name", - type=str, - default="sc_histogram_with_without_normalization.png", - help="Figure name.", -) -parser.add_argument("--nameX", metavar="File name", type=str, default="sc_adata_X.npz", help="Name of the counts file.") -parser.add_argument( - "--nameVar", metavar="File name", type=str, default="sc_adata.var.csv", help="Name of the features file." -) -parser.add_argument( - "--nameObs", metavar="File name", type=str, default="sc_adata.obs.csv", help="Name of the observations file." -) -parser.add_argument( - "--nameDataPlain", metavar="File name", type=str, default="sc_adata_plain.h5ad", help="Name of the data save file." -) -parser.add_argument( - "--nameDataNorm", metavar="File name", type=str, default="sc_adata_norm.h5ad", help="Name of the data save file." -) -args = parser.parse_args() - - -# Main script -# See more settings at: -# https://scanpy.readthedocs.io/en/stable/generated/scanpy._settings.ScanpyConfig.html - -f_temp = np.load(args.npFactorsOutputName) -f_temp = f_temp[list(f_temp.keys())[0]] -print(f_temp.shape) - -sc_adata = sc.read(args.rawAdata) -print(sc_adata.shape) - -sc_adata.obs["norm_factors"] = pd.Series(index=sc_adata.obs.index, data=f_temp) - -mito = pd.read_csv(args.mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] -mito = mito.xs(1, level="MCARTA2_LIST").sort_index().reset_index() -print(mito) - -sc_adata.var["mt"] = sc_adata.var_names.isin(mito["Symbol"]) -sc.pp.calculate_qc_metrics(sc_adata, qc_vars=["mt"], inplace=True) -print(sc_adata) - -print(sc_adata.var) - -plt.rcParams["figure.figsize"] = (args.pltFigSize, args.pltFigSize) - - -def histplotQC(se_data, bins, ax): - print(se_data.shape) - ax.hist(se_data, density=True, bins=bins, color="navy", alpha=0.3) - kde = scipy.stats.gaussian_kde(se_data) - xx = np.linspace(min(se_data), max(se_data), 300) - ax.set_xlabel(se_data.name) - ax.set_ylabel("Density") - ax.plot(xx, kde(xx), color="crimson") - ax.set_xlim([0, ax.get_xlim()[1]]) - return - - -fig, axs = plt.subplots(1, 5, figsize=(args.pltFigSize * 5, args.pltFigSize)) -histplotQC(sc_adata.obs["total_counts"], bins=args.histplotQCbins, ax=axs[0]) -histplotQC( - sc_adata.obs["total_counts"][sc_adata.obs["total_counts"] < args.histplotQCmaxTotalCounts], - bins=args.histplotQCbins, - ax=axs[1], -) -histplotQC(sc_adata.obs["n_genes_by_counts"], bins=args.histplotQCbins, ax=axs[2]) -histplotQC( - sc_adata.obs["n_genes_by_counts"][sc_adata.obs["n_genes_by_counts"] < args.histplotQCminGeneCounts], - bins=args.histplotQCbins, - ax=axs[3], -) -histplotQC(sc_adata.obs["pct_counts_mt"], bins=args.histplotQCbins, ax=axs[4]) -fig.tight_layout() -fig.savefig(args.histogramPlotAllName, facecolor="white") - -# Filter cells and genes -sc.pp.filter_cells(sc_adata, min_counts=args.minCounts) -sc.pp.filter_cells(sc_adata, min_genes=args.minGenes) -sc.pp.filter_genes(sc_adata, min_cells=args.minCells) -print(sc_adata.shape) - -fig, axs = plt.subplots(1, 5, figsize=(args.pltFigSize * 5, args.pltFigSize)) -histplotQC(sc_adata.obs["total_counts"], bins=args.histplotQCbins, ax=axs[0]) -histplotQC( - sc_adata.obs["total_counts"][sc_adata.obs["total_counts"] < args.histplotQCmaxTotalCounts], - bins=args.histplotQCbins, - ax=axs[1], -) -histplotQC(sc_adata.obs["n_genes_by_counts"], bins=args.histplotQCbins, ax=axs[2]) -histplotQC( - sc_adata.obs["n_genes_by_counts"][sc_adata.obs["n_genes_by_counts"] < args.histplotQCminGeneCounts], - bins=args.histplotQCbins, - ax=axs[3], -) -histplotQC(sc_adata.obs["pct_counts_mt"], bins=args.histplotQCbins, ax=axs[4]) -fig.tight_layout() -fig.savefig(args.histogramPlotFilteredName, facecolor="white") - -# Effect of normalization by size factors -fig, ax = plt.subplots(figsize=(args.pltFigSize, args.pltFigSize)) -display_cutoff = 5 * 10**3 -se = pd.Series(np.array(sc_adata.X.sum(axis=1)).T[0]) -se = se[se < display_cutoff] -print("Number of cells displayed:", se.shape) -se.hist(bins=100, alpha=0.75, ax=ax) -ax.set_xlim(0, display_cutoff) -sc_adata_c = sc_adata.copy() -sc_adata_c.X = csr_matrix(sc_adata.X / sc_adata.obs["norm_factors"].values[:, None]) -se = pd.Series(np.array(sc_adata_c.X.sum(axis=1)).T[0]) -se = se[se < display_cutoff] -print("Number of cells displayed:", se.shape) -se.hist(bins=100, alpha=0.75, ax=ax) -ax.set_xlim(0, display_cutoff) -fig.savefig(args.histWithWithoutNorm, facecolor="white", dpi=300) -plt.close(fig) - -# Save raw filtered data -sc_adata.write(args.nameDataPlain) - -# Save normalized data -sc_adata.X = csr_matrix(sc_adata.X / sc_adata.obs["norm_factors"].values[:, None]) -sc.pp.log1p(sc_adata) -sc_adata.write(args.nameDataNorm) - -# Save to open in R -np.savez_compressed(args.nameX, sc_adata.X.T.todense()) -sc_adata.var.to_csv(args.nameVar) -sc_adata.obs.to_csv(args.nameObs) diff --git a/bin/script_read_sc_data.py b/bin/script_read_sc_data.py deleted file mode 100755 index f5d78d8..0000000 --- a/bin/script_read_sc_data.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python - -# Load packages -import os -import argparse -import scanpy as sc -import numpy as np -import pandas as pd - -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Load scRNA-seq data from MTX.") -parser.add_argument( - "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Path to Space Range output directory, etc." -) -parser.add_argument( - "--outAnnData", metavar="outAnnData", type=str, default=None, help="Path to a file to save h5ad data into." -) -parser.add_argument("--outSCCounts", metavar="outSCCounts", type=str, default=None, help="Name of the NPZ file.") -parser.add_argument("--minCounts", metavar="minCounts", type=int, default=1, help="Min counts per spot.") -parser.add_argument("--minGenes", metavar="minGenes", type=int, default=1, help="Min genes per spot.") -parser.add_argument("--minCells", metavar="minCells", type=int, default=1, help="Min cells per gene.") -args = parser.parse_args() - -matrixFilename = os.path.join(args.SRCountDir, "raw_feature_bc_matrix", "matrix.mtx.gz") -featuresFiles = os.path.join(args.SRCountDir, "raw_feature_bc_matrix", "features.tsv.gz") -barcodesFilename = os.path.join(args.SRCountDir, "raw_feature_bc_matrix", "barcodes.tsv.gz") - -sc_adata = sc.read_mtx(matrixFilename).T -genes = pd.read_csv(featuresFiles, header=None, sep="\t") -print(genes) -if len(genes.columns) == 1: - gs = genes[0] -else: - gs = genes[1] -sc_adata.var_names = gs -sc_adata.var["gene_symbols"] = gs.values -sc_adata.obs_names = pd.read_csv(barcodesFilename, header=None)[0] -print(sc_adata.var) - -sc_adata.var_names_make_unique() -sc.pp.filter_cells(sc_adata, min_counts=args.minCounts) -sc.pp.filter_genes(sc_adata, min_cells=args.minCells) - -# Save raw anndata to file -sc_adata.write(args.outAnnData) - -# Save counts anndata to file -X = np.array(sc_adata.X.todense()).T -np.savez_compressed(args.outSCCounts, X) diff --git a/bin/stClusteringWorkflow.py b/bin/stClusteringWorkflow.py deleted file mode 100755 index 0c5b0f2..0000000 --- a/bin/stClusteringWorkflow.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python - -# Load packages -# import sys -# import os -import argparse -import scanpy as sc -import numpy as np -import pandas as pd -import scanorama - -# from sklearn.manifold import TSNE -from umap import UMAP -from matplotlib import pyplot as plt - -# from matplotlib import cm -# from sklearn.metrics.pairwise import cosine_similarity -from scipy.spatial.distance import cdist - - -def label_transfer(dist, labels): - class_prob = pd.get_dummies(labels).to_numpy().T @ dist - class_prob /= np.linalg.norm(class_prob, 2, axis=0) - return (class_prob.T - class_prob.min(1)) / np.ptp(class_prob, axis=1) - - -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Preprocess single cell transcriptomics data.") -parser.add_argument("--fileNameST", metavar="name", type=str, default="st_adata_norm.h5ad", help="") -parser.add_argument("--fileNameSC", metavar="name", type=str, default="sc_adata_norm.h5ad", help="") -parser.add_argument( - "--STdeconvolveSCclusterIds", metavar="name", type=str, default="STdeconvolve_sc_cluster_ids.csv", help="" -) -parser.add_argument( - "--STdeconvolvePropNormName", metavar="name", type=str, default="STdeconvolve_prop_norm.csv", help="" -) -parser.add_argument( - "--STdeconvolveBetaNormName", metavar="name", type=str, default="STdeconvolve_beta_norm.csv", help="" -) -parser.add_argument( - "--STdeconvolveSCloadings", metavar="name", type=str, default="STdeconvolve_sc_pca_feature_loadings.csv", help="" -) -parser.add_argument( - "--STdeconvolveSCclusterMarkers", metavar="name", type=str, default="STdeconvolve_sc_cluster_markers.csv", help="" -) -parser.add_argument("--SPOTlightPropNorm", metavar="name", type=str, default="SPOTlight_prop_norm.csv", help="") -parser.add_argument("--resolution", metavar="name", type=float, default=0.4, help="") -parser.add_argument("--scanpy_UMAP_st_sc", metavar="name", type=str, default="scanpy_UMAP_st_sc.png", help="") -parser.add_argument( - "--scanpy_UMAP_st_sc_BBKNN", metavar="name", type=str, default="scanpy_UMAP_st_sc_BBKNN.png", help="" -) -parser.add_argument("--scanorama_UMAP_st_sc", metavar="name", type=str, default="scanorama_UMAP_st_sc.png", help="") -parser.add_argument( - "--LT_individual_histograms", metavar="name", type=str, default="LT_individual_histograms.png", help="" -) -parser.add_argument( - "--LT_individual_histograms_combined", - metavar="name", - type=str, - default="LT_individual_histograms_combined.png", - help="", -) -parser.add_argument("--Topics_LDA_spatial", metavar="name", type=str, default="Topics_LDA_spatial.png", help="") -parser.add_argument("--Topics_NMF_spatial", metavar="name", type=str, default="Topics_NMF_spatial.png", help="") -parser.add_argument("--Topics_LT_PC_spatial", metavar="name", type=str, default="Topics_LT_PC_spatial.png", help="") -parser.add_argument("--UMAP_st_spots_clusters", metavar="name", type=str, default="UMAP_st_spots_clusters.png", help="") -parser.add_argument( - "--UMAP_clusters_embedding_density", - metavar="name", - type=str, - default="UMAP_clusters_embedding_density.png", - help="", -) -parser.add_argument("--UMAP_LDA_topics", metavar="name", type=str, default="UMAP_LDA_topics.png", help="") -parser.add_argument("--UMAP_NMF_topics", metavar="name", type=str, default="UMAP_NMF_topics.png", help="") -parser.add_argument("--UMAP_LT_PC_topics", metavar="name", type=str, default="UMAP_LT_PC_topics.png", help="") -parser.add_argument( - "--Clusters_scanpy_spatial", metavar="name", type=str, default="Clusters_scanpy_spatial.png", help="" -) -parser.add_argument("--violin_topics_LDA", metavar="name", type=str, default="violin_topics_LDA.png", help="") -parser.add_argument("--violin_topics_NMF", metavar="name", type=str, default="violin_topics_NMF.png", help="") -parser.add_argument("--violin_topics_LT_PC", metavar="name", type=str, default="violin_topics_LT_PC.png", help="") -parser.add_argument( - "--saveFileST", - metavar="savefile", - type=str, - default="st_adata_processed.h5ad", - help="Path to a file to save h5ad data into.", -) -parser.add_argument( - "--saveFileSC", - metavar="savefile", - type=str, - default="sc_adata_processed.h5ad", - help="Path to a file to save h5ad data into.", -) -args = parser.parse_args() - - -# Main script -# See more settings at: -# https://scanpy.readthedocs.io/en/stable/generated/scanpy._settings.ScanpyConfig.html -sc.set_figure_params(dpi_save=300, facecolor="white") - -# Label transfer -# 1. To topics from single cells -# 2. To spots from single cells -# 3. Compare spots transfer to deconvolution results - -# Task: Determine cluster composition of LDA topics -# Challenge: topics are intertwined -> hard to match the clusters - -# Task: direct label transfer from SC clusters to ST spots -# Prerequisite: Do integration via Scanorama or BBKNN -# Post-task: compare deconvolution results to STdeconvolve proportions -# (correlate proportions) - -# Read pre-processed data -st_adata = sc.read(args.fileNameST) -sc_adata = sc.read(args.fileNameSC) -df_st = pd.DataFrame(index=st_adata.var.index, data=st_adata.X.T.todense(), columns=st_adata.obs.index) -df_sc = pd.DataFrame(index=sc_adata.var.index, data=sc_adata.X.T.todense(), columns=sc_adata.obs.index) -ind = df_sc.index.intersection(df_st.index) -df_st = df_st.loc[ind] -df_sc = df_sc.loc[ind] -print(df_st.shape, df_sc.shape) - - -# Read STdeconvolve results -se_clusters_SC = pd.read_csv(args.STdeconvolveSCclusterIds, index_col=0).rename({"x": "cluster_id"}, axis=1)[ - "cluster_id" -] -df_theta = pd.read_csv(args.STdeconvolvePropNormName, index_col=0) -df_beta = pd.read_csv(args.STdeconvolveBetaNormName, index_col=0) -hvg_SC = pd.read_csv(args.STdeconvolveSCloadings, index_col=0).index -df_markers_SC = pd.read_csv(args.STdeconvolveSCclusterMarkers, index_col=0) - - -# Simple comparison of clusters to topics -df_beta_st = df_beta.loc[df_beta.index.intersection(df_st.index)] -df_beta_st.columns = "X " + df_beta_st.columns.astype(str) -df_sc_cl = df_sc.copy().loc[df_beta.index.intersection(df_st.index)] -df_sc_cl.columns = pd.MultiIndex.from_arrays(se_clusters_SC.reset_index().values.T, names=["cell", "cluster"]) -df_sc_cl = df_sc_cl.groupby(level="cluster", axis=1).mean() -df_sc_cl.columns = "Cluster " + df_sc_cl.columns.astype(str) -# Similar approach to label transfer -df = df_sc_cl.T @ df_beta_st / df_sc_cl.shape[0] -df /= np.linalg.norm(df, 2, axis=0) -df = (df.T - df.min(1)) / df.values.ptp(1) -df.T.round(3) # .style.background_gradient(axis=None) - -# Integrating with BBKNN -# This is only to see the UMAP of SC and ST together -# Not using it with label transfer -if True: - st_adata = sc.read(args.fileNameST) - sc_adata = sc.read(args.fileNameSC) - adata_all = st_adata.concatenate(sc_adata, batch_categories=["st", "sc"]) - sc.pp.highly_variable_genes(adata_all, flavor="seurat", n_top_genes=2000) - sc.pp.pca(adata_all, use_highly_variable=True) - sc.pp.neighbors(adata_all) - sc.tl.umap(adata_all) - sc.pl.umap(adata_all, color=["batch"], palette=sc.pl.palettes.vega_20_scanpy, save=args.scanpy_UMAP_st_sc) - df_before = ( - pd.DataFrame(adata_all.obsp["connectivities"].todense(), index=adata_all.obs.batch, columns=adata_all.obs.batch) - .xs("st", axis=0) - .xs("sc", axis=1) - ) - sc.external.pp.bbknn(adata_all, batch_key="batch") - sc.tl.umap(adata_all) - sc.pl.umap(adata_all, color=["batch"], palette=sc.pl.palettes.vega_20_scanpy, save=args.scanpy_UMAP_st_sc_BBKNN) - df_after = ( - pd.DataFrame(adata_all.obsp["connectivities"].todense(), index=adata_all.obs.batch, columns=adata_all.obs.batch) - .xs("st", axis=0) - .xs("sc", axis=1) - ) - -# Correlation similarity of gene expression in PC space -# Integrating with Scanorama -integrated = scanorama.integrate([df_st.values.T, df_sc.values.T], [df_st.index.values, df_sc.index.values])[0] -umap_st = UMAP(random_state=2).fit_transform(integrated[0]).T -umap_sc = UMAP(random_state=2).fit_transform(integrated[1]).T -fig, ax = plt.subplots(1, 1, figsize=(6, 6)) -ax.scatter(umap_st[0], umap_st[1], alpha=0.95, s=2, label="st") -ax.scatter(umap_sc[0], umap_sc[1], alpha=0.95, s=2, label="sc") -plt.legend() -fig.savefig(args.scanorama_UMAP_st_sc, facecolor="white", dpi=300) -plt.close(fig) - -df_similarity = pd.DataFrame(data=1 - cdist(integrated[0], integrated[1], metric="correlation").T).fillna(0) -v_in_pc = label_transfer(df_similarity.values, se_clusters_SC.values) - -# Plot individual histograms -if True: - fig, ax = plt.subplots(1, 1, figsize=(10, 10)) - pd.DataFrame(v_in_pc).hist(bins=50, ax=ax) - fig.savefig(args.LT_individual_histograms, facecolor="white", dpi=300) - plt.close(fig) - -# Plot combined histogram -if True: - fig, ax = plt.subplots(1, 1, figsize=(5, 5)) - for i in range(v_in_pc.shape[1]): - pd.DataFrame(v_in_pc)[i].hist(bins=100, alpha=0.5, ax=ax) - fig.savefig(args.LT_individual_histograms_combined, facecolor="white", dpi=300) - plt.close(fig) - -# Add LT results to adata -dfb = pd.DataFrame(v_in_pc) -dfb.columns = "LT PC " + dfb.columns.astype(str) -dfb.index = st_adata.obs.index.values -for col in dfb.columns: - st_adata.obs[col] = dfb[col] - -# dfc = pd.DataFrame(v_in_bbknn) -# dfc.columns = 'LT BBKNN ' + dfc.columns.astype(str) -# dfc.index = st_adata.obs.index.values -# for col in dfc.columns: -# st_adata.obs[col] = dfc[col] - -# Add LDA proportions to adata -df_theta = pd.read_csv(args.STdeconvolvePropNormName, index_col=0) -df_theta.columns = "Topic LDA " + df_theta.columns.astype(str) -for col in df_theta.columns: - st_adata.obs[col] = df_theta[col] - -# Add NMF proportions to adata -df_theta = pd.read_csv(args.SPOTlightPropNorm, index_col=0).set_index("barcodes").drop("res_ss", axis=1) -df_theta.columns = "Topic NMF " + df_theta.columns.astype(str) -for col in df_theta.columns: - st_adata.obs[col] = df_theta[col] - -# Make plots with proportions -# These plots have a uniform color-mapping of scanpy -if True: - plt.rcParams["figure.figsize"] = (5, 5) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic LDA ")] - sc.pl.spatial(st_adata, img_key="hires", color=keys, ncols=10, save=args.Topics_LDA_spatial) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic NMF ")] - sc.pl.spatial(st_adata, img_key="hires", color=keys, ncols=10, save=args.Topics_NMF_spatial) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("LT PC ")] - sc.pl.spatial(st_adata, img_key="hires", color=keys, ncols=10, save=args.Topics_LT_PC_spatial) - # keys = st_adata.var.index.intersection(all_markers).values - # sc.pl.spatial(st_adata, img_key="hires", color=keys, ncols=9, save=args.All_Markers_spatial) - - -# Clsutering and Selection -sc.pp.pca(st_adata) -sc.pp.neighbors(st_adata) -sc.tl.umap(st_adata) -sc.tl.leiden(st_adata, key_added="clusters", resolution=0.4) - -# Make plots of UMAP of ST spots clusters -plt.rcParams["figure.figsize"] = (4, 4) -sc.pl.umap( - st_adata, color=["clusters", "total_counts", "n_genes_by_counts"], wspace=0.4, save=args.UMAP_st_spots_clusters -) -sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") -sc.pl.embedding_density(st_adata, groupby="clusters", ncols=10, save=args.UMAP_clusters_embedding_density) - -# Plot LDA cell topics proportions in UMAP -if True: - plt.rcParams["figure.figsize"] = (4, 4) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic LDA ")] - sc.pl.umap(st_adata, color=keys, wspace=0.4, ncols=5, save=args.UMAP_LDA_topics) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic NMF ")] - sc.pl.umap(st_adata, color=keys, wspace=0.4, ncols=5, save=args.UMAP_NMF_topics) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("LT PC ")] - sc.pl.umap(st_adata, color=keys, wspace=0.4, ncols=5, save=args.UMAP_LT_PC_topics) - -# Make plots of spatial ST spots clusters -if True: - plt.rcParams["figure.figsize"] = (10, 10) - sc.pl.spatial( - st_adata, img_key="hires", color=["clusters"], save=args.Clusters_scanpy_spatial - ) # groups=['1', '2', '3'] - -# Make violin plots of topic proportions of ST spots by clusters -if True: - plt.rcParams["figure.figsize"] = (3.5, 3.5) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic LDA ")] - sc.pl.violin(st_adata, keys, jitter=0.4, groupby="clusters", rotation=0, save=args.violin_topics_LDA) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("Topic NMF ")] - sc.pl.violin(st_adata, keys, jitter=0.4, groupby="clusters", rotation=0, save=args.violin_topics_NMF) - keys = st_adata.obs.columns[st_adata.obs.columns.str.contains("LT PC ")] - sc.pl.violin(st_adata, keys, jitter=0.4, groupby="clusters", rotation=0, save=args.violin_topics_LT_PC) - -st_adata.write(args.saveFileST) -sc_adata.write(args.saveFileSC) diff --git a/bin/stPreprocess.py b/bin/stPreprocess.qmd old mode 100755 new mode 100644 similarity index 100% rename from bin/stPreprocess.py rename to bin/stPreprocess.qmd diff --git a/bin/stSpatialDE.py b/bin/stSpatialDE.qmd old mode 100755 new mode 100644 similarity index 100% rename from bin/stSpatialDE.py rename to bin/stSpatialDE.qmd From a635f5bfa28e76967279853796900cb7c3950a80 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 31 Mar 2023 22:29:37 +0200 Subject: [PATCH 014/410] Add SpatialDE environment --- env/environment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/env/environment.yml b/env/environment.yml index 6b99dd2..711f169 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -15,3 +15,6 @@ dependencies: - umap-learn=0.5.2 - papermill=2.3.4 - jupyter=1.0.0 + - pip=20.2.4 + - pip: + - SpatialDE==1.1.3 From 971dd8297115082ac9342bc74b13cf629256f3a3 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 31 Mar 2023 22:31:36 +0200 Subject: [PATCH 015/410] Remove unused scripts --- bin/script_read_st_data.py | 11 -- conf/analysis.config | 37 +----- modules/local/calculate_sum_factors.nf | 27 ---- modules/local/read_st_and_sc_data.nf | 11 +- modules/local/sc_preprocess.nf | 45 ------- nextflow_schema.json | 150 ----------------------- subworkflows/local/preprocess_sc_data.nf | 45 ------- subworkflows/local/preprocess_st_data.nf | 1 - workflows/spatialtranscriptomics.nf | 1 - 9 files changed, 2 insertions(+), 326 deletions(-) delete mode 100644 modules/local/calculate_sum_factors.nf delete mode 100644 modules/local/sc_preprocess.nf delete mode 100644 subworkflows/local/preprocess_sc_data.nf diff --git a/bin/script_read_st_data.py b/bin/script_read_st_data.py index a9fabff..7dbac15 100755 --- a/bin/script_read_st_data.py +++ b/bin/script_read_st_data.py @@ -16,21 +16,10 @@ parser.add_argument( "--outAnnData", metavar="outAnnData", type=str, default=None, help="Path to a file to save h5ad data into." ) -parser.add_argument("--outSTCounts", metavar="outSTCounts", type=str, default=None, help="Name of the NPZ file.") -parser.add_argument("--minCounts", metavar="minCounts", type=int, default=1, help="Min counts per spot.") -parser.add_argument("--minCells", metavar="minCells", type=int, default=1, help="Min cells per gene.") args = parser.parse_args() # Main script st_adata = read_visium_mtx.read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) -st_adata.var_names_make_unique() -sc.pp.filter_cells(st_adata, min_counts=args.minCounts) -sc.pp.filter_genes(st_adata, min_cells=args.minCells) - # Save raw anndata to file st_adata.write(args.outAnnData) - -# Save counts anndata to file -X = np.array(st_adata[st_adata.obs["in_tissue"] == 1].X.todense()).T -np.savez_compressed(args.outSTCounts, X) diff --git a/conf/analysis.config b/conf/analysis.config index 4b2c4a4..e041190 100644 --- a/conf/analysis.config +++ b/conf/analysis.config @@ -8,10 +8,6 @@ params { STload_minCounts = 1 STload_minCells = 1 - SCload_minCounts = 1 - SCload_minGenes = 1 - SCload_minCells = 1 - STpreprocess_pltFigSize = 6 STpreprocess_minCounts = 500 STpreprocess_minGenes = 250 @@ -20,39 +16,8 @@ params { STpreprocess_histplotQCminGeneCounts= 4000 STpreprocess_histplotQCbins = 40 - SCpreprocess_pltFigSize = 6 - SCpreprocess_minCounts = 500 - SCpreprocess_minGenes = 250 - SCpreprocess_minCells = 1 - SCpreprocess_histplotQCmaxTotalCounts= 5000 - SCpreprocess_histplotQCminGeneCounts= 2000 - SCpreprocess_histplotQCbins = 40 - SpatialDE_plotTopHVG = 15 SpatialDE_numberOfColumns = 5 - Clustering_resolution = 0.4 - - BayesSpace_numberHVG = 2000 - BayesSpace_numberPCs = 7 - BayesSpace_minClusters = 2 - BayesSpace_maxClusters = 10 - BayesSpace_optimalQ = 5 - BayesSpace_STplatform = "Visium" - - STdeconvolve_mtxGeneColumn = 2 - STdeconvolve_countsFactor = 100 - STdeconvolve_corpusRemoveAbove = 1.0 - STdeconvolve_corpusRemoveBelow = 0.05 - STdeconvolve_LDAminTopics = 8 - STdeconvolve_LDAmaxTopics = 9 - STdeconvolve_ScatterpiesSize = 2.85 - STdeconvolve_FeaturesSizeFactor = 1.0 - - SPOTlight_mtxGeneColumn = 2 - SPOTlight_countsFactor = 100 - SPOTlight_clusterResolution = 0.3 - SPOTlight_numberHVG = 3000 - SPOTlight_numberCellsPerCelltype = 100 - SPOTlight_ScatterpiesSize = 0.35 + Clustering_resolution = 1 } diff --git a/modules/local/calculate_sum_factors.nf b/modules/local/calculate_sum_factors.nf deleted file mode 100644 index 6864b99..0000000 --- a/modules/local/calculate_sum_factors.nf +++ /dev/null @@ -1,27 +0,0 @@ -// -// Calculate sum factors for use in downstream normalisation -// -process CALCULATE_SUM_FACTORS { - - // TODO: Add proper Conda/container directive - // Export versions - - tag "${meta.id}" - label "process_low" - - container "erikfas/spatialtranscriptomics" - - input: - tuple val(meta), path(counts) - - output: - tuple val(meta), path("*.npz"), emit: factors - // path("versions.yml") , emit: versions - - script: - """ - calculateSumFactors.R \ - --npCountsOutputName=${counts} \ - --npFactorsOutputName=${meta.id}.factors.npz - """ -} diff --git a/modules/local/read_st_and_sc_data.nf b/modules/local/read_st_and_sc_data.nf index 9602dff..9af53fc 100644 --- a/modules/local/read_st_and_sc_data.nf +++ b/modules/local/read_st_and_sc_data.nf @@ -23,22 +23,13 @@ process READ_ST_AND_SC_DATA { output: tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw - tuple val(meta), path("sc_adata_raw.h5ad"), emit: sc_raw - tuple val(meta), path("st_counts.npz") , emit: st_counts - tuple val(meta), path("sc_counts.npz") , emit: sc_counts path("versions.yml") , emit: versions script: """ script_read_st_data.py \ --SRCountDir ./SRCount \ - --outAnnData st_adata_raw.h5ad \ - --outSTCounts st_counts.npz - - script_read_sc_data.py \ - --SRCountDir ./SRCount \ - --outAnnData sc_adata_raw.h5ad \ - --outSCCounts sc_counts.npz + --outAnnData st_adata_raw.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/sc_preprocess.nf b/modules/local/sc_preprocess.nf deleted file mode 100644 index 9e29f12..0000000 --- a/modules/local/sc_preprocess.nf +++ /dev/null @@ -1,45 +0,0 @@ -// -// Single cell data pre-processing -// -process SC_PREPROCESS { - - // TODO: Add final Conda/container directive - // TODO: Export versions - - tag "${meta.id}" - label "process_low" - - container "erikfas/spatialtranscriptomics" - - input: - tuple val(meta), path(sc_raw), path(sc_factors) - path(mito_data) - - output: - tuple val(meta), path("*_norm.h5ad") , emit: sc_data_norm - tuple val(meta), path("*_plain.h5ad") , emit: sc_data_plain - tuple val(meta), path("*.sc_adata_x.npz") , emit: sc_adata_x - tuple val(meta), path("*.sc_adata_var.npz"), emit: sc_adata_var - tuple val(meta), path("*.sc_adata_obs.npz"), emit: sc_adata_obs - tuple val(meta), path("*.png") , emit: figures - - script: - """ - scPreprocess.py \ - --npFactorsOutputName=${sc_factors} \ - --rawAdata=${sc_raw} \ - --mitoFile=${mito_data} \ - --pltFigSize=${params.SCpreprocess_pltFigSize} \ - --minCounts=${params.SCpreprocess_minCounts} \ - --minGenes=${params.SCpreprocess_minGenes} \ - --minCells=${params.SCpreprocess_minCells} \ - --histplotQCmaxTotalCounts=${params.SCpreprocess_histplotQCmaxTotalCounts} \ - --histplotQCminGeneCounts=${params.SCpreprocess_histplotQCminGeneCounts} \ - --histplotQCbins=${params.SCpreprocess_histplotQCbins} \ - --nameDataPlain=${meta.id}.sc_adata_plain.h5ad \ - --nameDataNorm=${meta.id}.sc_adata_norm.h5ad \ - --nameX ${meta.id}.sc_adata_x.npz \ - --nameVar ${meta.id}.sc_adata_var.npz \ - --nameObs ${meta.id}.sc_adata_obs.npz - """ -} diff --git a/nextflow_schema.json b/nextflow_schema.json index c68f590..c1bbec6 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -141,21 +141,6 @@ "default": 1, "description": "Minimum cells count" }, - "SCload_minCounts": { - "type": "integer", - "default": 1, - "description": "Minimum UMI count" - }, - "SCload_minGenes": { - "type": "integer", - "default": 1, - "description": "Minimum genes count" - }, - "SCload_minCells": { - "type": "integer", - "default": 1, - "description": "Minimum cells count" - }, "STpreprocess_pltFigSize": { "type": "integer", "default": 6, @@ -191,41 +176,6 @@ "default": 40, "description": "Histogram QC plot number of bins" }, - "SCpreprocess_pltFigSize": { - "type": "integer", - "default": 6, - "description": "Figure size, inches" - }, - "SCpreprocess_minCounts": { - "type": "integer", - "default": 500, - "description": "Minimum UMI count" - }, - "SCpreprocess_minGenes": { - "type": "integer", - "default": 250, - "description": "Minimum genes count" - }, - "SCpreprocess_minCells": { - "type": "integer", - "default": 1, - "description": "Minimum cells count" - }, - "SCpreprocess_histplotQCmaxTotalCounts": { - "type": "integer", - "default": 5000, - "description": "Max total counts cutoff for histogram QC plot" - }, - "SCpreprocess_histplotQCminGeneCounts": { - "type": "integer", - "default": 2000, - "description": "Min gene total counts cutoff for histogram QC plot" - }, - "SCpreprocess_histplotQCbins": { - "type": "integer", - "default": 40, - "description": "Histogram QC plot number of bins" - }, "SpatialDE_plotTopHVG": { "type": "integer", "default": 15, @@ -240,106 +190,6 @@ "type": "number", "default": 0.4, "description": "Clustering resolution for ST spots" - }, - "BayesSpace_numberHVG": { - "type": "integer", - "default": 2000, - "description": "Number of top highly variable genes to use" - }, - "BayesSpace_numberPCs": { - "type": "integer", - "default": 7, - "description": "Number of principal components to use" - }, - "BayesSpace_minClusters": { - "type": "integer", - "default": 2, - "description": "Minimum number of clusters to try" - }, - "BayesSpace_maxClusters": { - "type": "integer", - "default": 10, - "description": "Maxium number of clusters to try" - }, - "BayesSpace_optimalQ": { - "type": "integer", - "default": 5, - "description": "Default optimal number of clusters" - }, - "BayesSpace_STplatform": { - "type": "string", - "default": "Visium", - "description": "Spatial technology platform" - }, - "STdeconvolve_mtxGeneColumn": { - "type": "integer", - "default": 2, - "description": "Gene column identifier in the MTX formatted data" - }, - "STdeconvolve_countsFactor": { - "type": "integer", - "default": 100, - "description": "Integer transformation factor" - }, - "STdeconvolve_corpusRemoveAbove": { - "type": "number", - "default": 1.0, - "description": "Upper cutoff for genes" - }, - "STdeconvolve_corpusRemoveBelow": { - "type": "number", - "default": 0.05, - "description": "Lower cutoff for genes" - }, - "STdeconvolve_LDAminTopics": { - "type": "integer", - "default": 8, - "description": "Minimum number of topics to try" - }, - "STdeconvolve_LDAmaxTopics": { - "type": "integer", - "default": 9, - "description": "Maximum number of topics to try" - }, - "STdeconvolve_ScatterpiesSize": { - "type": "number", - "default": 2.85, - "description": "Size of scatterpies" - }, - "STdeconvolve_FeaturesSizeFactor": { - "type": "number", - "default": 1.0, - "description": "Size of spots in the feature plots" - }, - "SPOTlight_mtxGeneColumn": { - "type": "integer", - "default": 2, - "description": "Gene column identifier in the MTX formatted data" - }, - "SPOTlight_countsFactor": { - "type": "integer", - "default": 100, - "description": "Integer transformation factor" - }, - "SPOTlight_clusterResolution": { - "type": "number", - "default": 0.3, - "description": "Clustering resolution" - }, - "SPOTlight_numberHVG": { - "type": "integer", - "default": 3000, - "description": "Number of top highly variable genes to use" - }, - "SPOTlight_numberCellsPerCelltype": { - "type": "integer", - "default": 100, - "description": "Number of cells per celltype" - }, - "SPOTlight_ScatterpiesSize": { - "type": "number", - "default": 0.35, - "description": "Size of scatterpies" } } }, diff --git a/subworkflows/local/preprocess_sc_data.nf b/subworkflows/local/preprocess_sc_data.nf deleted file mode 100644 index 8474058..0000000 --- a/subworkflows/local/preprocess_sc_data.nf +++ /dev/null @@ -1,45 +0,0 @@ -// -// Pre-processing of SC data -// - -include { CALCULATE_SUM_FACTORS } from '../../modules/local/calculate_sum_factors' -include { SC_PREPROCESS } from '../../modules/local/sc_preprocess' - -workflow PREPROCESS_SC_DATA { - - take: - sc_counts - sc_raw - mito_data - - main: - - ch_versions = Channel.empty() - - // TODO: Incorporate this step into `READ_ST_AND_SC_DATA` or skip it? - // - // Calculate sum factors used for normalisation in pre-processing - // - CALCULATE_SUM_FACTORS ( - sc_counts - ) - // ch_versions = ch_versions.mix(CALCULATE_SUM_FACTORS.out.versions) - - // - // Spatial pre-processing - // - SC_PREPROCESS ( - sc_raw.join(CALCULATE_SUM_FACTORS.out.factors), - mito_data - ) - // ch_versions = ch_versions.mix(SC_PREPROCESS.out.versions) - - emit: - sc_data_norm = SC_PREPROCESS.out.sc_data_norm // channel: [ val(sample), h5ad ] - sc_data_plain = SC_PREPROCESS.out.sc_data_plain // channel: [ val(sample), h5ad ] - sc_adata_x = SC_PREPROCESS.out.sc_adata_x // channel: [ val(sample), npz ] - sc_adata_var = SC_PREPROCESS.out.sc_adata_var // channel: [ val(sample), npz ] - sc_adata_obs = SC_PREPROCESS.out.sc_adata_obs // channel: [ val(sample), npz ] - - versions = ch_versions // channel: [ version.yml ] -} diff --git a/subworkflows/local/preprocess_st_data.nf b/subworkflows/local/preprocess_st_data.nf index fefb492..4473f25 100644 --- a/subworkflows/local/preprocess_st_data.nf +++ b/subworkflows/local/preprocess_st_data.nf @@ -2,7 +2,6 @@ // Pre-processing of ST data // -include { CALCULATE_SUM_FACTORS } from '../../modules/local/calculate_sum_factors' include { ST_PREPROCESS } from '../../modules/local/st_preprocess' workflow PREPROCESS_ST_DATA { diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 7676627..5fee9e0 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -49,7 +49,6 @@ include { READ_ST_AND_SC_DATA } from '../modules/local/read_st_and_sc_data' // SUBWORKFLOW: Consisting of a mix of local and nf-core/modules // include { INPUT_CHECK } from '../subworkflows/local/input_check' -include { PREPROCESS_SC_DATA } from '../subworkflows/local/preprocess_sc_data' include { PREPROCESS_ST_DATA } from '../subworkflows/local/preprocess_st_data' include { SPACERANGER } from '../subworkflows/local/spaceranger' include { ST_POSTPROCESSING } from '../subworkflows/local/st_postprocessing' From 0945ba817b990db8996db94087e5c3c0fc8d25a9 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 31 Mar 2023 22:32:00 +0200 Subject: [PATCH 016/410] Add preProcess reporting --- bin/stPreprocess.qmd | 196 ++++++++++------------- modules/local/st_preprocess.nf | 47 +++--- subworkflows/local/preprocess_st_data.nf | 15 +- workflows/spatialtranscriptomics.nf | 1 - 4 files changed, 115 insertions(+), 144 deletions(-) diff --git a/bin/stPreprocess.qmd b/bin/stPreprocess.qmd index 5ae34c6..7323cd4 100644 --- a/bin/stPreprocess.qmd +++ b/bin/stPreprocess.qmd @@ -1,7 +1,46 @@ -#!/usr/bin/env python +--- +title: "Pre-processing and filtering of Spatial Transcriptomics data" +format: + html: + code-fold: true +jupyter: python3 +--- + +```{python} +#| tags: [parameters] +#| echo: false + +fileNameST = None +resolution = 1 +saveFileST = None + +rawAdata = None #Name of the h5ad file +mitoFile = None #Path and name of the mito file +pltFigSize = 6 #Figure size +minCounts = 500 #Min counts per spot +minGenes = 250 #Min genes per spot +minCells = 1 #Min cells per gene +histplotQCmaxTotalCounts = 10000 #Max total counts +histplotQCminGeneCounts = 4000 #Min gene counts +histplotQCbins = 40 #Number of bins +nameDataPlain = "st_adata_plain.h5ad" #Name of the raw data save file +nameDataNorm = "st_adata_norm.h5ad" #Name of the normalized data save file +``` + +```{python} +#| echo: false +# Hide warnings in output html. +import scanpy as sc +import warnings +warnings.filterwarnings("ignore") +sc.settings.verbosity = 0 +``` + +```{python} +#| echo: false +# Hide warnings in output html. # Load packages -import argparse import scanpy as sc import numpy as np import pandas as pd @@ -9,91 +48,32 @@ import scipy.stats from matplotlib import pyplot as plt from scipy.sparse import csr_matrix - -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Preprocess spatial traqnscriptomics data.") -parser.add_argument("--filePath", metavar="path", type=str, default=None, help="Path to data.") -parser.add_argument( - "--npFactorsOutputName", metavar="filename", type=str, default=None, help="Name of files with counts." -) -parser.add_argument("--rawAdata", metavar="h5file", type=str, default=None, help="Name of the h5ad file.") -parser.add_argument("--mitoFile", metavar="file", type=str, default=None, help="Path and name of the mito file.") -parser.add_argument("--pltFigSize", metavar="figsize", type=int, default=6, help="Figure size.") -parser.add_argument("--stQCinName", metavar="name", type=str, default="st_QC_in.png", help="Figure name.") -parser.add_argument("--stQCoutName", metavar="name", type=str, default="st_QC_out.png", help="Figure name.") -parser.add_argument("--minCounts", metavar="cutoff", type=int, default=500, help="Min counts per spot.") -parser.add_argument("--minGenes", metavar="cutoff", type=int, default=250, help="Min genes per spot.") -parser.add_argument("--minCells", metavar="cutoff", type=int, default=1, help="Min cells per gene.") -parser.add_argument("--histplotQCmaxTotalCounts", metavar="cutoff", type=int, default=10000, help="Max total counts.") -parser.add_argument("--histplotQCminGeneCounts", metavar="cutoff", type=int, default=4000, help="Min gene counts.") -parser.add_argument("--histplotQCbins", metavar="number", type=int, default=40, help="Number of bins.") -parser.add_argument( - "--histogramPlotAllName", metavar="name", type=str, default="st_histogrtam_all.png", help="Figure name." -) -parser.add_argument( - "--histogramPlotOutName", metavar="name", type=str, default="st_histogrtam_out.png", help="Figure name." -) -parser.add_argument( - "--histWithWithoutNorm", - metavar="name", - type=str, - default="st_histogram_with_without_normalization.png", - help="Figure name.", -) -parser.add_argument("--nameX", metavar="File name", type=str, default="st_adata_X.npz", help="Name of the counts file.") -parser.add_argument( - "--nameVar", metavar="File name", type=str, default="st_adata.var.csv", help="Name of the features file." -) -parser.add_argument( - "--nameObs", metavar="File name", type=str, default="st_adata.obs.csv", help="Name of the observations file." -) -parser.add_argument( - "--nameDataPlain", metavar="File name", type=str, default="st_adata_plain.h5ad", help="Name of the data save file." -) -parser.add_argument( - "--nameDataNorm", metavar="File name", type=str, default="st_adata_norm.h5ad", help="Name of the data save file." -) - -args = parser.parse_args() - -# Main script -# See more settings at: -# https://scanpy.readthedocs.io/en/stable/generated/scanpy._settings.ScanpyConfig.html -# sc.settings.figdir = args.filePath - -# if not os.path.exists(args.filePath + 'show/'): -# os.makedirs(args.filePath + 'show/') - -f_temp = np.load(args.npFactorsOutputName) -f_temp = f_temp[list(f_temp.keys())[0]] -print(f_temp.shape) - -st_adata = sc.read(args.rawAdata) +st_adata = sc.read(rawAdata) print(st_adata.shape) -st_adata.obs["norm_factors"] = pd.Series(index=st_adata.obs[st_adata.obs["in_tissue"] == 1].index, data=f_temp).reindex( - st_adata.obs.index -) +#st_adata.obs["norm_factors"] = pd.Series(index=st_adata.obs[st_adata.obs["in_tissue"] == 1].index, data=f_temp).reindex( +# st_adata.obs.index +#) -mito = pd.read_csv(args.mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] +mito = pd.read_csv(mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] mito = mito.xs(1, level="MCARTA2_LIST").sort_index().reset_index() print(mito) st_adata.var["mt"] = st_adata.var_names.isin(mito["Symbol"]) sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt"], inplace=True) -plt.rcParams["figure.figsize"] = (args.pltFigSize, args.pltFigSize) +plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) keys = ["in_tissue", "pct_counts_mt", "total_counts", "n_genes_by_counts"] st_adata_in = st_adata[st_adata.obs["in_tissue"] == 1].copy() -sc.pl.spatial(st_adata_in, img_key="hires", color=keys, save=args.stQCinName) +sc.pl.spatial(st_adata_in, img_key="hires", color=keys) keys = ["pct_counts_mt", "total_counts", "n_genes_by_counts"] st_adata_out = st_adata[st_adata.obs["in_tissue"] != 1].copy() -sc.pp.filter_cells(st_adata_out, min_counts=args.minCounts) -sc.pp.filter_cells(st_adata_out, min_genes=args.minCells) -sc.pp.filter_genes(st_adata_out, min_cells=args.minGenes) -sc.pl.spatial(st_adata_out, img_key="hires", color=keys, save=args.stQCoutName) +sc.pp.filter_cells(st_adata_out, min_counts=minCounts) +sc.pp.filter_cells(st_adata_out, min_genes=minCells) +sc.pp.filter_genes(st_adata_out, min_cells=minGenes) +sc.pl.spatial(st_adata_out, img_key="hires", color=keys) def histplotQC(se_data, bins, ax): @@ -107,68 +87,67 @@ def histplotQC(se_data, bins, ax): return -fig, axs = plt.subplots(1, 5, figsize=(args.pltFigSize * 5, args.pltFigSize)) -histplotQC(st_adata.obs["total_counts"], bins=args.histplotQCbins, ax=axs[0]) +fig, axs = plt.subplots(1, 5, figsize=(pltFigSize * 5, pltFigSize)) +histplotQC(st_adata.obs["total_counts"], bins=histplotQCbins, ax=axs[0]) histplotQC( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < args.histplotQCmaxTotalCounts], - bins=args.histplotQCbins, + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + bins=histplotQCbins, ax=axs[1], ) -histplotQC(st_adata.obs["n_genes_by_counts"], bins=args.histplotQCbins, ax=axs[2]) +histplotQC(st_adata.obs["n_genes_by_counts"], bins=histplotQCbins, ax=axs[2]) histplotQC( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < args.histplotQCminGeneCounts], - bins=args.histplotQCbins, + st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], + bins=histplotQCbins, ax=axs[3], ) -histplotQC(st_adata.obs["pct_counts_mt"], bins=args.histplotQCbins, ax=axs[4]) +histplotQC(st_adata.obs["pct_counts_mt"], bins=histplotQCbins, ax=axs[4]) fig.tight_layout() fig.savefig("st_histogrtam_all.png", facecolor="white") -fig, axs = plt.subplots(1, 5, figsize=(args.pltFigSize * 5, args.pltFigSize)) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"], bins=args.histplotQCbins, ax=axs[0]) +fig, axs = plt.subplots(1, 5, figsize=(pltFigSize * 5, pltFigSize)) +histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"], bins=histplotQCbins, ax=axs[0]) histplotQC( st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"][ - st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"] < args.histplotQCmaxTotalCounts + st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"] < histplotQCmaxTotalCounts ], - bins=args.histplotQCbins, + bins=histplotQCbins, ax=axs[1], ) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"], bins=args.histplotQCbins, ax=axs[2]) +histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"], bins=histplotQCbins, ax=axs[2]) histplotQC( st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"][ - st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"] < args.histplotQCminGeneCounts + st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"] < histplotQCminGeneCounts ], - bins=args.histplotQCbins, + bins=histplotQCbins, ax=axs[3], ) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["pct_counts_mt"], bins=args.histplotQCbins, ax=axs[4]) +histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["pct_counts_mt"], bins=histplotQCbins, ax=axs[4]) fig.tight_layout() fig.savefig("st_histogrtam_in.png", facecolor="white") plt.close(fig) -# TO DO: args.histogramPlotOutName - # Remove spots outside tissue st_adata = st_adata[st_adata.obs["in_tissue"] == 1] print("Filtered out spots outside tissue:", st_adata.shape) # Save to open in R -st_adata_R = st_adata.copy() -print(st_adata_R) -st_adata_R.X = csr_matrix(st_adata_R.X / st_adata_R.obs["norm_factors"].values[:, None]) -sc.pp.log1p(st_adata_R) -np.savez_compressed(args.nameX, st_adata_R.X.T.todense()) -st_adata_R.var.to_csv(args.nameVar) -st_adata_R.obs.to_csv(args.nameObs) - - -sc.pp.filter_cells(st_adata, min_counts=args.minCounts) -sc.pp.filter_cells(st_adata, min_genes=args.minGenes) -sc.pp.filter_genes(st_adata, min_cells=args.minCells) +#st_adata_R = st_adata.copy() +#print(st_adata_R) +#st_adata_R.X = csr_matrix(st_adata_R.X / st_adata_R.obs["norm_factors"].values[:, None]) +#sc.pp.log1p(st_adata_R) +#np.savez_compressed(nameX, st_adata_R.X.T.todense()) +#st_adata_R.var.to_csv(nameVar) +#st_adata_R.obs.to_csv(nameObs) + + +sc.pp.filter_cells(st_adata, min_counts=minCounts) +sc.pp.filter_cells(st_adata, min_genes=minGenes) +sc.pp.filter_genes(st_adata, min_cells=minCells) print("Filtered spots and genes:", st_adata.shape) # Effect of normalization by size factors -fig, ax = plt.subplots(figsize=(args.pltFigSize, args.pltFigSize)) +# TODO: Add correct normalization within scanpy +fig, ax = plt.subplots(figsize=(pltFigSize, pltFigSize)) display_cutoff = 10**5 se = pd.Series(np.array(st_adata.X.sum(axis=1)).T[0]) se = se[se < display_cutoff] @@ -176,19 +155,20 @@ print("Number of spots displayed:", se.shape) se.hist(bins=100, alpha=0.75, ax=ax) ax.set_xlim(0, display_cutoff) st_adata_c = st_adata[st_adata.obs["in_tissue"] == 1].copy() -st_adata_c.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) +###st_adata_c.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) se = pd.Series(np.array(st_adata_c.X.sum(axis=1)).T[0]) se = se[se < display_cutoff] print("Number of spots displayed:", se.shape) se.hist(bins=100, alpha=0.75, ax=ax) ax.set_xlim(0, display_cutoff) -fig.savefig(args.histWithWithoutNorm, facecolor="white", dpi=300) +#fig.savefig(histWithWithoutNorm, facecolor="white", dpi=300) plt.close(fig) # Save raw filtered data -st_adata.write(args.nameDataPlain) +st_adata.write(nameDataPlain) # Save normalized data -st_adata.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) +##st_adata.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) sc.pp.log1p(st_adata) -st_adata.write(args.nameDataNorm) +st_adata.write(nameDataNorm) +``` \ No newline at end of file diff --git a/modules/local/st_preprocess.nf b/modules/local/st_preprocess.nf index 9b41ed4..893fc39 100644 --- a/modules/local/st_preprocess.nf +++ b/modules/local/st_preprocess.nf @@ -9,38 +9,37 @@ process ST_PREPROCESS { tag "${sample_id}" label "process_low" - container "erikfas/spatialtranscriptomics" + container "cavenel/spatialtranscriptomics" input: - tuple val(sample_id), path(st_raw), path(st_factors) + path(report_template_summary) + tuple val(sample_id), path(st_raw, stageAs: "adata_raw.h5ad") path(mito_data) output: - tuple val(sample_id), path("*_norm.h5ad") , emit: st_data_norm - tuple val(sample_id), path("*_plain.h5ad") , emit: st_data_plain - tuple val(sample_id), path("*.st_adata_x.npz") , emit: st_adata_x - tuple val(sample_id), path("*.st_adata_var.npz"), emit: st_adata_var - tuple val(sample_id), path("*.st_adata_obs.npz"), emit: st_adata_obs - tuple val(sample_id), path("*.png") , emit: figures + tuple val(sample_id), path("*.st_adata_norm.h5ad") , emit: st_data_norm + tuple val(sample_id), path("*.st_adata_plain.h5ad"), emit: st_data_plain + tuple val(sample_id), path("*.stPreprocessing.html") , emit: report + tuple val(sample_id), path("stPreprocess_files/*") , emit: report_files + // path("versions.yml") , emit: versions script: """ - stPreprocess.py \ - --npFactorsOutputName ${st_factors} \ - --rawAdata ${st_raw} \ - --mitoFile ${mito_data} \ - --pltFigSize ${params.STpreprocess_pltFigSize} \ - --minCounts ${params.STpreprocess_minCounts} \ - --minGenes ${params.STpreprocess_minGenes} \ - --minCells ${params.STpreprocess_minCells} \ - --histplotQCmaxTotalCounts ${params.STpreprocess_histplotQCmaxTotalCounts} \ - --histplotQCminGeneCounts ${params.STpreprocess_histplotQCminGeneCounts} \ - --histplotQCbins ${params.STpreprocess_histplotQCbins} \ - --nameDataPlain ${sample_id}.st_adata_plain.h5ad \ - --nameDataNorm ${sample_id}.st_adata_norm.h5ad \ - --nameX ${sample_id}.st_adata_x.npz \ - --nameVar ${sample_id}.st_adata_var.npz \ - --nameObs ${sample_id}.st_adata_obs.npz + quarto render "${report_template_summary}" --output "${sample_id}.stPreprocessing.html" \ + -P rawAdata:${st_raw} \ + -P mitoFile:${mito_data} \ + -P pltFigSize:${params.STpreprocess_pltFigSize} \ + -P minCounts:${params.STpreprocess_minCounts} \ + -P minGenes:${params.STpreprocess_minGenes} \ + -P minCells:${params.STpreprocess_minCells} \ + -P histplotQCmaxTotalCounts:${params.STpreprocess_histplotQCmaxTotalCounts} \ + -P histplotQCminGeneCounts:${params.STpreprocess_histplotQCminGeneCounts} \ + -P histplotQCbins:${params.STpreprocess_histplotQCbins} \ + -P nameDataPlain:st_adata_plain.h5ad \ + -P nameDataNorm:st_adata_norm.h5ad + + mv st_adata_plain.h5ad "${sample_id}.st_adata_plain.h5ad" + mv st_adata_norm.h5ad "${sample_id}.st_adata_norm.h5ad" """ } diff --git a/subworkflows/local/preprocess_st_data.nf b/subworkflows/local/preprocess_st_data.nf index 4473f25..f291d8d 100644 --- a/subworkflows/local/preprocess_st_data.nf +++ b/subworkflows/local/preprocess_st_data.nf @@ -7,7 +7,6 @@ include { ST_PREPROCESS } from '../../modules/local/st_preprocess' workflow PREPROCESS_ST_DATA { take: - st_counts st_raw mito_data @@ -15,20 +14,17 @@ workflow PREPROCESS_ST_DATA { ch_versions = Channel.empty() - // TODO: Incorporate this step into `READ_ST_AND_SC_DATA` or skip it? // - // Calculate sum factors used for normalisation in pre-processing + // Report files // - CALCULATE_SUM_FACTORS ( - st_counts - ) - // ch_versions = ch_versions.mix(CALCULATE_SUM_FACTORS.out.versions) + report_template_summary = file("${projectDir}/bin/stPreprocess.qmd") // // Spatial pre-processing // ST_PREPROCESS ( - st_raw.join(CALCULATE_SUM_FACTORS.out.factors), + report_template_summary, + st_raw, mito_data ) // ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) @@ -36,9 +32,6 @@ workflow PREPROCESS_ST_DATA { emit: st_data_norm = ST_PREPROCESS.out.st_data_norm // channel: [ val(sample), h5ad ] st_data_plain = ST_PREPROCESS.out.st_data_plain // channel: [ val(sample), h5ad ] - st_adata_x = ST_PREPROCESS.out.st_adata_x // channel: [ val(sample), npz ] - st_adata_var = ST_PREPROCESS.out.st_adata_var // channel: [ val(sample), npz ] - st_adata_obs = ST_PREPROCESS.out.st_adata_obs // channel: [ val(sample), npz ] versions = ch_versions // channel: [ version.yml ] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 5fee9e0..162135d 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -121,7 +121,6 @@ workflow ST { // SUBWORKFLOW: Pre-processing of ST data // PREPROCESS_ST_DATA ( - READ_ST_AND_SC_DATA.out.st_counts, READ_ST_AND_SC_DATA.out.st_raw, ch_mito_data ) From c2d6abfc434ca718ece49e62b94926d48f635ddf Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 31 Mar 2023 22:32:34 +0200 Subject: [PATCH 017/410] Add SpatialDE reporting --- bin/stSpatialDE.qmd | 43 ++++++++++++++++--------- modules/local/st_clustering.nf | 6 ++-- modules/local/st_spatial_de.nf | 22 ++++++++----- subworkflows/local/st_postprocessing.nf | 9 +++--- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/bin/stSpatialDE.qmd b/bin/stSpatialDE.qmd index e38d6ea..be6c5f4 100644 --- a/bin/stSpatialDE.qmd +++ b/bin/stSpatialDE.qmd @@ -1,25 +1,35 @@ -#!/usr/bin/env python - +--- +title: "Clustering Spatial Transcriptomics data" +format: + html: + code-fold: true +jupyter: python3 +--- + +```{python} +#| tags: [parameters] +#| echo: false + +fileNameST = "" +numberOfColumns = 5 +saveDEFileName = "" +saveSpatialDEFileName = "" +plotTopHVG = 15 +``` + +```{python} +#| echo: false +# Hide warnings in output html. # Load packages import argparse import scanpy as sc import pandas as pd import SpatialDE - -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Preprocess single cell transcriptomics data.") -parser.add_argument("--fileName", metavar="file", type=str, default="st_adata_norm.h5ad", help="File name.") -parser.add_argument("--saveFileName", metavar="file", type=str, default="stSpatialDE.csv", help="File name.") -parser.add_argument("--savePlotName", metavar="plot", type=str, default="stSpatialDE.png", help="File name.") -parser.add_argument("--plotTopHVG", metavar="number", type=int, default=15, help="File name.") -parser.add_argument("--numberOfColumns", metavar="number", type=int, default=5, help="File name.") -args = parser.parse_args() - # Main script # See more settings at: # https://scanpy.readthedocs.io/en/stable/generated/scanpy._settings.ScanpyConfig.html -st_adata = sc.read(args.fileName) +st_adata = sc.read(fileNameST) print(st_adata.shape) counts = pd.DataFrame(st_adata.X.todense(), columns=st_adata.var_names, index=st_adata.obs_names) @@ -30,8 +40,9 @@ df_results = SpatialDE.run(coord, counts) df_results.index = df_results["g"] df_results = df_results.sort_values("qval", ascending=True) -df_results.to_csv(args.saveFileName) +df_results.to_csv(saveSpatialDEFileName) # Plotting top most-HVG -keys = df_results.index.values[: args.plotTopHVG] -sc.pl.spatial(st_adata, img_key="hires", color=keys, alpha=0.7, save=args.savePlotName, ncols=args.numberOfColumns) +keys = df_results.index.values[: plotTopHVG] +sc.pl.spatial(st_adata, img_key="hires", color=keys, alpha=0.7, ncols=numberOfColumns) +``` \ No newline at end of file diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index d604b2c..3a3f5aa 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -7,7 +7,7 @@ process ST_CLUSTERING { // TODO: Add proper Conda/container directive // TODO: Export versions - label "process_medium" + label "process_low" container "cavenel/spatialtranscriptomics" @@ -16,7 +16,7 @@ process ST_CLUSTERING { tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample_id), path("*.st_*.h5ad"), emit: st_adata_processed + tuple val(sample_id), path("*.st_adata_processed.h5ad"), emit: st_adata_processed tuple val(sample_id), path("*.stClustering.html") , emit: report tuple val(sample_id), path("stClustering_files/*") , emit: report_files // path("versions.yml") , emit: versions @@ -25,7 +25,7 @@ process ST_CLUSTERING { """ quarto render "${report_template_summary}" --output "${sample_id}.stClustering.html" \ -P fileNameST:${st_adata_norm} \ - -P resolution:$params.Clustering_resolution \ + -P resolution:${params.Clustering_resolution} \ -P saveFileST:st_adata_processed.h5ad mv st_adata_processed.h5ad "${sample_id}.st_adata_processed.h5ad" diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 81cffd0..bcc39f5 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -9,22 +9,28 @@ process ST_SPATIAL_DE { tag "${sample_id}" label "process_low" - container "erikfas/spatialtranscriptomics" + container "cavenel/spatialtranscriptomics" input: - tuple val(sample_id), path(st_data_norm) + path(report_template_summary) + tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: tuple val(sample_id), path("*.csv"), emit: degs - path("*.png") , emit: figures, optional: true + tuple val(sample_id), path("*.stSpatialDE.html") , emit: report + tuple val(sample_id), path("stSpatialDE_files/*") , emit: report_files + // path("versions.yml") , emit: versions script: """ - stSpatialDE.py \ - --fileName=${st_data_norm} \ - --numberOfColumns=${params.SpatialDE_numberOfColumns} \ - --saveFileName=${sample_id}.stSpatialDE.csv \ - --savePlotName=${sample_id}.stSpatialDE.png + quarto render "${report_template_summary}" --output "${sample_id}.stSpatialDE.html" \ + -P fileNameST:${st_adata_norm} \ + -P numberOfColumns:${params.SpatialDE_numberOfColumns} \ + -P saveDEFileName:stDE.csv \ + -P saveSpatialDEFileName:stSpatialDE.csv + + # mv stDE.csv "${sample_id}.stDE.csv" + mv stSpatialDE.csv "${sample_id}.stSpatialDE.csv" """ } diff --git a/subworkflows/local/st_postprocessing.nf b/subworkflows/local/st_postprocessing.nf index c75380a..754de03 100644 --- a/subworkflows/local/st_postprocessing.nf +++ b/subworkflows/local/st_postprocessing.nf @@ -18,11 +18,12 @@ workflow ST_POSTPROCESSING { // // Report files // - report_template_summary = file("${projectDir}/bin/stClustering.qmd") + report_template_summary_clustering = file("${projectDir}/bin/stClustering.qmd") + report_template_summary_spatial_de = file("${projectDir}/bin/stSpatialDE.qmd") // Clustering ST_CLUSTERING ( - report_template_summary, + report_template_summary_clustering, st_adata_norm ) @@ -30,7 +31,8 @@ workflow ST_POSTPROCESSING { // Spatial differential expression // ST_SPATIAL_DE ( - st_adata_norm + report_template_summary_spatial_de, + ST_CLUSTERING.out.st_adata_processed ) // TODO: Add reporting @@ -41,7 +43,6 @@ workflow ST_POSTPROCESSING { emit: spatial_degs = ST_SPATIAL_DE.out.degs // channel: [ val(sample), csv ] - spatial_figures = ST_SPATIAL_DE.out.figures // channel: [ val(sample), png ] versions = ch_versions // channel: [ versions.yml ] } From 150c21c1b5a46ece4b7151a1d396e6ef013ec86d Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Sat, 1 Apr 2023 14:30:52 +0200 Subject: [PATCH 018/410] Format preprocessing for reporting --- bin/stPreprocess.qmd | 163 ++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 111 deletions(-) diff --git a/bin/stPreprocess.qmd b/bin/stPreprocess.qmd index 7323cd4..ec36a28 100644 --- a/bin/stPreprocess.qmd +++ b/bin/stPreprocess.qmd @@ -3,6 +3,7 @@ title: "Pre-processing and filtering of Spatial Transcriptomics data" format: html: code-fold: true + embed-resources: false jupyter: python3 --- @@ -36,139 +37,79 @@ warnings.filterwarnings("ignore") sc.settings.verbosity = 0 ``` +Importing modules ```{python} -#| echo: false -# Hide warnings in output html. - -# Load packages import scanpy as sc -import numpy as np import pandas as pd -import scipy.stats -from matplotlib import pyplot as plt -from scipy.sparse import csr_matrix +import matplotlib.pyplot as plt +import seaborn as sns -st_adata = sc.read(rawAdata) -print(st_adata.shape) +plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) +``` -#st_adata.obs["norm_factors"] = pd.Series(index=st_adata.obs[st_adata.obs["in_tissue"] == 1].index, data=f_temp).reindex( -# st_adata.obs.index -#) +```{python} +st_adata = sc.read(rawAdata) +#st_adata.var_names_make_unique() mito = pd.read_csv(mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] mito = mito.xs(1, level="MCARTA2_LIST").sort_index().reset_index() -print(mito) st_adata.var["mt"] = st_adata.var_names.isin(mito["Symbol"]) sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt"], inplace=True) +``` -plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) +This is how the adata structure looks like for Visium data -keys = ["in_tissue", "pct_counts_mt", "total_counts", "n_genes_by_counts"] -st_adata_in = st_adata[st_adata.obs["in_tissue"] == 1].copy() -sc.pl.spatial(st_adata_in, img_key="hires", color=keys) - -keys = ["pct_counts_mt", "total_counts", "n_genes_by_counts"] -st_adata_out = st_adata[st_adata.obs["in_tissue"] != 1].copy() -sc.pp.filter_cells(st_adata_out, min_counts=minCounts) -sc.pp.filter_cells(st_adata_out, min_genes=minCells) -sc.pp.filter_genes(st_adata_out, min_cells=minGenes) -sc.pl.spatial(st_adata_out, img_key="hires", color=keys) - - -def histplotQC(se_data, bins, ax): - ax.hist(se_data, density=True, bins=bins, color="navy", alpha=0.3) - kde = scipy.stats.gaussian_kde(se_data) - xx = np.linspace(min(se_data), max(se_data), 300) - ax.set_xlabel(se_data.name) - ax.set_ylabel("Density") - ax.plot(xx, kde(xx), color="crimson") - ax.set_xlim([0, ax.get_xlim()[1]]) - return - - -fig, axs = plt.subplots(1, 5, figsize=(pltFigSize * 5, pltFigSize)) -histplotQC(st_adata.obs["total_counts"], bins=histplotQCbins, ax=axs[0]) -histplotQC( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], - bins=histplotQCbins, - ax=axs[1], -) -histplotQC(st_adata.obs["n_genes_by_counts"], bins=histplotQCbins, ax=axs[2]) -histplotQC( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], - bins=histplotQCbins, - ax=axs[3], -) -histplotQC(st_adata.obs["pct_counts_mt"], bins=histplotQCbins, ax=axs[4]) -fig.tight_layout() -fig.savefig("st_histogrtam_all.png", facecolor="white") - -fig, axs = plt.subplots(1, 5, figsize=(pltFigSize * 5, pltFigSize)) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"], bins=histplotQCbins, ax=axs[0]) -histplotQC( - st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"][ - st_adata[st_adata.obs["in_tissue"] == 1].obs["total_counts"] < histplotQCmaxTotalCounts - ], - bins=histplotQCbins, - ax=axs[1], -) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"], bins=histplotQCbins, ax=axs[2]) -histplotQC( - st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"][ - st_adata[st_adata.obs["in_tissue"] == 1].obs["n_genes_by_counts"] < histplotQCminGeneCounts - ], - bins=histplotQCbins, - ax=axs[3], -) -histplotQC(st_adata[st_adata.obs["in_tissue"] == 1].obs["pct_counts_mt"], bins=histplotQCbins, ax=axs[4]) -fig.tight_layout() -fig.savefig("st_histogrtam_in.png", facecolor="white") -plt.close(fig) +```{python} +st_adata +``` -# Remove spots outside tissue -st_adata = st_adata[st_adata.obs["in_tissue"] == 1] -print("Filtered out spots outside tissue:", st_adata.shape) +## QC and preprocessing -# Save to open in R -#st_adata_R = st_adata.copy() -#print(st_adata_R) -#st_adata_R.X = csr_matrix(st_adata_R.X / st_adata_R.obs["norm_factors"].values[:, None]) -#sc.pp.log1p(st_adata_R) -#np.savez_compressed(nameX, st_adata_R.X.T.todense()) -#st_adata_R.var.to_csv(nameVar) -#st_adata_R.obs.to_csv(nameObs) +We perform some basic filtering of spots based on total counts and expressed genes + +```{python} +fig, axs = plt.subplots(2, 2, figsize=(8, 7)) +p = sns.distplot(st_adata.obs["total_counts"], kde=True, ax=axs[0, 0]) +p = sns.distplot(st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1]) +p = sns.distplot(st_adata.obs["n_genes_by_counts"], kde=True, bins=histplotQCbins, ax=axs[1, 0]) +p = sns.distplot(st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], kde=True, bins=histplotQCbins, ax=axs[1, 1]) +``` +```{python} +# Remove spots outside tissue +st_adata = st_adata[st_adata.obs["in_tissue"] == 1] sc.pp.filter_cells(st_adata, min_counts=minCounts) +#sc.pp.filter_cells(st_adata, max_counts=maxCounts) sc.pp.filter_cells(st_adata, min_genes=minGenes) +#st_adata = st_adata[st_adata.obs["pct_counts_mt"] < 20] +print(f"#cells after MT filter: {st_adata.n_obs}") sc.pp.filter_genes(st_adata, min_cells=minCells) -print("Filtered spots and genes:", st_adata.shape) - -# Effect of normalization by size factors -# TODO: Add correct normalization within scanpy -fig, ax = plt.subplots(figsize=(pltFigSize, pltFigSize)) -display_cutoff = 10**5 -se = pd.Series(np.array(st_adata.X.sum(axis=1)).T[0]) -se = se[se < display_cutoff] -print("Number of spots displayed:", se.shape) -se.hist(bins=100, alpha=0.75, ax=ax) -ax.set_xlim(0, display_cutoff) -st_adata_c = st_adata[st_adata.obs["in_tissue"] == 1].copy() -###st_adata_c.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) -se = pd.Series(np.array(st_adata_c.X.sum(axis=1)).T[0]) -se = se[se < display_cutoff] -print("Number of spots displayed:", se.shape) -se.hist(bins=100, alpha=0.75, ax=ax) -ax.set_xlim(0, display_cutoff) -#fig.savefig(histWithWithoutNorm, facecolor="white", dpi=300) -plt.close(fig) - -# Save raw filtered data +print("Filtered out spots outside tissue:", st_adata.shape) +``` + +Distribution after filtering: +```{python} +fig, axs = plt.subplots(2, 2, figsize=(8, 7)) +p = sns.distplot(st_adata.obs["total_counts"], kde=True, ax=axs[0, 0]) +p = sns.distplot(st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1]) +p = sns.distplot(st_adata.obs["n_genes_by_counts"], kde=True, bins=histplotQCbins, ax=axs[1, 0]) +p = sns.distplot(st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], kde=True, bins=histplotQCbins, ax=axs[1, 1]) +``` + +We proceed to normalize Visium counts data with the built-in `normalize_total` method from Scanpy, and detect highly-variable genes (for later). Note that there are alternatives for normalization (see discussion in [[Luecken19](https://www.embopress.org/doi/full/10.15252/msb.20188746)], and more recent alternatives such as [SCTransform](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1874-1) or [GLM-PCA](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1861-6)). + +```{python} st_adata.write(nameDataPlain) +``` -# Save normalized data -##st_adata.X = csr_matrix(st_adata.X / st_adata.obs["norm_factors"].values[:, None]) +```{python} +sc.pp.normalize_total(st_adata, inplace=True) sc.pp.log1p(st_adata) +sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) +``` + +```{python} st_adata.write(nameDataNorm) ``` \ No newline at end of file From 4a459abd9dcd224cd738261cf10d3226cee2431f Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Sat, 1 Apr 2023 14:31:31 +0200 Subject: [PATCH 019/410] Format Differential Gene Expression for reporting --- bin/stClustering.qmd | 14 +--------- bin/stSpatialDE.qmd | 61 +++++++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/bin/stClustering.qmd b/bin/stClustering.qmd index 028ab39..3fd460a 100644 --- a/bin/stClustering.qmd +++ b/bin/stClustering.qmd @@ -3,6 +3,7 @@ title: "Clustering Spatial Transcriptomics data" format: html: code-fold: true + embed-resources: false jupyter: python3 --- @@ -94,19 +95,6 @@ Spots belonging to the same cluster in gene expression space often co-occur in s st_adata.uns['log1p']['base'] = None ``` -## Differential Gene Expression - -```{python} -plt.rcParams["figure.figsize"] = (5, 5) -sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) -``` - -```{python} -sc.tl.rank_genes_groups(st_adata, 'clusters', method='wilcoxon') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) -``` - ## Saving anndata file for future use. ```{python} if saveFileST is not None: diff --git a/bin/stSpatialDE.qmd b/bin/stSpatialDE.qmd index be6c5f4..9d47044 100644 --- a/bin/stSpatialDE.qmd +++ b/bin/stSpatialDE.qmd @@ -3,6 +3,7 @@ title: "Clustering Spatial Transcriptomics data" format: html: code-fold: true + embed-resources: false jupyter: python3 --- @@ -18,31 +19,61 @@ plotTopHVG = 15 ``` ```{python} -#| echo: false -# Hide warnings in output html. # Load packages -import argparse import scanpy as sc import pandas as pd import SpatialDE +from matplotlib import pyplot as plt +``` -# Main script -# See more settings at: -# https://scanpy.readthedocs.io/en/stable/generated/scanpy._settings.ScanpyConfig.html +```{python} st_adata = sc.read(fileNameST) -print(st_adata.shape) +st_adata +``` + +## Differential Gene Expression + +```{python} +plt.rcParams["figure.figsize"] = (5, 5) +st_adata.uns['log1p']['base'] = None + +sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +``` + +```{python} +sc.tl.rank_genes_groups(st_adata, 'clusters', method='wilcoxon') +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +``` +## Spatially variable genes + +Spatial transcriptomics allows researchers to investigate how gene expression trends varies in space, thus identifying spatial patterns of gene expression. For this purpose, we use SpatialDE [Svensson18](https://www.nature.com/articles/nmeth.4636) ([code](https://github.com/Teichlab/SpatialDE)), a Gaussian process-based statistical framework that aims to identify spatially variable genes. + +First, we convert normalized counts and coordinates to pandas dataframe, needed for inputs to spatialDE. + +```{python} counts = pd.DataFrame(st_adata.X.todense(), columns=st_adata.var_names, index=st_adata.obs_names) -coord = pd.DataFrame(st_adata.obsm["spatial"], columns=["x_coord", "y_coord"], index=st_adata.obs_names) +coord = pd.DataFrame(st_adata.obsm['spatial'], columns=['x_coord', 'y_coord'], index=st_adata.obs_names) +results = SpatialDE.run(coord, counts) +``` + +We concatenate the results with the DataFrame of annotations of variables: `st_adata.var`. -df_results = SpatialDE.run(coord, counts) +```{python} +results.index = results["g"] +st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, :]], axis=1) +``` -df_results.index = df_results["g"] -df_results = df_results.sort_values("qval", ascending=True) +Then we can inspect significant genes that varies in space and visualize them with `sc.pl.spatial` function. -df_results.to_csv(saveSpatialDEFileName) +```{python} +results = results.sort_values("qval", ascending=True) +results.to_csv(saveSpatialDEFileName) +results.head(10) +``` -# Plotting top most-HVG -keys = df_results.index.values[: plotTopHVG] +```{python} +keys = results.index.values[: plotTopHVG] sc.pl.spatial(st_adata, img_key="hires", color=keys, alpha=0.7, ncols=numberOfColumns) -``` \ No newline at end of file +``` From 0d3541d45aafc66eac515ab37dc79a8f65bb8c25 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Apr 2023 12:20:29 +0200 Subject: [PATCH 020/410] Update Quarto version --- env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/Dockerfile b/env/Dockerfile index 366a910..044b833 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get -y update; apt-get -y install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install quarto -ARG QUARTO_VERSION="0.9.522" +ARG QUARTO_VERSION="1.2.475" RUN curl -o quarto-linux-amd64.deb -L https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb RUN gdebi --non-interactive quarto-linux-amd64.deb From 85d3ab83f6d2289e7b1602f5f867fc1f318c6dde Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Apr 2023 16:03:33 +0200 Subject: [PATCH 021/410] Update Quarto version in Docker image Update the Quarto version in the Docker image, which solves an issue on ARM64 architectures. --- env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/Dockerfile b/env/Dockerfile index 044b833..22471a8 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get -y update; apt-get -y install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install quarto -ARG QUARTO_VERSION="1.2.475" +ARG QUARTO_VERSION="1.3.302" RUN curl -o quarto-linux-amd64.deb -L https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb RUN gdebi --non-interactive quarto-linux-amd64.deb From 7e33fbe69ddc6d063a5eed7dc851a1d742a29db4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Apr 2023 16:04:31 +0200 Subject: [PATCH 022/410] Update Conda environment in Docker image Update the Conda environment in the Docker image, which gets us later versions of the packages and solves a kniwn issue with `numpy` / `numba`. --- env/environment.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/env/environment.yml b/env/environment.yml index 711f169..186f34c 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -1,20 +1,20 @@ channels: - conda-forge - bioconda - - defaults dependencies: # Python packages - bbknn=1.5.1 - - leidenalg=0.8.9 - - matplotlib=3.5.1 - - scikit-learn=1.0.2 - - scanorama=1.7.1 - - scanpy=1.8.2 - - scipy=1.8.0 - - seaborn=0.11.2 - - umap-learn=0.5.2 + - leidenalg=0.9.1 + - matplotlib=3.7.1 + - numpy=1.23.0 + - scikit-learn=1.2.2 + - scanorama=1.7.3 + - scanpy=1.9.3 + - scipy=1.10.1 + - seaborn=0.12.2 + - umap-learn=0.5.3 - papermill=2.3.4 - jupyter=1.0.0 - - pip=20.2.4 + - pip=23.0.1 - pip: - SpatialDE==1.1.3 From 68d89c6f5914d974130de3cb4af891ac1c4e38ed Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Apr 2023 16:05:38 +0200 Subject: [PATCH 023/410] Fix an issue with reading h5ad files Fix an issue with reading h5ad files related to needing either an absolute path or prepending the file path with `./` to correctly read h5ad files in the current directory (*e.g.* the Nextflow process work directory, in the case of this pipeline). --- bin/stClustering.qmd | 4 ++-- bin/stPreprocess.qmd | 4 ++-- bin/stSpatialDE.qmd | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/stClustering.qmd b/bin/stClustering.qmd index 3fd460a..f790d20 100644 --- a/bin/stClustering.qmd +++ b/bin/stClustering.qmd @@ -41,7 +41,7 @@ The data has already been filtered and is saved in AnnData format. ```{python} #| echo: true #| code-fold: false -st_adata = sc.read(str(fileNameST)) +st_adata = sc.read("./" + fileNameST) st_adata ``` @@ -99,4 +99,4 @@ st_adata.uns['log1p']['base'] = None ```{python} if saveFileST is not None: st_adata.write(saveFileST) -``` \ No newline at end of file +``` diff --git a/bin/stPreprocess.qmd b/bin/stPreprocess.qmd index ec36a28..b84b494 100644 --- a/bin/stPreprocess.qmd +++ b/bin/stPreprocess.qmd @@ -48,7 +48,7 @@ plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) ``` ```{python} -st_adata = sc.read(rawAdata) +st_adata = sc.read("./" + rawAdata) #st_adata.var_names_make_unique() mito = pd.read_csv(mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] @@ -112,4 +112,4 @@ sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) ```{python} st_adata.write(nameDataNorm) -``` \ No newline at end of file +``` diff --git a/bin/stSpatialDE.qmd b/bin/stSpatialDE.qmd index 9d47044..81f6c55 100644 --- a/bin/stSpatialDE.qmd +++ b/bin/stSpatialDE.qmd @@ -27,7 +27,7 @@ from matplotlib import pyplot as plt ``` ```{python} -st_adata = sc.read(fileNameST) +st_adata = sc.read("./" + fileNameST) st_adata ``` From 958fb1433a75f577270b628a61bfcc1737080ed3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 4 Apr 2023 16:55:47 +0200 Subject: [PATCH 024/410] Change names to conform to nf-core style Change file/process/subworkflow/etc. names to better conform to the nf-core style. Prepending of `ST_` or `st_` throughout many changes, to better prepare for future inclusions of single cell data. "Pre-" and "postprocessing" names has been kept as subworkflows, but includes more descriptive modules (*e.g.* `st_qc_and_normalisation`). Changed from mixed case to only `snake_case` in file names. Grouping of related modules, *e.g.* Spaceranger modules. --- ...script_read_st_data.py => read_st_data.py} | 0 bin/{stClustering.qmd => st_clustering.qmd} | 0 ...rocess.qmd => st_qc_and_normalisation.qmd} | 0 bin/{stSpatialDE.qmd => st_spatial_de.qmd} | 0 modules/local/report_all.nf | 38 ------------------- ...et.nf => spaceranger_download_probeset.nf} | 2 +- ...e.nf => spaceranger_download_reference.nf} | 2 +- modules/local/st_clustering.nf | 8 ++-- ...eprocess.nf => st_qc_and_normalisation.nf} | 19 +++++----- ...read_st_and_sc_data.nf => st_read_data.nf} | 8 ++-- modules/local/st_spatial_de.nf | 12 +++--- modules/nf-core/multiqc/main.nf | 2 +- subworkflows/local/preprocess_st_data.nf | 37 ------------------ subworkflows/local/spaceranger.nf | 8 ++-- ...st_postprocessing.nf => st_postprocess.nf} | 19 ++++------ subworkflows/local/st_preprocess.nf | 37 ++++++++++++++++++ workflows/spatialtranscriptomics.nf | 32 ++++++++-------- 17 files changed, 91 insertions(+), 133 deletions(-) rename bin/{script_read_st_data.py => read_st_data.py} (100%) rename bin/{stClustering.qmd => st_clustering.qmd} (100%) rename bin/{stPreprocess.qmd => st_qc_and_normalisation.qmd} (100%) rename bin/{stSpatialDE.qmd => st_spatial_de.qmd} (100%) delete mode 100644 modules/local/report_all.nf rename modules/local/{download_probeset.nf => spaceranger_download_probeset.nf} (95%) rename modules/local/{download_reference.nf => spaceranger_download_reference.nf} (95%) rename modules/local/{st_preprocess.nf => st_qc_and_normalisation.nf} (63%) rename modules/local/{read_st_and_sc_data.nf => st_read_data.nf} (91%) delete mode 100644 subworkflows/local/preprocess_st_data.nf rename subworkflows/local/{st_postprocessing.nf => st_postprocess.nf} (61%) create mode 100644 subworkflows/local/st_preprocess.nf diff --git a/bin/script_read_st_data.py b/bin/read_st_data.py similarity index 100% rename from bin/script_read_st_data.py rename to bin/read_st_data.py diff --git a/bin/stClustering.qmd b/bin/st_clustering.qmd similarity index 100% rename from bin/stClustering.qmd rename to bin/st_clustering.qmd diff --git a/bin/stPreprocess.qmd b/bin/st_qc_and_normalisation.qmd similarity index 100% rename from bin/stPreprocess.qmd rename to bin/st_qc_and_normalisation.qmd diff --git a/bin/stSpatialDE.qmd b/bin/st_spatial_de.qmd similarity index 100% rename from bin/stSpatialDE.qmd rename to bin/st_spatial_de.qmd diff --git a/modules/local/report_all.nf b/modules/local/report_all.nf deleted file mode 100644 index cd898d3..0000000 --- a/modules/local/report_all.nf +++ /dev/null @@ -1,38 +0,0 @@ -import groovy.json.JsonSlurper - -// -// Report -// -process REPORT_ALL { - - // TODO: Create the final report script - // TODO: Change this process to correspond to final report - // TODO: Add Conda/container directive - // TODO: Export versions - - label "process_low" - - input: - val sample_state - val outdir - - output: - tuple env(sample_id), env(outpath) - // path("versions.yml"), emit: versions - - script: - def sample_id_gr = sample_state[0] - def fileName = String.format("%s/sample_%s.json", outdir, sample_id_gr) - sample_info = new JsonSlurper().parse(new File(fileName)) - - """ - #!/bin/bash - - sample_id=${sample_id_gr} - - dname=${outdir}/\${sample_id} - - echo \${dname}/ - echo "completed" > "output.out" && outpath=`pwd`/output.out - """ -} diff --git a/modules/local/download_probeset.nf b/modules/local/spaceranger_download_probeset.nf similarity index 95% rename from modules/local/download_probeset.nf rename to modules/local/spaceranger_download_probeset.nf index 36d5908..d1ca567 100644 --- a/modules/local/download_probeset.nf +++ b/modules/local/spaceranger_download_probeset.nf @@ -2,7 +2,7 @@ // Download SpaceRanger probeset // -process DOWNLOAD_PROBESET { +process SPACERANGER_DOWNLOAD_PROBESET { tag "${name}" label "process_low" diff --git a/modules/local/download_reference.nf b/modules/local/spaceranger_download_reference.nf similarity index 95% rename from modules/local/download_reference.nf rename to modules/local/spaceranger_download_reference.nf index 8c1a623..07ad1d7 100644 --- a/modules/local/download_reference.nf +++ b/modules/local/spaceranger_download_reference.nf @@ -2,7 +2,7 @@ // Download genome reference // -process DOWNLOAD_REFERENCE { +process SPACERANGER_DOWNLOAD_REFERENCE { tag "${name}" label "process_low" diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 3a3f5aa..437f47b 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -12,18 +12,18 @@ process ST_CLUSTERING { container "cavenel/spatialtranscriptomics" input: - path(report_template_summary) + path(report) tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: tuple val(sample_id), path("*.st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(sample_id), path("*.stClustering.html") , emit: report - tuple val(sample_id), path("stClustering_files/*") , emit: report_files + tuple val(sample_id), path("*.st_clustering.html") , emit: html + tuple val(sample_id), path("st_clustering_files/*") , emit: html_files // path("versions.yml") , emit: versions script: """ - quarto render "${report_template_summary}" --output "${sample_id}.stClustering.html" \ + quarto render "${report}" --output "${sample_id}.st_clustering.html" \ -P fileNameST:${st_adata_norm} \ -P resolution:${params.Clustering_resolution} \ -P saveFileST:st_adata_processed.h5ad diff --git a/modules/local/st_preprocess.nf b/modules/local/st_qc_and_normalisation.nf similarity index 63% rename from modules/local/st_preprocess.nf rename to modules/local/st_qc_and_normalisation.nf index 893fc39..f291b96 100644 --- a/modules/local/st_preprocess.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -1,7 +1,7 @@ // // Spatial data pre-processing // -process ST_PREPROCESS { +process ST_QC_AND_NORMALISATION { // TODO: Add final Conda/container directive // TODO: Export versions @@ -12,21 +12,22 @@ process ST_PREPROCESS { container "cavenel/spatialtranscriptomics" input: - path(report_template_summary) + path(report) tuple val(sample_id), path(st_raw, stageAs: "adata_raw.h5ad") path(mito_data) output: - tuple val(sample_id), path("*.st_adata_norm.h5ad") , emit: st_data_norm - tuple val(sample_id), path("*.st_adata_plain.h5ad"), emit: st_data_plain - tuple val(sample_id), path("*.stPreprocessing.html") , emit: report - tuple val(sample_id), path("stPreprocess_files/*") , emit: report_files + tuple val(sample_id), path("*.st_adata_norm.h5ad") , emit: st_data_norm + tuple val(sample_id), path("*.st_adata_plain.h5ad") , emit: st_data_plain + tuple val(sample_id), path("*.st_qc_and_normalisation.html") , emit: html + tuple val(sample_id), path("st_qc_and_normalisation_files/*"), emit: html_files // path("versions.yml") , emit: versions script: """ - quarto render "${report_template_summary}" --output "${sample_id}.stPreprocessing.html" \ + quarto render ${report} \ + --output ${sample_id}.st_qc_and_normalisation.html \ -P rawAdata:${st_raw} \ -P mitoFile:${mito_data} \ -P pltFigSize:${params.STpreprocess_pltFigSize} \ @@ -39,7 +40,7 @@ process ST_PREPROCESS { -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad - mv st_adata_plain.h5ad "${sample_id}.st_adata_plain.h5ad" - mv st_adata_norm.h5ad "${sample_id}.st_adata_norm.h5ad" + mv st_adata_plain.h5ad ${sample_id}.st_adata_plain.h5ad + mv st_adata_norm.h5ad ${sample_id}.st_adata_norm.h5ad """ } diff --git a/modules/local/read_st_and_sc_data.nf b/modules/local/st_read_data.nf similarity index 91% rename from modules/local/read_st_and_sc_data.nf rename to modules/local/st_read_data.nf index 9af53fc..fde22e9 100644 --- a/modules/local/read_st_and_sc_data.nf +++ b/modules/local/st_read_data.nf @@ -1,7 +1,7 @@ // // Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file // -process READ_ST_AND_SC_DATA { +process ST_READ_DATA { tag "${meta.id}" label "process_low" @@ -27,9 +27,9 @@ process READ_ST_AND_SC_DATA { script: """ - script_read_st_data.py \ - --SRCountDir ./SRCount \ - --outAnnData st_adata_raw.h5ad + read_st_data.py \ + --SRCountDir ./SRCount \ + --outAnnData st_adata_raw.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index bcc39f5..fc3fcaf 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -12,25 +12,25 @@ process ST_SPATIAL_DE { container "cavenel/spatialtranscriptomics" input: - path(report_template_summary) + path(report) tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: tuple val(sample_id), path("*.csv"), emit: degs - tuple val(sample_id), path("*.stSpatialDE.html") , emit: report - tuple val(sample_id), path("stSpatialDE_files/*") , emit: report_files + tuple val(sample_id), path("*.st_spatial_de.html") , emit: html + tuple val(sample_id), path("st_spatial_de_files/*") , emit: html_files // path("versions.yml") , emit: versions script: """ - quarto render "${report_template_summary}" --output "${sample_id}.stSpatialDE.html" \ + quarto render "${report}" --output "${sample_id}.st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P numberOfColumns:${params.SpatialDE_numberOfColumns} \ -P saveDEFileName:stDE.csv \ - -P saveSpatialDEFileName:stSpatialDE.csv + -P saveSpatialDEFileName:st_spatial_de.csv # mv stDE.csv "${sample_id}.stDE.csv" - mv stSpatialDE.csv "${sample_id}.stSpatialDE.csv" + mv st_spatial_de.csv "${sample_id}.st_spatial_de.csv" """ } diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 68f66be..2bf6c80 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -13,7 +13,7 @@ process MULTIQC { path(multiqc_logo) output: - path "*multiqc_report.html", emit: report + path "*multiqc_report.html", emit: html path "*_data" , emit: data path "*_plots" , optional:true, emit: plots path "versions.yml" , emit: versions diff --git a/subworkflows/local/preprocess_st_data.nf b/subworkflows/local/preprocess_st_data.nf deleted file mode 100644 index f291d8d..0000000 --- a/subworkflows/local/preprocess_st_data.nf +++ /dev/null @@ -1,37 +0,0 @@ -// -// Pre-processing of ST data -// - -include { ST_PREPROCESS } from '../../modules/local/st_preprocess' - -workflow PREPROCESS_ST_DATA { - - take: - st_raw - mito_data - - main: - - ch_versions = Channel.empty() - - // - // Report files - // - report_template_summary = file("${projectDir}/bin/stPreprocess.qmd") - - // - // Spatial pre-processing - // - ST_PREPROCESS ( - report_template_summary, - st_raw, - mito_data - ) - // ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) - - emit: - st_data_norm = ST_PREPROCESS.out.st_data_norm // channel: [ val(sample), h5ad ] - st_data_plain = ST_PREPROCESS.out.st_data_plain // channel: [ val(sample), h5ad ] - - versions = ch_versions // channel: [ version.yml ] -} diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 5a55ca4..d1bdaf6 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -2,9 +2,9 @@ // Raw data processing with SpaceRanger // -include { DOWNLOAD_PROBESET } from '../../modules/local/download_probeset' -include { DOWNLOAD_REFERENCE } from '../../modules/local/download_reference' -include { SPACERANGER_COUNT } from '../../modules/local/spaceranger_count' +include { SPACERANGER_DOWNLOAD_PROBESET } from '../../modules/local/spaceranger_download_probeset' +include { SPACERANGER_DOWNLOAD_REFERENCE } from '../../modules/local/spaceranger_download_reference' +include { SPACERANGER_COUNT } from '../../modules/local/spaceranger_count' workflow SPACERANGER { @@ -32,7 +32,7 @@ workflow SPACERANGER { .fromPath ( params.spaceranger_reference, type: "dir", checkIfExists: true ) } else { address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-mm10-2020-A.tar.gz" - ch_reference = DOWNLOAD_REFERENCE ( address ).reference + ch_reference = SPACERANGER_SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference } // diff --git a/subworkflows/local/st_postprocessing.nf b/subworkflows/local/st_postprocess.nf similarity index 61% rename from subworkflows/local/st_postprocessing.nf rename to subworkflows/local/st_postprocess.nf index 754de03..102d513 100644 --- a/subworkflows/local/st_postprocessing.nf +++ b/subworkflows/local/st_postprocess.nf @@ -4,9 +4,8 @@ include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' include { ST_CLUSTERING } from '../../modules/local/st_clustering' -include { REPORT_ALL } from '../../modules/local/report_all' -workflow ST_POSTPROCESSING { +workflow ST_POSTPROCESS { take: st_adata_norm @@ -18,12 +17,14 @@ workflow ST_POSTPROCESSING { // // Report files // - report_template_summary_clustering = file("${projectDir}/bin/stClustering.qmd") - report_template_summary_spatial_de = file("${projectDir}/bin/stSpatialDE.qmd") + report_clustering = file("${projectDir}/bin/st_clustering.qmd") + report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") + // // Clustering + // ST_CLUSTERING ( - report_template_summary_clustering, + report_clustering, st_adata_norm ) @@ -31,16 +32,10 @@ workflow ST_POSTPROCESSING { // Spatial differential expression // ST_SPATIAL_DE ( - report_template_summary_spatial_de, + report_spatial_de, ST_CLUSTERING.out.st_adata_processed ) - // TODO: Add reporting - // - // Reporting and final outputs - // - // REPORT_ALL ( ) - emit: spatial_degs = ST_SPATIAL_DE.out.degs // channel: [ val(sample), csv ] diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf new file mode 100644 index 0000000..66f099a --- /dev/null +++ b/subworkflows/local/st_preprocess.nf @@ -0,0 +1,37 @@ +// +// Pre-processing of ST data +// + +include { ST_QC_AND_NORMALISATION } from '../../modules/local/st_qc_and_normalisation' + +workflow ST_PREPROCESS { + + take: + st_raw + mito_data + + main: + + ch_versions = Channel.empty() + + // + // Report files + // + report = file("${projectDir}/bin/st_qc_and_normalisation.qmd") + + // + // Spatial pre-processing + // + ST_QC_AND_NORMALISATION ( + report, + st_raw, + mito_data + ) + // ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) + + emit: + st_data_norm = ST_QC_AND_NORMALISATION.out.st_data_norm // channel: [ val(sample), h5ad ] + st_data_plain = ST_QC_AND_NORMALISATION.out.st_data_plain // channel: [ val(sample), h5ad ] + + versions = ch_versions // channel: [ version.yml ] +} diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 162135d..e31b2f0 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -43,15 +43,15 @@ ch_multiqc_custom_methods_description = params.multiqc_methods_description ? fil // // MODULE: Loaded from modules/local/ // -include { READ_ST_AND_SC_DATA } from '../modules/local/read_st_and_sc_data' +include { ST_READ_DATA } from '../modules/local/st_read_data' // // SUBWORKFLOW: Consisting of a mix of local and nf-core/modules // -include { INPUT_CHECK } from '../subworkflows/local/input_check' -include { PREPROCESS_ST_DATA } from '../subworkflows/local/preprocess_st_data' -include { SPACERANGER } from '../subworkflows/local/spaceranger' -include { ST_POSTPROCESSING } from '../subworkflows/local/st_postprocessing' +include { INPUT_CHECK } from '../subworkflows/local/input_check' +include { SPACERANGER } from '../subworkflows/local/spaceranger' +include { ST_PREPROCESS } from '../subworkflows/local/st_preprocess' +include { ST_POSTPROCESS } from '../subworkflows/local/st_postprocess' /* ================================================================================ @@ -103,12 +103,12 @@ workflow ST { } // - // MODULE: Read ST and SC data and save as `anndata` + // MODULE: Read ST data and save as `anndata` // - READ_ST_AND_SC_DATA ( + ST_READ_DATA ( ch_st_data ) - ch_versions = ch_versions.mix(READ_ST_AND_SC_DATA.out.versions) + ch_versions = ch_versions.mix(ST_READ_DATA.out.versions) // TODO: Add file manifest or other non-hard-coded path // @@ -120,19 +120,19 @@ workflow ST { // // SUBWORKFLOW: Pre-processing of ST data // - PREPROCESS_ST_DATA ( - READ_ST_AND_SC_DATA.out.st_raw, + ST_PREPROCESS ( + ST_READ_DATA.out.st_raw, ch_mito_data ) - ch_versions = ch_versions.mix(PREPROCESS_ST_DATA.out.versions) + ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) // // SUBWORKFLOW (optional): Pre-processing of SC data // if ( params.single_cell ) { PREPROCESS_SC_DATA ( - READ_ST_AND_SC_DATA.out.sc_counts, - READ_ST_AND_SC_DATA.out.sc_raw, + ST_READ_DATA.out.sc_counts, + ST_READ_DATA.out.sc_raw, ch_mito_data ) ch_versions = ch_versions.mix(PREPROCESS_SC_DATA.out.versions) @@ -141,10 +141,10 @@ workflow ST { // // SUBWORKFLOW: Post-processing and reporting // - ST_POSTPROCESSING ( - PREPROCESS_ST_DATA.out.st_data_norm + ST_POSTPROCESS ( + ST_PREPROCESS.out.st_data_norm ) - ch_versions = ch_versions.mix(ST_POSTPROCESSING.out.versions) + ch_versions = ch_versions.mix(ST_POSTPROCESS.out.versions) } /* From 7f9a8a75a10d3fdc03a2afab90778ac4d6d14767 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 4 Apr 2023 17:02:47 +0200 Subject: [PATCH 025/410] Remove old and unused single-cell parts --- nextflow.config | 3 --- nextflow_schema.json | 5 ----- workflows/spatialtranscriptomics.nf | 12 ------------ 3 files changed, 20 deletions(-) diff --git a/nextflow.config b/nextflow.config index 2b56dbc..a82b4ba 100644 --- a/nextflow.config +++ b/nextflow.config @@ -21,9 +21,6 @@ params { spaceranger_probeset = null spaceranger_manual_alignment = null - // Single cell options - single_cell = false - // References genome = null igenomes_base = 's3://ngi-igenomes/igenomes' diff --git a/nextflow_schema.json b/nextflow_schema.json index c1bbec6..8ade396 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -28,11 +28,6 @@ "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", "fa_icon": "fas fa-folder-open" }, - "single_cell": { - "type": "boolean", - "description": "Also run analyses dependent on additional single cell data.", - "default": "false" - }, "run_spaceranger": { "type": "boolean", "description": "Run SpaceRanger on input data", diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index e31b2f0..566c93c 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -126,18 +126,6 @@ workflow ST { ) ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) - // - // SUBWORKFLOW (optional): Pre-processing of SC data - // - if ( params.single_cell ) { - PREPROCESS_SC_DATA ( - ST_READ_DATA.out.sc_counts, - ST_READ_DATA.out.sc_raw, - ch_mito_data - ) - ch_versions = ch_versions.mix(PREPROCESS_SC_DATA.out.versions) - } - // // SUBWORKFLOW: Post-processing and reporting // From 4da8d5a0555a9ad6a2825b66fd48a796578f3824 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 4 Apr 2023 17:10:09 +0200 Subject: [PATCH 026/410] Remove unused modules multiqc and fastqc --- modules.json | 10 ------ modules/nf-core/fastqc/main.nf | 51 ----------------------------- modules/nf-core/fastqc/meta.yml | 52 ------------------------------ modules/nf-core/multiqc/main.nf | 53 ------------------------------ modules/nf-core/multiqc/meta.yml | 55 -------------------------------- 5 files changed, 221 deletions(-) delete mode 100644 modules/nf-core/fastqc/main.nf delete mode 100644 modules/nf-core/fastqc/meta.yml delete mode 100644 modules/nf-core/multiqc/main.nf delete mode 100644 modules/nf-core/multiqc/meta.yml diff --git a/modules.json b/modules.json index 54fbbe3..bea5984 100644 --- a/modules.json +++ b/modules.json @@ -9,16 +9,6 @@ "branch": "master", "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", "installed_by": ["modules"] - }, - "fastqc": { - "branch": "master", - "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", - "installed_by": ["modules"] - }, - "multiqc": { - "branch": "master", - "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", - "installed_by": ["modules"] } } } diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf deleted file mode 100644 index 9ae5838..0000000 --- a/modules/nf-core/fastqc/main.nf +++ /dev/null @@ -1,51 +0,0 @@ -process FASTQC { - tag "$meta.id" - label 'process_medium' - - conda "bioconda::fastqc=0.11.9" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/fastqc:0.11.9--0' : - 'quay.io/biocontainers/fastqc:0.11.9--0' }" - - input: - tuple val(meta), path(reads) - - output: - tuple val(meta), path("*.html"), emit: html - tuple val(meta), path("*.zip") , emit: zip - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - // Make list of old name and new name pairs to use for renaming in the bash while loop - def old_new_pairs = reads instanceof Path || reads.size() == 1 ? [[ reads, "${prefix}.${reads.extension}" ]] : reads.withIndex().collect { entry, index -> [ entry, "${prefix}_${index + 1}.${entry.extension}" ] } - def rename_to = old_new_pairs*.join(' ').join(' ') - def renamed_files = old_new_pairs.collect{ old_name, new_name -> new_name }.join(' ') - """ - printf "%s %s\\n" $rename_to | while read old_name new_name; do - [ -f "\${new_name}" ] || ln -s \$old_name \$new_name - done - fastqc $args --threads $task.cpus $renamed_files - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - fastqc: \$( fastqc --version | sed -e "s/FastQC v//g" ) - END_VERSIONS - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - """ - touch ${prefix}.html - touch ${prefix}.zip - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - fastqc: \$( fastqc --version | sed -e "s/FastQC v//g" ) - END_VERSIONS - """ -} diff --git a/modules/nf-core/fastqc/meta.yml b/modules/nf-core/fastqc/meta.yml deleted file mode 100644 index 4da5bb5..0000000 --- a/modules/nf-core/fastqc/meta.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: fastqc -description: Run FastQC on sequenced reads -keywords: - - quality control - - qc - - adapters - - fastq -tools: - - fastqc: - description: | - FastQC gives general quality metrics about your reads. - It provides information about the quality score distribution - across your reads, the per base sequence content (%A/C/G/T). - You get information about adapter contamination and other - overrepresented sequences. - homepage: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/ - documentation: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/ - licence: ["GPL-2.0-only"] -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. -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}" - - zip: - type: file - description: FastQC report archive - pattern: "*_{fastqc.zip}" - - versions: - type: file - description: File containing software versions - pattern: "versions.yml" -authors: - - "@drpatelh" - - "@grst" - - "@ewels" - - "@FelixKrueger" diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf deleted file mode 100644 index 2bf6c80..0000000 --- a/modules/nf-core/multiqc/main.nf +++ /dev/null @@ -1,53 +0,0 @@ -process MULTIQC { - label 'process_single' - - conda "bioconda::multiqc=1.13" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.13--pyhdfd78af_0' : - 'quay.io/biocontainers/multiqc:1.13--pyhdfd78af_0' }" - - input: - path multiqc_files, stageAs: "?/*" - path(multiqc_config) - path(extra_multiqc_config) - path(multiqc_logo) - - output: - path "*multiqc_report.html", emit: html - path "*_data" , emit: data - path "*_plots" , optional:true, emit: plots - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def args = task.ext.args ?: '' - def config = multiqc_config ? "--config $multiqc_config" : '' - def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' - """ - multiqc \\ - --force \\ - $args \\ - $config \\ - $extra_config \\ - . - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) - END_VERSIONS - """ - - stub: - """ - touch multiqc_data - touch multiqc_plots - touch multiqc_report.html - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) - END_VERSIONS - """ -} diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml deleted file mode 100644 index ebc29b2..0000000 --- a/modules/nf-core/multiqc/meta.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: MultiQC -description: Aggregate results from bioinformatics analyses across many samples into a single report -keywords: - - QC - - bioinformatics tools - - Beautiful stand-alone HTML report -tools: - - multiqc: - description: | - MultiQC searches a given directory for analysis logs and compiles a HTML report. - It's a general use tool, perfect for summarising the output from numerous bioinformatics tools. - homepage: https://multiqc.info/ - documentation: https://multiqc.info/docs/ - licence: ["GPL-3.0-or-later"] - -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}" - -output: - - report: - type: file - description: MultiQC report file - pattern: "multiqc_report.html" - - data: - type: dir - description: MultiQC data dir - pattern: "multiqc_data" - - plots: - type: file - description: Plots created by MultiQC - pattern: "*_data" - - versions: - type: file - description: File containing software versions - pattern: "versions.yml" -authors: - - "@abhi18av" - - "@bunop" - - "@drpatelh" - - "@jfy133" From 445f48e852d340c992b69c40b9c40aa2fac9cd05 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 4 Apr 2023 17:15:05 +0200 Subject: [PATCH 027/410] Minor formatting --- modules/local/st_clustering.nf | 9 +++++---- modules/local/st_qc_and_normalisation.nf | 3 +-- modules/local/st_spatial_de.nf | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 437f47b..065b1aa 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -17,13 +17,14 @@ process ST_CLUSTERING { output: tuple val(sample_id), path("*.st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(sample_id), path("*.st_clustering.html") , emit: html - tuple val(sample_id), path("st_clustering_files/*") , emit: html_files - // path("versions.yml") , emit: versions + tuple val(sample_id), path("*.st_clustering.html") , emit: html + tuple val(sample_id), path("st_clustering_files/*") , emit: html_files + // path("versions.yml") , emit: versions script: """ - quarto render "${report}" --output "${sample_id}.st_clustering.html" \ + quarto render "${report}" \ + --output "${sample_id}.st_clustering.html" \ -P fileNameST:${st_adata_norm} \ -P resolution:${params.Clustering_resolution} \ -P saveFileST:st_adata_processed.h5ad diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index f291b96..942cdad 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -21,8 +21,7 @@ process ST_QC_AND_NORMALISATION { tuple val(sample_id), path("*.st_adata_plain.h5ad") , emit: st_data_plain tuple val(sample_id), path("*.st_qc_and_normalisation.html") , emit: html tuple val(sample_id), path("st_qc_and_normalisation_files/*"), emit: html_files - - // path("versions.yml") , emit: versions + // path("versions.yml") , emit: versions script: """ diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index fc3fcaf..13311d1 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -24,13 +24,13 @@ process ST_SPATIAL_DE { script: """ - quarto render "${report}" --output "${sample_id}.st_spatial_de.html" \ + quarto render "${report}" \ + --output "${sample_id}.st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P numberOfColumns:${params.SpatialDE_numberOfColumns} \ -P saveDEFileName:stDE.csv \ -P saveSpatialDEFileName:st_spatial_de.csv - # mv stDE.csv "${sample_id}.stDE.csv" mv st_spatial_de.csv "${sample_id}.st_spatial_de.csv" """ } From faf5009aa47df99ceb6586b4044a8e1fc46462ed Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 5 Apr 2023 09:42:39 +0200 Subject: [PATCH 028/410] Change parameter names --- conf/analysis.config | 29 ++++++++++++++---------- modules/local/st_clustering.nf | 2 +- modules/local/st_qc_and_normalisation.nf | 14 ++++++------ modules/local/st_spatial_de.nf | 2 +- nextflow_schema.json | 24 ++++++++++---------- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/conf/analysis.config b/conf/analysis.config index e041190..fe5f124 100644 --- a/conf/analysis.config +++ b/conf/analysis.config @@ -5,19 +5,24 @@ Default config options // TODO : add bool flags params { - STload_minCounts = 1 - STload_minCells = 1 - STpreprocess_pltFigSize = 6 - STpreprocess_minCounts = 500 - STpreprocess_minGenes = 250 - STpreprocess_minCells = 1 - STpreprocess_histplotQCmaxTotalCounts= 10000 - STpreprocess_histplotQCminGeneCounts= 4000 - STpreprocess_histplotQCbins = 40 + // Data loading + st_load_min_counts = 1 + st_load_min_cells = 1 - SpatialDE_plotTopHVG = 15 - SpatialDE_numberOfColumns = 5 + // Preprocessing, QC and normalisation + st_preprocess_fig_size = 6 + st_preprocess_min_counts = 500 + st_preprocess_min_genes = 250 + st_preprocess_min_cells = 1 + st_preprocess_hist_qc_max_total_counts = 10000 + st_preprocess_hist_qc_min_gene_counts = 4000 + st_preprocess_hist_qc_bins = 40 - Clustering_resolution = 1 + // Clustering + st_cluster_resolution = 1 + + // Spatial differential expression + st_spatial_de_top_hgv = 15 + st_spatial_de_ncols = 5 } diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 065b1aa..66edb8f 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -26,7 +26,7 @@ process ST_CLUSTERING { quarto render "${report}" \ --output "${sample_id}.st_clustering.html" \ -P fileNameST:${st_adata_norm} \ - -P resolution:${params.Clustering_resolution} \ + -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad mv st_adata_processed.h5ad "${sample_id}.st_adata_processed.h5ad" diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 942cdad..b158a86 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -29,13 +29,13 @@ process ST_QC_AND_NORMALISATION { --output ${sample_id}.st_qc_and_normalisation.html \ -P rawAdata:${st_raw} \ -P mitoFile:${mito_data} \ - -P pltFigSize:${params.STpreprocess_pltFigSize} \ - -P minCounts:${params.STpreprocess_minCounts} \ - -P minGenes:${params.STpreprocess_minGenes} \ - -P minCells:${params.STpreprocess_minCells} \ - -P histplotQCmaxTotalCounts:${params.STpreprocess_histplotQCmaxTotalCounts} \ - -P histplotQCminGeneCounts:${params.STpreprocess_histplotQCminGeneCounts} \ - -P histplotQCbins:${params.STpreprocess_histplotQCbins} \ + -P pltFigSize:${params.st_preprocess_fig_size} \ + -P minCounts:${params.st_preprocess_min_counts} \ + -P minGenes:${params.st_preprocess_min_genes} \ + -P minCells:${params.st_preprocess_min_cells} \ + -P histplotQCmaxTotalCounts:${params.st_preprocess_hist_qc_max_total_counts} \ + -P histplotQCminGeneCounts:${params.st_preprocess_hist_qc_min_gene_counts} \ + -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 13311d1..aec65c5 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -27,7 +27,7 @@ process ST_SPATIAL_DE { quarto render "${report}" \ --output "${sample_id}.st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ - -P numberOfColumns:${params.SpatialDE_numberOfColumns} \ + -P numberOfColumns:${params.st_spatial_de_ncols} \ -P saveDEFileName:stDE.csv \ -P saveSpatialDEFileName:st_spatial_de.csv diff --git a/nextflow_schema.json b/nextflow_schema.json index 8ade396..9d7f224 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -126,62 +126,62 @@ "description": "Define options for each tool in the pipeline", "default": "", "properties": { - "STload_minCounts": { + "st_load_min_counts": { "type": "integer", "default": 1, "description": "Minimum genes count" }, - "STload_minCells": { + "st_load_min_cells": { "type": "integer", "default": 1, "description": "Minimum cells count" }, - "STpreprocess_pltFigSize": { + "st_preprocess_fig_size": { "type": "integer", "default": 6, "description": "Figure size, inches" }, - "STpreprocess_minCounts": { + "st_preprocess_min_counts": { "type": "integer", "default": 500, "description": "Minimum UMI count" }, - "STpreprocess_minGenes": { + "st_preprocess_min_genes": { "type": "integer", "default": 250, "description": "Minimum genes count" }, - "STpreprocess_minCells": { + "st_preprocess_min_cells": { "type": "integer", "default": 1, "description": "Minimum cells count" }, - "STpreprocess_histplotQCmaxTotalCounts": { + "st_preprocess_hist_qc_max_total_counts": { "type": "integer", "default": 10000, "description": "Max total counts cutoff for histogram QC plot" }, - "STpreprocess_histplotQCminGeneCounts": { + "st_preprocess_hist_qc_min_gene_counts": { "type": "integer", "default": 4000, "description": "Min total gene counts cutoff for histogram QC plot" }, - "STpreprocess_histplotQCbins": { + "st_preprocess_hist_qc_bins": { "type": "integer", "default": 40, "description": "Histogram QC plot number of bins" }, - "SpatialDE_plotTopHVG": { + "st_spatial_de_top_hgv": { "type": "integer", "default": 15, "description": "Number of top highly variable genes to plot" }, - "SpatialDE_numberOfColumns": { + "st_spatial_de_ncols": { "type": "integer", "default": 5, "description": "Number of columns to group genes plots into" }, - "Clustering_resolution": { + "st_cluster_resolution": { "type": "number", "default": 0.4, "description": "Clustering resolution for ST spots" From db47cbdac2bf3175f0992874251667df52052fe5 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 5 Apr 2023 15:19:46 +0200 Subject: [PATCH 029/410] Fix prettier warning --- env/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/environment.yml b/env/environment.yml index 186f34c..a50904e 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -17,4 +17,4 @@ dependencies: - jupyter=1.0.0 - pip=23.0.1 - pip: - - SpatialDE==1.1.3 + - SpatialDE==1.1.3 From 064ac4e6dd2ca40167123fbab7f4c1958f219ad3 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 5 Apr 2023 15:32:50 +0200 Subject: [PATCH 030/410] Fix pandas error in SpatialDE when running on a pandas dataset Use `.to_numpy()` to convert the pandas dataset into a numpy array before processing by SpatialDE. --- bin/st_spatial_de.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 81f6c55..4d20780 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -54,7 +54,7 @@ First, we convert normalized counts and coordinates to pandas dataframe, needed ```{python} counts = pd.DataFrame(st_adata.X.todense(), columns=st_adata.var_names, index=st_adata.obs_names) -coord = pd.DataFrame(st_adata.obsm['spatial'], columns=['x_coord', 'y_coord'], index=st_adata.obs_names) +coord = pd.DataFrame(st_adata.obsm['spatial'], columns=['x_coord', 'y_coord'], index=st_adata.obs_names).to_numpy() results = SpatialDE.run(coord, counts) ``` From 36b9cdd328e40d67bedd80514ba5af01c7983016 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Tue, 11 Apr 2023 15:15:45 +0200 Subject: [PATCH 031/410] Make output of modules saved into subdirectory with sample id name --- bin/st_clustering.qmd | 3 ++- bin/st_qc_and_normalisation.qmd | 3 ++- bin/st_spatial_de.qmd | 3 ++- modules/local/st_clustering.nf | 11 ++++++----- modules/local/st_qc_and_normalisation.nf | 15 ++++++++------- modules/local/st_spatial_de.nf | 15 +++++++++------ 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index f790d20..d552a4b 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -2,8 +2,9 @@ title: "Clustering Spatial Transcriptomics data" format: html: + embed-resources: true + page-layout: full code-fold: true - embed-resources: false jupyter: python3 --- diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index b84b494..7d26a94 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -2,8 +2,9 @@ title: "Pre-processing and filtering of Spatial Transcriptomics data" format: html: + embed-resources: true + page-layout: full code-fold: true - embed-resources: false jupyter: python3 --- diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 4d20780..3d149e4 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -2,8 +2,9 @@ title: "Clustering Spatial Transcriptomics data" format: html: + embed-resources: true + page-layout: full code-fold: true - embed-resources: false jupyter: python3 --- diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 66edb8f..c384dd0 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -16,19 +16,20 @@ process ST_CLUSTERING { tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample_id), path("*.st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(sample_id), path("*.st_clustering.html") , emit: html - tuple val(sample_id), path("st_clustering_files/*") , emit: html_files + tuple val(sample_id), path("*/st_adata_processed.h5ad"), emit: st_adata_processed + tuple val(sample_id), path("*/st_clustering.html") , emit: html // path("versions.yml") , emit: versions script: """ quarto render "${report}" \ - --output "${sample_id}.st_clustering.html" \ + --output "st_clustering.html" \ -P fileNameST:${st_adata_norm} \ -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad - mv st_adata_processed.h5ad "${sample_id}.st_adata_processed.h5ad" + mkdir "${sample_id}" -p + mv st_adata_processed.h5ad "${sample_id}/st_adata_processed.h5ad" + mv st_clustering.html "${sample_id}/st_clustering.html" """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index b158a86..e8f73f1 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -17,16 +17,15 @@ process ST_QC_AND_NORMALISATION { path(mito_data) output: - tuple val(sample_id), path("*.st_adata_norm.h5ad") , emit: st_data_norm - tuple val(sample_id), path("*.st_adata_plain.h5ad") , emit: st_data_plain - tuple val(sample_id), path("*.st_qc_and_normalisation.html") , emit: html - tuple val(sample_id), path("st_qc_and_normalisation_files/*"), emit: html_files + tuple val(sample_id), path("*/st_adata_norm.h5ad") , emit: st_data_norm + tuple val(sample_id), path("*/st_adata_plain.h5ad") , emit: st_data_plain + tuple val(sample_id), path("*/st_qc_and_normalisation.html") , emit: html // path("versions.yml") , emit: versions script: """ quarto render ${report} \ - --output ${sample_id}.st_qc_and_normalisation.html \ + --output st_qc_and_normalisation.html \ -P rawAdata:${st_raw} \ -P mitoFile:${mito_data} \ -P pltFigSize:${params.st_preprocess_fig_size} \ @@ -39,7 +38,9 @@ process ST_QC_AND_NORMALISATION { -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad - mv st_adata_plain.h5ad ${sample_id}.st_adata_plain.h5ad - mv st_adata_norm.h5ad ${sample_id}.st_adata_norm.h5ad + mkdir "${sample_id}" -p + mv st_adata_plain.h5ad ${sample_id}/st_adata_plain.h5ad + mv st_adata_norm.h5ad ${sample_id}/st_adata_norm.h5ad + mv st_qc_and_normalisation.html "${sample_id}/st_qc_and_normalisation.html" """ } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index aec65c5..5f07b1b 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -16,21 +16,24 @@ process ST_SPATIAL_DE { tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample_id), path("*.csv"), emit: degs - tuple val(sample_id), path("*.st_spatial_de.html") , emit: html - tuple val(sample_id), path("st_spatial_de_files/*") , emit: html_files + tuple val(sample_id), path("*/*.csv"), emit: degs + tuple val(sample_id), path("*/st_spatial_de.html") , emit: html // path("versions.yml") , emit: versions script: """ quarto render "${report}" \ - --output "${sample_id}.st_spatial_de.html" \ + --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P numberOfColumns:${params.st_spatial_de_ncols} \ - -P saveDEFileName:stDE.csv \ + -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv - mv st_spatial_de.csv "${sample_id}.st_spatial_de.csv" + mkdir "${sample_id}" -p + + # mv st_gde.csv "${sample_id}/st_gde.csv" + mv st_spatial_de.csv "${sample_id}/st_spatial_de.csv" + mv st_spatial_de.html "${sample_id}/st_spatial_de.html" """ } From 660e818d66a27e29967ed7f7e21718af2175de36 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Thu, 13 Apr 2023 16:08:42 +0200 Subject: [PATCH 032/410] Remove embed-resources option for quarto output --- bin/st_clustering.qmd | 2 +- bin/st_qc_and_normalisation.qmd | 2 +- bin/st_spatial_de.qmd | 2 +- modules/local/st_clustering.nf | 4 +++- modules/local/st_qc_and_normalisation.nf | 10 ++++++---- modules/local/st_spatial_de.nf | 6 ++++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index d552a4b..2cac4b4 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -2,7 +2,7 @@ title: "Clustering Spatial Transcriptomics data" format: html: - embed-resources: true + embed-resources: false page-layout: full code-fold: true jupyter: python3 diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 7d26a94..b55a04f 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -2,7 +2,7 @@ title: "Pre-processing and filtering of Spatial Transcriptomics data" format: html: - embed-resources: true + embed-resources: false page-layout: full code-fold: true jupyter: python3 diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 3d149e4..8fa2ccc 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -2,7 +2,7 @@ title: "Clustering Spatial Transcriptomics data" format: html: - embed-resources: true + embed-resources: false page-layout: full code-fold: true jupyter: python3 diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index c384dd0..71707b4 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -18,7 +18,8 @@ process ST_CLUSTERING { output: tuple val(sample_id), path("*/st_adata_processed.h5ad"), emit: st_adata_processed tuple val(sample_id), path("*/st_clustering.html") , emit: html - // path("versions.yml") , emit: versions + tuple val(sample_id), path("*/st_clustering_files/*") , emit: html_files + // path("versions.yml") , emit: versions script: """ @@ -31,5 +32,6 @@ process ST_CLUSTERING { mkdir "${sample_id}" -p mv st_adata_processed.h5ad "${sample_id}/st_adata_processed.h5ad" mv st_clustering.html "${sample_id}/st_clustering.html" + mv st_clustering_files/ "${sample_id}/st_clustering_files/" """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index e8f73f1..eb13b63 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -17,10 +17,11 @@ process ST_QC_AND_NORMALISATION { path(mito_data) output: - tuple val(sample_id), path("*/st_adata_norm.h5ad") , emit: st_data_norm - tuple val(sample_id), path("*/st_adata_plain.h5ad") , emit: st_data_plain - tuple val(sample_id), path("*/st_qc_and_normalisation.html") , emit: html - // path("versions.yml") , emit: versions + tuple val(sample_id), path("*/st_adata_norm.h5ad") , emit: st_data_norm + tuple val(sample_id), path("*/st_adata_plain.h5ad") , emit: st_data_plain + tuple val(sample_id), path("*/st_qc_and_normalisation.html") , emit: html + tuple val(sample_id), path("*/st_qc_and_normalisation_files/*"), emit: html_files + // path("versions.yml") , emit: versions script: """ @@ -42,5 +43,6 @@ process ST_QC_AND_NORMALISATION { mv st_adata_plain.h5ad ${sample_id}/st_adata_plain.h5ad mv st_adata_norm.h5ad ${sample_id}/st_adata_norm.h5ad mv st_qc_and_normalisation.html "${sample_id}/st_qc_and_normalisation.html" + mv st_qc_and_normalisation_files/ "${sample_id}/st_qc_and_normalisation_files/" """ } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index ffbb78a..73cd511 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -17,9 +17,10 @@ process ST_SPATIAL_DE { output: tuple val(sample_id), path("*/*.csv"), emit: degs - tuple val(sample_id), path("*/st_spatial_de.html") , emit: html + tuple val(sample_id), path("*/st_spatial_de.html") , emit: html + tuple val(sample_id), path("*/st_spatial_de_files/*"), emit: html_files - // path("versions.yml") , emit: versions + // path("versions.yml") , emit: versions script: """ @@ -35,5 +36,6 @@ process ST_SPATIAL_DE { # mv st_gde.csv "${sample_id}/st_gde.csv" mv st_spatial_de.csv "${sample_id}/st_spatial_de.csv" mv st_spatial_de.html "${sample_id}/st_spatial_de.html" + mv st_spatial_de_files/ "${sample_id}/st_spatial_de_files/" """ } From a3899deccfb8ade9c108914eb3b74af141777acd Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Thu, 13 Apr 2023 16:09:07 +0200 Subject: [PATCH 033/410] Fix spatial_de title --- bin/st_spatial_de.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 8fa2ccc..21f29c0 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -1,5 +1,5 @@ --- -title: "Clustering Spatial Transcriptomics data" +title: "Differential Gene Expression and spatially variable genes" format: html: embed-resources: false From a4567f18b93dc72fc82c89ed92acde286bdf21af Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 14 Apr 2023 10:27:27 +0200 Subject: [PATCH 034/410] Fix variable naming; use `meta.id` instead Fix variable and output directory naming by explicitly referencing the `id` field of the input channel contents. Also change to use `meta.id` instead of *e.g.* `sample_id.id`. --- modules/local/st_clustering.nf | 15 ++++++++------- modules/local/st_qc_and_normalisation.nf | 20 ++++++++++---------- modules/local/st_spatial_de.nf | 17 ++++++++--------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index c384dd0..5a6e6b5 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -7,18 +7,19 @@ process ST_CLUSTERING { // TODO: Add proper Conda/container directive // TODO: Export versions + tag "${meta.id}" label "process_low" container "cavenel/spatialtranscriptomics" input: path(report) - tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") + tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample_id), path("*/st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(sample_id), path("*/st_clustering.html") , emit: html - // path("versions.yml") , emit: versions + tuple val(meta), path("*/st_adata_processed.h5ad"), emit: st_adata_processed + tuple val(meta), path("*/st_clustering.html") , emit: html + // path("versions.yml") , emit: versions script: """ @@ -28,8 +29,8 @@ process ST_CLUSTERING { -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad - mkdir "${sample_id}" -p - mv st_adata_processed.h5ad "${sample_id}/st_adata_processed.h5ad" - mv st_clustering.html "${sample_id}/st_clustering.html" + mkdir "${meta.id}" -p + mv st_adata_processed.h5ad "${meta.id}/st_adata_processed.h5ad" + mv st_clustering.html "${meta.id}/st_clustering.html" """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index e8f73f1..c37e90d 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -6,21 +6,21 @@ process ST_QC_AND_NORMALISATION { // TODO: Add final Conda/container directive // TODO: Export versions - tag "${sample_id}" + tag "${meta.id}" label "process_low" container "cavenel/spatialtranscriptomics" input: path(report) - tuple val(sample_id), path(st_raw, stageAs: "adata_raw.h5ad") + tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") path(mito_data) output: - tuple val(sample_id), path("*/st_adata_norm.h5ad") , emit: st_data_norm - tuple val(sample_id), path("*/st_adata_plain.h5ad") , emit: st_data_plain - tuple val(sample_id), path("*/st_qc_and_normalisation.html") , emit: html - // path("versions.yml") , emit: versions + tuple val(meta), path("*/st_adata_norm.h5ad") , emit: st_data_norm + tuple val(meta), path("*/st_adata_plain.h5ad") , emit: st_data_plain + tuple val(meta), path("*/st_qc_and_normalisation.html") , emit: html + // path("versions.yml") , emit: versions script: """ @@ -38,9 +38,9 @@ process ST_QC_AND_NORMALISATION { -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad - mkdir "${sample_id}" -p - mv st_adata_plain.h5ad ${sample_id}/st_adata_plain.h5ad - mv st_adata_norm.h5ad ${sample_id}/st_adata_norm.h5ad - mv st_qc_and_normalisation.html "${sample_id}/st_qc_and_normalisation.html" + mkdir "${meta.id}" -p + mv st_adata_plain.h5ad ${meta.id}/st_adata_plain.h5ad + mv st_adata_norm.h5ad ${meta.id}/st_adata_norm.h5ad + mv st_qc_and_normalisation.html "${meta.id}/st_qc_and_normalisation.html" """ } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index ffbb78a..a25aa29 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -6,18 +6,18 @@ process ST_SPATIAL_DE { // TODO: Add proper Conda/container directive // TODO: Export versions - tag "${sample_id}" + tag "${meta.id}" label "process_medium" container "cavenel/spatialtranscriptomics" input: path(report) - tuple val(sample_id), path(st_adata_norm, stageAs: "adata_norm.h5ad") + tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(sample_id), path("*/*.csv"), emit: degs - tuple val(sample_id), path("*/st_spatial_de.html") , emit: html + tuple val(meta), path("*/*.csv"), emit: degs + tuple val(meta), path("*/st_spatial_de.html") , emit: html // path("versions.yml") , emit: versions @@ -30,10 +30,9 @@ process ST_SPATIAL_DE { -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv - mkdir "${sample_id}" -p - - # mv st_gde.csv "${sample_id}/st_gde.csv" - mv st_spatial_de.csv "${sample_id}/st_spatial_de.csv" - mv st_spatial_de.html "${sample_id}/st_spatial_de.html" + mkdir "${meta.id}" -p + # mv st_gde.csv "${meta.id}/st_gde.csv" + mv st_spatial_de.csv "${meta.id}/st_spatial_de.csv" + mv st_spatial_de.html "${meta.id}/st_spatial_de.html" """ } From 77194b81cf23a3d5f0df58630b7ace948533e03b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 14 Apr 2023 10:52:30 +0200 Subject: [PATCH 035/410] Remove `tracedir` parameter --- nextflow.config | 9 ++++----- nextflow_schema.json | 7 ------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/nextflow.config b/nextflow.config index a82b4ba..745e92b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -35,7 +35,6 @@ params { // Boilerplate options outdir = null - tracedir = "${params.outdir}/pipeline_info" publish_dir_mode = 'copy' email = null email_on_fail = null @@ -181,19 +180,19 @@ process.shell = ['/bin/bash', '-euo', 'pipefail'] def trace_timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') timeline { enabled = true - file = "${params.tracedir}/execution_timeline_${trace_timestamp}.html" + file = "${params.outdir}/pipeline_info/execution_timeline_${trace_timestamp}.html" } report { enabled = true - file = "${params.tracedir}/execution_report_${trace_timestamp}.html" + file = "${params.outdir}/pipeline_info/execution_report_${trace_timestamp}.html" } trace { enabled = true - file = "${params.tracedir}/execution_trace_${trace_timestamp}.txt" + file = "${params.outdir}/pipeline_info/execution_trace_${trace_timestamp}.txt" } dag { enabled = true - file = "${params.tracedir}/pipeline_dag_${trace_timestamp}.html" + file = "${params.outdir}/pipeline_info/pipeline_dag_${trace_timestamp}.html" } manifest { diff --git a/nextflow_schema.json b/nextflow_schema.json index 9d7f224..6c99293 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -354,13 +354,6 @@ "description": "Custom MultiQC yaml file containing HTML including a methods description.", "fa_icon": "fas fa-cog" }, - "tracedir": { - "type": "string", - "description": "Directory to keep pipeline Nextflow logs and reports.", - "default": "${params.outdir}/pipeline_info", - "fa_icon": "fas fa-cogs", - "hidden": true - }, "validate_params": { "type": "boolean", "description": "Boolean whether to validate parameters against the schema at runtime", From 7a07bde2d2272df1eb3b0adc3a8093ed49c8619a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 14 Apr 2023 10:52:41 +0200 Subject: [PATCH 036/410] Minor formatting --- modules/local/st_clustering.nf | 8 ++++---- modules/local/st_qc_and_normalisation.nf | 4 ++-- modules/local/st_read_data.nf | 2 +- modules/local/st_spatial_de.nf | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 5a6e6b5..4b51392 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -23,14 +23,14 @@ process ST_CLUSTERING { script: """ - quarto render "${report}" \ + quarto render ${report} \ --output "st_clustering.html" \ -P fileNameST:${st_adata_norm} \ -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad - mkdir "${meta.id}" -p - mv st_adata_processed.h5ad "${meta.id}/st_adata_processed.h5ad" - mv st_clustering.html "${meta.id}/st_clustering.html" + mkdir -p ${meta.id} + mv st_adata_processed.h5ad ${meta.id}/st_adata_processed.h5ad + mv st_clustering.html ${meta.id}/st_clustering.html """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index c37e90d..1fbd69b 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -38,9 +38,9 @@ process ST_QC_AND_NORMALISATION { -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad - mkdir "${meta.id}" -p + mkdir -p ${meta.id} mv st_adata_plain.h5ad ${meta.id}/st_adata_plain.h5ad mv st_adata_norm.h5ad ${meta.id}/st_adata_norm.h5ad - mv st_qc_and_normalisation.html "${meta.id}/st_qc_and_normalisation.html" + mv st_qc_and_normalisation.html ${meta.id}/st_qc_and_normalisation.html """ } diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index fde22e9..4808781 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -12,7 +12,7 @@ process ST_READ_DATA { 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" input: - tuple val (meta), + tuple val (meta), path (tissue_positions_list, stageAs: "SRCount/spatial/tissue_positions_list.csv"), path (tissue_lowres_image , stageAs: "SRCount/spatial/tissue_lowres_image.png"), path (tissue_hires_image , stageAs: "SRCount/spatial/tissue_hires_image.png"), diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index a25aa29..48b69d1 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -23,16 +23,16 @@ process ST_SPATIAL_DE { script: """ - quarto render "${report}" \ + quarto render ${report} \ --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P numberOfColumns:${params.st_spatial_de_ncols} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv - mkdir "${meta.id}" -p - # mv st_gde.csv "${meta.id}/st_gde.csv" - mv st_spatial_de.csv "${meta.id}/st_spatial_de.csv" - mv st_spatial_de.html "${meta.id}/st_spatial_de.html" + mkdir -p ${meta.id} + # mv st_gde.csv ${meta.id}/st_gde.csv + mv st_spatial_de.csv ${meta.id}/st_spatial_de.csv + mv st_spatial_de.html ${meta.id}/st_spatial_de.html """ } From 37e67101337413e816783fa827778e30e6ee4161 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 14 Apr 2023 12:13:34 +0200 Subject: [PATCH 037/410] Specify output directory structure in config Specify the output directory in the config instead of manual creation of directories and file renaming inside processes themselves. --- conf/modules.config | 20 ++++++++++++++++++++ modules/local/st_clustering.nf | 11 +++-------- modules/local/st_qc_and_normalisation.nf | 16 +++++----------- modules/local/st_spatial_de.nf | 14 ++++---------- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index ef71dd5..4381fbe 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -26,4 +26,24 @@ process { ] } + withName: 'ST_READ_DATA|ST_QC_AND_NORMALISATION|ST_CLUSTERING|ST_SPATIAL_DE' { + publishDir = [ + [ + path: { "${params.outdir}/${meta.id}/reports" }, + mode: params.publish_dir_mode, + pattern: "*{.html,_files}" + ], + [ + path: { "${params.outdir}/${meta.id}/data" }, + mode: params.publish_dir_mode, + pattern: "*.h5ad" + ], + [ + path: { "${params.outdir}/${meta.id}/degs" }, + mode: params.publish_dir_mode, + pattern: "*.csv" + ] + ] + } + } diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index b1650aa..80d646e 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -17,9 +17,9 @@ process ST_CLUSTERING { tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(meta), path("*/st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(meta), path("*/st_clustering.html") , emit: html - tuple val(meta), path("*/st_clustering_files/*") , emit: html_files + tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed + tuple val(meta), path("st_clustering.html") , emit: html + tuple val(meta), path("st_clustering_files") , emit: html_files // path("versions.yml") , emit: versions script: @@ -29,10 +29,5 @@ process ST_CLUSTERING { -P fileNameST:${st_adata_norm} \ -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad - - mkdir -p ${meta.id} - mv st_adata_processed.h5ad ${meta.id}/st_adata_processed.h5ad - mv st_clustering.html ${meta.id}/st_clustering.html - mv st_clustering_files ${meta.id}/st_clustering_files/ """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 329e440..1432597 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -17,11 +17,11 @@ process ST_QC_AND_NORMALISATION { path(mito_data) output: - tuple val(meta), path("*/st_adata_norm.h5ad") , emit: st_data_norm - tuple val(meta), path("*/st_adata_plain.h5ad") , emit: st_data_plain - tuple val(meta), path("*/st_qc_and_normalisation.html") , emit: html - tuple val(meta), path("*/st_qc_and_normalisation_files/*"), emit: html_files - // path("versions.yml") , emit: versions + tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm + tuple val(meta), path("st_adata_plain.h5ad") , emit: st_data_plain + tuple val(meta), path("st_qc_and_normalisation.html") , emit: html + tuple val(meta), path("st_qc_and_normalisation_files"), emit: html_files + // path("versions.yml") , emit: versions script: """ @@ -38,11 +38,5 @@ process ST_QC_AND_NORMALISATION { -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad - - mkdir -p ${meta.id} - mv st_adata_plain.h5ad ${meta.id}/st_adata_plain.h5ad - mv st_adata_norm.h5ad ${meta.id}/st_adata_norm.h5ad - mv st_qc_and_normalisation.html ${meta.id}/st_qc_and_normalisation.html - mv st_qc_and_normalisation_files/ ${meta.id}/st_qc_and_normalisation_files/ """ } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 5493b40..abfa244 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -16,11 +16,10 @@ process ST_SPATIAL_DE { tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(meta), path("*/*.csv") , emit: degs - tuple val(meta), path("*/st_spatial_de.html") , emit: html - tuple val(meta), path("*/st_spatial_de_files/*"), emit: html_files - - // path("versions.yml") , emit: versions + tuple val(meta), path("*.csv") , emit: degs + tuple val(meta), path("st_spatial_de.html") , emit: html + tuple val(meta), path("st_spatial_de_files"), emit: html_files + // path("versions.yml") , emit: versions script: """ @@ -30,10 +29,5 @@ process ST_SPATIAL_DE { -P numberOfColumns:${params.st_spatial_de_ncols} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv - - mkdir -p ${meta.id} - mv st_spatial_de.csv ${meta.id}/st_spatial_de.csv - mv st_spatial_de.html ${meta.id}/st_spatial_de.html - mv st_spatial_de_files/ ${meta.id}/st_spatial_de_files/ """ } From dc5afd9790d362313b00e3471657f7879c61b9a3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 17 Apr 2023 12:57:32 +0200 Subject: [PATCH 038/410] Move `read_visium_mtx` function to `read_st_data` Move the `read_visium_mtx` python function into the `read_st_data` script, which is the only location where it is currently used. --- bin/read_st_data.py | 153 +++++++++++++++++++++++++++++++++++---- bin/read_visium_mtx.py | 157 ----------------------------------------- 2 files changed, 140 insertions(+), 170 deletions(-) delete mode 100644 bin/read_visium_mtx.py diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 7dbac15..21f3fd0 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -2,24 +2,151 @@ # Load packages import argparse -import scanpy as sc -import numpy as np -import read_visium_mtx +import json +import pandas as pd +from anndata import AnnData +from matplotlib.image import imread +from pathlib import Path +from scanpy import read_10x_mtx +from typing import Union, Optional + + +# Function to read MTX +def read_visium_mtx( + path: Union[str, Path], + genome: Optional[str] = None, + *, + count_file: str = "filtered_feature_bc_matrix.h5", + library_id: str = None, + load_images: Optional[bool] = True, + source_image_path: Optional[Union[str, Path]] = None, +) -> AnnData: + """\ + Read 10x-Genomics-formatted visum dataset. + In addition to reading regular 10x output, + this looks for the `spatial` folder and loads images, + coordinates and scale factors. + Based on the `Space Ranger output docs`_. + See :func:`~scanpy.pl.spatial` for a compatible plotting function. + .. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview + Parameters + ---------- + path + Path to directory for visium datafiles. + genome + Filter expression to genes within this genome. + count_file + Which file in the passed directory to use as the count file. Typically would be one of: + 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. + library_id + Identifier for the visium library. Can be modified when concatenating multiple adata objects. + source_image_path + Path to the high-resolution tissue image. Path will be included in + `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. + Returns + ------- + Annotated data matrix, where observations/cells are named by their + barcode and variables/genes by gene name. Stores the following information: + :attr:`~anndata.AnnData.X` + The data matrix is stored + :attr:`~anndata.AnnData.obs_names` + Cell names + :attr:`~anndata.AnnData.var_names` + Gene names + :attr:`~anndata.AnnData.var`\\ `['gene_ids']` + Gene IDs + :attr:`~anndata.AnnData.var`\\ `['feature_types']` + Feature types + :attr:`~anndata.AnnData.uns`\\ `['spatial']` + Dict of spaceranger output files with 'library_id' as key + :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['images']` + Dict of images (`'hires'` and `'lowres'`) + :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['scalefactors']` + Scale factors for the spots + :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['metadata']` + Files metadata: 'chemistry_description', 'software_version', 'source_image_path' + :attr:`~anndata.AnnData.obsm`\\ `['spatial']` + Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. + """ + + path = Path(path) + adata = read_10x_mtx(path / "raw_feature_bc_matrix") + + adata.uns["spatial"] = dict() + + if library_id is None: + library_id = "library_id" + + adata.uns["spatial"][library_id] = dict() + + if load_images: + files = dict( + tissue_positions_file=path / "spatial/tissue_positions_list.csv", + scalefactors_json_file=path / "spatial/scalefactors_json.json", + hires_image=path / "spatial/tissue_hires_image.png", + lowres_image=path / "spatial/tissue_lowres_image.png", + ) + + # Check if files exist; continue if images are missing + for f in files.values(): + if not f.exists(): + if any(x in str(f) for x in ["hires_image", "lowres_image"]): + print("You seem to be missing an image file.") + print("Could not find '{f}'.") + else: + raise OSError(f"Could not find '{f}'") + + # Check for existance of images + adata.uns["spatial"][library_id]["images"] = dict() + for res in ["hires", "lowres"]: + try: + adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) + except Exception: + raise OSError(f"Could not find '{res}_image'") + + # Read JSON scale factors + adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) + adata.uns["spatial"][library_id]["metadata"] = {k: "NA" for k in ("chemistry_description", "software_version")} + + # Read coordinates + positions = pd.read_csv(files["tissue_positions_file"], header=None) + positions.columns = [ + "barcode", + "in_tissue", + "array_row", + "array_col", + "pxl_col_in_fullres", + "pxl_row_in_fullres", + ] + positions.index = positions["barcode"] + adata.obs = adata.obs.join(positions, how="left") + adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() + adata.obs.drop( + columns=["barcode", "pxl_row_in_fullres", "pxl_col_in_fullres"], + inplace=True, + ) + + # Put absolute image path in uns + if source_image_path is not None: + source_image_path = str(Path(source_image_path).resolve()) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) + + return adata + # Parse command-line arguments parser = argparse.ArgumentParser( - description="Load spatial transcriptomics data from MTX " + "matrices and aligned images." -) -parser.add_argument( - "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Path to Space Range output directory, etc." -) -parser.add_argument( - "--outAnnData", metavar="outAnnData", type=str, default=None, help="Path to a file to save h5ad data into." + description="Load spatial transcriptomics data from MTX " + + "matrices and aligned images." ) +parser.add_argument("--SRCountDir", metavar="SRCountDir", type=str, + default=None, help="Input directory with Spaceranger data.") +parser.add_argument("--outAnnData", metavar="outAnnData", type=str, + default=None, help="Output h5ad file path.") args = parser.parse_args() -# Main script -st_adata = read_visium_mtx.read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) +# Read Visium data +st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) -# Save raw anndata to file +# Write raw anndata to file st_adata.write(args.outAnnData) diff --git a/bin/read_visium_mtx.py b/bin/read_visium_mtx.py deleted file mode 100644 index 6c85ebf..0000000 --- a/bin/read_visium_mtx.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python - -# Load packages -import os -import argparse -import scanpy as sc - -from scanpy import read_10x_mtx -from pathlib import Path -from typing import Union, Dict, Optional -import json -import pandas as pd -from matplotlib.image import imread -import anndata -from anndata import AnnData, read_csv - - -# Function to read MTX -def read_visium_mtx( - path: Union[str, Path], - genome: Optional[str] = None, - *, - count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str = None, - load_images: Optional[bool] = True, - source_image_path: Optional[Union[str, Path]] = None, -) -> AnnData: - """\ - Read 10x-Genomics-formatted visum dataset. - In addition to reading regular 10x output, - this looks for the `spatial` folder and loads images, - coordinates and scale factors. - Based on the `Space Ranger output docs`_. - See :func:`~scanpy.pl.spatial` for a compatible plotting function. - .. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview - Parameters - ---------- - path - Path to directory for visium datafiles. - genome - Filter expression to genes within this genome. - count_file - Which file in the passed directory to use as the count file. Typically would be one of: - 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. - library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. - source_image_path - Path to the high-resolution tissue image. Path will be included in - `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. - Returns - ------- - Annotated data matrix, where observations/cells are named by their - barcode and variables/genes by gene name. Stores the following information: - :attr:`~anndata.AnnData.X` - The data matrix is stored - :attr:`~anndata.AnnData.obs_names` - Cell names - :attr:`~anndata.AnnData.var_names` - Gene names - :attr:`~anndata.AnnData.var`\\ `['gene_ids']` - Gene IDs - :attr:`~anndata.AnnData.var`\\ `['feature_types']` - Feature types - :attr:`~anndata.AnnData.uns`\\ `['spatial']` - Dict of spaceranger output files with 'library_id' as key - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['images']` - Dict of images (`'hires'` and `'lowres'`) - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['scalefactors']` - Scale factors for the spots - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['metadata']` - Files metadata: 'chemistry_description', 'software_version', 'source_image_path' - :attr:`~anndata.AnnData.obsm`\\ `['spatial']` - Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. - """ - path = Path(path) - # adata = read_10x_h5(path / count_file, genome=genome) - adata = read_10x_mtx(path / "raw_feature_bc_matrix") - - adata.uns["spatial"] = dict() - - # from h5py import File - - # with File(path / 'raw_feature_bc_matrix' / count_file, mode="r") as f: - # attrs = dict(f.attrs) - # if library_id is None: - # library_id = str(attrs.pop("library_ids")[0], "utf-8") - if library_id is None: - library_id = "library_id" - - adata.uns["spatial"][library_id] = dict() - - if load_images: - files = dict( - tissue_positions_file=path / "spatial/tissue_positions_list.csv", - scalefactors_json_file=path / "spatial/scalefactors_json.json", - hires_image=path / "spatial/tissue_hires_image.png", - lowres_image=path / "spatial/tissue_lowres_image.png", - ) - - # check if files exists, continue if images are missing - for f in files.values(): - if not f.exists(): - if any(x in str(f) for x in ["hires_image", "lowres_image"]): - print("You seem to be missing an image file.") - print("Could not find '{f}'.") - # logg.warning( - # f"You seem to be missing an image file.\n" - # f"Could not find '{f}'." - # ) - else: - raise OSError(f"Could not find '{f}'") - - adata.uns["spatial"][library_id]["images"] = dict() - for res in ["hires", "lowres"]: - try: - adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) - except Exception: - raise OSError(f"Could not find '{res}_image'") - - # read json scalefactors - adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) - - # adata.uns["spatial"][library_id]["metadata"] = { - # k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) - # for k in ("chemistry_description", "software_version") - # if k in attrs - # } - - adata.uns["spatial"][library_id]["metadata"] = {k: "NA" for k in ("chemistry_description", "software_version")} - - # read coordinates - positions = pd.read_csv(files["tissue_positions_file"], header=None) - positions.columns = [ - "barcode", - "in_tissue", - "array_row", - "array_col", - "pxl_col_in_fullres", - "pxl_row_in_fullres", - ] - positions.index = positions["barcode"] - - adata.obs = adata.obs.join(positions, how="left") - - adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() - adata.obs.drop( - columns=["barcode", "pxl_row_in_fullres", "pxl_col_in_fullres"], - inplace=True, - ) - - # put image path in uns - if source_image_path is not None: - # get an absolute path - source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) - - return adata From 24d2b5e1586998baf547dda4ebae440c2515593a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 19 Apr 2023 14:55:02 +0200 Subject: [PATCH 039/410] Add new multi-platform Docker image Add a new multi-platform Docker image that works on both AMD64 and ARM64 architectures. The image uses multi-stage builds to combine a minimal Quarto image with a Mambaforge image, followed by installation of packages using Mamba. The new image is also more optimised in terms of size: 1.33 GB (new) versus 3.37 GB (old). --- env/Dockerfile | 38 +++++++++++------------- env/environment.yml | 13 ++------ modules/local/st_clustering.nf | 5 ++-- modules/local/st_qc_and_normalisation.nf | 6 ++-- modules/local/st_spatial_de.nf | 6 ++-- 5 files changed, 28 insertions(+), 40 deletions(-) diff --git a/env/Dockerfile b/env/Dockerfile index 22471a8..e1cb40e 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -1,26 +1,24 @@ -FROM --platform=linux/amd64 nfcore/base:2.1 -LABEL \ - author = "Christophe Avenel" \ - description = "Temporary Dockerfile for nf-core/spatialtranscriptomics" \ - maintainer = "christophe.avenel@it.uu.se" +# First stage: multi-platform Quarto image +FROM jdutant/quarto-minimal:1.3.313 as quarto -RUN apt-get -y update; apt-get -y install -y --no-install-recommends \ - pandoc \ - pandoc-citeproc \ - curl \ - gdebi-core \ - && rm -rf /var/lib/apt/lists/* +# Second stage: multi-platform Mamba image +FROM condaforge/mambaforge:23.1.0-1 -# Install quarto -ARG QUARTO_VERSION="1.3.302" -RUN curl -o quarto-linux-amd64.deb -L https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb -RUN gdebi --non-interactive quarto-linux-amd64.deb +# Copy Quarto installation from first stage and add to PATH +COPY --from=quarto /opt/quarto /opt/quarto +ENV PATH="${PATH}:/opt/quarto/bin" -# Copy and install the Conda environment +# Install packages using Mamba; also remove static libraries, python bytecode +# files and javascript source maps that are not required for execution COPY environment.yml ./ -RUN conda install -c conda-forge mamba \ - && mamba env update --name base --file environment.yml \ - && mamba clean --all --force-pkgs-dirs --yes +RUN mamba env update --name base --file environment.yml \ + && mamba clean --all --force-pkgs-dirs --yes \ + && find /opt/conda -follow -type f -name '*.a' -delete \ + && find /opt/conda -follow -type f -name '*.pyc' -delete \ + && find /opt/conda -follow -type f -name '*.js.map' -delete -# Start Bash shell by default CMD /bin/bash + +LABEL \ + authors = "Erik Fasterius, Christophe Avenel" \ + description = "Dockerfile for nf-core/spatialtranscriptomics report modules" diff --git a/env/environment.yml b/env/environment.yml index a50904e..0c52f97 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -2,19 +2,10 @@ channels: - conda-forge - bioconda dependencies: - # Python packages - - bbknn=1.5.1 + - jupyter=1.0.0 - leidenalg=0.9.1 - - matplotlib=3.7.1 - - numpy=1.23.0 - - scikit-learn=1.2.2 - - scanorama=1.7.3 - - scanpy=1.9.3 - - scipy=1.10.1 - - seaborn=0.12.2 - - umap-learn=0.5.3 - papermill=2.3.4 - - jupyter=1.0.0 - pip=23.0.1 + - scanpy=1.9.3 - pip: - SpatialDE==1.1.3 diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 80d646e..b18be43 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -4,13 +4,12 @@ process ST_CLUSTERING { // TODO: Add a better description - // TODO: Add proper Conda/container directive - // TODO: Export versions + // TODO: Find solution for Quarto with Conda tag "${meta.id}" label "process_low" - container "cavenel/spatialtranscriptomics" + container "erikfas/spatialtranscriptomics" input: path(report) diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 1432597..a8a7c46 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -3,13 +3,13 @@ // process ST_QC_AND_NORMALISATION { - // TODO: Add final Conda/container directive - // TODO: Export versions + // TODO: Add a better description + // TODO: Find solution for Quarto with Conda tag "${meta.id}" label "process_low" - container "cavenel/spatialtranscriptomics" + container "erikfas/spatialtranscriptomics" input: path(report) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index abfa244..e44c4bc 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -3,13 +3,13 @@ // process ST_SPATIAL_DE { - // TODO: Add proper Conda/container directive - // TODO: Export versions + // TODO: Add a better description + // TODO: Find solution for Quarto with Conda tag "${meta.id}" label "process_medium" - container "cavenel/spatialtranscriptomics" + container "erikfas/spatialtranscriptomics" input: path(report) From baa3111d3a7882f6048695964b17246bb205830b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 19 Apr 2023 15:00:15 +0200 Subject: [PATCH 040/410] Export versions for report processes --- modules/local/st_clustering.nf | 8 +++++++- modules/local/st_qc_and_normalisation.nf | 8 +++++++- modules/local/st_spatial_de.nf | 10 +++++++++- subworkflows/local/st_postprocess.nf | 2 ++ subworkflows/local/st_preprocess.nf | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index b18be43..6c9d75a 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -19,7 +19,7 @@ process ST_CLUSTERING { tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed tuple val(meta), path("st_clustering.html") , emit: html tuple val(meta), path("st_clustering_files") , emit: html_files - // path("versions.yml") , emit: versions + path("versions.yml") , emit: versions script: """ @@ -28,5 +28,11 @@ process ST_CLUSTERING { -P fileNameST:${st_adata_norm} \ -P resolution:${params.st_cluster_resolution} \ -P saveFileST:st_adata_processed.h5ad + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + quarto: \$(quarto -v) + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS """ } diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index a8a7c46..7897ecf 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -21,7 +21,7 @@ process ST_QC_AND_NORMALISATION { tuple val(meta), path("st_adata_plain.h5ad") , emit: st_data_plain tuple val(meta), path("st_qc_and_normalisation.html") , emit: html tuple val(meta), path("st_qc_and_normalisation_files"), emit: html_files - // path("versions.yml") , emit: versions + path("versions.yml") , emit: versions script: """ @@ -38,5 +38,11 @@ process ST_QC_AND_NORMALISATION { -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + quarto: \$(quarto -v) + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS """ } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index e44c4bc..67c57da 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -19,7 +19,7 @@ process ST_SPATIAL_DE { tuple val(meta), path("*.csv") , emit: degs tuple val(meta), path("st_spatial_de.html") , emit: html tuple val(meta), path("st_spatial_de_files"), emit: html_files - // path("versions.yml") , emit: versions + path("versions.yml") , emit: versions script: """ @@ -29,5 +29,13 @@ process ST_SPATIAL_DE { -P numberOfColumns:${params.st_spatial_de_ncols} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + quarto: \$(quarto -v) + leidenalg: \$(python -c "import leidenalg; print(leidenalg.__version__)") + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + SpatialDE: \$(python -c "import SpatialDE; print(SpatialDE.__version__)") + END_VERSIONS """ } diff --git a/subworkflows/local/st_postprocess.nf b/subworkflows/local/st_postprocess.nf index 102d513..defa6d6 100644 --- a/subworkflows/local/st_postprocess.nf +++ b/subworkflows/local/st_postprocess.nf @@ -27,6 +27,7 @@ workflow ST_POSTPROCESS { report_clustering, st_adata_norm ) + ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) // // Spatial differential expression @@ -35,6 +36,7 @@ workflow ST_POSTPROCESS { report_spatial_de, ST_CLUSTERING.out.st_adata_processed ) + ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) emit: spatial_degs = ST_SPATIAL_DE.out.degs // channel: [ val(sample), csv ] diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index 66f099a..690fd68 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -27,7 +27,7 @@ workflow ST_PREPROCESS { st_raw, mito_data ) - // ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) + ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) emit: st_data_norm = ST_QC_AND_NORMALISATION.out.st_data_norm // channel: [ val(sample), h5ad ] From f6abf85af606b4ee8e902d5b6ca77321747f5375 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 May 2023 15:08:08 +0200 Subject: [PATCH 041/410] Streamline samplesheet checker Streamline the samplesheet checker; remove unused functions, add correct documentation and simplify argument parser. --- bin/check_samplesheet.py | 253 +++++---------------------------------- 1 file changed, 28 insertions(+), 225 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 491dc9c..4d3f870 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -1,170 +1,18 @@ #!/usr/bin/env python - -"""Provide a command line tool to validate and transform tabular samplesheets.""" - - import argparse -import csv import errno -import logging import os import sys -from collections import Counter -from pathlib import Path - -logger = logging.getLogger() - - -class RowChecker: - """ - Define a service that can validate and transform each given row. - - Attributes: - modified (list): A list of dicts, where each dict corresponds to a previously - validated and transformed row. The order of rows is maintained. - - """ - - VALID_FORMATS = ( - ".fq.gz", - ".fastq.gz", - ) - - def __init__( - self, - sample_col="sample", - first_col="fastq_1", - second_col="fastq_2", - single_col="single_end", - **kwargs, - ): - """ - Initialize the row checker with the expected column names. - - Args: - sample_col (str): The name of the column that contains the sample name - (default "sample"). - first_col (str): The name of the column that contains the first (or only) - FASTQ file path (default "fastq_1"). - second_col (str): The name of the column that contains the second (if any) - FASTQ file path (default "fastq_2"). - single_col (str): The name of the new column that will be inserted and - records whether the sample contains single- or paired-end sequencing - reads (default "single_end"). - - """ - super().__init__(**kwargs) - self._sample_col = sample_col - self._first_col = first_col - self._second_col = second_col - self._single_col = single_col - self._seen = set() - self.modified = [] - - def validate_and_transform(self, row): - """ - Perform all validations on the given row and insert the read pairing status. - - Args: - row (dict): A mapping from column headers (keys) to elements of that row - (values). - - """ - self._validate_sample(row) - self._validate_first(row) - self._validate_second(row) - self._validate_pair(row) - self._seen.add((row[self._sample_col], row[self._first_col])) - self.modified.append(row) - - def _validate_sample(self, row): - """Assert that the sample name exists and convert spaces to underscores.""" - if len(row[self._sample_col]) <= 0: - raise AssertionError("Sample input is required.") - # Sanitize samples slightly. - row[self._sample_col] = row[self._sample_col].replace(" ", "_") - - def _validate_first(self, row): - """Assert that the first FASTQ entry is non-empty and has the right format.""" - if len(row[self._first_col]) <= 0: - raise AssertionError("At least the first FASTQ file is required.") - self._validate_fastq_format(row[self._first_col]) - - def _validate_second(self, row): - """Assert that the second FASTQ entry has the right format if it exists.""" - if len(row[self._second_col]) > 0: - self._validate_fastq_format(row[self._second_col]) - - def _validate_pair(self, row): - """Assert that read pairs have the same file extension. Report pair status.""" - if row[self._first_col] and row[self._second_col]: - row[self._single_col] = False - first_col_suffix = Path(row[self._first_col]).suffixes[-2:] - second_col_suffix = Path(row[self._second_col]).suffixes[-2:] - if first_col_suffix != second_col_suffix: - raise AssertionError("FASTQ pairs must have the same file extensions.") - else: - row[self._single_col] = True - - def _validate_fastq_format(self, filename): - """Assert that a given filename has one of the expected FASTQ extensions.""" - if not any(filename.endswith(extension) for extension in self.VALID_FORMATS): - raise AssertionError( - f"The FASTQ file has an unrecognized extension: {filename}\n" - f"It should be one of: {', '.join(self.VALID_FORMATS)}" - ) - def validate_unique_samples(self): - """ - Assert that the combination of sample name and FASTQ filename is unique. - In addition to the validation, also rename all samples to have a suffix of _T{n}, where n is the - number of times the same sample exist, but with different FASTQ files, e.g., multiple runs per experiment. - - """ - if len(self._seen) != len(self.modified): - raise AssertionError("The pair of sample name and FASTQ must be unique.") - seen = Counter() - for row in self.modified: - sample = row[self._sample_col] - seen[sample] += 1 - row[self._sample_col] = f"{sample}_T{seen[sample]}" - - -def read_head(handle, num_lines=10): - """Read the specified number of lines from the current position in the file.""" - lines = [] - for idx, line in enumerate(handle): - if idx == num_lines: - break - lines.append(line) - return "".join(lines) - - -def sniff_format(handle): - """ - Detect the tabular format. - - Args: - handle (text file): A handle to a `text file`_ object. The read position is - expected to be at the beginning (index 0). - - Returns: - csv.Dialect: The detected tabular format. - - .. _text file: - https://docs.python.org/3/glossary.html#term-text-file - - """ - peek = read_head(handle) - handle.seek(0) - sniffer = csv.Sniffer() - if not sniffer.has_header(peek): - logger.critical("The given sample sheet does not appear to contain a header.") - sys.exit(1) - dialect = sniffer.sniff(peek) - return dialect +def parse_args(argv=None): + Description="Check contents of nf-core/spatialtranscriptomics samplesheet." + Epilog="Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" + parser = argparse.ArgumentParser(description=Description, epilog=Epilog) + parser.add_argument("FILE_IN", help="Input samplesheet file.") + parser.add_argument("FILE_OUT", help="Output validated samplesheet file.") + return parser.parse_args(argv) def make_dir(path): @@ -186,36 +34,27 @@ def print_error(error, context="Line", context_str=""): sys.exit(1) -def check_samplesheet(file_in, file_out): +def check_samplesheet(FILE_IN, FILE_OUT): """ Check that the tabular samplesheet has the structure expected by nf-core pipelines. - - Validate the general shape of the table, expected columns, and each row. Also add - an additional column which records whether one or two FASTQ reads were found. + Validate the general shape of the table, expected columns and each row. Args: - file_in (pathlib.Path): The given tabular samplesheet. The format can be either - CSV, TSV, or any other format automatically recognized by ``csv.Sniffer``. - file_out (pathlib.Path): Where the validated and transformed samplesheet should - be created; always in CSV format. + FILE_IN (pathlib.Path): The given tabular samplesheet. + FILE_OUT (pathlib.Path): Where the validated samplesheet should be created. - Example: - This function checks that the samplesheet follows the following structure, - see also the `viral recon samplesheet`_:: - - sample,fastq_1,fastq_2 - SAMPLE_PE,SAMPLE_PE_RUN1_1.fastq.gz,SAMPLE_PE_RUN1_2.fastq.gz - SAMPLE_PE,SAMPLE_PE_RUN2_1.fastq.gz,SAMPLE_PE_RUN2_2.fastq.gz - SAMPLE_SE,SAMPLE_SE_RUN1_1.fastq.gz, - - .. _viral recon samplesheet: - https://raw.githubusercontent.com/nf-core/test-datasets/viralrecon/samplesheet/samplesheet_test_illumina_amplicon.csv + The following structure is expected: + sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix + SAMPLE,TISSUE_POSITIONS_LIST.csv,TISSUE_LOWRES_IMAGE.png,TISSUE_HIGHRES_IMAGE.png,SCALEFACTORS_JSON.json,BARCODES.tsv.gz,FEATURES.tsv.gz,MATRIX.mtx.gz + For a complete example see: + https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv """ sample_mapping_dict = {} - with open(file_in, "r") as fin: - ## Check header + with open(FILE_IN, "r") as fin: + + # Check header MIN_COLS = 7 HEADER = [ "sample", @@ -232,7 +71,7 @@ def check_samplesheet(file_in, file_out): print("ERROR: Please check samplesheet header -> {} != {}".format(",".join(header), ",".join(HEADER))) sys.exit(1) - ## Check sample entries + # Check sample entries for line in fin: lspl = [x.strip().strip('"') for x in line.strip().split(",")] @@ -251,7 +90,7 @@ def check_samplesheet(file_in, file_out): line, ) - ## Check sample name entries + # Check sample name entries ( sample, tissue_positions_list, @@ -266,7 +105,7 @@ def check_samplesheet(file_in, file_out): if not sample: print_error("Sample entry has not been specified!", "Line", line) - ## Auto-detect paired-end/single-end + # Create sample mapping dictionary sample_info = [ tissue_positions_list, tissue_lowres_image, @@ -275,9 +114,7 @@ def check_samplesheet(file_in, file_out): barcodes, features, matrix, - ] ## [single_end, fastq_1, fastq_2] - - ## Create sample mapping dictionary = { sample: [ single_end, fastq_1, fastq_2 ] } + ] if sample not in sample_mapping_dict: sample_mapping_dict[sample] = [sample_info] else: @@ -286,11 +123,11 @@ def check_samplesheet(file_in, file_out): else: sample_mapping_dict[sample].append(sample_info) - ## Write validated samplesheet with appropriate columns + # Write validated samplesheet with appropriate columns if len(sample_mapping_dict) > 0: - out_dir = os.path.dirname(file_out) + out_dir = os.path.dirname(FILE_OUT) make_dir(out_dir) - with open(file_out, "w") as fout: + with open(FILE_OUT, "w") as fout: fout.write( ",".join( [ @@ -310,46 +147,12 @@ def check_samplesheet(file_in, file_out): for idx, val in enumerate(sample_mapping_dict[sample]): fout.write(",".join(["{}_T{}".format(sample, idx + 1)] + val) + "\n") else: - print_error("No entries to process!", "Samplesheet: {}".format(file_in)) - - -def parse_args(argv=None): - """Define and immediately parse command line arguments.""" - parser = argparse.ArgumentParser( - description="Validate and transform a tabular samplesheet.", - epilog="Example: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv", - ) - parser.add_argument( - "file_in", - metavar="FILE_IN", - type=Path, - help="Tabular input samplesheet in CSV or TSV format.", - ) - parser.add_argument( - "file_out", - metavar="FILE_OUT", - type=Path, - help="Transformed output samplesheet in CSV format.", - ) - parser.add_argument( - "-l", - "--log-level", - help="The desired log level (default WARNING).", - choices=("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"), - default="WARNING", - ) - return parser.parse_args(argv) + print_error("No entries to process!", "Samplesheet: {}".format(FILE_IN)) def main(argv=None): - """Coordinate argument parsing and program execution.""" args = parse_args(argv) - logging.basicConfig(level=args.log_level, format="[%(levelname)s] %(message)s") - if not args.file_in.is_file(): - logger.error(f"The given input file {args.file_in} was not found!") - sys.exit(2) - args.file_out.parent.mkdir(parents=True, exist_ok=True) - check_samplesheet(args.file_in, args.file_out) + check_samplesheet(args.FILE_IN, args.FILE_OUT) if __name__ == "__main__": From efefeaffbb3e752101c784aad22d72f1f806720b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 9 May 2023 14:42:46 +0200 Subject: [PATCH 042/410] Ignore `result*/` directories --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e785fa7..dda44c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .nextflow* work/ data/ -results/ +results*/ .DS_Store testing/ testing* From ac14e75da3491380964d8d354197b58917867560 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 9 May 2023 14:48:16 +0200 Subject: [PATCH 043/410] Use a single samplesheet for all input data types Use a single samplesheet for all types of input data, *e.g.* for both raw data to be processed by SpaceRanger as well as for spatial data already gone through SpaceRanger processing; specification of data type is done with the `--run_spaceranger` flag (default: false). Add checks for raw data samplesheets. --- bin/check_samplesheet.py | 130 ++++++++++++++++------------ conf/modules.config | 7 ++ modules/local/samplesheet_check.nf | 6 +- modules/local/spaceranger_count.nf | 4 +- subworkflows/local/input_check.nf | 50 +++++++++-- subworkflows/local/spaceranger.nf | 33 +------ workflows/spatialtranscriptomics.nf | 7 +- 7 files changed, 132 insertions(+), 105 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 4d3f870..be67599 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -10,8 +10,9 @@ def parse_args(argv=None): Description="Check contents of nf-core/spatialtranscriptomics samplesheet." Epilog="Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" parser = argparse.ArgumentParser(description=Description, epilog=Epilog) - parser.add_argument("FILE_IN", help="Input samplesheet file.") - parser.add_argument("FILE_OUT", help="Output validated samplesheet file.") + parser.add_argument("file_in", help="Input samplesheet file.") + parser.add_argument("file_out", help="Output validated samplesheet file.") + parser.add_argument("--is_raw_data", action="store_true", help="Whether input is raw data to be processed by SpaceRanger.") return parser.parse_args(argv) @@ -34,38 +35,52 @@ def print_error(error, context="Line", context_str=""): sys.exit(1) -def check_samplesheet(FILE_IN, FILE_OUT): +def check_samplesheet(file_in, file_out, is_raw_data): """ Check that the tabular samplesheet has the structure expected by nf-core pipelines. Validate the general shape of the table, expected columns and each row. Args: - FILE_IN (pathlib.Path): The given tabular samplesheet. - FILE_OUT (pathlib.Path): Where the validated samplesheet should be created. + file_in (pathlib.Path): The given tabular samplesheet. + file_out (pathlib.Path): Where the validated samplesheet should be created. + is_raw_data (boolean): Whether the input is raw spatial data to be processed by SpaceRanger. The following structure is expected: sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix SAMPLE,TISSUE_POSITIONS_LIST.csv,TISSUE_LOWRES_IMAGE.png,TISSUE_HIGHRES_IMAGE.png,SCALEFACTORS_JSON.json,BARCODES.tsv.gz,FEATURES.tsv.gz,MATRIX.mtx.gz For a complete example see: - https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv + https://data.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv """ + sample_mapping_dict = {} - with open(FILE_IN, "r") as fin: + with open(file_in, "r") as fin: + + # Get cols and header depending on samplesheet type + if is_raw_data: + MIN_COLS = 4 + HEADER = [ + "sample", + "fastq_dir", + "tissue_hires_image", + "slide", + "area" + ] + else: + MIN_COLS = 7 + HEADER = [ + "sample", + "tissue_positions_list", + "tissue_lowres_image", + "tissue_hires_image", + "scale_factors", + "barcodes", + "features", + "matrix", + ] # Check header - MIN_COLS = 7 - HEADER = [ - "sample", - "tissue_positions_list", - "tissue_lowres_image", - "tissue_hires_image", - "scale_factors", - "barcodes", - "features", - "matrix", - ] header = [x.strip('"') for x in fin.readline().strip().split(",")] if header[: len(HEADER)] != HEADER: print("ERROR: Please check samplesheet header -> {} != {}".format(",".join(header), ",".join(HEADER))) @@ -91,30 +106,45 @@ def check_samplesheet(FILE_IN, FILE_OUT): ) # Check sample name entries - ( - sample, - tissue_positions_list, - tissue_lowres_image, - tissue_hires_image, - scale_factors, - barcodes, - features, - matrix, - ) = lspl[: len(HEADER)] + if is_raw_data: + ( + sample, + fastq_dir, + tissue_hires_image, + slide, + area, + ) = lspl[: len(HEADER)] + sample_info = [ + fastq_dir, + tissue_hires_image, + slide, + area, + ] + else: + ( + sample, + tissue_positions_list, + tissue_lowres_image, + tissue_hires_image, + scale_factors, + barcodes, + features, + matrix, + ) = lspl[: len(HEADER)] + sample_info = [ + tissue_positions_list, + tissue_lowres_image, + tissue_hires_image, + scale_factors, + barcodes, + features, + matrix, + ] sample = sample.replace(" ", "_") if not sample: print_error("Sample entry has not been specified!", "Line", line) # Create sample mapping dictionary - sample_info = [ - tissue_positions_list, - tissue_lowres_image, - tissue_hires_image, - scale_factors, - barcodes, - features, - matrix, - ] if sample not in sample_mapping_dict: sample_mapping_dict[sample] = [sample_info] else: @@ -125,34 +155,20 @@ def check_samplesheet(FILE_IN, FILE_OUT): # Write validated samplesheet with appropriate columns if len(sample_mapping_dict) > 0: - out_dir = os.path.dirname(FILE_OUT) + out_dir = os.path.dirname(file_out) make_dir(out_dir) - with open(FILE_OUT, "w") as fout: - fout.write( - ",".join( - [ - "sample", - "tissue_positions_list", - "tissue_lowres_image", - "tissue_hires_image", - "scale_factors", - "barcodes", - "features", - "matrix", - ] - ) - + "\n" - ) + with open(file_out, "w") as fout: + fout.write(",".join(HEADER) + "\n") for sample in sorted(sample_mapping_dict.keys()): for idx, val in enumerate(sample_mapping_dict[sample]): - fout.write(",".join(["{}_T{}".format(sample, idx + 1)] + val) + "\n") + fout.write(",".join(["{}".format(sample, idx + 1)] + val) + "\n") else: - print_error("No entries to process!", "Samplesheet: {}".format(FILE_IN)) + print_error("No entries to process!", "Samplesheet: {}".format(file_in)) def main(argv=None): args = parse_args(argv) - check_samplesheet(args.FILE_IN, args.FILE_OUT) + check_samplesheet(args.file_in, args.file_out, args.is_raw_data) if __name__ == "__main__": diff --git a/conf/modules.config b/conf/modules.config index 4381fbe..3211507 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -26,6 +26,13 @@ process { ] } + withName: SPACERANGER_COUNT { + publishDir = [ + path: { "${params.outdir}/${meta.id}" }, + mode: params.publish_dir_mode + ] + } + withName: 'ST_READ_DATA|ST_QC_AND_NORMALISATION|ST_CLUSTERING|ST_SPATIAL_DE' { publishDir = [ [ diff --git a/modules/local/samplesheet_check.nf b/modules/local/samplesheet_check.nf index f3234bf..36f9749 100644 --- a/modules/local/samplesheet_check.nf +++ b/modules/local/samplesheet_check.nf @@ -19,10 +19,12 @@ process SAMPLESHEET_CHECK { task.ext.when == null || task.ext.when script: // This script is bundled with the pipeline, in nf-core/spatialtranscriptomics/bin/ + def is_raw_data = params.run_spaceranger ? '--is_raw_data' : '' """ check_samplesheet.py \\ - $samplesheet \\ - samplesheet.valid.csv + ${samplesheet} \\ + samplesheet.valid.csv \\ + ${is_raw_data} cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index f26482e..25ec03c 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -16,7 +16,7 @@ process SPACERANGER_COUNT { path(manual_alignment) output: - path "spaceranger-${meta.id}", type: "dir" , emit: sr_dir + path "spaceranger", type: "dir" , emit: sr_dir tuple val (meta), path ("*/outs/spatial/tissue_positions_list.csv"), path ("*/outs/spatial/tissue_lowres_image.png"), @@ -32,7 +32,7 @@ process SPACERANGER_COUNT { def manual_alignment = manual_alignment.name != 'EMPTY_ALIGNMENT' ? "--loupe-alignment=${manual_alignment}" : '' """ spaceranger count \ - --id=spaceranger-${meta.id} \ + --id=spaceranger \ --sample=${meta.id} \ --fastqs=${fastq_dir} \ --image=${image} \ diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index ff76df2..56ccbd7 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -11,16 +11,43 @@ workflow INPUT_CHECK { main: SAMPLESHEET_CHECK ( samplesheet ) - .csv - .splitCsv ( header: true, sep: ',' ) - .map { create_visium_channels(it) } - .set { reads } + if ( params.run_spaceranger ) { + st_data = SAMPLESHEET_CHECK.out.csv + .splitCsv ( header: true, sep: ',' ) + .map { create_spaceranger_channels(it) } + } else { + st_data = SAMPLESHEET_CHECK.out.csv + .splitCsv ( header: true, sep: ',' ) + .map { create_visium_channels(it) } + } emit: - reads // channel: [ val(meta), [ reads ] ] + st_data // channel: [ val(meta), [ st data ] ] versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } +// Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] +def create_spaceranger_channels(LinkedHashMap row) { + def meta = [:] + meta.id = row.sample + + def array = [] + if (!file(row.fastq_dir).exists()) { + exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_1}" + } + if (!file(row.tissue_hires_image).exists()) { + exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.fastq_1}" + } + array = [ + meta, + file(row.fastq_dir), + file(row.tissue_hires_image), + row.slide, + row.area, + ] + return array +} + // Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ // scale_factors, barcodes, features, matrix ] ] def create_visium_channels(LinkedHashMap row) { @@ -49,10 +76,15 @@ def create_visium_channels(LinkedHashMap row) { if (!file(row.matrix).exists()) { exit 1, "ERROR: Please check input samplesheet -> matrix file does not exist!\n${row.fastq_1}" } - array = [ meta, - file(row.tissue_positions_list), file(row.tissue_lowres_image), file(row.tissue_hires_image), - file(row.scale_factors), file(row.barcodes), - file(row.features), file(row.matrix) + array = [ + meta, + file(row.tissue_positions_list), + file(row.tissue_lowres_image), + file(row.tissue_hires_image), + file(row.scale_factors), + file(row.barcodes), + file(row.features), + file(row.matrix) ] return array } diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index d1bdaf6..71820e5 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -9,20 +9,12 @@ include { SPACERANGER_COUNT } from '../../modules/local/spaceranger workflow SPACERANGER { take: - samplesheet // file: path/to/samplesheet.csv + ch_st_data // channel: [ val(meta), [ raw st data ] ] main: ch_versions = Channel.empty() - // - // Read input samplesheet - // - ch_input = Channel - .fromPath ( samplesheet ) - .splitCsv ( header: true, sep: ',' ) - .map { create_spaceranger_channels(it) } - // // Reference files // @@ -61,7 +53,7 @@ workflow SPACERANGER { // Run SpaceRanger count // SPACERANGER_COUNT ( - ch_input, + ch_st_data, ch_reference, ch_probeset, ch_manual_alignment @@ -74,24 +66,3 @@ workflow SPACERANGER { versions = ch_versions // channel: [ versions.yml ] } - -def create_spaceranger_channels(LinkedHashMap row) { - def meta = [:] - meta.id = row.sample - - def array = [] - if (!file(row.fastq_dir).exists()) { - exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_1}" - } - if (!file(row.tissue_hires_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.fastq_1}" - } - array = [ - meta, - file(row.fastq_dir), - file(row.tissue_hires_image), - row.slide, - row.area - ] - return array -} diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 566c93c..f08ea2c 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -78,11 +78,10 @@ def multiqc_report = [] // workflow ST { - // TODO: Collect versions for all modules/subworkflows ch_versions = Channel.empty() // - // SUBWORKFLOW: Read in samplesheet, validate and stage input files + // SUBWORKFLOW: Read and validate samplesheet // INPUT_CHECK ( ch_input @@ -94,12 +93,12 @@ workflow ST { // if ( params.run_spaceranger ) { SPACERANGER ( - params.spaceranger_input + INPUT_CHECK.out.st_data ) ch_st_data = SPACERANGER.out.sr_out ch_versions = ch_versions.mix(SPACERANGER.out.versions) } else { - ch_st_data = INPUT_CHECK.out.reads + ch_st_data = INPUT_CHECK.out.st_data } // From 4016082b70f347d80eacc726957403d0ceecdd07 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 9 May 2023 18:00:26 +0200 Subject: [PATCH 044/410] Update pipeline introduction text --- README.md | 66 ++++++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 599618f..27c8083 100644 --- a/README.md +++ b/README.md @@ -12,44 +12,36 @@ ## Introduction - - -**nf-core/spatialtranscriptomics** is a bioinformatics best-practice analysis pipeline for Spatial Transcriptomics. - -The pipeline is built using [Nextflow](https://www.nextflow.io), a workflow tool to run tasks across multiple compute infrastructures in a very portable manner. It uses Docker/Singularity containers making installation trivial and results highly reproducible. The [Nextflow DSL2](https://www.nextflow.io/docs/latest/dsl2.html) implementation of this pipeline uses one container per process which makes it much easier to maintain and update software dependencies. Where possible, these processes have been submitted to and installed from [nf-core/modules](https://github.com/nf-core/modules) in order to make them available to all nf-core pipelines, and to everyone within the Nextflow community! +**nf-core/spatialtranscriptomics** is a bioinformatics analysis pipeline for +Spatial Transcriptomics. It can process and analyse 10X spatial data either +directly from raw data by running [SpaceRanger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) +or data already processed by SpaceRanger. The pipeline currently consists of the +following steps: + +0. Raw data processing with SpaceRanger (optional) +1. Quality controls and filtering +2. Normalisation +3. Dimensionality reduction and clustering +4. Differential gene expression testing + +The pipeline is built using [Nextflow](https://www.nextflow.io), a workflow tool +to run tasks across multiple compute infrastructures in a very portable manner. +It uses Docker/Singularity containers making installation trivial and results +highly reproducible. The [Nextflow DSL2](https://www.nextflow.io/docs/latest/dsl2.html) +implementation of this pipeline uses one container per process which makes it +much easier to maintain and update software dependencies. Where possible, these +processes have been submitted to and installed from [nf-core/modules](https://github.com/nf-core/modules) +in order to make them available to all nf-core pipelines, and to everyone within +the Nextflow community! -On release, automated continuous integration tests run the pipeline on a full-sized dataset on the AWS cloud infrastructure. This ensures that the pipeline runs on AWS, has sensible resource allocation defaults set to run on real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources.The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialtranscriptomics/results). - -## Pipeline summary - -1. Normalization and Quality Control ([`scran`](https://doi.org/doi:10.18129/B9.bioc.scran), [`scanpy`](https://github.com/theislab/scanpy)) -2. Spots cell types and cell topics deconvolution ([`STdeconvolve`](https://jef.works/STdeconvolve/), [`SPOTlight`](https://github.com/MarcElosua/SPOTlight)) -3. Spot resolution enhancement ([`BayesSpace`](https://github.com/edward130603/BayesSpace)) -4. Dimensionality reduction and projection ([`scanpy`](https://github.com/theislab/scanpy), [`Seurat`](https://satijalab.org/seurat/)) -5. Integration with scRNA-seq data ([`scanorama`](https://github.com/brianhie/scanorama), [`BBKNN`](https://github.com/Teichlab/bbknn)) -6. Label transfer from scRNA-seq data ([`Seurat`](https://satijalab.org/seurat/)) -7. Clustering of the spots ([`scanpy Leiden`](https://arxiv.org/abs/1810.08473), [`BayesSpace`](https://github.com/edward130603/BayesSpace)) -8. Visualization of clusters and features in spatial coordinates and 2D projection layout ([`scanpy`](https://github.com/theislab/scanpy), [`Seurat`](https://satijalab.org/seurat/)) -9. Identification of spatially variable features ([`SpatialDE`](https://github.com/Teichlab/SpatialDE)) - -The pipeline combines multiple tools, toolkits and platforms: - -- [`Bioconductor`](https://www.bioconductor.org/) - software resource for the analysis of genomic data. Based primarily on the statistical R programming language. -- [`Seurat`](https://satijalab.org/seurat/) - R toolkit for single cell genomics. -- [`scran`](https://doi.org/doi:10.18129/B9.bioc.scran) - R package implements miscellaneous functions for analysis and interpretation of single-cell RNA-seq data. -- [`SpatialExperiment`](https://doi.org/doi:10.18129/B9.bioc.SpatialExperiment) - R package for storing, retrieving spatial coordinates and gene expression. -- [`Giotto`](https://rubd.github.io/Giotto_site/) - (R package) a toolbox for integrative analysis and visualization of spatial expression data. -- [`reticulate`](https://github.com/rstudio/reticulate/) - comprehensive set of tools for interoperability between Python and R. -- [`STdeconvolve`](https://jef.works/STdeconvolve/) - R implementation of LDA-based cell-topics deconvolution of spots. -- [`SPOTlight`](https://github.com/MarcElosua/SPOTlight) - R implementation of NMF-based cell-types deconvolution of spots. -- [`BayesSpace`](https://github.com/edward130603/BayesSpace) - R package for spatially-aware clustering and resolution enhancement. -- [`SpatialDE`](https://github.com/Teichlab/SpatialDE) - Python package for identification of spatially variable genes. -- [`scanorama`](https://github.com/brianhie/scanorama) - Python package that enables batch-correction and integration of heterogeneous scRNA-seq datasets. -- [`BBKNN`](https://github.com/Teichlab/bbknn) - (Python package) batch effect removal tool. -- [`scanpy`](https://github.com/theislab/scanpy) - scalable Python toolkit for analyzing single-cell gene expression data. -- [`anndata`](https://github.com/theislab/anndata) - a Python package for handling annotated data matrices in memory and on disk. +On release, automated continuous integration tests run the pipeline on a +full-sized dataset on the AWS cloud infrastructure. This ensures that the +pipeline runs on AWS, has sensible resource allocation defaults set to run on +real-world datasets, and permits the persistent storage of results to benchmark +between pipeline releases and other analysis sources. The results obtained from +the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialtranscriptomics/results). ## Quick Start @@ -57,6 +49,8 @@ The pipeline combines multiple tools, toolkits and platforms: 2. Install any of [`Docker`](https://docs.docker.com/engine/installation/), [`Singularity`](https://www.sylabs.io/guides/3.0/user-guide/) (you can follow [this tutorial](https://singularity-tutorial.github.io/01-installation/)), [`Podman`](https://podman.io/), [`Shifter`](https://nersc.gitlab.io/development/shifter/how-to-use/) or [`Charliecloud`](https://hpc.github.io/charliecloud/) for full pipeline reproducibility _(you can use [`Conda`](https://conda.io/miniconda.html) both to install Nextflow itself and also to manage software within pipelines. Please only use it within pipelines as a last resort; see [docs](https://nf-co.re/usage/configuration#basic-configuration-profiles))_. + + 3. Download the pipeline and test it on a minimal dataset with a single command: > - The pipeline comes with config profiles called `docker`, `singularity`, `podman`, `shifter`, `charliecloud` and `conda` which instruct the pipeline to use the named tool for software management. For example, `-profile test,docker`. @@ -66,8 +60,6 @@ The pipeline combines multiple tools, toolkits and platforms: 4. Start running your own analysis! - - ```bash nextflow run nf-core/spatialtranscriptomics -profile --input samplesheet.csv ``` From ef7db18cae5b30b0cb4e829287786eaf43819536 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 10 May 2023 07:48:53 +0200 Subject: [PATCH 045/410] Use `^MT-` in names to finding mitochondrial genes Use the prefix `^MT-` in gene names for finding mitochondrial genes instead of MitoCarta, as it is a more standard procedure in both Python- and R-based single cell/spatial workflows. --- bin/st_qc_and_normalisation.qmd | 8 ++------ modules/local/st_qc_and_normalisation.nf | 2 -- subworkflows/local/st_preprocess.nf | 4 +--- workflows/spatialtranscriptomics.nf | 10 +--------- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index b55a04f..b3d32cc 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -17,7 +17,6 @@ resolution = 1 saveFileST = None rawAdata = None #Name of the h5ad file -mitoFile = None #Path and name of the mito file pltFigSize = 6 #Figure size minCounts = 500 #Min counts per spot minGenes = 250 #Min genes per spot @@ -50,12 +49,9 @@ plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) ```{python} st_adata = sc.read("./" + rawAdata) -#st_adata.var_names_make_unique() -mito = pd.read_csv(mitoFile, index_col=["Symbol", "MCARTA2_LIST"], delimiter="\t")["EnsemblGeneID"] -mito = mito.xs(1, level="MCARTA2_LIST").sort_index().reset_index() - -st_adata.var["mt"] = st_adata.var_names.isin(mito["Symbol"]) +# Get mitochondrial percentages +st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt"], inplace=True) ``` diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 7897ecf..e33cb0e 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -14,7 +14,6 @@ process ST_QC_AND_NORMALISATION { input: path(report) tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") - path(mito_data) output: tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm @@ -28,7 +27,6 @@ process ST_QC_AND_NORMALISATION { quarto render ${report} \ --output st_qc_and_normalisation.html \ -P rawAdata:${st_raw} \ - -P mitoFile:${mito_data} \ -P pltFigSize:${params.st_preprocess_fig_size} \ -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index 690fd68..3827392 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -8,7 +8,6 @@ workflow ST_PREPROCESS { take: st_raw - mito_data main: @@ -24,8 +23,7 @@ workflow ST_PREPROCESS { // ST_QC_AND_NORMALISATION ( report, - st_raw, - mito_data + st_raw ) ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index f08ea2c..e509e78 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -109,19 +109,11 @@ workflow ST { ) ch_versions = ch_versions.mix(ST_READ_DATA.out.versions) - // TODO: Add file manifest or other non-hard-coded path - // - // Channel for mitochondrial data - // - ch_mito_data = Channel - .fromPath("ftp://ftp.broadinstitute.org/distribution/metabolic/papers/Pagliarini/MitoCarta2.0/Human.MitoCarta2.0.txt") - // // SUBWORKFLOW: Pre-processing of ST data // ST_PREPROCESS ( - ST_READ_DATA.out.st_raw, - ch_mito_data + ST_READ_DATA.out.st_raw ) ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) From e8e7e6600f3ab46638a1da227fda436a493e3ed5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 10 May 2023 08:09:24 +0200 Subject: [PATCH 046/410] Fix Black linting --- bin/check_samplesheet.py | 18 ++++++------------ bin/read_st_data.py | 11 ++++------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index be67599..dbb2ff0 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -7,12 +7,14 @@ def parse_args(argv=None): - Description="Check contents of nf-core/spatialtranscriptomics samplesheet." - Epilog="Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" + Description = "Check contents of nf-core/spatialtranscriptomics samplesheet." + Epilog = "Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" parser = argparse.ArgumentParser(description=Description, epilog=Epilog) parser.add_argument("file_in", help="Input samplesheet file.") parser.add_argument("file_out", help="Output validated samplesheet file.") - parser.add_argument("--is_raw_data", action="store_true", help="Whether input is raw data to be processed by SpaceRanger.") + parser.add_argument( + "--is_raw_data", action="store_true", help="Whether input is raw data to be processed by SpaceRanger." + ) return parser.parse_args(argv) @@ -53,20 +55,12 @@ def check_samplesheet(file_in, file_out, is_raw_data): https://data.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv """ - sample_mapping_dict = {} with open(file_in, "r") as fin: - # Get cols and header depending on samplesheet type if is_raw_data: MIN_COLS = 4 - HEADER = [ - "sample", - "fastq_dir", - "tissue_hires_image", - "slide", - "area" - ] + HEADER = ["sample", "fastq_dir", "tissue_hires_image", "slide", "area"] else: MIN_COLS = 7 HEADER = [ diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 21f3fd0..89381fb 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -135,14 +135,11 @@ def read_visium_mtx( # Parse command-line arguments -parser = argparse.ArgumentParser( - description="Load spatial transcriptomics data from MTX " + - "matrices and aligned images." +parser = argparse.ArgumentParser(description="Load spatial transcriptomics data from MTX matrices and aligned images.") +parser.add_argument( + "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." ) -parser.add_argument("--SRCountDir", metavar="SRCountDir", type=str, - default=None, help="Input directory with Spaceranger data.") -parser.add_argument("--outAnnData", metavar="outAnnData", type=str, - default=None, help="Output h5ad file path.") +parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") args = parser.parse_args() # Read Visium data From 835cc154f511f42bbb8bd227f066cd37ba71c407 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 10 May 2023 08:10:43 +0200 Subject: [PATCH 047/410] Revert "Ignore `result*/` directories" This reverts commit efefeaffbb3e752101c784aad22d72f1f806720b. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dda44c5..e785fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .nextflow* work/ data/ -results*/ +results/ .DS_Store testing/ testing* From a97a81626b00f8af97eca5ae371beac3992a4a7d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 11 May 2023 08:17:42 +0200 Subject: [PATCH 048/410] Update CITATIONS.md --- CITATIONS.md | 59 ++++------------------------------------------------ 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index 686f138..426e69b 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,68 +10,17 @@ ## Pipeline tools -- [Bioconductor](https://www.bioconductor.org/) - - > Huber W, Carey V, Gentleman R. et al. Orchestrating high-throughput genomic analysis with Bioconductor. Nat Methods 12, 115–121 (2015). - > Gentleman RC, Carey VJ, Bates DM et al. Bioconductor: open software development for computational biology and bioinformatics. Genome Biol 5, R80 (2004). - -- [Seurat](https://satijalab.org/seurat/) - - > Hao Y, Hao S, Andersen-Nissen E, III WMM, Zheng S, Butler A, Lee MJ, Wilk AJ, Darby C, Zagar M, Hoffman P, Stoeckius M, Papalexi E, Mimitou EP, Jain J, Srivastava A, Stuart T, Fleming LB, Yeung B, Rogers AJ, McElrath JM, Blish CA, Gottardo R, Smibert P, Satija R (2021). Integrated analysis of multimodal single-cell data. Cell. doi: 10.1016/j.cell.2021.04.048, . - > Stuart T, Butler A, Hoffman P, Hafemeister C, Papalexi E, III WMM, Hao Y, Stoeckius M, Smibert P, Satija R (2019). Comprehensive Integration of Single-Cell Data. Cell, 177, 1888-1902. doi: 10.1016/j.cell.2019.05.031, . - > Butler A, Hoffman P, Smibert P, Papalexi E, Satija R (2018). Integrating single-cell transcriptomic data across different conditions, technologies, and species. Nature Biotechnology, 36, 411-420. doi: 10.1038/nbt.4096, . - > Satija R, Farrell JA, Gennert D, Schier AF, Regev A (2015). Spatial reconstruction of single-cell gene expression data. Nature Biotechnology, 33, 495-502. doi: 10.1038/nbt.3192, . - -- [scran](https://doi.org/doi:10.18129/B9.bioc.scran) - - > Lun ATL, McCarthy DJ, Marioni JC (2016). A step-by-step workflow for low-level analysis of single-cell RNA-seq data with Bioconductor. F1000Res., 5, 2122. doi: . - -- [SpatialExperiment](https://doi.org/doi:10.18129/B9.bioc.SpatialExperiment) - - > Righelli D, Risso D, Crowell H, Weber L (2021). SpatialExperiment: S4 Class for Spatial Experiments handling. R package version 1.4.0. - -- [Giotto](https://rubd.github.io/Giotto_site/) - - > Dries R, Zhu Q, Dong R, Eng C-HL, Li H, Liu K, Fu Y, Zhao T, Sarkar A, Bao F, George RE, Pierson N, Cai L, Yuan G-C. Giotto, a toolbox for integrative analysis and visualization of spatial expression data. bioRxiv 701680 (2019). doi: . - -- [reticulate](https://github.com/rstudio/reticulate/) - - > Ushey K, Allaire J, Tang Y (2021). Reticulate: Interface to 'Python', R package version 1.20, - -- [STdeconvolve](https://jef.works/STdeconvolve/) - - > Miller BF, Huang F, Atta L, Sahoo A, Fan J. Reference-free cell-type deconvolution of multi-cellular pixel-resolution spatially resolved transcriptomics data. bioRxiv 2021.06.15.448381; doi: - -- [SPOTlight](https://github.com/MarcElosua/SPOTlight) - - > Elosua-Bayes M, Nieto P, Mereu E, Gut I, Heyn H (2021): SPOTlight: seeded NMF regression to deconvolute spatial transcriptomics spots with single-cell transcriptomes. Nucleic Acids Res 49(9):e50. doi: . - -- [BayesSpace](https://github.com/edward130603/BayesSpace) - - > Zhao E, Stone MR, Ren X, Pulliam T, Nghiem P, Bielas JH, Gottardo R (2020). BayesSpace enables the robust characterization of spatial gene expression architecture in tissue sections at increased resolution. bioRxiv. . - -- [SpatialDE](https://github.com/Teichlab/SpatialDE) - - > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). - -- [scanorama](https://github.com/brianhie/scanorama) - - > Hie B, Bryson B, Berger B. Efficient integration of heterogeneous single-cell transcriptomes using Scanorama. Nat Biotechnol 37, 685–691 (2019). - -- [BBKNN](https://github.com/Teichlab/bbknn) +- [anndata](https://github.com/theislab/anndata) - > Polański K, Young MD, Miao Z, Meyer KB, Teichmann SA, Park J-E, BBKNN: fast batch alignment of single cell transcriptomes, Bioinformatics, Volume 36, Issue 3, 1 February 2020, Pages 964–965, + > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: - [scanpy](https://github.com/theislab/scanpy) > Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). -- [anndata](https://github.com/theislab/anndata) - - > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: +- [SpatialDE](https://github.com/Teichlab/SpatialDE) -- [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: . Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. + > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). ## Software packaging/containerisation tools From 14eb244f1922c0350737a87b22f535698372f67e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 11 May 2023 08:34:45 +0200 Subject: [PATCH 049/410] Reduce number of cores of test config to 2 --- conf/test.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/test.config b/conf/test.config index 95c2110..e9a0923 100644 --- a/conf/test.config +++ b/conf/test.config @@ -15,7 +15,7 @@ params { config_profile_description = 'Minimal test dataset to check pipeline function' // Limit resources so that this can run on GitHub Actions - max_cpus = 4 + max_cpus = 2 max_memory = '6.GB' max_time = '1.h' From 3d4dd0e4ffae9f7f4e955865c34491c979566523 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 May 2023 12:49:38 +0200 Subject: [PATCH 050/410] Change default SpaceRanger species to human --- subworkflows/local/spaceranger.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 71820e5..147145d 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -23,8 +23,8 @@ workflow SPACERANGER { ch_reference = Channel .fromPath ( params.spaceranger_reference, type: "dir", checkIfExists: true ) } else { - address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-mm10-2020-A.tar.gz" - ch_reference = SPACERANGER_SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference + address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" + ch_reference = SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference } // From 72baaa7bd4f415d08f8d7aa195c8df1bc41237ab Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 May 2023 13:01:10 +0200 Subject: [PATCH 051/410] Update usage documentation --- docs/usage.md | 108 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index dfa8427..a835bef 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,41 +4,107 @@ > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ -## Introduction - -The pipeline is designed to take multiple samples to process them in parallel. Currently processing of samples is fully independent from other samples. The spatial transcriptomics input is expected to be output from [10x Visium](https://www.10xgenomics.com/products/spatial-gene-expression) technology experiment processed with [SpaceRanger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger), which includes gene-spot count matrices in either HDF5 of MTX format, and aligned images with spatial coordinates files. - ## Samplesheet input -You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 or more columns, and a header row as shown in the examples below. +You will need to create a samplesheet with information about the samples you +would like to analyse before running the pipeline. Use this parameter to specify +its location. It has to be a comma-separated file with at least 5 or 8 columns +(depending on input data type, [see below](#raw-spatial-data)), and a header row +as shown in the examples below. ```bash --input '[path to samplesheet file]' ``` -### Multiple samples +### Raw spatial data -The `sample` identifiers have to be unique. There can be arbirtary number of samples in the samplesheet. The pipeline will perform any downstream analysis for each sample independently: +The samplesheet for raw spatial data yet to be analysed with SpaceRanger is +specified like so: -```console -sample_id,species,st_data_dir,sc_data_dir -SAMPLE_1,Human,/path/to/ST/data1/,/path/to/scRNA-seq/data1/ -SAMPLE_2,Mouse,/path/to/ST/data2/,/path/to/scRNA-seq/data2/ +```no-highlight +sample,fastq_dir,tissue_hires_image,slide,area +SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1 +SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 ``` -### Full samplesheet +| Column | Description +| -------------------- | ------------------------------------------------------- +| `sample` | Custom sample name. +| `fastq_dir` | Path to directory where the sample FASTQ files are stored. +| `tissue_hires_image` | Path to the high-resolution image for the sample. +| `slide` | The Visium slide ID used for the sequencing. +| `area` | Which slide area contains the tissue sample. + +If you are unsure, please see the Visium documentation for details regarding the +different variants of [FASTQ directory structures](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/fastq-input) +and [slide parameters](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/slide-info) +appropriate for your samples. + +> **NB:** You will have to supply the `--run_spaceranger` parameter when you +> execute the pipeline. + +### Processed data + +If your data has already been processed by SpaceRanger the samplesheet will look +like this: + +```no-highlight +sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features, +matrix +SAMPLE_1,tissue_positions_list_1.csv,tissue_lowres_image_1.png,tissue_hires_image_1.png,scale_factors_1.json,barcodes_1.tsv.gz,features_1.tsv.gz,matrix_1.mtx.gz +SAMPLE_2,tissue_positions_list_2.csv,tissue_lowres_image_2.png,tissue_hires_image_2.png,scale_factors_2.json,barcodes_2.tsv.gz,features_2.tsv.gz,matrix_2.mtx.gz +``` + +| Column | Description +| -------------------- | ---------------------------------------------------- +| `sample` | Custom sample name. +| `tissue_positions_list` | Path to the CSV with spot barcodes and their array positions. +| `tissue_lowres_image` | Path to the low-resolution image for the sample. +| `tissue_hires_image` | Path to the high-resolution image for the sample. +| `scale_factors` | Path to the JSON file with scale conversion factors for the spots. +| `barcodes` | Path to TSV file with barcode IDs. +| `features` | Path to TSV file with features IDs. +| `matrix` | Path to MTX file with UMIs, barcodes and features. + +The latter three elements should be taken from the `filtered_feature_bc_matrix/` +directory, *i.e.* only tissue-associated barcodes and their data. -The pipeline will auto-detect whether scRNA-seq data was specified using the information provided in the samplesheet. The samplesheet can have as many columns as you desire, however, there is a strict requirement for the first 3 columns to match those defined in the table below. +## Space Ranger options -| Column | Description | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sample_id` | Custom sample name. This entry will be unique. | -| `species` | Species of the sample. Currently supported "Human" and "Mouse". | -| `st_data_dir` | Full path to directory with spatial transcriptomics data. There shold be a sub-directory `spatial` containing files `tissue_hires_image.png`, `tissue_lowres_image.png`, `detected_tissue_image.jpg`, `aligned_fiducials.jpg`, `scalefactors_json.json`, and `tissue_positions_list.csv`. There should also be either a sub-directory `raw_feature_bc_matrix` containing `features.tsv.gz`, `barcodes.tsv.gz`, and `matrix.mtx.gz` if the gene-spot count data is in MTX format, or `raw_feature_bc_matrix.h5` HDF5 file geneated with [SpaceRanger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). See [`nf-core/test-datasets`](https://github.com/nf-core/test-datasets/tree/spatialtranscriptomics) for description of these files. If the path is provided as URL the data will be downloaded to the local environment where the pipeline is executed. | -| `sc_data_dir` (in future optional, currently required) | Full path to directory with single cell transcriptomics data (a.k.a scRNA-seq data). The path should contain `features.tsv.gz`, `barcodes.tsv.gz`, and `matrix.mtx.gz` if the gene-spot count data is in MTX format, or `*.h5ad` AnnData HDF5 file format generated with [scanpy](https://github.com/theislab/scanpy). | -| `signature` (optional, not implemented) | Full path to the transcriptomics signature profiles in the `*.csv` or `*.csv.gz` format. | +The pipeline exposes several of Space Ranger's parameters when executing with +raw spatial data (`--run_spaceranger`). Space Ranger requieres a lot of memory +(64 GB) and several threads (8) to be able to run. You can find the Space Ranger +documentation at the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). + +You are only able to run Space Ranger on the [officially supported organisms](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest): +human and mouse. If you have already downloaded a reference you may supply the +path to its directory (or another link from the 10X website above) using the +`--spaceranger_reference` parameter, otherwise the pipeline will download the +default human reference for you automatically. + +You may optionally supply file paths to probe sets or manual fiducial alignment +using the `--spaceranger_probeset` and `--spaceranger_manual_alignment`, +respectively. + +## Analysis options + + + +[WIP] + +## Running the pipeline + +The typical command for running the pipeline is as follows: + +```bash +# Run the pipeline with raw data yet to be processed by SpaceRanger +nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker --run_spaceranger + +# Run pipeline with data already processed by SpaceRanger +nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker +``` -This will launch the pipeline with the `singularity` configuration profile. See below for more information about profiles. +This will launch the pipeline with the docker configuration profile. See below for more information about profiles. Note that the pipeline will create the following files in your working directory: From a2f60c169da18b9602b325fcb8aa07fd2229af7f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 May 2023 16:33:06 +0200 Subject: [PATCH 052/410] Prettier formatting --- docs/usage.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index a835bef..f26c0ce 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -27,13 +27,13 @@ SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1 SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 ``` -| Column | Description -| -------------------- | ------------------------------------------------------- -| `sample` | Custom sample name. -| `fastq_dir` | Path to directory where the sample FASTQ files are stored. -| `tissue_hires_image` | Path to the high-resolution image for the sample. -| `slide` | The Visium slide ID used for the sequencing. -| `area` | Which slide area contains the tissue sample. +| Column | Description | +| -------------------- | ---------------------------------------------------------- | +| `sample` | Custom sample name. | +| `fastq_dir` | Path to directory where the sample FASTQ files are stored. | +| `tissue_hires_image` | Path to the high-resolution image for the sample. | +| `slide` | The Visium slide ID used for the sequencing. | +| `area` | Which slide area contains the tissue sample. | If you are unsure, please see the Visium documentation for details regarding the different variants of [FASTQ directory structures](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/fastq-input) @@ -55,19 +55,19 @@ SAMPLE_1,tissue_positions_list_1.csv,tissue_lowres_image_1.png,tissue_hires_imag SAMPLE_2,tissue_positions_list_2.csv,tissue_lowres_image_2.png,tissue_hires_image_2.png,scale_factors_2.json,barcodes_2.tsv.gz,features_2.tsv.gz,matrix_2.mtx.gz ``` -| Column | Description -| -------------------- | ---------------------------------------------------- -| `sample` | Custom sample name. -| `tissue_positions_list` | Path to the CSV with spot barcodes and their array positions. -| `tissue_lowres_image` | Path to the low-resolution image for the sample. -| `tissue_hires_image` | Path to the high-resolution image for the sample. -| `scale_factors` | Path to the JSON file with scale conversion factors for the spots. -| `barcodes` | Path to TSV file with barcode IDs. -| `features` | Path to TSV file with features IDs. -| `matrix` | Path to MTX file with UMIs, barcodes and features. +| Column | Description | +| ----------------------- | ------------------------------------------------------------------ | +| `sample` | Custom sample name. | +| `tissue_positions_list` | Path to the CSV with spot barcodes and their array positions. | +| `tissue_lowres_image` | Path to the low-resolution image for the sample. | +| `tissue_hires_image` | Path to the high-resolution image for the sample. | +| `scale_factors` | Path to the JSON file with scale conversion factors for the spots. | +| `barcodes` | Path to TSV file with barcode IDs. | +| `features` | Path to TSV file with features IDs. | +| `matrix` | Path to MTX file with UMIs, barcodes and features. | The latter three elements should be taken from the `filtered_feature_bc_matrix/` -directory, *i.e.* only tissue-associated barcodes and their data. +directory, _i.e._ only tissue-associated barcodes and their data. ## Space Ranger options From f6652fbc90dce900a81120a8223a182f67a9be05 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 May 2023 16:45:46 +0200 Subject: [PATCH 053/410] Publish validated samplesheet in `pipeline_info/` --- conf/modules.config | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/conf/modules.config b/conf/modules.config index 3211507..ed40065 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -18,6 +18,14 @@ process { saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] + withName: 'SAMPLESHEET_CHECK' { + publishDir = [ + path: { "${params.outdir}/pipeline_info" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + withName: CUSTOM_DUMPSOFTWAREVERSIONS { publishDir = [ path: { "${params.outdir}/pipeline_info" }, From f0f1473876359c73480e958f951513e90498b120 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 May 2023 10:16:51 +0200 Subject: [PATCH 054/410] Add missing dumpversions process --- conf/modules.config | 3 ++- workflows/spatialtranscriptomics.nf | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index ed40065..9b2521c 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -37,7 +37,8 @@ process { withName: SPACERANGER_COUNT { publishDir = [ path: { "${params.outdir}/${meta.id}" }, - mode: params.publish_dir_mode + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index e509e78..c5d2e83 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -124,6 +124,13 @@ workflow ST { ST_PREPROCESS.out.st_data_norm ) ch_versions = ch_versions.mix(ST_POSTPROCESS.out.versions) + + // + // MODULE: Pipeline reporting + // + CUSTOM_DUMPSOFTWAREVERSIONS ( + ch_versions.unique().collectFile(name: 'collated_versions.yml') + ) } /* From 375da8b933e903710d1abbc8a35f7c5714e75334 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 May 2023 15:56:39 +0200 Subject: [PATCH 055/410] Update output documentation --- docs/output.md | 136 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 25 deletions(-) diff --git a/docs/output.md b/docs/output.md index 0dc4e6f..5325f12 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,55 +1,131 @@ # nf-core/spatialtranscriptomics: Output + + + + + ## Introduction -This document describes the output produced by the pipeline. Most of the plots are taken from the MultiQC report, which summarises results at the end of the pipeline. +This document describes the output produced by the pipeline. Most of the output +is contained within HTML reports created with [Quarto](https://quarto.org/), but +there are also other files which you can either take and analyse further by +yourself or explore interactively with _e.g._ [TissUUmaps](https://tissuumaps.github.io/). -The directories listed below will be created in the results directory after the pipeline has finished. All paths are relative to the top-level results directory. +The directories listed below will be created in the results directory after the +pipeline has finished. Results for individual samples will be created in +subdirectories following the `//` structure. All paths are +relative to the top-level results directory. ## Pipeline overview - +The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes +data using the following steps: + +- [Space Ranger](#space-ranger) +- [Data](#data) +- [Reports](#reports) + - [Quality controls and normalisation](#quality-controls-and-normalisation) + - [Clustering](#clustering) + - [Differential expression](#differential-expression) +- [Workflow reporting](#workflow-reporting) + - [Pipeline information](#pipeline-information) - Report metrics generated + during the workflow execution + +## Space Ranger + +
+Output files + +- `/spaceranger/` + - `outs/spatial/tissue_[hi/low]res_image.png`: High and low resolution images. + - `outs/spatial/tissue_positions_list.csv`: Spot barcodes and their array + positions. + - `outs/spatial/scalefactors_json.json`: Scale conversion factors for the + spots. + - `outs/filtered_feature_bc_matrix/barcodes.tsv.gz`: List of barcode IDs. + - `outs/filtered_feature_bc_matrix/features.tsv.gz`: List of feature IDs. + - `outs/filtered_feature_bc_matrix/matrix.mtx.gz`: Matrix of UMIs, barcodes + and features. + +
+ +All files produced by Space Ranger are currently published as output of this +pipeline, regardless if they're being used downstream or not; you can find more +information about these files at the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview). + +## Data -The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: + -- [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline -- [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution +
+Output files + +- `/data/` + - `st_adata_norm.h5ad`: Filtered and normalised adata. + - `st_adata_plain.h5ad`: Filtered adata. + - `st_adata_processed.h5ad`: Filtered, normalised and clustered adata. + - `st_adata_raw.h5ad`: Raw adata.
+Data in `.h5ad` format as processed by the pipeline, which can be used for +further downstream analyses if desired. They can also be used by the +[TissUUmaps](https://tissuumaps.github.io/) browser-based tool for visualisation +and exploration, allowing you to delve into the data in an interactive way. + +## Reports + +### Quality controls and normalisation +
-LDA-based deconvolution with STdeconvolve +Output files -![LDA-based deconvolution with STdeconvolve](images/sm-STdeconvolve_st_scatterpies.png) -![LDA topics spatial layout](images/sm-Topics_LDA_spatial.png) -![LDA topics UMAP layout](images/sm-UMAP_LDA_topics.png) -![LDA topics violin plot](images/sm-violin_topics_LDA.png) +- `/reports/` + - `st_qc_and_normalisation.html`: HTML report. + - `st_qc_and_normalisation_files/`: Data needed for the HTML report.
+Report containing analyses related to quality controls, filtering and +normalisation of spatial data. Spots are filtered based on total counts, +number of expressed genes, mitochondrial content as well as presence in tissue; +you can find more details in the report itself. + +### Clustering +
-BayesSpace spatially-aware clustering +Output files -![BayesSpace enhanced spatial clusters](images/sm-st_bayes_clusters.png) -![BayesSpace enhanced resolution clusters](images/sm-st_bayes_clusters_enhanced.png) +- `/reports/` + - `st_clustering.html`: HTML report. + - `st_clustering_files/`: Data needed for the HTML report.
-### MultiQC +Report containing analyses related to dimensionality reduction, clustering and +spatial visualisation. Leiden clustering is currently the only clustering +option; you can find more details in the report itself. + +### Differential expression
Output files -- `multiqc/` - - `multiqc_report.html`: a standalone HTML file that can be viewed in your web browser. - - `multiqc_data/`: directory containing parsed statistics from the different tools used in the pipeline. - - `multiqc_plots/`: directory containing static images from the report in various formats. +- `/reports/` + - `st_spatial_de.html`: HTML report. + - `st_spatial_de_files/`: Data needed for the HTML report. +- `/degs/` + - `st_spatial_de.csv`: List of spatially varying genes.
-[MultiQC](http://multiqc.info) is a visualization tool that generates a single HTML report summarising all samples in your project. Most of the pipeline QC results are visualised in the report and further statistics are available in the report data directory. +Report containing analyses related to differential expression testing and +spatially varying genes. The [SpatialDE](https://github.com/Teichlab/SpatialDE) +package is currently the only option for spatial testing; you can find more +details in the report itself. -Results generated by MultiQC collate pipeline QC from supported tools e.g. FastQC. The pipeline has special steps which also allow the software versions to be reported in the MultiQC output for future traceability. For more information about how to use MultiQC reports, see . +## Workflow reporting ### Pipeline information @@ -57,10 +133,20 @@ Results generated by MultiQC collate pipeline QC from supported tools e.g. FastQ Output files - `pipeline_info/` - - Reports generated by Nextflow: `execution_report.html`, `execution_timeline.html`, `execution_trace.txt` and `pipeline_dag.dot`/`pipeline_dag.svg`. - - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. - - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. + - Reports generated by Nextflow: `execution_report.html`, + `execution_timeline.html`, `execution_trace.txt` and + `pipeline_dag.dot`/`pipeline_dag.svg`. + - Reports generated by the pipeline: `pipeline_report.html`, + `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` + files will only be present if the `--email` / `--email_on_fail` parameter's + are used when running the pipeline. + - Reformatted samplesheet files used as input to the pipeline: + `samplesheet.valid.csv`. -[Nextflow](https://www.nextflow.io/docs/latest/tracing.html) provides excellent functionality for generating various reports relevant to the running and execution of the pipeline. This will allow you to troubleshoot errors with the running of the pipeline, and also provide you with other information such as launch commands, run times and resource usage. +[Nextflow](https://www.nextflow.io/docs/latest/tracing.html) provides excellent +functionality for generating various reports relevant to the running and +execution of the pipeline. This will allow you to troubleshoot errors with the +running of the pipeline, and also provide you with other information such as +launch commands, run times and resource usage. From eb6e3a908b570157f793ddfdf212a819d6527e54 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 May 2023 16:08:59 +0200 Subject: [PATCH 056/410] Remove some finished TODOs --- conf/base.config | 9 +-------- conf/test_full.config | 3 --- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/conf/base.config b/conf/base.config index 7b766b9..f5730af 100644 --- a/conf/base.config +++ b/conf/base.config @@ -11,7 +11,7 @@ process { - // TODO nf-core: Check the defaults for all processes + // Default process resource requirements cpus = { check_max( 1 * task.attempt, 'cpus' ) } memory = { check_max( 6.GB * task.attempt, 'memory' ) } time = { check_max( 4.h * task.attempt, 'time' ) } @@ -21,13 +21,6 @@ process { maxErrors = '-1' // Process-specific resource requirements - // NOTE - Please try and re-use the labels below as much as possible. - // These labels are used and recognised by default in DSL2 files hosted on nf-core/modules. - // If possible, it would be nice to keep the same label naming convention when - // adding in your local modules too. - // TODO nf-core: Customise requirements for specific processes. - // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors - withLabel:process_low { cpus = { check_max( 2 * task.attempt, 'cpus' ) } memory = { check_max( 12.GB * task.attempt, 'memory' ) } diff --git a/conf/test_full.config b/conf/test_full.config index b3eb242..d1bd890 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -16,7 +16,4 @@ params { // Input data for full size test input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset/samplesheet.csv' - - // TODO nf-core: Give any required params for the test so that command line flags are not needed - // ... } From c20119721707e70db07b68cdc132c15ae9259253 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 10:23:46 +0200 Subject: [PATCH 057/410] Remove MultiQC-related code Remove MultiQC-related code throughout the pipeline, as MultiQC is not utilised in nf-core/spatialtranscriptomics. --- assets/sendmail_template.txt | 24 ---------- docs/usage.md | 2 +- lib/NfcoreTemplate.groovy | 26 +---------- lib/WorkflowSpatialtranscriptomics.groovy | 44 ------------------- .../custom/dumpsoftwareversions/main.nf | 5 +-- .../custom/dumpsoftwareversions/meta.yml | 4 -- .../templates/dumpsoftwareversions.py | 13 +----- nextflow.config | 8 ---- nextflow_schema.json | 30 ------------- workflows/spatialtranscriptomics.nf | 18 +------- 10 files changed, 8 insertions(+), 166 deletions(-) diff --git a/assets/sendmail_template.txt b/assets/sendmail_template.txt index 8a54c9a..7c32561 100644 --- a/assets/sendmail_template.txt +++ b/assets/sendmail_template.txt @@ -24,30 +24,6 @@ Content-Disposition: inline; filename="nf-core-spatialtranscriptomics_logo_light collect { it.join() }. flatten(). join( '\n' ) %> - -<% -if (mqcFile){ -def mqcFileObj = new File("$mqcFile") -if (mqcFileObj.length() < mqcMaxSize){ -out << """ ---nfcoremimeboundary -Content-Type: text/html; name=\"multiqc_report\" -Content-Transfer-Encoding: base64 -Content-ID: -Content-Disposition: attachment; filename=\"${mqcFileObj.getName()}\" - -${mqcFileObj. - bytes. - encodeBase64(). - toString(). - tokenize( '\n' )*. - toList()*. - collate( 76 )*. - collect { it.join() }. - flatten(). - join( '\n' )} -""" -}} %> --nfcoremimeboundary-- diff --git a/docs/usage.md b/docs/usage.md index f26c0ce..92022b2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -129,7 +129,7 @@ It is a good idea to specify a pipeline version when running the pipeline on you First, go to the [nf-core/spatialtranscriptomics releases page](https://github.com/nf-core/spatialtranscriptomics/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. -This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. For example, at the bottom of the MultiQC reports. +This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. ## Core Nextflow arguments diff --git a/lib/NfcoreTemplate.groovy b/lib/NfcoreTemplate.groovy index 25a0a74..3d513a9 100755 --- a/lib/NfcoreTemplate.groovy +++ b/lib/NfcoreTemplate.groovy @@ -54,7 +54,7 @@ class NfcoreTemplate { // // Construct and send completion email // - public static void email(workflow, params, summary_params, projectDir, log, multiqc_report=[]) { + public static void email(workflow, params, summary_params, projectDir, log) { // Set up the e-mail variables def subject = "[$workflow.manifest.name] Successful: $workflow.runName" @@ -92,24 +92,6 @@ class NfcoreTemplate { email_fields['projectDir'] = workflow.projectDir email_fields['summary'] = summary << misc_fields - // On success try attach the multiqc report - def mqc_report = null - try { - if (workflow.success) { - 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" - } - mqc_report = mqc_report[0] - } - } - } catch (all) { - if (multiqc_report) { - log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" - } - } - // Check if we are only sending emails on failure def email_address = params.email if (!params.email && params.email_on_fail && !workflow.success) { @@ -128,8 +110,7 @@ class NfcoreTemplate { def email_html = html_template.toString() // Render the sendmail template - def max_multiqc_email_size = params.max_multiqc_email_size as nextflow.util.MemoryUnit - def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "$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: "$projectDir" ] def sf = new File("$projectDir/assets/sendmail_template.txt") def sendmail_template = engine.createTemplate(sf).make(smail_fields) def sendmail_html = sendmail_template.toString() @@ -145,9 +126,6 @@ class NfcoreTemplate { } catch (all) { // Catch failures and try with plaintext def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] - if ( mqc_report.size() <= max_multiqc_email_size.toBytes() ) { - mail_cmd += [ '-A', mqc_report ] - } mail_cmd.execute() << email_html log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-" } diff --git a/lib/WorkflowSpatialtranscriptomics.groovy b/lib/WorkflowSpatialtranscriptomics.groovy index 1193386..4f396d9 100644 --- a/lib/WorkflowSpatialtranscriptomics.groovy +++ b/lib/WorkflowSpatialtranscriptomics.groovy @@ -1,5 +1,3 @@ -import groovy.text.SimpleTemplateEngine - // // This file holds several functions specific to the workflow/spatialtranscriptomics.nf in the nf-core/spatialtranscriptomics pipeline // @@ -19,48 +17,6 @@ class WorkflowSpatialtranscriptomics { } // - // Get workflow summary for MultiQC - // - public static String paramsSummaryMultiqc(workflow, summary) { - String summary_section = '' - for (group in summary.keySet()) { - def group_params = summary.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()) { - summary_section += "
$param
${group_params.get(param) ?: 'N/A'}
\n" - } - summary_section += "
\n" - } - } - - String yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" - 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 - } - - public static String methodsDescriptionText(run_workflow, mqc_methods_yaml) { - // Convert to a named map so can be used as with familar NXF ${workflow} variable syntax in the MultiQC YML file - def meta = [:] - meta.workflow = run_workflow.toMap() - meta["manifest_map"] = run_workflow.manifest.toMap() - - meta["doi_text"] = meta.manifest_map.doi ? "(doi: ${meta.manifest_map.doi})" : "" - 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.
  • " - - def methods_text = mqc_methods_yaml.text - - def engine = new SimpleTemplateEngine() - def description_html = engine.createTemplate(methods_text).make(meta) - - return description_html - }// // Exit pipeline if incorrect --genome key provided // private static void genomeExistsError(params, log) { diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index 3df2176..4f7f360 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -11,9 +11,8 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { path versions output: - path "software_versions.yml" , emit: yml - path "software_versions_mqc.yml", emit: mqc_yml - path "versions.yml" , emit: versions + path "software_versions.yml", emit: yml + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when diff --git a/modules/nf-core/custom/dumpsoftwareversions/meta.yml b/modules/nf-core/custom/dumpsoftwareversions/meta.yml index 60b546a..9b4a043 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/meta.yml +++ b/modules/nf-core/custom/dumpsoftwareversions/meta.yml @@ -20,10 +20,6 @@ output: type: file description: Standard YML file containing software versions pattern: "software_versions.yml" - - mqc_yml: - type: file - description: MultiQC custom content YML file containing software versions - pattern: "software_versions_mqc.yml" - versions: type: file description: File containing software versions diff --git a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py index e55b8d4..c54dc00 100755 --- a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +++ b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py @@ -11,7 +11,7 @@ def _make_versions_html(versions): - """Generate a tabular HTML output of all versions for MultiQC.""" + """Generate a tabular HTML output of all versions.""" html = [ dedent( """\\ @@ -80,19 +80,8 @@ def main(): "$workflow.manifest.name": "$workflow.manifest.version", } - versions_mqc = { - "id": "software_versions", - "section_name": "${workflow.manifest.name} Software Versions", - "section_href": "https://github.com/${workflow.manifest.name}", - "plot_type": "html", - "description": "are collected at run time from the software output.", - "data": _make_versions_html(versions_by_module), - } - with open("software_versions.yml", "w") as f: yaml.dump(versions_by_module, f, default_flow_style=False) - with open("software_versions_mqc.yml", "w") as f: - yaml.dump(versions_mqc, f, default_flow_style=False) with open("versions.yml", "w") as f: yaml.dump(versions_this_module, f, default_flow_style=False) diff --git a/nextflow.config b/nextflow.config index 745e92b..83097c5 100644 --- a/nextflow.config +++ b/nextflow.config @@ -13,7 +13,6 @@ params { // Input options input = null - spaceranger_input = null // Spaceranger options run_spaceranger = false @@ -26,13 +25,6 @@ params { igenomes_base = 's3://ngi-igenomes/igenomes' igenomes_ignore = false - // MultiQC options - multiqc_config = null - multiqc_title = null - multiqc_logo = null - max_multiqc_email_size = '25.MB' - multiqc_methods_description = null - // Boilerplate options outdir = null publish_dir_mode = 'copy' diff --git a/nextflow_schema.json b/nextflow_schema.json index 6c99293..44cd20b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -70,11 +70,6 @@ "fa_icon": "fas fa-envelope", "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" - }, - "multiqc_title": { - "type": "string", - "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", - "fa_icon": "fas fa-file-signature" } } }, @@ -316,14 +311,6 @@ "fa_icon": "fas fa-remove-format", "hidden": true }, - "max_multiqc_email_size": { - "type": "string", - "description": "File size limit when attaching MultiQC reports to summary emails.", - "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", - "default": "25.MB", - "fa_icon": "fas fa-file-upload", - "hidden": true - }, "monochrome_logs": { "type": "boolean", "description": "Do not use coloured log outputs.", @@ -337,23 +324,6 @@ "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", "hidden": true }, - "multiqc_config": { - "type": "string", - "description": "Custom config file to supply to MultiQC.", - "fa_icon": "fas fa-cog", - "hidden": true - }, - "multiqc_logo": { - "type": "string", - "description": "Custom logo file to supply to MultiQC. File name must also be set in the MultiQC config file", - "fa_icon": "fas fa-image", - "hidden": true - }, - "multiqc_methods_description": { - "type": "string", - "description": "Custom MultiQC yaml file containing HTML including a methods description.", - "fa_icon": "fas fa-cog" - }, "validate_params": { "type": "boolean", "description": "Boolean whether to validate parameters against the schema at runtime", diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index c5d2e83..add747f 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -17,23 +17,12 @@ log.info """\ .stripIndent() -def checkPathParamList = [ params.input, params.multiqc_config ] +def checkPathParamList = [ params.input ] for (param in checkPathParamList) { if (param) { file(param, checkIfExists: true) } } // Check mandatory parameters if (params.input) { ch_input = file(params.input) } else { exit 1, 'Input samplesheet not specified!' } -/* -================================================================================ - CONFIG FILES -================================================================================ -*/ - -ch_multiqc_config = Channel.fromPath("$projectDir/assets/multiqc_config.yml", checkIfExists: true) -ch_multiqc_custom_config = params.multiqc_config ? Channel.fromPath( params.multiqc_config, checkIfExists: true ) : Channel.empty() -ch_multiqc_logo = params.multiqc_logo ? Channel.fromPath( params.multiqc_logo, checkIfExists: true ) : Channel.empty() -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) - /* ================================================================================ IMPORT LOCAL MODULES/SUBWORKFLOWS @@ -70,9 +59,6 @@ include { CUSTOM_DUMPSOFTWAREVERSIONS } from '../modules/nf-core/custom/dumpsoft ================================================================================ */ -// Info required for completion email and summary -def multiqc_report = [] - // // Spatial transcriptomics workflow // @@ -141,7 +127,7 @@ workflow ST { workflow.onComplete { if (params.email || params.email_on_fail) { - NfcoreTemplate.email(workflow, params, summary_params, projectDir, log, multiqc_report) + NfcoreTemplate.email(workflow, params, summary_params, projectDir, log) } NfcoreTemplate.summary(workflow, params, log) if (params.hook_url) { From 9b3f01585bbcea9011c49d907ea8f96de53fdfb6 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 11:17:54 +0200 Subject: [PATCH 058/410] Revert template-related changes from c201197 --- assets/sendmail_template.txt | 24 +++++++++++++++++ lib/NfcoreTemplate.groovy | 26 +++++++++++++++++-- .../custom/dumpsoftwareversions/main.nf | 5 ++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/assets/sendmail_template.txt b/assets/sendmail_template.txt index 7c32561..8a54c9a 100644 --- a/assets/sendmail_template.txt +++ b/assets/sendmail_template.txt @@ -24,6 +24,30 @@ Content-Disposition: inline; filename="nf-core-spatialtranscriptomics_logo_light collect { it.join() }. flatten(). join( '\n' ) %> + +<% +if (mqcFile){ +def mqcFileObj = new File("$mqcFile") +if (mqcFileObj.length() < mqcMaxSize){ +out << """ +--nfcoremimeboundary +Content-Type: text/html; name=\"multiqc_report\" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: attachment; filename=\"${mqcFileObj.getName()}\" + +${mqcFileObj. + bytes. + encodeBase64(). + toString(). + tokenize( '\n' )*. + toList()*. + collate( 76 )*. + collect { it.join() }. + flatten(). + join( '\n' )} +""" +}} %> --nfcoremimeboundary-- diff --git a/lib/NfcoreTemplate.groovy b/lib/NfcoreTemplate.groovy index 3d513a9..25a0a74 100755 --- a/lib/NfcoreTemplate.groovy +++ b/lib/NfcoreTemplate.groovy @@ -54,7 +54,7 @@ class NfcoreTemplate { // // Construct and send completion email // - public static void email(workflow, params, summary_params, projectDir, log) { + public static void email(workflow, params, summary_params, projectDir, log, multiqc_report=[]) { // Set up the e-mail variables def subject = "[$workflow.manifest.name] Successful: $workflow.runName" @@ -92,6 +92,24 @@ class NfcoreTemplate { email_fields['projectDir'] = workflow.projectDir email_fields['summary'] = summary << misc_fields + // On success try attach the multiqc report + def mqc_report = null + try { + if (workflow.success) { + 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" + } + mqc_report = mqc_report[0] + } + } + } catch (all) { + if (multiqc_report) { + log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" + } + } + // Check if we are only sending emails on failure def email_address = params.email if (!params.email && params.email_on_fail && !workflow.success) { @@ -110,7 +128,8 @@ class NfcoreTemplate { def email_html = html_template.toString() // Render the sendmail template - def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "$projectDir" ] + def max_multiqc_email_size = params.max_multiqc_email_size as nextflow.util.MemoryUnit + def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "$projectDir", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ] def sf = new File("$projectDir/assets/sendmail_template.txt") def sendmail_template = engine.createTemplate(sf).make(smail_fields) def sendmail_html = sendmail_template.toString() @@ -126,6 +145,9 @@ class NfcoreTemplate { } catch (all) { // Catch failures and try with plaintext def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] + if ( mqc_report.size() <= max_multiqc_email_size.toBytes() ) { + mail_cmd += [ '-A', mqc_report ] + } mail_cmd.execute() << email_html log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-" } diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index 4f7f360..3df2176 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -11,8 +11,9 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { path versions output: - path "software_versions.yml", emit: yml - path "versions.yml" , emit: versions + path "software_versions.yml" , emit: yml + path "software_versions_mqc.yml", emit: mqc_yml + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when From bc80ef3416df6f359600e53153f925c619629f50 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 11:18:46 +0200 Subject: [PATCH 059/410] Remove unused `spaceranger_input` from schema --- nextflow_schema.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 44cd20b..18cd1f1 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -34,14 +34,6 @@ "help_text": "Use this when your data is raw spatial data that needs to be processed by SpaceRanger before downstream analyses can be executed.", "default": "false" }, - "spaceranger_input": { - "type": "string", - "format": "file-path", - "mimetype": "text/csv", - "pattern": "^\\S+\\.csv$", - "description": "Path to comma-separated file containing information about the raw spatial samples in the experiment.", - "fa_icon": "fas fa-file-csv" - }, "spaceranger_reference": { "type": "string", "format": "file-path", From 8e608b20397205f753933754b1199ac21e1094d7 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 11:23:40 +0200 Subject: [PATCH 060/410] Remove igenome-related code and configuration Remove igenome-related code and configuration throughout the pipeline, as it is not used by nf-core/spatialtranscriptomics. --- conf/igenomes.config | 432 ---------------------- lib/WorkflowMain.groovy | 13 +- lib/WorkflowSpatialtranscriptomics.groovy | 14 - main.nf | 7 - nextflow.config | 14 - nextflow_schema.json | 44 --- 6 files changed, 1 insertion(+), 523 deletions(-) delete mode 100644 conf/igenomes.config diff --git a/conf/igenomes.config b/conf/igenomes.config deleted file mode 100644 index 7a1b3ac..0000000 --- a/conf/igenomes.config +++ /dev/null @@ -1,432 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for iGenomes paths -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines reference genomes using iGenome paths. - Can be used by any config that customises the base path using: - $params.igenomes_base / --igenomes_base ----------------------------------------------------------------------------------------- -*/ - -params { - // illumina iGenomes reference file paths - genomes { - 'GRCh37' { - fasta = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/GRCh37-blacklist.bed" - } - 'GRCh38' { - fasta = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg38-blacklist.bed" - } - 'GRCm38' { - fasta = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "1.87e9" - blacklist = "${projectDir}/assets/blacklists/GRCm38-blacklist.bed" - } - 'TAIR10' { - fasta = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/README.txt" - mito_name = "Mt" - } - 'EB2' { - fasta = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/README.txt" - } - 'UMD3.1' { - fasta = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/README.txt" - mito_name = "MT" - } - 'WBcel235' { - fasta = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Annotation/Genes/genes.bed" - mito_name = "MtDNA" - macs_gsize = "9e7" - } - 'CanFam3.1' { - fasta = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/README.txt" - mito_name = "MT" - } - 'GRCz10' { - fasta = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'BDGP6' { - fasta = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Annotation/Genes/genes.bed" - mito_name = "M" - macs_gsize = "1.2e8" - } - 'EquCab2' { - fasta = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/README.txt" - mito_name = "MT" - } - 'EB1' { - fasta = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/README.txt" - } - 'Galgal4' { - fasta = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'Gm01' { - fasta = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/README.txt" - } - 'Mmul_1' { - fasta = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/README.txt" - mito_name = "MT" - } - 'IRGSP-1.0' { - fasta = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Annotation/Genes/genes.bed" - mito_name = "Mt" - } - 'CHIMP2.1.4' { - fasta = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/README.txt" - mito_name = "MT" - } - 'Rnor_5.0' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'Rnor_6.0' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'R64-1-1' { - fasta = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Annotation/Genes/genes.bed" - mito_name = "MT" - macs_gsize = "1.2e7" - } - 'EF2' { - fasta = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "1.21e7" - } - 'Sbi1' { - fasta = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/README.txt" - } - 'Sscrofa10.2' { - fasta = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/README.txt" - mito_name = "MT" - } - 'AGPv3' { - fasta = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Annotation/Genes/genes.bed" - mito_name = "Mt" - } - 'hg38' { - fasta = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg38-blacklist.bed" - } - 'hg19' { - fasta = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg19-blacklist.bed" - } - 'mm10' { - fasta = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "1.87e9" - blacklist = "${projectDir}/assets/blacklists/mm10-blacklist.bed" - } - 'bosTau8' { - fasta = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Annotation/Genes/genes.bed" - mito_name = "chrM" - } - 'ce10' { - fasta = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "9e7" - } - 'canFam3' { - fasta = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/README.txt" - mito_name = "chrM" - } - 'danRer10' { - fasta = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "1.37e9" - } - 'dm6' { - fasta = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "1.2e8" - } - 'equCab2' { - fasta = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/README.txt" - mito_name = "chrM" - } - 'galGal4' { - fasta = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/README.txt" - mito_name = "chrM" - } - 'panTro4' { - fasta = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/README.txt" - mito_name = "chrM" - } - 'rn6' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Annotation/Genes/genes.bed" - mito_name = "chrM" - } - 'sacCer3' { - fasta = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/BismarkIndex/" - readme = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "1.2e7" - } - 'susScr3' { - fasta = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/README.txt" - mito_name = "chrM" - } - } -} diff --git a/lib/WorkflowMain.groovy b/lib/WorkflowMain.groovy index 760209d..b4a0406 100755 --- a/lib/WorkflowMain.groovy +++ b/lib/WorkflowMain.groovy @@ -22,7 +22,7 @@ class WorkflowMain { // Generate help string // public static String help(workflow, params, log) { - def command = "nextflow run ${workflow.manifest.name} --input samplesheet.csv --genome GRCh37 -profile docker" + def command = "nextflow run ${workflow.manifest.name} --input samplesheet.csv -profile docker" def help_string = '' help_string += NfcoreTemplate.logo(workflow, params.monochrome_logs) help_string += NfcoreSchema.paramsHelp(workflow, params, command) @@ -85,15 +85,4 @@ class WorkflowMain { System.exit(1) } } - // - // Get attribute from genome config file e.g. fasta - // - public static Object getGenomeAttribute(params, attribute) { - if (params.genomes && params.genome && params.genomes.containsKey(params.genome)) { - if (params.genomes[ params.genome ].containsKey(attribute)) { - return params.genomes[ params.genome ][ attribute ] - } - } - return null - } } diff --git a/lib/WorkflowSpatialtranscriptomics.groovy b/lib/WorkflowSpatialtranscriptomics.groovy index 4f396d9..432d840 100644 --- a/lib/WorkflowSpatialtranscriptomics.groovy +++ b/lib/WorkflowSpatialtranscriptomics.groovy @@ -8,7 +8,6 @@ class WorkflowSpatialtranscriptomics { // Check and validate parameters // public static void initialise(params, log) { - genomeExistsError(params, log) //if (!params.fasta) { //log.error "Genome fasta file not specified with e.g. '--fasta genome.fa' or via a detectable config file." @@ -16,17 +15,4 @@ class WorkflowSpatialtranscriptomics { //} } - // - // Exit pipeline if incorrect --genome key provided - // - private static void genomeExistsError(params, log) { - if (params.genomes && params.genome && !params.genomes.containsKey(params.genome)) { - log.error "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - " Genome '${params.genome}' not found in any config files provided to the pipeline.\n" + - " Currently, the available genome keys are:\n" + - " ${params.genomes.keySet().join(", ")}\n" + - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" - System.exit(1) - } - } } diff --git a/main.nf b/main.nf index aa67015..dbb781a 100644 --- a/main.nf +++ b/main.nf @@ -11,13 +11,6 @@ nextflow.enable.dsl = 2 -/* -================================================================================ - GENOME PARAMETER VALUES -================================================================================ -*/ - -params.fasta = WorkflowMain.getGenomeAttribute(params, 'fasta') /* ================================================================================ diff --git a/nextflow.config b/nextflow.config index 83097c5..795577d 100644 --- a/nextflow.config +++ b/nextflow.config @@ -20,11 +20,6 @@ params { spaceranger_probeset = null spaceranger_manual_alignment = null - // References - genome = null - igenomes_base = 's3://ngi-igenomes/igenomes' - igenomes_ignore = false - // Boilerplate options outdir = null publish_dir_mode = 'copy' @@ -37,7 +32,6 @@ params { version = false validate_params = true show_hidden_params = false - schema_ignore_params = 'genomes' // Config options custom_config_version = 'master' @@ -147,14 +141,6 @@ profiles { } -// Load igenomes.config if required -if (!params.igenomes_ignore) { - includeConfig 'conf/igenomes.config' -} else { - params.genomes = [:] -} - - // 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. diff --git a/nextflow_schema.json b/nextflow_schema.json index 18cd1f1..198c640 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -66,47 +66,6 @@ } }, - "reference_genome_options": { - "title": "Reference genome options", - "type": "object", - "fa_icon": "fas fa-dna", - "description": "Reference genome related files and options required for the workflow.", - "properties": { - "genome": { - "type": "string", - "description": "Name of iGenomes reference.", - "fa_icon": "fas fa-book", - "help_text": "If using a reference genome configured in the pipeline using iGenomes, use this parameter to give the ID for the reference. This is then used to build the full paths for all required reference genome files e.g. `--genome GRCh38`. \n\nSee the [nf-core website docs](https://nf-co.re/usage/reference_genomes) for more details.", - "hidden": true - }, - "fasta": { - "type": "string", - "format": "file-path", - "mimetype": "text/plain", - "pattern": "^\\S+\\.fn?a(sta)?(\\.gz)?$", - "description": "Path to FASTA genome file.", - "help_text": "This parameter is *mandatory* if `--genome` is not specified. If you don't have a BWA index available this will be generated for you automatically. Combine with `--save_reference` to save BWA index for future runs.", - "fa_icon": "far fa-file-code", - "hidden": true - }, - "igenomes_base": { - "type": "string", - "format": "directory-path", - "description": "Directory / URL base for iGenomes references.", - "default": "s3://ngi-igenomes/igenomes", - "fa_icon": "fas fa-cloud-download-alt", - "hidden": true - }, - "igenomes_ignore": { - "type": "boolean", - "description": "Do not load the iGenomes reference config.", - "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`." - } - } - }, - "analysis_option": { "title": "Analysis option", "type": "object", @@ -341,9 +300,6 @@ { "$ref": "#/definitions/input_output_options" }, - { - "$ref": "#/definitions/reference_genome_options" - }, { "$ref": "#/definitions/institutional_config_options" }, From 4e3c028561752515a0c0f542e67c413ccb304649 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 11:29:04 +0200 Subject: [PATCH 061/410] Add `schema_ignore_params` back in to config --- nextflow.config | 1 + 1 file changed, 1 insertion(+) diff --git a/nextflow.config b/nextflow.config index 795577d..cc2ecf5 100644 --- a/nextflow.config +++ b/nextflow.config @@ -32,6 +32,7 @@ params { version = false validate_params = true show_hidden_params = false + schema_ignore_params = '' // Config options custom_config_version = 'master' From f736b0626c1ecf66a84a5ab352f73b3f59c350e5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 13:50:04 +0200 Subject: [PATCH 062/410] Add patch to `dumpsoftwareversions` module Add a patch to the `dumpsoftwareversions` module with removal of MultiQC code and content, as it is not used by the pipeline. --- modules.json | 3 +- .../custom-dumpsoftwareversions.diff | 71 +++++++++++++++++++ .../custom/dumpsoftwareversions/main.nf | 5 +- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff diff --git a/modules.json b/modules.json index bea5984..884ee95 100644 --- a/modules.json +++ b/modules.json @@ -8,7 +8,8 @@ "custom/dumpsoftwareversions": { "branch": "master", "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", - "installed_by": ["modules"] + "installed_by": ["modules"], + "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" } } } diff --git a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff new file mode 100644 index 0000000..79927d6 --- /dev/null +++ b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff @@ -0,0 +1,71 @@ +Changes in module 'nf-core/custom/dumpsoftwareversions' +--- modules/nf-core/custom/dumpsoftwareversions/meta.yml ++++ modules/nf-core/custom/dumpsoftwareversions/meta.yml +@@ -20,10 +20,6 @@ + type: file + description: Standard YML file containing software versions + pattern: "software_versions.yml" +- - mqc_yml: +- type: file +- description: MultiQC custom content YML file containing software versions +- pattern: "software_versions_mqc.yml" + - versions: + type: file + description: File containing software versions + +--- modules/nf-core/custom/dumpsoftwareversions/main.nf ++++ modules/nf-core/custom/dumpsoftwareversions/main.nf +@@ -11,9 +11,8 @@ + path versions + + output: +- path "software_versions.yml" , emit: yml +- path "software_versions_mqc.yml", emit: mqc_yml +- path "versions.yml" , emit: versions ++ path "software_versions.yml", emit: yml ++ path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + +--- modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py ++++ modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +@@ -4,13 +4,14 @@ + """Provide functions to merge multiple versions.yml files.""" + + +-import yaml + import platform + from textwrap import dedent + ++import yaml ++ + + def _make_versions_html(versions): +- """Generate a tabular HTML output of all versions for MultiQC.""" ++ """Generate a tabular HTML output of all versions.""" + html = [ + dedent( + """\\ +@@ -79,19 +80,8 @@ + "$workflow.manifest.name": "$workflow.manifest.version", + } + +- versions_mqc = { +- "id": "software_versions", +- "section_name": "${workflow.manifest.name} Software Versions", +- "section_href": "https://github.com/${workflow.manifest.name}", +- "plot_type": "html", +- "description": "are collected at run time from the software output.", +- "data": _make_versions_html(versions_by_module), +- } +- + with open("software_versions.yml", "w") as f: + yaml.dump(versions_by_module, f, default_flow_style=False) +- with open("software_versions_mqc.yml", "w") as f: +- yaml.dump(versions_mqc, f, default_flow_style=False) + + with open("versions.yml", "w") as f: + yaml.dump(versions_this_module, f, default_flow_style=False) + +************************************************************ diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index 3df2176..4f7f360 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -11,9 +11,8 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { path versions output: - path "software_versions.yml" , emit: yml - path "software_versions_mqc.yml", emit: mqc_yml - path "versions.yml" , emit: versions + path "software_versions.yml", emit: yml + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when From 6a7cbfbb5460fc83718491c50779f10dcbba7210 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 15:07:46 +0200 Subject: [PATCH 063/410] Add checks for more file path parameter existance --- nextflow.config | 2 -- workflows/spatialtranscriptomics.nf | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/nextflow.config b/nextflow.config index cc2ecf5..5328a2e 100644 --- a/nextflow.config +++ b/nextflow.config @@ -9,8 +9,6 @@ // Global default params, used in configs params { - // TODO nf-core: Specify your pipeline's command line flags - // Input options input = null diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index add747f..dcb153d 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -9,15 +9,16 @@ def summary_params = NfcoreSchema.paramsSummaryMap(workflow, params) // Validate input parameters WorkflowSpatialtranscriptomics.initialise(params, log) -// TODO nf-core: Add all file path parameters for the pipeline to the list below // Check input path parameters to see if they exist log.info """\ Project directory: ${projectDir} """ .stripIndent() - -def checkPathParamList = [ params.input ] +def checkPathParamList = [ params.input, + params.spaceranger_reference, + params.spaceranger_probeset, + params.spaceranger_manual_alignment ] for (param in checkPathParamList) { if (param) { file(param, checkIfExists: true) } } // Check mandatory parameters From dfca4846e5ca9ada5fe9572506e6ca8954f1fd62 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 May 2023 16:49:20 +0200 Subject: [PATCH 064/410] Harmonise Space Ranger naming Harmonise the usage of Space Ranger naming; the official 10X website and resources use "Space Ranger" when written in text (not "SpaceRanger") and `spaceranger` (without underscore as an equivalent to the character in the text version) in code and on the command line (_e.g._ `spaceranger count (...)` to execute). --- README.md | 6 +++--- bin/check_samplesheet.py | 4 ++-- docs/usage.md | 8 ++++---- modules/local/spaceranger_count.nf | 4 ++-- modules/local/spaceranger_download_probeset.nf | 2 +- nextflow_schema.json | 10 +++++----- subworkflows/local/spaceranger.nf | 4 ++-- workflows/spatialtranscriptomics.nf | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 27c8083..e27017e 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ **nf-core/spatialtranscriptomics** is a bioinformatics analysis pipeline for Spatial Transcriptomics. It can process and analyse 10X spatial data either -directly from raw data by running [SpaceRanger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) -or data already processed by SpaceRanger. The pipeline currently consists of the +directly from raw data by running [Space Ranger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) +or data already processed by Space Ranger. The pipeline currently consists of the following steps: -0. Raw data processing with SpaceRanger (optional) +0. Raw data processing with Space Ranger (optional) 1. Quality controls and filtering 2. Normalisation 3. Dimensionality reduction and clustering diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index dbb2ff0..bfdf198 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -13,7 +13,7 @@ def parse_args(argv=None): parser.add_argument("file_in", help="Input samplesheet file.") parser.add_argument("file_out", help="Output validated samplesheet file.") parser.add_argument( - "--is_raw_data", action="store_true", help="Whether input is raw data to be processed by SpaceRanger." + "--is_raw_data", action="store_true", help="Whether input is raw data to be processed by Space Ranger." ) return parser.parse_args(argv) @@ -45,7 +45,7 @@ def check_samplesheet(file_in, file_out, is_raw_data): Args: file_in (pathlib.Path): The given tabular samplesheet. file_out (pathlib.Path): Where the validated samplesheet should be created. - is_raw_data (boolean): Whether the input is raw spatial data to be processed by SpaceRanger. + is_raw_data (boolean): Whether the input is raw spatial data to be processed by Space Ranger. The following structure is expected: sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix diff --git a/docs/usage.md b/docs/usage.md index 92022b2..25d061b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -18,7 +18,7 @@ as shown in the examples below. ### Raw spatial data -The samplesheet for raw spatial data yet to be analysed with SpaceRanger is +The samplesheet for raw spatial data yet to be analysed with Space Ranger is specified like so: ```no-highlight @@ -45,7 +45,7 @@ appropriate for your samples. ### Processed data -If your data has already been processed by SpaceRanger the samplesheet will look +If your data has already been processed by Space Ranger the samplesheet will look like this: ```no-highlight @@ -97,10 +97,10 @@ respectively. The typical command for running the pipeline is as follows: ```bash -# Run the pipeline with raw data yet to be processed by SpaceRanger +# Run the pipeline with raw data yet to be processed by Space Ranger nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker --run_spaceranger -# Run pipeline with data already processed by SpaceRanger +# Run pipeline with data already processed by Space Ranger nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker ``` diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index 25ec03c..cf72999 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -1,5 +1,5 @@ // -// Run SpaceRanger count +// Run Space Ranger count // process SPACERANGER_COUNT { @@ -45,7 +45,7 @@ process SPACERANGER_COUNT { cat <<-END_VERSIONS > versions.yml "${task.process}": - SpaceRanger: \$(spaceranger -V | sed -e "s/spaceranger spaceranger-//g") + Space Ranger: \$(spaceranger -V | sed -e "s/spaceranger spaceranger-//g") END_VERSIONS """ } diff --git a/modules/local/spaceranger_download_probeset.nf b/modules/local/spaceranger_download_probeset.nf index d1ca567..be043dc 100644 --- a/modules/local/spaceranger_download_probeset.nf +++ b/modules/local/spaceranger_download_probeset.nf @@ -1,5 +1,5 @@ // -// Download SpaceRanger probeset +// Download Space Ranger probeset // process SPACERANGER_DOWNLOAD_PROBESET { diff --git a/nextflow_schema.json b/nextflow_schema.json index 198c640..9e9dc01 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -30,14 +30,14 @@ }, "run_spaceranger": { "type": "boolean", - "description": "Run SpaceRanger on input data", - "help_text": "Use this when your data is raw spatial data that needs to be processed by SpaceRanger before downstream analyses can be executed.", + "description": "Run Space Ranger on input data", + "help_text": "Use this when your data is raw spatial data that needs to be processed by Space Ranger before downstream analyses can be executed.", "default": "false" }, "spaceranger_reference": { "type": "string", "format": "file-path", - "description": "Location of SpaceRanger reference directory", + "description": "Location of Space Ranger reference directory", "fa_icon": "fas fa-folder-open" }, "spaceranger_probeset": { @@ -45,7 +45,7 @@ "format": "file-path", "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", - "description": "Location of SpaceRanger probeset file", + "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, "spaceranger_manual_alignment": { @@ -53,7 +53,7 @@ "format": "file-path", "mimetype": "text/csv", "pattern": "^\\S+\\.json$", - "description": "Location of SpaceRanger manual alignment file", + "description": "Location of Space Ranger manual alignment file", "fa_icon": "fas fa-file-csv" }, "email": { diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 147145d..986e1c0 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -1,5 +1,5 @@ // -// Raw data processing with SpaceRanger +// Raw data processing with Space Ranger // include { SPACERANGER_DOWNLOAD_PROBESET } from '../../modules/local/spaceranger_download_probeset' @@ -50,7 +50,7 @@ workflow SPACERANGER { } // - // Run SpaceRanger count + // Run Space Ranger count // SPACERANGER_COUNT ( ch_st_data, diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index dcb153d..6b8cc02 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -76,7 +76,7 @@ workflow ST { ch_versions = ch_versions.mix(INPUT_CHECK.out.versions) // - // SUBWORKFLOW: SpaceRanger raw data processing + // SUBWORKFLOW: Space Ranger raw data processing // if ( params.run_spaceranger ) { SPACERANGER ( From 00f7d8b1983862444df9c92dc3f694de757941f1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 08:33:58 +0200 Subject: [PATCH 065/410] Only output last (processed) adata file --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index de72566..714561a 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -52,7 +52,7 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "*.h5ad" + pattern: "st_adata_processed.h5ad" ], [ path: { "${params.outdir}/${meta.id}/degs" }, From 080871ee819658ce1ca22a92fe68823d2db4f89a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 08:43:36 +0200 Subject: [PATCH 066/410] Remove redundant documentation --- docs/output.md | 2 -- docs/usage.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/output.md b/docs/output.md index c733abb..11baa50 100644 --- a/docs/output.md +++ b/docs/output.md @@ -17,8 +17,6 @@ pipeline has finished. Results for individual samples will be created in subdirectories following the `//` structure. All paths are relative to the top-level results directory. - - The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: diff --git a/docs/usage.md b/docs/usage.md index be5a183..1a4b6de 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -131,8 +131,6 @@ with `params.yaml` containing: ```yaml input: './samplesheet.csv' outdir: './results/' -genome: 'GRCh37' -input: 'data' <...> ``` From 9ca0249e85fc5d947514eb574359b9e3fbf29816 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 08:51:39 +0200 Subject: [PATCH 067/410] Fix linting errors --- bin/check_samplesheet.py | 14 +++----------- nextflow_schema.json | 5 ++++- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index f92f2b3..bfdf198 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -10,18 +10,10 @@ def parse_args(argv=None): Description = "Check contents of nf-core/spatialtranscriptomics samplesheet." Epilog = "Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" parser = argparse.ArgumentParser(description=Description, epilog=Epilog) + parser.add_argument("file_in", help="Input samplesheet file.") + parser.add_argument("file_out", help="Output validated samplesheet file.") parser.add_argument( - "file_in", - help="Input samplesheet file." - ) - parser.add_argument( - "file_out", - help="Output validated samplesheet file." - ) - parser.add_argument( - "--is_raw_data", - action="store_true", - help="Whether input is raw data to be processed by Space Ranger." + "--is_raw_data", action="store_true", help="Whether input is raw data to be processed by Space Ranger." ) return parser.parse_args(argv) diff --git a/nextflow_schema.json b/nextflow_schema.json index 47e222d..441f7c7 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -66,7 +66,7 @@ } }, - "analysis_option": { + "analysis_options": { "title": "Analysis option", "type": "object", "description": "Define options for each tool in the pipeline", @@ -294,6 +294,9 @@ { "$ref": "#/definitions/input_output_options" }, + { + "$ref": "#/definitions/analysis_options" + }, { "$ref": "#/definitions/institutional_config_options" }, From 924f59ecd7026ac5c4cd3e3e65fe5f9f61c170c0 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 09:04:51 +0200 Subject: [PATCH 068/410] Fix more linting errors --- LICENSE | 2 +- nextflow.config | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 13cf00a..4928877 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) Sergii Domanskyi, Jeffrey Chuang, Anuj Srivastava +Copyright (c) Erik Fasterius, Christophe Avenel, Sergii Domanskyi, Jeffrey Chuang, Anuj Srivastava Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/nextflow.config b/nextflow.config index 9f56e7b..70067f8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -54,13 +54,12 @@ includeConfig 'conf/base.config' // Default analysis parameters includeConfig 'conf/analysis.config' -// Load nf-core/spatialtranscriptomics custom profiles from different institutions. -// Warning: Uncomment only if a pipeline-specific instititutional config already exists on nf-core/configs! -// try { -// includeConfig "${params.custom_config_base}/pipeline/spatialtranscriptomics.config" -// } catch (Exception e) { -// System.err.println("WARNING: Could not load nf-core/config/spatialtranscriptomics profiles: ${params.custom_config_base}/pipeline/spatialtranscriptomics.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") +} profiles { debug { From 0064780b18c2aa26d25b79f1a76201940117f14b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 09:07:50 +0200 Subject: [PATCH 069/410] Re-generate custom/dumpsoftwareversions patch --- .../custom-dumpsoftwareversions.diff | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff index 79927d6..0c21dd4 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff +++ b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff @@ -1,7 +1,17 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' --- modules/nf-core/custom/dumpsoftwareversions/meta.yml +++ modules/nf-core/custom/dumpsoftwareversions/meta.yml -@@ -20,10 +20,6 @@ +@@ -1,7 +1,9 @@ ++# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json + name: custom_dumpsoftwareversions + description: Custom module used to dump software versions within the nf-core pipeline template + keywords: + - custom ++ - dump + - version + tools: + - custom: +@@ -20,10 +22,6 @@ type: file description: Standard YML file containing software versions pattern: "software_versions.yml" @@ -15,7 +25,19 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' --- modules/nf-core/custom/dumpsoftwareversions/main.nf +++ modules/nf-core/custom/dumpsoftwareversions/main.nf -@@ -11,9 +11,8 @@ +@@ -2,18 +2,17 @@ + label 'process_single' + + // Requires `pyyaml` which does not have a dedicated container but is in the MultiQC container +- conda "bioconda::multiqc=1.13" ++ conda "bioconda::multiqc=1.14" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? +- 'https://depot.galaxyproject.org/singularity/multiqc:1.13--pyhdfd78af_0' : +- 'quay.io/biocontainers/multiqc:1.13--pyhdfd78af_0' }" ++ 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : ++ 'quay.io/biocontainers/multiqc:1.14--pyhdfd78af_0' }" + + input: path versions output: From f78e242f850693d25c055f25677e859fd5dab969 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 May 2023 09:32:07 +0200 Subject: [PATCH 070/410] Fix output data documentation --- docs/output.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/output.md b/docs/output.md index 11baa50..90430c4 100644 --- a/docs/output.md +++ b/docs/output.md @@ -60,17 +60,15 @@ information about these files at the [10X website](https://support.10xgenomics.c Output files - `/data/` - - `st_adata_norm.h5ad`: Filtered and normalised adata. - - `st_adata_plain.h5ad`: Filtered adata. - `st_adata_processed.h5ad`: Filtered, normalised and clustered adata. - - `st_adata_raw.h5ad`: Raw adata. Data in `.h5ad` format as processed by the pipeline, which can be used for -further downstream analyses if desired. They can also be used by the -[TissUUmaps](https://tissuumaps.github.io/) browser-based tool for visualisation -and exploration, allowing you to delve into the data in an interactive way. +further downstream analyses if desired; unprocessed data is also present in this +file. It can also be used by the [TissUUmaps](https://tissuumaps.github.io/) +browser-based tool for visualisation and exploration, allowing you to delve into +the data in an interactive way. ## Reports From 56d46b460827d1f936124a1ea98e02b7cb3b8111 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 19 May 2023 13:27:46 +0200 Subject: [PATCH 071/410] Fix quarto cache issues on singularity --- .gitignore | 2 ++ conf/modules.config | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 5124c9a..388e635 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ results/ testing/ testing* *.pyc +log +reports diff --git a/conf/modules.config b/conf/modules.config index 714561a..5ff5cb4 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -9,6 +9,10 @@ ext.prefix = File name prefix for output files. ---------------------------------------------------------------------------------------- */ +env { + XDG_CACHE_HOME = "./.quarto_cache_home" + XDG_DATA_HOME = "./.quarto_data_home" +} process { From 7299294183cb87c150b9f6f3e5bd0121f899bb1e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 25 May 2023 17:01:45 +0200 Subject: [PATCH 072/410] Use value channel for spaceranger references --- subworkflows/local/spaceranger.nf | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 986e1c0..17ee97e 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -20,11 +20,10 @@ workflow SPACERANGER { // ch_reference = Channel.empty() if (params.spaceranger_reference) { - ch_reference = Channel - .fromPath ( params.spaceranger_reference, type: "dir", checkIfExists: true ) + ch_reference = file(params.spaceranger_reference, type: "dir", checkIfExists: true) } else { address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" - ch_reference = SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference + ch_reference = SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference.collect() } // @@ -32,8 +31,7 @@ workflow SPACERANGER { // ch_probeset = Channel.empty() if (params.spaceranger_probeset) { - ch_probeset = Channel - .fromPath ( params.spaceranger_probeset, checkIfExists: true ) + ch_probeset = file( params.spaceranger_probeset, checkIfExists: true ) } else { ch_probeset = file ( 'EMPTY_PROBESET' ) } From aa3f4658603b9f9daeac046abce62b51513fd3d0 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 26 May 2023 08:59:35 +0200 Subject: [PATCH 073/410] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5124c9a..135588c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ work/ data/ results/ +log/ +reports/ .DS_Store testing/ testing* From d03b6ebb8870ac027666b65a3c23ad9ee9b59c8b Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 26 May 2023 11:10:56 +0200 Subject: [PATCH 074/410] reset gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 135588c..5124c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ work/ data/ results/ -log/ -reports/ .DS_Store testing/ testing* From 1ba04a414eb801fac1df680ae312a8db8545caab Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 31 May 2023 17:35:00 +0200 Subject: [PATCH 075/410] Use newer samplesheet checker from the template Use the newer samplesheet checker from the nf-core template, which was not previously merged from template PRs. --- bin/check_samplesheet.py | 449 +++++++++++++++++++++++++++------------ 1 file changed, 312 insertions(+), 137 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index bfdf198..470e75c 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -1,167 +1,342 @@ #!/usr/bin/env python + +"""Provide a command line tool to validate and transform tabular samplesheets.""" + + import argparse -import errno -import os +import csv +import logging import sys +# from collections import Counter +from pathlib import Path +logger = logging.getLogger() -def parse_args(argv=None): - Description = "Check contents of nf-core/spatialtranscriptomics samplesheet." - Epilog = "Example usage: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv" - parser = argparse.ArgumentParser(description=Description, epilog=Epilog) - parser.add_argument("file_in", help="Input samplesheet file.") - parser.add_argument("file_out", help="Output validated samplesheet file.") - parser.add_argument( - "--is_raw_data", action="store_true", help="Whether input is raw data to be processed by Space Ranger." + +class RowChecker: + """ + Define a service that can validate and transform each given row. + + Attributes: + modified (list): A list of dicts, where each dict corresponds to a previously + validated and transformed row. The order of rows is maintained. + + """ + + VALID_FORMATS = ( + ".fq.gz", + ".fastq.gz" ) - return parser.parse_args(argv) + def __init__( + self, + area_col="area", + barcodes_col = "barcodes", + fastq_dir_col="fastq_dir", + features_col = "features", + hires_image_col="tissue_hires_image", + lowres_image_col="tissue_lowres_image", + matrix_col = "matrix", + sample_col="sample", + scale_factors_col="scale_factors", + slide_col="slide", + tissue_positions_list_col="tissue_positions_list", + **kwargs, + ): + """ + Initialize the row checker with the expected column names, depending on + whether the input data is raw data (to be processed with Space Ranger) + or processed data. + + Args: + area_col (str): The name of the column that contains the slide area + (default "area"). + barcodes_col (str): The name of the column that contains the + barcodes file path (default "barcodes"). + fastq_dir_col (str): The name of the column that contains the + directory in which the input FASTQ files are stored (default + "fastq_dir"). + features_col (str): The name of the column that contains the + features file path (default "features"). + hires_image_col (str): The name of the column that contains the high + resolution image file path (default "tissue_hires_image"). + lowres_image_col (str): The name of the column that contains the low + resolution image file path (default "tissue_lowres_image"). + matrix_col (str): The name of the column that contains the matrix + matrix file path (default "matrix"). + sample_col (str): The name of the column that contains the sample name + (default "sample"). + scale_factors_col (str): The name of the column that contains the + scale factors file path (default "scale_factors"). + slide_col (str): The name of the column that contains the slide ID + (default "slide"). + tissue_positions_list_col (str): The column that contains the tissue + poositions list file path (default "tissue_positions_list"). + + """ + super().__init__(**kwargs) + self._area_col = area_col + self._barcodes_col = barcodes_col + self._fastq_dir_col = fastq_dir_col + self._features_col = features_col + self._hires_image_col = hires_image_col + self._lowres_image_col = lowres_image_col + self._matrix_col = matrix_col + self._sample_col = sample_col + self._scale_factors_col = scale_factors_col + self._slide_col = slide_col + self._tissue_positions_list_col = tissue_positions_list_col + self.modified = [] + + def validate_and_transform(self, row, is_raw_data): + """ + Perform all validations on the given row and insert the read pairing status. + + Args: + row (dict): A mapping from column headers (keys) to elements of that row + (values). + + """ + if is_raw_data: + self._validate_sample(row) + self._validate_fastq_dir(row) + self._validate_hires_image(row) + self._validate_slide(row) + self._validate_area(row) + self.modified.append(row) + else: + self._validate_sample(row) + self._validate_tissue_positions_list(row) + self._validate_lowres_image(row) + self._validate_hires_image(row) + self._validate_scale_factors(row) + self._validate_barcodes(row) + self._validate_features(row) + self._validate_matrix(row) + self.modified.append(row) + + def _validate_sample(self, row): + """Assert that the sample name exists and convert spaces to underscores.""" + if len(row[self._sample_col]) <= 0: + raise AssertionError("Sample input is required.") + # Sanitize samples slightly. + row[self._sample_col] = row[self._sample_col].replace(" ", "_") + + def _validate_fastq_dir(self, row): + """Assert that the FASTQ directory entry is non-empty.""" + # TODO: Possibly add a check to make sure that at least one FASTQ file + # exists in the specified directory + if len(row[self._fastq_dir_col]) <= 0: + raise AssertionError("The directory in which the FASTQ files are stored is required") + + def _validate_hires_image(self, row): + """Assert that the high resolution image entry exists.""" + # TODO: Possibly add a check for image formats + if len(row[self._hires_image_col]) <= 0: + raise AssertionError("The high resolution image is required") + + def _validate_slide(self, row): + """Assert that the slide entry exists.""" + # TODO: Possibly add a check for valid slide IDs + if len(row[self._slide_col]) <= 0: + raise AssertionError("The slide ID is required") + + def _validate_area(self, row): + """Assert that the slide area exists.""" + # TODO: Possibly add a check for valid area specifications + if len(row[self._area_col]) <= 0: + raise AssertionError("The area is required") + + def _validate_tissue_positions_list(self, row): + """Assert that the tissue positions list entry exists.""" + # TODO: Add a CSV file check + if len(row[self._hires_image_col]) <= 0: + raise AssertionError("The high resolution image is required") + + def _validate_lowres_image(self, row): + """Assert that the low resolution image entry exists.""" + # TODO: Possibly add a check for image formats + if len(row[self._lowres_image_col]) <= 0: + raise AssertionError("The low resolution image is required") + + def _validate_scale_factors(self, row): + """Assert that the scale factors entry exists.""" + # TODO: Possibly add a JSON format check + if len(row[self._scale_factors_col]) <= 0: + raise AssertionError("The scale factors file is required") + + def _validate_barcodes(self, row): + """Assert that the barcodes entry exists.""" + # TODO: Possibly add a check for TSV file formats + if len(row[self._barcodes_col]) <= 0: + raise AssertionError("The barcodes file is required") + + def _validate_features(self, row): + """Assert that the features entry exists.""" + # TODO: Possibly add a check for TSV file formats + if len(row[self._features_col]) <= 0: + raise AssertionError("The features file is required") + + def _validate_matrix(self, row): + """Assert that the matrix entry exists.""" + # TODO: Possibly add a check for MTX file formats + if len(row[self._matrix_col]) <= 0: + raise AssertionError("The matrix file is required") -def make_dir(path): - if len(path) > 0: - try: - os.makedirs(path) - except OSError as exception: - if exception.errno != errno.EEXIST: - raise exception +def read_head(handle, num_lines=10): + """Read the specified number of lines from the current position in the file.""" + lines = [] + for idx, line in enumerate(handle): + if idx == num_lines: + break + lines.append(line) + return "".join(lines) -def print_error(error, context="Line", context_str=""): - error_str = "ERROR: Please check samplesheet -> {}".format(error) - if context != "" and context_str != "": - error_str = "ERROR: Please check samplesheet -> {}\n{}: '{}'".format( - error, context.strip(), context_str.strip() - ) - print(error_str) - sys.exit(1) + +def sniff_format(handle): + """ + Detect the tabular format. + + Args: + handle (text file): A handle to a `text file`_ object. The read position is + expected to be at the beginning (index 0). + + Returns: + csv.Dialect: The detected tabular format. + + .. _text file: + https://docs.python.org/3/glossary.html#term-text-file + + """ + peek = read_head(handle) + handle.seek(0) + sniffer = csv.Sniffer() + dialect = sniffer.sniff(peek) + return dialect def check_samplesheet(file_in, file_out, is_raw_data): """ Check that the tabular samplesheet has the structure expected by nf-core pipelines. - Validate the general shape of the table, expected columns and each row. + + Validate the general shape of the table, expected columns, and each row. Also add + an additional column which records whether one or two FASTQ reads were found. Args: - file_in (pathlib.Path): The given tabular samplesheet. - file_out (pathlib.Path): Where the validated samplesheet should be created. - is_raw_data (boolean): Whether the input is raw spatial data to be processed by Space Ranger. + file_in (pathlib.Path): The given tabular samplesheet. The format can be either + CSV, TSV, or any other format automatically recognized by ``csv.Sniffer``. + file_out (pathlib.Path): Where the validated and transformed samplesheet should + be created; always in CSV format. + is_raw_data (boolean): Whether the data is raw data to be processed by + Space Ranger or not. - The following structure is expected: - sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix - SAMPLE,TISSUE_POSITIONS_LIST.csv,TISSUE_LOWRES_IMAGE.png,TISSUE_HIGHRES_IMAGE.png,SCALEFACTORS_JSON.json,BARCODES.tsv.gz,FEATURES.tsv.gz,MATRIX.mtx.gz + Example (processed data): + This function checks that the samplesheet follows the following + structure by default, see also the `spatial transcriptomics + samplesheet`_:: - For a complete example see: - https://data.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv - """ + sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix + SAMPLE,TISSUE_POSITIONS_LIST.csv,TISSUE_LOWRES_IMAGE.png,tissue_hires_image.png,SCALEFACTORS_JSON.json,BARCODES.tsv.gz,FEATURES.tsv.gz,MATRIX.mtx.gz - sample_mapping_dict = {} - with open(file_in, "r") as fin: - # Get cols and header depending on samplesheet type - if is_raw_data: - MIN_COLS = 4 - HEADER = ["sample", "fastq_dir", "tissue_hires_image", "slide", "area"] - else: - MIN_COLS = 7 - HEADER = [ - "sample", - "tissue_positions_list", - "tissue_lowres_image", - "tissue_hires_image", - "scale_factors", - "barcodes", - "features", - "matrix", - ] - - # Check header - header = [x.strip('"') for x in fin.readline().strip().split(",")] - if header[: len(HEADER)] != HEADER: - print("ERROR: Please check samplesheet header -> {} != {}".format(",".join(header), ",".join(HEADER))) - sys.exit(1) + .. _spatial transcriptomics samplesheet: + https://data.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv + + Example (raw data): + This function check that the samplesheet follows the following structure + if the `--is_raw_data` parameter is set: + + sample,fastq_dir,tissue_hires_image,slide,area + SAMPLE,FASTQ_DIRECTORY,TISSUE_HIRES_IMAGE.png,SLIDE_ID,SLIDE_AREA - # Check sample entries - for line in fin: - lspl = [x.strip().strip('"') for x in line.strip().split(",")] - - # Check valid number of columns per row - if len(lspl) < len(HEADER): - print_error( - "Invalid number of columns (minimum = {})!".format(len(HEADER)), - "Line", - line, - ) - num_cols = len([x for x in lspl if x]) - if num_cols < MIN_COLS: - print_error( - "Invalid number of populated columns (minimum = {})!".format(MIN_COLS), - "Line", - line, - ) - - # Check sample name entries - if is_raw_data: - ( - sample, - fastq_dir, - tissue_hires_image, - slide, - area, - ) = lspl[: len(HEADER)] - sample_info = [ - fastq_dir, - tissue_hires_image, - slide, - area, - ] - else: - ( - sample, - tissue_positions_list, - tissue_lowres_image, - tissue_hires_image, - scale_factors, - barcodes, - features, - matrix, - ) = lspl[: len(HEADER)] - sample_info = [ - tissue_positions_list, - tissue_lowres_image, - tissue_hires_image, - scale_factors, - barcodes, - features, - matrix, - ] - sample = sample.replace(" ", "_") - if not sample: - print_error("Sample entry has not been specified!", "Line", line) - - # Create sample mapping dictionary - if sample not in sample_mapping_dict: - sample_mapping_dict[sample] = [sample_info] - else: - if sample_info in sample_mapping_dict[sample]: - print_error("Samplesheet contains duplicate rows!", "Line", line) - else: - sample_mapping_dict[sample].append(sample_info) - - # Write validated samplesheet with appropriate columns - if len(sample_mapping_dict) > 0: - out_dir = os.path.dirname(file_out) - make_dir(out_dir) - with open(file_out, "w") as fout: - fout.write(",".join(HEADER) + "\n") - for sample in sorted(sample_mapping_dict.keys()): - for idx, val in enumerate(sample_mapping_dict[sample]): - fout.write(",".join(["{}".format(sample, idx + 1)] + val) + "\n") + """ + if is_raw_data: + required_columns = { + "sample", + "fastq_dir", + "tissue_hires_image", + "slide", + "area" + } else: - print_error("No entries to process!", "Samplesheet: {}".format(file_in)) + required_columns = { + "sample", + "tissue_positions_list", + "tissue_lowres_image", + "tissue_hires_image", + "scale_factors", + "barcodes", + "features", + "matrix" + } + # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. + with file_in.open(newline="") as in_handle: + reader = csv.DictReader(in_handle, dialect=sniff_format(in_handle)) + # Validate the existence of the expected header columns. + if not required_columns.issubset(reader.fieldnames): + req_cols = ", ".join(required_columns) + logger.critical(f"The sample sheet **must** contain these column headers: {req_cols}.") + sys.exit(1) + # Validate each row. + checker = RowChecker() + for i, row in enumerate(reader): + try: + checker.validate_and_transform(row, is_raw_data) + except AssertionError as error: + logger.critical(f"{str(error)} On line {i + 2}.") + sys.exit(1) + header = list(reader.fieldnames) + # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. + with file_out.open(mode="w", newline="") as out_handle: + writer = csv.DictWriter(out_handle, header, delimiter=",") + writer.writeheader() + for row in checker.modified: + writer.writerow(row) + + +def parse_args(argv=None): + """Define and immediately parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Validate and transform a tabular samplesheet.", + epilog="Example: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv", + ) + parser.add_argument( + "file_in", + metavar="FILE_IN", + type=Path, + help="Tabular input samplesheet in CSV or TSV format.", + ) + parser.add_argument( + "file_out", + metavar="FILE_OUT", + type=Path, + help="Transformed output samplesheet in CSV format.", + ) + parser.add_argument( + "-r", + "--is_raw_data", + action="store_true", + help="Transformed output samplesheet in CSV format.", + ) + parser.add_argument( + "-l", + "--log-level", + help="The desired log level (default WARNING).", + choices=("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"), + default="WARNING", + ) + return parser.parse_args(argv) def main(argv=None): + """Coordinate argument parsing and program execution.""" args = parse_args(argv) + logging.basicConfig(level=args.log_level, format="[%(levelname)s] %(message)s") + if not args.file_in.is_file(): + logger.error(f"The given input file {args.file_in} was not found!") + sys.exit(2) + args.file_out.parent.mkdir(parents=True, exist_ok=True) check_samplesheet(args.file_in, args.file_out, args.is_raw_data) From e2fd6981403d29d495d7897d3d6cc637ed780916 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sat, 3 Jun 2023 12:02:57 +0200 Subject: [PATCH 076/410] Fix samplechecker subworkflow diagnostic outputs --- subworkflows/local/input_check.nf | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 56ccbd7..e28e02b 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -33,10 +33,10 @@ def create_spaceranger_channels(LinkedHashMap row) { def array = [] if (!file(row.fastq_dir).exists()) { - exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_dir}" } if (!file(row.tissue_hires_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" } array = [ meta, @@ -54,29 +54,29 @@ def create_visium_channels(LinkedHashMap row) { def meta = [:] meta.id = row.sample - def array = [] + def processed_meta = [] if (!file(row.tissue_positions_list).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_positions_list file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> tissue_positions_list file does not exist!\n${row.tissue_positions_list}" } if (!file(row.tissue_lowres_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_lowres_image file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> tissue_lowres_image file does not exist!\n${row.tissue_lowres_image}" } if (!file(row.tissue_hires_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" } if (!file(row.scale_factors).exists()) { - exit 1, "ERROR: Please check input samplesheet -> scale_factors file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> scale_factors file does not exist!\n${row.scale_factors}" } if (!file(row.barcodes).exists()) { - exit 1, "ERROR: Please check input samplesheet -> barcodes file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> barcodes file does not exist!\n${row.barcodes}" } if (!file(row.features).exists()) { - exit 1, "ERROR: Please check input samplesheet -> features file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> features file does not exist!\n${row.features}" } if (!file(row.matrix).exists()) { - exit 1, "ERROR: Please check input samplesheet -> matrix file does not exist!\n${row.fastq_1}" + exit 1, "ERROR: Please check input samplesheet -> matrix file does not exist!\n${row.matrix}" } - array = [ + processed_meta = [ meta, file(row.tissue_positions_list), file(row.tissue_lowres_image), @@ -86,5 +86,5 @@ def create_visium_channels(LinkedHashMap row) { file(row.features), file(row.matrix) ] - return array + return processed_meta } From cbdb94020cba8298b20eba1332e847030f1c21f5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sat, 3 Jun 2023 12:02:21 +0200 Subject: [PATCH 077/410] Move manual alignment file into samplesheet Move the specification of manual alignment files into the samplesheet, instead of having a single alignment file for the whole pipeline; this worked in testing because of mostly testing single samples at a time, but does not work in real-world applications. Relevant parameters and documentation has been altered accordingly. --- assets/samplesheet_spaceranger.csv | 4 ++-- bin/check_samplesheet.py | 10 +++++++++- docs/usage.md | 16 ++++++++++------ modules/local/spaceranger_count.nf | 3 +-- nextflow.config | 1 - nextflow_schema.json | 8 -------- subworkflows/local/input_check.nf | 15 ++++++++++++--- subworkflows/local/spaceranger.nf | 14 +------------- workflows/spatialtranscriptomics.nf | 3 +-- 9 files changed, 36 insertions(+), 38 deletions(-) diff --git a/assets/samplesheet_spaceranger.csv b/assets/samplesheet_spaceranger.csv index 83a4550..679d18b 100644 --- a/assets/samplesheet_spaceranger.csv +++ b/assets/samplesheet_spaceranger.csv @@ -1,2 +1,2 @@ -sample,fastq_dir,tissue_hires_image,slide,area -Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer/Visium_FFPE_Human_Cervical_Cancer_image.jpg,V10L13-019,A1 +sample,fastq_dir,tissue_hires_image,slide,area,manual_alignment +Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer/Visium_FFPE_Human_Cervical_Cancer_image.jpg,V10L13-019,A1, diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 470e75c..3b8a418 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -38,6 +38,7 @@ def __init__( hires_image_col="tissue_hires_image", lowres_image_col="tissue_lowres_image", matrix_col = "matrix", + manual_alignment_col = "manual_alignment", sample_col="sample", scale_factors_col="scale_factors", slide_col="slide", @@ -82,6 +83,7 @@ def __init__( self._features_col = features_col self._hires_image_col = hires_image_col self._lowres_image_col = lowres_image_col + self._manual_alignment_col = manual_alignment_col self._matrix_col = matrix_col self._sample_col = sample_col self._scale_factors_col = scale_factors_col @@ -104,6 +106,7 @@ def validate_and_transform(self, row, is_raw_data): self._validate_hires_image(row) self._validate_slide(row) self._validate_area(row) + self._validate_manual_alignment(row) self.modified.append(row) else: self._validate_sample(row) @@ -148,6 +151,10 @@ def _validate_area(self, row): if len(row[self._area_col]) <= 0: raise AssertionError("The area is required") + def _validate_manual_alignment(self, row): + """Assert that the manual alignment entry has the right format if it exists.""" + return + def _validate_tissue_positions_list(self, row): """Assert that the tissue positions list entry exists.""" # TODO: Add a CSV file check @@ -257,7 +264,8 @@ def check_samplesheet(file_in, file_out, is_raw_data): "fastq_dir", "tissue_hires_image", "slide", - "area" + "area", + "manual_alignment" } else: required_columns = { diff --git a/docs/usage.md b/docs/usage.md index 1a4b6de..e83ccca 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,9 +22,9 @@ The samplesheet for raw spatial data yet to be analysed with Space Ranger is specified like so: ```no-highlight -sample,fastq_dir,tissue_hires_image,slide,area -SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1 -SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 +sample,fastq_dir,tissue_hires_image,slide,area,manual_alignment +SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1, +SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1, ``` | Column | Description | @@ -34,6 +34,11 @@ SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 | `tissue_hires_image` | Path to the high-resolution image for the sample. | | `slide` | The Visium slide ID used for the sequencing. | | `area` | Which slide area contains the tissue sample. | +| `manual_alignment` | Path to the manual alignment file (optional) + +> **NB:** The `manual_alignment` column is only required for samples for which a +> manual alignment file is needed and can be ignored if you're using automatic +> alignment. If you are unsure, please see the Visium documentation for details regarding the different variants of [FASTQ directory structures](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/fastq-input) @@ -82,9 +87,8 @@ path to its directory (or another link from the 10X website above) using the `--spaceranger_reference` parameter, otherwise the pipeline will download the default human reference for you automatically. -You may optionally supply file paths to probe sets or manual fiducial alignment -using the `--spaceranger_probeset` and `--spaceranger_manual_alignment`, -respectively. +You may optionally supply file path to a probe sets using the +`--spaceranger_probeset` parameter. ## Analysis options diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index e50ec51..1f68365 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -10,10 +10,9 @@ process SPACERANGER_COUNT { container "docker.io/nfcore/spaceranger:1.3.0" input: - tuple val(meta), path(fastq_dir), path(image), val(slide), val(area) + tuple val(meta), path(fastq_dir), path(image), val(slide), val(area), path(manual_alignment) path(reference) path(probeset) - path(manual_alignment) output: path "spaceranger", type: "dir" , emit: sr_dir diff --git a/nextflow.config b/nextflow.config index 70067f8..924e6e9 100644 --- a/nextflow.config +++ b/nextflow.config @@ -16,7 +16,6 @@ params { run_spaceranger = false spaceranger_reference = null spaceranger_probeset = null - spaceranger_manual_alignment = null // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 441f7c7..773c2e4 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -48,14 +48,6 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, - "spaceranger_manual_alignment": { - "type": "string", - "format": "file-path", - "mimetype": "text/csv", - "pattern": "^\\S+\\.json$", - "description": "Location of Space Ranger manual alignment file", - "fa_icon": "fas fa-file-csv" - }, "email": { "type": "string", "description": "Email address for completion summary.", diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index e28e02b..d54ced4 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -31,21 +31,30 @@ def create_spaceranger_channels(LinkedHashMap row) { def meta = [:] meta.id = row.sample - def array = [] + def raw_meta = [] if (!file(row.fastq_dir).exists()) { exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_dir}" } if (!file(row.tissue_hires_image).exists()) { exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" } - array = [ + if ( row.manual_alignment.isEmpty() ) { + manual_alignment = file ( "EMPTY_ALIGNMENT" ) + } else { + if (!file(row.manual_alignment).exists()) { + exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" + } + manual_alignment = file ( row.manual_alignment ) + } + raw_meta = [ meta, file(row.fastq_dir), file(row.tissue_hires_image), row.slide, row.area, + manual_alignment ] - return array + return raw_meta } // Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 17ee97e..4952d7b 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -36,25 +36,13 @@ workflow SPACERANGER { ch_probeset = file ( 'EMPTY_PROBESET' ) } - // - // Optional: manual alignment file - // - ch_manual_alignment = Channel.empty() - if (params.spaceranger_manual_alignment) { - ch_manual_alignment = Channel - .fromPath ( params.spaceranger_manual_alignment, checkIfExists: true ) - } else { - ch_manual_alignment = file ( 'EMPTY_ALIGNMENT' ) - } - // // Run Space Ranger count // SPACERANGER_COUNT ( ch_st_data, ch_reference, - ch_probeset, - ch_manual_alignment + ch_probeset ) ch_versions = ch_versions.mix(SPACERANGER_COUNT.out.versions.first()) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 0dc4cb6..712f11e 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -17,8 +17,7 @@ log.info """\ def checkPathParamList = [ params.input, params.spaceranger_reference, - params.spaceranger_probeset, - params.spaceranger_manual_alignment ] + params.spaceranger_probeset ] for (param in checkPathParamList) { if (param) { file(param, checkIfExists: true) } } // Check mandatory parameters From b4d1d951a678c0bd10cfda66f5163863c7e20a65 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 5 Jun 2023 07:05:48 +0200 Subject: [PATCH 078/410] Fix linting --- bin/check_samplesheet.py | 25 ++++++++----------------- docs/usage.md | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 3b8a418..664a329 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -8,6 +8,7 @@ import csv import logging import sys + # from collections import Counter from pathlib import Path @@ -24,21 +25,18 @@ class RowChecker: """ - VALID_FORMATS = ( - ".fq.gz", - ".fastq.gz" - ) + VALID_FORMATS = (".fq.gz", ".fastq.gz") def __init__( self, area_col="area", - barcodes_col = "barcodes", + barcodes_col="barcodes", fastq_dir_col="fastq_dir", - features_col = "features", + features_col="features", hires_image_col="tissue_hires_image", lowres_image_col="tissue_lowres_image", - matrix_col = "matrix", - manual_alignment_col = "manual_alignment", + matrix_col="matrix", + manual_alignment_col="manual_alignment", sample_col="sample", scale_factors_col="scale_factors", slide_col="slide", @@ -259,14 +257,7 @@ def check_samplesheet(file_in, file_out, is_raw_data): """ if is_raw_data: - required_columns = { - "sample", - "fastq_dir", - "tissue_hires_image", - "slide", - "area", - "manual_alignment" - } + required_columns = {"sample", "fastq_dir", "tissue_hires_image", "slide", "area", "manual_alignment"} else: required_columns = { "sample", @@ -276,7 +267,7 @@ def check_samplesheet(file_in, file_out, is_raw_data): "scale_factors", "barcodes", "features", - "matrix" + "matrix", } # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. with file_in.open(newline="") as in_handle: diff --git a/docs/usage.md b/docs/usage.md index e83ccca..ac8773c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -34,7 +34,7 @@ SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1, | `tissue_hires_image` | Path to the high-resolution image for the sample. | | `slide` | The Visium slide ID used for the sequencing. | | `area` | Which slide area contains the tissue sample. | -| `manual_alignment` | Path to the manual alignment file (optional) +| `manual_alignment` | Path to the manual alignment file (optional) | > **NB:** The `manual_alignment` column is only required for samples for which a > manual alignment file is needed and can be ignored if you're using automatic From 73604a1847db832bb430a5a9fd49d12d8682f647 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 5 Jun 2023 14:25:31 +0200 Subject: [PATCH 079/410] Add missing `when:` directives --- modules/local/spaceranger_count.nf | 3 +++ modules/local/spaceranger_download_probeset.nf | 3 +++ modules/local/spaceranger_download_reference.nf | 3 +++ modules/local/st_clustering.nf | 3 +++ modules/local/st_qc_and_normalisation.nf | 3 +++ modules/local/st_read_data.nf | 3 +++ modules/local/st_spatial_de.nf | 3 +++ 7 files changed, 21 insertions(+) diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index e50ec51..1d3a2fc 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -27,6 +27,9 @@ process SPACERANGER_COUNT { path ("*/outs/raw_feature_bc_matrix/matrix.mtx.gz") , emit: sr_out path "versions.yml" , emit: versions + when: + task.ext.when == null || task.ext.when + script: def probeset = probeset.name != 'EMPTY_PROBESET' ? "--probe-set=${probeset}" : '' def manual_alignment = manual_alignment.name != 'EMPTY_ALIGNMENT' ? "--loupe-alignment=${manual_alignment}" : '' diff --git a/modules/local/spaceranger_download_probeset.nf b/modules/local/spaceranger_download_probeset.nf index be043dc..7566351 100644 --- a/modules/local/spaceranger_download_probeset.nf +++ b/modules/local/spaceranger_download_probeset.nf @@ -19,6 +19,9 @@ process SPACERANGER_DOWNLOAD_PROBESET { path("*.csv") , emit: probeset path("versions.yml"), emit: versions + when: + task.ext.when == null || task.ext.when + script: probeset = address.tokenize("/").last() name = probeset.take(probeset.lastIndexOf('.')) diff --git a/modules/local/spaceranger_download_reference.nf b/modules/local/spaceranger_download_reference.nf index 22c79c3..3c105de 100644 --- a/modules/local/spaceranger_download_reference.nf +++ b/modules/local/spaceranger_download_reference.nf @@ -19,6 +19,9 @@ process SPACERANGER_DOWNLOAD_REFERENCE { path "${name}", type: "dir", emit: reference path "versions.yml" , emit: versions + when: + task.ext.when == null || task.ext.when + script: reference = address.tokenize("/").last() name = reference.split("\\.", 2)[0] diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index a2e844b..eaaac1b 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -21,6 +21,9 @@ process ST_CLUSTERING { tuple val(meta), path("st_clustering_files") , emit: html_files path("versions.yml") , emit: versions + when: + task.ext.when == null || task.ext.when + script: """ quarto render ${report} \ diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 0588b49..fdcaec9 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -22,6 +22,9 @@ process ST_QC_AND_NORMALISATION { tuple val(meta), path("st_qc_and_normalisation_files"), emit: html_files path("versions.yml") , emit: versions + when: + task.ext.when == null || task.ext.when + script: """ quarto render ${report} \ diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index 4808781..3d20e1d 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -25,6 +25,9 @@ process ST_READ_DATA { tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw path("versions.yml") , emit: versions + when: + task.ext.when == null || task.ext.when + script: """ read_st_data.py \ diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 054ec12..75359e4 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -21,6 +21,9 @@ process ST_SPATIAL_DE { tuple val(meta), path("st_spatial_de_files"), emit: html_files path("versions.yml") , emit: versions + when: + task.ext.when == null || task.ext.when + script: """ quarto render ${report} \ From d10d5510c3510dc39f903ece18593047373804e6 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 6 Jun 2023 13:29:02 +0200 Subject: [PATCH 080/410] Bootstrap nf-test --- .gitattributes | 1 + .github/workflows/ci.yml | 37 ++++++++++++++----- .gitignore | 2 + .nf-core.yml | 4 ++ nf-test.config | 8 ++++ tests/config/tags.yml | 10 +++++ tests/modules/local/.gitkeep | 0 tests/pipeline/spatialtranscriptomics.nf.test | 22 +++++++++++ 8 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 nf-test.config create mode 100644 tests/config/tags.yml create mode 100644 tests/modules/local/.gitkeep create mode 100644 tests/pipeline/spatialtranscriptomics.nf.test diff --git a/.gitattributes b/.gitattributes index 7a2dabc..31ba574 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.nf.test linguist-language=nextflow modules/nf-core/** linguist-generated subworkflows/nf-core/** linguist-generated +tests/**/*nf.test.snap linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca31d9d..b5c5c4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,15 @@ on: branches: - dev pull_request: + branches: + - dev + - master release: types: [published] env: NXF_ANSI_LOG: false + CAPSULE_LOG: none concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" @@ -18,26 +22,39 @@ concurrency: jobs: test: name: Run pipeline with test data - # 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/spatialtranscriptomics') }}" runs-on: ubuntu-latest strategy: matrix: - NXF_VER: - - "22.10.1" - - "latest-everything" + NXF_VER: ["latest-everything"] steps: - name: Check out pipeline code uses: actions/checkout@v3 + # Install Nextflow - name: Install Nextflow uses: nf-core/setup-nextflow@v1 with: version: "${{ matrix.NXF_VER }}" - - name: Run pipeline with test data - # 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 + # Install nf-test + - name: Install nf-test run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,docker --outdir ./results + wget -qO- https://code.askimed.com/install/nf-test | bash + sudo mv nf-test /usr/local/bin/ + + # Run nf-test + - name: Run nf-test + run: nf-test test tests/pipeline/ --tap=test.tap + + # If the test fails, output the software_versions.yml using the 'batcat' utility + - name: Output log on failure + if: failure() + run: | + sudo apt install bat > /dev/null + batcat --decorations=always --color=always /home/runner/work/fetchngs/fetchngs/.nf-test/tests/*/output/pipeline_info/software_versions.yml + + # Comment on the PR with the test results + - name: PR comment with file + uses: thollander/actions-comment-pull-request@v2 + with: + filePath: test.tap diff --git a/.gitignore b/.gitignore index 5124c9a..935326a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ results/ testing/ testing* *.pyc +.nf-test/ +nf-test diff --git a/.nf-core.yml b/.nf-core.yml index 3805dc8..3b33744 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1 +1,5 @@ repository_type: pipeline +lint: + files_unchanged: + - .gitattributes + actions_ci: False diff --git a/nf-test.config b/nf-test.config new file mode 100644 index 0000000..f345cb2 --- /dev/null +++ b/nf-test.config @@ -0,0 +1,8 @@ +config { + + testsDir "tests" + workDir ".nf-test" + configFile "nextflow.config" + profile "test,docker" + +} diff --git a/tests/config/tags.yml b/tests/config/tags.yml new file mode 100644 index 0000000..dce2348 --- /dev/null +++ b/tests/config/tags.yml @@ -0,0 +1,10 @@ +default: + - bin/** + - conf/** + - lib/** + - modules/** + - subworkflows/** + - tests/** + - workflows/** + - nextflow.config + - main.nf diff --git a/tests/modules/local/.gitkeep b/tests/modules/local/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test new file mode 100644 index 0000000..0c3a3c0 --- /dev/null +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -0,0 +1,22 @@ +nextflow_pipeline { + name "Test Spatialtranscriptomics QC workflow" + script "main.nf" + tag "pipeline" + + test("QC reports") { + when { + params { + outdir = "$outputDir" + } + } + + then { + def softwareVersions = path("$outputDir/pipeline_info/software_versions.yml").yaml + if (softwareVersions.containsKey("Workflow")) { softwareVersions.Workflow.remove("Nextflow") } + + assertAll( + { assert workflow.success }, + ) + } + } +} From e1a44bd776307c37d1cf93af9533d3511e8cad92 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 09:00:54 +0200 Subject: [PATCH 081/410] Remove PR comment feature --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5c5c4c..4f62074 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,3 @@ jobs: run: | sudo apt install bat > /dev/null batcat --decorations=always --color=always /home/runner/work/fetchngs/fetchngs/.nf-test/tests/*/output/pipeline_info/software_versions.yml - - # Comment on the PR with the test results - - name: PR comment with file - uses: thollander/actions-comment-pull-request@v2 - with: - filePath: test.tap From 9827fe40049ebcabdc1cc29c024f0a46b5b9e849 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 09:26:40 +0200 Subject: [PATCH 082/410] Add snap --- tests/pipeline/spatialtranscriptomics.nf.test | 10 ++++++++- .../spatialtranscriptomics.nf.test.snap | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/pipeline/spatialtranscriptomics.nf.test.snap diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index 0c3a3c0..547cb8c 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -3,7 +3,7 @@ nextflow_pipeline { script "main.nf" tag "pipeline" - test("QC reports") { + test("Spatialtranscriptomics outputs") { when { params { outdir = "$outputDir" @@ -16,6 +16,14 @@ nextflow_pipeline { assertAll( { assert workflow.success }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad")).match("anndata") }, + { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv")).match("spatial_de") }, + { assert snapshot( + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html") + ).match("reports")} ) } } diff --git a/tests/pipeline/spatialtranscriptomics.nf.test.snap b/tests/pipeline/spatialtranscriptomics.nf.test.snap new file mode 100644 index 0000000..601f3ef --- /dev/null +++ b/tests/pipeline/spatialtranscriptomics.nf.test.snap @@ -0,0 +1,22 @@ +{ + "reports": { + "content": [ + "st_clustering.html:md5,c0eef3dad596f1489d30a0172871168d", + "st_qc_and_normalisation.html:md5,dc7cbe12e1048f6d913b222e57e0c534", + "st_spatial_de.html:md5,b486defdc23bf1f0d2c71b16126cad8c" + ], + "timestamp": "2023-06-07T07:25:08+0000" + }, + "anndata": { + "content": [ + "st_adata_processed.h5ad:md5,48dea93087c1ad0f78bfd0e65d8ba9f2" + ], + "timestamp": "2023-06-07T07:25:08+0000" + }, + "spatial_de": { + "content": [ + "st_spatial_de.csv:md5,fd1b6818b859d0d7a58a62b6e26925ae" + ], + "timestamp": "2023-06-07T07:25:08+0000" + } +} \ No newline at end of file From a34d3c26f08abeac352f7be5e983fd1934510e4f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 09:55:26 +0200 Subject: [PATCH 083/410] Add snapshot for test --- CHANGELOG.md | 6 +++++- tests/pipeline/lib/UTILS.groovy | 11 +++++++++++ tests/pipeline/spatialtranscriptomics.nf.test | 6 +++--- .../spatialtranscriptomics.nf.test.snap | 17 ++++++++--------- 4 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 tests/pipeline/lib/UTILS.groovy diff --git a/CHANGELOG.md b/CHANGELOG.md index b61f70f..3491598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## v1.0dev - [date] +## [Unreleased] + +- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] + +## v1.0 - 2023-03-31 Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. diff --git a/tests/pipeline/lib/UTILS.groovy b/tests/pipeline/lib/UTILS.groovy new file mode 100644 index 0000000..a76b18a --- /dev/null +++ b/tests/pipeline/lib/UTILS.groovy @@ -0,0 +1,11 @@ +// Function to remove Nextflow version from software_versions.yml + +class UTILS { + public static String removeNextflowVersion(outputDir) { + def softwareVersions = path("$outputDir/pipeline_info/software_versions.yml").yaml + if (softwareVersions.containsKey("Workflow")) { + softwareVersions.Workflow.remove("Nextflow") + } + return softwareVersions + } +} \ No newline at end of file diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index 547cb8c..189e25e 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -18,12 +18,12 @@ nextflow_pipeline { { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad")).match("anndata") }, - { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv")).match("spatial_de") }, + { assert path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, { assert snapshot( path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html") - ).match("reports")} + ).match("reports")}, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } ) } } diff --git a/tests/pipeline/spatialtranscriptomics.nf.test.snap b/tests/pipeline/spatialtranscriptomics.nf.test.snap index 601f3ef..1043631 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test.snap +++ b/tests/pipeline/spatialtranscriptomics.nf.test.snap @@ -2,21 +2,20 @@ "reports": { "content": [ "st_clustering.html:md5,c0eef3dad596f1489d30a0172871168d", - "st_qc_and_normalisation.html:md5,dc7cbe12e1048f6d913b222e57e0c534", - "st_spatial_de.html:md5,b486defdc23bf1f0d2c71b16126cad8c" + "st_qc_and_normalisation.html:md5,dc7cbe12e1048f6d913b222e57e0c534" ], - "timestamp": "2023-06-07T07:25:08+0000" + "timestamp": "2023-06-07T07:45:37+0000" }, - "anndata": { + "software_versions": { "content": [ - "st_adata_processed.h5ad:md5,48dea93087c1ad0f78bfd0e65d8ba9f2" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.8.3}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-07T07:25:08+0000" + "timestamp": "2023-06-07T07:45:37+0000" }, - "spatial_de": { + "anndata": { "content": [ - "st_spatial_de.csv:md5,fd1b6818b859d0d7a58a62b6e26925ae" + "st_adata_processed.h5ad:md5,48dea93087c1ad0f78bfd0e65d8ba9f2" ], - "timestamp": "2023-06-07T07:25:08+0000" + "timestamp": "2023-06-07T07:45:37+0000" } } \ No newline at end of file From 1c994b132d9a14d7fb4eabb2802cd4f189288454 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 09:59:35 +0200 Subject: [PATCH 084/410] Fix editorconfig --- tests/pipeline/lib/UTILS.groovy | 2 +- tests/pipeline/spatialtranscriptomics.nf.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pipeline/lib/UTILS.groovy b/tests/pipeline/lib/UTILS.groovy index a76b18a..311403c 100644 --- a/tests/pipeline/lib/UTILS.groovy +++ b/tests/pipeline/lib/UTILS.groovy @@ -8,4 +8,4 @@ class UTILS { } return softwareVersions } -} \ No newline at end of file +} diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index 189e25e..69c68b7 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -18,7 +18,7 @@ nextflow_pipeline { { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad")).match("anndata") }, - { assert path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, { assert snapshot( path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), From 952f6c750c0bf86ca61728f0e0cd099824f815b0 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 10:20:21 +0200 Subject: [PATCH 085/410] Fix test with unstable checksum --- .github/workflows/ci.yml | 2 +- tests/pipeline/spatialtranscriptomics.nf.test | 5 +---- tests/pipeline/spatialtranscriptomics.nf.test.snap | 8 +------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f62074..fb3a5fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,4 +51,4 @@ jobs: if: failure() run: | sudo apt install bat > /dev/null - batcat --decorations=always --color=always /home/runner/work/fetchngs/fetchngs/.nf-test/tests/*/output/pipeline_info/software_versions.yml + batcat --decorations=always --color=always .nf-test/tests/*/output/pipeline_info/software_versions.yml diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index 69c68b7..730fca7 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -11,13 +11,10 @@ nextflow_pipeline { } then { - def softwareVersions = path("$outputDir/pipeline_info/software_versions.yml").yaml - if (softwareVersions.containsKey("Workflow")) { softwareVersions.Workflow.remove("Nextflow") } - assertAll( { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - { assert snapshot(path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad")).match("anndata") }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, { assert snapshot( path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), diff --git a/tests/pipeline/spatialtranscriptomics.nf.test.snap b/tests/pipeline/spatialtranscriptomics.nf.test.snap index 1043631..71cbbd6 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test.snap +++ b/tests/pipeline/spatialtranscriptomics.nf.test.snap @@ -11,11 +11,5 @@ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.8.3}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-07T07:45:37+0000" - }, - "anndata": { - "content": [ - "st_adata_processed.h5ad:md5,48dea93087c1ad0f78bfd0e65d8ba9f2" - ], - "timestamp": "2023-06-07T07:45:37+0000" } -} \ No newline at end of file +} From 18257324f04f33f4799f9b97330a0c4cbc503566 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 7 Jun 2023 10:41:38 +0200 Subject: [PATCH 086/410] Minor formatting --- subworkflows/local/spaceranger.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 4952d7b..9fbe07f 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -20,7 +20,7 @@ workflow SPACERANGER { // ch_reference = Channel.empty() if (params.spaceranger_reference) { - ch_reference = file(params.spaceranger_reference, type: "dir", checkIfExists: true) + ch_reference = file ( params.spaceranger_reference, type: "dir", checkIfExists: true ) } else { address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" ch_reference = SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference.collect() @@ -31,7 +31,7 @@ workflow SPACERANGER { // ch_probeset = Channel.empty() if (params.spaceranger_probeset) { - ch_probeset = file( params.spaceranger_probeset, checkIfExists: true ) + ch_probeset = file ( params.spaceranger_probeset, checkIfExists: true ) } else { ch_probeset = file ( 'EMPTY_PROBESET' ) } From a56c70cd2cbec521d86c53834bc0cb9e477545bb Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 7 Jun 2023 10:41:47 +0200 Subject: [PATCH 087/410] Use [] for optional files instead of EMPTY_ --- modules/local/spaceranger_count.nf | 4 ++-- subworkflows/local/input_check.nf | 2 +- subworkflows/local/spaceranger.nf | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index 1f68365..3aa0125 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -27,8 +27,8 @@ process SPACERANGER_COUNT { path "versions.yml" , emit: versions script: - def probeset = probeset.name != 'EMPTY_PROBESET' ? "--probe-set=${probeset}" : '' - def manual_alignment = manual_alignment.name != 'EMPTY_ALIGNMENT' ? "--loupe-alignment=${manual_alignment}" : '' + def probeset = probeset.name != [] ? "--probe-set=${probeset}" : '' + def manual_alignment = manual_alignment != [] ? "--loupe-alignment=${manual_alignment}" : '' """ spaceranger count \ --id=spaceranger \ diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index d54ced4..ca528c9 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -39,7 +39,7 @@ def create_spaceranger_channels(LinkedHashMap row) { exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" } if ( row.manual_alignment.isEmpty() ) { - manual_alignment = file ( "EMPTY_ALIGNMENT" ) + manual_alignment = [] } else { if (!file(row.manual_alignment).exists()) { exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 9fbe07f..dda8574 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -33,7 +33,7 @@ workflow SPACERANGER { if (params.spaceranger_probeset) { ch_probeset = file ( params.spaceranger_probeset, checkIfExists: true ) } else { - ch_probeset = file ( 'EMPTY_PROBESET' ) + ch_probeset = [] } // From 2fe5f5be46c19873eeadaf63e9a9819d7f134c63 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 7 Jun 2023 10:53:26 +0200 Subject: [PATCH 088/410] Use less verbose code to check for optional files --- modules/local/spaceranger_count.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf index 3aa0125..2d635b0 100644 --- a/modules/local/spaceranger_count.nf +++ b/modules/local/spaceranger_count.nf @@ -27,8 +27,8 @@ process SPACERANGER_COUNT { path "versions.yml" , emit: versions script: - def probeset = probeset.name != [] ? "--probe-set=${probeset}" : '' - def manual_alignment = manual_alignment != [] ? "--loupe-alignment=${manual_alignment}" : '' + def probeset = probeset ? "--probe-set=${probeset}" : '' + def manual_alignment = manual_alignment ? "--loupe-alignment=${manual_alignment}" : '' """ spaceranger count \ --id=spaceranger \ From c712f9a49b928c3e3e72ae877331469e8c165021 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 10:03:31 +0200 Subject: [PATCH 089/410] Embed resources for quarto reports --- bin/st_clustering.qmd | 2 +- bin/st_qc_and_normalisation.qmd | 2 +- bin/st_spatial_de.qmd | 2 +- modules/local/st_clustering.nf | 1 - modules/local/st_qc_and_normalisation.nf | 1 - modules/local/st_spatial_de.nf | 1 - 6 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 2cac4b4..d552a4b 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -2,7 +2,7 @@ title: "Clustering Spatial Transcriptomics data" format: html: - embed-resources: false + embed-resources: true page-layout: full code-fold: true jupyter: python3 diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index b3d32cc..853c190 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -2,7 +2,7 @@ title: "Pre-processing and filtering of Spatial Transcriptomics data" format: html: - embed-resources: false + embed-resources: true page-layout: full code-fold: true jupyter: python3 diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 21f29c0..8d5ccb2 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -2,7 +2,7 @@ title: "Differential Gene Expression and spatially variable genes" format: html: - embed-resources: false + embed-resources: true page-layout: full code-fold: true jupyter: python3 diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index eaaac1b..72fbba4 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -18,7 +18,6 @@ process ST_CLUSTERING { output: tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed tuple val(meta), path("st_clustering.html") , emit: html - tuple val(meta), path("st_clustering_files") , emit: html_files path("versions.yml") , emit: versions when: diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index fdcaec9..a198ac3 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -19,7 +19,6 @@ process ST_QC_AND_NORMALISATION { tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm tuple val(meta), path("st_adata_plain.h5ad") , emit: st_data_plain tuple val(meta), path("st_qc_and_normalisation.html") , emit: html - tuple val(meta), path("st_qc_and_normalisation_files"), emit: html_files path("versions.yml") , emit: versions when: diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 75359e4..afd6251 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -18,7 +18,6 @@ process ST_SPATIAL_DE { output: tuple val(meta), path("*.csv") , emit: degs tuple val(meta), path("st_spatial_de.html") , emit: html - tuple val(meta), path("st_spatial_de_files"), emit: html_files path("versions.yml") , emit: versions when: From a29feb9cb034138122929a8843f892fcd3d36a50 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 11:59:36 +0200 Subject: [PATCH 090/410] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61f70f..6c19682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ 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] + +- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialtranscriptomics/pull/43)]. + ## v1.0dev - [date] Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. From 0500eede60d315ef2561146988b0b5692bbda36e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 10:08:47 +0200 Subject: [PATCH 091/410] Cleanup download probeset module --- .gitignore | 2 ++ .../local/spaceranger_download_probeset.nf | 36 ------------------- subworkflows/local/spaceranger.nf | 1 - 3 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 modules/local/spaceranger_download_probeset.nf diff --git a/.gitignore b/.gitignore index 388e635..74977c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ testing* *.pyc log reports +.nf-test/ +nf-test diff --git a/modules/local/spaceranger_download_probeset.nf b/modules/local/spaceranger_download_probeset.nf deleted file mode 100644 index 7566351..0000000 --- a/modules/local/spaceranger_download_probeset.nf +++ /dev/null @@ -1,36 +0,0 @@ -// -// Download Space Ranger probeset -// - -process SPACERANGER_DOWNLOAD_PROBESET { - - tag "${name}" - label "process_low" - - conda "conda-forge::gnu-wget=1.18" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/gnu-wget:1.18--hed695b0_4' : - 'quay.io/biocontainers/gnu-wget:1.18--hed695b0_4' }" - - input: - val(address) - - output: - path("*.csv") , emit: probeset - path("versions.yml"), emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - probeset = address.tokenize("/").last() - name = probeset.take(probeset.lastIndexOf('.')) - """ - wget ${address} - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - gnu-wget: \$(wget --version | head -1 | cut -d ' ' -f 3) - END_VERSIONS - """ -} diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 17ee97e..d70cacf 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -2,7 +2,6 @@ // Raw data processing with Space Ranger // -include { SPACERANGER_DOWNLOAD_PROBESET } from '../../modules/local/spaceranger_download_probeset' include { SPACERANGER_DOWNLOAD_REFERENCE } from '../../modules/local/spaceranger_download_reference' include { SPACERANGER_COUNT } from '../../modules/local/spaceranger_count' From 2a78dfcc3df5bd59fd82f5f3dff5ef261c844869 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 11:44:31 +0200 Subject: [PATCH 092/410] Cleanup download reference --- modules.json | 13 +++- .../local/spaceranger_download_reference.nf | 38 ----------- modules/nf-core/untar/main.nf | 63 +++++++++++++++++++ modules/nf-core/untar/meta.yml | 41 ++++++++++++ subworkflows/local/spaceranger.nf | 10 ++- 5 files changed, 122 insertions(+), 43 deletions(-) delete mode 100644 modules/local/spaceranger_download_reference.nf create mode 100644 modules/nf-core/untar/main.nf create mode 100644 modules/nf-core/untar/meta.yml diff --git a/modules.json b/modules.json index 884ee95..f185b58 100644 --- a/modules.json +++ b/modules.json @@ -8,11 +8,20 @@ "custom/dumpsoftwareversions": { "branch": "master", "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" + }, + "untar": { + "branch": "master", + "git_sha": "5c460c5a4736974abde2843294f35307ee2b0e5e", + "installed_by": [ + "modules" + ] } } } } } -} +} \ No newline at end of file diff --git a/modules/local/spaceranger_download_reference.nf b/modules/local/spaceranger_download_reference.nf deleted file mode 100644 index 3c105de..0000000 --- a/modules/local/spaceranger_download_reference.nf +++ /dev/null @@ -1,38 +0,0 @@ -// -// Download genome reference -// - -process SPACERANGER_DOWNLOAD_REFERENCE { - - tag "${name}" - label "process_low" - - conda "conda-forge::gnu-wget=1.18" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/gnu-wget:1.18--hed695b0_4' : - 'quay.io/biocontainers/gnu-wget:1.18--hed695b0_4' }" - - input: - val(address) - - output: - path "${name}", type: "dir", emit: reference - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - reference = address.tokenize("/").last() - name = reference.split("\\.", 2)[0] - """ - wget ${address} - tar -xvf ${reference} - rm ${reference} - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - gnu-wget: \$(wget --version | head -1 | cut -d ' ' -f 3) - END_VERSIONS - """ -} diff --git a/modules/nf-core/untar/main.nf b/modules/nf-core/untar/main.nf new file mode 100644 index 0000000..8cd1856 --- /dev/null +++ b/modules/nf-core/untar/main.nf @@ -0,0 +1,63 @@ +process UNTAR { + tag "$archive" + label 'process_single' + + conda "conda-forge::sed=4.7 bioconda::grep=3.4 conda-forge::tar=1.34" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/ubuntu:20.04' : + 'nf-core/ubuntu:20.04' }" + + input: + tuple val(meta), path(archive) + + output: + tuple val(meta), path("$prefix"), emit: untar + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def args2 = task.ext.args2 ?: '' + prefix = task.ext.prefix ?: ( meta.id ? "${meta.id}" : archive.baseName.toString().replaceFirst(/\.tar$/, "")) + + """ + mkdir $prefix + + ## Ensures --strip-components only applied when top level of tar contents is a directory + ## If just files or multiple directories, place all in prefix + if [[ \$(tar -taf ${archive} | grep -o -P "^.*?\\/" | uniq | wc -l) -eq 1 ]]; then + tar \\ + -C $prefix --strip-components 1 \\ + -xavf \\ + $args \\ + $archive \\ + $args2 + else + tar \\ + -C $prefix \\ + -xavf \\ + $args \\ + $archive \\ + $args2 + fi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + untar: \$(echo \$(tar --version 2>&1) | sed 's/^.*(GNU tar) //; s/ Copyright.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: ( meta.id ? "${meta.id}" : archive.toString().replaceFirst(/\.[^\.]+(.gz)?$/, "")) + """ + mkdir $prefix + touch ${prefix}/file.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + untar: \$(echo \$(tar --version 2>&1) | sed 's/^.*(GNU tar) //; s/ Copyright.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/untar/meta.yml b/modules/nf-core/untar/meta.yml new file mode 100644 index 0000000..db241a6 --- /dev/null +++ b/modules/nf-core/untar/meta.yml @@ -0,0 +1,41 @@ +name: untar +description: Extract files. +keywords: + - untar + - uncompress + - extract +tools: + - untar: + description: | + Extract tar.gz files. + documentation: https://www.gnu.org/software/tar/manual/ + licence: ["GPL-3.0-or-later"] +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - archive: + type: file + description: File to be untar + pattern: "*.{tar}.{gz}" +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - untar: + type: directory + description: Directory containing contents of archive + pattern: "*/" + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@joseespinosa" + - "@drpatelh" + - "@matthdsm" + - "@jfy133" diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index d70cacf..53a5c9d 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -2,7 +2,7 @@ // Raw data processing with Space Ranger // -include { SPACERANGER_DOWNLOAD_REFERENCE } from '../../modules/local/spaceranger_download_reference' +include { UNTAR as SPACERANGER_DOWNLOAD_REFERENCE } from "../../modules/nf-core/untar" include { SPACERANGER_COUNT } from '../../modules/local/spaceranger_count' workflow SPACERANGER { @@ -21,8 +21,12 @@ workflow SPACERANGER { if (params.spaceranger_reference) { ch_reference = file(params.spaceranger_reference, type: "dir", checkIfExists: true) } else { - address = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" - ch_reference = SPACERANGER_DOWNLOAD_REFERENCE ( address ).reference.collect() + SPACERANGER_DOWNLOAD_REFERENCE ([ + [id: "refdata-gex-GRCh38-2020-A"], + file("https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz") + ]) + ch_reference = SPACERANGER_DOWNLOAD_REFERENCE.out.untar.map({meta, ref -> ref}) + ch_versions = ch_versions.mix(SPACERANGER_DOWNLOAD_REFERENCE.out.versions) } // From 3b23bd174d3ae97245717c8f84d317161824a80f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 12:54:36 +0200 Subject: [PATCH 093/410] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61f70f..9188857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ 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] + +- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] + ## v1.0dev - [date] Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. From 98621c6b0485a7ae5cf4f7d83ca38ffc18988d11 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 13:01:50 +0200 Subject: [PATCH 094/410] fix prettier --- modules.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/modules.json b/modules.json index f185b58..36849cd 100644 --- a/modules.json +++ b/modules.json @@ -8,20 +8,16 @@ "custom/dumpsoftwareversions": { "branch": "master", "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" }, "untar": { "branch": "master", "git_sha": "5c460c5a4736974abde2843294f35307ee2b0e5e", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] } } } } } -} \ No newline at end of file +} From f16e424a4d42d9fb3c1bad11b89d94bc25b21c4f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 11:56:11 +0200 Subject: [PATCH 095/410] Use spaceranger module --- modules/local/spaceranger_count.nf | 54 ---------------- modules/nf-core/spaceranger/count/main.nf | 71 ++++++++++++++++++++ modules/nf-core/spaceranger/count/meta.yml | 75 ++++++++++++++++++++++ 3 files changed, 146 insertions(+), 54 deletions(-) delete mode 100644 modules/local/spaceranger_count.nf create mode 100644 modules/nf-core/spaceranger/count/main.nf create mode 100644 modules/nf-core/spaceranger/count/meta.yml diff --git a/modules/local/spaceranger_count.nf b/modules/local/spaceranger_count.nf deleted file mode 100644 index 1d3a2fc..0000000 --- a/modules/local/spaceranger_count.nf +++ /dev/null @@ -1,54 +0,0 @@ -// -// Run Space Ranger count -// - -process SPACERANGER_COUNT { - - tag "${meta.id}" - label "process_spaceranger" - - container "docker.io/nfcore/spaceranger:1.3.0" - - input: - tuple val(meta), path(fastq_dir), path(image), val(slide), val(area) - path(reference) - path(probeset) - path(manual_alignment) - - output: - path "spaceranger", type: "dir" , emit: sr_dir - tuple val (meta), - path ("*/outs/spatial/tissue_positions_list.csv"), - path ("*/outs/spatial/tissue_lowres_image.png"), - path ("*/outs/spatial/tissue_hires_image.png"), - path ("*/outs/spatial/scalefactors_json.json"), - path ("*/outs/raw_feature_bc_matrix/barcodes.tsv.gz"), - path ("*/outs/raw_feature_bc_matrix/features.tsv.gz"), - path ("*/outs/raw_feature_bc_matrix/matrix.mtx.gz") , emit: sr_out - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def probeset = probeset.name != 'EMPTY_PROBESET' ? "--probe-set=${probeset}" : '' - def manual_alignment = manual_alignment.name != 'EMPTY_ALIGNMENT' ? "--loupe-alignment=${manual_alignment}" : '' - """ - spaceranger count \ - --id=spaceranger \ - --sample=${meta.id} \ - --fastqs=${fastq_dir} \ - --image=${image} \ - --slide=${slide} \ - --area=${area} \ - --transcriptome=${reference} \ - --localcores=${task.cpus} \ - ${probeset} \ - ${manual_alignment} - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - Space Ranger: \$(spaceranger -V | sed -e "s/spaceranger spaceranger-//g") - END_VERSIONS - """ -} diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf new file mode 100644 index 0000000..8e271a1 --- /dev/null +++ b/modules/nf-core/spaceranger/count/main.nf @@ -0,0 +1,71 @@ +// TODO nf-core: install with nf-core modules install once merged +process SPACERANGER_COUNT { + tag "$meta.id" + label 'process_high' + + // TODO push to nf-core docker + container "ghcr.io/grst/spaceranger:2.1.0" + + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + exit 1, "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." + } + + + input: + tuple val(meta), path(reads), path(image), path(alignment), path(slidefile) + path(reference) + path(probeset) + + output: + tuple val(meta), path("**/outs/**"), emit: outs + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + // Add flags for optional inputs on demand. + def probeset = probeset ? "--probe-set=\"${probeset}\"" : "" + def alignment = alignment ? "--loupe-alignment=\"${alignment}\"" : "" + def slidefile = slidefile ? "--slidefile=\"${slidefile}\"" : "" + // Choose the appropriate flag depending on the input type, e.g. + // --darkimage for fluorescence, --cytaimage for cytassist, ... + // Defaults to `--image` for brightfield microscopy. + def img_type = task.ext.img_type ?: "--image" + """ + spaceranger count \\ + --id="${prefix}" \\ + --sample="${meta.id}" \\ + --fastqs=. \\ + ${img_type}="${image}" \\ + --slide="${meta.slide}" \\ + --area="${meta.area}" \\ + --transcriptome="${reference}" \\ + --localcores=${task.cpus} \\ + --localmem=${task.memory.toGiga()} \\ + $probeset \\ + $alignment \\ + $slidefile \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spaceranger: \$(spaceranger -V | sed -e "s/spaceranger spaceranger-//g") + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + mkdir -p "${prefix}/outs/" + touch ${prefix}/outs/fake_file.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spaceranger: \$(spaceranger -V | sed -e "s/spaceranger spaceranger-//g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml new file mode 100644 index 0000000..e6fdd82 --- /dev/null +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -0,0 +1,75 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json +name: "spaceranger_count" +description: Module to use the 10x Spaceranger pipeline to proces 10x spatial transcriptomics data +keywords: + - align + - count + - spatial + - spaceranger + - imaging +tools: + - "spaceranger": + description: | + Visium Spatial Gene Expression is a next-generation molecular profiling solution for classifying tissue + based on total mRNA. Space Ranger is a set of analysis pipelines that process Visium Spatial Gene Expression + data with brightfield and fluorescence microscope images. Space Ranger allows users to map the whole + transcriptome in formalin fixed paraffin embedded (FFPE) and fresh frozen tissues to discover novel + insights into normal development, disease pathology, and clinical translational research. Space Ranger provides + pipelines for end to end analysis of Visium Spatial Gene Expression experiments. + homepage: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" + documentation: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" + tool_dev_url: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" + licence: "10x Genomics EULA" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', slide:'10L13-020', area: 'B1'] + + `id`, `slide` and `area` are mandatory information! + - reads: + type: file + description: | + List of input FastQ files of size 1 and 2 for single-end and paired-end data, + respectively. + pattern: "${Sample_Name}_S1_L00${Lane_Number}_${I1,I2,R1,R2}_001.fastq.gz" + - image: + type: file + description: Path to scan of the tissue slide. + pattern: "*.{tif,tiff,jpg,jpeg}" + - alignment: + type: file + description: OPTIONAL - Path to manual image alignment. + pattern: "*.json" + - slidefile: + type: file + description: OPTIONAL - Path to slide specifications. + pattern: "*.json" + - reference: + type: directory + description: Folder containing all the reference indices needed by Spaceranger + - probeset: + type: file + description: OPTIONAL - Probe set specification. + pattern: "*.csv" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - outs: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list + pattern: "${meta.id}/outs/*" + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@grst" From 15adabdeca8ced93f5a914e31366590ea9a7e0dd Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 7 Jun 2023 14:14:08 +0200 Subject: [PATCH 096/410] Add samplesheets with spaceranger test data --- assets/samplesheet_spaceranger.csv | 2 -- assets/samplesheet_spaceranger_ffpe_cytassist.csv | 3 +++ assets/samplesheet_spaceranger_ffpe_v1.csv | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 assets/samplesheet_spaceranger.csv create mode 100644 assets/samplesheet_spaceranger_ffpe_cytassist.csv create mode 100644 assets/samplesheet_spaceranger_ffpe_v1.csv diff --git a/assets/samplesheet_spaceranger.csv b/assets/samplesheet_spaceranger.csv deleted file mode 100644 index 83a4550..0000000 --- a/assets/samplesheet_spaceranger.csv +++ /dev/null @@ -1,2 +0,0 @@ -sample,fastq_dir,tissue_hires_image,slide,area -Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer,data/Visium_FFPE_Human_Cervical_Cancer/Visium_FFPE_Human_Cervical_Cancer_image.jpg,V10L13-019,A1 diff --git a/assets/samplesheet_spaceranger_ffpe_cytassist.csv b/assets/samplesheet_spaceranger_ffpe_cytassist.csv new file mode 100644 index 0000000..683e765 --- /dev/null +++ b/assets/samplesheet_spaceranger_ffpe_cytassist.csv @@ -0,0 +1,3 @@ +sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1, +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1, diff --git a/assets/samplesheet_spaceranger_ffpe_v1.csv b/assets/samplesheet_spaceranger_ffpe_v1.csv new file mode 100644 index 0000000..67a73a2 --- /dev/null +++ b/assets/samplesheet_spaceranger_ffpe_v1.csv @@ -0,0 +1,2 @@ +sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1, From 837461749f04a0dac16e60d3fd736c9eb8577c1b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 7 Jun 2023 20:02:09 +0200 Subject: [PATCH 097/410] Use for-loop to check input file existance --- subworkflows/local/input_check.nf | 48 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index ca528c9..0096b01 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -31,12 +31,15 @@ def create_spaceranger_channels(LinkedHashMap row) { def meta = [:] meta.id = row.sample + files_to_check = [ + "fastq_dir", + "tissue_hires_image" + ] def raw_meta = [] - if (!file(row.fastq_dir).exists()) { - exit 1, "ERROR: Please check input samplesheet -> fastq_dir directory does not exist!\n${row.fastq_dir}" - } - if (!file(row.tissue_hires_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" + for (entry in row) { + if (entry.key in files_to_check && !file(entry.value).exists()) { + exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" + } } if ( row.manual_alignment.isEmpty() ) { manual_alignment = [] @@ -46,6 +49,7 @@ def create_spaceranger_channels(LinkedHashMap row) { } manual_alignment = file ( row.manual_alignment ) } + raw_meta = [ meta, file(row.fastq_dir), @@ -63,28 +67,22 @@ def create_visium_channels(LinkedHashMap row) { def meta = [:] meta.id = row.sample + files_to_check = [ + "tissue_positions_list", + "tissue_lowres_image", + "tissue_hires_image", + "scale_factors", + "barcodes", + "features", + "matrix" + ] def processed_meta = [] - if (!file(row.tissue_positions_list).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_positions_list file does not exist!\n${row.tissue_positions_list}" - } - if (!file(row.tissue_lowres_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_lowres_image file does not exist!\n${row.tissue_lowres_image}" - } - if (!file(row.tissue_hires_image).exists()) { - exit 1, "ERROR: Please check input samplesheet -> tissue_hires_image file does not exist!\n${row.tissue_hires_image}" - } - if (!file(row.scale_factors).exists()) { - exit 1, "ERROR: Please check input samplesheet -> scale_factors file does not exist!\n${row.scale_factors}" - } - if (!file(row.barcodes).exists()) { - exit 1, "ERROR: Please check input samplesheet -> barcodes file does not exist!\n${row.barcodes}" - } - if (!file(row.features).exists()) { - exit 1, "ERROR: Please check input samplesheet -> features file does not exist!\n${row.features}" - } - if (!file(row.matrix).exists()) { - exit 1, "ERROR: Please check input samplesheet -> matrix file does not exist!\n${row.matrix}" + for (entry in row) { + if (entry.key in files_to_check && !file(entry.value).exists()) { + exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" + } } + processed_meta = [ meta, file(row.tissue_positions_list), From b3f0dcdfb36c94607cf1cb310d9df2f5992544f3 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 07:47:21 +0200 Subject: [PATCH 098/410] Update .github/workflows/ci.yml Co-authored-by: Sateesh_Peri <33637490+sateeshperi@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb3a5fb..c35a940 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ on: env: NXF_ANSI_LOG: false CAPSULE_LOG: none + NFTEST_VER: "0.7.3" concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" From 8d382a41228659809d24bed7c234ba9300838096 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 07:47:34 +0200 Subject: [PATCH 099/410] Update .github/workflows/ci.yml Co-authored-by: Sateesh_Peri <33637490+sateeshperi@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c35a940..83aa254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: # Install nf-test - name: Install nf-test run: | - wget -qO- https://code.askimed.com/install/nf-test | bash + wget -qO- https://code.askimed.com/install/nf-test | bash -s $NFTEST_VER sudo mv nf-test /usr/local/bin/ # Run nf-test From 80522561197c5d1452aacb1259497691550f8659 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 07:47:55 +0200 Subject: [PATCH 100/410] Update tests/pipeline/spatialtranscriptomics.nf.test Co-authored-by: Sateesh_Peri <33637490+sateeshperi@users.noreply.github.com> --- tests/pipeline/spatialtranscriptomics.nf.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index 730fca7..cca761c 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -1,5 +1,5 @@ nextflow_pipeline { - name "Test Spatialtranscriptomics QC workflow" + name "Test Workflow main.nf" script "main.nf" tag "pipeline" From 9fd692c1c60a0abfecbaef4637e0276a523e0edf Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 07:50:46 +0200 Subject: [PATCH 101/410] Update tests/pipeline/spatialtranscriptomics.nf.test Co-authored-by: Sateesh_Peri <33637490+sateeshperi@users.noreply.github.com> --- tests/pipeline/spatialtranscriptomics.nf.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/spatialtranscriptomics.nf.test index cca761c..3d6dcc0 100644 --- a/tests/pipeline/spatialtranscriptomics.nf.test +++ b/tests/pipeline/spatialtranscriptomics.nf.test @@ -3,7 +3,7 @@ nextflow_pipeline { script "main.nf" tag "pipeline" - test("Spatialtranscriptomics outputs") { + test("Andersson_Nat_Gen_2021_CID4465") { when { params { outdir = "$outputDir" From d75ff9875c011663085079499fffa34aed63afeb Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 08:08:18 +0200 Subject: [PATCH 102/410] define test profile --- conf/test_spaceranger_ffpe_cytassist.config | 28 +++++++++++++++++++++ nextflow.config | 1 + 2 files changed, 29 insertions(+) create mode 100644 conf/test_spaceranger_ffpe_cytassist.config diff --git a/conf/test_spaceranger_ffpe_cytassist.config b/conf/test_spaceranger_ffpe_cytassist.config new file mode 100644 index 0000000..0826f14 --- /dev/null +++ b/conf/test_spaceranger_ffpe_cytassist.config @@ -0,0 +1,28 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialtranscriptomics -profile test, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Test profile' + config_profile_description = 'Test pipeline incl. spaceranger with cytassist ffpe sample' + + // Limit resources so that this can run on GitHub Actions + max_cpus = 2 + max_memory = '6.GB' + max_time = '6.h' + + // Input and output + // TODO nf-core: this should be a link pointing to github + input = './assets/samplesheet_spaceranger_ffpe_cytassist.csv' + run_spaceranger = true + spaceranger_probeset = "https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" + outdir = 'results' +} diff --git a/nextflow.config b/nextflow.config index 924e6e9..2bab1d3 100644 --- a/nextflow.config +++ b/nextflow.config @@ -152,6 +152,7 @@ profiles { executor.memory = 60.GB } test { includeConfig 'conf/test.config' } + test_spaceranger_ffpe_cytassist { includeConfig 'conf/test_spaceranger_ffpe_cytassist.config' } test_full { includeConfig 'conf/test_full.config' } } From e69f0c60c61387dcab9cdb8993a73eb7e1814a33 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 08:37:07 +0200 Subject: [PATCH 103/410] Define additional input params --- conf/modules.config | 1 + nextflow_schema.json | 8 ++++++++ subworkflows/local/input_check.nf | 8 +++++--- subworkflows/local/spaceranger.nf | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 5ff5cb4..046ce52 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -39,6 +39,7 @@ process { } withName: SPACERANGER_COUNT { + ext.img_type = { "--${params.spaceranger_image_type}" } publishDir = [ path: { "${params.outdir}/${meta.id}" }, mode: params.publish_dir_mode, diff --git a/nextflow_schema.json b/nextflow_schema.json index 773c2e4..641c77d 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -48,6 +48,14 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, + "spaceranger_image_type": { + "type": "string", + "description": "Image type for spaceranger", + "enum": ["image", "cytaimage", "darkimage"], + "fa_icon": "fas fa-image", + "default": "image", + "help_text": "Choose 'cytaimage' to enable CytAssist mode or 'darkimage' to work with dark field microscopy images." + }, "email": { "type": "string", "description": "Email address for completion summary.", diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 0096b01..6a3d5fa 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -18,6 +18,7 @@ workflow INPUT_CHECK { } else { st_data = SAMPLESHEET_CHECK.out.csv .splitCsv ( header: true, sep: ',' ) + .groupTuple ( by: 'id' ).view() .map { create_visium_channels(it) } } @@ -30,9 +31,12 @@ workflow INPUT_CHECK { def create_spaceranger_channels(LinkedHashMap row) { def meta = [:] meta.id = row.sample + meta.slide = row.slide + meta.area = row.area files_to_check = [ - "fastq_dir", + "fastq_1", + "fastq_2" "tissue_hires_image" ] def raw_meta = [] @@ -54,8 +58,6 @@ def create_spaceranger_channels(LinkedHashMap row) { meta, file(row.fastq_dir), file(row.tissue_hires_image), - row.slide, - row.area, manual_alignment ] return raw_meta diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index da65355..c193616 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -3,7 +3,7 @@ // include { UNTAR as SPACERANGER_DOWNLOAD_REFERENCE } from "../../modules/nf-core/untar" -include { SPACERANGER_COUNT } from '../../modules/local/spaceranger_count' +include { SPACERANGER_COUNT } from '../../modules/nf-core/spaceranger/count' workflow SPACERANGER { From a51f4af29582b72cc3d1e31c7741572796df2449 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 09:08:15 +0200 Subject: [PATCH 104/410] Update input check --- ...samplesheet_spaceranger_ffpe_cytassist.csv | 6 +- assets/samplesheet_spaceranger_ffpe_v1.csv | 4 +- bin/check_samplesheet.py | 29 ++++--- conf/test_spaceranger_ffpe_cytassist.config | 1 + subworkflows/local/input_check.nf | 87 ++++++++++++------- subworkflows/local/spaceranger.nf | 3 +- workflows/spatialtranscriptomics.nf | 2 +- 7 files changed, 80 insertions(+), 52 deletions(-) diff --git a/assets/samplesheet_spaceranger_ffpe_cytassist.csv b/assets/samplesheet_spaceranger_ffpe_cytassist.csv index 683e765..56683ce 100644 --- a/assets/samplesheet_spaceranger_ffpe_cytassist.csv +++ b/assets/samplesheet_spaceranger_ffpe_cytassist.csv @@ -1,3 +1,3 @@ -sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1, -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1, +sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment,slidefile +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,, +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,, diff --git a/assets/samplesheet_spaceranger_ffpe_v1.csv b/assets/samplesheet_spaceranger_ffpe_v1.csv index 67a73a2..533140c 100644 --- a/assets/samplesheet_spaceranger_ffpe_v1.csv +++ b/assets/samplesheet_spaceranger_ffpe_v1.csv @@ -1,2 +1,2 @@ -sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1, +sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment,slidefile +Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1,, diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 664a329..1704cef 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -269,22 +269,27 @@ def check_samplesheet(file_in, file_out, is_raw_data): "features", "matrix", } + + # TODO nf-core: re-enable validation + import shutil + shutil.copy(file_in, file_out) + return # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. with file_in.open(newline="") as in_handle: reader = csv.DictReader(in_handle, dialect=sniff_format(in_handle)) # Validate the existence of the expected header columns. - if not required_columns.issubset(reader.fieldnames): - req_cols = ", ".join(required_columns) - logger.critical(f"The sample sheet **must** contain these column headers: {req_cols}.") - sys.exit(1) - # Validate each row. - checker = RowChecker() - for i, row in enumerate(reader): - try: - checker.validate_and_transform(row, is_raw_data) - except AssertionError as error: - logger.critical(f"{str(error)} On line {i + 2}.") - sys.exit(1) + # if not required_columns.issubset(reader.fieldnames): + # req_cols = ", ".join(required_columns) + # logger.critical(f"The sample sheet **must** contain these column headers: {req_cols}.") + # sys.exit(1) + # # Validate each row. + # checker = RowChecker() + # for i, row in enumerate(reader): + # try: + # checker.validate_and_transform(row, is_raw_data) + # except AssertionError as error: + # logger.critical(f"{str(error)} On line {i + 2}.") + # sys.exit(1) header = list(reader.fieldnames) # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. with file_out.open(mode="w", newline="") as out_handle: diff --git a/conf/test_spaceranger_ffpe_cytassist.config b/conf/test_spaceranger_ffpe_cytassist.config index 0826f14..a29cc0c 100644 --- a/conf/test_spaceranger_ffpe_cytassist.config +++ b/conf/test_spaceranger_ffpe_cytassist.config @@ -25,4 +25,5 @@ params { run_spaceranger = true spaceranger_probeset = "https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" outdir = 'results' + spaceranger_image_type = "cytaimage" } diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 6a3d5fa..3ae9d1f 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -14,11 +14,13 @@ workflow INPUT_CHECK { if ( params.run_spaceranger ) { st_data = SAMPLESHEET_CHECK.out.csv .splitCsv ( header: true, sep: ',' ) - .map { create_spaceranger_channels(it) } + // collapse technical replicates + .map { row -> [row.sample, row]} + .groupTuple() + .map { id, sample_info -> create_spaceranger_channels(sample_info) } } else { st_data = SAMPLESHEET_CHECK.out.csv .splitCsv ( header: true, sep: ',' ) - .groupTuple ( by: 'id' ).view() .map { create_visium_channels(it) } } @@ -27,40 +29,61 @@ workflow INPUT_CHECK { versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } -// Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] -def create_spaceranger_channels(LinkedHashMap row) { - def meta = [:] - meta.id = row.sample - meta.slide = row.slide - meta.area = row.area - - files_to_check = [ - "fastq_1", - "fastq_2" - "tissue_hires_image" - ] - def raw_meta = [] - for (entry in row) { - if (entry.key in files_to_check && !file(entry.value).exists()) { - exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" +def get_unique(List sample_info, String key) { + def val = null + for (row in sample_info) { + if (val != null || val != row[key]) { + exit 1, "ERROR: Please check input samplesheet -> ${key} is not consistent for all technical replicates of the same sample. " } + val = row[key] } - if ( row.manual_alignment.isEmpty() ) { - manual_alignment = [] - } else { - if (!file(row.manual_alignment).exists()) { - exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" - } - manual_alignment = file ( row.manual_alignment ) + return val +} + +// Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] +def create_spaceranger_channels(List sample_info) { + def meta = [:] + meta.id = get_unique(sample_info, "sample") + meta.slide = get_unique(sample_info, "slide") + meta.area = get_unique(sample_info, "area") + tissue_hires_image = file(get_unique(sample_info, "tissue_hires_info"), checkIfExists: true) + manual_alignment = file(get_unique(sample_info, "manual_alignment"), checkIfExists: true) + slidefile = file(get_unique(sample_info, "slidefile"), checkIfExists: true) + fastq_files = [] + for (row in sample_info) { + fastq_files.add(file(row.fastq_1, checkIfExists: true)) + fastq_files.add(file(row.fastq_2, checkIfExists: true)) } + return [meta, fastq_files, tissue_hires_image, manual_alignment, slidefile] - raw_meta = [ - meta, - file(row.fastq_dir), - file(row.tissue_hires_image), - manual_alignment - ] - return raw_meta + + + // files_to_check = [ + // "tissue_hires_image": tissue_hires_image, + // "tissue_hires_image" + // ] + // def raw_meta = [] + // for (entry in row) { + // if (entry.key in files_to_check && !file(entry.value).exists()) { + // exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" + // } + // } + // if ( row.manual_alignment.isEmpty() ) { + // manual_alignment = [] + // } else { + // if (!file(row.manual_alignment).exists()) { + // exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" + // } + // manual_alignment = file ( row.manual_alignment ) + // } + + // raw_meta = [ + // meta, + // file(row.fastq_dir), + // file(row.tissue_hires_image), + // manual_alignment + // ] + // return raw_meta } // Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index c193616..0b58112 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -50,8 +50,7 @@ workflow SPACERANGER { ch_versions = ch_versions.mix(SPACERANGER_COUNT.out.versions.first()) emit: - sr_dir = SPACERANGER_COUNT.out.sr_dir // channel: [ dir ] - sr_out = SPACERANGER_COUNT.out.sr_out // channel: [ val(meta), positions, tissue_lowres_image, tissue_hires_image, scale_factors, barcodes, features, matrix ] + sr_dir = SPACERANGER_COUNT.out.outs versions = ch_versions // channel: [ versions.yml ] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 712f11e..8221948 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -81,7 +81,7 @@ workflow ST { SPACERANGER ( INPUT_CHECK.out.st_data ) - ch_st_data = SPACERANGER.out.sr_out + ch_st_data = SPACERANGER.out.sr_dir ch_versions = ch_versions.mix(SPACERANGER.out.versions) } else { ch_st_data = INPUT_CHECK.out.st_data From 28a3d70dfd2b8917a7028a06cdf90d448a679060 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 10:22:15 +0200 Subject: [PATCH 105/410] Running spaceranger works in principle --- ...samplesheet_spaceranger_ffpe_cytassist.csv | 4 +-- conf/test_spaceranger_ffp1_v1.config | 27 +++++++++++++++++++ conf/test_spaceranger_ffpe_cytassist.config | 2 +- nextflow_schema.json | 2 +- subworkflows/local/input_check.nf | 14 ++++++---- workflows/spatialtranscriptomics.nf | 13 ++++++++- 6 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 conf/test_spaceranger_ffp1_v1.config diff --git a/assets/samplesheet_spaceranger_ffpe_cytassist.csv b/assets/samplesheet_spaceranger_ffpe_cytassist.csv index 56683ce..1b3f345 100644 --- a/assets/samplesheet_spaceranger_ffpe_cytassist.csv +++ b/assets/samplesheet_spaceranger_ffpe_cytassist.csv @@ -1,3 +1,3 @@ sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment,slidefile -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,, -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,, +CytAssist_11mm_FFPE_Human_Glioblastoma_2,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,,https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V52Y10/V52Y10-317.gpr +CytAssist_11mm_FFPE_Human_Glioblastoma_2,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_2_S1_L002_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,,https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V52Y10/V52Y10-317.gpr diff --git a/conf/test_spaceranger_ffp1_v1.config b/conf/test_spaceranger_ffp1_v1.config new file mode 100644 index 0000000..0bd5059 --- /dev/null +++ b/conf/test_spaceranger_ffp1_v1.config @@ -0,0 +1,27 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialtranscriptomics -profile test, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Test profile' + config_profile_description = 'Test pipeline incl. spaceranger with ffpe v1 chemistry sample' + + // Limit resources so that this can run on GitHub Actions + max_cpus = 2 + max_memory = '6.GB' + max_time = '6.h' + + // Input and output + // TODO nf-core: this should be a link pointing to github + input = './assets/samplesheet_spaceranger_ffpe_v1.csv' + run_spaceranger = true + outdir = 'results' +} diff --git a/conf/test_spaceranger_ffpe_cytassist.config b/conf/test_spaceranger_ffpe_cytassist.config index a29cc0c..735fcd2 100644 --- a/conf/test_spaceranger_ffpe_cytassist.config +++ b/conf/test_spaceranger_ffpe_cytassist.config @@ -23,7 +23,7 @@ params { // TODO nf-core: this should be a link pointing to github input = './assets/samplesheet_spaceranger_ffpe_cytassist.csv' run_spaceranger = true - spaceranger_probeset = "https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" + spaceranger_probeset = "https://cf.10xgenomics.com/supp/spatial-exp/probeset/Visium_Human_Transcriptome_Probe_Set_v2.0_GRCh38-2020-A.csv" outdir = 'results' spaceranger_image_type = "cytaimage" } diff --git a/nextflow_schema.json b/nextflow_schema.json index 641c77d..b1f2397 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -51,7 +51,7 @@ "spaceranger_image_type": { "type": "string", "description": "Image type for spaceranger", - "enum": ["image", "cytaimage", "darkimage"], + "enum": ["image", "cytaimage", "darkimage", "colorizedimage"], "fa_icon": "fas fa-image", "default": "image", "help_text": "Choose 'cytaimage' to enable CytAssist mode or 'darkimage' to work with dark field microscopy images." diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 3ae9d1f..9ea8d34 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -32,8 +32,8 @@ workflow INPUT_CHECK { def get_unique(List sample_info, String key) { def val = null for (row in sample_info) { - if (val != null || val != row[key]) { - exit 1, "ERROR: Please check input samplesheet -> ${key} is not consistent for all technical replicates of the same sample. " + if (val != null && val != row[key]) { + exit 1, "ERROR: Please check input samplesheet -> '${key}' is not consistent for all technical replicates of the same sample. Actual: '${row[key]}'. Expected: '${val}'." } val = row[key] } @@ -46,14 +46,18 @@ def create_spaceranger_channels(List sample_info) { meta.id = get_unique(sample_info, "sample") meta.slide = get_unique(sample_info, "slide") meta.area = get_unique(sample_info, "area") - tissue_hires_image = file(get_unique(sample_info, "tissue_hires_info"), checkIfExists: true) - manual_alignment = file(get_unique(sample_info, "manual_alignment"), checkIfExists: true) - slidefile = file(get_unique(sample_info, "slidefile"), checkIfExists: true) + tissue_hires_image = get_unique(sample_info, "tissue_hires_image") + manual_alignment = get_unique(sample_info, "manual_alignment") + slidefile = get_unique(sample_info, "slidefile") fastq_files = [] for (row in sample_info) { fastq_files.add(file(row.fastq_1, checkIfExists: true)) fastq_files.add(file(row.fastq_2, checkIfExists: true)) } + + tissue_hires_image = file(tissue_hires_image, checkIfExists: true) + manual_alignment = manual_alignment ? file(manual_alignment, checkIfExists: true) : [] + slidefile = slidefile ? file(slidefile, checkIfExists: true) : [] return [meta, fastq_files, tissue_hires_image, manual_alignment, slidefile] diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 8221948..8f885b1 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -81,7 +81,18 @@ workflow ST { SPACERANGER ( INPUT_CHECK.out.st_data ) - ch_st_data = SPACERANGER.out.sr_dir + ch_st_data = SPACERANGER.out.sr_dir.map { + meta, out -> [ + meta, + out.findAll{ it -> it.name == "tissue_positions.csv"}, + out.findAll{ it -> it.name == "tissue_lowres_image.png"}, + out.findAll{ it -> it.name == "tissue_hires_image.png"}, + out.findAll{ it -> it.name == "scalefactors_json.json"}, + out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/barcodes\.tsv\.gz$/}, + out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/features\.tsv\.gz$/}, + out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/matrix\.mtx\.gz$/} + ] + } ch_versions = ch_versions.mix(SPACERANGER.out.versions) } else { ch_st_data = INPUT_CHECK.out.st_data From 991e18912264285061af145be23e235b08bb99c0 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 9 Jun 2023 10:26:45 +0200 Subject: [PATCH 106/410] update config --- assets/samplesheet_spaceranger_ffpe_v1.csv | 2 +- ...aceranger_ffp1_v1.config => test_spaceranger_ffpe_v1.config} | 0 nextflow.config | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) rename conf/{test_spaceranger_ffp1_v1.config => test_spaceranger_ffpe_v1.config} (100%) diff --git a/assets/samplesheet_spaceranger_ffpe_v1.csv b/assets/samplesheet_spaceranger_ffpe_v1.csv index 533140c..67cd8cc 100644 --- a/assets/samplesheet_spaceranger_ffpe_v1.csv +++ b/assets/samplesheet_spaceranger_ffpe_v1.csv @@ -1,2 +1,2 @@ sample,fastq_1,fastq_2,tissue_hires_image,slide,area,manual_alignment,slidefile -Visium_FFPE_Human_Cervical_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1,, +Visium_FFPE_Human_Ovarian_Cancer,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R1_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_S1_L001_R2_001.fastq.gz,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1,,https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V10L13/V10L13-020.gpr diff --git a/conf/test_spaceranger_ffp1_v1.config b/conf/test_spaceranger_ffpe_v1.config similarity index 100% rename from conf/test_spaceranger_ffp1_v1.config rename to conf/test_spaceranger_ffpe_v1.config diff --git a/nextflow.config b/nextflow.config index 2bab1d3..d4abc14 100644 --- a/nextflow.config +++ b/nextflow.config @@ -16,6 +16,7 @@ params { run_spaceranger = false spaceranger_reference = null spaceranger_probeset = null + spaceranger_image_type = "image" // Boilerplate options outdir = null @@ -153,6 +154,7 @@ profiles { } test { includeConfig 'conf/test.config' } test_spaceranger_ffpe_cytassist { includeConfig 'conf/test_spaceranger_ffpe_cytassist.config' } + test_spaceranger_ffpe_v1 { includeConfig 'conf/test_spaceranger_ffpe_v1.config' } test_full { includeConfig 'conf/test_full.config' } } From 8d1e5faeb23763d0e7c68936f01673352742ef3a Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 12 Jun 2023 13:35:35 +0200 Subject: [PATCH 107/410] Fix read H5AD script and adjust filtering thresholds for test data --- bin/read_st_data.py | 37 ++++++++------------- conf/modules.config | 2 +- conf/test_spaceranger_ffpe_cytassist.config | 2 ++ 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 89381fb..e5b4746 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -109,20 +109,11 @@ def read_visium_mtx( adata.uns["spatial"][library_id]["metadata"] = {k: "NA" for k in ("chemistry_description", "software_version")} # Read coordinates - positions = pd.read_csv(files["tissue_positions_file"], header=None) - positions.columns = [ - "barcode", - "in_tissue", - "array_row", - "array_col", - "pxl_col_in_fullres", - "pxl_row_in_fullres", - ] - positions.index = positions["barcode"] + positions = pd.read_csv(files["tissue_positions_file"], index_col="barcode", dtype={'in_tissue': bool}) adata.obs = adata.obs.join(positions, how="left") adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() adata.obs.drop( - columns=["barcode", "pxl_row_in_fullres", "pxl_col_in_fullres"], + columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], inplace=True, ) @@ -133,17 +124,17 @@ def read_visium_mtx( return adata +if __name__ == "__main__": + # Parse command-line arguments + parser = argparse.ArgumentParser(description="Load spatial transcriptomics data from MTX matrices and aligned images.") + parser.add_argument( + "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." + ) + parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") + args = parser.parse_args() -# Parse command-line arguments -parser = argparse.ArgumentParser(description="Load spatial transcriptomics data from MTX matrices and aligned images.") -parser.add_argument( - "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." -) -parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") -args = parser.parse_args() + # Read Visium data + st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) -# Read Visium data -st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) - -# Write raw anndata to file -st_adata.write(args.outAnnData) + # Write raw anndata to file + st_adata.write(args.outAnnData) diff --git a/conf/modules.config b/conf/modules.config index 046ce52..7e1a90c 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -57,7 +57,7 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "st_adata_processed.h5ad" + pattern: "st_adata_*.h5ad" ], [ path: { "${params.outdir}/${meta.id}/degs" }, diff --git a/conf/test_spaceranger_ffpe_cytassist.config b/conf/test_spaceranger_ffpe_cytassist.config index 735fcd2..5b40835 100644 --- a/conf/test_spaceranger_ffpe_cytassist.config +++ b/conf/test_spaceranger_ffpe_cytassist.config @@ -24,6 +24,8 @@ params { input = './assets/samplesheet_spaceranger_ffpe_cytassist.csv' run_spaceranger = true spaceranger_probeset = "https://cf.10xgenomics.com/supp/spatial-exp/probeset/Visium_Human_Transcriptome_Probe_Set_v2.0_GRCh38-2020-A.csv" + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 outdir = 'results' spaceranger_image_type = "cytaimage" } From 3666bb2157a736a52b065cf3f8faeeb84d9d894b Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 10:07:02 +0200 Subject: [PATCH 108/410] Update config filese to match conventions --- .github/workflows/ci.yml | 3 ++- nf-test.config | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83aa254..821f19c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: strategy: matrix: NXF_VER: ["latest-everything"] + profile: ["docker"] steps: - name: Check out pipeline code uses: actions/checkout@v3 @@ -45,7 +46,7 @@ jobs: # Run nf-test - name: Run nf-test - run: nf-test test tests/pipeline/ --tap=test.tap + run: nf-test test tests/pipeline/ --profile=${{ matrix.profile }} --tap=test.tap # If the test fails, output the software_versions.yml using the 'batcat' utility - name: Output log on failure diff --git a/nf-test.config b/nf-test.config index f345cb2..aa286e3 100644 --- a/nf-test.config +++ b/nf-test.config @@ -1,8 +1,16 @@ config { - + // location for all nf-tests testsDir "tests" + + // nf-test directory including temporary files for each test workDir ".nf-test" + + // location of library folder that is added automatically to the classpath + libDir "tests/pipeline/lib/" + + // location of an optional nextflow.config file specific for executing tests configFile "nextflow.config" - profile "test,docker" + // test profile is overriden in the `when` blocks + profile "" } From 4e91f28ee6b01dff85efdf979f3b741ab3a8b3cc Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 14 Jun 2023 12:16:08 +0200 Subject: [PATCH 109/410] Move environment specifications into subdirectory --- env/{ => reports}/Dockerfile | 0 env/{ => reports}/environment.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename env/{ => reports}/Dockerfile (100%) rename env/{ => reports}/environment.yml (100%) diff --git a/env/Dockerfile b/env/reports/Dockerfile similarity index 100% rename from env/Dockerfile rename to env/reports/Dockerfile diff --git a/env/environment.yml b/env/reports/environment.yml similarity index 100% rename from env/environment.yml rename to env/reports/environment.yml From 32ba88111a0249ce5cf4eaeab1cf6134e3efb4aa Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 12:22:30 +0200 Subject: [PATCH 110/410] Keep test profile in nf-test command --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 821f19c..6ca0126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,6 @@ jobs: strategy: matrix: NXF_VER: ["latest-everything"] - profile: ["docker"] steps: - name: Check out pipeline code uses: actions/checkout@v3 @@ -46,7 +45,7 @@ jobs: # Run nf-test - name: Run nf-test - run: nf-test test tests/pipeline/ --profile=${{ matrix.profile }} --tap=test.tap + run: nf-test test tests/pipeline/ --profile=test,docker --tap=test.tap # If the test fails, output the software_versions.yml using the 'batcat' utility - name: Output log on failure From 8858968da6e0fd7a749313c713e79d1bd26355d5 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 12:24:20 +0200 Subject: [PATCH 111/410] Rename tests file --- nf-test.config | 2 +- .../{spatialtranscriptomics.nf.test => test_downstream.nf.test} | 0 ...ranscriptomics.nf.test.snap => test_downstream.nf.test.snap} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/pipeline/{spatialtranscriptomics.nf.test => test_downstream.nf.test} (100%) rename tests/pipeline/{spatialtranscriptomics.nf.test.snap => test_downstream.nf.test.snap} (100%) diff --git a/nf-test.config b/nf-test.config index aa286e3..b57ac7d 100644 --- a/nf-test.config +++ b/nf-test.config @@ -11,6 +11,6 @@ config { // location of an optional nextflow.config file specific for executing tests configFile "nextflow.config" - // test profile is overriden in the `when` blocks + // test profile is overriden via nf-test CLI profile "" } diff --git a/tests/pipeline/spatialtranscriptomics.nf.test b/tests/pipeline/test_downstream.nf.test similarity index 100% rename from tests/pipeline/spatialtranscriptomics.nf.test rename to tests/pipeline/test_downstream.nf.test diff --git a/tests/pipeline/spatialtranscriptomics.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap similarity index 100% rename from tests/pipeline/spatialtranscriptomics.nf.test.snap rename to tests/pipeline/test_downstream.nf.test.snap From fb1be59d5acdbe7765337ccba5b6c8ab9e906cb9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 14 Jun 2023 13:14:51 +0200 Subject: [PATCH 112/410] Update samplesheet checker Python version Update the Python version of the samplesheet checker module to be compatible with both AMD64 and ARM64 for the Conda profile; the previously specified 3.8.3 is not available on ARM64. --- modules/local/samplesheet_check.nf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/samplesheet_check.nf b/modules/local/samplesheet_check.nf index 41a09a6..2fef24e 100644 --- a/modules/local/samplesheet_check.nf +++ b/modules/local/samplesheet_check.nf @@ -2,10 +2,10 @@ process SAMPLESHEET_CHECK { tag "$samplesheet" label 'process_single' - conda "conda-forge::python=3.8.3" + conda "conda-forge::python=3.9.16" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/python:3.8.3' : - 'biocontainers/python:3.8.3' }" + 'https://depot.galaxyproject.org/singularity/python:3.9.16' : + 'biocontainers/python:3.9.16' }" input: path samplesheet From ef6c20dff4c8adaf47c4d38b1559e9c96a730945 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 14 Jun 2023 13:16:22 +0200 Subject: [PATCH 113/410] Update ST_READ_DATA module Conda specification Update the Conda environment specification for the ST_READ_DATA module to be compatible with ARM64 architectures; specific earlier versions of `matplotlib` and `pandas` are required due to (1) a metaclass conflict and (2) deprecation of `is_categorical`. --- modules/local/st_read_data.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index 3d20e1d..385813d 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -6,7 +6,7 @@ process ST_READ_DATA { tag "${meta.id}" label "process_low" - conda "conda-forge::scanpy=1.7.2" + conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" From c27e7423ed8618484eab027e6ea3ef18c3ff385b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 14 Jun 2023 14:35:45 +0200 Subject: [PATCH 114/410] Add Conda directives for report modules Add Conda directives for the three report modules. Due to problems with Quarto and Pandoc with Conda, the Conda profile does currently not work on ARM64 systems, so these only work for non-ARM64 systems; container-based profiles work just fine, though. --- env/st_spatial_de/environment.yml | 13 +++++++++++++ modules/local/st_clustering.nf | 9 +++++++++ modules/local/st_qc_and_normalisation.nf | 9 +++++++++ modules/local/st_spatial_de.nf | 8 ++++++++ 4 files changed, 39 insertions(+) create mode 100644 env/st_spatial_de/environment.yml diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml new file mode 100644 index 0000000..28457d1 --- /dev/null +++ b/env/st_spatial_de/environment.yml @@ -0,0 +1,13 @@ +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - quarto=1.3.353 + - jupyter=1.0.0 + - leidenalg=0.9.1 + - papermill=2.3.4 + - pip=23.0.1 + - scanpy=1.9.3 + - pip: + - SpatialDE==1.1.3 diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index eaaac1b..28a4f11 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -9,8 +9,17 @@ process ST_CLUSTERING { tag "${meta.id}" label "process_low" + conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0 conda-forge::leidenalg=0.9.1" container "docker.io/erikfas/spatialtranscriptomics" + // Exit if running this module with -profile conda / -profile mamba on ARM64 + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + architecture = System.getProperty("os.arch") + if (architecture == "arm64" || architecture == "aarch64") { + exit 1, "The ST_CLUSTERING module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." + } + } + input: path(report) tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index fdcaec9..dc1a432 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -9,8 +9,17 @@ process ST_QC_AND_NORMALISATION { tag "${meta.id}" label "process_low" + conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0" container "docker.io/erikfas/spatialtranscriptomics" + // Exit if running this module with -profile conda / -profile mamba on ARM64 + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + architecture = System.getProperty("os.arch") + if (architecture == "arm64" || architecture == "aarch64") { + exit 1, "The ST_QC_AND_NORMALISATION module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." + } + } + input: path(report) tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 75359e4..4068b4f 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -9,8 +9,16 @@ process ST_SPATIAL_DE { tag "${meta.id}" label "process_medium" + conda "env/st_spatial_de/environment.yml" container "docker.io/erikfas/spatialtranscriptomics" + // Exit if running this module with -profile conda / -profile mamba on ARM64 + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + architecture = System.getProperty("os.arch") + if (architecture == "arm64" || architecture == "aarch64") { + exit 1, "The ST_SPATIAL_DE module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." + } + } input: path(report) tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") From 97ef9c4cdf631a619c98c8a667e487399c354264 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 14 Jun 2023 14:37:55 +0200 Subject: [PATCH 115/410] Add TODOs: update Conda when Quarto/Pandoc works --- modules/local/st_clustering.nf | 2 +- modules/local/st_qc_and_normalisation.nf | 2 +- modules/local/st_spatial_de.nf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 28a4f11..efe21aa 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -4,7 +4,7 @@ process ST_CLUSTERING { // TODO: Add a better description - // TODO: Find solution for Quarto with Conda + // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" label "process_low" diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index dc1a432..9ede26c 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -4,7 +4,7 @@ process ST_QC_AND_NORMALISATION { // TODO: Add a better description - // TODO: Find solution for Quarto with Conda + // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" label "process_low" diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 4068b4f..0e4cd60 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -4,7 +4,7 @@ process ST_SPATIAL_DE { // TODO: Add a better description - // TODO: Find solution for Quarto with Conda + // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" label "process_medium" From 9f58dd02b5495eab60132a67479400a036e4005d Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 12:57:25 +0200 Subject: [PATCH 116/410] install modules from nf-core --- modules.json | 5 +++++ modules/nf-core/spaceranger/count/main.nf | 15 +++++++-------- modules/nf-core/spaceranger/count/meta.yml | 22 +++++++++++++++++++++- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/modules.json b/modules.json index 36849cd..addd3c6 100644 --- a/modules.json +++ b/modules.json @@ -11,6 +11,11 @@ "installed_by": ["modules"], "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" }, + "spaceranger/count": { + "branch": "master", + "git_sha": "f874515c9b49c07b68cf1bf06d711fe46d8ab2aa", + "installed_by": ["modules"] + }, "untar": { "branch": "master", "git_sha": "5c460c5a4736974abde2843294f35307ee2b0e5e", diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index 8e271a1..7d9772b 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -1,10 +1,9 @@ -// TODO nf-core: install with nf-core modules install once merged process SPACERANGER_COUNT { tag "$meta.id" label 'process_high' // TODO push to nf-core docker - container "ghcr.io/grst/spaceranger:2.1.0" + container "docker.io/nfcore/spaceranger:2.1.0" // Exit if running this module with -profile conda / -profile mamba if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { @@ -13,7 +12,7 @@ process SPACERANGER_COUNT { input: - tuple val(meta), path(reads), path(image), path(alignment), path(slidefile) + tuple val(meta), path(reads), path(image), path(cytaimage), path(darkimage), path(colorizedimage), path(alignment), path(slidefile) path(reference) path(probeset) @@ -31,21 +30,21 @@ process SPACERANGER_COUNT { def probeset = probeset ? "--probe-set=\"${probeset}\"" : "" def alignment = alignment ? "--loupe-alignment=\"${alignment}\"" : "" def slidefile = slidefile ? "--slidefile=\"${slidefile}\"" : "" - // Choose the appropriate flag depending on the input type, e.g. - // --darkimage for fluorescence, --cytaimage for cytassist, ... - // Defaults to `--image` for brightfield microscopy. - def img_type = task.ext.img_type ?: "--image" + def image = image ? "--image=\"${image}\"" : "" + def cytaimage = cytaimage ? "--cytaimage=\"${cytaimage}\"" : "" + def darkimage = darkimage ? "--darkimage=\"${darkimage}\"" : "" + def colorizedimage = colorizedimage ? "--colorizedimage=\"${colorizedimage}\"" : "" """ spaceranger count \\ --id="${prefix}" \\ --sample="${meta.id}" \\ --fastqs=. \\ - ${img_type}="${image}" \\ --slide="${meta.slide}" \\ --area="${meta.area}" \\ --transcriptome="${reference}" \\ --localcores=${task.cpus} \\ --localmem=${task.memory.toGiga()} \\ + $image $cytaimage $darkimage $colorizedimage \\ $probeset \\ $alignment \\ $slidefile \\ diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml index e6fdd82..7ff599f 100644 --- a/modules/nf-core/spaceranger/count/meta.yml +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -38,7 +38,27 @@ input: pattern: "${Sample_Name}_S1_L00${Lane_Number}_${I1,I2,R1,R2}_001.fastq.gz" - image: type: file - description: Path to scan of the tissue slide. + description: Brightfield tissue H&E image in JPEG or TIFF format. + pattern: "*.{tif,tiff,jpg,jpeg}" + - cytaimage: + type: file + description: | + CytAssist instrument captured eosin stained Brightfield tissue image with fiducial + frame in TIFF format. The size of this image is set at 3k in both dimensions and this image should + not be modified any way before passing it as input to either Space Ranger or Loupe Browser. + pattern: "*.{tif,tiff}" + - darkimage: + type: file + description: | + Optional for dark background fluorescence microscope image input. Multi-channel, dark-background fluorescence + image as either a single, multi-layer TIFF file or as multiple TIFF or JPEG files. + pattern: "*.{tif,tiff,jpg,jpeg}" + - colorizedimage: + type: file + description: | + Required for color composite fluorescence microscope image input. + A color composite of one or more fluorescence image channels saved as a single-page, + single-file color TIFF or JPEG. pattern: "*.{tif,tiff,jpg,jpeg}" - alignment: type: file From 1c4db6b50144c9b6bc8b5039771bed778d20c323 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:31:51 +0200 Subject: [PATCH 117/410] Skip NF 22.10.1 on dev branch --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ca0126..e5b3b95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,19 @@ jobs: name: Run pipeline with test data runs-on: ubuntu-latest strategy: - matrix: - NXF_VER: ["latest-everything"] + fail-fast: false + NXF_VER: + - "22.10.1" + - "latest-everything" steps: + # Skip if it's a pull_request to dev and NXF_VER is '22.10.1' + - name: Skip condition + id: condition + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + fi + - name: Check out pipeline code uses: actions/checkout@v3 From 64db2f3f044af1f37799a95099d0de4664e11692 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:33:43 +0200 Subject: [PATCH 118/410] Fix matrix --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5b3b95..a4de7ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,10 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - NXF_VER: - - "22.10.1" - - "latest-everything" + matrix: + NXF_VER: + - "22.10.1" + - "latest-everything" steps: # Skip if it's a pull_request to dev and NXF_VER is '22.10.1' - name: Skip condition From 50d333ced3186b4f1f790cf387dd79bb425ba307 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:48:26 +0200 Subject: [PATCH 119/410] Try alternative approach to skipping --- .github/workflows/ci.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4de7ee..95b9777 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,24 +21,28 @@ concurrency: cancel-in-progress: true jobs: + define_nxf_versions: + name: Choose nextflow versions to test against depending on target branch + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.nxf_versions.outputs.matrix }} + steps: + - id: nxf_versions + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then + echo "['latest-everything']" >> $GITHUB_OUTPUT + else + echo "['latest-everything', '22.10.1']" >> $GITHUB_OUTPUT + fi + test: name: Run pipeline with test data runs-on: ubuntu-latest strategy: fail-fast: false matrix: - NXF_VER: - - "22.10.1" - - "latest-everything" + NXF_VER: ${{ fromJson(needs.define_nxf_versions.outputs.matrix) }} steps: - # Skip if it's a pull_request to dev and NXF_VER is '22.10.1' - - name: Skip condition - id: condition - run: | - if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then - echo "skip=true" >> $GITHUB_OUTPUT - fi - - name: Check out pipeline code uses: actions/checkout@v3 From 4392c91f3448225ec16bd3c649cdf80b6a058ea3 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:53:41 +0200 Subject: [PATCH 120/410] fix echo --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95b9777..39929bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,9 @@ jobs: - id: nxf_versions run: | if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then - echo "['latest-everything']" >> $GITHUB_OUTPUT + echo matrix='["latest-everything"]' >> $GITHUB_OUTPUT else - echo "['latest-everything', '22.10.1']" >> $GITHUB_OUTPUT + echo matrix='["latest-everything", "22.10.1"]' >> $GITHUB_OUTPUT fi test: From 3efcfea84f9b7b1332b0a8650871dd68593d94fb Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:57:27 +0200 Subject: [PATCH 121/410] debug --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39929bc..d529c81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: else echo matrix='["latest-everything", "22.10.1"]' >> $GITHUB_OUTPUT fi + cat $GITHUB_OUTPUT test: name: Run pipeline with test data From 7a41a00bdf69524d780534f55a80df419e6a07d7 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Wed, 14 Jun 2023 16:59:46 +0200 Subject: [PATCH 122/410] Fix action dependency --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d529c81..fe53a39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,14 @@ jobs: - id: nxf_versions run: | if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then - echo matrix='["latest-everything"]' >> $GITHUB_OUTPUT + echo matrix='["latest-everything"]' | tee -a $GITHUB_OUTPUT else - echo matrix='["latest-everything", "22.10.1"]' >> $GITHUB_OUTPUT + echo matrix='["latest-everything", "22.10.1"]' | tee -a $GITHUB_OUTPUT fi - cat $GITHUB_OUTPUT test: name: Run pipeline with test data + needs: define_nxf_versions runs-on: ubuntu-latest strategy: fail-fast: false From 6799eb7463bd85a0199cd26c0020f8887285b87f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 15 Jun 2023 13:13:12 +0200 Subject: [PATCH 123/410] Reorganize test cases --- assets/homo_sapiens_chr22_reference.tar.gz | Bin 0 -> 305040 bytes conf/test.config | 11 +++- conf/test_spaceranger_ffpe_cytassist.config | 31 ---------- conf/test_spaceranger_ffpe_v1.config | 27 --------- tests/pipeline/test_downstream.nf.test | 8 ++- tests/pipeline/test_sapceranger.nf.test | 61 ++++++++++++++++++++ 6 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 assets/homo_sapiens_chr22_reference.tar.gz delete mode 100644 conf/test_spaceranger_ffpe_cytassist.config delete mode 100644 conf/test_spaceranger_ffpe_v1.config create mode 100644 tests/pipeline/test_sapceranger.nf.test diff --git a/assets/homo_sapiens_chr22_reference.tar.gz b/assets/homo_sapiens_chr22_reference.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..d8517073ee4d11e045b50f14f2665659c73f21f2 GIT binary patch literal 305040 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�o>I$+ETU$#@R>1yumE$mY4k-7*+c+)8scZiaE9O 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

    =da~kR|g3-F*JD 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∓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

    {dLyzk4)JC%geiP6uzDs7+1 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 --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Test pipeline incl. spaceranger with cytassist ffpe sample' - - // Limit resources so that this can run on GitHub Actions - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' - - // Input and output - // TODO nf-core: this should be a link pointing to github - input = './assets/samplesheet_spaceranger_ffpe_cytassist.csv' - run_spaceranger = true - spaceranger_probeset = "https://cf.10xgenomics.com/supp/spatial-exp/probeset/Visium_Human_Transcriptome_Probe_Set_v2.0_GRCh38-2020-A.csv" - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 - outdir = 'results' - spaceranger_image_type = "cytaimage" -} diff --git a/conf/test_spaceranger_ffpe_v1.config b/conf/test_spaceranger_ffpe_v1.config deleted file mode 100644 index 0bd5059..0000000 --- a/conf/test_spaceranger_ffpe_v1.config +++ /dev/null @@ -1,27 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - - Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test, --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Test pipeline incl. spaceranger with ffpe v1 chemistry sample' - - // Limit resources so that this can run on GitHub Actions - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' - - // Input and output - // TODO nf-core: this should be a link pointing to github - input = './assets/samplesheet_spaceranger_ffpe_v1.csv' - run_spaceranger = true - outdir = 'results' -} diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 3d6dcc0..045fb4d 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -1,11 +1,17 @@ nextflow_pipeline { - name "Test Workflow main.nf" + name "Test workflow (only downstream part)" script "main.nf" tag "pipeline" test("Andersson_Nat_Gen_2021_CID4465") { when { params { + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv' + run_spaceranger = false + spaceranger_probeset = null + spaceranger_image_type = null + st_preprocess_min_counts = 500 + st_preprocess_min_genes = 250 outdir = "$outputDir" } } diff --git a/tests/pipeline/test_sapceranger.nf.test b/tests/pipeline/test_sapceranger.nf.test new file mode 100644 index 0000000..ac0d388 --- /dev/null +++ b/tests/pipeline/test_sapceranger.nf.test @@ -0,0 +1,61 @@ +nextflow_pipeline { + name "Test full workflow including spaceranger" + script "main.nf" + tag "pipeline" + + test("spaceranger ffpe v2 cytassist (default `-profile test`)") { + when { + params { + // This is the default `test` profile, no need to specify additional parameters + outdir = "$outputDir" + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + { assert snapshot( + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + ).match("reports")}, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } + ) + } + } + + + test("spaceranger ffpe v1") { + when { + params { + // TODO nf-core: this should be a link pointing to github + input = './assets/samplesheet_spaceranger_ffpe_v1.csv' + run_spaceranger = true + spaceranger_probeset = null + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 + spaceranger_image_type = "image" + outdir = "$outputDir" + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + { assert snapshot( + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), + path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + ).match("reports")}, + { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } + ) + } + } + + + +} From 9c7ba7d66f6d2ded7b24bcf18d3930c875df40ac Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 15 Jun 2023 13:43:43 +0200 Subject: [PATCH 124/410] Update samplesheets to work with directory --- .gitignore | 1 + assets/homo_sapiens_chr22_reference.tar.gz | Bin 305040 -> 0 bytes assets/samplesheet.csv | 2 -- assets/samplesheet_downstream.csv | 2 ++ .../samplesheet_spaceranger_ffpe_cytassist.csv | 5 ++--- assets/samplesheet_spaceranger_ffpe_v1.csv | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 assets/homo_sapiens_chr22_reference.tar.gz delete mode 100644 assets/samplesheet.csv create mode 100644 assets/samplesheet_downstream.csv diff --git a/.gitignore b/.gitignore index 74977c3..1eb2788 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ log reports .nf-test/ nf-test +test-datasets diff --git a/assets/homo_sapiens_chr22_reference.tar.gz b/assets/homo_sapiens_chr22_reference.tar.gz deleted file mode 100644 index d8517073ee4d11e045b50f14f2665659c73f21f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 305040 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�o>I$+ETU$#@R>1yumE$mY4k-7*+c+)8scZiaE9O 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

    =da~kR|g3-F*JD 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∓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

    {dLyzk4)JC%geiP6uzDs7+1 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 Date: Thu, 15 Jun 2023 15:45:19 +0200 Subject: [PATCH 125/410] move samplesheets to test data --- assets/samplesheet_downstream.csv | 2 -- assets/samplesheet_spaceranger_ffpe_cytassist.csv | 2 -- assets/samplesheet_spaceranger_ffpe_v1.csv | 2 -- conf/test.config | 6 +++--- 4 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 assets/samplesheet_downstream.csv delete mode 100644 assets/samplesheet_spaceranger_ffpe_cytassist.csv delete mode 100644 assets/samplesheet_spaceranger_ffpe_v1.csv diff --git a/assets/samplesheet_downstream.csv b/assets/samplesheet_downstream.csv deleted file mode 100644 index 2970820..0000000 --- a/assets/samplesheet_downstream.csv +++ /dev/null @@ -1,2 +0,0 @@ -sample,spaceranger_dir -Andersson_Nat_Gen_2021_CID4465_subsampled,TODO diff --git a/assets/samplesheet_spaceranger_ffpe_cytassist.csv b/assets/samplesheet_spaceranger_ffpe_cytassist.csv deleted file mode 100644 index ad11af2..0000000 --- a/assets/samplesheet_spaceranger_ffpe_cytassist.csv +++ /dev/null @@ -1,2 +0,0 @@ -sample,fastq_dir,tissue_hires_image,slide,area,manual_alignment,slidefile -CytAssist_11mm_FFPE_Human_Glioblastoma_2,assets/test-datasets/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_image.tif,V52Y10-317,B1,,https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V52Y10/V52Y10-317.gpr diff --git a/assets/samplesheet_spaceranger_ffpe_v1.csv b/assets/samplesheet_spaceranger_ffpe_v1.csv deleted file mode 100644 index ed0a238..0000000 --- a/assets/samplesheet_spaceranger_ffpe_v1.csv +++ /dev/null @@ -1,2 +0,0 @@ -sample,fastq_dir,tissue_hires_image,slide,area,manual_alignment,slidefile -Visium_FFPE_Human_Ovarian_Cancer,assets/test-datasets/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe,https://github.com/nf-core/test-datasets/raw/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-ovarian-cancer-1-standard_v1_ffpe/Visium_FFPE_Human_Ovarian_Cancer_image.jpg,V10L13-020,D1,,https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V10L13/V10L13-020.gpr diff --git a/conf/test.config b/conf/test.config index ed5c8e8..4315490 100644 --- a/conf/test.config +++ b/conf/test.config @@ -21,10 +21,10 @@ params { // Input and output // TODO nf-core: this should be a link pointing to github - input = './assets/samplesheet_spaceranger_ffpe_cytassist.csv' + input = './test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv' run_spaceranger = true - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/10xgenomics/spaceranger/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" - spaceranger_reference = "./assets/homo_sapiens_chr22_reference.tar.gz" + spaceranger_probeset = "./test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" + spaceranger_reference = "./test-datasets/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 spaceranger_image_type = "cytaimage" From a3362a4cb587ff40c0ec1f1993ee2067335d33ab Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 15 Jun 2023 16:12:39 +0200 Subject: [PATCH 126/410] WIP input check with folders instead of files --- subworkflows/local/input_check.nf | 94 ++++++++++++------------------- 1 file changed, 35 insertions(+), 59 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 9ea8d34..ddb4b86 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -14,10 +14,7 @@ workflow INPUT_CHECK { if ( params.run_spaceranger ) { st_data = SAMPLESHEET_CHECK.out.csv .splitCsv ( header: true, sep: ',' ) - // collapse technical replicates - .map { row -> [row.sample, row]} - .groupTuple() - .map { id, sample_info -> create_spaceranger_channels(sample_info) } + .map { create_spaceranger_channels(it) } } else { st_data = SAMPLESHEET_CHECK.out.csv .splitCsv ( header: true, sep: ',' ) @@ -29,65 +26,44 @@ workflow INPUT_CHECK { versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } -def get_unique(List sample_info, String key) { - def val = null - for (row in sample_info) { - if (val != null && val != row[key]) { - exit 1, "ERROR: Please check input samplesheet -> '${key}' is not consistent for all technical replicates of the same sample. Actual: '${row[key]}'. Expected: '${val}'." - } - val = row[key] - } - return val -} - // Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] -def create_spaceranger_channels(List sample_info) { - def meta = [:] - meta.id = get_unique(sample_info, "sample") - meta.slide = get_unique(sample_info, "slide") - meta.area = get_unique(sample_info, "area") - tissue_hires_image = get_unique(sample_info, "tissue_hires_image") - manual_alignment = get_unique(sample_info, "manual_alignment") - slidefile = get_unique(sample_info, "slidefile") - fastq_files = [] - for (row in sample_info) { - fastq_files.add(file(row.fastq_1, checkIfExists: true)) - fastq_files.add(file(row.fastq_2, checkIfExists: true)) - } +def create_spaceranger_channels(LinkedHashMap meta) { + meta["id"] = meta["sample"] + meta.remove("sample") - tissue_hires_image = file(tissue_hires_image, checkIfExists: true) - manual_alignment = manual_alignment ? file(manual_alignment, checkIfExists: true) : [] - slidefile = slidefile ? file(slidefile, checkIfExists: true) : [] - return [meta, fastq_files, tissue_hires_image, manual_alignment, slidefile] + fastq_dir = meta.remove("fastq_dir") + files_to_check = [ + "fastq_dir", + "image", + "cytaimage", + "colorizedimage", + "darkimage" + ] + def raw_meta = [] + for (entry in row) { + if (entry.key in files_to_check && !file(entry.value).exists()) { + exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" + } + } + if ( row.manual_alignment.isEmpty() ) { + manual_alignment = [] + } else { + if (!file(row.manual_alignment).exists()) { + exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" + } + manual_alignment = file ( row.manual_alignment ) + } - - // files_to_check = [ - // "tissue_hires_image": tissue_hires_image, - // "tissue_hires_image" - // ] - // def raw_meta = [] - // for (entry in row) { - // if (entry.key in files_to_check && !file(entry.value).exists()) { - // exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" - // } - // } - // if ( row.manual_alignment.isEmpty() ) { - // manual_alignment = [] - // } else { - // if (!file(row.manual_alignment).exists()) { - // exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" - // } - // manual_alignment = file ( row.manual_alignment ) - // } - - // raw_meta = [ - // meta, - // file(row.fastq_dir), - // file(row.tissue_hires_image), - // manual_alignment - // ] - // return raw_meta + raw_meta = [ + meta, + file(row.fastq_dir), + file(row.tissue_hires_image), + row.slide, + row.area, + manual_alignment + ] + return raw_meta } // Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ From 779e04959bfabc3d1fbf95dbbdcbe7ada6e594da Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 15 Jun 2023 16:30:31 +0200 Subject: [PATCH 127/410] Update usage docs --- docs/usage.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index ac8773c..4ecc7cf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -170,7 +170,11 @@ Use this parameter to choose a configuration profile. Profiles can give configur Several generic profiles are bundled with the pipeline which instruct the pipeline to use software packaged using different methods (Docker, Singularity, Podman, Shifter, Charliecloud, Apptainer, Conda) - see below. -> We highly recommend the use of Docker or Singularity containers for full pipeline reproducibility, however when this is not possible, Conda is also supported. +> We highly recommend the use of Docker or Singularity containers for full +> pipeline reproducibility, however when this is not possible, Conda is also +> supported. Please note that Conda is not at all supported for Space Ranger +> processing, and only supported on non-ARM64 architectures for analyses +> downstream of Space Ranger. The pipeline also dynamically loads configurations from [https://github.com/nf-core/configs](https://github.com/nf-core/configs) when it runs, making multiple config profiles for various institutional clusters available at run time. For more information and to see if your system is available in these configs please see the [nf-core/configs documentation](https://github.com/nf-core/configs#documentation). From 179bb97362bf77f3975077efd50fe036fa481089 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 16 Jun 2023 09:30:28 +0200 Subject: [PATCH 128/410] Change SAMPLESHEET_CHECK module to python=3.9 Version 3.9.16 is available in Conda but not in biocontainers, so a downgrade to 3.9 is required for compatibility with both container and conda profiles. --- modules/local/samplesheet_check.nf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/local/samplesheet_check.nf b/modules/local/samplesheet_check.nf index 2fef24e..5bc09e4 100644 --- a/modules/local/samplesheet_check.nf +++ b/modules/local/samplesheet_check.nf @@ -2,10 +2,10 @@ process SAMPLESHEET_CHECK { tag "$samplesheet" label 'process_single' - conda "conda-forge::python=3.9.16" + conda "conda-forge::python=3.9" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/python:3.9.16' : - 'biocontainers/python:3.9.16' }" + 'https://depot.galaxyproject.org/singularity/python:3.9' : + 'biocontainers/python:3.9' }" input: path samplesheet From 61bd1878781d5f693876c013b7562aaca8622e73 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 16 Jun 2023 09:47:22 +0200 Subject: [PATCH 129/410] Update nf-test snapshot --- tests/pipeline/test_downstream.nf.test.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 71cbbd6..fd326ff 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,c0eef3dad596f1489d30a0172871168d", "st_qc_and_normalisation.html:md5,dc7cbe12e1048f6d913b222e57e0c534" ], - "timestamp": "2023-06-07T07:45:37+0000" + "timestamp": "2023-06-16T07:44:23+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.8.3}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-07T07:45:37+0000" + "timestamp": "2023-06-16T07:44:23+0000" } -} +} \ No newline at end of file From fce7d235f2d0e81786a04b79d8a77c15f1a7916b Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 10:15:14 +0200 Subject: [PATCH 130/410] WIP create channels --- subworkflows/local/input_check.nf | 62 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index ddb4b86..d15b843 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -26,44 +26,46 @@ workflow INPUT_CHECK { versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } + + // Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] def create_spaceranger_channels(LinkedHashMap meta) { meta["id"] = meta["sample"] meta.remove("sample") + def get_file_from_meta = {key -> + v = meta.remove(key); + return v ? file(v) : [] + } + fastq_dir = meta.remove("fastq_dir") + manual_alignment = get_file_from_meta("manual_alignment") + println("${fastq_dir}/${meta['id']}*.fastq.gz") + fastq_files = Channel.fromPath("${fastq_dir}/${meta['id']}*.fastq.gz").view().to_list().view() + slidefile = get_file_from_meta("slidefile") + image = get_file_from_meta("image") + cytaimage = get_file_from_meta("cytaimage") + colorizedimage = get_file_from_meta("colorizedimage") + darkimage = get_file_from_meta("darkimage") - files_to_check = [ - "fastq_dir", - "image", - "cytaimage", - "colorizedimage", - "darkimage" - ] - def raw_meta = [] - for (entry in row) { - if (entry.key in files_to_check && !file(entry.value).exists()) { - exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" - } - } - if ( row.manual_alignment.isEmpty() ) { - manual_alignment = [] - } else { - if (!file(row.manual_alignment).exists()) { - exit 1, "ERROR: Please check input samplesheet -> manual_alignment file does not exist!\n${row.manual_alignment}" - } - manual_alignment = file ( row.manual_alignment ) - } + // if(!fastq_files.length) { + // error "No `fastq_dir` specified or no samples found in folder." + // } else { + // log.info "${fastq_files.length} FASTQ files found for sample ${meta['id']}." + // } - raw_meta = [ - meta, - file(row.fastq_dir), - file(row.tissue_hires_image), - row.slide, - row.area, - manual_alignment - ] - return raw_meta + // if(manual_alignment && !manual_alignment.exist()) { + // error "Manual alignment file does not exist: ${manual_alignment}" + // } + // if(slidefile && !slidefile.exist()) { + // error "Slidefile does not exist: ${manual_alignment}" + // } + // if(!(image || cytaimage || colorizedimage || darkimage)) { + // error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" + // } + // println(this.binding) + + return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] } // Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ From 8329c7e5d116b62c27bfbd9fcf8c6607199bc334 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 10:39:03 +0200 Subject: [PATCH 131/410] Update input checks --- subworkflows/local/input_check.nf | 34 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index d15b843..c0a3ffe 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -39,31 +39,29 @@ def create_spaceranger_channels(LinkedHashMap meta) { } fastq_dir = meta.remove("fastq_dir") + fastq_files = file("${fastq_dir}/${meta['id']}*.fastq.gz") manual_alignment = get_file_from_meta("manual_alignment") - println("${fastq_dir}/${meta['id']}*.fastq.gz") - fastq_files = Channel.fromPath("${fastq_dir}/${meta['id']}*.fastq.gz").view().to_list().view() slidefile = get_file_from_meta("slidefile") image = get_file_from_meta("image") cytaimage = get_file_from_meta("cytaimage") colorizedimage = get_file_from_meta("colorizedimage") darkimage = get_file_from_meta("darkimage") - // if(!fastq_files.length) { - // error "No `fastq_dir` specified or no samples found in folder." - // } else { - // log.info "${fastq_files.length} FASTQ files found for sample ${meta['id']}." - // } - - // if(manual_alignment && !manual_alignment.exist()) { - // error "Manual alignment file does not exist: ${manual_alignment}" - // } - // if(slidefile && !slidefile.exist()) { - // error "Slidefile does not exist: ${manual_alignment}" - // } - // if(!(image || cytaimage || colorizedimage || darkimage)) { - // error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" - // } - // println(this.binding) + if(!fastq_files.size()) { + error "No `fastq_dir` specified or no samples found in folder." + } else { + log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." + } + + check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] + for(k in check_optional_files) { + if(this.binding[k] && !this.binding[k].exists()) { + error "File for `${k}` is specified, but does not exist: ${this.binding[k]}." + } + } + if(!(image || cytaimage || colorizedimage || darkimage)) { + error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" + } return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] } From 214306b06bcd0410478fd5fe11a47ca3775b3f92 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 12:15:52 +0200 Subject: [PATCH 132/410] Support tar.gz archives asinput for spaceranger_reference --- conf/test.config | 1 - nextflow.config | 2 +- nextflow_schema.json | 13 +++---------- subworkflows/local/input_check.nf | 3 +-- subworkflows/local/spaceranger.nf | 19 ++++++++++--------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/conf/test.config b/conf/test.config index 4315490..9953d36 100644 --- a/conf/test.config +++ b/conf/test.config @@ -27,6 +27,5 @@ params { spaceranger_reference = "./test-datasets/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 - spaceranger_image_type = "cytaimage" outdir = 'results' } diff --git a/nextflow.config b/nextflow.config index d4abc14..f00dc05 100644 --- a/nextflow.config +++ b/nextflow.config @@ -14,7 +14,7 @@ params { // Spaceranger options run_spaceranger = false - spaceranger_reference = null + spaceranger_reference = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" spaceranger_probeset = null spaceranger_image_type = "image" diff --git a/nextflow_schema.json b/nextflow_schema.json index b1f2397..1615de2 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -37,8 +37,9 @@ "spaceranger_reference": { "type": "string", "format": "file-path", - "description": "Location of Space Ranger reference directory", - "fa_icon": "fas fa-folder-open" + "description": "Location of Space Ranger reference directory. May be packed as `tar.gz` file.", + "fa_icon": "fas fa-folder-open", + "default": "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" }, "spaceranger_probeset": { "type": "string", @@ -48,14 +49,6 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, - "spaceranger_image_type": { - "type": "string", - "description": "Image type for spaceranger", - "enum": ["image", "cytaimage", "darkimage", "colorizedimage"], - "fa_icon": "fas fa-image", - "default": "image", - "help_text": "Choose 'cytaimage' to enable CytAssist mode or 'darkimage' to work with dark field microscopy images." - }, "email": { "type": "string", "description": "Email address for completion summary.", diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index c0a3ffe..7c109f1 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -30,8 +30,7 @@ workflow INPUT_CHECK { // Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] def create_spaceranger_channels(LinkedHashMap meta) { - meta["id"] = meta["sample"] - meta.remove("sample") + meta["id"] = meta.remove("sample") def get_file_from_meta = {key -> v = meta.remove(key); diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 0b58112..ce10089 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -2,7 +2,7 @@ // Raw data processing with Space Ranger // -include { UNTAR as SPACERANGER_DOWNLOAD_REFERENCE } from "../../modules/nf-core/untar" +include { UNTAR as SPACERANGER_UNTAR_REFERENCE } from "../../modules/nf-core/untar" include { SPACERANGER_COUNT } from '../../modules/nf-core/spaceranger/count' workflow SPACERANGER { @@ -18,15 +18,16 @@ workflow SPACERANGER { // Reference files // ch_reference = Channel.empty() - if (params.spaceranger_reference) { - ch_reference = file ( params.spaceranger_reference, type: "dir", checkIfExists: true ) - } else { - SPACERANGER_DOWNLOAD_REFERENCE ([ - [id: "refdata-gex-GRCh38-2020-A"], - file("https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz") + if (params.spaceranger_reference ==~ /.*\.tar\.gz$/) { + ref_file = file(params.spaceranger_reference) + SPACERANGER_UNTAR_REFERENCE ([ + [id: "reference"], + ref_file ]) - ch_reference = SPACERANGER_DOWNLOAD_REFERENCE.out.untar.map({meta, ref -> ref}) - ch_versions = ch_versions.mix(SPACERANGER_DOWNLOAD_REFERENCE.out.versions) + ch_reference = SPACERANGER_UNTAR_REFERENCE.out.untar.map({meta, ref -> ref}) + ch_versions = ch_versions.mix(SPACERANGER_UNTAR_REFERENCE.out.versions) + } else { + ch_reference = file ( params.spaceranger_reference, type: "dir", checkIfExists: true ) } // From 5c70e202622f1f068bf9cc22dbe6a92b12b9a10e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 12:45:14 +0200 Subject: [PATCH 133/410] Auto detect samplesheet type --- conf/test.config | 1 - nextflow.config | 2 - nextflow_schema.json | 6 -- subworkflows/local/input_check.nf | 103 +++++++------------ tests/pipeline/test_downstream.nf.test | 2 - tests/pipeline/test_sapceranger.nf.test | 5 +- tests/pipeline/test_sapceranger.nf.test.snap | 8 ++ 7 files changed, 47 insertions(+), 80 deletions(-) create mode 100644 tests/pipeline/test_sapceranger.nf.test.snap diff --git a/conf/test.config b/conf/test.config index 9953d36..2e139d6 100644 --- a/conf/test.config +++ b/conf/test.config @@ -22,7 +22,6 @@ params { // Input and output // TODO nf-core: this should be a link pointing to github input = './test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv' - run_spaceranger = true spaceranger_probeset = "./test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" spaceranger_reference = "./test-datasets/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 diff --git a/nextflow.config b/nextflow.config index f00dc05..00eb598 100644 --- a/nextflow.config +++ b/nextflow.config @@ -13,10 +13,8 @@ params { input = null // Spaceranger options - run_spaceranger = false spaceranger_reference = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" spaceranger_probeset = null - spaceranger_image_type = "image" // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 1615de2..f4c735a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -28,12 +28,6 @@ "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", "fa_icon": "fas fa-folder-open" }, - "run_spaceranger": { - "type": "boolean", - "description": "Run Space Ranger on input data", - "help_text": "Use this when your data is raw spatial data that needs to be processed by Space Ranger before downstream analyses can be executed.", - "default": "false" - }, "spaceranger_reference": { "type": "string", "format": "file-path", diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 7c109f1..0fd6a62 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -11,14 +11,11 @@ workflow INPUT_CHECK { main: SAMPLESHEET_CHECK ( samplesheet ) - if ( params.run_spaceranger ) { - st_data = SAMPLESHEET_CHECK.out.csv - .splitCsv ( header: true, sep: ',' ) - .map { create_spaceranger_channels(it) } - } else { - st_data = SAMPLESHEET_CHECK.out.csv - .splitCsv ( header: true, sep: ',' ) - .map { create_visium_channels(it) } + st_data = SAMPLESHEET_CHECK.out.csv.splitCsv( + header: true, + sep: ',' + ).map{ + create_channel(it) } emit: @@ -29,73 +26,49 @@ workflow INPUT_CHECK { // Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] -def create_spaceranger_channels(LinkedHashMap meta) { +def create_channel(LinkedHashMap meta) { meta["id"] = meta.remove("sample") + // Convert a path in `meta` to a file object and return it. If `key` is not contained in `meta` + // return an empty list which is recognized as 'no file' by nextflow. def get_file_from_meta = {key -> v = meta.remove(key); return v ? file(v) : [] } - fastq_dir = meta.remove("fastq_dir") - fastq_files = file("${fastq_dir}/${meta['id']}*.fastq.gz") - manual_alignment = get_file_from_meta("manual_alignment") - slidefile = get_file_from_meta("slidefile") - image = get_file_from_meta("image") - cytaimage = get_file_from_meta("cytaimage") - colorizedimage = get_file_from_meta("colorizedimage") - darkimage = get_file_from_meta("darkimage") - - if(!fastq_files.size()) { - error "No `fastq_dir` specified or no samples found in folder." + if(meta.containsKey("spaceranger_dir")) { + spaceranger_dir = file(meta.remove("spaceranger_dir")) + if(!spaceranger_dir.exists()) { + error "Spaceranger output dir does not exist for sample ${meta['id']}." + } + return [meta, spaceranger_dir] } else { - log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." - } - - check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] - for(k in check_optional_files) { - if(this.binding[k] && !this.binding[k].exists()) { - error "File for `${k}` is specified, but does not exist: ${this.binding[k]}." + fastq_dir = meta.remove("fastq_dir") + fastq_files = file("${fastq_dir}/${meta['id']}*.fastq.gz") + manual_alignment = get_file_from_meta("manual_alignment") + slidefile = get_file_from_meta("slidefile") + image = get_file_from_meta("image") + cytaimage = get_file_from_meta("cytaimage") + colorizedimage = get_file_from_meta("colorizedimage") + darkimage = get_file_from_meta("darkimage") + + if(!fastq_files.size()) { + error "No `fastq_dir` specified or no samples found in folder." + } else { + log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." } - } - if(!(image || cytaimage || colorizedimage || darkimage)) { - error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" - } - return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] -} - -// Function to get list of [ meta, [ tissue_positions_list, tissue_hires_image, \ -// scale_factors, barcodes, features, matrix ] ] -def create_visium_channels(LinkedHashMap row) { - def meta = [:] - meta.id = row.sample - - files_to_check = [ - "tissue_positions_list", - "tissue_lowres_image", - "tissue_hires_image", - "scale_factors", - "barcodes", - "features", - "matrix" - ] - def processed_meta = [] - for (entry in row) { - if (entry.key in files_to_check && !file(entry.value).exists()) { - exit 1, "ERROR: Please check input samplesheet -> ${entry.key} file does not exist!\n${entry.value}" + check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] + for(k in check_optional_files) { + if(this.binding[k] && !this.binding[k].exists()) { + error "File for `${k}` is specified, but does not exist: ${this.binding[k]}." + } + } + if(!(image || cytaimage || colorizedimage || darkimage)) { + error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" } - } - processed_meta = [ - meta, - file(row.tissue_positions_list), - file(row.tissue_lowres_image), - file(row.tissue_hires_image), - file(row.scale_factors), - file(row.barcodes), - file(row.features), - file(row.matrix) - ] - return processed_meta + return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] + } } + diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 045fb4d..f177b92 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -7,9 +7,7 @@ nextflow_pipeline { when { params { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv' - run_spaceranger = false spaceranger_probeset = null - spaceranger_image_type = null st_preprocess_min_counts = 500 st_preprocess_min_genes = 250 outdir = "$outputDir" diff --git a/tests/pipeline/test_sapceranger.nf.test b/tests/pipeline/test_sapceranger.nf.test index ac0d388..d336d93 100644 --- a/tests/pipeline/test_sapceranger.nf.test +++ b/tests/pipeline/test_sapceranger.nf.test @@ -30,13 +30,10 @@ nextflow_pipeline { test("spaceranger ffpe v1") { when { params { - // TODO nf-core: this should be a link pointing to github - input = './assets/samplesheet_spaceranger_ffpe_v1.csv' - run_spaceranger = true + input = 'test-datasets/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' spaceranger_probeset = null st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 - spaceranger_image_type = "image" outdir = "$outputDir" } } diff --git a/tests/pipeline/test_sapceranger.nf.test.snap b/tests/pipeline/test_sapceranger.nf.test.snap new file mode 100644 index 0000000..d7e79f5 --- /dev/null +++ b/tests/pipeline/test_sapceranger.nf.test.snap @@ -0,0 +1,8 @@ +{ + "software_versions": { + "content": [ + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.8.3}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + ], + "timestamp": "2023-06-16T10:39:00+0000" + } +} \ No newline at end of file From e30bd23f2f584a5e1a17128064f5c497de19120a Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 13:03:29 +0200 Subject: [PATCH 134/410] Implement branching logic for detecting downstream samplesheet --- modules/local/st_read_data.nf | 13 ++--- subworkflows/local/input_check.nf | 75 +++++++++++++++-------------- workflows/spatialtranscriptomics.nf | 27 +++-------- 3 files changed, 48 insertions(+), 67 deletions(-) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index 3d20e1d..704412f 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -12,14 +12,7 @@ process ST_READ_DATA { 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" input: - tuple val (meta), - path (tissue_positions_list, stageAs: "SRCount/spatial/tissue_positions_list.csv"), - path (tissue_lowres_image , stageAs: "SRCount/spatial/tissue_lowres_image.png"), - path (tissue_hires_image , stageAs: "SRCount/spatial/tissue_hires_image.png"), - path (scale_factors , stageAs: "SRCount/spatial/scalefactors_json.json"), - path (barcodes , stageAs: "SRCount/raw_feature_bc_matrix/barcodes.tsv.gz"), - path (features , stageAs: "SRCount/raw_feature_bc_matrix/features.tsv.gz"), - path (matrix , stageAs: "SRCount/raw_feature_bc_matrix/matrix.mtx.gz") + tuple val (meta), path("${meta.id}") output: tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw @@ -30,8 +23,8 @@ process ST_READ_DATA { script: """ - read_st_data.py \ - --SRCountDir ./SRCount \ + read_st_data.py \\ + --SRCountDir "${meta.id}" \\ --outAnnData st_adata_raw.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 0fd6a62..33b92a9 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -11,22 +11,33 @@ workflow INPUT_CHECK { main: SAMPLESHEET_CHECK ( samplesheet ) - st_data = SAMPLESHEET_CHECK.out.csv.splitCsv( + ch_st = SAMPLESHEET_CHECK.out.csv.splitCsv( header: true, sep: ',' - ).map{ - create_channel(it) + ).branch { + spaceranger: !it.containsKey("spaceranger_dir") + downstream: it.containsKey("spaceranger_dir") } + ch_spaceranger_input = ch_st.spaceranger.map(create_channel_spaceranger) + ch_downstream_input = ch_st.downstream.map(create_channel_downstream) emit: - st_data // channel: [ val(meta), [ st data ] ] + ch_spaceranger_input // channel: [ val(meta), [ st data ] ] + ch_downstream_input // channel: [ val(meta), [ st data ] ] versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } - +def create_channel_downstream(LinkedHashMap meta) { + meta["id"] = meta.remove("sample") + spaceranger_dir = file(meta.remove("spaceranger_dir")) + if(!spaceranger_dir.exists()) { + error "Spaceranger output dir does not exist for sample ${meta['id']}." + } + return [meta, spaceranger_dir] +} // Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] -def create_channel(LinkedHashMap meta) { +def create_channel_spaceranger(LinkedHashMap meta) { meta["id"] = meta.remove("sample") // Convert a path in `meta` to a file object and return it. If `key` is not contained in `meta` @@ -36,39 +47,31 @@ def create_channel(LinkedHashMap meta) { return v ? file(v) : [] } - if(meta.containsKey("spaceranger_dir")) { - spaceranger_dir = file(meta.remove("spaceranger_dir")) - if(!spaceranger_dir.exists()) { - error "Spaceranger output dir does not exist for sample ${meta['id']}." - } - return [meta, spaceranger_dir] - } else { - fastq_dir = meta.remove("fastq_dir") - fastq_files = file("${fastq_dir}/${meta['id']}*.fastq.gz") - manual_alignment = get_file_from_meta("manual_alignment") - slidefile = get_file_from_meta("slidefile") - image = get_file_from_meta("image") - cytaimage = get_file_from_meta("cytaimage") - colorizedimage = get_file_from_meta("colorizedimage") - darkimage = get_file_from_meta("darkimage") + fastq_dir = meta.remove("fastq_dir") + fastq_files = file("${fastq_dir}/${meta['id']}*.fastq.gz") + manual_alignment = get_file_from_meta("manual_alignment") + slidefile = get_file_from_meta("slidefile") + image = get_file_from_meta("image") + cytaimage = get_file_from_meta("cytaimage") + colorizedimage = get_file_from_meta("colorizedimage") + darkimage = get_file_from_meta("darkimage") - if(!fastq_files.size()) { - error "No `fastq_dir` specified or no samples found in folder." - } else { - log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." - } + if(!fastq_files.size()) { + error "No `fastq_dir` specified or no samples found in folder." + } else { + log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." + } - check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] - for(k in check_optional_files) { - if(this.binding[k] && !this.binding[k].exists()) { - error "File for `${k}` is specified, but does not exist: ${this.binding[k]}." - } - } - if(!(image || cytaimage || colorizedimage || darkimage)) { - error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" + check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] + for(k in check_optional_files) { + if(this.binding[k] && !this.binding[k].exists()) { + error "File for `${k}` is specified, but does not exist: ${this.binding[k]}." } - - return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] } + if(!(image || cytaimage || colorizedimage || darkimage)) { + error "Need to specify at least one of 'image', 'cytaimage', 'colorizedimage', or 'darkimage' in the samplesheet" + } + + return [meta, fastq_files, image, cytaimage, darkimage, colorizedimage, manual_alignment, slidefile] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 8f885b1..0443ea4 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -77,32 +77,17 @@ workflow ST { // // SUBWORKFLOW: Space Ranger raw data processing // - if ( params.run_spaceranger ) { - SPACERANGER ( - INPUT_CHECK.out.st_data - ) - ch_st_data = SPACERANGER.out.sr_dir.map { - meta, out -> [ - meta, - out.findAll{ it -> it.name == "tissue_positions.csv"}, - out.findAll{ it -> it.name == "tissue_lowres_image.png"}, - out.findAll{ it -> it.name == "tissue_hires_image.png"}, - out.findAll{ it -> it.name == "scalefactors_json.json"}, - out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/barcodes\.tsv\.gz$/}, - out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/features\.tsv\.gz$/}, - out.findAll{ it -> it ==~ /.*\/raw_feature_bc_matrix\/matrix\.mtx\.gz$/} - ] - } - ch_versions = ch_versions.mix(SPACERANGER.out.versions) - } else { - ch_st_data = INPUT_CHECK.out.st_data - } + SPACERANGER ( + INPUT_CHECK.out.ch_spaceranger_input + ) + ch_versions = ch_versions.mix(SPACERANGER.out.versions) + ch_downstream_input = INPUT_CHECK.out.ch_downstream_input.concat(SPACERANGER.out.outs) // // MODULE: Read ST data and save as `anndata` // ST_READ_DATA ( - ch_st_data + ch_downstream_input ) ch_versions = ch_versions.mix(ST_READ_DATA.out.versions) From 09a8df14fee89843fb421d5bc6d49822965528ae Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 16 Jun 2023 13:42:50 +0200 Subject: [PATCH 135/410] Fix file handling for downstream analysis --- bin/read_st_data.py | 38 ++++++++++------------------- modules/local/samplesheet_check.nf | 2 -- modules/local/st_read_data.nf | 2 +- subworkflows/local/input_check.nf | 6 ++--- subworkflows/local/spaceranger.nf | 1 - workflows/spatialtranscriptomics.nf | 11 ++++++++- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index e5b4746..f9fba30 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -7,19 +7,16 @@ from anndata import AnnData from matplotlib.image import imread from pathlib import Path -from scanpy import read_10x_mtx +from scanpy import read_10x_h5 from typing import Union, Optional # Function to read MTX def read_visium_mtx( path: Union[str, Path], - genome: Optional[str] = None, *, - count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str = None, - load_images: Optional[bool] = True, - source_image_path: Optional[Union[str, Path]] = None, + load_images: bool = True, + library_id: Optional[str] = None, ) -> AnnData: """\ Read 10x-Genomics-formatted visum dataset. @@ -32,17 +29,12 @@ def read_visium_mtx( Parameters ---------- path - Path to directory for visium datafiles. - genome - Filter expression to genes within this genome. - count_file - Which file in the passed directory to use as the count file. Typically would be one of: - 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. + Path to a spaceranger output directory + load_images: + Whether or not to load images library_id Identifier for the visium library. Can be modified when concatenating multiple adata objects. - source_image_path - Path to the high-resolution tissue image. Path will be included in - `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. + Returns ------- Annotated data matrix, where observations/cells are named by their @@ -70,7 +62,7 @@ def read_visium_mtx( """ path = Path(path) - adata = read_10x_mtx(path / "raw_feature_bc_matrix") + adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") adata.uns["spatial"] = dict() @@ -81,10 +73,10 @@ def read_visium_mtx( if load_images: files = dict( - tissue_positions_file=path / "spatial/tissue_positions_list.csv", - scalefactors_json_file=path / "spatial/scalefactors_json.json", - hires_image=path / "spatial/tissue_hires_image.png", - lowres_image=path / "spatial/tissue_lowres_image.png", + tissue_positions_file=path / "tissue_positions.csv", + scalefactors_json_file=path / "scalefactors_json.json", + hires_image=path / "tissue_hires_image.png", + lowres_image=path / "tissue_lowres_image.png", ) # Check if files exist; continue if images are missing @@ -117,10 +109,6 @@ def read_visium_mtx( inplace=True, ) - # Put absolute image path in uns - if source_image_path is not None: - source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) return adata @@ -134,7 +122,7 @@ def read_visium_mtx( args = parser.parse_args() # Read Visium data - st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True, source_image_path=None) + st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True) # Write raw anndata to file st_adata.write(args.outAnnData) diff --git a/modules/local/samplesheet_check.nf b/modules/local/samplesheet_check.nf index 41a09a6..8014447 100644 --- a/modules/local/samplesheet_check.nf +++ b/modules/local/samplesheet_check.nf @@ -18,12 +18,10 @@ process SAMPLESHEET_CHECK { task.ext.when == null || task.ext.when script: // This script is bundled with the pipeline, in nf-core/spatialtranscriptomics/bin/ - def is_raw_data = params.run_spaceranger ? '--is_raw_data' : '' """ check_samplesheet.py \\ ${samplesheet} \\ samplesheet.valid.csv \\ - ${is_raw_data} cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index 704412f..0e148fc 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -12,7 +12,7 @@ process ST_READ_DATA { 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" input: - tuple val (meta), path("${meta.id}") + tuple val (meta), path("${meta.id}/*") output: tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 33b92a9..b294b4b 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -18,8 +18,8 @@ workflow INPUT_CHECK { spaceranger: !it.containsKey("spaceranger_dir") downstream: it.containsKey("spaceranger_dir") } - ch_spaceranger_input = ch_st.spaceranger.map(create_channel_spaceranger) - ch_downstream_input = ch_st.downstream.map(create_channel_downstream) + ch_spaceranger_input = ch_st.spaceranger.map{create_channel_spaceranger(it)} + ch_downstream_input = ch_st.downstream.map{create_channel_downstream(it)} emit: ch_spaceranger_input // channel: [ val(meta), [ st data ] ] @@ -29,7 +29,7 @@ workflow INPUT_CHECK { def create_channel_downstream(LinkedHashMap meta) { meta["id"] = meta.remove("sample") - spaceranger_dir = file(meta.remove("spaceranger_dir")) + spaceranger_dir = file("${meta.remove("spaceranger_dir")}/*") if(!spaceranger_dir.exists()) { error "Spaceranger output dir does not exist for sample ${meta['id']}." } diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index ce10089..cdff67c 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -52,6 +52,5 @@ workflow SPACERANGER { emit: sr_dir = SPACERANGER_COUNT.out.outs - versions = ch_versions // channel: [ versions.yml ] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 0443ea4..d0f8a3d 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -81,7 +81,16 @@ workflow ST { INPUT_CHECK.out.ch_spaceranger_input ) ch_versions = ch_versions.mix(SPACERANGER.out.versions) - ch_downstream_input = INPUT_CHECK.out.ch_downstream_input.concat(SPACERANGER.out.outs) + required_spaceranger_files = [ + "raw_feature_bc_matrix.h5", + "tissue_positions.csv", + "scalefactors_json.json", + "tissue_hires_image.png", + "tissue_lowres_image.png" + ] + ch_downstream_input = INPUT_CHECK.out.ch_downstream_input.concat(SPACERANGER.out.sr_dir).map{ + meta, outs -> [meta, outs.findAll{ it -> required_spaceranger_files.contains(it.name) }] + } // // MODULE: Read ST data and save as `anndata` From f147b230383d2f13e5af2af1d1aec989a9b14660 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 11:19:56 +0200 Subject: [PATCH 136/410] update config --- conf/modules.config | 5 +++-- conf/test.config | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 7e1a90c..ea36de5 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -39,9 +39,10 @@ process { } withName: SPACERANGER_COUNT { - ext.img_type = { "--${params.spaceranger_image_type}" } publishDir = [ - path: { "${params.outdir}/${meta.id}" }, + // NOTE: the sample name is already excluded in the path that's getting published. + // This publishDir directive puts spaceranger outputs in results/${meta.id}/outs + path: { "${params.outdir}" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] diff --git a/conf/test.config b/conf/test.config index 2e139d6..a0fcc76 100644 --- a/conf/test.config +++ b/conf/test.config @@ -20,7 +20,6 @@ params { max_time = '6.h' // Input and output - // TODO nf-core: this should be a link pointing to github input = './test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv' spaceranger_probeset = "./test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" spaceranger_reference = "./test-datasets/testdata/homo_sapiens_chr22_reference.tar.gz" From 83498293947a6ffdfbabb875e306ba02fbf3e65b Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 11:50:58 +0200 Subject: [PATCH 137/410] Check for downstream input files --- lib/Utils.groovy | 8 ++++++++ subworkflows/local/input_check.nf | 8 +++++--- workflows/spatialtranscriptomics.nf | 9 +-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/Utils.groovy b/lib/Utils.groovy index 8d030f4..9d9d635 100644 --- a/lib/Utils.groovy +++ b/lib/Utils.groovy @@ -6,6 +6,14 @@ import org.yaml.snakeyaml.Yaml class Utils { + public static List DOWNSTREAM_REQUIRED_SPACERANGER_FILES = [ + "raw_feature_bc_matrix.h5", + "tissue_positions.csv", + "scalefactors_json.json", + "tissue_hires_image.png", + "tissue_lowres_image.png" + ] + // // When running with -profile conda, warn if channels have not been set-up appropriately // diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index b294b4b..81678f6 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -29,9 +29,11 @@ workflow INPUT_CHECK { def create_channel_downstream(LinkedHashMap meta) { meta["id"] = meta.remove("sample") - spaceranger_dir = file("${meta.remove("spaceranger_dir")}/*") - if(!spaceranger_dir.exists()) { - error "Spaceranger output dir does not exist for sample ${meta['id']}." + spaceranger_dir = file("${meta.remove('spaceranger_dir')}/**") + for (f in Utils.DOWNSTREAM_REQUIRED_SPACERANGER_FILES) { + if(!spaceranger_dir*.name.contains(f)) { + error "The specified spaceranger output directory doesn't contain the required file `${f}` for sample `${meta.id}`" + } } return [meta, spaceranger_dir] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index d0f8a3d..78dd307 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -81,15 +81,8 @@ workflow ST { INPUT_CHECK.out.ch_spaceranger_input ) ch_versions = ch_versions.mix(SPACERANGER.out.versions) - required_spaceranger_files = [ - "raw_feature_bc_matrix.h5", - "tissue_positions.csv", - "scalefactors_json.json", - "tissue_hires_image.png", - "tissue_lowres_image.png" - ] ch_downstream_input = INPUT_CHECK.out.ch_downstream_input.concat(SPACERANGER.out.sr_dir).map{ - meta, outs -> [meta, outs.findAll{ it -> required_spaceranger_files.contains(it.name) }] + meta, outs -> [meta, outs.findAll{ it -> Utils.DOWNSTREAM_REQUIRED_SPACERANGER_FILES.contains(it.name) }] } // From d64921996c9273ace1f707340600fef93c589db1 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 11:51:28 +0200 Subject: [PATCH 138/410] Implement downstream tests --- tests/pipeline/test_downstream.nf.test | 22 ++++++++++++-------- tests/pipeline/test_downstream.nf.test.snap | 11 +++++----- tests/pipeline/test_sapceranger.nf.test.snap | 4 ++-- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index f177b92..d282b77 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -3,13 +3,11 @@ nextflow_pipeline { script "main.nf" tag "pipeline" - test("Andersson_Nat_Gen_2021_CID4465") { + test("CytAssist_11mm_FFPE_Human_Glioblastoma_2") { when { params { - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv' + input = 'test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' spaceranger_probeset = null - st_preprocess_min_counts = 500 - st_preprocess_min_genes = 250 outdir = "$outputDir" } } @@ -18,13 +16,19 @@ nextflow_pipeline { assertAll( { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + // data + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, + // reports { assert snapshot( - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), ).match("reports")}, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } + // degs + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, ) } } diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index fd326ff..a0eea3c 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,15 +1,16 @@ { "reports": { "content": [ - "st_clustering.html:md5,c0eef3dad596f1489d30a0172871168d", - "st_qc_and_normalisation.html:md5,dc7cbe12e1048f6d913b222e57e0c534" + "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875", + "st_spatial_de.html:md5,8b853559f63cddf6761e908c30b98979" ], - "timestamp": "2023-06-16T07:44:23+0000" + "timestamp": "2023-06-19T09:48:32+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-16T07:44:23+0000" + "timestamp": "2023-06-19T09:48:32+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_sapceranger.nf.test.snap b/tests/pipeline/test_sapceranger.nf.test.snap index d7e79f5..9f85b00 100644 --- a/tests/pipeline/test_sapceranger.nf.test.snap +++ b/tests/pipeline/test_sapceranger.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.8.3}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-16T10:39:00+0000" + "timestamp": "2023-06-19T08:33:42+0000" } } \ No newline at end of file From e8097d031542d27c1deaec678262a2f626c8b6cf Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 11:51:38 +0200 Subject: [PATCH 139/410] Download testdata on CI --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe53a39..75594a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,15 @@ jobs: - name: Check out pipeline code uses: actions/checkout@v3 + - name: Checkout test data + uses: actions/checkout@v3 + with: + # TODO grst: update to nf-core after merge. + repository: grst/test-datasets + ref: spatialtranscriptomics + fetch-depth: 1 + path: test-datasets + # Install Nextflow - name: Install Nextflow uses: nf-core/setup-nextflow@v1 From 754b9ddc1065572e75d8827b5cc59f55c6596d10 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 14:31:21 +0200 Subject: [PATCH 140/410] update spaceranger test cases --- tests/pipeline/test_sapceranger.nf.test | 48 ++++++++++++++++---- tests/pipeline/test_sapceranger.nf.test.snap | 10 +++- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/tests/pipeline/test_sapceranger.nf.test b/tests/pipeline/test_sapceranger.nf.test index d336d93..310351c 100644 --- a/tests/pipeline/test_sapceranger.nf.test +++ b/tests/pipeline/test_sapceranger.nf.test @@ -15,13 +15,27 @@ nextflow_pipeline { assertAll( { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + // data + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, + // reports { assert snapshot( - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), ).match("reports")}, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } + // degs + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, + // spaceranger + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, + { assert snapshot( + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), + )} + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() } ) } } @@ -42,13 +56,27 @@ nextflow_pipeline { assertAll( { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/degs/st_spatial_de.csv").exists() }, + // data + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_norm.h5ad").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_plain.h5ad").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_raw.h5ad").exists() }, + // reports { assert snapshot( - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_clustering.html"), - path("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_qc_and_normalisation.html"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html"), ).match("reports")}, - { assert file("$outputDir/Andersson_Nat_Gen_2021_CID4465_subsampled/reports/st_spatial_de.html").exists() } + // degs + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, + // spaceranger + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/web_summary.html").exists() }, + { assert snapshot( + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), + )} + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() } ) } } diff --git a/tests/pipeline/test_sapceranger.nf.test.snap b/tests/pipeline/test_sapceranger.nf.test.snap index 9f85b00..c81fa2f 100644 --- a/tests/pipeline/test_sapceranger.nf.test.snap +++ b/tests/pipeline/test_sapceranger.nf.test.snap @@ -1,8 +1,16 @@ { + "reports": { + "content": [ + "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875", + "st_spatial_de.html:md5,d5e508c8c5f9d8ff21c0c424feae9821" + ], + "timestamp": "2023-06-19T12:13:32+0000" + }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T08:33:42+0000" + "timestamp": "2023-06-19T12:13:32+0000" } } \ No newline at end of file From f8e4f89c19361b9ad2e0edd8ee9d0cf249627067 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 16:56:20 +0200 Subject: [PATCH 141/410] Update test cases --- .github/workflows/ci.yml | 6 ++- ....test => test_sapceranger_ffpe_v1.nf.test} | 42 +------------------ .../test_sapceranger_ffpe_v1.nf.test.snap | 16 +++++++ ...test_sapceranger_ffpe_v2_cytassist.nf.test | 42 +++++++++++++++++++ ...apceranger_ffpe_v2_cytassist.nf.test.snap} | 6 +-- 5 files changed, 68 insertions(+), 44 deletions(-) rename tests/pipeline/{test_sapceranger.nf.test => test_sapceranger_ffpe_v1.nf.test} (50%) create mode 100644 tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap create mode 100644 tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test rename tests/pipeline/{test_sapceranger.nf.test.snap => test_sapceranger_ffpe_v2_cytassist.nf.test.snap} (81%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75594a3..7599d14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,10 @@ jobs: fail-fast: false matrix: NXF_VER: ${{ fromJson(needs.define_nxf_versions.outputs.matrix) }} + test: + - tests/pipeline/test_spaceranger_ffpe_v1.nf.test + - tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test + - tests/pipeline/test_downstream.nf.test steps: - name: Check out pipeline code uses: actions/checkout@v3 @@ -70,7 +74,7 @@ jobs: # Run nf-test - name: Run nf-test - run: nf-test test tests/pipeline/ --profile=test,docker --tap=test.tap + run: nf-test test tests/pipeline/ --profile=test,docker --tap=test.tap ${{ matrix.test }} # If the test fails, output the software_versions.yml using the 'batcat' utility - name: Output log on failure diff --git a/tests/pipeline/test_sapceranger.nf.test b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test similarity index 50% rename from tests/pipeline/test_sapceranger.nf.test rename to tests/pipeline/test_sapceranger_ffpe_v1.nf.test index 310351c..c4deb44 100644 --- a/tests/pipeline/test_sapceranger.nf.test +++ b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test @@ -3,49 +3,11 @@ nextflow_pipeline { script "main.nf" tag "pipeline" - test("spaceranger ffpe v2 cytassist (default `-profile test`)") { - when { - params { - // This is the default `test` profile, no need to specify additional parameters - outdir = "$outputDir" - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - // data - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, - // reports - { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), - ).match("reports")}, - // degs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, - // spaceranger - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, - { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), - )} - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() } - ) - } - } - - test("spaceranger ffpe v1") { when { params { input = 'test-datasets/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' - spaceranger_probeset = null + spaceranger_probeset = 'test-datasets/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 outdir = "$outputDir" @@ -74,7 +36,7 @@ nextflow_pipeline { { assert snapshot( path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), - )} + )}, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() } ) diff --git a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap new file mode 100644 index 0000000..1b25a45 --- /dev/null +++ b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap @@ -0,0 +1,16 @@ +{ + "reports": { + "content": [ + "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", + "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956", + "st_spatial_de.html:md5,18ec13076780d1e327fca567a8183808" + ], + "timestamp": "2023-06-19T14:54:24+0000" + }, + "software_versions": { + "content": [ + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + ], + "timestamp": "2023-06-19T14:54:24+0000" + } +} \ No newline at end of file diff --git a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test new file mode 100644 index 0000000..6f286b4 --- /dev/null +++ b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test @@ -0,0 +1,42 @@ +nextflow_pipeline { + name "Test full workflow including spaceranger" + script "main.nf" + tag "pipeline" + + test("spaceranger ffpe v2 cytassist (default `-profile test`)") { + when { + params { + // This is the default `test` profile, no need to specify additional parameters + outdir = "$outputDir" + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + // data + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, + // reports + { assert snapshot( + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), + ).match("reports")}, + // degs + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, + // spaceranger + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, + { assert snapshot( + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), + )}, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() } + ) + } + } +} diff --git a/tests/pipeline/test_sapceranger.nf.test.snap b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test.snap similarity index 81% rename from tests/pipeline/test_sapceranger.nf.test.snap rename to tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test.snap index c81fa2f..5a41f04 100644 --- a/tests/pipeline/test_sapceranger.nf.test.snap +++ b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test.snap @@ -3,14 +3,14 @@ "content": [ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875", - "st_spatial_de.html:md5,d5e508c8c5f9d8ff21c0c424feae9821" + "st_spatial_de.html:md5,25ea9c3ac7148caf3f24661fe437e1d8" ], - "timestamp": "2023-06-19T12:13:32+0000" + "timestamp": "2023-06-19T14:54:45+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T12:13:32+0000" + "timestamp": "2023-06-19T14:54:45+0000" } } \ No newline at end of file From 77f4870679f860320a5ac11cb0becc47b0962fb6 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 17:01:45 +0200 Subject: [PATCH 142/410] fix black --- bin/check_samplesheet.py | 1 + bin/read_st_data.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py index 1704cef..a3f381e 100755 --- a/bin/check_samplesheet.py +++ b/bin/check_samplesheet.py @@ -272,6 +272,7 @@ def check_samplesheet(file_in, file_out, is_raw_data): # TODO nf-core: re-enable validation import shutil + shutil.copy(file_in, file_out) return # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. diff --git a/bin/read_st_data.py b/bin/read_st_data.py index f9fba30..5d36843 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -101,7 +101,7 @@ def read_visium_mtx( adata.uns["spatial"][library_id]["metadata"] = {k: "NA" for k in ("chemistry_description", "software_version")} # Read coordinates - positions = pd.read_csv(files["tissue_positions_file"], index_col="barcode", dtype={'in_tissue': bool}) + positions = pd.read_csv(files["tissue_positions_file"], index_col="barcode", dtype={"in_tissue": bool}) adata.obs = adata.obs.join(positions, how="left") adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() adata.obs.drop( @@ -109,12 +109,14 @@ def read_visium_mtx( inplace=True, ) - return adata + if __name__ == "__main__": # Parse command-line arguments - parser = argparse.ArgumentParser(description="Load spatial transcriptomics data from MTX matrices and aligned images.") + parser = argparse.ArgumentParser( + description="Load spatial transcriptomics data from MTX matrices and aligned images." + ) parser.add_argument( "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." ) From 9b39c19c7b3f59454063c0b4dd8c843dcd1c229b Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 17:04:38 +0200 Subject: [PATCH 143/410] fix editorconfig --- tests/pipeline/test_sapceranger_ffpe_v1.nf.test | 4 ++-- tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test index c4deb44..2febabf 100644 --- a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_sapceranger_ffpe_v1.nf.test @@ -34,8 +34,8 @@ nextflow_pipeline { // spaceranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/web_summary.html").exists() }, { assert snapshot( - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), )}, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() } diff --git a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test index 6f286b4..04289ee 100644 --- a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test @@ -31,8 +31,8 @@ nextflow_pipeline { // spaceranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), )}, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() } From aa00f71485fb4be758e4c528bea58714db032e89 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 17:05:49 +0200 Subject: [PATCH 144/410] Fix CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7599d14..e5ff327 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: # Run nf-test - name: Run nf-test - run: nf-test test tests/pipeline/ --profile=test,docker --tap=test.tap ${{ matrix.test }} + run: nf-test test --profile=test,docker --tap=test.tap ${{ matrix.test }} # If the test fails, output the software_versions.yml using the 'batcat' utility - name: Output log on failure From 393f2907a14f34af9e928aec669cb3820bec9a27 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 17:20:57 +0200 Subject: [PATCH 145/410] Fix filename typo --- tests/pipeline/test_downstream.nf.test | 2 +- ...eranger_ffpe_v1.nf.test => test_spaceranger_ffpe_v1.nf.test} | 2 +- ...pe_v1.nf.test.snap => test_spaceranger_ffpe_v1.nf.test.snap} | 0 ...ssist.nf.test => test_spaceranger_ffpe_v2_cytassist.nf.test} | 2 +- ...est.snap => test_spaceranger_ffpe_v2_cytassist.nf.test.snap} | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename tests/pipeline/{test_sapceranger_ffpe_v1.nf.test => test_spaceranger_ffpe_v1.nf.test} (95%) rename tests/pipeline/{test_sapceranger_ffpe_v1.nf.test.snap => test_spaceranger_ffpe_v1.nf.test.snap} (100%) rename tests/pipeline/{test_sapceranger_ffpe_v2_cytassist.nf.test => test_spaceranger_ffpe_v2_cytassist.nf.test} (94%) rename tests/pipeline/{test_sapceranger_ffpe_v2_cytassist.nf.test.snap => test_spaceranger_ffpe_v2_cytassist.nf.test.snap} (100%) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index d282b77..ff2a580 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -25,8 +25,8 @@ nextflow_pipeline { { assert snapshot( path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), ).match("reports")}, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").exists() }, // degs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, ) diff --git a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test similarity index 95% rename from tests/pipeline/test_sapceranger_ffpe_v1.nf.test rename to tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 2febabf..a6d11a0 100644 --- a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -27,8 +27,8 @@ nextflow_pipeline { { assert snapshot( path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html"), path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html"), - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html"), ).match("reports")}, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").exists() }, // degs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, // spaceranger diff --git a/tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap similarity index 100% rename from tests/pipeline/test_sapceranger_ffpe_v1.nf.test.snap rename to tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap diff --git a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test similarity index 94% rename from tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test rename to tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 04289ee..482c61e 100644 --- a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -24,8 +24,8 @@ nextflow_pipeline { { assert snapshot( path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html"), ).match("reports")}, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").exists() }, // degs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, // spaceranger diff --git a/tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap similarity index 100% rename from tests/pipeline/test_sapceranger_ffpe_v2_cytassist.nf.test.snap rename to tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap From 1a97ec988fd547dcbf54d616c9907550fb21aa0c Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Mon, 19 Jun 2023 17:36:03 +0200 Subject: [PATCH 146/410] Remove unstable checksum --- tests/pipeline/test_downstream.nf.test.snap | 7 +++---- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 7 +++---- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index a0eea3c..13c1748 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -2,15 +2,14 @@ "reports": { "content": [ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875", - "st_spatial_de.html:md5,8b853559f63cddf6761e908c30b98979" + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-19T09:48:32+0000" + "timestamp": "2023-06-19T15:30:06+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T09:48:32+0000" + "timestamp": "2023-06-19T15:30:06+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 1b25a45..84b2ec0 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -2,15 +2,14 @@ "reports": { "content": [ "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", - "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956", - "st_spatial_de.html:md5,18ec13076780d1e327fca567a8183808" + "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" ], - "timestamp": "2023-06-19T14:54:24+0000" + "timestamp": "2023-06-19T15:32:05+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T14:54:24+0000" + "timestamp": "2023-06-19T15:32:05+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 5a41f04..42230d5 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -2,15 +2,14 @@ "reports": { "content": [ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875", - "st_spatial_de.html:md5,25ea9c3ac7148caf3f24661fe437e1d8" + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-19T14:54:45+0000" + "timestamp": "2023-06-19T15:34:56+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T14:54:45+0000" + "timestamp": "2023-06-19T15:34:56+0000" } } \ No newline at end of file From b29db0c6595b5effb0d18b7e43ac92259aea9c7e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 20 Jun 2023 08:54:20 +0200 Subject: [PATCH 147/410] Adapt resource requirements for github actions --- conf/test.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/test.config b/conf/test.config index a0fcc76..0a91e5c 100644 --- a/conf/test.config +++ b/conf/test.config @@ -16,8 +16,8 @@ params { // Limit resources so that this can run on GitHub Actions max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' + max_memory = '3.GB' + max_time = '2.h' // Input and output input = './test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv' From 312e1ef1ca12c6495493286510ce3bb68b153b88 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 20 Jun 2023 10:32:11 +0200 Subject: [PATCH 148/410] Use gene ids as var index for they are unique --- bin/read_st_data.py | 3 +++ tests/pipeline/test_downstream.nf.test.snap | 8 ++++---- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 8 ++++---- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 8 ++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 5d36843..5c519ca 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -63,6 +63,9 @@ def read_visium_mtx( path = Path(path) adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") + # use ensemble IDs as index, because they are unique + adata.var["gene_symbol"] = adata.var_names + adata.var.set_index("gene_ids", inplace=True) adata.uns["spatial"] = dict() diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 13c1748..6e3291a 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", + "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" ], - "timestamp": "2023-06-19T15:30:06+0000" + "timestamp": "2023-06-20T07:35:09+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:30:06+0000" + "timestamp": "2023-06-20T07:35:09+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 84b2ec0..98f0b89 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", - "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" + "st_clustering.html:md5,905395042fa8b8a012718c1dbff39db4", + "st_qc_and_normalisation.html:md5,f89922b04edd7299e34ba1e43c257000" ], - "timestamp": "2023-06-19T15:32:05+0000" + "timestamp": "2023-06-20T08:17:32+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:32:05+0000" + "timestamp": "2023-06-20T08:17:32+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 42230d5..526bae5 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", + "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" ], - "timestamp": "2023-06-19T15:34:56+0000" + "timestamp": "2023-06-20T08:18:52+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:34:56+0000" + "timestamp": "2023-06-20T08:18:52+0000" } } \ No newline at end of file From a8908f8d3668e8177905fed279139682ff2e6df4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 21 Jun 2023 14:07:31 +0200 Subject: [PATCH 149/410] Fix version export of `leidenalg` Python module --- modules/local/st_spatial_de.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 0e4cd60..d00a4ad 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -44,7 +44,7 @@ process ST_SPATIAL_DE { cat <<-END_VERSIONS > versions.yml "${task.process}": quarto: \$(quarto -v) - leidenalg: \$(python -c "import leidenalg; print(leidenalg.__version__)") + leidenalg: \$(python -c "import leidenalg; print(leidenalg.version)") scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") SpatialDE: \$(python -c "import SpatialDE; print(SpatialDE.__version__)") END_VERSIONS From 3b0ff94679b5ac0847ad4e1bc13f77920fece7ca Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 21 Jun 2023 14:07:47 +0200 Subject: [PATCH 150/410] Add TODO for `SpatialDE` Python module version --- modules/local/st_spatial_de.nf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index d00a4ad..8d2200c 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -3,6 +3,7 @@ // process ST_SPATIAL_DE { + // TODO nf-core: fix exporting of SpatialDE version, which does not work with `__version__` // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 From 29610b7caf8d176a755ce339726415fa9c66b1b3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 20 Jun 2023 17:20:07 +0200 Subject: [PATCH 151/410] Update CHANGELOG with missing entries/description --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5d3d7..05264c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] -- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] - -## v0.1.0 - 2023-03-31 - -Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. +This marks the point at which the pipeline development was moved to nf-core and +NBIS. The pipeline has undergone several iterations regarding its functionality +and content; there are a significant number of changes, of which not all are +listed here. In summary, the pipeline contains best-practice processing and +analyses of pre- and post-Space Ranger-processed data, including quality +controls, normalisation, dimensionality reduction, clustering, differential +expression testing as well as output files compatible with further downstream +analyses and/or exploration in _e.g._ [TissUUmaps](https://tissuumaps.github.io/) +or bespoke user code. ### `Added` +- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] +- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialtranscriptomics/pull/43)] +- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] +- Use a samplesheet for input specification [[#30](https://github.com/nf-core/spatialtranscriptomics/pull/30), [#31](https://github.com/nf-core/spatialtranscriptomics/pull/31) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] +- Add Space Ranger pre-processing as an optional pipeline step using the `spaceranger` nf-core module [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] +- Add `env/` directory with pipeline-specific container and Conda environment specifications [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#28](https://github.com/nf-core/spatialtranscriptomics/pull/28)] +- Use a more standardised practice to find mitochondrial genes [[#30](https://github.com/nf-core/spatialtranscriptomics/pull/30)] +- Make pipeline output compatible with TissUUmaps [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] +- Add custom Quarto-based reports for all downstream processing [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] + ### `Fixed` +- [#38](https://github.com/nf-core/spatialtranscriptomics/issues/38): Specify manual alignment files in samplesheet +- [#20](https://github.com/nf-core/spatialtranscriptomics/issues/20) and [#22](https://github.com/nf-core/spatialtranscriptomics/issues/22): Add missing Groovy module + ### `Dependencies` -### `Deprecated` +Note, since the pipeline is using Nextflow DSL2, each process will be run +with its own [Biocontainer](https://biocontainers.pro/#/registry). This means +that on occasion it is entirely possible for the pipeline to be using different +versions of the same tool. + +| Dependency | Version | +| ----------- | ------- | +| `SpatialDE` | 1.1.3 | +| `leidenalg` | 0.9.1 | +| `python` | 3.11.0 | +| `quarto` | 1.3.302 | +| `scanpy` | 1.9.3 | + +### `Removed` + +- Streamline pipeline for basic ST data processing; remove SC processing and deconvolution (for now) [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] + +## v0.1.0 - 2023-03-31 + +Initial release of nf-core/spatialtranscriptomics, created with the +[nf-core](https://nf-co.re/) template by the Jackson Laboratory contributors +(see `README.md` for details). From 5c2bdc3344b58cc5270917b3655c3e3b8168659b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 21 Jun 2023 15:49:53 +0200 Subject: [PATCH 152/410] Fix SpatialDE version export --- modules/local/st_spatial_de.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 8d2200c..65bccd3 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -47,7 +47,7 @@ process ST_SPATIAL_DE { quarto: \$(quarto -v) leidenalg: \$(python -c "import leidenalg; print(leidenalg.version)") scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - SpatialDE: \$(python -c "import SpatialDE; print(SpatialDE.__version__)") + SpatialDE: \$(python -c "import SpatialDE; from importlib.metadata import version; print(version('SpatialDE'))") END_VERSIONS """ } From 3cca83c370f8846628e9ab55224d10102f566781 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 21 Jun 2023 15:51:23 +0200 Subject: [PATCH 153/410] Update CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5d3d7..65079ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] - Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] +### `Fixed` + +- [#51](https://github.com/nf-core/spatialtranscriptomics/issues/51): Fix version export of `leidenalg` and `SpatialDE` Python modules + ## v0.1.0 - 2023-03-31 Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. ### `Added` -### `Fixed` - ### `Dependencies` ### `Deprecated` From 82e6d2626a06f7e16d2c29adc40f60aee16bd294 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 21 Jun 2023 18:17:45 +0200 Subject: [PATCH 154/410] Remove redundant import statement Co-authored-by: Gregor Sturm --- modules/local/st_spatial_de.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 65bccd3..ba9c106 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -47,7 +47,7 @@ process ST_SPATIAL_DE { quarto: \$(quarto -v) leidenalg: \$(python -c "import leidenalg; print(leidenalg.version)") scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - SpatialDE: \$(python -c "import SpatialDE; from importlib.metadata import version; print(version('SpatialDE'))") + SpatialDE: \$(python -c "from importlib.metadata import version; print(version('SpatialDE'))") END_VERSIONS """ } From 235fc251bd73bebaa553958904f7086c58d54b10 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 10:48:02 +0200 Subject: [PATCH 155/410] Revert "Use gene ids as var index for they are unique" This reverts commit 312e1ef1ca12c6495493286510ce3bb68b153b88. --- bin/read_st_data.py | 3 --- tests/pipeline/test_downstream.nf.test.snap | 8 ++++---- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 8 ++++---- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 8 ++++---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 5c519ca..5d36843 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -63,9 +63,6 @@ def read_visium_mtx( path = Path(path) adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") - # use ensemble IDs as index, because they are unique - adata.var["gene_symbol"] = adata.var_names - adata.var.set_index("gene_ids", inplace=True) adata.uns["spatial"] = dict() diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 6e3291a..13c1748 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", - "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" + "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-20T07:35:09+0000" + "timestamp": "2023-06-19T15:30:06+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-20T07:35:09+0000" + "timestamp": "2023-06-19T15:30:06+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 98f0b89..84b2ec0 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,905395042fa8b8a012718c1dbff39db4", - "st_qc_and_normalisation.html:md5,f89922b04edd7299e34ba1e43c257000" + "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", + "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" ], - "timestamp": "2023-06-20T08:17:32+0000" + "timestamp": "2023-06-19T15:32:05+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-20T08:17:32+0000" + "timestamp": "2023-06-19T15:32:05+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 526bae5..42230d5 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", - "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" + "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", + "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-20T08:18:52+0000" + "timestamp": "2023-06-19T15:34:56+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-20T08:18:52+0000" + "timestamp": "2023-06-19T15:34:56+0000" } } \ No newline at end of file From a56cdf2b805e205ba60ae2734e1537ef91fbd4ad Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 10:53:42 +0200 Subject: [PATCH 156/410] Remove python samplesheet check --- bin/check_samplesheet.py | 349 ---------------------------- conf/modules.config | 8 - modules/local/samplesheet_check.nf | 31 --- subworkflows/local/input_check.nf | 6 +- workflows/spatialtranscriptomics.nf | 1 - 5 files changed, 1 insertion(+), 394 deletions(-) delete mode 100755 bin/check_samplesheet.py delete mode 100644 modules/local/samplesheet_check.nf diff --git a/bin/check_samplesheet.py b/bin/check_samplesheet.py deleted file mode 100755 index a3f381e..0000000 --- a/bin/check_samplesheet.py +++ /dev/null @@ -1,349 +0,0 @@ -#!/usr/bin/env python - - -"""Provide a command line tool to validate and transform tabular samplesheets.""" - - -import argparse -import csv -import logging -import sys - -# from collections import Counter -from pathlib import Path - -logger = logging.getLogger() - - -class RowChecker: - """ - Define a service that can validate and transform each given row. - - Attributes: - modified (list): A list of dicts, where each dict corresponds to a previously - validated and transformed row. The order of rows is maintained. - - """ - - VALID_FORMATS = (".fq.gz", ".fastq.gz") - - def __init__( - self, - area_col="area", - barcodes_col="barcodes", - fastq_dir_col="fastq_dir", - features_col="features", - hires_image_col="tissue_hires_image", - lowres_image_col="tissue_lowres_image", - matrix_col="matrix", - manual_alignment_col="manual_alignment", - sample_col="sample", - scale_factors_col="scale_factors", - slide_col="slide", - tissue_positions_list_col="tissue_positions_list", - **kwargs, - ): - """ - Initialize the row checker with the expected column names, depending on - whether the input data is raw data (to be processed with Space Ranger) - or processed data. - - Args: - area_col (str): The name of the column that contains the slide area - (default "area"). - barcodes_col (str): The name of the column that contains the - barcodes file path (default "barcodes"). - fastq_dir_col (str): The name of the column that contains the - directory in which the input FASTQ files are stored (default - "fastq_dir"). - features_col (str): The name of the column that contains the - features file path (default "features"). - hires_image_col (str): The name of the column that contains the high - resolution image file path (default "tissue_hires_image"). - lowres_image_col (str): The name of the column that contains the low - resolution image file path (default "tissue_lowres_image"). - matrix_col (str): The name of the column that contains the matrix - matrix file path (default "matrix"). - sample_col (str): The name of the column that contains the sample name - (default "sample"). - scale_factors_col (str): The name of the column that contains the - scale factors file path (default "scale_factors"). - slide_col (str): The name of the column that contains the slide ID - (default "slide"). - tissue_positions_list_col (str): The column that contains the tissue - poositions list file path (default "tissue_positions_list"). - - """ - super().__init__(**kwargs) - self._area_col = area_col - self._barcodes_col = barcodes_col - self._fastq_dir_col = fastq_dir_col - self._features_col = features_col - self._hires_image_col = hires_image_col - self._lowres_image_col = lowres_image_col - self._manual_alignment_col = manual_alignment_col - self._matrix_col = matrix_col - self._sample_col = sample_col - self._scale_factors_col = scale_factors_col - self._slide_col = slide_col - self._tissue_positions_list_col = tissue_positions_list_col - self.modified = [] - - def validate_and_transform(self, row, is_raw_data): - """ - Perform all validations on the given row and insert the read pairing status. - - Args: - row (dict): A mapping from column headers (keys) to elements of that row - (values). - - """ - if is_raw_data: - self._validate_sample(row) - self._validate_fastq_dir(row) - self._validate_hires_image(row) - self._validate_slide(row) - self._validate_area(row) - self._validate_manual_alignment(row) - self.modified.append(row) - else: - self._validate_sample(row) - self._validate_tissue_positions_list(row) - self._validate_lowres_image(row) - self._validate_hires_image(row) - self._validate_scale_factors(row) - self._validate_barcodes(row) - self._validate_features(row) - self._validate_matrix(row) - self.modified.append(row) - - def _validate_sample(self, row): - """Assert that the sample name exists and convert spaces to underscores.""" - if len(row[self._sample_col]) <= 0: - raise AssertionError("Sample input is required.") - # Sanitize samples slightly. - row[self._sample_col] = row[self._sample_col].replace(" ", "_") - - def _validate_fastq_dir(self, row): - """Assert that the FASTQ directory entry is non-empty.""" - # TODO: Possibly add a check to make sure that at least one FASTQ file - # exists in the specified directory - if len(row[self._fastq_dir_col]) <= 0: - raise AssertionError("The directory in which the FASTQ files are stored is required") - - def _validate_hires_image(self, row): - """Assert that the high resolution image entry exists.""" - # TODO: Possibly add a check for image formats - if len(row[self._hires_image_col]) <= 0: - raise AssertionError("The high resolution image is required") - - def _validate_slide(self, row): - """Assert that the slide entry exists.""" - # TODO: Possibly add a check for valid slide IDs - if len(row[self._slide_col]) <= 0: - raise AssertionError("The slide ID is required") - - def _validate_area(self, row): - """Assert that the slide area exists.""" - # TODO: Possibly add a check for valid area specifications - if len(row[self._area_col]) <= 0: - raise AssertionError("The area is required") - - def _validate_manual_alignment(self, row): - """Assert that the manual alignment entry has the right format if it exists.""" - return - - def _validate_tissue_positions_list(self, row): - """Assert that the tissue positions list entry exists.""" - # TODO: Add a CSV file check - if len(row[self._hires_image_col]) <= 0: - raise AssertionError("The high resolution image is required") - - def _validate_lowres_image(self, row): - """Assert that the low resolution image entry exists.""" - # TODO: Possibly add a check for image formats - if len(row[self._lowres_image_col]) <= 0: - raise AssertionError("The low resolution image is required") - - def _validate_scale_factors(self, row): - """Assert that the scale factors entry exists.""" - # TODO: Possibly add a JSON format check - if len(row[self._scale_factors_col]) <= 0: - raise AssertionError("The scale factors file is required") - - def _validate_barcodes(self, row): - """Assert that the barcodes entry exists.""" - # TODO: Possibly add a check for TSV file formats - if len(row[self._barcodes_col]) <= 0: - raise AssertionError("The barcodes file is required") - - def _validate_features(self, row): - """Assert that the features entry exists.""" - # TODO: Possibly add a check for TSV file formats - if len(row[self._features_col]) <= 0: - raise AssertionError("The features file is required") - - def _validate_matrix(self, row): - """Assert that the matrix entry exists.""" - # TODO: Possibly add a check for MTX file formats - if len(row[self._matrix_col]) <= 0: - raise AssertionError("The matrix file is required") - - -def read_head(handle, num_lines=10): - """Read the specified number of lines from the current position in the file.""" - lines = [] - for idx, line in enumerate(handle): - if idx == num_lines: - break - lines.append(line) - return "".join(lines) - - -def sniff_format(handle): - """ - Detect the tabular format. - - Args: - handle (text file): A handle to a `text file`_ object. The read position is - expected to be at the beginning (index 0). - - Returns: - csv.Dialect: The detected tabular format. - - .. _text file: - https://docs.python.org/3/glossary.html#term-text-file - - """ - peek = read_head(handle) - handle.seek(0) - sniffer = csv.Sniffer() - dialect = sniffer.sniff(peek) - return dialect - - -def check_samplesheet(file_in, file_out, is_raw_data): - """ - Check that the tabular samplesheet has the structure expected by nf-core pipelines. - - Validate the general shape of the table, expected columns, and each row. Also add - an additional column which records whether one or two FASTQ reads were found. - - Args: - file_in (pathlib.Path): The given tabular samplesheet. The format can be either - CSV, TSV, or any other format automatically recognized by ``csv.Sniffer``. - file_out (pathlib.Path): Where the validated and transformed samplesheet should - be created; always in CSV format. - is_raw_data (boolean): Whether the data is raw data to be processed by - Space Ranger or not. - - Example (processed data): - This function checks that the samplesheet follows the following - structure by default, see also the `spatial transcriptomics - samplesheet`_:: - - sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features,matrix - SAMPLE,TISSUE_POSITIONS_LIST.csv,TISSUE_LOWRES_IMAGE.png,tissue_hires_image.png,SCALEFACTORS_JSON.json,BARCODES.tsv.gz,FEATURES.tsv.gz,MATRIX.mtx.gz - - .. _spatial transcriptomics samplesheet: - https://data.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset-subsampled/samplesheet.csv - - Example (raw data): - This function check that the samplesheet follows the following structure - if the `--is_raw_data` parameter is set: - - sample,fastq_dir,tissue_hires_image,slide,area - SAMPLE,FASTQ_DIRECTORY,TISSUE_HIRES_IMAGE.png,SLIDE_ID,SLIDE_AREA - - """ - if is_raw_data: - required_columns = {"sample", "fastq_dir", "tissue_hires_image", "slide", "area", "manual_alignment"} - else: - required_columns = { - "sample", - "tissue_positions_list", - "tissue_lowres_image", - "tissue_hires_image", - "scale_factors", - "barcodes", - "features", - "matrix", - } - - # TODO nf-core: re-enable validation - import shutil - - shutil.copy(file_in, file_out) - return - # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. - with file_in.open(newline="") as in_handle: - reader = csv.DictReader(in_handle, dialect=sniff_format(in_handle)) - # Validate the existence of the expected header columns. - # if not required_columns.issubset(reader.fieldnames): - # req_cols = ", ".join(required_columns) - # logger.critical(f"The sample sheet **must** contain these column headers: {req_cols}.") - # sys.exit(1) - # # Validate each row. - # checker = RowChecker() - # for i, row in enumerate(reader): - # try: - # checker.validate_and_transform(row, is_raw_data) - # except AssertionError as error: - # logger.critical(f"{str(error)} On line {i + 2}.") - # sys.exit(1) - header = list(reader.fieldnames) - # See https://docs.python.org/3.9/library/csv.html#id3 to read up on `newline=""`. - with file_out.open(mode="w", newline="") as out_handle: - writer = csv.DictWriter(out_handle, header, delimiter=",") - writer.writeheader() - for row in checker.modified: - writer.writerow(row) - - -def parse_args(argv=None): - """Define and immediately parse command line arguments.""" - parser = argparse.ArgumentParser( - description="Validate and transform a tabular samplesheet.", - epilog="Example: python check_samplesheet.py samplesheet.csv samplesheet.valid.csv", - ) - parser.add_argument( - "file_in", - metavar="FILE_IN", - type=Path, - help="Tabular input samplesheet in CSV or TSV format.", - ) - parser.add_argument( - "file_out", - metavar="FILE_OUT", - type=Path, - help="Transformed output samplesheet in CSV format.", - ) - parser.add_argument( - "-r", - "--is_raw_data", - action="store_true", - help="Transformed output samplesheet in CSV format.", - ) - parser.add_argument( - "-l", - "--log-level", - help="The desired log level (default WARNING).", - choices=("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"), - default="WARNING", - ) - return parser.parse_args(argv) - - -def main(argv=None): - """Coordinate argument parsing and program execution.""" - args = parse_args(argv) - logging.basicConfig(level=args.log_level, format="[%(levelname)s] %(message)s") - if not args.file_in.is_file(): - logger.error(f"The given input file {args.file_in} was not found!") - sys.exit(2) - args.file_out.parent.mkdir(parents=True, exist_ok=True) - check_samplesheet(args.file_in, args.file_out, args.is_raw_data) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/conf/modules.config b/conf/modules.config index ea36de5..65b2408 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -22,14 +22,6 @@ process { saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] - withName: SAMPLESHEET_CHECK { - publishDir = [ - path: { "${params.outdir}/pipeline_info" }, - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } - ] - } - withName: CUSTOM_DUMPSOFTWAREVERSIONS { publishDir = [ path: { "${params.outdir}/pipeline_info" }, diff --git a/modules/local/samplesheet_check.nf b/modules/local/samplesheet_check.nf deleted file mode 100644 index b7bc750..0000000 --- a/modules/local/samplesheet_check.nf +++ /dev/null @@ -1,31 +0,0 @@ -process SAMPLESHEET_CHECK { - tag "$samplesheet" - label 'process_single' - - conda "conda-forge::python=3.9" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/python:3.9' : - 'biocontainers/python:3.9' }" - - input: - path samplesheet - - output: - path '*.csv' , emit: csv - path "versions.yml", emit: versions - - when: - task.ext.when == null || task.ext.when - - script: // This script is bundled with the pipeline, in nf-core/spatialtranscriptomics/bin/ - """ - check_samplesheet.py \\ - ${samplesheet} \\ - samplesheet.valid.csv \\ - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - python: \$(python --version | sed 's/Python //g') - END_VERSIONS - """ -} diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 81678f6..c714514 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -2,16 +2,13 @@ // Check input samplesheet and get read channels // -include { SAMPLESHEET_CHECK } from '../../modules/local/samplesheet_check' - workflow INPUT_CHECK { take: samplesheet // file: /path/to/samplesheet.csv main: - SAMPLESHEET_CHECK ( samplesheet ) - ch_st = SAMPLESHEET_CHECK.out.csv.splitCsv( + ch_st = Channel.from(samplesheet).splitCsv( header: true, sep: ',' ).branch { @@ -24,7 +21,6 @@ workflow INPUT_CHECK { emit: ch_spaceranger_input // channel: [ val(meta), [ st data ] ] ch_downstream_input // channel: [ val(meta), [ st data ] ] - versions = SAMPLESHEET_CHECK.out.versions // channel: [ versions.yml ] } def create_channel_downstream(LinkedHashMap meta) { diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 78dd307..5a1c609 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -72,7 +72,6 @@ workflow ST { INPUT_CHECK ( ch_input ) - ch_versions = ch_versions.mix(INPUT_CHECK.out.versions) // // SUBWORKFLOW: Space Ranger raw data processing From bab69454b3413781aa5abd192afc8325fd27efa2 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 10:55:16 +0200 Subject: [PATCH 157/410] Use nf-core test dataset version --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5ff327..36ef2ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,8 +54,7 @@ jobs: - name: Checkout test data uses: actions/checkout@v3 with: - # TODO grst: update to nf-core after merge. - repository: grst/test-datasets + repository: nf-core/test-datasets ref: spatialtranscriptomics fetch-depth: 1 path: test-datasets From 2d86390fc73725927b4fc80a421dee15ba30438f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 10:58:50 +0200 Subject: [PATCH 158/410] Update spaceranger module --- modules.json | 2 +- modules/nf-core/spaceranger/count/main.nf | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules.json b/modules.json index addd3c6..a44908f 100644 --- a/modules.json +++ b/modules.json @@ -13,7 +13,7 @@ }, "spaceranger/count": { "branch": "master", - "git_sha": "f874515c9b49c07b68cf1bf06d711fe46d8ab2aa", + "git_sha": "84bd059d24c74c44e31f98924413d2c7f04dd6c6", "installed_by": ["modules"] }, "untar": { diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index 7d9772b..43f3bee 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -2,7 +2,6 @@ process SPACERANGER_COUNT { tag "$meta.id" label 'process_high' - // TODO push to nf-core docker container "docker.io/nfcore/spaceranger:2.1.0" // Exit if running this module with -profile conda / -profile mamba From 61f5f9a24a89be36eabe5fa238eb0dccac6ec4e0 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 12:27:06 +0200 Subject: [PATCH 159/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 6 +++--- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 13c1748..fd47faf 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-19T15:30:06+0000" + "timestamp": "2023-06-22T09:27:49+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:30:06+0000" + "timestamp": "2023-06-22T09:27:49+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 84b2ec0..9f2a204 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" ], - "timestamp": "2023-06-19T15:32:05+0000" + "timestamp": "2023-06-22T09:30:11+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:32:05+0000" + "timestamp": "2023-06-22T09:30:11+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 42230d5..4cf3235 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-19T15:34:56+0000" + "timestamp": "2023-06-22T09:32:47+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SAMPLESHEET_CHECK={python=3.9.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-19T15:34:56+0000" + "timestamp": "2023-06-22T09:32:47+0000" } } \ No newline at end of file From 0ff185ceba25ec0da539bf0bbab3d8d0b976bb61 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 12:59:57 +0200 Subject: [PATCH 160/410] Update usage docs --- docs/usage.md | 105 +++++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 4ecc7cf..5030059 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,77 +7,91 @@ ## Samplesheet input You will need to create a samplesheet with information about the samples you -would like to analyse before running the pipeline. Use this parameter to specify -its location. It has to be a comma-separated file with at least 5 or 8 columns -(depending on input data type, [see below](#raw-spatial-data)), and a header row -as shown in the examples below. +would like to analyse before running the pipeline. It has to be a comma-separated file as described +in the examples below and depends on the input data type. Use this parameter to specify its location. ```bash --input '[path to samplesheet file]' ``` +The workflow will automatically detect the samplesheet type and run the appropriate analysis steps. + ### Raw spatial data -The samplesheet for raw spatial data yet to be analysed with Space Ranger is -specified like so: +This section describes samplesheets for processing *raw spatial data* yet to be analyzed with Space Ranger. + +Here is an example of a typical samplesheet for analyzing FFPE or fresh frozen (FF) data with bright field microscopy +imagery: ```no-highlight -sample,fastq_dir,tissue_hires_image,slide,area,manual_alignment -SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1, -SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1, +sample,fastq_dir,image,slide,area +SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1 +SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 ``` -| Column | Description | -| -------------------- | ---------------------------------------------------------- | -| `sample` | Custom sample name. | -| `fastq_dir` | Path to directory where the sample FASTQ files are stored. | -| `tissue_hires_image` | Path to the high-resolution image for the sample. | -| `slide` | The Visium slide ID used for the sequencing. | -| `area` | Which slide area contains the tissue sample. | -| `manual_alignment` | Path to the manual alignment file (optional) | +For Cytassist samples, the `image` column gets replaced with the `cytaimage` column: + +```no-highlight +sample,fastq_dir,cytaimage,slide,area +SAMPLE_1,fastqs_1/,cytassist_1.tif,V11J26,B1 +SAMPLE_2,fastqs_2/,cytassist_2.tif,V11J26,B1 +``` -> **NB:** The `manual_alignment` column is only required for samples for which a -> manual alignment file is needed and can be ignored if you're using automatic -> alignment. +Depending on the experimental setup, (additional) color composite fluorescence images or dark background +fluorescence images can be supplied using the `colorizedimage` or `darkimage` columns, respectively. + +Please refer to the following table for an overview of all supported columns: + + +| Column | Description | +| ------------------ | ------------------------------------------------------------------------------------------------------------------- | +| `sample` | Unique sample identifier. MUST match the prefix of the fastq files | +| `fastq_dir` | Path to directory where the sample FASTQ files are stored. | +| `image` | Brightfield microscopy image | +| `cytaimage` | Brightfield tissue image captured with Cytassist device | +| `colorizedimage` | A color composite of one or more fluorescence image channels saved as a single-page, single-file color TIFF or JPEG | +| `darkimage` | Dark background fluorescence microscopy image | +| `slide` | The Visium slide ID used for the sequencing. | +| `area` | Which slide area contains the tissue sample. | +| `manual_alignment` | Path to the manual alignment file (optional) | +| `slidefile` | Slide specification as JSON. Overrides `slide` and `area` if specified. (optional) | + +> **NB:** +> * You need to specify *at least one* of `image`, `cytaimage`, `darkimage`, `colorizedimage`. Most commonly, you'll +> specify `image` for bright field microscopy data, or `cytaimage` for tissue scans generated with the 10x Cyatassist +> device. Please refer to the [Space Ranger documentation](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger), how multiple image types can be combined. +> * The `manual_alignment` column is only required for samples for which a +> manual alignment file is needed and can be ignored if you're using automatic +> alignment. +> If you are unsure, please see the Visium documentation for details regarding the different variants of [FASTQ directory structures](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/fastq-input) and [slide parameters](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/slide-info) appropriate for your samples. -> **NB:** You will have to supply the `--run_spaceranger` parameter when you -> execute the pipeline. ### Processed data -If your data has already been processed by Space Ranger the samplesheet will look -like this: +If your data has already been processed by Space Ranger and you are only interested in running downstream QC steps, +the samplesheet looks as follows: ```no-highlight -sample,tissue_positions_list,tissue_lowres_image,tissue_hires_image,scale_factors,barcodes,features, -matrix -SAMPLE_1,tissue_positions_list_1.csv,tissue_lowres_image_1.png,tissue_hires_image_1.png,scale_factors_1.json,barcodes_1.tsv.gz,features_1.tsv.gz,matrix_1.mtx.gz -SAMPLE_2,tissue_positions_list_2.csv,tissue_lowres_image_2.png,tissue_hires_image_2.png,scale_factors_2.json,barcodes_2.tsv.gz,features_2.tsv.gz,matrix_2.mtx.gz +sample,spaceranger_dir +SAMPLE_1,results/SAMPLE_1/outs +SAMPLE_2,results/SAMPLE_2/outs ``` -| Column | Description | -| ----------------------- | ------------------------------------------------------------------ | -| `sample` | Custom sample name. | -| `tissue_positions_list` | Path to the CSV with spot barcodes and their array positions. | -| `tissue_lowres_image` | Path to the low-resolution image for the sample. | -| `tissue_hires_image` | Path to the high-resolution image for the sample. | -| `scale_factors` | Path to the JSON file with scale conversion factors for the spots. | -| `barcodes` | Path to TSV file with barcode IDs. | -| `features` | Path to TSV file with features IDs. | -| `matrix` | Path to MTX file with UMIs, barcodes and features. | +| Column | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Unique sample identifier. | +| `spaceranger_dir` | Output directory generated by spaceranger. This is typically caled `outs` and contains both gene expression matrices and spatial information | -The latter three elements should be taken from the `filtered_feature_bc_matrix/` -directory, _i.e._ only tissue-associated barcodes and their data. -## Space Ranger options +## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with -raw spatial data (`--run_spaceranger`). Space Ranger requieres a lot of memory +raw spatial data. Space Ranger requieres a lot of memory (64 GB) and several threads (8) to be able to run. You can find the Space Ranger documentation at the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). @@ -87,8 +101,11 @@ path to its directory (or another link from the 10X website above) using the `--spaceranger_reference` parameter, otherwise the pipeline will download the default human reference for you automatically. -You may optionally supply file path to a probe sets using the -`--spaceranger_probeset` parameter. +> **Important**: +> +> For FFPE and Cytassist experiments, you need to manually supply the appropriate probset using the `--spaceranger_probeset` parameter +> Please refer to the [Spaceranger Downloads page](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) +> to obtain the correct probeset. ## Analysis options From e33d52546646acbd78d7a796dbaeb0e373cd9891 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 22 Jun 2023 13:02:42 +0200 Subject: [PATCH 161/410] fix prettier --- docs/usage.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 5030059..6b02a13 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -18,7 +18,7 @@ The workflow will automatically detect the samplesheet type and run the appropri ### Raw spatial data -This section describes samplesheets for processing *raw spatial data* yet to be analyzed with Space Ranger. +This section describes samplesheets for processing _raw spatial data_ yet to be analyzed with Space Ranger. Here is an example of a typical samplesheet for analyzing FFPE or fresh frozen (FF) data with bright field microscopy imagery: @@ -42,8 +42,7 @@ fluorescence images can be supplied using the `colorizedimage` or `darkimage` co Please refer to the following table for an overview of all supported columns: - -| Column | Description | +| Column | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | `sample` | Unique sample identifier. MUST match the prefix of the fastq files | | `fastq_dir` | Path to directory where the sample FASTQ files are stored. | @@ -57,20 +56,19 @@ Please refer to the following table for an overview of all supported columns: | `slidefile` | Slide specification as JSON. Overrides `slide` and `area` if specified. (optional) | > **NB:** -> * You need to specify *at least one* of `image`, `cytaimage`, `darkimage`, `colorizedimage`. Most commonly, you'll +> +> - You need to specify _at least one_ of `image`, `cytaimage`, `darkimage`, `colorizedimage`. Most commonly, you'll > specify `image` for bright field microscopy data, or `cytaimage` for tissue scans generated with the 10x Cyatassist > device. Please refer to the [Space Ranger documentation](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger), how multiple image types can be combined. -> * The `manual_alignment` column is only required for samples for which a +> - The `manual_alignment` column is only required for samples for which a > manual alignment file is needed and can be ignored if you're using automatic > alignment. -> If you are unsure, please see the Visium documentation for details regarding the different variants of [FASTQ directory structures](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/fastq-input) and [slide parameters](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/using/slide-info) appropriate for your samples. - ### Processed data If your data has already been processed by Space Ranger and you are only interested in running downstream QC steps, @@ -82,12 +80,11 @@ SAMPLE_1,results/SAMPLE_1/outs SAMPLE_2,results/SAMPLE_2/outs ``` -| Column | Description | +| Column | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `sample` | Unique sample identifier. | | `spaceranger_dir` | Output directory generated by spaceranger. This is typically caled `outs` and contains both gene expression matrices and spatial information | - ## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with @@ -105,7 +102,7 @@ default human reference for you automatically. > > For FFPE and Cytassist experiments, you need to manually supply the appropriate probset using the `--spaceranger_probeset` parameter > Please refer to the [Spaceranger Downloads page](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) -> to obtain the correct probeset. +> to obtain the correct probeset. ## Analysis options From deac82a2edda72c7e13f9a69fa835ba776e441a7 Mon Sep 17 00:00:00 2001 From: Alexander Peltzer Date: Fri, 23 Jun 2023 09:04:50 +0200 Subject: [PATCH 162/410] Update docs/usage.md Co-authored-by: Erik Fasterius --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index 6b02a13..43df9e1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -83,7 +83,7 @@ SAMPLE_2,results/SAMPLE_2/outs | Column | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `sample` | Unique sample identifier. | -| `spaceranger_dir` | Output directory generated by spaceranger. This is typically caled `outs` and contains both gene expression matrices and spatial information | +| `spaceranger_dir` | Output directory generated by spaceranger. This is typically called `outs` and contains both gene expression matrices and spatial information | ## Space Ranger From 6150f38bf0c4e417cf4dc1c154446ab68232c41b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 23 Jun 2023 09:08:57 +0200 Subject: [PATCH 163/410] Fix table formatting --- docs/usage.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 43df9e1..6636a8f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -80,9 +80,9 @@ SAMPLE_1,results/SAMPLE_1/outs SAMPLE_2,results/SAMPLE_2/outs ``` -| Column | Description | -| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `sample` | Unique sample identifier. | +| Column | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Unique sample identifier. | | `spaceranger_dir` | Output directory generated by spaceranger. This is typically called `outs` and contains both gene expression matrices and spatial information | ## Space Ranger From 12ebf6f943a819dd5d871b4e42ea8089dda0e087 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Tue, 20 Jun 2023 10:32:11 +0200 Subject: [PATCH 164/410] Use gene ids as var index for they are unique --- bin/read_st_data.py | 3 +++ tests/pipeline/test_downstream.nf.test.snap | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 6 +++--- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 5d36843..5c519ca 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -63,6 +63,9 @@ def read_visium_mtx( path = Path(path) adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") + # use ensemble IDs as index, because they are unique + adata.var["gene_symbol"] = adata.var_names + adata.var.set_index("gene_ids", inplace=True) adata.uns["spatial"] = dict() diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index fd47faf..d1502b8 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,8 +1,8 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", + "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" ], "timestamp": "2023-06-22T09:27:49+0000" }, @@ -12,4 +12,4 @@ ], "timestamp": "2023-06-22T09:27:49+0000" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 9f2a204..1e960a0 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,8 +1,8 @@ { "reports": { "content": [ - "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", - "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" + "st_clustering.html:md5,905395042fa8b8a012718c1dbff39db4", + "st_qc_and_normalisation.html:md5,f89922b04edd7299e34ba1e43c257000" ], "timestamp": "2023-06-22T09:30:11+0000" }, @@ -12,4 +12,4 @@ ], "timestamp": "2023-06-22T09:30:11+0000" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 4cf3235..68b5554 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,8 +1,8 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", + "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" ], "timestamp": "2023-06-22T09:32:47+0000" }, @@ -12,4 +12,4 @@ ], "timestamp": "2023-06-22T09:32:47+0000" } -} \ No newline at end of file +} From e69aa5177f09bd9cd736bdbb2af13f4e68bbac9e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 10:54:11 +0200 Subject: [PATCH 165/410] Workaround https://github.com/Teichlab/SpatialDE/issues/36 --- bin/st_spatial_de.qmd | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 21f29c0..649fc2d 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -28,7 +28,7 @@ from matplotlib import pyplot as plt ``` ```{python} -st_adata = sc.read("./" + fileNameST) +st_adata = sc.read(fileNameST) st_adata ``` @@ -54,15 +54,16 @@ Spatial transcriptomics allows researchers to investigate how gene expression tr First, we convert normalized counts and coordinates to pandas dataframe, needed for inputs to spatialDE. ```{python} -counts = pd.DataFrame(st_adata.X.todense(), columns=st_adata.var_names, index=st_adata.obs_names) -coord = pd.DataFrame(st_adata.obsm['spatial'], columns=['x_coord', 'y_coord'], index=st_adata.obs_names).to_numpy() -results = SpatialDE.run(coord, counts) +results = SpatialDE.run(st_adata.obsm["spatial"], st_adata.to_df()) ``` We concatenate the results with the DataFrame of annotations of variables: `st_adata.var`. ```{python} -results.index = results["g"] +results.set_index("g", inplace=True) +# workaround for https://github.com/Teichlab/SpatialDE/issues/36 +results = results.loc[~results.index.duplicated(keep="first")] + st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, :]], axis=1) ``` From bac2f8d085031bda541ff5bf2b0173ba7005126e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 11:44:03 +0200 Subject: [PATCH 166/410] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05264c9..09fc6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ or bespoke user code. - [#38](https://github.com/nf-core/spatialtranscriptomics/issues/38): Specify manual alignment files in samplesheet - [#20](https://github.com/nf-core/spatialtranscriptomics/issues/20) and [#22](https://github.com/nf-core/spatialtranscriptomics/issues/22): Add missing Groovy module +- [#53](https://github.com/nf-core/spatialtranscriptomics/pull/53): Use ensemble IDs as index in adata.var and fix related + issue with SpatialDE ### `Dependencies` From 457a50ca4a69f8592bd6d09a2e46579125255f20 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 12:09:47 +0200 Subject: [PATCH 167/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 8 ++++---- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 8 ++++---- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index fd47faf..877772e 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,d4b686cb4c71aa2a24b5ead39c295a76", + "st_qc_and_normalisation.html:md5,5c6683d34030ccfd9c0626ff4b00e8d6" ], - "timestamp": "2023-06-22T09:27:49+0000" + "timestamp": "2023-06-23T09:52:35+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:27:49+0000" + "timestamp": "2023-06-23T09:52:35+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 9f2a204..1acb103 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", - "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" + "st_clustering.html:md5,5acd043d24a729c0df8ac9bc2e6c6b8d", + "st_qc_and_normalisation.html:md5,c3c7355f9c0423d2ca0295145d141c8e" ], - "timestamp": "2023-06-22T09:30:11+0000" + "timestamp": "2023-06-23T09:59:53+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:30:11+0000" + "timestamp": "2023-06-23T09:59:53+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 4cf3235..f882a75 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,15 +1,15 @@ { "reports": { "content": [ - "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", - "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" + "st_clustering.html:md5,d4b686cb4c71aa2a24b5ead39c295a76", + "st_qc_and_normalisation.html:md5,5c6683d34030ccfd9c0626ff4b00e8d6" ], - "timestamp": "2023-06-22T09:32:47+0000" + "timestamp": "2023-06-23T10:09:28+0000" }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:32:47+0000" + "timestamp": "2023-06-23T10:09:28+0000" } } \ No newline at end of file From 82a41e898c7f2fcb886031fe0f10fe96c69551ac Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 12:10:58 +0200 Subject: [PATCH 168/410] Swap X and Y coords --- bin/read_st_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 5d36843..4290161 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -103,7 +103,7 @@ def read_visium_mtx( # Read coordinates positions = pd.read_csv(files["tissue_positions_file"], index_col="barcode", dtype={"in_tissue": bool}) adata.obs = adata.obs.join(positions, how="left") - adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() + adata.obsm["spatial"] = adata.obs[["pxl_col_in_fullres", "pxl_row_in_fullres"]].to_numpy() adata.obs.drop( columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], inplace=True, From ddddf68c93d3abd659a76caebd2bccfd18d75994 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 13:04:52 +0200 Subject: [PATCH 169/410] Remove snapshots as HTML files have now unstable checksums --- tests/pipeline/test_downstream.nf.test | 8 +++----- tests/pipeline/test_downstream.nf.test.snap | 9 +-------- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 8 +++----- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 9 +-------- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 8 +++----- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 9 +-------- 6 files changed, 12 insertions(+), 39 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index ff2a580..fee1dbc 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -22,11 +22,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, // reports - { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), - ).match("reports")}, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, // degs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, ) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 877772e..c3c82b0 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,15 +1,8 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,d4b686cb4c71aa2a24b5ead39c295a76", - "st_qc_and_normalisation.html:md5,5c6683d34030ccfd9c0626ff4b00e8d6" - ], - "timestamp": "2023-06-23T09:52:35+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-23T09:52:35+0000" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index a6d11a0..301d215 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -24,11 +24,9 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_raw.h5ad").exists() }, // reports - { assert snapshot( - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html"), - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html"), - ).match("reports")}, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, // degs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, // spaceranger diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 1acb103..bbd4549 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,15 +1,8 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,5acd043d24a729c0df8ac9bc2e6c6b8d", - "st_qc_and_normalisation.html:md5,c3c7355f9c0423d2ca0295145d141c8e" - ], - "timestamp": "2023-06-23T09:59:53+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-23T09:59:53+0000" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 482c61e..1e22084 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -21,11 +21,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, // reports - { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html"), - ).match("reports")}, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, // degs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, // spaceranger diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index f882a75..bcac9af 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,15 +1,8 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,d4b686cb4c71aa2a24b5ead39c295a76", - "st_qc_and_normalisation.html:md5,5c6683d34030ccfd9c0626ff4b00e8d6" - ], - "timestamp": "2023-06-23T10:09:28+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-23T10:09:28+0000" } -} \ No newline at end of file +} From 10b302cab47fdb21abe8bc7de6404dcc94c8743e Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Fri, 23 Jun 2023 13:22:33 +0200 Subject: [PATCH 170/410] Use gene symbols for plotting --- bin/st_spatial_de.qmd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 649fc2d..259ad84 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -39,12 +39,12 @@ plt.rcParams["figure.figsize"] = (5, 5) st_adata.uns['log1p']['base'] = None sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") ``` ```{python} sc.tl.rank_genes_groups(st_adata, 'clusters', method='wilcoxon') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False) +sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") ``` ## Spatially variable genes @@ -70,12 +70,12 @@ st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, : Then we can inspect significant genes that varies in space and visualize them with `sc.pl.spatial` function. ```{python} -results = results.sort_values("qval", ascending=True) -results.to_csv(saveSpatialDEFileName) -results.head(10) +results_tab = st_adata.var.sort_values("qval", ascending=True) +results_tab.to_csv(saveSpatialDEFileName) +results_tab.head(10) ``` ```{python} -keys = results.index.values[: plotTopHVG] -sc.pl.spatial(st_adata, img_key="hires", color=keys, alpha=0.7, ncols=numberOfColumns) +symbols = results_tab.iloc[: plotTopHVG]["gene_symbol"] +sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, ncols=numberOfColumns, title=symbols) ``` From 6a507a7be0fec0b99b139a8f66c3c1ea9f93ceae Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sun, 25 Jun 2023 10:01:05 +0200 Subject: [PATCH 171/410] Update nf-test snapshots --- tests/pipeline/test_downstream.nf.test.snap | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 6 +++--- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index fd47faf..d5b1a5a 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-22T09:27:49+0000" + "timestamp": "2023-06-25T06:54:28+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:27:49+0000" + "timestamp": "2023-06-25T06:54:28+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 9f2a204..2288ed1 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,ee09bee760712d19b395b4f16142f9d6", "st_qc_and_normalisation.html:md5,5b8eb77ea0591d0c326f771eb29c1956" ], - "timestamp": "2023-06-22T09:30:11+0000" + "timestamp": "2023-06-25T07:20:44+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:30:11+0000" + "timestamp": "2023-06-25T07:20:44+0000" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 4cf3235..a35e622 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,4125e3d54838133dd439f5ab516c6b10", "st_qc_and_normalisation.html:md5,b7d00e43923f76ead51e48e4064ae875" ], - "timestamp": "2023-06-22T09:32:47+0000" + "timestamp": "2023-06-25T07:35:14+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=, leidenalg=, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-22T09:32:47+0000" + "timestamp": "2023-06-25T07:35:14+0000" } } \ No newline at end of file From 8f0cc35aba9e70987280a768619ec40db8452e42 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sun, 25 Jun 2023 10:05:40 +0200 Subject: [PATCH 172/410] Update `.prettierignore` --- .prettierignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.prettierignore b/.prettierignore index 437d763..bd2d59c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,5 @@ testing/ testing* *.pyc bin/ +test-datasets/ +.nf-test/ From f7e81ef56aae5b8371ba424927f088d9f208c250 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sun, 25 Jun 2023 13:56:40 +0200 Subject: [PATCH 173/410] Fix process labels --- modules/local/st_clustering.nf | 2 +- modules/local/st_qc_and_normalisation.nf | 2 +- modules/local/st_read_data.nf | 2 +- modules/local/st_spatial_de.nf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 81a8e80..0da7fbb 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -7,7 +7,7 @@ process ST_CLUSTERING { // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" - label "process_low" + label 'process_low' conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0 conda-forge::leidenalg=0.9.1" container "docker.io/erikfas/spatialtranscriptomics" diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index d9e64de..6405c12 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -7,7 +7,7 @@ process ST_QC_AND_NORMALISATION { // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" - label "process_low" + label 'process_low' conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0" container "docker.io/erikfas/spatialtranscriptomics" diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index bdbadbe..e159853 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -4,7 +4,7 @@ process ST_READ_DATA { tag "${meta.id}" - label "process_low" + label 'process_low' conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 02bfcbb..af8ff05 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -8,7 +8,7 @@ process ST_SPATIAL_DE { // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" - label "process_medium" + label 'process_medium' conda "env/st_spatial_de/environment.yml" container "docker.io/erikfas/spatialtranscriptomics" From 6f40eb856dd270bc0b904046f41eaed27526d979 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Sun, 25 Jun 2023 13:57:54 +0200 Subject: [PATCH 174/410] Remove finished TODO statements --- conf/analysis.config | 2 -- docs/output.md | 2 -- modules/local/st_spatial_de.nf | 1 - 3 files changed, 5 deletions(-) diff --git a/conf/analysis.config b/conf/analysis.config index fe5f124..5e062eb 100644 --- a/conf/analysis.config +++ b/conf/analysis.config @@ -2,8 +2,6 @@ Default config options */ -// TODO : add bool flags - params { // Data loading diff --git a/docs/output.md b/docs/output.md index 90430c4..6eb2f9e 100644 --- a/docs/output.md +++ b/docs/output.md @@ -54,8 +54,6 @@ information about these files at the [10X website](https://support.10xgenomics.c ## Data - -

    Output files diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index af8ff05..14dbc18 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -3,7 +3,6 @@ // process ST_SPATIAL_DE { - // TODO nf-core: fix exporting of SpatialDE version, which does not work with `__version__` // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 From 7ec901b512303cbd0bffea5ea7bc6a5f3afa8762 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 26 Jun 2023 15:00:10 +0200 Subject: [PATCH 175/410] Update DUMPSOFTWAREVERSIONS nf-core module --- modules.json | 2 +- .../custom-dumpsoftwareversions.diff | 38 ++----------------- .../custom/dumpsoftwareversions/main.nf | 2 +- .../templates/dumpsoftwareversions.py | 3 +- 4 files changed, 7 insertions(+), 38 deletions(-) diff --git a/modules.json b/modules.json index a44908f..a7d46a8 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "custom/dumpsoftwareversions": { "branch": "master", - "git_sha": "c8e35eb2055c099720a75538d1b8adb3fb5a464c", + "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", "installed_by": ["modules"], "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" }, diff --git a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff index 0c21dd4..e2f4ce0 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff +++ b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff @@ -1,17 +1,7 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' --- modules/nf-core/custom/dumpsoftwareversions/meta.yml +++ modules/nf-core/custom/dumpsoftwareversions/meta.yml -@@ -1,7 +1,9 @@ -+# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json - name: custom_dumpsoftwareversions - description: Custom module used to dump software versions within the nf-core pipeline template - keywords: - - custom -+ - dump - - version - tools: - - custom: -@@ -20,10 +22,6 @@ +@@ -22,10 +22,6 @@ type: file description: Standard YML file containing software versions pattern: "software_versions.yml" @@ -25,19 +15,7 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' --- modules/nf-core/custom/dumpsoftwareversions/main.nf +++ modules/nf-core/custom/dumpsoftwareversions/main.nf -@@ -2,18 +2,17 @@ - label 'process_single' - - // Requires `pyyaml` which does not have a dedicated container but is in the MultiQC container -- conda "bioconda::multiqc=1.13" -+ conda "bioconda::multiqc=1.14" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? -- 'https://depot.galaxyproject.org/singularity/multiqc:1.13--pyhdfd78af_0' : -- 'quay.io/biocontainers/multiqc:1.13--pyhdfd78af_0' }" -+ 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : -+ 'quay.io/biocontainers/multiqc:1.14--pyhdfd78af_0' }" - - input: +@@ -11,9 +11,8 @@ path versions output: @@ -52,16 +30,8 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' --- modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +++ modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py -@@ -4,13 +4,14 @@ - """Provide functions to merge multiple versions.yml files.""" - - --import yaml - import platform - from textwrap import dedent +@@ -10,7 +10,7 @@ -+import yaml -+ def _make_versions_html(versions): - """Generate a tabular HTML output of all versions for MultiQC.""" @@ -69,7 +39,7 @@ Changes in module 'nf-core/custom/dumpsoftwareversions' html = [ dedent( """\\ -@@ -79,19 +80,8 @@ +@@ -79,19 +79,8 @@ "$workflow.manifest.name": "$workflow.manifest.version", } diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index fc16a12..cf3d25d 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -5,7 +5,7 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { conda "bioconda::multiqc=1.14" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : - 'quay.io/biocontainers/multiqc:1.14--pyhdfd78af_0' }" + 'biocontainers/multiqc:1.14--pyhdfd78af_0' }" input: path versions diff --git a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py index c54dc00..2600e95 100755 --- a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +++ b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py @@ -4,11 +4,10 @@ """Provide functions to merge multiple versions.yml files.""" +import yaml import platform from textwrap import dedent -import yaml - def _make_versions_html(versions): """Generate a tabular HTML output of all versions.""" From 5f68a80b7b04135f097b725d4662d4f70b3541e9 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 29 Jun 2023 12:49:43 +0200 Subject: [PATCH 176/410] Install modules --- modules.json | 10 ++++++ modules/nf-core/fastqc/main.nf | 51 +++++++++++++++++++++++++++++ modules/nf-core/fastqc/meta.yml | 52 +++++++++++++++++++++++++++++ modules/nf-core/multiqc/main.nf | 53 ++++++++++++++++++++++++++++++ modules/nf-core/multiqc/meta.yml | 56 ++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 modules/nf-core/fastqc/main.nf create mode 100644 modules/nf-core/fastqc/meta.yml create mode 100644 modules/nf-core/multiqc/main.nf create mode 100644 modules/nf-core/multiqc/meta.yml diff --git a/modules.json b/modules.json index a7d46a8..63761aa 100644 --- a/modules.json +++ b/modules.json @@ -11,6 +11,16 @@ "installed_by": ["modules"], "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" }, + "fastqc": { + "branch": "master", + "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", + "installed_by": ["modules"] + }, + "multiqc": { + "branch": "master", + "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", + "installed_by": ["modules"] + }, "spaceranger/count": { "branch": "master", "git_sha": "84bd059d24c74c44e31f98924413d2c7f04dd6c6", diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf new file mode 100644 index 0000000..07d5e43 --- /dev/null +++ b/modules/nf-core/fastqc/main.nf @@ -0,0 +1,51 @@ +process FASTQC { + tag "$meta.id" + label 'process_medium' + + conda "bioconda::fastqc=0.11.9" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/fastqc:0.11.9--0' : + 'biocontainers/fastqc:0.11.9--0' }" + + input: + tuple val(meta), path(reads) + + output: + tuple val(meta), path("*.html"), emit: html + tuple val(meta), path("*.zip") , emit: zip + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + // Make list of old name and new name pairs to use for renaming in the bash while loop + def old_new_pairs = reads instanceof Path || reads.size() == 1 ? [[ reads, "${prefix}.${reads.extension}" ]] : reads.withIndex().collect { entry, index -> [ entry, "${prefix}_${index + 1}.${entry.extension}" ] } + def rename_to = old_new_pairs*.join(' ').join(' ') + def renamed_files = old_new_pairs.collect{ old_name, new_name -> new_name }.join(' ') + """ + printf "%s %s\\n" $rename_to | while read old_name new_name; do + [ -f "\${new_name}" ] || ln -s \$old_name \$new_name + done + fastqc $args --threads $task.cpus $renamed_files + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fastqc: \$( fastqc --version | sed -e "s/FastQC v//g" ) + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}.html + touch ${prefix}.zip + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fastqc: \$( fastqc --version | sed -e "s/FastQC v//g" ) + END_VERSIONS + """ +} diff --git a/modules/nf-core/fastqc/meta.yml b/modules/nf-core/fastqc/meta.yml new file mode 100644 index 0000000..4da5bb5 --- /dev/null +++ b/modules/nf-core/fastqc/meta.yml @@ -0,0 +1,52 @@ +name: fastqc +description: Run FastQC on sequenced reads +keywords: + - quality control + - qc + - adapters + - fastq +tools: + - fastqc: + description: | + FastQC gives general quality metrics about your reads. + It provides information about the quality score distribution + across your reads, the per base sequence content (%A/C/G/T). + You get information about adapter contamination and other + overrepresented sequences. + homepage: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/ + documentation: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/ + licence: ["GPL-2.0-only"] +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. +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}" + - zip: + type: file + description: FastQC report archive + pattern: "*_{fastqc.zip}" + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@drpatelh" + - "@grst" + - "@ewels" + - "@FelixKrueger" diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf new file mode 100644 index 0000000..1fc387b --- /dev/null +++ b/modules/nf-core/multiqc/main.nf @@ -0,0 +1,53 @@ +process MULTIQC { + label 'process_single' + + conda "bioconda::multiqc=1.14" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : + 'biocontainers/multiqc:1.14--pyhdfd78af_0' }" + + input: + path multiqc_files, stageAs: "?/*" + path(multiqc_config) + path(extra_multiqc_config) + path(multiqc_logo) + + output: + path "*multiqc_report.html", emit: report + path "*_data" , emit: data + path "*_plots" , optional:true, emit: plots + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def config = multiqc_config ? "--config $multiqc_config" : '' + def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' + """ + multiqc \\ + --force \\ + $args \\ + $config \\ + $extra_config \\ + . + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) + END_VERSIONS + """ + + stub: + """ + touch multiqc_data + touch multiqc_plots + touch multiqc_report.html + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) + END_VERSIONS + """ +} diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml new file mode 100644 index 0000000..f93b5ee --- /dev/null +++ b/modules/nf-core/multiqc/meta.yml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json +name: MultiQC +description: Aggregate results from bioinformatics analyses across many samples into a single report +keywords: + - QC + - bioinformatics tools + - Beautiful stand-alone HTML report +tools: + - multiqc: + description: | + MultiQC searches a given directory for analysis logs and compiles a HTML report. + It's a general use tool, perfect for summarising the output from numerous bioinformatics tools. + homepage: https://multiqc.info/ + documentation: https://multiqc.info/docs/ + licence: ["GPL-3.0-or-later"] + +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}" + +output: + - report: + type: file + description: MultiQC report file + pattern: "multiqc_report.html" + - data: + type: directory + description: MultiQC data dir + pattern: "multiqc_data" + - plots: + type: file + description: Plots created by MultiQC + pattern: "*_data" + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@abhi18av" + - "@bunop" + - "@drpatelh" + - "@jfy133" From 8b2e3b231fe4dd8dd50b9c81685b1051979d99e7 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 29 Jun 2023 12:52:06 +0200 Subject: [PATCH 177/410] Use vanilla version of dumpsoftwareversions --- modules.json | 3 +- .../custom-dumpsoftwareversions.diff | 63 ------------------- .../custom/dumpsoftwareversions/main.nf | 5 +- .../custom/dumpsoftwareversions/meta.yml | 4 ++ .../templates/dumpsoftwareversions.py | 13 +++- 5 files changed, 20 insertions(+), 68 deletions(-) delete mode 100644 modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff diff --git a/modules.json b/modules.json index 63761aa..f7a14ea 100644 --- a/modules.json +++ b/modules.json @@ -8,8 +8,7 @@ "custom/dumpsoftwareversions": { "branch": "master", "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", - "installed_by": ["modules"], - "patch": "modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff" + "installed_by": ["modules"] }, "fastqc": { "branch": "master", diff --git a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff b/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff deleted file mode 100644 index e2f4ce0..0000000 --- a/modules/nf-core/custom/dumpsoftwareversions/custom-dumpsoftwareversions.diff +++ /dev/null @@ -1,63 +0,0 @@ -Changes in module 'nf-core/custom/dumpsoftwareversions' ---- modules/nf-core/custom/dumpsoftwareversions/meta.yml -+++ modules/nf-core/custom/dumpsoftwareversions/meta.yml -@@ -22,10 +22,6 @@ - type: file - description: Standard YML file containing software versions - pattern: "software_versions.yml" -- - mqc_yml: -- type: file -- description: MultiQC custom content YML file containing software versions -- pattern: "software_versions_mqc.yml" - - versions: - type: file - description: File containing software versions - ---- modules/nf-core/custom/dumpsoftwareversions/main.nf -+++ modules/nf-core/custom/dumpsoftwareversions/main.nf -@@ -11,9 +11,8 @@ - path versions - - output: -- path "software_versions.yml" , emit: yml -- path "software_versions_mqc.yml", emit: mqc_yml -- path "versions.yml" , emit: versions -+ path "software_versions.yml", emit: yml -+ path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - ---- modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py -+++ modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py -@@ -10,7 +10,7 @@ - - - def _make_versions_html(versions): -- """Generate a tabular HTML output of all versions for MultiQC.""" -+ """Generate a tabular HTML output of all versions.""" - html = [ - dedent( - """\\ -@@ -79,19 +79,8 @@ - "$workflow.manifest.name": "$workflow.manifest.version", - } - -- versions_mqc = { -- "id": "software_versions", -- "section_name": "${workflow.manifest.name} Software Versions", -- "section_href": "https://github.com/${workflow.manifest.name}", -- "plot_type": "html", -- "description": "are collected at run time from the software output.", -- "data": _make_versions_html(versions_by_module), -- } -- - with open("software_versions.yml", "w") as f: - yaml.dump(versions_by_module, f, default_flow_style=False) -- with open("software_versions_mqc.yml", "w") as f: -- yaml.dump(versions_mqc, f, default_flow_style=False) - - with open("versions.yml", "w") as f: - yaml.dump(versions_this_module, f, default_flow_style=False) - -************************************************************ diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index cf3d25d..ebc8727 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -11,8 +11,9 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { path versions output: - path "software_versions.yml", emit: yml - path "versions.yml" , emit: versions + path "software_versions.yml" , emit: yml + path "software_versions_mqc.yml", emit: mqc_yml + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when diff --git a/modules/nf-core/custom/dumpsoftwareversions/meta.yml b/modules/nf-core/custom/dumpsoftwareversions/meta.yml index 71ff6ca..c32657d 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/meta.yml +++ b/modules/nf-core/custom/dumpsoftwareversions/meta.yml @@ -22,6 +22,10 @@ output: type: file description: Standard YML file containing software versions pattern: "software_versions.yml" + - mqc_yml: + type: file + description: MultiQC custom content YML file containing software versions + pattern: "software_versions_mqc.yml" - versions: type: file description: File containing software versions diff --git a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py index 2600e95..da03340 100755 --- a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +++ b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py @@ -10,7 +10,7 @@ def _make_versions_html(versions): - """Generate a tabular HTML output of all versions.""" + """Generate a tabular HTML output of all versions for MultiQC.""" html = [ dedent( """\\ @@ -79,8 +79,19 @@ def main(): "$workflow.manifest.name": "$workflow.manifest.version", } + versions_mqc = { + "id": "software_versions", + "section_name": "${workflow.manifest.name} Software Versions", + "section_href": "https://github.com/${workflow.manifest.name}", + "plot_type": "html", + "description": "are collected at run time from the software output.", + "data": _make_versions_html(versions_by_module), + } + with open("software_versions.yml", "w") as f: yaml.dump(versions_by_module, f, default_flow_style=False) + with open("software_versions_mqc.yml", "w") as f: + yaml.dump(versions_mqc, f, default_flow_style=False) with open("versions.yml", "w") as f: yaml.dump(versions_this_module, f, default_flow_style=False) From feaa0f4c63a559981d5c90fb1a58162d3313f46d Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 29 Jun 2023 13:04:45 +0200 Subject: [PATCH 178/410] Add fastqc + multiqc --- workflows/spatialtranscriptomics.nf | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 5a1c609..d302faa 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -23,6 +23,19 @@ for (param in checkPathParamList) { if (param) { file(param, checkIfExists: true // Check mandatory parameters if (params.input) { ch_input = file(params.input) } else { exit 1, 'Input samplesheet not specified!' } + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CONFIG FILES +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +ch_multiqc_config = Channel.fromPath("$projectDir/assets/multiqc_config.yml", checkIfExists: true) +ch_multiqc_custom_config = params.multiqc_config ? Channel.fromPath( params.multiqc_config, checkIfExists: true ) : Channel.empty() +ch_multiqc_logo = params.multiqc_logo ? Channel.fromPath( params.multiqc_logo, checkIfExists: true ) : Channel.empty() +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) + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORT LOCAL MODULES/SUBWORKFLOWS @@ -51,6 +64,8 @@ include { ST_POSTPROCESS } from '../subworkflows/local/st_postprocess' // // MODULE: Installed directly from nf-core/modules // +include { FASTQC } from "../modules/nf-core/fastqc/main" +include { MULTIQC } from "../modules/nf-core/multiqc/main" include { CUSTOM_DUMPSOFTWAREVERSIONS } from '../modules/nf-core/custom/dumpsoftwareversions/main' /* @@ -73,6 +88,14 @@ workflow ST { ch_input ) + // + // MODULE: FastQC + // + FASTQC( + INPUT_CHECK.out.ch_spaceranger_input.map{ it -> [it[0] /* meta */, it[1] /* reads */]} + ) + ch_versions = ch_versions.mix(FASTQ.out.versions) + // // SUBWORKFLOW: Space Ranger raw data processing // @@ -114,6 +137,29 @@ workflow ST { CUSTOM_DUMPSOFTWAREVERSIONS ( ch_versions.unique().collectFile(name: 'collated_versions.yml') ) + + // + // MODULE: MultiQC + // + workflow_summary = WorkflowScrnaseq.paramsSummaryMultiqc(workflow, summary_params) + ch_workflow_summary = Channel.value(workflow_summary) + + methods_description = WorkflowScrnaseq.methodsDescriptionText(workflow, ch_multiqc_custom_methods_description) + ch_methods_description = Channel.value(methods_description) + + ch_multiqc_files = ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml').mix( + ch_methods_description.collectFile(name: 'methods_description_mqc.yaml'), + CUSTOM_DUMPSOFTWAREVERSIONS.out.mqc_yml.collect(), + FASTQC.out.zip.collect{ meta, qcfile -> qcfile } + ) + + MULTIQC ( + ch_multiqc_files.collect(), + ch_multiqc_config.toList(), + ch_multiqc_custom_config.toList(), + ch_multiqc_logo.toList() + ) + multiqc_report = MULTIQC.out.report.toList() } /* @@ -124,7 +170,7 @@ workflow ST { workflow.onComplete { if (params.email || params.email_on_fail) { - NfcoreTemplate.email(workflow, params, summary_params, projectDir, log) + NfcoreTemplate.email(workflow, params, summary_params, projectDir, log, multiqc_report) } NfcoreTemplate.summary(workflow, params, log) if (params.hook_url) { From 9be1ae5f3a89a6ae327a21f55f4f990354f4792f Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 29 Jun 2023 13:08:36 +0200 Subject: [PATCH 179/410] Add multiqc config options --- nextflow.config | 7 +++++++ nextflow_schema.json | 30 +++++++++++++++++++++++++++++ workflows/spatialtranscriptomics.nf | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index 00eb598..5a63ab3 100644 --- a/nextflow.config +++ b/nextflow.config @@ -16,6 +16,13 @@ params { spaceranger_reference = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" spaceranger_probeset = null + // MultiQC options + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' + multiqc_methods_description = null + // Boilerplate options outdir = null publish_dir_mode = 'copy' diff --git a/nextflow_schema.json b/nextflow_schema.json index f4c735a..b57cbee 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -49,6 +49,11 @@ "fa_icon": "fas fa-envelope", "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" + }, + "multiqc_title": { + "type": "string", + "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", + "fa_icon": "fas fa-file-signature" } } }, @@ -241,6 +246,14 @@ "help_text": "An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does not exit successfully.", "hidden": true }, + "max_multiqc_email_size": { + "type": "string", + "description": "File size limit when attaching MultiQC reports to summary emails.", + "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", + "default": "25.MB", + "fa_icon": "fas fa-file-upload", + "hidden": true + }, "plaintext_email": { "type": "boolean", "description": "Send plain-text email instead of HTML.", @@ -260,6 +273,23 @@ "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", "hidden": true }, + "multiqc_config": { + "type": "string", + "description": "Custom config file to supply to MultiQC.", + "fa_icon": "fas fa-cog", + "hidden": true + }, + "multiqc_logo": { + "type": "string", + "description": "Custom logo file to supply to MultiQC. File name must also be set in the MultiQC config file", + "fa_icon": "fas fa-image", + "hidden": true + }, + "multiqc_methods_description": { + "type": "string", + "description": "Custom MultiQC yaml file containing HTML including a methods description.", + "fa_icon": "fas fa-cog" + }, "validate_params": { "type": "boolean", "description": "Boolean whether to validate parameters against the schema at runtime", diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index d302faa..22debe4 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -94,7 +94,7 @@ workflow ST { FASTQC( INPUT_CHECK.out.ch_spaceranger_input.map{ it -> [it[0] /* meta */, it[1] /* reads */]} ) - ch_versions = ch_versions.mix(FASTQ.out.versions) + ch_versions = ch_versions.mix(FASTQC.out.versions) // // SUBWORKFLOW: Space Ranger raw data processing From 08fda754f1422f9bbff31367341c00c3bb045743 Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 29 Jun 2023 13:59:58 +0200 Subject: [PATCH 180/410] Update tests --- conf/modules.config | 9 ++++ lib/WorkflowSpatialtranscriptomics.groovy | 47 +++++++++++++++++++ tests/pipeline/test_downstream.nf.test | 2 + .../pipeline/test_spaceranger_ffpe_v1.nf.test | 4 +- .../test_spaceranger_ffpe_v1.nf.test.snap | 8 ++-- ...test_spaceranger_ffpe_v2_cytassist.nf.test | 7 ++- ...spaceranger_ffpe_v2_cytassist.nf.test.snap | 8 ++-- workflows/spatialtranscriptomics.nf | 4 +- 8 files changed, 77 insertions(+), 12 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 65b2408..32bc8d6 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -30,6 +30,15 @@ process { ] } + // store sample-specifc results in the per-sample subfolder + withName: FASTQC { + publishDir = [ + path: { "${params.outdir}/${meta.id}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + withName: SPACERANGER_COUNT { publishDir = [ // NOTE: the sample name is already excluded in the path that's getting published. diff --git a/lib/WorkflowSpatialtranscriptomics.groovy b/lib/WorkflowSpatialtranscriptomics.groovy index c6e7f3c..6d7673b 100755 --- a/lib/WorkflowSpatialtranscriptomics.groovy +++ b/lib/WorkflowSpatialtranscriptomics.groovy @@ -2,6 +2,9 @@ // This file holds several functions specific to the workflow/spatialtranscriptomics.nf in the nf-core/spatialtranscriptomics pipeline // +import nextflow.Nextflow +import groovy.text.SimpleTemplateEngine + class WorkflowSpatialtranscriptomics { // @@ -14,4 +17,48 @@ class WorkflowSpatialtranscriptomics { // } } + // + // Get workflow summary for MultiQC + // + public static String paramsSummaryMultiqc(workflow, summary) { + String summary_section = '' + for (group in summary.keySet()) { + def group_params = summary.get(group) // This gets the parameters of that particular group + if (group_params) { + summary_section += "

    $group

    \n" + summary_section += "
    \n" + } + } + + String yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" + 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 + } + + public static String methodsDescriptionText(run_workflow, mqc_methods_yaml) { + // Convert to a named map so can be used as with familar NXF ${workflow} variable syntax in the MultiQC YML file + def meta = [:] + meta.workflow = run_workflow.toMap() + meta["manifest_map"] = run_workflow.manifest.toMap() + + meta["doi_text"] = meta.manifest_map.doi ? "(doi: ${meta.manifest_map.doi})" : "" + 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.
  • " + + def methods_text = mqc_methods_yaml.text + + def engine = new SimpleTemplateEngine() + def description_html = engine.createTemplate(methods_text).make(meta) + + return description_html + } + } diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index fee1dbc..8ed783a 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -27,6 +27,8 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, // degs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, + // multiqc + { assert file("$outputDir/multiqc/multiqc_report.html").exists() } ) } } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 301d215..7f03e1a 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -36,7 +36,9 @@ nextflow_pipeline { path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), )}, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() } + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() }, + // multiqc + { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("Visium_FFPE_Human_Ovarian_Cancer")} ) } } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index a82ab44..d9d705d 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,905395042fa8b8a012718c1dbff39db4", "st_qc_and_normalisation.html:md5,f89922b04edd7299e34ba1e43c257000" ], - "timestamp": "2023-06-25T07:20:44+0000" + "timestamp": "2023-06-29T12:46:05+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, FASTQC={fastqc=0.11.9}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-25T07:20:44+0000" + "timestamp": "2023-06-29T12:46:05+0000" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 1e22084..82b0d92 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -33,8 +33,13 @@ nextflow_pipeline { path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), )}, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() } + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() }, + // multiqc + { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("CytAssist_11mm_FFPE_Human_Glioblastoma_2_4")} ) } } + + + } diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 1d70131..24f6f88 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -4,12 +4,12 @@ "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" ], - "timestamp": "2023-06-25T07:35:14+0000" + "timestamp": "2023-06-29T12:48:09+0000" }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, FASTQC={fastqc=0.11.9}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-25T07:35:14+0000" + "timestamp": "2023-06-29T12:48:09+0000" } -} +} \ No newline at end of file diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 22debe4..b869cb8 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -141,10 +141,10 @@ workflow ST { // // MODULE: MultiQC // - workflow_summary = WorkflowScrnaseq.paramsSummaryMultiqc(workflow, summary_params) + workflow_summary = WorkflowSpatialtranscriptomics.paramsSummaryMultiqc(workflow, summary_params) ch_workflow_summary = Channel.value(workflow_summary) - methods_description = WorkflowScrnaseq.methodsDescriptionText(workflow, ch_multiqc_custom_methods_description) + methods_description = WorkflowSpatialtranscriptomics.methodsDescriptionText(workflow, ch_multiqc_custom_methods_description) ch_methods_description = Channel.value(methods_description) ch_multiqc_files = ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml').mix( From a8227a08694bf1c6ca21d955e1d8cad5e8e37f3e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 15:37:26 +0200 Subject: [PATCH 181/410] Add @grst to credits --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fa1cb39..aa83a6b 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ Infastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://sci it is currently developed and maintained by [Erik Fasterius](https://github.com/fasterius) and [Christophe Avenel](https://github.com/cavenel). +Many thanks to others who have helped out along the way too, especially [Gregor +Sturm](https://github.com/grst)! + 1 Supported by grants from the US National Institutes of Health [U24CA224067](https://reporter.nih.gov/project-details/10261367) and [U54AG075941](https://reporter.nih.gov/project-details/10376627). Original From 6d752b6985f19fe6e9085690f1f063fd00401498 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 15:41:53 +0200 Subject: [PATCH 182/410] Fix new validation params --- nextflow.config | 3 --- 1 file changed, 3 deletions(-) diff --git a/nextflow.config b/nextflow.config index f19c070..8a1efff 100644 --- a/nextflow.config +++ b/nextflow.config @@ -33,9 +33,6 @@ params { hook_url = null help = false version = false - validate_params = true - show_hidden_params = false - schema_ignore_params = '' // Config options custom_config_version = 'master' From df183e6bfff20f4f4457b6aa63d41f8fc32b80da Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 15:44:42 +0200 Subject: [PATCH 183/410] Update FastQC nf-core module --- modules.json | 2 +- modules/nf-core/fastqc/main.nf | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules.json b/modules.json index f7a14ea..127ce48 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "fastqc": { "branch": "master", - "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", + "git_sha": "bd8092b67b5103bdd52e300f75889442275c3117", "installed_by": ["modules"] }, "multiqc": { diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf index 07d5e43..249f906 100644 --- a/modules/nf-core/fastqc/main.nf +++ b/modules/nf-core/fastqc/main.nf @@ -29,7 +29,11 @@ process FASTQC { printf "%s %s\\n" $rename_to | while read old_name new_name; do [ -f "\${new_name}" ] || ln -s \$old_name \$new_name done - fastqc $args --threads $task.cpus $renamed_files + + fastqc \\ + $args \\ + --threads $task.cpus \\ + $renamed_files cat <<-END_VERSIONS > versions.yml "${task.process}": From 652ee147ff06d438da28f3537fe9844d8b32b3b9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 15:45:43 +0200 Subject: [PATCH 184/410] Remove unnecessary `quay.io/...` container address --- modules/local/st_read_data.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index e159853..eac6100 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -9,7 +9,7 @@ process ST_READ_DATA { conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/scanpy:1.7.2--pyhdfd78af_0' : - 'quay.io/biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" + 'biocontainers/scanpy:1.7.2--pyhdfd78af_0' }" input: tuple val (meta), path("${meta.id}/*") From cbe79a4a9010a26ebe878657cdf16ef9b2fe1f55 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 15:54:46 +0200 Subject: [PATCH 185/410] Remove duplicate schema entry --- nextflow_schema.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 502800f..e4cde1a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -263,12 +263,6 @@ "fa_icon": "fas fa-file-upload", "hidden": true }, - "plaintext_email": { - "type": "boolean", - "description": "Send plain-text email instead of HTML.", - "fa_icon": "fas fa-remove-format", - "hidden": true - }, "monochrome_logs": { "type": "boolean", "description": "Do not use coloured log outputs.", From f6941d9925adf99d710287f41935ad5c48e15620 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 16:02:12 +0200 Subject: [PATCH 186/410] Remove reference to non-existing versions channel --- workflows/spatialtranscriptomics.nf | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index de6405f..4e06b96 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -91,12 +91,8 @@ workflow ST { // SUBWORKFLOW: Read and validate samplesheet // INPUT_CHECK ( - file(params.input) + ch_input ) - ch_versions = ch_versions.mix(INPUT_CHECK.out.versions) - // TODO: OPTIONAL, you can use nf-validation plugin to create an input channel from the samplesheet with Channel.fromSamplesheet("input") - // See the documentation https://nextflow-io.github.io/nf-validation/samplesheets/fromSamplesheet/ - // ! There is currently no tooling to help you write a sample sheet schema // // MODULE: FastQC From 9ce879a04484b3d05c40ac0ef28cc5ebfcbee65e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 3 Jul 2023 16:02:50 +0200 Subject: [PATCH 187/410] Minor formatting --- subworkflows/local/spaceranger.nf | 2 +- workflows/spatialtranscriptomics.nf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index cdff67c..8816074 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -3,7 +3,7 @@ // include { UNTAR as SPACERANGER_UNTAR_REFERENCE } from "../../modules/nf-core/untar" -include { SPACERANGER_COUNT } from '../../modules/nf-core/spaceranger/count' +include { SPACERANGER_COUNT } from '../../modules/nf-core/spaceranger/count' workflow SPACERANGER { diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 4e06b96..51d04bc 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -70,8 +70,8 @@ include { ST_POSTPROCESS } from '../subworkflows/local/st_postprocess' // // MODULE: Installed directly from nf-core/modules // -include { FASTQC } from "../modules/nf-core/fastqc/main" -include { MULTIQC } from "../modules/nf-core/multiqc/main" +include { FASTQC } from "../modules/nf-core/fastqc/main" +include { MULTIQC } from "../modules/nf-core/multiqc/main" include { CUSTOM_DUMPSOFTWAREVERSIONS } from '../modules/nf-core/custom/dumpsoftwareversions/main' /* From 02f7e93da9328487de33d4f392a8114f9ce7b6da Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Tue, 28 Nov 2023 20:11:20 +0100 Subject: [PATCH 188/410] Update read_visium to support SpaceRange 2 --- bin/read_st_data.py | 135 +++++++++++++++++++++++++--------- modules/local/st_read_data.nf | 9 ++- 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 0f57e76..18f872e 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -2,53 +2,74 @@ # Load packages import argparse +from scanpy import read_10x_h5 +from pathlib import Path +from typing import Union, Optional + import json import pandas as pd -from anndata import AnnData from matplotlib.image import imread -from pathlib import Path -from scanpy import read_10x_h5 -from typing import Union, Optional +from anndata import ( + AnnData +) +from scanpy import logging as logg -# Function to read MTX -def read_visium_mtx( +def read_visium( path: Union[str, Path], + genome: Optional[str] = None, *, - load_images: bool = True, - library_id: Optional[str] = None, + count_file: str = "filtered_feature_bc_matrix.h5", + library_id: str = None, + load_images: Optional[bool] = True, + source_image_path: Optional[Union[str, Path]] = None, ) -> AnnData: """\ Read 10x-Genomics-formatted visum dataset. + In addition to reading regular 10x output, this looks for the `spatial` folder and loads images, coordinates and scale factors. Based on the `Space Ranger output docs`_. + See :func:`~scanpy.pl.spatial` for a compatible plotting function. + .. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview + Parameters ---------- path - Path to a spaceranger output directory - load_images: - Whether or not to load images + Path to directory for visium datafiles. + genome + Filter expression to genes within this genome. + count_file + Which file in the passed directory to use as the count file. Typically would be one of: + 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. library_id Identifier for the visium library. Can be modified when concatenating multiple adata objects. + source_image_path + Path to the high-resolution tissue image. Path will be included in + `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. Returns ------- Annotated data matrix, where observations/cells are named by their barcode and variables/genes by gene name. Stores the following information: + :attr:`~anndata.AnnData.X` The data matrix is stored :attr:`~anndata.AnnData.obs_names` Cell names :attr:`~anndata.AnnData.var_names` - Gene names + Gene names for a feature barcode matrix, probe names for a probe bc matrix :attr:`~anndata.AnnData.var`\\ `['gene_ids']` Gene IDs :attr:`~anndata.AnnData.var`\\ `['feature_types']` Feature types + :attr:`~anndata.AnnData.obs`\\ `[filtered_barcodes]` + filtered barcodes if present in the matrix + :attr:`~anndata.AnnData.var` + Any additional metadata present in /matrix/features is read in. :attr:`~anndata.AnnData.uns`\\ `['spatial']` Dict of spaceranger output files with 'library_id' as key :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['images']` @@ -60,7 +81,6 @@ def read_visium_mtx( :attr:`~anndata.AnnData.obsm`\\ `['spatial']` Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. """ - path = Path(path) adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") # use ensemble IDs as index, because they are unique @@ -69,49 +89,91 @@ def read_visium_mtx( adata.uns["spatial"] = dict() + from h5py import File + + with File(path / count_file, mode="r") as f: + attrs = dict(f.attrs) if library_id is None: - library_id = "library_id" + library_id = str(attrs.pop("library_ids")[0], "utf-8") adata.uns["spatial"][library_id] = dict() if load_images: + tissue_positions_file = ( + path / "spatial/tissue_positions.csv" + if (path / "spatial/tissue_positions.csv").exists() + else path / "spatial/tissue_positions_list.csv" + ) files = dict( - tissue_positions_file=path / "tissue_positions.csv", - scalefactors_json_file=path / "scalefactors_json.json", - hires_image=path / "tissue_hires_image.png", - lowres_image=path / "tissue_lowres_image.png", + tissue_positions_file=tissue_positions_file, + scalefactors_json_file=path / 'spatial/scalefactors_json.json', + hires_image=path / 'spatial/tissue_hires_image.png', + lowres_image=path / 'spatial/tissue_lowres_image.png', ) - # Check if files exist; continue if images are missing + # check if files exists, continue if images are missing for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - print("You seem to be missing an image file.") - print("Could not find '{f}'.") + logg.warning( + f"You seem to be missing an image file.\n" + f"Could not find '{f}'." + ) else: raise OSError(f"Could not find '{f}'") - # Check for existance of images - adata.uns["spatial"][library_id]["images"] = dict() - for res in ["hires", "lowres"]: + adata.uns["spatial"][library_id]['images'] = dict() + for res in ['hires', 'lowres']: try: - adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) + adata.uns["spatial"][library_id]['images'][res] = imread( + str(files[f'{res}_image']) + ) except Exception: raise OSError(f"Could not find '{res}_image'") - # Read JSON scale factors - adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) - adata.uns["spatial"][library_id]["metadata"] = {k: "NA" for k in ("chemistry_description", "software_version")} + # read json scalefactors + adata.uns["spatial"][library_id]['scalefactors'] = json.loads( + files['scalefactors_json_file'].read_bytes() + ) + + adata.uns["spatial"][library_id]["metadata"] = { + k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) + for k in ("chemistry_description", "software_version") + if k in attrs + } + + # read coordinates + positions = pd.read_csv( + files['tissue_positions_file'], + header=0 if tissue_positions_file.name == "tissue_positions.csv" else None, + index_col=0, + ) + positions.columns = [ + 'in_tissue', + 'array_row', + 'array_col', + 'pxl_col_in_fullres', + 'pxl_row_in_fullres', + ] - # Read coordinates - positions = pd.read_csv(files["tissue_positions_file"], index_col="barcode", dtype={"in_tissue": bool}) adata.obs = adata.obs.join(positions, how="left") - adata.obsm["spatial"] = adata.obs[["pxl_col_in_fullres", "pxl_row_in_fullres"]].to_numpy() + + adata.obsm['spatial'] = adata.obs[ + ['pxl_row_in_fullres', 'pxl_col_in_fullres'] + ].to_numpy() adata.obs.drop( - columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], + columns=['pxl_row_in_fullres', 'pxl_col_in_fullres'], inplace=True, ) + # put image path in uns + if source_image_path is not None: + # get an absolute path + source_image_path = str(Path(source_image_path).resolve()) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + source_image_path + ) + return adata @@ -125,9 +187,14 @@ def read_visium_mtx( ) parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") args = parser.parse_args() - + # Read Visium data - st_adata = read_visium_mtx(args.SRCountDir, library_id=None, load_images=True) + st_adata = read_visium( + args.SRCountDir, + count_file='raw_feature_bc_matrix.h5', + library_id=None, + load_images=True + ) # Write raw anndata to file st_adata.write(args.outAnnData) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index eac6100..eeace75 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -23,10 +23,17 @@ process ST_READ_DATA { script: """ + mkdir "${meta.id}/spatial" + mv "${meta.id}/scalefactors_json.json" \\ + "${meta.id}/tissue_hires_image.png" \\ + "${meta.id}/tissue_lowres_image.png" \\ + "${meta.id}/tissue_positions.csv" \\ + "${meta.id}/spatial/" + read_st_data.py \\ --SRCountDir "${meta.id}" \\ --outAnnData st_adata_raw.h5ad - + cat <<-END_VERSIONS > versions.yml "${task.process}": scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") From 5ac5ee2c70bcbb55e2bf4db893b6eb2675ba5b55 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Tue, 28 Nov 2023 20:12:23 +0100 Subject: [PATCH 189/410] Add quality control and filtering reports --- bin/st_qc_and_normalisation.qmd | 210 +++++++++++++++++++++++++------- 1 file changed, 169 insertions(+), 41 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 853c190..d4e844e 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -1,5 +1,5 @@ --- -title: "Pre-processing and filtering of Spatial Transcriptomics data" +title: "Pre-processing and quality control of Spatial Transcriptomics data" format: html: embed-resources: true @@ -8,6 +8,13 @@ format: jupyter: python3 --- +## Introduction + +Spatial Transcriptomics data analysis involves several steps, including quality control (QC) and pre-processing, to ensure the reliability of downstream analyses. This report outlines the QC and pre-processing steps for Visium Spatial Transcriptomics data using the (https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html)[anndata format]. + +Quality control is an essential step in spatial transcriptomics to identify and filter out spots and genes that may introduce noise or bias into the analysis. + + ```{python} #| tags: [parameters] #| echo: false @@ -16,30 +23,23 @@ fileNameST = None resolution = 1 saveFileST = None -rawAdata = None #Name of the h5ad file -pltFigSize = 6 #Figure size -minCounts = 500 #Min counts per spot -minGenes = 250 #Min genes per spot -minCells = 1 #Min cells per gene -histplotQCmaxTotalCounts = 10000 #Max total counts -histplotQCminGeneCounts = 4000 #Min gene counts -histplotQCbins = 40 #Number of bins -nameDataPlain = "st_adata_plain.h5ad" #Name of the raw data save file -nameDataNorm = "st_adata_norm.h5ad" #Name of the normalized data save file +rawAdata = None # Name of the h5ad file +pltFigSize = 6 # Figure size +minCounts = 500 # Min counts per spot +minGenes = 250 # Min genes per spot +minCells = 1 # Min cells per gene +histplotQCmaxTotalCounts = 10000 # Max total counts +histplotQCminGeneCounts = 4000 # Min gene counts +histplotQCbins = 40 # Number of bins +nameDataPlain = "st_adata_plain.h5ad" # Name of the raw data save file +nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` ```{python} -#| echo: false +###| echo: false # Hide warnings in output html. import scanpy as sc -import warnings -warnings.filterwarnings("ignore") -sc.settings.verbosity = 0 -``` - -Importing modules -```{python} -import scanpy as sc +import scipy import pandas as pd import matplotlib.pyplot as plt import seaborn as sns @@ -47,66 +47,194 @@ import seaborn as sns plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) ``` +## Anndata object + +The anndata format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and additional metadata. The anndata format ensures compatibility with various analysis tools and facilitates seamless integration into existing workflows. + ```{python} st_adata = sc.read("./" + rawAdata) +# Convert X matrix from csr to csc dense matrix for output compatibility: +st_adata.X = scipy.sparse.csc_matrix(st_adata.X) + # Get mitochondrial percentages st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') -sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt"], inplace=True) -``` +st_adata.var['hb'] = st_adata.var_names.str.contains(("^Hb.*-")) -This is how the adata structure looks like for Visium data +sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "hb"], inplace=True) -```{python} +print ("Content of the anndata object:") st_adata ``` -## QC and preprocessing +## Quality Control -We perform some basic filtering of spots based on total counts and expressed genes +The following plots show the distribution of the number of genes per counts and counts per spot, as well as the percentage of counts from mitochondrial genes and hemoglobin genes. ```{python} -fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -p = sns.distplot(st_adata.obs["total_counts"], kde=True, ax=axs[0, 0]) -p = sns.distplot(st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1]) -p = sns.distplot(st_adata.obs["n_genes_by_counts"], kde=True, bins=histplotQCbins, ax=axs[1, 0]) -p = sns.distplot(st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], kde=True, bins=histplotQCbins, ax=axs[1, 1]) +sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) +sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` +The same can be plotted on top of the tissue: + +```{python} +sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) +sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_hb"]) +``` + +## Filtering + +### In-tissue Filtering + +Spots outside the tissue are removed: + ```{python} +# Create a string observation "obs/in_tissue_str" with "In tissue" and "Outside tissue": +st_adata.obs["in_tissue_str"] = ["In tissue" if x == 1 else "Outside tissue" for x in st_adata.obs["in_tissue"]] + +sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue") +del st_adata.obs["in_tissue_str"] + # Remove spots outside tissue +Number_spots = st_adata.shape[0] st_adata = st_adata[st_adata.obs["in_tissue"] == 1] +Number_spots_in_tissue = st_adata.shape[0] +print (f"{Number_spots_in_tissue} spots in tissue on {Number_spots} spots in total.") +``` +Distribution of counts per spots and genes per counts. + +```{python} +fig, axs = plt.subplots(2, 2, figsize=(8, 7)) +p = sns.histplot( + st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] +).set(title=f"Total counts") +p = sns.histplot( + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + kde=True, + bins=histplotQCbins, + ax=axs[0, 1] +).set(title=f"Total counts < {histplotQCmaxTotalCounts}") +p = sns.histplot( + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, + ax=axs[1, 0] +).set(title=f"Genes by counts") +p = sns.histplot( + st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], + kde=True, + bins=histplotQCbins, + ax=axs[1, 1] +).set(title=f"Genes by counts < {histplotQCminGeneCounts}") +fig.tight_layout() +``` + +_Top row: Distribution of the number of counts per spots._ +_Bottom row: Distribution of the number of genes with at least 1 count in a spot._ + +### Cell and Gene Filtering + +We filter cells based on minimum counts and genes, and filter genes based on minimum cells. + +```{python} +# Filter cells based on counts +Number_spots = st_adata.shape[0] +Number_genes = st_adata.shape[1] sc.pp.filter_cells(st_adata, min_counts=minCounts) -#sc.pp.filter_cells(st_adata, max_counts=maxCounts) +Number_spots_filtered_minCounts = st_adata.shape[0] +print (f"Removed {Number_spots - Number_spots_filtered_minCounts} spots with less than {minCounts} total counts.") + +# Filter cells based on genes sc.pp.filter_cells(st_adata, min_genes=minGenes) -#st_adata = st_adata[st_adata.obs["pct_counts_mt"] < 20] -print(f"#cells after MT filter: {st_adata.n_obs}") +Number_spots_filtered_minGenes = st_adata.shape[0] +print (f"Removed {Number_spots_filtered_minCounts - Number_spots_filtered_minGenes} spots with less than {minGenes} genes expressed.") + +# Filter genes based on cells sc.pp.filter_genes(st_adata, min_cells=minCells) -print("Filtered out spots outside tissue:", st_adata.shape) +Number_genes_filtered_minCells = st_adata.shape[1] +print (f"Removed {Number_genes - Number_genes_filtered_minCells} genes expressed in less than {minCells} spots.") + +print (f"{Number_spots_filtered_minGenes} out of {Number_spots} spots remaining after filtering.") +print (f"{Number_genes_filtered_minCells} out of {Number_genes} genes remaining after filtering.") ``` -Distribution after filtering: +Distributions after filtering: + ```{python} fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -p = sns.distplot(st_adata.obs["total_counts"], kde=True, ax=axs[0, 0]) -p = sns.distplot(st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1]) -p = sns.distplot(st_adata.obs["n_genes_by_counts"], kde=True, bins=histplotQCbins, ax=axs[1, 0]) -p = sns.distplot(st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], kde=True, bins=histplotQCbins, ax=axs[1, 1]) +p = sns.histplot( + st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] +).set(title=f"Total counts") +p = sns.histplot( + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + kde=True, + bins=histplotQCbins, + ax=axs[0, 1] +).set(title=f"Total counts < {histplotQCmaxTotalCounts}") +p = sns.histplot( + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, + ax=axs[1, 0] +).set(title=f"Genes by counts") +p = sns.histplot( + st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], + kde=True, + bins=histplotQCbins, + ax=axs[1, 1] +).set(title=f"Genes by counts < {histplotQCminGeneCounts}") +fig.tight_layout() ``` -We proceed to normalize Visium counts data with the built-in `normalize_total` method from Scanpy, and detect highly-variable genes (for later). Note that there are alternatives for normalization (see discussion in [[Luecken19](https://www.embopress.org/doi/full/10.15252/msb.20188746)], and more recent alternatives such as [SCTransform](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1874-1) or [GLM-PCA](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1861-6)). +_Top row: Distribution of the number of counts per spots._ +_Bottom row: Distribution of the number of genes with at least 1 count in a spot._ ```{python} +# Write pre-processed data to files st_adata.write(nameDataPlain) +print (f"Non-normalized Anndata object saved to data/{nameDataPlain}.") ``` +### Normalization + +We proceed to normalize Visium counts data using the built-in `normalize_total` method from Scanpy followed by a log-transformation. + ```{python} sc.pp.normalize_total(st_adata, inplace=True) sc.pp.log1p(st_adata) +``` + +### Top expressed genes + +```{python} +sc.pl.highest_expr_genes(st_adata, n_top=20) +``` + +### Highly variable genes + +Highly variable genes are detected for subsequent analysis. + +```{python} sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) +var_genes_all = st_adata.var.highly_variable +print("Extracted highly variable genes: %d"%sum(var_genes_all)) ``` +Plot of highly variable genes dispersion: + +```{python} +# plot the table of highly variable genes +sc.pl.highly_variable_genes(st_adata) +``` + +### Anndata export + +All anndata files saved can be found in the `data` directory. + ```{python} +# Write normalized data to files st_adata.write(nameDataNorm) +print (f"Normalized Anndata object saved to data/{nameDataPlain}.") ``` From 7e8638e39703372fd1e998cede8ea495b5b9fd84 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 11:10:58 +0100 Subject: [PATCH 190/410] Update st_clustering report --- bin/st_clustering.qmd | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index d552a4b..cd64f70 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -5,6 +5,8 @@ format: embed-resources: true page-layout: full code-fold: true +execute: + keep-ipynb: true jupyter: python3 --- @@ -21,34 +23,32 @@ saveFileST = None #| echo: false # Hide warnings in output html. import scanpy as sc -import warnings -warnings.filterwarnings("ignore") -sc.settings.verbosity = 0 -``` - -```{python} -# Load packages -import scanpy as sc import numpy as np import pandas as pd from umap import UMAP from matplotlib import pyplot as plt import seaborn as sns +import warnings +warnings.filterwarnings("ignore") +sc.settings.verbosity = 0 sc.set_figure_params(dpi_save=300, facecolor="white") ``` + ## Reading the data -The data has already been filtered and is saved in AnnData format. +The data has already been filtered (see `st_qc_and_normalisation` report) and is saved in AnnData format. + ```{python} #| echo: true #| code-fold: false st_adata = sc.read("./" + fileNameST) +print (f"Loading {fileNameST}:") st_adata ``` ## Manifold embedding and clustering based on transcriptional similarity -We embed and cluster the manifold encoded by transcriptional similarity, using Leiden clustering [REF]. +To uncover the underlying structure of the transcriptional landscape, we perform manifold embedding and clustering based on transcriptional similarity. Principal Component Analysis (PCA) is applied to reduce dimensionality, and UMAP (Uniform Manifold Approximation and Projection) is used for visualization. The Leiden algorithm is employed for clustering with a specified resolution of {{< var resolution >}}. ```{python} sc.pp.pca(st_adata) @@ -57,7 +57,9 @@ sc.tl.umap(st_adata) sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) ``` -We plot some covariates to check if there is any particular structure in the UMAP associated with total counts and detected genes. +## Exploratory Data Analysis + +We generate UMAP plots to visualize the spatial distribution of clusters and investigate potential associations with total counts and detected genes. ```{python} # Make plots of UMAP of ST spots clusters @@ -70,7 +72,8 @@ sc.pl.embedding_density(st_adata, groupby="clusters", ncols=4) ``` ## Visualization in spatial coordinates -We now take a look at how `total_counts` and `n_genes_by_counts` behave in spatial coordinates. We will overlay the circular spots on top of the Hematoxylin and eosin stain (H&E) image provided. + +Next, we examine how total counts and the number of detected genes behave in spatial coordinates. We overlay the circular spots on the Hematoxylin and Eosin stain (H&E) image to provide a spatial context. ```{python} plt.rcParams["figure.figsize"] = (10, 10) @@ -79,7 +82,9 @@ sc.pl.spatial( ) ``` -Before, we performed clustering in gene expression space, and visualized the results with UMAP. By visualizing clustered samples in spatial dimensions, we can gain insights into tissue organization and, potentially, into inter-cellular communication. +## Spatial Clustering Visualization + +To gain insights into tissue organization and potential inter-cellular communication, we visualize the spatial distribution of clusters on the H&E image. ```{python} plt.rcParams["figure.figsize"] = (10, 10) @@ -88,7 +93,7 @@ sc.pl.spatial( ) ``` -Spots belonging to the same cluster in gene expression space often co-occur in spatial dimensions. +Spots belonging to the same cluster in gene expression space often co-occur in spatial dimensions, providing valuable information about the spatial organization of cells. ```{python} #| echo: false @@ -96,8 +101,12 @@ Spots belonging to the same cluster in gene expression space often co-occur in s st_adata.uns['log1p']['base'] = None ``` -## Saving anndata file for future use. +## Data Saving + +To facilitate future analyses and reproducibility, the processed AnnData object is saved in the `data` directory. + ```{python} if saveFileST is not None: st_adata.write(saveFileST) + print (f"Anndata object saved to data/{saveFileST}.") ``` From 66e17f2e55ea740acee88e0c8d16a110fc7ecd1f Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 11:12:37 +0100 Subject: [PATCH 191/410] Add analysis options --- docs/usage.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 214443f..e976d3a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -106,9 +106,27 @@ default human reference for you automatically. ## Analysis options - +The pipeline is using Python and the scverse tools to do the downstream analysis (quality control, filtering, clustering, spatial differential equations). -[WIP] +### Parameters for Quality Control and Filtering: + +The following parameters are exposed for preprocessing: + + - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. + - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. + - `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. + - `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (e.g., quality control plots). + - `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. + - `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. + - `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. + +### Parameters for Clustering : + + - `--st_cluster_resolution`: Resolution parameter for the clustering algorithm, controlling granularity. + +### Parameters for Spatial Differential Expression : + + - `st_spatial_de_ncols`: Number of columns in the output figure. ## Running the pipeline From caea8115acd711506c6d8eba2320b19f16fc86d6 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 11:12:50 +0100 Subject: [PATCH 192/410] Update scanpy to 1.9.3 --- env/reports/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/reports/environment.yml b/env/reports/environment.yml index 0c52f97..f8f1923 100644 --- a/env/reports/environment.yml +++ b/env/reports/environment.yml @@ -6,6 +6,6 @@ dependencies: - leidenalg=0.9.1 - papermill=2.3.4 - pip=23.0.1 - - scanpy=1.9.3 + - scanpy=1.9.6 - pip: - SpatialDE==1.1.3 From 4822d1ddd9e965032a934c0c2edc7b6cd8e370dd Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 11:13:13 +0100 Subject: [PATCH 193/410] Update st_spatial_de report todos --- bin/st_spatial_de.qmd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 390c250..f1f3265 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -1,3 +1,7 @@ + + --- title: "Differential Gene Expression and spatially variable genes" format: From 8d601e5f98f6d73ed76938deed334e6aa46820ea Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 29 Nov 2023 11:18:11 +0100 Subject: [PATCH 194/410] Update nf-core modules --- modules.json | 10 +- .../dumpsoftwareversions/environment.yml | 7 + .../custom/dumpsoftwareversions/main.nf | 6 +- .../custom/dumpsoftwareversions/meta.yml | 7 +- .../dumpsoftwareversions/tests/main.nf.test | 38 ++ .../tests/main.nf.test.snap | 27 + .../dumpsoftwareversions/tests/tags.yml | 2 + modules/nf-core/fastqc/environment.yml | 7 + modules/nf-core/fastqc/main.nf | 6 +- modules/nf-core/fastqc/meta.yml | 5 + modules/nf-core/fastqc/tests/main.nf.test | 41 ++ .../nf-core/fastqc/tests/main.nf.test.snap | 10 + modules/nf-core/fastqc/tests/tags.yml | 2 + modules/nf-core/multiqc/environment.yml | 7 + modules/nf-core/multiqc/main.nf | 8 +- modules/nf-core/multiqc/meta.yml | 11 +- modules/nf-core/multiqc/tests/main.nf.test | 91 ++++ modules/nf-core/multiqc/tests/tags.yml | 2 + .../nf-core/spaceranger/count/environment.yml | 5 + modules/nf-core/spaceranger/count/main.nf | 14 +- modules/nf-core/spaceranger/count/meta.yml | 5 +- modules/nf-core/untar/environment.yml | 9 + modules/nf-core/untar/main.nf | 2 +- modules/nf-core/untar/meta.yml | 5 + modules/nf-core/untar/tests/main.nf.test | 77 +++ modules/nf-core/untar/tests/main.nf.test.snap | 513 ++++++++++++++++++ modules/nf-core/untar/tests/tags.yml | 2 + 27 files changed, 888 insertions(+), 31 deletions(-) create mode 100644 modules/nf-core/custom/dumpsoftwareversions/environment.yml create mode 100644 modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test create mode 100644 modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap create mode 100644 modules/nf-core/custom/dumpsoftwareversions/tests/tags.yml create mode 100644 modules/nf-core/fastqc/environment.yml create mode 100644 modules/nf-core/fastqc/tests/main.nf.test create mode 100644 modules/nf-core/fastqc/tests/main.nf.test.snap create mode 100644 modules/nf-core/fastqc/tests/tags.yml create mode 100644 modules/nf-core/multiqc/environment.yml create mode 100644 modules/nf-core/multiqc/tests/main.nf.test create mode 100644 modules/nf-core/multiqc/tests/tags.yml create mode 100644 modules/nf-core/spaceranger/count/environment.yml create mode 100644 modules/nf-core/untar/environment.yml create mode 100644 modules/nf-core/untar/tests/main.nf.test create mode 100644 modules/nf-core/untar/tests/main.nf.test.snap create mode 100644 modules/nf-core/untar/tests/tags.yml diff --git a/modules.json b/modules.json index 127ce48..6810b8b 100644 --- a/modules.json +++ b/modules.json @@ -7,27 +7,27 @@ "nf-core": { "custom/dumpsoftwareversions": { "branch": "master", - "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", + "git_sha": "bba7e362e4afead70653f84d8700588ea28d0f9e", "installed_by": ["modules"] }, "fastqc": { "branch": "master", - "git_sha": "bd8092b67b5103bdd52e300f75889442275c3117", + "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", "installed_by": ["modules"] }, "multiqc": { "branch": "master", - "git_sha": "911696ea0b62df80e900ef244d7867d177971f73", + "git_sha": "1537442a7be4a78efa3d1ff700a923c627bbda5d", "installed_by": ["modules"] }, "spaceranger/count": { "branch": "master", - "git_sha": "84bd059d24c74c44e31f98924413d2c7f04dd6c6", + "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", "installed_by": ["modules"] }, "untar": { "branch": "master", - "git_sha": "5c460c5a4736974abde2843294f35307ee2b0e5e", + "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", "installed_by": ["modules"] } } diff --git a/modules/nf-core/custom/dumpsoftwareversions/environment.yml b/modules/nf-core/custom/dumpsoftwareversions/environment.yml new file mode 100644 index 0000000..f0c63f6 --- /dev/null +++ b/modules/nf-core/custom/dumpsoftwareversions/environment.yml @@ -0,0 +1,7 @@ +name: custom_dumpsoftwareversions +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - bioconda::multiqc=1.17 diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index ebc8727..7685b33 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -2,10 +2,10 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { label 'process_single' // Requires `pyyaml` which does not have a dedicated container but is in the MultiQC container - conda "bioconda::multiqc=1.14" + conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : - 'biocontainers/multiqc:1.14--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.17--pyhdfd78af_0' : + 'biocontainers/multiqc:1.17--pyhdfd78af_0' }" input: path versions diff --git a/modules/nf-core/custom/dumpsoftwareversions/meta.yml b/modules/nf-core/custom/dumpsoftwareversions/meta.yml index c32657d..5f15a5f 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/meta.yml +++ b/modules/nf-core/custom/dumpsoftwareversions/meta.yml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: custom_dumpsoftwareversions description: Custom module used to dump software versions within the nf-core pipeline template keywords: @@ -16,7 +16,6 @@ input: type: file description: YML file containing software versions pattern: "*.yml" - output: - yml: type: file @@ -30,7 +29,9 @@ output: type: file description: File containing software versions pattern: "versions.yml" - authors: - "@drpatelh" - "@grst" +maintainers: + - "@drpatelh" + - "@grst" diff --git a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test new file mode 100644 index 0000000..eec1db1 --- /dev/null +++ b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test @@ -0,0 +1,38 @@ +nextflow_process { + + name "Test Process CUSTOM_DUMPSOFTWAREVERSIONS" + script "../main.nf" + process "CUSTOM_DUMPSOFTWAREVERSIONS" + tag "modules" + tag "modules_nfcore" + tag "custom" + tag "dumpsoftwareversions" + tag "custom/dumpsoftwareversions" + + test("Should run without failures") { + when { + process { + """ + def tool1_version = ''' + TOOL1: + tool1: 0.11.9 + '''.stripIndent() + + def tool2_version = ''' + TOOL2: + tool2: 1.9 + '''.stripIndent() + + input[0] = Channel.of(tool1_version, tool2_version).collectFile() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap new file mode 100644 index 0000000..4274ed5 --- /dev/null +++ b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap @@ -0,0 +1,27 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + "software_versions.yml:md5,1c851188476409cda5752ce971b20b58" + ], + "1": [ + "software_versions_mqc.yml:md5,2570f4ba271ad08357b0d3d32a9cf84d" + ], + "2": [ + "versions.yml:md5,3843ac526e762117eedf8825b40683df" + ], + "mqc_yml": [ + "software_versions_mqc.yml:md5,2570f4ba271ad08357b0d3d32a9cf84d" + ], + "versions": [ + "versions.yml:md5,3843ac526e762117eedf8825b40683df" + ], + "yml": [ + "software_versions.yml:md5,1c851188476409cda5752ce971b20b58" + ] + } + ], + "timestamp": "2023-11-03T14:43:22.157011" + } +} diff --git a/modules/nf-core/custom/dumpsoftwareversions/tests/tags.yml b/modules/nf-core/custom/dumpsoftwareversions/tests/tags.yml new file mode 100644 index 0000000..405aa24 --- /dev/null +++ b/modules/nf-core/custom/dumpsoftwareversions/tests/tags.yml @@ -0,0 +1,2 @@ +custom/dumpsoftwareversions: + - modules/nf-core/custom/dumpsoftwareversions/** diff --git a/modules/nf-core/fastqc/environment.yml b/modules/nf-core/fastqc/environment.yml new file mode 100644 index 0000000..1787b38 --- /dev/null +++ b/modules/nf-core/fastqc/environment.yml @@ -0,0 +1,7 @@ +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 249f906..50e59f2 100644 --- a/modules/nf-core/fastqc/main.nf +++ b/modules/nf-core/fastqc/main.nf @@ -2,10 +2,10 @@ process FASTQC { tag "$meta.id" label 'process_medium' - conda "bioconda::fastqc=0.11.9" + conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/fastqc:0.11.9--0' : - 'biocontainers/fastqc:0.11.9--0' }" + 'https://depot.galaxyproject.org/singularity/fastqc:0.12.1--hdfd78af_0' : + 'biocontainers/fastqc:0.12.1--hdfd78af_0' }" input: tuple val(meta), path(reads) diff --git a/modules/nf-core/fastqc/meta.yml b/modules/nf-core/fastqc/meta.yml index 4da5bb5..ee5507e 100644 --- a/modules/nf-core/fastqc/meta.yml +++ b/modules/nf-core/fastqc/meta.yml @@ -50,3 +50,8 @@ authors: - "@grst" - "@ewels" - "@FelixKrueger" +maintainers: + - "@drpatelh" + - "@grst" + - "@ewels" + - "@FelixKrueger" diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test new file mode 100644 index 0000000..6437a14 --- /dev/null +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -0,0 +1,41 @@ +nextflow_process { + + name "Test Process FASTQC" + script "../main.nf" + process "FASTQC" + tag "modules" + tag "modules_nfcore" + tag "fastqc" + + test("Single-Read") { + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = [ + [ id: 'test', single_end:true ], + [ + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) + ] + ] + """ + } + } + + 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.get(0).get(1) ==~ ".*/test_fastqc.html" }, + { assert path(process.out.html.get(0).get(1)).getText().contains("File typeConventional base calls") }, + { assert snapshot(process.out.versions).match("versions") }, + { assert process.out.zip.get(0).get(1) ==~ ".*/test_fastqc.zip" } + ) + } + } +} diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap new file mode 100644 index 0000000..636a32c --- /dev/null +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -0,0 +1,10 @@ +{ + "versions": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "timestamp": "2023-10-09T23:40:54+0000" + } +} \ No newline at end of file diff --git a/modules/nf-core/fastqc/tests/tags.yml b/modules/nf-core/fastqc/tests/tags.yml new file mode 100644 index 0000000..7834294 --- /dev/null +++ b/modules/nf-core/fastqc/tests/tags.yml @@ -0,0 +1,2 @@ +fastqc: + - modules/nf-core/fastqc/** diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml new file mode 100644 index 0000000..bc0bdb5 --- /dev/null +++ b/modules/nf-core/multiqc/environment.yml @@ -0,0 +1,7 @@ +name: multiqc +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - bioconda::multiqc=1.18 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 1fc387b..00cc48d 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -1,10 +1,10 @@ process MULTIQC { label 'process_single' - conda "bioconda::multiqc=1.14" + conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.14--pyhdfd78af_0' : - 'biocontainers/multiqc:1.14--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.18--pyhdfd78af_0' : + 'biocontainers/multiqc:1.18--pyhdfd78af_0' }" input: path multiqc_files, stageAs: "?/*" @@ -25,12 +25,14 @@ process MULTIQC { def args = task.ext.args ?: '' 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}"'/ : '' """ multiqc \\ --force \\ $args \\ $config \\ $extra_config \\ + $logo \\ . cat <<-END_VERSIONS > versions.yml diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index f93b5ee..f1aa660 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -1,5 +1,5 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json -name: MultiQC +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: multiqc description: Aggregate results from bioinformatics analyses across many samples into a single report keywords: - QC @@ -13,7 +13,6 @@ tools: homepage: https://multiqc.info/ documentation: https://multiqc.info/docs/ licence: ["GPL-3.0-or-later"] - input: - multiqc_files: type: file @@ -31,7 +30,6 @@ input: type: file description: Optional logo file for MultiQC pattern: "*.{png}" - output: - report: type: file @@ -54,3 +52,8 @@ authors: - "@bunop" - "@drpatelh" - "@jfy133" +maintainers: + - "@abhi18av" + - "@bunop" + - "@drpatelh" + - "@jfy133" diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test new file mode 100644 index 0000000..68fffa9 --- /dev/null +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -0,0 +1,91 @@ +nextflow_process { + + name "Test Process MULTIQC" + script "../main.nf" + process "MULTIQC" + tag "modules" + tag "modules_nfcore" + tag "multiqc" + + test("MULTIQC: FASTQC") { + + setup { + run("FASTQC") { + script "../../fastqc/main.nf" + process { + """ + input[0] = Channel.of([ + [ id: 'test', single_end: false ], + [ file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true)] + ]) + """ + } + } + } + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = FASTQC.out.zip.collect { it[1] } + input[1] = [] + input[2] = [] + input[3] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert path(process.out.report.get(0)).exists() }, + { assert path(process.out.data.get(0)).exists() }, + { assert path(process.out.versions.get(0)).getText().contains("multiqc") } + ) + } + + } + + test("MULTIQC: FASTQC and a config file") { + + setup { + run("FASTQC") { + script "../../fastqc/main.nf" + process { + """ + input[0] = Channel.of([ + [ id: 'test', single_end: false ], + [ file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true)] + ]) + """ + } + } + } + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = FASTQC.out.zip.collect { it[1] } + 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] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert path(process.out.report.get(0)).exists() }, + { assert path(process.out.data.get(0)).exists() }, + { assert path(process.out.versions.get(0)).getText().contains("multiqc") } + ) + } + + } +} diff --git a/modules/nf-core/multiqc/tests/tags.yml b/modules/nf-core/multiqc/tests/tags.yml new file mode 100644 index 0000000..bea6c0d --- /dev/null +++ b/modules/nf-core/multiqc/tests/tags.yml @@ -0,0 +1,2 @@ +multiqc: + - modules/nf-core/multiqc/** diff --git a/modules/nf-core/spaceranger/count/environment.yml b/modules/nf-core/spaceranger/count/environment.yml new file mode 100644 index 0000000..a5049bf --- /dev/null +++ b/modules/nf-core/spaceranger/count/environment.yml @@ -0,0 +1,5 @@ +name: spaceranger_count +channels: + - conda-forge + - bioconda + - defaults diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index 43f3bee..c0f10e5 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -4,12 +4,6 @@ process SPACERANGER_COUNT { container "docker.io/nfcore/spaceranger:2.1.0" - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - exit 1, "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." - } - - input: tuple val(meta), path(reads), path(image), path(cytaimage), path(darkimage), path(colorizedimage), path(alignment), path(slidefile) path(reference) @@ -23,6 +17,10 @@ process SPACERANGER_COUNT { 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 "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." + } def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" // Add flags for optional inputs on demand. @@ -56,6 +54,10 @@ process SPACERANGER_COUNT { """ stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." + } def prefix = task.ext.prefix ?: "${meta.id}" """ mkdir -p "${prefix}/outs/" diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml index 7ff599f..5a5073b 100644 --- a/modules/nf-core/spaceranger/count/meta.yml +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -21,7 +21,6 @@ tools: documentation: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" tool_dev_url: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" licence: "10x Genomics EULA" - input: - meta: type: map @@ -75,7 +74,6 @@ input: type: file description: OPTIONAL - Probe set specification. pattern: "*.csv" - output: - meta: type: map @@ -90,6 +88,7 @@ output: type: file description: File containing software versions pattern: "versions.yml" - authors: - "@grst" +maintainers: + - "@grst" diff --git a/modules/nf-core/untar/environment.yml b/modules/nf-core/untar/environment.yml new file mode 100644 index 0000000..d6917da --- /dev/null +++ b/modules/nf-core/untar/environment.yml @@ -0,0 +1,9 @@ +name: untar +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - conda-forge::sed=4.7 + - conda-forge::grep=3.11 + - conda-forge::tar=1.34 diff --git a/modules/nf-core/untar/main.nf b/modules/nf-core/untar/main.nf index 8cd1856..8a75bb9 100644 --- a/modules/nf-core/untar/main.nf +++ b/modules/nf-core/untar/main.nf @@ -2,7 +2,7 @@ process UNTAR { tag "$archive" label 'process_single' - conda "conda-forge::sed=4.7 bioconda::grep=3.4 conda-forge::tar=1.34" + conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/ubuntu:20.04' : 'nf-core/ubuntu:20.04' }" diff --git a/modules/nf-core/untar/meta.yml b/modules/nf-core/untar/meta.yml index db241a6..a9a2110 100644 --- a/modules/nf-core/untar/meta.yml +++ b/modules/nf-core/untar/meta.yml @@ -39,3 +39,8 @@ authors: - "@drpatelh" - "@matthdsm" - "@jfy133" +maintainers: + - "@joseespinosa" + - "@drpatelh" + - "@matthdsm" + - "@jfy133" diff --git a/modules/nf-core/untar/tests/main.nf.test b/modules/nf-core/untar/tests/main.nf.test new file mode 100644 index 0000000..d40db13 --- /dev/null +++ b/modules/nf-core/untar/tests/main.nf.test @@ -0,0 +1,77 @@ +nextflow_process { + + name "Test Process UNTAR" + script "../main.nf" + process "UNTAR" + + tag "modules" + tag "modules_nfcore" + tag "untar" + + test("test_untar") { + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = [ [], file(params.test_data['sarscov2']['genome']['kraken2_tar_gz'], checkIfExists: true) ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.untar).match("test_untar") }, + ) + } + + } + + test("test_untar_different_output_path") { + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = [ [], file(params.test_data['homo_sapiens']['illumina']['test_flowcell'], checkIfExists: true) ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.untar).match("test_untar_different_output_path") }, + ) + } + + } + + test("test_untar_onlyfiles") { + + when { + params { + outdir = "$outputDir" + } + process { + """ + input[0] = [ [], file(params.test_data['generic']['tar']['tar_gz'], checkIfExists: true) ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.untar).match("test_untar_onlyfiles") }, + ) + } + + } + +} diff --git a/modules/nf-core/untar/tests/main.nf.test.snap b/modules/nf-core/untar/tests/main.nf.test.snap new file mode 100644 index 0000000..146c867 --- /dev/null +++ b/modules/nf-core/untar/tests/main.nf.test.snap @@ -0,0 +1,513 @@ +{ + "test_untar_different_output_path": { + "content": [ + [ + [ + [ + + ], + [ + [ + [ + [ + [ + [ + "s_1_1101.bcl:md5,ad01889e2ff43e2f194224e20bdb600c", + "s_1_1101.stats:md5,4bbbf103454b37fbc3138fadf1b4446b" + ], + [ + "s_1_1101.bcl:md5,565384bbe67a694dfd690bae6d1d30c2", + "s_1_1101.stats:md5,55e5abd8f129ff38ef169873547abdb8" + ], + [ + "s_1_1101.bcl:md5,650fa58a630a9148835ba79e323d4237", + "s_1_1101.stats:md5,77403669ca1b05340c390dff64425c1e" + ], + [ + "s_1_1101.bcl:md5,54471c9e97299cd141e202e204637702", + "s_1_1101.stats:md5,67b14c9a89b7f8556674a7524d5cfb2d" + ], + [ + "s_1_1101.bcl:md5,74e4f929fc7476c380fd9d741ddb6700", + "s_1_1101.stats:md5,5730a4c35463eaa12a06b6758710b98c" + ], + [ + "s_1_1101.bcl:md5,c785f472f4350c120c02c888c8189590", + "s_1_1101.stats:md5,fee4ec63895ea81007e06ee6a36ba5e0" + ], + [ + "s_1_1101.bcl:md5,b7ea50bb25f08d43c301741d77050a9b", + "s_1_1101.stats:md5,fa7c68f3122c74d14364e6f7b011af70" + ], + [ + "s_1_1101.bcl:md5,9d5087dc4bcae39d66486363d4f68ecf", + "s_1_1101.stats:md5,23cdceee4d82c4b8e7c60018b9276ace" + ], + [ + "s_1_1101.bcl:md5,581e0c5ee94e8f2de14b2b1d8e777530", + "s_1_1101.stats:md5,9a3536d573c97f66bb56b49463612607" + ], + [ + "s_1_1101.bcl:md5,296fc026bb34c67bbe2b44845fe0d1de", + "s_1_1101.stats:md5,a7f57a7770fb9c5ae2a0fb1ef403ec4f" + ], + [ + "s_1_1101.bcl:md5,2a3ca15531556c36d10d132a9e051de8", + "s_1_1101.stats:md5,2d0bcdb0a1b51d3d79e415db2ab2d3b1" + ], + [ + "s_1_1101.bcl:md5,1150d46a2ccd4ac58aee0585d3e4ffd7", + "s_1_1101.stats:md5,2e97550bd5b5864ffd0565bb7a3f6d40" + ], + [ + "s_1_1101.bcl:md5,0b85c4b3da0de95e7b862d849c5333ae", + "s_1_1101.stats:md5,6eab9746fbeb783b0cd70398f44e0c1a" + ], + [ + "s_1_1101.bcl:md5,e0e9c91f4698804d7a6d1058ef68b34f", + "s_1_1101.stats:md5,790022cdc7878a02b2ebd166e1ddf0a7" + ], + [ + "s_1_1101.bcl:md5,38cd0ad4de359e651c8ac0d5777ea625", + "s_1_1101.stats:md5,a1b1d5ea5371d326abb029774483c5e6" + ], + [ + "s_1_1101.bcl:md5,b0ddc05c4012ccba24e712a1cfec748f", + "s_1_1101.stats:md5,af3d232f839d720f76f40ba06caa2987" + ], + [ + "s_1_1101.bcl:md5,af32fcc5dc3b836cf7a5ba3db85a75dd", + "s_1_1101.stats:md5,f93f2c09bd4e486c74a5f6e2040f7296" + ], + [ + "s_1_1101.bcl:md5,54b7428e037ca87816107647d4a3d9db", + "s_1_1101.stats:md5,e5ac77a72cd7bed5e9bf03cccda0e48c" + ], + [ + "s_1_1101.bcl:md5,fc8b4eacd493bf3d0b20bc23998dc7ff", + "s_1_1101.stats:md5,190315e159e2f4bc4c057ded7470dc52" + ], + [ + "s_1_1101.bcl:md5,9484ecffda489927fce424ac6a44fa9d", + "s_1_1101.stats:md5,0825feeb457ecc9efcf6f8526ba32311" + ], + [ + "s_1_1101.bcl:md5,eec59e21036e31c95ce1e847bfb0a9c4", + "s_1_1101.stats:md5,9acc13f63c98e5a8445e7be70d49222b" + ], + [ + "s_1_1101.bcl:md5,a9fb24476f87cba4fba68e2b3c3f2c07", + "s_1_1101.stats:md5,dc0aa7db9790733291c3e6480ca2a0fc" + ], + [ + "s_1_1101.bcl:md5,ed950b3e82c500927c2e236c9df005c6", + "s_1_1101.stats:md5,dccb71ec47d1f9d33a192da6d5660a45" + ], + [ + "s_1_1101.bcl:md5,b3e992025e995ca56b5ea2820144ef47", + "s_1_1101.stats:md5,a6a829bf2cffb26ac5d9dc3012057699" + ], + [ + "s_1_1101.bcl:md5,89edc726a5a4e0b4ff8ca3899ed0232b", + "s_1_1101.stats:md5,5b9b4fd8110577a59b82d0c419519d29" + ], + [ + "s_1_1101.bcl:md5,4dc696149169f232c451225f563cb5cd", + "s_1_1101.stats:md5,d3514a71ea3adc60e2943c6b8f6e2598" + ], + [ + "s_1_1101.bcl:md5,35b992d0318afb7c825ceaa31b0755e6", + "s_1_1101.stats:md5,2826093acc175c16c3795de7c4ca8f07" + ], + [ + "s_1_1101.bcl:md5,7bc927f56a362e49c00b5d76ee048901", + "s_1_1101.stats:md5,e47d862b795fd6b88a31d7d482ab22f6" + ], + [ + "s_1_1101.bcl:md5,84742233ff2a651626fe9036f27f7cb2", + "s_1_1101.stats:md5,b78fad11d3c50bc76b722cdc03e3028b" + ], + [ + "s_1_1101.bcl:md5,3935341c86263a7938e8c49620ef39f8", + "s_1_1101.stats:md5,cc6585b2daac5354073d150874da9704" + ], + [ + "s_1_1101.bcl:md5,3627f4fd548bf6e64aaf08fba3a342be", + "s_1_1101.stats:md5,120ae4831ae004ff7d16728aef36e82f" + ], + [ + "s_1_1101.bcl:md5,07631014bc35124149fabd80ef19f933", + "s_1_1101.stats:md5,eadd63d91f47cc6db6b6f0a967a23927" + ], + [ + "s_1_1101.bcl:md5,a1149c80415dc2f34d768eeb397c43fb", + "s_1_1101.stats:md5,ca89a9def67611a9151c6ce685b7cce1" + ], + [ + "s_1_1101.bcl:md5,eb5f71d4741d2f40618756bc72eaf8b4", + "s_1_1101.stats:md5,90f48501e735e5915b843478e23d1ae2" + ], + [ + "s_1_1101.bcl:md5,9bf270fe3f6add1a591ebc24fff10078", + "s_1_1101.stats:md5,a4e429671d4098034293c638aa655e16" + ], + [ + "s_1_1101.bcl:md5,219bedcbd24bae54fe4cf05dae05282c", + "s_1_1101.stats:md5,dd97525b65b68207137d51fcf19132c7" + ], + [ + "s_1_1101.bcl:md5,5163bc00a68fd57ae50cae0b76350892", + "s_1_1101.stats:md5,b606a5368eff1f012f3ea5d11ccdf2e0" + ], + [ + "s_1_1101.bcl:md5,fc429195a5af59a59e0cc4c48e6c05ea", + "s_1_1101.stats:md5,d809aa19698053f90d639da4dcad8008" + ], + [ + "s_1_1101.bcl:md5,383340219a1dd77076a092a64a71a7e4", + "s_1_1101.stats:md5,b204a5cf256378679ffc906c15cc1bae" + ], + [ + "s_1_1101.bcl:md5,0c369540d3e24696cf1f9c55bab69315", + "s_1_1101.stats:md5,a2bc69a4031a22ce9621dcc623a0bf4b" + ], + [ + "s_1_1101.bcl:md5,3127abc8016ba8eb954f8f8015dff387", + "s_1_1101.stats:md5,5deafff31150b7bf757f814e49a53bc2" + ], + [ + "s_1_1101.bcl:md5,045f40c82de676bafec3d59f91376a7a", + "s_1_1101.stats:md5,890700edc20687c090ef52248c7884b1" + ], + [ + "s_1_1101.bcl:md5,78af269aa2b39a1d765703f0a4739a86", + "s_1_1101.stats:md5,303cf457aa1543a8208544f694cbc531" + ], + [ + "s_1_1101.bcl:md5,0ab8c781959b783b62888e9274364a46", + "s_1_1101.stats:md5,2605b0e8322f83aa4d0dae5da4ec7a7a" + ], + [ + "s_1_1101.bcl:md5,d0cf823ffe352e8b3f75d589544ab617", + "s_1_1101.stats:md5,efa3c0e01e3db71e12fd961cb2d03739" + ], + [ + "s_1_1101.bcl:md5,db4ca4ab7a01e03c246f9160c3758d82", + "s_1_1101.stats:md5,f61550d9e4a90df6b860e68f41f82f60" + ], + [ + "s_1_1101.bcl:md5,1af39a2c7e5ff20ece91cb8160b51d17", + "s_1_1101.stats:md5,d0e20879afcaf6dfcd88c73f1c5c78cf" + ], + [ + "s_1_1101.bcl:md5,4cf7123bb0fffcd79266df03aef01665", + "s_1_1101.stats:md5,29bff4075109a121b087116b58d7e927" + ], + [ + "s_1_1101.bcl:md5,aa9980428cb60cd6320f4b48f4dd0d74", + "s_1_1101.stats:md5,6b0e20bde93133117a8d1a6df3d6f37b" + ], + [ + "s_1_1101.bcl:md5,0f6e440374e15b9b491d52fb83a8adfe", + "s_1_1101.stats:md5,55cb5eb0ecdabd23dca39ab8c4607598" + ], + [ + "s_1_1101.bcl:md5,2c645d7bdaddaa403f6e304d36df9e4b", + "s_1_1101.stats:md5,53acf33d21f832779b400c2447386ce4" + ], + [ + "s_1_1101.bcl:md5,3bbf0863b423b770c879203644420206", + "s_1_1101.stats:md5,579bdc7293cac8c3d7407249cacf4c25" + ], + [ + "s_1_1101.bcl:md5,6658a08409e81d29cfeb2d096b491985", + "s_1_1101.stats:md5,bb559ffbea46d612f9933cefa84c4c03" + ], + [ + "s_1_1101.bcl:md5,1700d9a13d3d4f7643af2943ef838acb", + "s_1_1101.stats:md5,f01cb6050ebfb15da1e0399ebd791eb4" + ], + [ + "s_1_1101.bcl:md5,1ac7aa9ffae25eb103f755f33e4a39c6", + "s_1_1101.stats:md5,0b9d45d7929ccf336d5e5b95373ed3c2" + ], + [ + "s_1_1101.bcl:md5,812a97af2e983a53226e18c75190b06c", + "s_1_1101.stats:md5,d2410c7b0e506dab2972e77e2398de1e" + ], + [ + "s_1_1101.bcl:md5,c981e8e4dcc434956c2b86159da268bc", + "s_1_1101.stats:md5,e9c826e85361ce673f1f248786c9a611" + ], + [ + "s_1_1101.bcl:md5,88e09e99a0a4ef3357b203a41b22f77c", + "s_1_1101.stats:md5,ef06f2e5ad667bbd383f9ed6a05b7b42" + ], + [ + "s_1_1101.bcl:md5,461c8b146fc8a7938be38689978ecd09", + "s_1_1101.stats:md5,65115693935da66f9791b27136e22fb0" + ], + [ + "s_1_1101.bcl:md5,c7b827df5ce20e0f21916fe60860ca3f", + "s_1_1101.stats:md5,87be73613aeb507847f94d3cac5bb30a" + ], + [ + "s_1_1101.bcl:md5,7c4cc3dc9c8a1b0f15917b282dfb40ce", + "s_1_1101.stats:md5,bdd9181fa89debbfafe7b6ea3e064065" + ], + [ + "s_1_1101.bcl:md5,19f4debaf91e118aca8934517179ac33", + "s_1_1101.stats:md5,1143082719e136241d21b14a6b19b8a2" + ], + [ + "s_1_1101.bcl:md5,38aa256ad2d697d84b0b2c0e876a3eba", + "s_1_1101.stats:md5,64dd82f03df23f7f437eede2671ed4fe" + ], + [ + "s_1_1101.bcl:md5,b7929970378949571fed922c1b8cab32", + "s_1_1101.stats:md5,3d6d7985a41629fe196e4342d7fe36aa" + ], + [ + "s_1_1101.bcl:md5,fb2ed0bf6e89d79624ee78754e773491", + "s_1_1101.stats:md5,f34940810ff255aee79953496a12716d" + ], + [ + "s_1_1101.bcl:md5,4f8a8311f5f9c3a7629c1a973a7b280e", + "s_1_1101.stats:md5,4fd7cd28c09f4e152e7c2ad1ab541cd2" + ], + [ + "s_1_1101.bcl:md5,9eb46c903d0344e25af51f88cc311d60", + "s_1_1101.stats:md5,df3abd5f620d9e7f99496098d9fd3f7f" + ], + [ + "s_1_1101.bcl:md5,3ecbc17f3660e2014b58d7fe70ae62d5", + "s_1_1101.stats:md5,8e89a13c85a6d6ab3ccd251b66d1f165" + ], + [ + "s_1_1101.bcl:md5,5d59cc2499a77791233a64f73fe82894", + "s_1_1101.stats:md5,32ec99cd400f4b80cb26e2fa8e07ece0" + ], + [ + "s_1_1101.bcl:md5,1c052da47b9ae8554388f0fa3aade482", + "s_1_1101.stats:md5,d23f438772673688aa7bc92421dc6dce" + ], + [ + "s_1_1101.bcl:md5,1a52bd4f23130c0c96bc967ccd448a2b", + "s_1_1101.stats:md5,9b597e3388d59ef1f61aba30ac90ea79" + ], + [ + "s_1_1101.bcl:md5,8a1e84b79cf3f80794c20e3a0cc84688", + "s_1_1101.stats:md5,9561f7b6ef4b1849afc72b2bb49792bd" + ], + [ + "s_1_1101.bcl:md5,75c00111051f3fa95d04286823cb9109", + "s_1_1101.stats:md5,1fe786cdf8181767deafbd60b3c76610" + ], + [ + "s_1_1101.bcl:md5,529255d8deee0873ed5565e6d1a2ebda", + "s_1_1101.stats:md5,3fa7f467e97a75880f32d17b7429d316" + ], + [ + "s_1_1101.bcl:md5,ea4d960e3d9355d2149da71b88a21df4", + "s_1_1101.stats:md5,2540fe65586e8e800c1ddd8cddd1e8cd" + ], + [ + "s_1_1101.bcl:md5,0dfe1fd92a2dce2f23119aa483429744", + "s_1_1101.stats:md5,78257b2169fb9f0cf40966e06e847e86" + ], + [ + "s_1_1101.bcl:md5,f692ddc9aa3ab849271d07c666d0b3b9", + "s_1_1101.stats:md5,aa2ec6a3e3a9c116e34fe74a21e6459e" + ], + [ + "s_1_1101.bcl:md5,29cc4c239eae7c871c9a1adf92ebdb98", + "s_1_1101.stats:md5,263184813090acd740a5bf25304aed3a" + ], + [ + "s_1_1101.bcl:md5,e005af6a84925e326afbfe264241f047", + "s_1_1101.stats:md5,b6fb20868eebaffcc19daa694a449795" + ], + [ + "s_1_1101.bcl:md5,02f1a699b1ba9967accccf99a7af3d24", + "s_1_1101.stats:md5,4f007efacecaf26dc0e0231aede28754" + ], + [ + "s_1_1101.bcl:md5,df308c72a2dcc655cd95e98f5457187a", + "s_1_1101.stats:md5,130c4b07f4c14030bab012824cbe34da" + ], + [ + "s_1_1101.bcl:md5,f3ce10d8d2406b72355023bfa8c96822", + "s_1_1101.stats:md5,2638f4db393ed5b699ec2ce59ff0ec19" + ], + [ + "s_1_1101.bcl:md5,cc2f6d675ad1593ff96f734b172d249e", + "s_1_1101.stats:md5,f5b13f1e1ababc9e1a7a73b0b993cbf1" + ], + [ + "s_1_1101.bcl:md5,7938a0b21448305a951b023b1845b3a7", + "s_1_1101.stats:md5,fcd57511adabfc3ba1ac045165330006" + ], + [ + "s_1_1101.bcl:md5,44879bc6a38df1fee8def61868115041", + "s_1_1101.stats:md5,517e20e4b58a8023a37f9af62e0e2036" + ], + [ + "s_1_1101.bcl:md5,8749611e62406a7d2f34c610a55e56af", + "s_1_1101.stats:md5,8ccf24b3676ef84f2e513be8f2a9f3d1" + ], + [ + "s_1_1101.bcl:md5,a9846a037611cda3721958088f714c0e", + "s_1_1101.stats:md5,6438fa5a1892f328cab1605a95d80a3b" + ], + [ + "s_1_1101.bcl:md5,d6c4a2a726496476eb826532f974ed5f", + "s_1_1101.stats:md5,8c2c65b5e8b00dbf61ada65252aeb266" + ], + [ + "s_1_1101.bcl:md5,be3dde6cae7dd85855a6bf295ebfacfe", + "s_1_1101.stats:md5,93bc13f3b0749b2b8d8bcb0b1199f4f0" + ], + [ + "s_1_1101.bcl:md5,7c64514735a6cf1565b60647edd17d20", + "s_1_1101.stats:md5,4a0aa6c49b24f876415e5878cef7f805" + ], + [ + "s_1_1101.bcl:md5,3983b4043bc9df4b505202a5134ccf03", + "s_1_1101.stats:md5,1c9d9a8558adc1279ca27c96bc1b9758" + ], + [ + "s_1_1101.bcl:md5,a0b8d77f116ec95975f9253dcb768136", + "s_1_1101.stats:md5,c3992b786756e7ec42f65ef4b13b50d4" + ], + [ + "s_1_1101.bcl:md5,43c95ba35d06bb7c57fbd16f3d1cfd6c", + "s_1_1101.stats:md5,3cb69d04698c39f97f962e5bf1eea7f0" + ], + [ + "s_1_1101.bcl:md5,3dbeea0cad7052f19f53ff6f19dd4d90", + "s_1_1101.stats:md5,58bbc8254f0f5f4a244531e8e9c12a04" + ], + [ + "s_1_1101.bcl:md5,da56d088996376c898d855b6cd0a7dfc", + "s_1_1101.stats:md5,9f2d78af6908ce1576b89cdc059844ff" + ], + [ + "s_1_1101.bcl:md5,7b641a5565f095e9a6ffcad9e4305033", + "s_1_1101.stats:md5,3ada06c59b4fb41b83ab6abd0979e9fc" + ], + [ + "s_1_1101.bcl:md5,a3843d397a01d51657825bb652c191e5", + "s_1_1101.stats:md5,19341e52a4bfc7d9d48e9d2acc68c519" + ], + [ + "s_1_1101.bcl:md5,048e3ebfc8efeb8012def6b741c9060d", + "s_1_1101.stats:md5,88bd38deca1e87d700effab1fd099565" + ], + [ + "s_1_1101.bcl:md5,b340db0e07e829dd5da22371916a1a9e", + "s_1_1101.stats:md5,e44cfaddcc4ffb968e5b1a2f41ac48a5" + ], + [ + "s_1_1101.bcl:md5,e6011ec6eabbc2b8792deb283c621ce0", + "s_1_1101.stats:md5,090875dcd1a431af24bc631333f089c4" + ], + [ + "s_1_1101.bcl:md5,a08f216e3352345031ed100ec4245082", + "s_1_1101.stats:md5,97b949ef4b96219e1369f673cf5f8a6c" + ], + [ + "s_1_1101.bcl:md5,b43337c76fb037dfcf5f8f7bcb3618e5", + "s_1_1101.stats:md5,ddef585805e79951f69d23ab7354f69b" + ], + [ + "s_1_1101.bcl:md5,8c61fd004104397b360855e058bbf1bf", + "s_1_1101.stats:md5,0f8d253816d594dcfea3ccf48c826401" + ], + [ + "s_1_1101.bcl:md5,594d06310d328b188aa0b3edfff22cb2", + "s_1_1101.stats:md5,3160bf271b39aeb7590e4fd2984710ba" + ], + [ + "s_1_1101.bcl:md5,4c9eada67c9d55437211d83e111961d5", + "s_1_1101.stats:md5,2901b46ab16ec4863d30e4c84ec29c97" + ], + [ + "s_1_1101.bcl:md5,e03971ae5282f0accc0c1b7374d9ef1b", + "s_1_1101.stats:md5,60d2a19ce59bf70a21a28555484cead8" + ], + [ + "s_1_1101.bcl:md5,e1c6f7a06e63d149895d3e48e63df155", + "s_1_1101.stats:md5,44beb10af847ea3dddaf06dda7031126" + ], + [ + "s_1_1101.bcl:md5,960a99bf29a8f9d936e9b8582d46c9c6", + "s_1_1101.stats:md5,544cd1a7aaaa841914b40ece43399334" + ], + [ + "s_1_1101.bcl:md5,5706679f349fd4a6b6313bc2c41c7a42", + "s_1_1101.stats:md5,627eea844b26dae033848c2f9f69177b" + ], + [ + "s_1_1101.bcl:md5,21da5abc4b0402bbac14b5ab998b0b4f", + "s_1_1101.stats:md5,515bd140b095ad90473ca7a9a69877ab" + ], + "s_1_1101.control:md5,08a72e2198ae95150718e8adf011d105", + "s_1_1101.filter:md5,3a72bc73b323c8cb0ac5bfeb62d98989" + ] + ], + [ + "s_1_1101.locs:md5,0827ea802e5257cc5b20e757a33d4c98" + ], + "RTAConfiguration.xml:md5,c7d6e257bc374f142dc64b9d2281d4c9", + "config.xml:md5,9a4cc7ec01fefa2f1ce9bcb45bbad6e9" + ] + ], + [ + "ControlMetricsOut.bin:md5,6d77b38d0793a6e1ce1e85706e488953", + "CorrectedIntMetricsOut.bin:md5,2bbf84d3be72734addaa2fe794711434", + "ErrorMetricsOut.bin:md5,38c88def138e9bb832539911affdb286", + "ExtractionMetricsOut.bin:md5,7497c3178837eea8f09350b5cd252e99", + "IndexMetricsOut.bin:md5,d41d8cd98f00b204e9800998ecf8427e", + "QMetricsOut.bin:md5,7e9f198d53ebdfbb699a5f94cf1ed51c", + "TileMetricsOut.bin:md5,83891751ec1c91a425a524b476b6ca3c" + ], + "RunInfo.xml:md5,03038959f4dd181c86bc97ae71fe270a" + ] + ] + ] + ], + "timestamp": "2023-10-18T11:56:39.562418" + }, + "test_untar_onlyfiles": { + "content": [ + [ + [ + [ + + ], + [ + "hello.txt:md5,e59ff97941044f85df5297e1c302d260" + ] + ] + ] + ], + "timestamp": "2023-10-18T11:56:46.878844" + }, + "test_untar": { + "content": [ + [ + [ + [ + + ], + [ + "hash.k2d:md5,8b8598468f54a7087c203ad0190555d9", + "opts.k2d:md5,a033d00cf6759407010b21700938f543", + "taxo.k2d:md5,094d5891cdccf2f1468088855c214b2c" + ] + ] + ] + ], + "timestamp": "2023-10-18T11:56:08.16574" + } +} \ No newline at end of file diff --git a/modules/nf-core/untar/tests/tags.yml b/modules/nf-core/untar/tests/tags.yml new file mode 100644 index 0000000..feb6f15 --- /dev/null +++ b/modules/nf-core/untar/tests/tags.yml @@ -0,0 +1,2 @@ +untar: + - modules/nf-core/untar/** From c19d53853a3be4cae099b49130e0e7f1b8abc0f1 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 11:48:58 +0100 Subject: [PATCH 195/410] Add warning if filtering removes all spots / all genes And keep the unfiltered anndata object for downstream analysis. --- bin/st_qc_and_normalisation.qmd | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index d4e844e..14a347e 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -36,13 +36,13 @@ nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` ```{python} -###| echo: false -# Hide warnings in output html. +#| echo: false import scanpy as sc import scipy import pandas as pd import matplotlib.pyplot as plt import seaborn as sns +from IPython.display import display, Markdown plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) ``` @@ -63,6 +63,8 @@ st_adata.var['hb'] = st_adata.var_names.str.contains(("^Hb.*-")) sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "hb"], inplace=True) +st_adata_before_filtering = st_adata.copy() + print ("Content of the anndata object:") st_adata ``` @@ -139,6 +141,8 @@ _Bottom row: Distribution of the number of genes with at least 1 count in a spot We filter cells based on minimum counts and genes, and filter genes based on minimum cells. ```{python} +#| warning: false + # Filter cells based on counts Number_spots = st_adata.shape[0] Number_genes = st_adata.shape[1] @@ -158,6 +162,20 @@ print (f"Removed {Number_genes - Number_genes_filtered_minCells} genes expressed print (f"{Number_spots_filtered_minGenes} out of {Number_spots} spots remaining after filtering.") print (f"{Number_genes_filtered_minCells} out of {Number_genes} genes remaining after filtering.") + +if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): + st_adata = st_adata_before_filtering + display( + Markdown(""" +::: {.callout-important .content-visible when-format="html"} +## Issue: No Spots Remain After Filtering + +An anomaly has been detected in the data: following the filtering process, all spots have been excluded. It is imperative to assess the data quality and carefully review the Analysis Options outlined in `docs/usage.md`. + +To ensure the smooth progression of downstream analysis, the exported AnnData will, for the time being, remain unfiltered. This precautionary measure is implemented to facilitate continued analysis while investigating and resolving the cause of the unexpected removal of all spots during filtering. +:::""" + ) + ) ``` Distributions after filtering: From 0edc6b99f5948e3f93d3dea177e81c6799cfd338 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 13:02:50 +0100 Subject: [PATCH 196/410] Fix linting --- docs/usage.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index df3e6b6..f1f86ea 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -112,21 +112,21 @@ The pipeline is using Python and the scverse tools to do the downstream analysis The following parameters are exposed for preprocessing: - - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. - - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. - - `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. - - `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (e.g., quality control plots). - - `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. - - `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. - - `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. +- `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. +- `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. +- `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. +- `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (e.g., quality control plots). +- `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. +- `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. +- `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. ### Parameters for Clustering : - - `--st_cluster_resolution`: Resolution parameter for the clustering algorithm, controlling granularity. +- `--st_cluster_resolution`: Resolution parameter for the clustering algorithm, controlling granularity. ### Parameters for Spatial Differential Expression : - - `st_spatial_de_ncols`: Number of columns in the output figure. +- `st_spatial_de_ncols`: Number of columns in the output figure. ## Running the pipeline From 91f28462374be2264d934381593b872481531419 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 13:20:09 +0100 Subject: [PATCH 197/410] Fix black --- bin/read_st_data.py | 59 ++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index 18f872e..f83301b 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -9,12 +9,11 @@ import json import pandas as pd from matplotlib.image import imread -from anndata import ( - AnnData -) +from anndata import AnnData from scanpy import logging as logg + def read_visium( path: Union[str, Path], genome: Optional[str] = None, @@ -106,35 +105,28 @@ def read_visium( ) files = dict( tissue_positions_file=tissue_positions_file, - scalefactors_json_file=path / 'spatial/scalefactors_json.json', - hires_image=path / 'spatial/tissue_hires_image.png', - lowres_image=path / 'spatial/tissue_lowres_image.png', + scalefactors_json_file=path / "spatial/scalefactors_json.json", + hires_image=path / "spatial/tissue_hires_image.png", + lowres_image=path / "spatial/tissue_lowres_image.png", ) # check if files exists, continue if images are missing for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning( - f"You seem to be missing an image file.\n" - f"Could not find '{f}'." - ) + logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") else: raise OSError(f"Could not find '{f}'") - adata.uns["spatial"][library_id]['images'] = dict() - for res in ['hires', 'lowres']: + adata.uns["spatial"][library_id]["images"] = dict() + for res in ["hires", "lowres"]: try: - adata.uns["spatial"][library_id]['images'][res] = imread( - str(files[f'{res}_image']) - ) + adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]['scalefactors'] = json.loads( - files['scalefactors_json_file'].read_bytes() - ) + adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -144,25 +136,23 @@ def read_visium( # read coordinates positions = pd.read_csv( - files['tissue_positions_file'], + files["tissue_positions_file"], header=0 if tissue_positions_file.name == "tissue_positions.csv" else None, index_col=0, ) positions.columns = [ - 'in_tissue', - 'array_row', - 'array_col', - 'pxl_col_in_fullres', - 'pxl_row_in_fullres', + "in_tissue", + "array_row", + "array_col", + "pxl_col_in_fullres", + "pxl_row_in_fullres", ] adata.obs = adata.obs.join(positions, how="left") - adata.obsm['spatial'] = adata.obs[ - ['pxl_row_in_fullres', 'pxl_col_in_fullres'] - ].to_numpy() + adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() adata.obs.drop( - columns=['pxl_row_in_fullres', 'pxl_col_in_fullres'], + columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], inplace=True, ) @@ -170,9 +160,7 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( - source_image_path - ) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) return adata @@ -187,14 +175,9 @@ def read_visium( ) parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") args = parser.parse_args() - + # Read Visium data - st_adata = read_visium( - args.SRCountDir, - count_file='raw_feature_bc_matrix.h5', - library_id=None, - load_images=True - ) + st_adata = read_visium(args.SRCountDir, count_file="raw_feature_bc_matrix.h5", library_id=None, load_images=True) # Write raw anndata to file st_adata.write(args.outAnnData) From 04785d178b20cd70876d99a81509fa24ea519403 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 13:49:15 +0100 Subject: [PATCH 198/410] Update st_clustering.qmd Fix jinja variable in qmd file --- bin/st_clustering.qmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index cd64f70..8a6da8e 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -48,12 +48,13 @@ st_adata ## Manifold embedding and clustering based on transcriptional similarity -To uncover the underlying structure of the transcriptional landscape, we perform manifold embedding and clustering based on transcriptional similarity. Principal Component Analysis (PCA) is applied to reduce dimensionality, and UMAP (Uniform Manifold Approximation and Projection) is used for visualization. The Leiden algorithm is employed for clustering with a specified resolution of {{< var resolution >}}. +To uncover the underlying structure of the transcriptional landscape, we perform manifold embedding and clustering based on transcriptional similarity. Principal Component Analysis (PCA) is applied to reduce dimensionality, and UMAP (Uniform Manifold Approximation and Projection) is used for visualization. The Leiden algorithm is employed for clustering with a givent resolution. ```{python} sc.pp.pca(st_adata) sc.pp.neighbors(st_adata) sc.tl.umap(st_adata) +print (f"Resolution for Leiden clustering: {resolution}") sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) ``` From 09fa05810dcfdacf783f25c77c55caf1880141f6 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 13:57:37 +0100 Subject: [PATCH 199/410] fix trailing whitespace --- modules/local/st_read_data.nf | 84 +++++++++++++++++------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index eeace75..22ebcdd 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -1,42 +1,42 @@ -// -// Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file -// -process ST_READ_DATA { - - tag "${meta.id}" - label 'process_low' - - conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" - 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("${meta.id}/*") - - output: - tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw - path("versions.yml") , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - mkdir "${meta.id}/spatial" - mv "${meta.id}/scalefactors_json.json" \\ - "${meta.id}/tissue_hires_image.png" \\ - "${meta.id}/tissue_lowres_image.png" \\ - "${meta.id}/tissue_positions.csv" \\ - "${meta.id}/spatial/" - - read_st_data.py \\ - --SRCountDir "${meta.id}" \\ - --outAnnData st_adata_raw.h5ad - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - END_VERSIONS - """ -} +// +// Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file +// +process ST_READ_DATA { + + tag "${meta.id}" + label 'process_low' + + conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" + 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("${meta.id}/*") + + output: + tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw + path("versions.yml") , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + """ + mkdir "${meta.id}/spatial" + mv "${meta.id}/scalefactors_json.json" \\ + "${meta.id}/tissue_hires_image.png" \\ + "${meta.id}/tissue_lowres_image.png" \\ + "${meta.id}/tissue_positions.csv" \\ + "${meta.id}/spatial/" + + read_st_data.py \\ + --SRCountDir "${meta.id}" \\ + --outAnnData st_adata_raw.h5ad + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS + """ +} From b2e3a747fc97adc3221ad9b2c2d6dc7015ac708b Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 29 Nov 2023 13:59:01 +0100 Subject: [PATCH 200/410] fix trailing whitespace --- modules/local/st_read_data.nf | 84 +++++++++++++++++------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index 22ebcdd..d482cc5 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -1,42 +1,42 @@ -// -// Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file -// -process ST_READ_DATA { - - tag "${meta.id}" - label 'process_low' - - conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" - 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("${meta.id}/*") - - output: - tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw - path("versions.yml") , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - mkdir "${meta.id}/spatial" - mv "${meta.id}/scalefactors_json.json" \\ - "${meta.id}/tissue_hires_image.png" \\ - "${meta.id}/tissue_lowres_image.png" \\ - "${meta.id}/tissue_positions.csv" \\ - "${meta.id}/spatial/" - - read_st_data.py \\ - --SRCountDir "${meta.id}" \\ - --outAnnData st_adata_raw.h5ad - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - END_VERSIONS - """ -} +// +// Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file +// +process ST_READ_DATA { + + tag "${meta.id}" + label 'process_low' + + conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" + 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("${meta.id}/*") + + output: + tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw + path("versions.yml") , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + """ + mkdir "${meta.id}/spatial" + mv "${meta.id}/scalefactors_json.json" \\ + "${meta.id}/tissue_hires_image.png" \\ + "${meta.id}/tissue_lowres_image.png" \\ + "${meta.id}/tissue_positions.csv" \\ + "${meta.id}/spatial/" + + read_st_data.py \\ + --SRCountDir "${meta.id}" \\ + --outAnnData st_adata_raw.h5ad + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS + """ +} From 9c7ce2bc4919a6092c95311b38ee12e48d3c38e9 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Thu, 30 Nov 2023 13:52:26 +0100 Subject: [PATCH 201/410] Add xdg paths to fix scanpy cache issue --- conf/modules.config | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf/modules.config b/conf/modules.config index f439574..f212649 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -9,6 +9,10 @@ ext.prefix = File name prefix for output files. ---------------------------------------------------------------------------------------- */ +env { + XDG_CACHE_HOME = "./.xdg_cache_home" + XDG_DATA_HOME = "./.xdg_data_home" +} process { From cb0e10f2a05767391eba84ab142b8e1ad139c463 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 14:21:01 +0100 Subject: [PATCH 202/410] Add `.nf-test*` to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1eb2788..0699257 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ log reports .nf-test/ nf-test +.nf-test* test-datasets From a9d02149c398f0462c85b2f66beb697ed874575a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 14:21:23 +0100 Subject: [PATCH 203/410] Use compressed tarball for downstream test data Use a compressed tarball of the downstream (post-Space Ranger) test data as a workaround of not being able to stage a remote directory. --- conf/test.config | 6 ++-- subworkflows/local/input_check.nf | 53 +++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/conf/test.config b/conf/test.config index 0a91e5c..c2eec5c 100644 --- a/conf/test.config +++ b/conf/test.config @@ -20,9 +20,9 @@ params { max_time = '2.h' // Input and output - input = './test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv' - spaceranger_probeset = "./test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/CytAssist_11mm_FFPE_Human_Glioblastoma_probe_set.csv" - spaceranger_reference = "./test-datasets/testdata/homo_sapiens_chr22_reference.tar.gz" + input = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 outdir = 'results' diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index c714514..6aa1545 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -2,27 +2,62 @@ // Check input samplesheet and get read channels // +include { UNTAR as UNTAR_COMPRESSED_INPUT } from "../../modules/nf-core/untar" + workflow INPUT_CHECK { take: samplesheet // file: /path/to/samplesheet.csv main: - ch_st = Channel.from(samplesheet).splitCsv( - header: true, - sep: ',' - ).branch { - spaceranger: !it.containsKey("spaceranger_dir") - downstream: it.containsKey("spaceranger_dir") + ch_st = Channel.from(samplesheet) + .splitCsv ( header: true, sep: ',') + .branch { + spaceranger: !it.containsKey("spaceranger_dir") + downstream: it.containsKey("spaceranger_dir") + } + + // Pre-Space Ranger analysis: create meta map and check input existance + ch_spaceranger_input = ch_st.spaceranger.map { create_channel_spaceranger(it) } + + // Post-Space Ranger analysis: + // Check if running the `test` profile, which uses a tarball of the testdata + if ( workflow.profile.contains('test') ) { + + // Untar Space Ranger output stored as tarball + ch_downstream_tar = ch_st.downstream.map { create_channel_downstream_tar(it) } + UNTAR_COMPRESSED_INPUT ( + ch_downstream_tar + ) + + // Create meta map corresponding to non-tarballed input + ch_downstream = UNTAR_COMPRESSED_INPUT.out.untar + .map { meta, dir -> [sample: meta.id, spaceranger_dir: dir] } + + } else { + + // Non-tarballed input data + ch_downstream = ch_st.downstream + } - ch_spaceranger_input = ch_st.spaceranger.map{create_channel_spaceranger(it)} - ch_downstream_input = ch_st.downstream.map{create_channel_downstream(it)} + + // Create meta map and check input existance + ch_downstream_input = ch_downstream.map { create_channel_downstream(it) } emit: ch_spaceranger_input // channel: [ val(meta), [ st data ] ] ch_downstream_input // channel: [ val(meta), [ st data ] ] } +// Function to get list of [ meta, [ spaceranger_dir ]] +def create_channel_downstream_tar(LinkedHashMap meta) { + meta['id'] = meta.remove('sample') + spaceranger_dir = meta.remove('spaceranger_dir') + return [meta, spaceranger_dir] +} + +// Function to get list of [ meta, [ raw_feature_bc_matrix, tissue_positions, +// scalefactors, hires_image, lowres_image ]] def create_channel_downstream(LinkedHashMap meta) { meta["id"] = meta.remove("sample") spaceranger_dir = file("${meta.remove('spaceranger_dir')}/**") @@ -34,7 +69,7 @@ def create_channel_downstream(LinkedHashMap meta) { return [meta, spaceranger_dir] } -// Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ] +// Function to get list of [ meta, [ fastq_dir, tissue_hires_image, slide, area ]] def create_channel_spaceranger(LinkedHashMap meta) { meta["id"] = meta.remove("sample") From f567498b5d23e620b28535c3593180e57ac1b141 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 15:19:32 +0100 Subject: [PATCH 204/410] Remove old `run_spaceranger` parameter mention --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index f1f86ea..dddcd0c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -134,7 +134,7 @@ The typical command for running the pipeline is as follows: ```bash # Run the pipeline with raw data yet to be processed by Space Ranger -nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker --run_spaceranger +nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker # Run pipeline with data already processed by Space Ranger nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker From 3d08dd6e16384fce3e0a4c0d3bdad91a7f77c01e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 15:20:40 +0100 Subject: [PATCH 205/410] Clarify test profile description --- conf/test.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/test.config b/conf/test.config index c2eec5c..29f95c6 100644 --- a/conf/test.config +++ b/conf/test.config @@ -12,7 +12,7 @@ params { config_profile_name = 'Test profile' - config_profile_description = 'Test pipeline incl. spaceranger with cytassist ffpe sample' + config_profile_description = 'Test pipeline for post-Space Ranger functionality' // Limit resources so that this can run on GitHub Actions max_cpus = 2 From 00185fbd9d2a06e033c99ecc1ed0098df77ad711 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 16:02:48 +0100 Subject: [PATCH 206/410] Use remote files for `nf-test` downstream test --- tests/pipeline/test_downstream.nf.test | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 8ed783a..9307b88 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -6,8 +6,9 @@ nextflow_pipeline { test("CytAssist_11mm_FFPE_Human_Glioblastoma_2") { when { params { - input = 'test-datasets/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' - spaceranger_probeset = null + input = "https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv" + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" outdir = "$outputDir" } } From 0e51ef30b503aa3c9ffc5c377c2c391711c09cf2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 17:09:20 +0100 Subject: [PATCH 207/410] Update nf-test snapshot --- tests/pipeline/test_downstream.nf.test.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index a2bee29..a74e549 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -8,8 +8,8 @@ }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-25T06:54:28+0000" + "timestamp": "2023-12-18T16:46:26.313038" } -} +} \ No newline at end of file From 152f0fd278d39a5ed6f5f1ea2a221839f004d6e3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 18 Dec 2023 17:52:51 +0100 Subject: [PATCH 208/410] Use channel branching to check input type Use channel branching to check the input type (directory or tarball) of post-Space Ranger input instead of checking for the `test` profile. This is more generalisable and a better overall solution to the problem of different input types. --- subworkflows/local/input_check.nf | 35 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 6aa1545..006367f 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -21,28 +21,27 @@ workflow INPUT_CHECK { ch_spaceranger_input = ch_st.spaceranger.map { create_channel_spaceranger(it) } // Post-Space Ranger analysis: - // Check if running the `test` profile, which uses a tarball of the testdata - if ( workflow.profile.contains('test') ) { - // Untar Space Ranger output stored as tarball - ch_downstream_tar = ch_st.downstream.map { create_channel_downstream_tar(it) } - UNTAR_COMPRESSED_INPUT ( - ch_downstream_tar - ) - - // Create meta map corresponding to non-tarballed input - ch_downstream = UNTAR_COMPRESSED_INPUT.out.untar - .map { meta, dir -> [sample: meta.id, spaceranger_dir: dir] } - - } else { + // Split channel into tarballed and directory inputs + ch_downstream = ch_st.downstream + .map { create_channel_downstream_tar(it) } + .branch { + tar: it[1].contains(".tar.gz") + dir: !it[1].contains(".tar.gz") + } - // Non-tarballed input data - ch_downstream = ch_st.downstream + // Extract tarballed inputs + UNTAR_COMPRESSED_INPUT ( + ch_downstream.tar + ) - } + // Combine extracted and directory inputs into one channel + ch_downstream_combined = UNTAR_COMPRESSED_INPUT.out.untar + .mix ( ch_downstream.dir ) + .map { meta, dir -> [sample: meta.id, spaceranger_dir: dir] } - // Create meta map and check input existance - ch_downstream_input = ch_downstream.map { create_channel_downstream(it) } + // Create final meta map and check input file existance + ch_downstream_input = ch_downstream_combined.map { create_channel_downstream(it) } emit: ch_spaceranger_input // channel: [ val(meta), [ st data ] ] From f248416782d9fcf90138fc9c17ba447fd45e8ddf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Dec 2023 16:07:38 +0100 Subject: [PATCH 209/410] Add info on tarball input in usage docs --- docs/usage.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index dddcd0c..2837793 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -85,6 +85,14 @@ SAMPLE_2,results/SAMPLE_2/outs | `sample` | Unique sample identifier. | | `spaceranger_dir` | Output directory generated by spaceranger. This is typically called `outs` and contains both gene expression matrices and spatial information | +You may alternatively supply a compressed tarball containing the Space Ranger output: + +```no-highlight +sample,spaceranger_dir +SAMPLE_1,outs.tar.gz +SAMPLE_2,outs.tar.gz +``` + ## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with From 2b357640afc6582a058de01d590700f52c7dc75a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Dec 2023 17:48:37 +0100 Subject: [PATCH 210/410] Use compressed tarball of Space Ranger test data --- subworkflows/local/input_check.nf | 33 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 006367f..8d9630d 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -2,7 +2,8 @@ // Check input samplesheet and get read channels // -include { UNTAR as UNTAR_COMPRESSED_INPUT } from "../../modules/nf-core/untar" +include { UNTAR as UNTAR_SPACERANGER_INPUT } from "../../modules/nf-core/untar" +include { UNTAR as UNTAR_DOWNSTREAM_INPUT } from "../../modules/nf-core/untar" workflow INPUT_CHECK { @@ -17,10 +18,28 @@ workflow INPUT_CHECK { downstream: it.containsKey("spaceranger_dir") } - // Pre-Space Ranger analysis: create meta map and check input existance - ch_spaceranger_input = ch_st.spaceranger.map { create_channel_spaceranger(it) } + // Space Ranger analysis: -------------------------------------------------- - // Post-Space Ranger analysis: + // Split channel into tarballed and directory inputs + ch_spaceranger = ch_st.spaceranger + .map { it -> [it, it.fastq_dir]} + .branch { + tar: it[1].contains(".tar.gz") + dir: !it[1].contains(".tar.gz") + } + + // Extract tarballed inputs + UNTAR_SPACERANGER_INPUT ( ch_spaceranger.tar ) + + // Combine extracted and directory inputs into one channel + ch_spaceranger_combined = UNTAR_SPACERANGER_INPUT.out.untar + .mix ( ch_spaceranger.dir ) + .map { meta, dir -> meta + [fastq_dir: dir] } + + // Create final meta map and check input existance + ch_spaceranger_input = ch_spaceranger_combined.map { create_channel_spaceranger(it) } + + // Downstream analysis: ---------------------------------------------------- // Split channel into tarballed and directory inputs ch_downstream = ch_st.downstream @@ -31,12 +50,10 @@ workflow INPUT_CHECK { } // Extract tarballed inputs - UNTAR_COMPRESSED_INPUT ( - ch_downstream.tar - ) + UNTAR_DOWNSTREAM_INPUT ( ch_downstream.tar ) // Combine extracted and directory inputs into one channel - ch_downstream_combined = UNTAR_COMPRESSED_INPUT.out.untar + ch_downstream_combined = UNTAR_DOWNSTREAM_INPUT.out.untar .mix ( ch_downstream.dir ) .map { meta, dir -> [sample: meta.id, spaceranger_dir: dir] } From 5ae23b027462ce119c80854f6180f39b99681ec8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Dec 2023 18:04:55 +0100 Subject: [PATCH 211/410] Add info on Space Ranger tarball input in docs --- docs/usage.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 2837793..8010490 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,6 +29,15 @@ SAMPLE_1,fastqs_1/,hires_1.png,V11J26,B1 SAMPLE_2,fastqs_2/,hires_2.png,V11J26,B1 ``` +You may also supply a compressed tarball containing the FASTQ files in lieu of a +directory path: + +```no-highlight +sample,fastq_dir,image,slide,area +SAMPLE_1,fastqs_1.tar.gz,hires_1.png,V11J26,B1 +SAMPLE_2,fastqs_2.tar.gz,hires_2.png,V11J26,B1 +``` + For Cytassist samples, the `image` column gets replaced with the `cytaimage` column: ```no-highlight @@ -45,7 +54,7 @@ Please refer to the following table for an overview of all supported columns: | Column | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | `sample` | Unique sample identifier. MUST match the prefix of the fastq files | -| `fastq_dir` | Path to directory where the sample FASTQ files are stored. | +| `fastq_dir` | Path to directory where the sample FASTQ files are stored. May be a `.tar.gz` file instead of a directory. | | `image` | Brightfield microscopy image | | `cytaimage` | Brightfield tissue image captured with Cytassist device | | `colorizedimage` | A color composite of one or more fluorescence image channels saved as a single-page, single-file color TIFF or JPEG | @@ -80,11 +89,6 @@ SAMPLE_1,results/SAMPLE_1/outs SAMPLE_2,results/SAMPLE_2/outs ``` -| Column | Description | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| `sample` | Unique sample identifier. | -| `spaceranger_dir` | Output directory generated by spaceranger. This is typically called `outs` and contains both gene expression matrices and spatial information | - You may alternatively supply a compressed tarball containing the Space Ranger output: ```no-highlight @@ -93,6 +97,14 @@ SAMPLE_1,outs.tar.gz SAMPLE_2,outs.tar.gz ``` +| Column | Description | +| ----------------- | ----------------------------------------------------------------------------------------- | +| `sample` | Unique sample identifier. | +| `spaceranger_dir` | Output directory generated by spaceranger. May be a `.tar.gz` file instead of a directory | + +The Space Ranger output directory is typically called `outs` and contains both +gene expression matrices as well as spatial information. + ## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with From 1c9ad186ac84cf82735a1e9eacaa0bd67c113b09 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Dec 2023 18:29:43 +0100 Subject: [PATCH 212/410] Update downstream `nf-test` tests --- tests/pipeline/test_downstream.nf.test | 20 +++++++++++--------- tests/pipeline/test_downstream.nf.test.snap | 7 ------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 9307b88..07dec42 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -6,29 +6,31 @@ nextflow_pipeline { test("CytAssist_11mm_FFPE_Human_Glioblastoma_2") { when { params { - input = "https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv" - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" outdir = "$outputDir" } } then { assertAll( + // Workflow { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - // data + + // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, - // reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + + // Reports + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, - // degs + + // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, - // multiqc + + // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } ) } diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index a74e549..58aa1a3 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,11 +1,4 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", - "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" - ], - "timestamp": "2023-06-25T06:54:28+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" From fabdc22bc142ee56c2d4bc62a15cf4e2a3c67e27 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 09:50:44 +0100 Subject: [PATCH 213/410] Update Space Ranger `nf-test` pipelines --- .../pipeline/test_spaceranger_ffpe_v1.nf.test | 25 +++++++++++------ ...test_spaceranger_ffpe_v2_cytassist.nf.test | 28 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 7f03e1a..2d43b9c 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -6,8 +6,8 @@ nextflow_pipeline { test("spaceranger ffpe v1") { when { params { - input = 'test-datasets/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' - spaceranger_probeset = 'test-datasets/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + input = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' + spaceranger_probeset = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 outdir = "$outputDir" @@ -16,20 +16,26 @@ nextflow_pipeline { then { assertAll( + + // Workflow { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - // data + + // Data { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_norm.h5ad").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_raw.h5ad").exists() }, - // reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + + // Reports + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, - // degs + + // DEGs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, - // spaceranger + + // Space Ranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/web_summary.html").exists() }, { assert snapshot( path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), @@ -37,7 +43,8 @@ nextflow_pipeline { )}, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() }, - // multiqc + + // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("Visium_FFPE_Human_Ovarian_Cancer")} ) } diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 82b0d92..baad55b 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -6,27 +6,38 @@ nextflow_pipeline { test("spaceranger ffpe v2 cytassist (default `-profile test`)") { when { params { - // This is the default `test` profile, no need to specify additional parameters + input = "https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 outdir = "$outputDir" } } then { assertAll( + + // Workflow { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, - // data + + // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, - // reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("Saving anndata file for future use") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distribution after filtering") }, + + // Reports + { assert + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, + file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, - // degs + + // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, - // spaceranger + + // Space Ranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, { assert snapshot( path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), @@ -34,7 +45,8 @@ nextflow_pipeline { )}, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() }, - // multiqc + + // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("CytAssist_11mm_FFPE_Human_Glioblastoma_2_4")} ) } From 20bc7ef228a0d30dcbebf9bd1345a38713702aa3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 09:55:22 +0100 Subject: [PATCH 214/410] Remove misplaced newline --- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index baad55b..af846b9 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -29,9 +29,8 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, // Reports - { assert { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, - file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, // DEGs @@ -51,7 +50,4 @@ nextflow_pipeline { ) } } - - - } From 33839e9739b8f3531b76c2451347478cde049e42 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 10:08:44 +0100 Subject: [PATCH 215/410] Update Space Ranger `nf-test` pipeline snapshots --- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 4 ++-- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index d9d705d..1ea6423 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -8,8 +8,8 @@ }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, FASTQC={fastqc=0.11.9}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-29T12:46:05+0000" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 24f6f88..3acc223 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -8,8 +8,8 @@ }, "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.0, yaml=6.0}, FASTQC={fastqc=0.11.9}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-29T12:48:09+0000" } -} \ No newline at end of file +} From 2102629ef7a4addf4f4a3a0f7f2e9b4e05561309 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 10:24:53 +0100 Subject: [PATCH 216/410] Clarify `nf-test` pipeline names --- tests/pipeline/test_downstream.nf.test | 5 +++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 7 ++++--- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 07dec42..0b74606 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -1,11 +1,12 @@ nextflow_pipeline { - name "Test workflow (only downstream part)" + name "Test downstream workflow (excl. Space Ranger)" script "main.nf" tag "pipeline" - test("CytAssist_11mm_FFPE_Human_Glioblastoma_2") { + test("Downstream FFPE v2 CytAssist") { when { params { + // This is the default `test` profile; params are not necessary outdir = "$outputDir" } } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 2d43b9c..97deb41 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -1,13 +1,14 @@ nextflow_pipeline { - name "Test full workflow including spaceranger" + name "Test full workflow (incl. Space Ranger)" script "main.nf" tag "pipeline" - test("spaceranger ffpe v1") { + test("Space Ranger FFPE v1 Standard") { when { params { input = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' - spaceranger_probeset = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 outdir = "$outputDir" diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index af846b9..d259b47 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -1,9 +1,9 @@ nextflow_pipeline { - name "Test full workflow including spaceranger" + name "Test full workflow (incl. Space Ranger)" script "main.nf" tag "pipeline" - test("spaceranger ffpe v2 cytassist (default `-profile test`)") { + test("Space Ranger FFPE v2 CytAssist") { when { params { input = "https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" From a1800010b4e5f07624a83af5a185299f5cb650a4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 10:27:08 +0100 Subject: [PATCH 217/410] Do not checkout test data in CI testing --- .github/workflows/ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36ef2ae..6a02675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,14 +51,6 @@ jobs: - name: Check out pipeline code uses: actions/checkout@v3 - - name: Checkout test data - uses: actions/checkout@v3 - with: - repository: nf-core/test-datasets - ref: spatialtranscriptomics - fetch-depth: 1 - path: test-datasets - # Install Nextflow - name: Install Nextflow uses: nf-core/setup-nextflow@v1 From 0405d590703c96d5fbbc95abbef6dbbb97f74176 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 12:01:19 +0100 Subject: [PATCH 218/410] Update FastQC and MultiQC nf-core modules --- modules.json | 4 +- modules/nf-core/fastqc/tests/main.nf.test | 68 ++++++++++++++++++++++ modules/nf-core/multiqc/tests/main.nf.test | 32 +--------- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/modules.json b/modules.json index 6810b8b..0f162bb 100644 --- a/modules.json +++ b/modules.json @@ -12,12 +12,12 @@ }, "fastqc": { "branch": "master", - "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", + "git_sha": "65ad3e0b9a4099592e1102e92e10455dc661cf53", "installed_by": ["modules"] }, "multiqc": { "branch": "master", - "git_sha": "1537442a7be4a78efa3d1ff700a923c627bbda5d", + "git_sha": "4ab13872435962dadc239979554d13709e20bf29", "installed_by": ["modules"] }, "spaceranger/count": { diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index 6437a14..b9e8f92 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -38,4 +38,72 @@ nextflow_process { ) } } +// TODO +// // +// // Test with paired-end data +// // +// workflow test_fastqc_paired_end { +// input = [ +// [id: 'test', single_end: false], // meta map +// [ +// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), +// file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true) +// ] +// ] + +// FASTQC ( input ) +// } + +// // +// // Test with interleaved data +// // +// workflow test_fastqc_interleaved { +// input = [ +// [id: 'test', single_end: false], // meta map +// file(params.test_data['sarscov2']['illumina']['test_interleaved_fastq_gz'], checkIfExists: true) +// ] + +// FASTQC ( input ) +// } + +// // +// // Test with bam data +// // +// workflow test_fastqc_bam { +// input = [ +// [id: 'test', single_end: false], // meta map +// file(params.test_data['sarscov2']['illumina']['test_paired_end_sorted_bam'], checkIfExists: true) +// ] + +// FASTQC ( input ) +// } + +// // +// // Test with multiple samples +// // +// workflow test_fastqc_multiple { +// input = [ +// [id: 'test', single_end: false], // meta map +// [ +// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), +// file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true), +// file(params.test_data['sarscov2']['illumina']['test2_1_fastq_gz'], checkIfExists: true), +// file(params.test_data['sarscov2']['illumina']['test2_2_fastq_gz'], checkIfExists: true) +// ] +// ] + +// FASTQC ( input ) +// } + +// // +// // Test with custom prefix +// // +// workflow test_fastqc_custom_prefix { +// input = [ +// [ id:'mysample', single_end:true ], // meta map +// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) +// ] + +// FASTQC ( input ) +// } } diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test index 68fffa9..c2dad21 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -9,27 +9,13 @@ nextflow_process { test("MULTIQC: FASTQC") { - setup { - run("FASTQC") { - script "../../fastqc/main.nf" - process { - """ - input[0] = Channel.of([ - [ id: 'test', single_end: false ], - [ file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true)] - ]) - """ - } - } - } - when { params { outdir = "$outputDir" } process { """ - input[0] = FASTQC.out.zip.collect { it[1] } + input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) input[1] = [] input[2] = [] input[3] = [] @@ -50,27 +36,13 @@ nextflow_process { test("MULTIQC: FASTQC and a config file") { - setup { - run("FASTQC") { - script "../../fastqc/main.nf" - process { - """ - input[0] = Channel.of([ - [ id: 'test', single_end: false ], - [ file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true)] - ]) - """ - } - } - } - when { params { outdir = "$outputDir" } process { """ - input[0] = FASTQC.out.zip.collect { it[1] } + input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) 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] = [] From a8098435054239a599a324dd3e6b099c41e00539 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 13:22:50 +0100 Subject: [PATCH 219/410] Update `CHANGELOG.md` --- CHANGELOG.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 521f26f..af81800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,24 +5,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -Initial release of nf-core/spatialtranscriptomics, created with the [nf-core](https://nf-co.re/) template. -This marks the point at which the pipeline development was moved to nf-core and -NBIS. The pipeline has undergone several iterations regarding its functionality -and content; there are a significant number of changes, of which not all are -listed here. In summary, the pipeline contains best-practice processing and -analyses of pre- and post-Space Ranger-processed data, including quality -controls, normalisation, dimensionality reduction, clustering, differential -expression testing as well as output files compatible with further downstream -analyses and/or exploration in _e.g._ [TissUUmaps](https://tissuumaps.github.io/) -or bespoke user code. +Initial release of nf-core/spatialtranscriptomics, created with the +[nf-core](https://nf-co.re/) template. This marks the point at which the +pipeline development was moved to nf-core and NBIS. The pipeline has undergone +several iterations regarding its functionality and content; there are a +significant number of changes, of which not all are listed here. In summary, the +pipeline contains best-practice processing and analyses of pre- and post-Space +Ranger-processed data, including quality controls, normalisation, dimensionality +reduction, clustering, differential expression testing as well as output files +compatible with further downstream analyses and/or exploration in _e.g._ +[TissUUmaps](https://tissuumaps.github.io/) or bespoke user code. ### `Added` +- Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) +- Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] - Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] - Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] -- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] - Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialtranscriptomics/pull/43)] -- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] - Use a samplesheet for input specification [[#30](https://github.com/nf-core/spatialtranscriptomics/pull/30), [#31](https://github.com/nf-core/spatialtranscriptomics/pull/31) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] - Add Space Ranger pre-processing as an optional pipeline step using the `spaceranger` nf-core module [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] - Add `env/` directory with pipeline-specific container and Conda environment specifications [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#28](https://github.com/nf-core/spatialtranscriptomics/pull/28)] @@ -50,7 +50,7 @@ versions of the same tool. | ----------- | ------- | | `SpatialDE` | 1.1.3 | | `leidenalg` | 0.9.1 | -| `python` | 3.11.0 | +| `python` | 3.12.0 | | `quarto` | 1.3.302 | | `scanpy` | 1.9.3 | From 4a06e971cb467bd9c7ac460ff72f77bd965e5ab2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 13:34:02 +0100 Subject: [PATCH 220/410] Use `nf-core/test-datasets` for test data --- conf/test.config | 2 +- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 2 +- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/test.config b/conf/test.config index 29f95c6..21e7c9d 100644 --- a/conf/test.config +++ b/conf/test.config @@ -20,7 +20,7 @@ params { max_time = '2.h' // Input and output - input = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 97deb41..8e4690e 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -6,7 +6,7 @@ nextflow_pipeline { test("Space Ranger FFPE v1 Standard") { when { params { - input = 'https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index d259b47..c67e138 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -6,7 +6,7 @@ nextflow_pipeline { test("Space Ranger FFPE v2 CytAssist") { when { params { - input = "https://raw.githubusercontent.com/fasterius/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" + input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" st_preprocess_min_counts = 5 From 7f1745c55dcd4627ce8c2d5faa4c8f947f4a0b51 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 14:39:03 +0100 Subject: [PATCH 221/410] Update Nextflow version for continuous integration --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a4ce80..f659755 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then echo matrix='["latest-everything"]' | tee -a $GITHUB_OUTPUT else - echo matrix='["latest-everything", "22.10.1"]' | tee -a $GITHUB_OUTPUT + echo matrix='["latest-everything", "23.04.0"]' | tee -a $GITHUB_OUTPUT fi test: From fa154a7d8196214f128d06d0923b910e80326407 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 16:50:22 +0100 Subject: [PATCH 222/410] Add missing citations --- CITATIONS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CITATIONS.md b/CITATIONS.md index bbffd40..fbdcf1c 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -14,14 +14,26 @@ > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: +- [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) + + > 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. +- [Quarto](https://quarto.org/) + + > Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048 + - [scanpy](https://github.com/theislab/scanpy) > Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). +- [Space Ranger](https://www.10xgenomics.com/support/software/space-ranger) + + > 10x Genomics Space Ranger 2.1.0 + - [SpatialDE](https://github.com/Teichlab/SpatialDE) > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). From 54bc5264e5fe4e614d8d89677e497f08bc26a929 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 16:50:45 +0100 Subject: [PATCH 223/410] Remove TODO from README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index c78e691..71689af 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,6 @@ the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spat > [!NOTE] > If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data. - - - An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. You can cite the `nf-core` publication as follows: From 0dc6bb3993b4cc3c89f3f9e1a67fb84cda662804 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 16:51:01 +0100 Subject: [PATCH 224/410] Remove commented module code --- modules/nf-core/fastqc/tests/main.nf.test | 68 ----------------------- 1 file changed, 68 deletions(-) diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index b9e8f92..6437a14 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -38,72 +38,4 @@ nextflow_process { ) } } -// TODO -// // -// // Test with paired-end data -// // -// workflow test_fastqc_paired_end { -// input = [ -// [id: 'test', single_end: false], // meta map -// [ -// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), -// file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true) -// ] -// ] - -// FASTQC ( input ) -// } - -// // -// // Test with interleaved data -// // -// workflow test_fastqc_interleaved { -// input = [ -// [id: 'test', single_end: false], // meta map -// file(params.test_data['sarscov2']['illumina']['test_interleaved_fastq_gz'], checkIfExists: true) -// ] - -// FASTQC ( input ) -// } - -// // -// // Test with bam data -// // -// workflow test_fastqc_bam { -// input = [ -// [id: 'test', single_end: false], // meta map -// file(params.test_data['sarscov2']['illumina']['test_paired_end_sorted_bam'], checkIfExists: true) -// ] - -// FASTQC ( input ) -// } - -// // -// // Test with multiple samples -// // -// workflow test_fastqc_multiple { -// input = [ -// [id: 'test', single_end: false], // meta map -// [ -// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), -// file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true), -// file(params.test_data['sarscov2']['illumina']['test2_1_fastq_gz'], checkIfExists: true), -// file(params.test_data['sarscov2']['illumina']['test2_2_fastq_gz'], checkIfExists: true) -// ] -// ] - -// FASTQC ( input ) -// } - -// // -// // Test with custom prefix -// // -// workflow test_fastqc_custom_prefix { -// input = [ -// [ id:'mysample', single_end:true ], // meta map -// file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) -// ] - -// FASTQC ( input ) -// } } From 6d3df45e4b83654d35ad187ef48fd1dbdaa3e322 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 17:01:18 +0100 Subject: [PATCH 225/410] Fix citation DOI formatting --- CITATIONS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index fbdcf1c..78a4ca4 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -12,7 +12,7 @@ - [anndata](https://github.com/theislab/anndata) - > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: + > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: https://doi.org/10.1101/2021.12.16.473007 - [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) @@ -28,7 +28,7 @@ - [scanpy](https://github.com/theislab/scanpy) - > Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). + > Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: https://doi.org/10.1186/s13059-017-1382-0 - [Space Ranger](https://www.10xgenomics.com/support/software/space-ranger) @@ -36,7 +36,7 @@ - [SpatialDE](https://github.com/Teichlab/SpatialDE) - > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). + > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: https://doi.org/10.1038/nmeth.4636 ## Software packaging/containerisation tools From 1c48ba8ddf7be4ef4bfe3536ef4aaea132fd4dfb Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 17:15:25 +0100 Subject: [PATCH 226/410] Add comment with Quarto-related env info --- conf/modules.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conf/modules.config b/conf/modules.config index f212649..8ce91cf 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -9,6 +9,8 @@ ext.prefix = File name prefix for output files. ---------------------------------------------------------------------------------------- */ + +// Environment specification needed for Quarto env { XDG_CACHE_HOME = "./.xdg_cache_home" XDG_DATA_HOME = "./.xdg_data_home" From 1dd396ac6bb737d18756c08145295bccc9ea0db0 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 17:51:50 +0100 Subject: [PATCH 227/410] Capitalise citations for AnnData and Scanpy --- CITATIONS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index 78a4ca4..b6b79a1 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,7 +10,7 @@ ## Pipeline tools -- [anndata](https://github.com/theislab/anndata) +- [AnnData](https://github.com/theislab/anndata) > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: https://doi.org/10.1101/2021.12.16.473007 @@ -26,7 +26,7 @@ > Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048 -- [scanpy](https://github.com/theislab/scanpy) +- [Scanpy](https://github.com/theislab/scanpy) > Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: https://doi.org/10.1186/s13059-017-1382-0 From 97667725673167780fa4597f749f933bde367766 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 17:52:42 +0100 Subject: [PATCH 228/410] Add citation and bibliography to MultiQC report --- lib/WorkflowSpatialtranscriptomics.groovy | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/WorkflowSpatialtranscriptomics.groovy b/lib/WorkflowSpatialtranscriptomics.groovy index c58273c..99fb1ad 100755 --- a/lib/WorkflowSpatialtranscriptomics.groovy +++ b/lib/WorkflowSpatialtranscriptomics.groovy @@ -50,14 +50,15 @@ class WorkflowSpatialtranscriptomics { public static String toolCitationText(params) { - // TODO nf-core: Optionally add in-text citation tools to this list. - // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "Tool (Foo et al. 2023)" : "", - // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ "Tools used in the workflow included:", + "AnnData (Virshup et al. 2021),", "FastQC (Andrews 2010),", - "MultiQC (Ewels et al. 2016)", - "." + "MultiQC (Ewels et al. 2016),", + "Quarto (Allaire et al. 2022),", + "Scanpy (Wolf et al. 2018),", + "Space Ranger (10x Genomics) and", + "SpatialDE (Svensson et al. 2018)." ].join(' ').trim() return citation_text @@ -65,12 +66,14 @@ class WorkflowSpatialtranscriptomics { public static String toolBibliographyText(params) { - // TODO Optionally add bibliographic entries to this list. - // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "
  • Author (2023) Pub name, Journal, DOI
  • " : "", - // Uncomment function in methodsDescriptionText to render in MultiQC report def reference_text = [ - "
  • Andrews S, (2010) FastQC, URL: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/).
  • ", - "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: /10.1093/bioinformatics/btw354
  • " + "
  • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: 10.1101/2021.12.16.473007
  • ", + "
  • Andrews S, (2010) FastQC, URL: bioinformatics.babraham.ac.uk.
  • ", + "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: 10.1093/bioinformatics/btw354
  • ", + "
  • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
  • ", + "
  • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
  • ", + "
  • 10x Genomics Space Ranger 2.1.0, URL: 10xgenomics.com/support/software/space-ranger
  • ", + "
  • Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: 10.1038/nmeth.4636
  • ", ].join(' ').trim() return reference_text @@ -89,10 +92,8 @@ class WorkflowSpatialtranscriptomics { // Tool references meta["tool_citations"] = "" meta["tool_bibliography"] = "" - - // TODO Only uncomment below if logic in toolCitationText/toolBibliographyText has been filled! - //meta["tool_citations"] = toolCitationText(params).replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") - //meta["tool_bibliography"] = toolBibliographyText(params) + meta["tool_citations"] = toolCitationText(params).replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") + meta["tool_bibliography"] = toolBibliographyText(params) def methods_text = mqc_methods_yaml.text From 71b2d5f746b53ac6fa63bb45569bec4e8a2d97e9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 18:10:15 +0100 Subject: [PATCH 229/410] Store UNTAR output in sample `data/` subdirectory --- conf/modules.config | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index 8ce91cf..eccef41 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -32,7 +32,15 @@ process { ] } - // store sample-specifc results in the per-sample subfolder + // Store sample-specific results in per-sample subfolders + withName: 'UNTAR_SPACERANGER_INPUT|UNTAR_DOWNSTREAM_INPUT' { + publishDir = [ + path: { "${params.outdir}/${meta.id}/data/untar" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + withName: FASTQC { publishDir = [ path: { "${params.outdir}/${meta.id}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, From 075f559df3febb782e8da0003eb848aa22f94349 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Dec 2023 18:17:11 +0100 Subject: [PATCH 230/410] Do not lint existence of `conf/igenomes.config` --- .nf-core.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.nf-core.yml b/.nf-core.yml index 3b33744..bc2e3d4 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,5 +1,7 @@ repository_type: pipeline lint: + actions_ci: False + files_exist: + - conf/igenomes.config files_unchanged: - .gitattributes - actions_ci: False From 32a8c0be9888eb7f4f77447b1df8a28646d1ff83 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 17:30:50 +0100 Subject: [PATCH 231/410] Do not publish UNTAR_* output by default Do not publish tar archives extracted by `UNTAR_*` processes by default, but add the `--keep_untar_output` parameter so that the user may do so if desired. --- conf/modules.config | 1 + nextflow.config | 3 +++ nextflow_schema.json | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/conf/modules.config b/conf/modules.config index eccef41..ad49855 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -35,6 +35,7 @@ process { // Store sample-specific results in per-sample subfolders withName: 'UNTAR_SPACERANGER_INPUT|UNTAR_DOWNSTREAM_INPUT' { publishDir = [ + enabled: params.keep_untar_output, path: { "${params.outdir}/${meta.id}/data/untar" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } diff --git a/nextflow.config b/nextflow.config index 6aeb506..ee8ad6f 100644 --- a/nextflow.config +++ b/nextflow.config @@ -23,6 +23,9 @@ params { max_multiqc_email_size = '25.MB' multiqc_methods_description = null + // Untar options + keep_untar_output = false + // Boilerplate options outdir = null publish_dir_mode = 'copy' diff --git a/nextflow_schema.json b/nextflow_schema.json index cd7f48f..a12fb0a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -43,6 +43,11 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, + "keep_untar_output": { + "type": "boolean", + "description": "Keep output files from extracted tar archives.", + "fa_icon": "fas fa-box-archive" + }, "email": { "type": "string", "description": "Email address for completion summary.", From f97ab3164a267028290052066b38740fee3a94e9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 17:34:04 +0100 Subject: [PATCH 232/410] Only publish final `st_adata_processed.h5ad` file Only publish the final `st_adata_processed.h5ad` file from the reports, as it contains all the information from previous files as well. This was already stated in the documentation, but the module configuration was erroneously publishing all `h5ad` files. --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index ad49855..9375f7c 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -70,7 +70,7 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "st_adata_*.h5ad" + pattern: "st_adata_processed.h5ad" ], [ path: { "${params.outdir}/${meta.id}/degs" }, From 9082b5506f492a1cba6f54bbfd3a46033b1a66d7 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 17:55:01 +0100 Subject: [PATCH 233/410] Add missing parameter icons to schema --- nextflow_schema.json | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index a12fb0a..802c358 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -72,62 +72,74 @@ "st_load_min_counts": { "type": "integer", "default": 1, - "description": "Minimum genes count" + "description": "Minimum genes count", + "fa_icon": "fas fa-hashtag" }, "st_load_min_cells": { "type": "integer", "default": 1, - "description": "Minimum cells count" + "description": "Minimum cells count", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_fig_size": { "type": "integer", "default": 6, - "description": "Figure size, inches" + "description": "Figure size, inches", + "fa_icon": "fas fa-up-right-and-down-left-from-center" }, "st_preprocess_min_counts": { "type": "integer", "default": 500, - "description": "Minimum UMI count" + "description": "Minimum UMI count", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_genes": { "type": "integer", "default": 250, - "description": "Minimum genes count" + "description": "Minimum genes count", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_cells": { "type": "integer", "default": 1, - "description": "Minimum cells count" + "description": "Minimum cells count", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_max_total_counts": { "type": "integer", "default": 10000, - "description": "Max total counts cutoff for histogram QC plot" + "description": "Max total counts cutoff for histogram QC plot", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_min_gene_counts": { "type": "integer", "default": 4000, - "description": "Min total gene counts cutoff for histogram QC plot" + "description": "Min total gene counts cutoff for histogram QC plot", + "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_bins": { "type": "integer", "default": 40, - "description": "Histogram QC plot number of bins" + "description": "Histogram QC plot number of bins", + "fa_icon": "fas fa-chart-simple" + }, + "st_cluster_resolution": { + "type": "number", + "default": 0.4, + "description": "Clustering resolution for ST spots", + "fa_icon": "fas fa-circle-nodes" }, "st_spatial_de_top_hgv": { "type": "integer", "default": 15, - "description": "Number of top highly variable genes to plot" + "description": "Number of top highly variable genes to plot", + "fa_icon": "fas fa-hashtag" }, "st_spatial_de_ncols": { "type": "integer", "default": 5, - "description": "Number of columns to group genes plots into" - }, - "st_cluster_resolution": { - "type": "number", - "default": 0.4, - "description": "Clustering resolution for ST spots" + "description": "Number of columns to group genes plots into", + "fa_icon": "fas fa-hashtag" } } }, From d5a58e138e5610869aa1a04dcd2a1b0b8c9c8bb2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 18:40:01 +0100 Subject: [PATCH 234/410] Rename `keep_untar_output` to `save_untar_output` --- conf/modules.config | 4 ++-- nextflow.config | 2 +- nextflow_schema.json | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 9375f7c..e517e87 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -32,10 +32,10 @@ process { ] } - // Store sample-specific results in per-sample subfolders + // Store sample-specific results in per-sample subdirectories withName: 'UNTAR_SPACERANGER_INPUT|UNTAR_DOWNSTREAM_INPUT' { publishDir = [ - enabled: params.keep_untar_output, + enabled: params.save_untar_output, path: { "${params.outdir}/${meta.id}/data/untar" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } diff --git a/nextflow.config b/nextflow.config index ee8ad6f..52b81ce 100644 --- a/nextflow.config +++ b/nextflow.config @@ -24,7 +24,7 @@ params { multiqc_methods_description = null // Untar options - keep_untar_output = false + save_untar_output = false // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 802c358..7e314fc 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -43,10 +43,10 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, - "keep_untar_output": { + "save_untar_output": { "type": "boolean", - "description": "Keep output files from extracted tar archives.", - "fa_icon": "fas fa-box-archive" + "description": "Save extracted tar archives of input data.", + "fa_icon": "fas fa-floppy-disk" }, "email": { "type": "string", From d03c251f235e384d11265a6309d9311dc13c753f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 18:45:16 +0100 Subject: [PATCH 235/410] Do not publish SR reference by default Do not publish extracted tar archives of the supplied Space Ranger reference by default, but allow the user to save the reference using the new `--spaceranger_save_reference` parameter. --- conf/modules.config | 11 +++++++++++ nextflow.config | 13 +++++++------ nextflow_schema.json | 5 +++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index e517e87..6eaad35 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -42,6 +42,16 @@ process { ] } + // Optionally save extracted Space Ranger reference archive + withName: 'SPACERANGER_UNTAR_REFERENCE' { + publishDir = [ + enabled: params.spaceranger_save_reference, + path: { "${params.outdir}/reference" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + withName: FASTQC { publishDir = [ path: { "${params.outdir}/${meta.id}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, @@ -60,6 +70,7 @@ process { ] } + // Store reports in sample-specific subdirectories withName: 'ST_READ_DATA|ST_QC_AND_NORMALISATION|ST_CLUSTERING|ST_SPATIAL_DE' { publishDir = [ [ diff --git a/nextflow.config b/nextflow.config index 52b81ce..b5f8900 100644 --- a/nextflow.config +++ b/nextflow.config @@ -15,16 +15,17 @@ params { // Spaceranger options spaceranger_reference = "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" spaceranger_probeset = null + spaceranger_save_reference = false // MultiQC options - multiqc_config = null - multiqc_title = null - multiqc_logo = null - max_multiqc_email_size = '25.MB' - multiqc_methods_description = null + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' + multiqc_methods_description = null // Untar options - save_untar_output = false + save_untar_output = false // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 7e314fc..b489a54 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -43,6 +43,11 @@ "description": "Location of Space Ranger probeset file", "fa_icon": "fas fa-file-csv" }, + "spaceranger_save_reference": { + "type": "boolean", + "description": "Save the extracted tar archive of the Space Ranger reference.", + "fa_icon": "fas fa-floppy-disk" + }, "save_untar_output": { "type": "boolean", "description": "Save extracted tar archives of input data.", From f50e5a9668e917c55e4d2be6b0301d96a6871b5d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Jan 2024 18:59:33 +0100 Subject: [PATCH 236/410] Add missing test profiles Add missing test profiles for testing the workflow from raw data, i.e. including Space Ranger processing. --- conf/test.config | 2 ++ conf/test_spaceranger_v1.config | 31 +++++++++++++++++++++++++++++++ conf/test_spaceranger_v2.config | 31 +++++++++++++++++++++++++++++++ nextflow.config | 8 ++++---- 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 conf/test_spaceranger_v1.config create mode 100644 conf/test_spaceranger_v2.config diff --git a/conf/test.config b/conf/test.config index 21e7c9d..dc13a86 100644 --- a/conf/test.config +++ b/conf/test.config @@ -23,6 +23,8 @@ params { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + + // Parameters st_preprocess_min_counts = 5 st_preprocess_min_genes = 3 outdir = 'results' diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config new file mode 100644 index 0000000..bc98725 --- /dev/null +++ b/conf/test_spaceranger_v1.config @@ -0,0 +1,31 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Space Ranger v1 test profile' + config_profile_description = 'Test all pipeline functionality, including Space Ranger v1' + + // Limit resources so that this can run on GitHub Actions + max_cpus = 2 + max_memory = '3.GB' + max_time = '2.h' + + // Input and output + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' + spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + + // Parameters + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 + outdir = 'results' +} diff --git a/conf/test_spaceranger_v2.config b/conf/test_spaceranger_v2.config new file mode 100644 index 0000000..3e7b3df --- /dev/null +++ b/conf/test_spaceranger_v2.config @@ -0,0 +1,31 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Space Ranger v2 test profile' + config_profile_description = 'Test all pipeline functionality, including Space Ranger v2' + + // Limit resources so that this can run on GitHub Actions + max_cpus = 2 + max_memory = '3.GB' + max_time = '2.h' + + // Input and output + input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + + // Parameters + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 + outdir = 'results' +} diff --git a/nextflow.config b/nextflow.config index b5f8900..a801be0 100644 --- a/nextflow.config +++ b/nextflow.config @@ -165,10 +165,10 @@ profiles { executor.cpus = 4 executor.memory = 8.GB } - test { includeConfig 'conf/test.config' } - test_spaceranger_ffpe_cytassist { includeConfig 'conf/test_spaceranger_ffpe_cytassist.config' } - test_spaceranger_ffpe_v1 { includeConfig 'conf/test_spaceranger_ffpe_v1.config' } - test_full { includeConfig 'conf/test_full.config' } + test { includeConfig 'conf/test.config' } + test_full { includeConfig 'conf/test_full.config' } + test_spaceranger_v1 { includeConfig 'conf/test_spaceranger_v1.config' } + test_spaceranger_v2 { includeConfig 'conf/test_spaceranger_v2.config' } } // Set default registry for Apptainer, Docker, Podman and Singularity independent of -profile From 648e4919da2fe138144768ae293f42806cdbfd20 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 Jan 2024 10:14:28 +0100 Subject: [PATCH 237/410] Clarify FASTQC module publishDir --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index 6eaad35..4c32b9b 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -54,7 +54,7 @@ process { withName: FASTQC { publishDir = [ - path: { "${params.outdir}/${meta.id}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, + path: { "${params.outdir}/${meta.id}/fastqc" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] From fa9f7aaa3e0b9bba3e0c098771f60913421541f9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 Jan 2024 10:38:11 +0100 Subject: [PATCH 238/410] Clarify SPACERANGER_COUNT publishDir note --- conf/modules.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/modules.config b/conf/modules.config index 4c32b9b..028c4cd 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -62,7 +62,7 @@ process { withName: SPACERANGER_COUNT { publishDir = [ - // NOTE: the sample name is already excluded in the path that's getting published. + // NOTE: the sample name is already included in the path that's getting published. // This publishDir directive puts spaceranger outputs in results/${meta.id}/outs path: { "${params.outdir}" }, mode: params.publish_dir_mode, From 7225543424b3da9dded141820498f282a99208de Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 Jan 2024 11:30:02 +0100 Subject: [PATCH 239/410] Re-order module config for clarify --- conf/modules.config | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 028c4cd..cbf2a7b 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -32,21 +32,21 @@ process { ] } - // Store sample-specific results in per-sample subdirectories - withName: 'UNTAR_SPACERANGER_INPUT|UNTAR_DOWNSTREAM_INPUT' { + // Optionally save extracted Space Ranger reference archive + withName: 'SPACERANGER_UNTAR_REFERENCE' { publishDir = [ - enabled: params.save_untar_output, - path: { "${params.outdir}/${meta.id}/data/untar" }, + enabled: params.spaceranger_save_reference, + path: { "${params.outdir}/reference" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] } - // Optionally save extracted Space Ranger reference archive - withName: 'SPACERANGER_UNTAR_REFERENCE' { + // Store sample-specific results in per-sample subdirectories + withName: 'UNTAR_SPACERANGER_INPUT|UNTAR_DOWNSTREAM_INPUT' { publishDir = [ - enabled: params.spaceranger_save_reference, - path: { "${params.outdir}/reference" }, + enabled: params.save_untar_output, + path: { "${params.outdir}/${meta.id}/data/untar" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] @@ -70,7 +70,6 @@ process { ] } - // Store reports in sample-specific subdirectories withName: 'ST_READ_DATA|ST_QC_AND_NORMALISATION|ST_CLUSTERING|ST_SPATIAL_DE' { publishDir = [ [ From 70726cd8e4aca8ab147ee4b9b4ceb3c9187b691b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 Jan 2024 11:35:50 +0100 Subject: [PATCH 240/410] Publish SR output in `{meta.id}/spaceranger/outs` Publish Space Ranger count output in `{meta.id}/spaceranger/outs` instead of just `{meta.id}/outs`, which adheres better to the "results/{software}" strategy for other modules. This was hard-coded in the Space Ranger module as `{meta.id}/outs`. This commit achieves the change by modifying the module code to instead be just `outs`, so that the exact publishDir may be specified in `modules.config` as `${params.outdir}/{meta.id}/spaceranger` instead. This did not work before, as modifying `modules.config` could just add paths on top of the already hard-coded `${params.outdir}/{meta.id}/outs` module publishDir. A PR with these changes will be submitted to the modules repo. --- conf/modules.config | 4 +- modules.json | 3 +- modules/nf-core/spaceranger/count/main.nf | 8 ++-- modules/nf-core/spaceranger/count/meta.yml | 2 +- .../spaceranger/count/spaceranger-count.diff | 47 +++++++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 modules/nf-core/spaceranger/count/spaceranger-count.diff diff --git a/conf/modules.config b/conf/modules.config index cbf2a7b..6cffaed 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -62,9 +62,7 @@ process { withName: SPACERANGER_COUNT { publishDir = [ - // NOTE: the sample name is already included in the path that's getting published. - // This publishDir directive puts spaceranger outputs in results/${meta.id}/outs - path: { "${params.outdir}" }, + path: { "${params.outdir}/${meta.id}/spaceranger" }, mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] diff --git a/modules.json b/modules.json index 0f162bb..b06cceb 100644 --- a/modules.json +++ b/modules.json @@ -23,7 +23,8 @@ "spaceranger/count": { "branch": "master", "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", - "installed_by": ["modules"] + "installed_by": ["modules"], + "patch": "modules/nf-core/spaceranger/count/spaceranger-count.diff" }, "untar": { "branch": "master", diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index c0f10e5..cac83e0 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -10,7 +10,7 @@ process SPACERANGER_COUNT { path(probeset) output: - tuple val(meta), path("**/outs/**"), emit: outs + tuple val(meta), path("outs/**"), emit: outs path "versions.yml", emit: versions when: @@ -46,6 +46,7 @@ process SPACERANGER_COUNT { $alignment \\ $slidefile \\ $args + mv ${prefix}/outs outs cat <<-END_VERSIONS > versions.yml "${task.process}": @@ -58,10 +59,9 @@ process SPACERANGER_COUNT { if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." } - def prefix = task.ext.prefix ?: "${meta.id}" """ - mkdir -p "${prefix}/outs/" - touch ${prefix}/outs/fake_file.txt + mkdir -p outs/ + touch outs/fake_file.txt cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml index 5a5073b..2811fd2 100644 --- a/modules/nf-core/spaceranger/count/meta.yml +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -83,7 +83,7 @@ output: - outs: type: file description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list - pattern: "${meta.id}/outs/*" + pattern: "outs/*" - versions: type: file description: File containing software versions diff --git a/modules/nf-core/spaceranger/count/spaceranger-count.diff b/modules/nf-core/spaceranger/count/spaceranger-count.diff new file mode 100644 index 0000000..e138217 --- /dev/null +++ b/modules/nf-core/spaceranger/count/spaceranger-count.diff @@ -0,0 +1,47 @@ +Changes in module 'nf-core/spaceranger/count' +--- modules/nf-core/spaceranger/count/meta.yml ++++ modules/nf-core/spaceranger/count/meta.yml +@@ -83,7 +83,7 @@ + - outs: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list +- pattern: "${meta.id}/outs/*" ++ pattern: "outs/*" + - versions: + type: file + description: File containing software versions + +--- modules/nf-core/spaceranger/count/main.nf ++++ modules/nf-core/spaceranger/count/main.nf +@@ -10,7 +10,7 @@ + path(probeset) + + output: +- tuple val(meta), path("**/outs/**"), emit: outs ++ tuple val(meta), path("outs/**"), emit: outs + path "versions.yml", emit: versions + + when: +@@ -46,6 +46,7 @@ + $alignment \\ + $slidefile \\ + $args ++ mv ${prefix}/outs outs + + cat <<-END_VERSIONS > versions.yml + "${task.process}": +@@ -58,10 +59,9 @@ + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." + } +- def prefix = task.ext.prefix ?: "${meta.id}" + """ +- mkdir -p "${prefix}/outs/" +- touch ${prefix}/outs/fake_file.txt ++ mkdir -p outs/ ++ touch outs/fake_file.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + +************************************************************ From 78963ce530d7b298e284ded8fd403026c1c98cd2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 3 Jan 2024 16:05:54 +0100 Subject: [PATCH 241/410] Update tests --- tests/pipeline/test_downstream.nf.test | 3 --- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 16 +++++----------- .../test_spaceranger_ffpe_v1.nf.test.snap | 9 +-------- .../test_spaceranger_ffpe_v2_cytassist.nf.test | 13 +++++-------- ...st_spaceranger_ffpe_v2_cytassist.nf.test.snap | 9 +-------- 5 files changed, 12 insertions(+), 38 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 0b74606..7ec809a 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -19,9 +19,6 @@ nextflow_pipeline { // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 8e4690e..928fa8a 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -24,9 +24,6 @@ nextflow_pipeline { // Data { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_norm.h5ad").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_plain.h5ad").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_raw.h5ad").exists() }, // Reports { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, @@ -37,20 +34,17 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, // Space Ranger - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/web_summary.html").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/web_summary.html").exists() }, { assert snapshot( - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5"), - path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/filtered_feature_bc_matrix.h5"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/filtered_feature_bc_matrix.h5"), )}, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/raw_feature_bc_matrix.h5").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/outs/spatial/tissue_positions.csv").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/raw_feature_bc_matrix.h5").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/spatial/tissue_positions.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("Visium_FFPE_Human_Ovarian_Cancer")} ) } } - - - } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 1ea6423..9bdda01 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,15 +1,8 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,905395042fa8b8a012718c1dbff39db4", - "st_qc_and_normalisation.html:md5,f89922b04edd7299e34ba1e43c257000" - ], - "timestamp": "2023-06-29T12:46:05+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-29T12:46:05+0000" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index c67e138..94f54d3 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -24,9 +24,6 @@ nextflow_pipeline { // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_norm.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_plain.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_raw.h5ad").exists() }, // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, @@ -37,13 +34,13 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, // Space Ranger - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/web_summary.html").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/web_summary.html").exists() }, { assert snapshot( - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5"), - path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/filtered_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/raw_feature_bc_matrix.h5"), + path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/filtered_feature_bc_matrix.h5"), )}, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/raw_feature_bc_matrix.h5").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/outs/spatial/tissue_positions.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/raw_feature_bc_matrix.h5").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/spatial/tissue_positions.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").text.contains("CytAssist_11mm_FFPE_Human_Glioblastoma_2_4")} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 3acc223..94b2a26 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,15 +1,8 @@ { - "reports": { - "content": [ - "st_clustering.html:md5,b52e0fd3843c595634f1f2c6dbf76a3a", - "st_qc_and_normalisation.html:md5,82746189bdc1bef051724cd843fe8b20" - ], - "timestamp": "2023-06-29T12:48:09+0000" - }, "software_versions": { "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2023-06-29T12:48:09+0000" } -} +} \ No newline at end of file From da040753bf94b7a29ac8fe6b4e2207e0a3165c0d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 12:04:52 +0100 Subject: [PATCH 242/410] Clean-up of intro doc --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 71689af..f8736dd 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,7 @@ nextflow run nf-core/spatialtranscriptomics \ ``` > [!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/usage/configuration#custom-configuration-files). For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialtranscriptomics/usage) and the [parameter documentation](https://nf-co.re/spatialtranscriptomics/parameters). @@ -74,18 +73,18 @@ nf-core/spatialtranscriptomics was originally developed by the Jackson Laboratory1, up to the [0.1.0](https://github.com/nf-core/spatialtranscriptomics/releases/tag/0.1.0) tag. It was further developed in a collaboration between the [National Bioinformatics Infrastructure Sweden](https://nbis.se/) and [National Genomics -Infastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/); +Infrastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/); it is currently developed and maintained by [Erik Fasterius](https://github.com/fasterius) and [Christophe Avenel](https://github.com/cavenel). Many thanks to others who have helped out along the way too, especially [Gregor Sturm](https://github.com/grst)! -1 Supported by grants from the US National Institutes of Health +_1 Supported by grants from the US National Institutes of Health [U24CA224067](https://reporter.nih.gov/project-details/10261367) and [U54AG075941](https://reporter.nih.gov/project-details/10376627). Original authors [Dr. Sergii Domanskyi](https://github.com/sdomanskyi), Prof. Jeffrey -Chuang and Dr. Anuj Srivastava. +Chuang and Dr. Anuj Srivastava._ ## Contributions and Support From 0a075bbbf812f2948e099b76b2e0a8d4adde01c4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 12:31:30 +0100 Subject: [PATCH 243/410] Clarify and fix formatting of usage docs --- docs/usage.md | 88 +++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 8010490..694218b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -14,13 +14,17 @@ in the examples below and depends on the input data type. Use this parameter to --input '[path to samplesheet file]' ``` -The workflow will automatically detect the samplesheet type and run the appropriate analysis steps. +There are two types of samplesheets that the pipeline can handle: those +specifying _raw data_ (to be analysed by Space Ranger) and _processed data_ +(_i.e._ already analysed by Space Ranger). The workflow will automatically +detect the samplesheet type and run the appropriate analysis steps. The two +types of samplesheet are described in the following sections. ### Raw spatial data -This section describes samplesheets for processing _raw spatial data_ yet to be analyzed with Space Ranger. +This section describes samplesheets for processing _raw spatial data_ yet to be analysed with Space Ranger. -Here is an example of a typical samplesheet for analyzing FFPE or fresh frozen (FF) data with bright field microscopy +Here is an example of a typical samplesheet for analysing FFPE or fresh frozen (FF) data with bright field microscopy imagery: ```no-highlight @@ -46,29 +50,30 @@ SAMPLE_1,fastqs_1/,cytassist_1.tif,V11J26,B1 SAMPLE_2,fastqs_2/,cytassist_2.tif,V11J26,B1 ``` -Depending on the experimental setup, (additional) color composite fluorescence images or dark background +Depending on the experimental setup, (additional) colour composite fluorescence images or dark background fluorescence images can be supplied using the `colorizedimage` or `darkimage` columns, respectively. Please refer to the following table for an overview of all supported columns: -| Column | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `sample` | Unique sample identifier. MUST match the prefix of the fastq files | -| `fastq_dir` | Path to directory where the sample FASTQ files are stored. May be a `.tar.gz` file instead of a directory. | -| `image` | Brightfield microscopy image | -| `cytaimage` | Brightfield tissue image captured with Cytassist device | -| `colorizedimage` | A color composite of one or more fluorescence image channels saved as a single-page, single-file color TIFF or JPEG | -| `darkimage` | Dark background fluorescence microscopy image | -| `slide` | The Visium slide ID used for the sequencing. | -| `area` | Which slide area contains the tissue sample. | -| `manual_alignment` | Path to the manual alignment file (optional) | -| `slidefile` | Slide specification as JSON. Overrides `slide` and `area` if specified. (optional) | - -> **NB:** -> -> - You need to specify _at least one_ of `image`, `cytaimage`, `darkimage`, `colorizedimage`. Most commonly, you'll -> specify `image` for bright field microscopy data, or `cytaimage` for tissue scans generated with the 10x Cyatassist -> device. Please refer to the [Space Ranger documentation](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger), how multiple image types can be combined. +| Column | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------- | +| `sample` | Unique sample identifier. MUST match the prefix of the fastq files | +| `fastq_dir` | Path to directory where the sample FASTQ files are stored. May be a `.tar.gz` file instead of a directory. | +| `image` | Brightfield microscopy image | +| `cytaimage` | Brightfield tissue image captured with Cytassist device | +| `colorizedimage` | A colour composite of one or more fluorescence image channels saved as a single-page, single-file colour TIFF or JPEG | +| `darkimage` | Dark background fluorescence microscopy image | +| `slide` | The Visium slide ID used for the sequencing. | +| `area` | Which slide area contains the tissue sample. | +| `manual_alignment` | Path to the manual alignment file (optional) | +| `slidefile` | Slide specification as JSON. Overrides `slide` and `area` if specified. (optional) | + +> [!NOTE] +> - You need to specify _at least one_ of `image`, `cytaimage`, `darkimage`, +> `colorizedimage`. Most commonly, you'll specify `image` for bright field +> microscopy data, or `cytaimage` for tissue scans generated with the 10x +> Cyatassist device. Please refer to the [Space Ranger documentation](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger), +> how multiple image types can be combined. > - The `manual_alignment` column is only required for samples for which a > manual alignment file is needed and can be ignored if you're using automatic > alignment. @@ -80,8 +85,8 @@ appropriate for your samples. ### Processed data -If your data has already been processed by Space Ranger and you are only interested in running downstream QC steps, -the samplesheet looks as follows: +If your data has already been processed by Space Ranger and you are only +interested in running downstream steps, the samplesheet looks as follows: ```no-highlight sample,spaceranger_dir @@ -118,15 +123,15 @@ path to its directory (or another link from the 10X website above) using the `--spaceranger_reference` parameter, otherwise the pipeline will download the default human reference for you automatically. -> **Important**: -> +> [!NOTE] > For FFPE and Cytassist experiments, you need to manually supply the appropriate probset using the `--spaceranger_probeset` parameter > Please refer to the [Spaceranger Downloads page](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) > to obtain the correct probeset. ## Analysis options -The pipeline is using Python and the scverse tools to do the downstream analysis (quality control, filtering, clustering, spatial differential equations). +The pipeline uses Python and the `scverse` tools to do the downstream analysis +(quality control, filtering, clustering, spatial differential equations). ### Parameters for Quality Control and Filtering: @@ -135,7 +140,7 @@ The following parameters are exposed for preprocessing: - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. - `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. -- `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (e.g., quality control plots). +- `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (_e.g._, quality control plots). - `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. @@ -153,14 +158,14 @@ The following parameters are exposed for preprocessing: The typical command for running the pipeline is as follows: ```bash -# Run the pipeline with raw data yet to be processed by Space Ranger -nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker - -# Run pipeline with data already processed by Space Ranger -nextflow run nf-core/spatialtranscriptomics --input samplesheet.csv --outdir -profile docker +nextflow run \ + nf-core/spatialtranscriptomics \ + --input \ + --outdir \ + -profile docker ``` -This will launch the pipeline with the docker configuration profile. See below for more information about profiles. +This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. Note that the pipeline will create the following files in your working directory: @@ -188,8 +193,8 @@ nextflow run nf-core/spatialtranscriptomics -profile docker -params-file params. with `params.yaml` containing: ```yaml -input: './samplesheet.csv' -outdir: './results/' +input: '' +outdir: '' <...> ``` @@ -211,7 +216,7 @@ First, go to the [nf-core/spatialtranscriptomics releases page](https://github.c This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. -To further assist in reproducbility, you can use share and re-use [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. +To further assist in reproducibility, you can use share and re-use [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. :::tip If you wish to share such profile (such as upload as supplementary material for academic publications), make sure to NOT include cluster specific paths to files, nor institutional specific profiles. @@ -229,10 +234,11 @@ Use this parameter to choose a configuration profile. Profiles can give configur Several generic profiles are bundled with the pipeline which instruct the pipeline to use software packaged using different methods (Docker, Singularity, Podman, Shifter, Charliecloud, Apptainer, Conda) - see below. +> [!INFO] > We highly recommend the use of Docker or Singularity containers for full -> pipeline reproducibility, however when this is not possible, Conda is also -> supported. Please note that Conda is not at all supported for Space Ranger -> processing, and only supported on non-ARM64 architectures for analyses +> pipeline reproducibility, however when this is not possible, Conda is +> partially supported. Please note that Conda is not at all supported for Space +> Ranger processing, and only supported on non-ARM64 architectures for analyses > downstream of Space Ranger. The pipeline also dynamically loads configurations from [https://github.com/nf-core/configs](https://github.com/nf-core/configs) when it runs, making multiple config profiles for various institutional clusters available at run time. For more information and to see if your system is available in these configs please see the [nf-core/configs documentation](https://github.com/nf-core/configs#documentation). @@ -240,7 +246,7 @@ The pipeline also dynamically loads configurations from [https://github.com/nf-c Note that multiple profiles can be loaded, for example: `-profile test,docker` - the order of arguments is important! They are loaded in sequence, so later profiles can overwrite earlier profiles. -If `-profile` is not specified, the pipeline will run locally and expect all software to be installed and available on the `PATH`. This is _not_ recommended, since it can lead to different results on different machines dependent on the computer enviroment. +If `-profile` is not specified, the pipeline will run locally and expect all software to be installed and available on the `PATH`. This is _not_ recommended, since it can lead to different results on different machines dependent on the computer environment. - `test` - A profile with a complete configuration for automated testing From 7528170276d83f3a70a0a8ec887dfb23f7e46b6f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 12:48:58 +0100 Subject: [PATCH 244/410] Fix minor formatting of schema params --- nextflow_schema.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index b489a54..793553d 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -40,7 +40,7 @@ "format": "file-path", "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", - "description": "Location of Space Ranger probeset file", + "description": "Location of Space Ranger probeset file.", "fa_icon": "fas fa-file-csv" }, "spaceranger_save_reference": { @@ -77,73 +77,73 @@ "st_load_min_counts": { "type": "integer", "default": 1, - "description": "Minimum genes count", + "description": "Minimum genes count.", "fa_icon": "fas fa-hashtag" }, "st_load_min_cells": { "type": "integer", "default": 1, - "description": "Minimum cells count", + "description": "Minimum cells count.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_fig_size": { "type": "integer", "default": 6, - "description": "Figure size, inches", + "description": "Figure size, inches.", "fa_icon": "fas fa-up-right-and-down-left-from-center" }, "st_preprocess_min_counts": { "type": "integer", "default": 500, - "description": "Minimum UMI count", + "description": "Minimum UMI count.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_genes": { "type": "integer", "default": 250, - "description": "Minimum genes count", + "description": "Minimum genes count.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_cells": { "type": "integer", "default": 1, - "description": "Minimum cells count", + "description": "Minimum cells count.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_max_total_counts": { "type": "integer", "default": 10000, - "description": "Max total counts cutoff for histogram QC plot", + "description": "Max total counts cutoff for histogram QC plot.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_min_gene_counts": { "type": "integer", "default": 4000, - "description": "Min total gene counts cutoff for histogram QC plot", + "description": "Min total gene counts cutoff for histogram QC plot.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_bins": { "type": "integer", "default": 40, - "description": "Histogram QC plot number of bins", + "description": "Histogram QC plot number of bins.", "fa_icon": "fas fa-chart-simple" }, "st_cluster_resolution": { "type": "number", "default": 0.4, - "description": "Clustering resolution for ST spots", + "description": "Clustering resolution for ST spots.", "fa_icon": "fas fa-circle-nodes" }, "st_spatial_de_top_hgv": { "type": "integer", "default": 15, - "description": "Number of top highly variable genes to plot", + "description": "Number of top highly variable genes to plot.", "fa_icon": "fas fa-hashtag" }, "st_spatial_de_ncols": { "type": "integer", "default": 5, - "description": "Number of columns to group genes plots into", + "description": "Number of columns to group genes plots into.", "fa_icon": "fas fa-hashtag" } } From 819041083aa0d7ad2eacdca54a2301146cab3d6b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 12:49:20 +0100 Subject: [PATCH 245/410] Add additional schema help texts --- nextflow_schema.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 793553d..5cab25c 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -19,7 +19,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Path to comma-separated file containing information about the samples in the experiment.", - "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/spatialtranscriptomics/usage#samplesheet-input).", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline, use this parameter to specify its location. It has to be a comma-separated file with 2 or 5 columns, plus a header row. See [usage docs](https://nf-co.re/spatialtranscriptomics/usage#samplesheet-input).", "fa_icon": "fas fa-file-csv" }, "outdir": { @@ -46,11 +46,13 @@ "spaceranger_save_reference": { "type": "boolean", "description": "Save the extracted tar archive of the Space Ranger reference.", + "help_text": "By default, extracted versions of archived Space Ranger reference data will not be saved to the results directory. Specify this flag (or set to true in your config file) to copy these files to the results directory when complete.", "fa_icon": "fas fa-floppy-disk" }, "save_untar_output": { "type": "boolean", "description": "Save extracted tar archives of input data.", + "help_text": "By default, extracted versions of archived input data will not be saved to the results directory. Specify this flag (or set to true in your config file) to copy these files to the results directory when complete.", "fa_icon": "fas fa-floppy-disk" }, "email": { From cefbabb60c1e1c5abf702bc53fc677b5f75e3c24 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 13:20:51 +0100 Subject: [PATCH 246/410] Add additional schema categories; add help texts --- nextflow_schema.json | 64 +++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 5cab25c..b1bdb93 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -5,6 +5,7 @@ "description": "Spatial Transcriptomics", "type": "object", "definitions": { + "input_output_options": { "title": "Input/output options", "type": "object", @@ -28,13 +29,27 @@ "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", "fa_icon": "fas fa-folder-open" }, - "spaceranger_reference": { + "email": { "type": "string", - "format": "file-path", - "description": "Location of Space Ranger reference directory. May be packed as `tar.gz` file.", - "fa_icon": "fas fa-folder-open", - "default": "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" + "description": "Email address for completion summary.", + "fa_icon": "fas fa-envelope", + "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" }, + "multiqc_title": { + "type": "string", + "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", + "fa_icon": "fas fa-file-signature" + } + } + }, + + "spaceranger_options": { + "title": "Space Ranger options", + "type": "object", + "fa_icon": "fas fa-rocket", + "description": "Options related to Space Ranger execution and raw spatial data processing", + "properties": { "spaceranger_probeset": { "type": "string", "format": "file-path", @@ -43,6 +58,23 @@ "description": "Location of Space Ranger probeset file.", "fa_icon": "fas fa-file-csv" }, + "spaceranger_reference": { + "type": "string", + "format": "file-path", + "description": "Location of Space Ranger reference directory. May be packed as `tar.gz` file.", + "help_text": "Please see the [10x website](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) to download either of the supported human or mouse references. If not specified the GRCh38 human reference is automatically downladed and used.", + "fa_icon": "fas fa-folder-open", + "default": "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" + } + } + }, + + "optional_outputs": { + "title": "Optional outputs", + "type": "object", + "fa_icon": "fas fa-floppy-disk", + "description": "Additional intermediate output files that can be optionally saved.", + "properties": { "spaceranger_save_reference": { "type": "boolean", "description": "Save the extracted tar archive of the Space Ranger reference.", @@ -54,18 +86,6 @@ "description": "Save extracted tar archives of input data.", "help_text": "By default, extracted versions of archived input data will not be saved to the results directory. Specify this flag (or set to true in your config file) to copy these files to the results directory when complete.", "fa_icon": "fas fa-floppy-disk" - }, - "email": { - "type": "string", - "description": "Email address for completion summary.", - "fa_icon": "fas fa-envelope", - "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", - "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" - }, - "multiqc_title": { - "type": "string", - "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", - "fa_icon": "fas fa-file-signature" } } }, @@ -73,8 +93,8 @@ "analysis_options": { "title": "Analysis option", "type": "object", + "fa_icon": "fas fa-magnifying-glass-chart", "description": "Define options for each tool in the pipeline", - "default": "", "properties": { "st_load_min_counts": { "type": "integer", @@ -199,6 +219,7 @@ } } }, + "max_job_request_options": { "title": "Max job request options", "type": "object", @@ -234,6 +255,7 @@ } } }, + "generic_options": { "title": "Generic options", "type": "object", @@ -350,6 +372,12 @@ { "$ref": "#/definitions/input_output_options" }, + { + "$ref": "#/definitions/spaceranger_options" + }, + { + "$ref": "#/definitions/optional_outputs" + }, { "$ref": "#/definitions/analysis_options" }, From 35b306d17c464c07b7eb9703dfc859e74c75e5d4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 13:24:08 +0100 Subject: [PATCH 247/410] Fix formatting with prettier --- docs/usage.md | 1 + nextflow_schema.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index 694218b..e255a6f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -69,6 +69,7 @@ Please refer to the following table for an overview of all supported columns: | `slidefile` | Slide specification as JSON. Overrides `slide` and `area` if specified. (optional) | > [!NOTE] +> > - You need to specify _at least one_ of `image`, `cytaimage`, `darkimage`, > `colorizedimage`. Most commonly, you'll specify `image` for bright field > microscopy data, or `cytaimage` for tissue scans generated with the 10x diff --git a/nextflow_schema.json b/nextflow_schema.json index b1bdb93..9e39722 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -5,7 +5,6 @@ "description": "Spatial Transcriptomics", "type": "object", "definitions": { - "input_output_options": { "title": "Input/output options", "type": "object", From 70960c3c2c2dfd845c8236ad53b94add270bf27c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 16:35:29 +0100 Subject: [PATCH 248/410] Remove unused `st_load` parameters --- conf/analysis.config | 4 ---- nextflow_schema.json | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/conf/analysis.config b/conf/analysis.config index 5e062eb..cc08da4 100644 --- a/conf/analysis.config +++ b/conf/analysis.config @@ -4,10 +4,6 @@ Default config options params { - // Data loading - st_load_min_counts = 1 - st_load_min_cells = 1 - // Preprocessing, QC and normalisation st_preprocess_fig_size = 6 st_preprocess_min_counts = 500 diff --git a/nextflow_schema.json b/nextflow_schema.json index 9e39722..670ad73 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -95,18 +95,6 @@ "fa_icon": "fas fa-magnifying-glass-chart", "description": "Define options for each tool in the pipeline", "properties": { - "st_load_min_counts": { - "type": "integer", - "default": 1, - "description": "Minimum genes count.", - "fa_icon": "fas fa-hashtag" - }, - "st_load_min_cells": { - "type": "integer", - "default": 1, - "description": "Minimum cells count.", - "fa_icon": "fas fa-hashtag" - }, "st_preprocess_fig_size": { "type": "integer", "default": 6, From 2eb7312cb2b5542d7e36f5c29d4f01fc6dbc5315 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 16:38:09 +0100 Subject: [PATCH 249/410] Pass `st_spatial_de_top_hvg` param to analysis --- conf/analysis.config | 2 +- modules/local/st_spatial_de.nf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/conf/analysis.config b/conf/analysis.config index cc08da4..5fad12f 100644 --- a/conf/analysis.config +++ b/conf/analysis.config @@ -17,6 +17,6 @@ params { st_cluster_resolution = 1 // Spatial differential expression - st_spatial_de_top_hgv = 15 + st_spatial_de_top_hvg = 15 st_spatial_de_ncols = 5 } diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 14dbc18..6f0f617 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -37,6 +37,7 @@ process ST_SPATIAL_DE { --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P numberOfColumns:${params.st_spatial_de_ncols} \ + -P plotTopHVG:${params.st_spatial_de_top_hvg} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv From a4fcad2b6613f29d8239711bc98976d6352f1de1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 16:38:56 +0100 Subject: [PATCH 250/410] Also use `st_spatial_de_top_hvg` for HVG table --- bin/st_spatial_de.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index f1f3265..d8a407a 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -76,7 +76,7 @@ Then we can inspect significant genes that varies in space and visualize them wi ```{python} results_tab = st_adata.var.sort_values("qval", ascending=True) results_tab.to_csv(saveSpatialDEFileName) -results_tab.head(10) +results_tab.head(plotTopHVG) ``` ```{python} From e8b627a411997d14d244bc0f609fc1d68fb9cbbf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 17:41:40 +0100 Subject: [PATCH 251/410] Clarify analysis params; add help text --- nextflow_schema.json | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 670ad73..19c226d 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -5,6 +5,7 @@ "description": "Spatial Transcriptomics", "type": "object", "definitions": { + "input_output_options": { "title": "Input/output options", "type": "object", @@ -90,33 +91,33 @@ }, "analysis_options": { - "title": "Analysis option", + "title": "Analysis options", "type": "object", "fa_icon": "fas fa-magnifying-glass-chart", - "description": "Define options for each tool in the pipeline", + "description": "Options related to the downstream analyses performed by the pipeline.", "properties": { "st_preprocess_fig_size": { "type": "integer", "default": 6, - "description": "Figure size, inches.", + "description": "The size of the QC figures, in inches.", "fa_icon": "fas fa-up-right-and-down-left-from-center" }, "st_preprocess_min_counts": { "type": "integer", "default": 500, - "description": "Minimum UMI count.", + "description": "The minimum number of UMIs needed in a spot for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_genes": { "type": "integer", "default": 250, - "description": "Minimum genes count.", + "description": "The minimum number of expressed genes in a spot needed for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_min_cells": { "type": "integer", "default": 1, - "description": "Minimum cells count.", + "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", "fa_icon": "fas fa-hashtag" }, "st_preprocess_hist_qc_max_total_counts": { @@ -134,25 +135,27 @@ "st_preprocess_hist_qc_bins": { "type": "integer", "default": 40, - "description": "Histogram QC plot number of bins.", + "description": "The number of bins for the QC histogram plots.", "fa_icon": "fas fa-chart-simple" }, "st_cluster_resolution": { "type": "number", "default": 0.4, - "description": "Clustering resolution for ST spots.", + "description": "The resolution for the clustering of the spots.", + "help_text": "The resolution controls the coarseness of the clustering, where a higher resolution leads to more clusters.", "fa_icon": "fas fa-circle-nodes" }, - "st_spatial_de_top_hgv": { + "st_spatial_de_top_hvg": { "type": "integer", "default": 15, - "description": "Number of top highly variable genes to plot.", + "description": "The number of top spatially highly variable genes to plot.", "fa_icon": "fas fa-hashtag" }, "st_spatial_de_ncols": { "type": "integer", "default": 5, - "description": "Number of columns to group genes plots into.", + "description": "Number of columns to group gene plots into.", + "help_text": "The default, 5, will plot the top spatially highly variable genes into groups of 5 plots per row. This, in combinationation with the default number of top HVGs to plot (15) will yield three rows with 5 plots each.", "fa_icon": "fas fa-hashtag" } } From 841dcfd2c6c21fea9b547ae29f550d84e3d065cf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 18:04:31 +0100 Subject: [PATCH 252/410] Move analysis params to `nextflow.config` --- conf/analysis.config | 22 ---------------------- nextflow.config | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 25 deletions(-) delete mode 100644 conf/analysis.config diff --git a/conf/analysis.config b/conf/analysis.config deleted file mode 100644 index 5fad12f..0000000 --- a/conf/analysis.config +++ /dev/null @@ -1,22 +0,0 @@ -/* -Default config options -*/ - -params { - - // Preprocessing, QC and normalisation - st_preprocess_fig_size = 6 - st_preprocess_min_counts = 500 - st_preprocess_min_genes = 250 - st_preprocess_min_cells = 1 - st_preprocess_hist_qc_max_total_counts = 10000 - st_preprocess_hist_qc_min_gene_counts = 4000 - st_preprocess_hist_qc_bins = 40 - - // Clustering - st_cluster_resolution = 1 - - // Spatial differential expression - st_spatial_de_top_hvg = 15 - st_spatial_de_ncols = 5 -} diff --git a/nextflow.config b/nextflow.config index a801be0..e351f8b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -17,6 +17,22 @@ params { spaceranger_probeset = null spaceranger_save_reference = false + // Preprocessing, QC and normalisation + st_preprocess_fig_size = 6 + st_preprocess_min_counts = 500 + st_preprocess_min_genes = 250 + st_preprocess_min_cells = 1 + st_preprocess_hist_qc_max_total_counts = 10000 + st_preprocess_hist_qc_min_gene_counts = 4000 + st_preprocess_hist_qc_bins = 40 + + // Clustering + st_cluster_resolution = 1 + + // Spatial differential expression + st_spatial_de_top_hvg = 15 + st_spatial_de_ncols = 5 + // MultiQC options multiqc_config = null multiqc_title = null @@ -64,9 +80,6 @@ params { // Load base.config by default for all pipelines includeConfig 'conf/base.config' -// Default analysis parameters -includeConfig 'conf/analysis.config' - // Load nf-core custom profiles from different Institutions try { includeConfig "${params.custom_config_base}/nfcore_custom.config" From 06ee063749b319b6dd3112f3c479335594e6f1a5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 18:06:13 +0100 Subject: [PATCH 253/410] Fix prettier formatting --- nextflow_schema.json | 1 - 1 file changed, 1 deletion(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 19c226d..31648bf 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -5,7 +5,6 @@ "description": "Spatial Transcriptomics", "type": "object", "definitions": { - "input_output_options": { "title": "Input/output options", "type": "object", From 6fcdce5cb81212b4e7156a1b96a9061bd305211d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 18:10:12 +0100 Subject: [PATCH 254/410] Update `fastqc` module --- modules.json | 2 +- modules/nf-core/fastqc/tests/main.nf.test | 203 ++++++++++++++++-- .../nf-core/fastqc/tests/main.nf.test.snap | 12 +- 3 files changed, 203 insertions(+), 14 deletions(-) diff --git a/modules.json b/modules.json index b06cceb..922f593 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "fastqc": { "branch": "master", - "git_sha": "65ad3e0b9a4099592e1102e92e10455dc661cf53", + "git_sha": "617777a807a1770f73deb38c80004bac06807eef", "installed_by": ["modules"] }, "multiqc": { diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index 6437a14..ad9bc54 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -3,23 +3,21 @@ nextflow_process { name "Test Process FASTQC" script "../main.nf" process "FASTQC" + tag "modules" tag "modules_nfcore" tag "fastqc" - test("Single-Read") { + test("sarscov2 single-end [fastq]") { when { - params { - outdir = "$outputDir" - } process { """ input[0] = [ - [ id: 'test', single_end:true ], - [ - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) - ] + [ id: 'test', single_end:true ], + [ + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) + ] ] """ } @@ -28,14 +26,195 @@ 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.get(0).get(1) ==~ ".*/test_fastqc.html" }, - { assert path(process.out.html.get(0).get(1)).getText().contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match("versions") }, - { assert process.out.zip.get(0).get(1) ==~ ".*/test_fastqc.zip" } + + { 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("versions") } + ) + } + } + + test("sarscov2 paired-end [fastq]") { + + when { + process { + """ + input[0] = [ + [id: 'test', single_end: false], // meta map + [ + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), + file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true) + ] + ] + """ + } + } + + 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("versions") } + ) + } + } + + test("sarscov2 interleaved [fastq]") { + + when { + process { + """ + input[0] = [ + [id: 'test', single_end: false], // meta map + file(params.test_data['sarscov2']['illumina']['test_interleaved_fastq_gz'], checkIfExists: true) + ] + """ + } + } + + 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("versions") } + ) + } + } + + test("sarscov2 paired-end [bam]") { + + when { + process { + """ + input[0] = [ + [id: 'test', single_end: false], // meta map + file(params.test_data['sarscov2']['illumina']['test_paired_end_sorted_bam'], checkIfExists: true) + ] + """ + } + } + + 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("versions") } ) } } + + test("sarscov2 multiple [fastq]") { + + when { + process { + """ + input[0] = [ + [id: 'test', single_end: false], // meta map + [ + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), + file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true), + file(params.test_data['sarscov2']['illumina']['test2_1_fastq_gz'], checkIfExists: true), + file(params.test_data['sarscov2']['illumina']['test2_2_fastq_gz'], checkIfExists: true) + ] + ] + """ + } + } + + 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("versions") } + ) + } + } + + test("sarscov2 custom_prefix") { + + when { + process { + """ + input[0] = [ + [ id:'mysample', single_end:true ], // meta map + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) + ] + """ + } + } + + 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("versions") } + ) + } + } + + test("sarscov2 single-end [fastq] - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id: 'test', single_end:true ], + [ + file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) + ] + ] + """ + } + } + + 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() } + ) + } + } + } diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap index 636a32c..5ef5afb 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -1,10 +1,20 @@ { + "sarscov2 single-end [fastq] - stub": { + "content": [ + [ + "test.html", + "test.zip", + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "timestamp": "2023-12-29T02:48:05.126117287" + }, "versions": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], - "timestamp": "2023-10-09T23:40:54+0000" + "timestamp": "2023-12-29T02:46:49.507942667" } } \ No newline at end of file From 0fe89c8c0ca4163827bac7b077a691cf239a3212 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Jan 2024 18:10:40 +0100 Subject: [PATCH 255/410] Update `multiqc` module --- modules.json | 2 +- modules/nf-core/multiqc/main.nf | 2 +- modules/nf-core/multiqc/meta.yml | 1 - modules/nf-core/multiqc/tests/main.nf.test | 48 +++++++++++++------ .../nf-core/multiqc/tests/main.nf.test.snap | 21 ++++++++ 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 modules/nf-core/multiqc/tests/main.nf.test.snap diff --git a/modules.json b/modules.json index 922f593..ad78221 100644 --- a/modules.json +++ b/modules.json @@ -17,7 +17,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "4ab13872435962dadc239979554d13709e20bf29", + "git_sha": "642a0d8afe373ac45244a7947fb8a6c0a5a312d4", "installed_by": ["modules"] }, "spaceranger/count": { diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 00cc48d..70708f3 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -43,7 +43,7 @@ process MULTIQC { stub: """ - touch multiqc_data + mkdir multiqc_data touch multiqc_plots touch multiqc_report.html diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index f1aa660..45a9bc3 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -1,4 +1,3 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: multiqc description: Aggregate results from bioinformatics analyses across many samples into a single report keywords: diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test index c2dad21..d0438ed 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -7,12 +7,9 @@ nextflow_process { tag "modules_nfcore" tag "multiqc" - test("MULTIQC: FASTQC") { + test("sarscov2 single-end [fastqc]") { when { - params { - outdir = "$outputDir" - } process { """ input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) @@ -26,20 +23,17 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert path(process.out.report.get(0)).exists() }, - { assert path(process.out.data.get(0)).exists() }, - { assert path(process.out.versions.get(0)).getText().contains("multiqc") } + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.versions).match("versions") } ) } } - test("MULTIQC: FASTQC and a config file") { + test("sarscov2 single-end [fastqc] [config]") { when { - params { - outdir = "$outputDir" - } process { """ input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) @@ -53,9 +47,35 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert path(process.out.report.get(0)).exists() }, - { assert path(process.out.data.get(0)).exists() }, - { assert path(process.out.versions.get(0)).getText().contains("multiqc") } + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.versions).match("versions") } + ) + } + } + + test("sarscov2 single-end [fastqc] - stub") { + + options "-stub" + + when { + process { + """ + input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) + input[1] = [] + input[2] = [] + input[3] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.report.collect { file(it).getName() } + + process.out.data.collect { file(it).getName() } + + process.out.plots.collect { file(it).getName() } + + process.out.versions ).match() } ) } diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap new file mode 100644 index 0000000..d087a9d --- /dev/null +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -0,0 +1,21 @@ +{ + "versions": { + "content": [ + [ + "versions.yml:md5,f81e19ab3a8e2b6f2b5d22078117df71" + ] + ], + "timestamp": "2023-12-30T00:26:14.048089591" + }, + "sarscov2 single-end [fastqc] - stub": { + "content": [ + [ + "multiqc_report.html", + "multiqc_data", + "multiqc_plots", + "versions.yml:md5,f81e19ab3a8e2b6f2b5d22078117df71" + ] + ], + "timestamp": "2023-12-30T00:26:52.963964055" + } +} \ No newline at end of file From 47f4f7f37fc33b2091bcd8839b96ec852e275dd8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 5 Jan 2024 13:46:45 +0100 Subject: [PATCH 256/410] Change default test profile Space Ranger profile Change the default test profile from only running downstream analyses to including Space Ranger (v2) processing. This is a better test of pipeline functionality, as it includes all of the steps that the pipeline could include and not just downstream processing. --- conf/test.config | 4 ++-- ...test_spaceranger_v2.config => test_downstream.config} | 8 ++++---- conf/test_spaceranger_v1.config | 4 ++-- nextflow.config | 4 ++-- tests/pipeline/test_downstream.nf.test | 9 ++++++++- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 2 +- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 8 ++------ 7 files changed, 21 insertions(+), 18 deletions(-) rename conf/{test_spaceranger_v2.config => test_downstream.config} (73%) diff --git a/conf/test.config b/conf/test.config index dc13a86..b707385 100644 --- a/conf/test.config +++ b/conf/test.config @@ -12,7 +12,7 @@ params { config_profile_name = 'Test profile' - config_profile_description = 'Test pipeline for post-Space Ranger functionality' + config_profile_description = 'Test pipeline functionality, including Space Ranger v2' // Limit resources so that this can run on GitHub Actions max_cpus = 2 @@ -20,7 +20,7 @@ params { max_time = '2.h' // Input and output - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" diff --git a/conf/test_spaceranger_v2.config b/conf/test_downstream.config similarity index 73% rename from conf/test_spaceranger_v2.config rename to conf/test_downstream.config index 3e7b3df..971f16b 100644 --- a/conf/test_spaceranger_v2.config +++ b/conf/test_downstream.config @@ -5,14 +5,14 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger, --outdir + nextflow run nf-core/spatialtranscriptomics -profile test_downstream, --outdir ---------------------------------------------------------------------------------------- */ params { - config_profile_name = 'Space Ranger v2 test profile' - config_profile_description = 'Test all pipeline functionality, including Space Ranger v2' + config_profile_name = 'Downstream test profile' + config_profile_description = 'Test pipeline for downstream (post-Space Ranger) functionality' // Limit resources so that this can run on GitHub Actions max_cpus = 2 @@ -20,7 +20,7 @@ params { max_time = '2.h' // Input and output - input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index bc98725..67cf02a 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -5,14 +5,14 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger, --outdir + nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger_v1, --outdir ---------------------------------------------------------------------------------------- */ params { config_profile_name = 'Space Ranger v1 test profile' - config_profile_description = 'Test all pipeline functionality, including Space Ranger v1' + config_profile_description = 'Test pipeline functionality, including Space Ranger v1' // Limit resources so that this can run on GitHub Actions max_cpus = 2 diff --git a/nextflow.config b/nextflow.config index e351f8b..b25e307 100644 --- a/nextflow.config +++ b/nextflow.config @@ -179,9 +179,9 @@ profiles { executor.memory = 8.GB } test { includeConfig 'conf/test.config' } - test_full { includeConfig 'conf/test_full.config' } test_spaceranger_v1 { includeConfig 'conf/test_spaceranger_v1.config' } - test_spaceranger_v2 { includeConfig 'conf/test_spaceranger_v2.config' } + test_downstream { includeConfig 'conf/test_downstream.config' } + test_full { includeConfig 'conf/test_full.config' } } // Set default registry for Apptainer, Docker, Podman and Singularity independent of -profile diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 7ec809a..11d9d3a 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -6,7 +6,14 @@ nextflow_pipeline { test("Downstream FFPE v2 CytAssist") { when { params { - // This is the default `test` profile; params are not necessary + // Input and output + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + + // Parameters + st_preprocess_min_counts = 5 + st_preprocess_min_genes = 3 outdir = "$outputDir" } } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 928fa8a..0421158 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -1,5 +1,5 @@ nextflow_pipeline { - name "Test full workflow (incl. Space Ranger)" + name "Test full workflow (incl. Space Ranger v1)" script "main.nf" tag "pipeline" diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 94f54d3..e9ee204 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -1,16 +1,12 @@ nextflow_pipeline { - name "Test full workflow (incl. Space Ranger)" + name "Test full workflow (incl. Space Ranger v2)" script "main.nf" tag "pipeline" test("Space Ranger FFPE v2 CytAssist") { when { params { - input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + // This is the default `test` profile; params are not necessary outdir = "$outputDir" } } From 2c5d1b78468442f8720d001e55b35b2c10e8fb8d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 5 Jan 2024 14:04:51 +0100 Subject: [PATCH 257/410] Remove unintuitive `st_preprocess_fig_size` param Remove the unintuitive `st_preprocess_fig_size` parameter. This was only used one occasion in the pipeline, where it only affected a single report figure - all other figures hard hard-coded sizes. This brings all figures in line in that all sizes are hard-coded. This should possibly be changed in the future, but giving users the ability to change figure sizes on a pipeline-level would be difficult, since it may be that not all figures can look good with the same size across the board. --- bin/st_qc_and_normalisation.qmd | 19 +++++++++---------- docs/usage.md | 1 - modules/local/st_qc_and_normalisation.nf | 1 - nextflow.config | 1 - nextflow_schema.json | 6 ------ 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 14a347e..d526d7d 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -24,7 +24,6 @@ resolution = 1 saveFileST = None rawAdata = None # Name of the h5ad file -pltFigSize = 6 # Figure size minCounts = 500 # Min counts per spot minGenes = 250 # Min genes per spot minCells = 1 # Min cells per gene @@ -44,7 +43,7 @@ import matplotlib.pyplot as plt import seaborn as sns from IPython.display import display, Markdown -plt.rcParams["figure.figsize"] = (pltFigSize, pltFigSize) +plt.rcParams["figure.figsize"] = (6, 6) ``` ## Anndata object @@ -113,15 +112,15 @@ p = sns.histplot( st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] ).set(title=f"Total counts") p = sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1] ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") p = sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, ax=axs[1, 0] ).set(title=f"Genes by counts") p = sns.histplot( @@ -186,15 +185,15 @@ p = sns.histplot( st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] ).set(title=f"Total counts") p = sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], kde=True, bins=histplotQCbins, ax=axs[0, 1] ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") p = sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, ax=axs[1, 0] ).set(title=f"Genes by counts") p = sns.histplot( diff --git a/docs/usage.md b/docs/usage.md index e255a6f..13a6921 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -141,7 +141,6 @@ The following parameters are exposed for preprocessing: - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. - `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. -- `--st_preprocess_fig_size`: The figure size for the plots generated during preprocessing (_e.g._, quality control plots). - `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 6405c12..e268ffb 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -38,7 +38,6 @@ process ST_QC_AND_NORMALISATION { quarto render ${report} \ --output st_qc_and_normalisation.html \ -P rawAdata:${st_raw} \ - -P pltFigSize:${params.st_preprocess_fig_size} \ -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ -P minCells:${params.st_preprocess_min_cells} \ diff --git a/nextflow.config b/nextflow.config index b25e307..58fb589 100644 --- a/nextflow.config +++ b/nextflow.config @@ -18,7 +18,6 @@ params { spaceranger_save_reference = false // Preprocessing, QC and normalisation - st_preprocess_fig_size = 6 st_preprocess_min_counts = 500 st_preprocess_min_genes = 250 st_preprocess_min_cells = 1 diff --git a/nextflow_schema.json b/nextflow_schema.json index 31648bf..8fce53e 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -95,12 +95,6 @@ "fa_icon": "fas fa-magnifying-glass-chart", "description": "Options related to the downstream analyses performed by the pipeline.", "properties": { - "st_preprocess_fig_size": { - "type": "integer", - "default": 6, - "description": "The size of the QC figures, in inches.", - "fa_icon": "fas fa-up-right-and-down-left-from-center" - }, "st_preprocess_min_counts": { "type": "integer", "default": 500, From dd6a2bc5d348ff7f7a12bf070560578ba341de29 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 10:41:18 +0100 Subject: [PATCH 258/410] Update `custom/dumpsoftwareversions` module --- modules.json | 2 +- .../dumpsoftwareversions/environment.yml | 2 +- .../custom/dumpsoftwareversions/main.nf | 4 +- .../dumpsoftwareversions/tests/main.nf.test | 7 ++- .../tests/main.nf.test.snap | 50 +++++++++++-------- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/modules.json b/modules.json index ad78221..0ef8d52 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "custom/dumpsoftwareversions": { "branch": "master", - "git_sha": "bba7e362e4afead70653f84d8700588ea28d0f9e", + "git_sha": "8ec825f465b9c17f9d83000022995b4f7de6fe93", "installed_by": ["modules"] }, "fastqc": { diff --git a/modules/nf-core/custom/dumpsoftwareversions/environment.yml b/modules/nf-core/custom/dumpsoftwareversions/environment.yml index f0c63f6..9b3272b 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/environment.yml +++ b/modules/nf-core/custom/dumpsoftwareversions/environment.yml @@ -4,4 +4,4 @@ channels: - bioconda - defaults dependencies: - - bioconda::multiqc=1.17 + - bioconda::multiqc=1.19 diff --git a/modules/nf-core/custom/dumpsoftwareversions/main.nf b/modules/nf-core/custom/dumpsoftwareversions/main.nf index 7685b33..f218761 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/main.nf +++ b/modules/nf-core/custom/dumpsoftwareversions/main.nf @@ -4,8 +4,8 @@ process CUSTOM_DUMPSOFTWAREVERSIONS { // Requires `pyyaml` which does not have a dedicated container but is in the MultiQC container conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.17--pyhdfd78af_0' : - 'biocontainers/multiqc:1.17--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.19--pyhdfd78af_0' : + 'biocontainers/multiqc:1.19--pyhdfd78af_0' }" input: path versions diff --git a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test index eec1db1..b1e1630 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test +++ b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test @@ -31,7 +31,12 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot( + process.out.versions, + file(process.out.mqc_yml[0]).readLines()[0..10], + file(process.out.yml[0]).readLines()[0..7] + ).match() + } ) } } diff --git a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap index 4274ed5..5f59a93 100644 --- a/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap +++ b/modules/nf-core/custom/dumpsoftwareversions/tests/main.nf.test.snap @@ -1,27 +1,33 @@ { "Should run without failures": { "content": [ - { - "0": [ - "software_versions.yml:md5,1c851188476409cda5752ce971b20b58" - ], - "1": [ - "software_versions_mqc.yml:md5,2570f4ba271ad08357b0d3d32a9cf84d" - ], - "2": [ - "versions.yml:md5,3843ac526e762117eedf8825b40683df" - ], - "mqc_yml": [ - "software_versions_mqc.yml:md5,2570f4ba271ad08357b0d3d32a9cf84d" - ], - "versions": [ - "versions.yml:md5,3843ac526e762117eedf8825b40683df" - ], - "yml": [ - "software_versions.yml:md5,1c851188476409cda5752ce971b20b58" - ] - } + [ + "versions.yml:md5,76d454d92244589d32455833f7c1ba6d" + ], + [ + "data: \"\\n\\n \\n \\n \\n \\n \\n \\n \\n\\", + " \\n\\n\\n \\n \\n\\", + " \\ \\n\\n\\n\\n \\n \\", + " \\ \\n \\n\\n\\n\\n\\", + " \\n\\n \\n \\n\\", + " \\ \\n\\n\\n\\n\\n\\n \\n\\", + " \\ \\n \\n\\n\\n\\n\\", + " \\n\\n \\n \\n\\" + ], + [ + "CUSTOM_DUMPSOFTWAREVERSIONS:", + " python: 3.11.7", + " yaml: 5.4.1", + "TOOL1:", + " tool1: 0.11.9", + "TOOL2:", + " tool2: '1.9'", + "Workflow:" + ] ], - "timestamp": "2023-11-03T14:43:22.157011" + "timestamp": "2024-01-09T23:01:18.710682" } -} +} \ No newline at end of file From 4f00660dd451440fdf4d56e2e4f36b5f2d868044 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 10:43:28 +0100 Subject: [PATCH 259/410] Update `multiqc` module --- modules.json | 2 +- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 4 ++-- modules/nf-core/multiqc/tests/main.nf.test.snap | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules.json b/modules.json index 0ef8d52..f0ffcdd 100644 --- a/modules.json +++ b/modules.json @@ -17,7 +17,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "642a0d8afe373ac45244a7947fb8a6c0a5a312d4", + "git_sha": "8ec825f465b9c17f9d83000022995b4f7de6fe93", "installed_by": ["modules"] }, "spaceranger/count": { diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index bc0bdb5..7625b75 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -4,4 +4,4 @@ channels: - bioconda - defaults dependencies: - - bioconda::multiqc=1.18 + - bioconda::multiqc=1.19 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 70708f3..1b9f7c4 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.18--pyhdfd78af_0' : - 'biocontainers/multiqc:1.18--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.19--pyhdfd78af_0' : + 'biocontainers/multiqc:1.19--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 d087a9d..d37e730 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,10 +2,10 @@ "versions": { "content": [ [ - "versions.yml:md5,f81e19ab3a8e2b6f2b5d22078117df71" + "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" ] ], - "timestamp": "2023-12-30T00:26:14.048089591" + "timestamp": "2024-01-09T23:02:49.911994" }, "sarscov2 single-end [fastqc] - stub": { "content": [ @@ -13,9 +13,9 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,f81e19ab3a8e2b6f2b5d22078117df71" + "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" ] ], - "timestamp": "2023-12-30T00:26:52.963964055" + "timestamp": "2024-01-09T23:03:14.524346" } } \ No newline at end of file From 67aa69bee40374e7c36e63f84401420949ce27ce Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 10:43:51 +0100 Subject: [PATCH 260/410] Update `untar` module --- modules.json | 2 +- modules/nf-core/untar/environment.yml | 4 +- modules/nf-core/untar/tests/main.nf.test | 26 +- modules/nf-core/untar/tests/main.nf.test.snap | 479 ------------------ 4 files changed, 6 insertions(+), 505 deletions(-) diff --git a/modules.json b/modules.json index f0ffcdd..0dfba23 100644 --- a/modules.json +++ b/modules.json @@ -28,7 +28,7 @@ }, "untar": { "branch": "master", - "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", + "git_sha": "8e6960947b6647952b97667173805b34b40f25d6", "installed_by": ["modules"] } } diff --git a/modules/nf-core/untar/environment.yml b/modules/nf-core/untar/environment.yml index d6917da..0c9cbb1 100644 --- a/modules/nf-core/untar/environment.yml +++ b/modules/nf-core/untar/environment.yml @@ -1,9 +1,11 @@ name: untar + channels: - conda-forge - bioconda - defaults + dependencies: - - conda-forge::sed=4.7 - conda-forge::grep=3.11 + - conda-forge::sed=4.7 - conda-forge::tar=1.34 diff --git a/modules/nf-core/untar/tests/main.nf.test b/modules/nf-core/untar/tests/main.nf.test index d40db13..f57234f 100644 --- a/modules/nf-core/untar/tests/main.nf.test +++ b/modules/nf-core/untar/tests/main.nf.test @@ -16,7 +16,7 @@ nextflow_process { } process { """ - input[0] = [ [], file(params.test_data['sarscov2']['genome']['kraken2_tar_gz'], checkIfExists: true) ] + input[0] = [ [], file(params.testdata_base_path + 'modules/genomics/sarscov2/genome/db/kraken2.tar.gz', checkIfExists: true) ] """ } } @@ -30,28 +30,6 @@ nextflow_process { } - test("test_untar_different_output_path") { - - when { - params { - outdir = "$outputDir" - } - process { - """ - input[0] = [ [], file(params.test_data['homo_sapiens']['illumina']['test_flowcell'], checkIfExists: true) ] - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out.untar).match("test_untar_different_output_path") }, - ) - } - - } - test("test_untar_onlyfiles") { when { @@ -60,7 +38,7 @@ nextflow_process { } process { """ - input[0] = [ [], file(params.test_data['generic']['tar']['tar_gz'], checkIfExists: true) ] + input[0] = [ [], file(params.testdata_base_path + 'modules/generic/tar/hello.tar.gz', checkIfExists: true) ] """ } } diff --git a/modules/nf-core/untar/tests/main.nf.test.snap b/modules/nf-core/untar/tests/main.nf.test.snap index 146c867..ace4257 100644 --- a/modules/nf-core/untar/tests/main.nf.test.snap +++ b/modules/nf-core/untar/tests/main.nf.test.snap @@ -1,483 +1,4 @@ { - "test_untar_different_output_path": { - "content": [ - [ - [ - [ - - ], - [ - [ - [ - [ - [ - [ - "s_1_1101.bcl:md5,ad01889e2ff43e2f194224e20bdb600c", - "s_1_1101.stats:md5,4bbbf103454b37fbc3138fadf1b4446b" - ], - [ - "s_1_1101.bcl:md5,565384bbe67a694dfd690bae6d1d30c2", - "s_1_1101.stats:md5,55e5abd8f129ff38ef169873547abdb8" - ], - [ - "s_1_1101.bcl:md5,650fa58a630a9148835ba79e323d4237", - "s_1_1101.stats:md5,77403669ca1b05340c390dff64425c1e" - ], - [ - "s_1_1101.bcl:md5,54471c9e97299cd141e202e204637702", - "s_1_1101.stats:md5,67b14c9a89b7f8556674a7524d5cfb2d" - ], - [ - "s_1_1101.bcl:md5,74e4f929fc7476c380fd9d741ddb6700", - "s_1_1101.stats:md5,5730a4c35463eaa12a06b6758710b98c" - ], - [ - "s_1_1101.bcl:md5,c785f472f4350c120c02c888c8189590", - "s_1_1101.stats:md5,fee4ec63895ea81007e06ee6a36ba5e0" - ], - [ - "s_1_1101.bcl:md5,b7ea50bb25f08d43c301741d77050a9b", - "s_1_1101.stats:md5,fa7c68f3122c74d14364e6f7b011af70" - ], - [ - "s_1_1101.bcl:md5,9d5087dc4bcae39d66486363d4f68ecf", - "s_1_1101.stats:md5,23cdceee4d82c4b8e7c60018b9276ace" - ], - [ - "s_1_1101.bcl:md5,581e0c5ee94e8f2de14b2b1d8e777530", - "s_1_1101.stats:md5,9a3536d573c97f66bb56b49463612607" - ], - [ - "s_1_1101.bcl:md5,296fc026bb34c67bbe2b44845fe0d1de", - "s_1_1101.stats:md5,a7f57a7770fb9c5ae2a0fb1ef403ec4f" - ], - [ - "s_1_1101.bcl:md5,2a3ca15531556c36d10d132a9e051de8", - "s_1_1101.stats:md5,2d0bcdb0a1b51d3d79e415db2ab2d3b1" - ], - [ - "s_1_1101.bcl:md5,1150d46a2ccd4ac58aee0585d3e4ffd7", - "s_1_1101.stats:md5,2e97550bd5b5864ffd0565bb7a3f6d40" - ], - [ - "s_1_1101.bcl:md5,0b85c4b3da0de95e7b862d849c5333ae", - "s_1_1101.stats:md5,6eab9746fbeb783b0cd70398f44e0c1a" - ], - [ - "s_1_1101.bcl:md5,e0e9c91f4698804d7a6d1058ef68b34f", - "s_1_1101.stats:md5,790022cdc7878a02b2ebd166e1ddf0a7" - ], - [ - "s_1_1101.bcl:md5,38cd0ad4de359e651c8ac0d5777ea625", - "s_1_1101.stats:md5,a1b1d5ea5371d326abb029774483c5e6" - ], - [ - "s_1_1101.bcl:md5,b0ddc05c4012ccba24e712a1cfec748f", - "s_1_1101.stats:md5,af3d232f839d720f76f40ba06caa2987" - ], - [ - "s_1_1101.bcl:md5,af32fcc5dc3b836cf7a5ba3db85a75dd", - "s_1_1101.stats:md5,f93f2c09bd4e486c74a5f6e2040f7296" - ], - [ - "s_1_1101.bcl:md5,54b7428e037ca87816107647d4a3d9db", - "s_1_1101.stats:md5,e5ac77a72cd7bed5e9bf03cccda0e48c" - ], - [ - "s_1_1101.bcl:md5,fc8b4eacd493bf3d0b20bc23998dc7ff", - "s_1_1101.stats:md5,190315e159e2f4bc4c057ded7470dc52" - ], - [ - "s_1_1101.bcl:md5,9484ecffda489927fce424ac6a44fa9d", - "s_1_1101.stats:md5,0825feeb457ecc9efcf6f8526ba32311" - ], - [ - "s_1_1101.bcl:md5,eec59e21036e31c95ce1e847bfb0a9c4", - "s_1_1101.stats:md5,9acc13f63c98e5a8445e7be70d49222b" - ], - [ - "s_1_1101.bcl:md5,a9fb24476f87cba4fba68e2b3c3f2c07", - "s_1_1101.stats:md5,dc0aa7db9790733291c3e6480ca2a0fc" - ], - [ - "s_1_1101.bcl:md5,ed950b3e82c500927c2e236c9df005c6", - "s_1_1101.stats:md5,dccb71ec47d1f9d33a192da6d5660a45" - ], - [ - "s_1_1101.bcl:md5,b3e992025e995ca56b5ea2820144ef47", - "s_1_1101.stats:md5,a6a829bf2cffb26ac5d9dc3012057699" - ], - [ - "s_1_1101.bcl:md5,89edc726a5a4e0b4ff8ca3899ed0232b", - "s_1_1101.stats:md5,5b9b4fd8110577a59b82d0c419519d29" - ], - [ - "s_1_1101.bcl:md5,4dc696149169f232c451225f563cb5cd", - "s_1_1101.stats:md5,d3514a71ea3adc60e2943c6b8f6e2598" - ], - [ - "s_1_1101.bcl:md5,35b992d0318afb7c825ceaa31b0755e6", - "s_1_1101.stats:md5,2826093acc175c16c3795de7c4ca8f07" - ], - [ - "s_1_1101.bcl:md5,7bc927f56a362e49c00b5d76ee048901", - "s_1_1101.stats:md5,e47d862b795fd6b88a31d7d482ab22f6" - ], - [ - "s_1_1101.bcl:md5,84742233ff2a651626fe9036f27f7cb2", - "s_1_1101.stats:md5,b78fad11d3c50bc76b722cdc03e3028b" - ], - [ - "s_1_1101.bcl:md5,3935341c86263a7938e8c49620ef39f8", - "s_1_1101.stats:md5,cc6585b2daac5354073d150874da9704" - ], - [ - "s_1_1101.bcl:md5,3627f4fd548bf6e64aaf08fba3a342be", - "s_1_1101.stats:md5,120ae4831ae004ff7d16728aef36e82f" - ], - [ - "s_1_1101.bcl:md5,07631014bc35124149fabd80ef19f933", - "s_1_1101.stats:md5,eadd63d91f47cc6db6b6f0a967a23927" - ], - [ - "s_1_1101.bcl:md5,a1149c80415dc2f34d768eeb397c43fb", - "s_1_1101.stats:md5,ca89a9def67611a9151c6ce685b7cce1" - ], - [ - "s_1_1101.bcl:md5,eb5f71d4741d2f40618756bc72eaf8b4", - "s_1_1101.stats:md5,90f48501e735e5915b843478e23d1ae2" - ], - [ - "s_1_1101.bcl:md5,9bf270fe3f6add1a591ebc24fff10078", - "s_1_1101.stats:md5,a4e429671d4098034293c638aa655e16" - ], - [ - "s_1_1101.bcl:md5,219bedcbd24bae54fe4cf05dae05282c", - "s_1_1101.stats:md5,dd97525b65b68207137d51fcf19132c7" - ], - [ - "s_1_1101.bcl:md5,5163bc00a68fd57ae50cae0b76350892", - "s_1_1101.stats:md5,b606a5368eff1f012f3ea5d11ccdf2e0" - ], - [ - "s_1_1101.bcl:md5,fc429195a5af59a59e0cc4c48e6c05ea", - "s_1_1101.stats:md5,d809aa19698053f90d639da4dcad8008" - ], - [ - "s_1_1101.bcl:md5,383340219a1dd77076a092a64a71a7e4", - "s_1_1101.stats:md5,b204a5cf256378679ffc906c15cc1bae" - ], - [ - "s_1_1101.bcl:md5,0c369540d3e24696cf1f9c55bab69315", - "s_1_1101.stats:md5,a2bc69a4031a22ce9621dcc623a0bf4b" - ], - [ - "s_1_1101.bcl:md5,3127abc8016ba8eb954f8f8015dff387", - "s_1_1101.stats:md5,5deafff31150b7bf757f814e49a53bc2" - ], - [ - "s_1_1101.bcl:md5,045f40c82de676bafec3d59f91376a7a", - "s_1_1101.stats:md5,890700edc20687c090ef52248c7884b1" - ], - [ - "s_1_1101.bcl:md5,78af269aa2b39a1d765703f0a4739a86", - "s_1_1101.stats:md5,303cf457aa1543a8208544f694cbc531" - ], - [ - "s_1_1101.bcl:md5,0ab8c781959b783b62888e9274364a46", - "s_1_1101.stats:md5,2605b0e8322f83aa4d0dae5da4ec7a7a" - ], - [ - "s_1_1101.bcl:md5,d0cf823ffe352e8b3f75d589544ab617", - "s_1_1101.stats:md5,efa3c0e01e3db71e12fd961cb2d03739" - ], - [ - "s_1_1101.bcl:md5,db4ca4ab7a01e03c246f9160c3758d82", - "s_1_1101.stats:md5,f61550d9e4a90df6b860e68f41f82f60" - ], - [ - "s_1_1101.bcl:md5,1af39a2c7e5ff20ece91cb8160b51d17", - "s_1_1101.stats:md5,d0e20879afcaf6dfcd88c73f1c5c78cf" - ], - [ - "s_1_1101.bcl:md5,4cf7123bb0fffcd79266df03aef01665", - "s_1_1101.stats:md5,29bff4075109a121b087116b58d7e927" - ], - [ - "s_1_1101.bcl:md5,aa9980428cb60cd6320f4b48f4dd0d74", - "s_1_1101.stats:md5,6b0e20bde93133117a8d1a6df3d6f37b" - ], - [ - "s_1_1101.bcl:md5,0f6e440374e15b9b491d52fb83a8adfe", - "s_1_1101.stats:md5,55cb5eb0ecdabd23dca39ab8c4607598" - ], - [ - "s_1_1101.bcl:md5,2c645d7bdaddaa403f6e304d36df9e4b", - "s_1_1101.stats:md5,53acf33d21f832779b400c2447386ce4" - ], - [ - "s_1_1101.bcl:md5,3bbf0863b423b770c879203644420206", - "s_1_1101.stats:md5,579bdc7293cac8c3d7407249cacf4c25" - ], - [ - "s_1_1101.bcl:md5,6658a08409e81d29cfeb2d096b491985", - "s_1_1101.stats:md5,bb559ffbea46d612f9933cefa84c4c03" - ], - [ - "s_1_1101.bcl:md5,1700d9a13d3d4f7643af2943ef838acb", - "s_1_1101.stats:md5,f01cb6050ebfb15da1e0399ebd791eb4" - ], - [ - "s_1_1101.bcl:md5,1ac7aa9ffae25eb103f755f33e4a39c6", - "s_1_1101.stats:md5,0b9d45d7929ccf336d5e5b95373ed3c2" - ], - [ - "s_1_1101.bcl:md5,812a97af2e983a53226e18c75190b06c", - "s_1_1101.stats:md5,d2410c7b0e506dab2972e77e2398de1e" - ], - [ - "s_1_1101.bcl:md5,c981e8e4dcc434956c2b86159da268bc", - "s_1_1101.stats:md5,e9c826e85361ce673f1f248786c9a611" - ], - [ - "s_1_1101.bcl:md5,88e09e99a0a4ef3357b203a41b22f77c", - "s_1_1101.stats:md5,ef06f2e5ad667bbd383f9ed6a05b7b42" - ], - [ - "s_1_1101.bcl:md5,461c8b146fc8a7938be38689978ecd09", - "s_1_1101.stats:md5,65115693935da66f9791b27136e22fb0" - ], - [ - "s_1_1101.bcl:md5,c7b827df5ce20e0f21916fe60860ca3f", - "s_1_1101.stats:md5,87be73613aeb507847f94d3cac5bb30a" - ], - [ - "s_1_1101.bcl:md5,7c4cc3dc9c8a1b0f15917b282dfb40ce", - "s_1_1101.stats:md5,bdd9181fa89debbfafe7b6ea3e064065" - ], - [ - "s_1_1101.bcl:md5,19f4debaf91e118aca8934517179ac33", - "s_1_1101.stats:md5,1143082719e136241d21b14a6b19b8a2" - ], - [ - "s_1_1101.bcl:md5,38aa256ad2d697d84b0b2c0e876a3eba", - "s_1_1101.stats:md5,64dd82f03df23f7f437eede2671ed4fe" - ], - [ - "s_1_1101.bcl:md5,b7929970378949571fed922c1b8cab32", - "s_1_1101.stats:md5,3d6d7985a41629fe196e4342d7fe36aa" - ], - [ - "s_1_1101.bcl:md5,fb2ed0bf6e89d79624ee78754e773491", - "s_1_1101.stats:md5,f34940810ff255aee79953496a12716d" - ], - [ - "s_1_1101.bcl:md5,4f8a8311f5f9c3a7629c1a973a7b280e", - "s_1_1101.stats:md5,4fd7cd28c09f4e152e7c2ad1ab541cd2" - ], - [ - "s_1_1101.bcl:md5,9eb46c903d0344e25af51f88cc311d60", - "s_1_1101.stats:md5,df3abd5f620d9e7f99496098d9fd3f7f" - ], - [ - "s_1_1101.bcl:md5,3ecbc17f3660e2014b58d7fe70ae62d5", - "s_1_1101.stats:md5,8e89a13c85a6d6ab3ccd251b66d1f165" - ], - [ - "s_1_1101.bcl:md5,5d59cc2499a77791233a64f73fe82894", - "s_1_1101.stats:md5,32ec99cd400f4b80cb26e2fa8e07ece0" - ], - [ - "s_1_1101.bcl:md5,1c052da47b9ae8554388f0fa3aade482", - "s_1_1101.stats:md5,d23f438772673688aa7bc92421dc6dce" - ], - [ - "s_1_1101.bcl:md5,1a52bd4f23130c0c96bc967ccd448a2b", - "s_1_1101.stats:md5,9b597e3388d59ef1f61aba30ac90ea79" - ], - [ - "s_1_1101.bcl:md5,8a1e84b79cf3f80794c20e3a0cc84688", - "s_1_1101.stats:md5,9561f7b6ef4b1849afc72b2bb49792bd" - ], - [ - "s_1_1101.bcl:md5,75c00111051f3fa95d04286823cb9109", - "s_1_1101.stats:md5,1fe786cdf8181767deafbd60b3c76610" - ], - [ - "s_1_1101.bcl:md5,529255d8deee0873ed5565e6d1a2ebda", - "s_1_1101.stats:md5,3fa7f467e97a75880f32d17b7429d316" - ], - [ - "s_1_1101.bcl:md5,ea4d960e3d9355d2149da71b88a21df4", - "s_1_1101.stats:md5,2540fe65586e8e800c1ddd8cddd1e8cd" - ], - [ - "s_1_1101.bcl:md5,0dfe1fd92a2dce2f23119aa483429744", - "s_1_1101.stats:md5,78257b2169fb9f0cf40966e06e847e86" - ], - [ - "s_1_1101.bcl:md5,f692ddc9aa3ab849271d07c666d0b3b9", - "s_1_1101.stats:md5,aa2ec6a3e3a9c116e34fe74a21e6459e" - ], - [ - "s_1_1101.bcl:md5,29cc4c239eae7c871c9a1adf92ebdb98", - "s_1_1101.stats:md5,263184813090acd740a5bf25304aed3a" - ], - [ - "s_1_1101.bcl:md5,e005af6a84925e326afbfe264241f047", - "s_1_1101.stats:md5,b6fb20868eebaffcc19daa694a449795" - ], - [ - "s_1_1101.bcl:md5,02f1a699b1ba9967accccf99a7af3d24", - "s_1_1101.stats:md5,4f007efacecaf26dc0e0231aede28754" - ], - [ - "s_1_1101.bcl:md5,df308c72a2dcc655cd95e98f5457187a", - "s_1_1101.stats:md5,130c4b07f4c14030bab012824cbe34da" - ], - [ - "s_1_1101.bcl:md5,f3ce10d8d2406b72355023bfa8c96822", - "s_1_1101.stats:md5,2638f4db393ed5b699ec2ce59ff0ec19" - ], - [ - "s_1_1101.bcl:md5,cc2f6d675ad1593ff96f734b172d249e", - "s_1_1101.stats:md5,f5b13f1e1ababc9e1a7a73b0b993cbf1" - ], - [ - "s_1_1101.bcl:md5,7938a0b21448305a951b023b1845b3a7", - "s_1_1101.stats:md5,fcd57511adabfc3ba1ac045165330006" - ], - [ - "s_1_1101.bcl:md5,44879bc6a38df1fee8def61868115041", - "s_1_1101.stats:md5,517e20e4b58a8023a37f9af62e0e2036" - ], - [ - "s_1_1101.bcl:md5,8749611e62406a7d2f34c610a55e56af", - "s_1_1101.stats:md5,8ccf24b3676ef84f2e513be8f2a9f3d1" - ], - [ - "s_1_1101.bcl:md5,a9846a037611cda3721958088f714c0e", - "s_1_1101.stats:md5,6438fa5a1892f328cab1605a95d80a3b" - ], - [ - "s_1_1101.bcl:md5,d6c4a2a726496476eb826532f974ed5f", - "s_1_1101.stats:md5,8c2c65b5e8b00dbf61ada65252aeb266" - ], - [ - "s_1_1101.bcl:md5,be3dde6cae7dd85855a6bf295ebfacfe", - "s_1_1101.stats:md5,93bc13f3b0749b2b8d8bcb0b1199f4f0" - ], - [ - "s_1_1101.bcl:md5,7c64514735a6cf1565b60647edd17d20", - "s_1_1101.stats:md5,4a0aa6c49b24f876415e5878cef7f805" - ], - [ - "s_1_1101.bcl:md5,3983b4043bc9df4b505202a5134ccf03", - "s_1_1101.stats:md5,1c9d9a8558adc1279ca27c96bc1b9758" - ], - [ - "s_1_1101.bcl:md5,a0b8d77f116ec95975f9253dcb768136", - "s_1_1101.stats:md5,c3992b786756e7ec42f65ef4b13b50d4" - ], - [ - "s_1_1101.bcl:md5,43c95ba35d06bb7c57fbd16f3d1cfd6c", - "s_1_1101.stats:md5,3cb69d04698c39f97f962e5bf1eea7f0" - ], - [ - "s_1_1101.bcl:md5,3dbeea0cad7052f19f53ff6f19dd4d90", - "s_1_1101.stats:md5,58bbc8254f0f5f4a244531e8e9c12a04" - ], - [ - "s_1_1101.bcl:md5,da56d088996376c898d855b6cd0a7dfc", - "s_1_1101.stats:md5,9f2d78af6908ce1576b89cdc059844ff" - ], - [ - "s_1_1101.bcl:md5,7b641a5565f095e9a6ffcad9e4305033", - "s_1_1101.stats:md5,3ada06c59b4fb41b83ab6abd0979e9fc" - ], - [ - "s_1_1101.bcl:md5,a3843d397a01d51657825bb652c191e5", - "s_1_1101.stats:md5,19341e52a4bfc7d9d48e9d2acc68c519" - ], - [ - "s_1_1101.bcl:md5,048e3ebfc8efeb8012def6b741c9060d", - "s_1_1101.stats:md5,88bd38deca1e87d700effab1fd099565" - ], - [ - "s_1_1101.bcl:md5,b340db0e07e829dd5da22371916a1a9e", - "s_1_1101.stats:md5,e44cfaddcc4ffb968e5b1a2f41ac48a5" - ], - [ - "s_1_1101.bcl:md5,e6011ec6eabbc2b8792deb283c621ce0", - "s_1_1101.stats:md5,090875dcd1a431af24bc631333f089c4" - ], - [ - "s_1_1101.bcl:md5,a08f216e3352345031ed100ec4245082", - "s_1_1101.stats:md5,97b949ef4b96219e1369f673cf5f8a6c" - ], - [ - "s_1_1101.bcl:md5,b43337c76fb037dfcf5f8f7bcb3618e5", - "s_1_1101.stats:md5,ddef585805e79951f69d23ab7354f69b" - ], - [ - "s_1_1101.bcl:md5,8c61fd004104397b360855e058bbf1bf", - "s_1_1101.stats:md5,0f8d253816d594dcfea3ccf48c826401" - ], - [ - "s_1_1101.bcl:md5,594d06310d328b188aa0b3edfff22cb2", - "s_1_1101.stats:md5,3160bf271b39aeb7590e4fd2984710ba" - ], - [ - "s_1_1101.bcl:md5,4c9eada67c9d55437211d83e111961d5", - "s_1_1101.stats:md5,2901b46ab16ec4863d30e4c84ec29c97" - ], - [ - "s_1_1101.bcl:md5,e03971ae5282f0accc0c1b7374d9ef1b", - "s_1_1101.stats:md5,60d2a19ce59bf70a21a28555484cead8" - ], - [ - "s_1_1101.bcl:md5,e1c6f7a06e63d149895d3e48e63df155", - "s_1_1101.stats:md5,44beb10af847ea3dddaf06dda7031126" - ], - [ - "s_1_1101.bcl:md5,960a99bf29a8f9d936e9b8582d46c9c6", - "s_1_1101.stats:md5,544cd1a7aaaa841914b40ece43399334" - ], - [ - "s_1_1101.bcl:md5,5706679f349fd4a6b6313bc2c41c7a42", - "s_1_1101.stats:md5,627eea844b26dae033848c2f9f69177b" - ], - [ - "s_1_1101.bcl:md5,21da5abc4b0402bbac14b5ab998b0b4f", - "s_1_1101.stats:md5,515bd140b095ad90473ca7a9a69877ab" - ], - "s_1_1101.control:md5,08a72e2198ae95150718e8adf011d105", - "s_1_1101.filter:md5,3a72bc73b323c8cb0ac5bfeb62d98989" - ] - ], - [ - "s_1_1101.locs:md5,0827ea802e5257cc5b20e757a33d4c98" - ], - "RTAConfiguration.xml:md5,c7d6e257bc374f142dc64b9d2281d4c9", - "config.xml:md5,9a4cc7ec01fefa2f1ce9bcb45bbad6e9" - ] - ], - [ - "ControlMetricsOut.bin:md5,6d77b38d0793a6e1ce1e85706e488953", - "CorrectedIntMetricsOut.bin:md5,2bbf84d3be72734addaa2fe794711434", - "ErrorMetricsOut.bin:md5,38c88def138e9bb832539911affdb286", - "ExtractionMetricsOut.bin:md5,7497c3178837eea8f09350b5cd252e99", - "IndexMetricsOut.bin:md5,d41d8cd98f00b204e9800998ecf8427e", - "QMetricsOut.bin:md5,7e9f198d53ebdfbb699a5f94cf1ed51c", - "TileMetricsOut.bin:md5,83891751ec1c91a425a524b476b6ca3c" - ], - "RunInfo.xml:md5,03038959f4dd181c86bc97ae71fe270a" - ] - ] - ] - ], - "timestamp": "2023-10-18T11:56:39.562418" - }, "test_untar_onlyfiles": { "content": [ [ From 0718260ac888082bfef724e794e38eaa693778f5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 10:49:21 +0100 Subject: [PATCH 261/410] Update `spaceranger/count` module --- modules.json | 5 +- .../nf-core/spaceranger/count/environment.yml | 5 - modules/nf-core/spaceranger/count/main.nf | 8 +- modules/nf-core/spaceranger/count/meta.yml | 5 +- .../spaceranger/count/spaceranger-count.diff | 47 ---- .../spaceranger/count/tests/main.nf.test | 226 ++++++++++++++++++ .../spaceranger/count/tests/main.nf.test.snap | 54 +++++ .../nf-core/spaceranger/count/tests/tags.yml | 2 + 8 files changed, 291 insertions(+), 61 deletions(-) delete mode 100644 modules/nf-core/spaceranger/count/environment.yml delete mode 100644 modules/nf-core/spaceranger/count/spaceranger-count.diff create mode 100644 modules/nf-core/spaceranger/count/tests/main.nf.test create mode 100644 modules/nf-core/spaceranger/count/tests/main.nf.test.snap create mode 100644 modules/nf-core/spaceranger/count/tests/tags.yml diff --git a/modules.json b/modules.json index 0dfba23..968eaf0 100644 --- a/modules.json +++ b/modules.json @@ -22,9 +22,8 @@ }, "spaceranger/count": { "branch": "master", - "git_sha": "3f5420aa22e00bd030a2556dfdffc9e164ec0ec5", - "installed_by": ["modules"], - "patch": "modules/nf-core/spaceranger/count/spaceranger-count.diff" + "git_sha": "e89a3110141cdf77abd4fbc85cd0d7cb8f01b49b", + "installed_by": ["modules"] }, "untar": { "branch": "master", diff --git a/modules/nf-core/spaceranger/count/environment.yml b/modules/nf-core/spaceranger/count/environment.yml deleted file mode 100644 index a5049bf..0000000 --- a/modules/nf-core/spaceranger/count/environment.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: spaceranger_count -channels: - - conda-forge - - bioconda - - defaults diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index cac83e0..c0f10e5 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -10,7 +10,7 @@ process SPACERANGER_COUNT { path(probeset) output: - tuple val(meta), path("outs/**"), emit: outs + tuple val(meta), path("**/outs/**"), emit: outs path "versions.yml", emit: versions when: @@ -46,7 +46,6 @@ process SPACERANGER_COUNT { $alignment \\ $slidefile \\ $args - mv ${prefix}/outs outs cat <<-END_VERSIONS > versions.yml "${task.process}": @@ -59,9 +58,10 @@ process SPACERANGER_COUNT { if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." } + def prefix = task.ext.prefix ?: "${meta.id}" """ - mkdir -p outs/ - touch outs/fake_file.txt + mkdir -p "${prefix}/outs/" + touch ${prefix}/outs/fake_file.txt cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml index 2811fd2..123c96c 100644 --- a/modules/nf-core/spaceranger/count/meta.yml +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -20,7 +20,8 @@ tools: homepage: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" documentation: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" tool_dev_url: "https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger" - licence: "10x Genomics EULA" + licence: + - "10x Genomics EULA" input: - meta: type: map @@ -83,7 +84,7 @@ output: - outs: type: file description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list - pattern: "outs/*" + pattern: "${meta.id}/outs/*" - versions: type: file description: File containing software versions diff --git a/modules/nf-core/spaceranger/count/spaceranger-count.diff b/modules/nf-core/spaceranger/count/spaceranger-count.diff deleted file mode 100644 index e138217..0000000 --- a/modules/nf-core/spaceranger/count/spaceranger-count.diff +++ /dev/null @@ -1,47 +0,0 @@ -Changes in module 'nf-core/spaceranger/count' ---- modules/nf-core/spaceranger/count/meta.yml -+++ modules/nf-core/spaceranger/count/meta.yml -@@ -83,7 +83,7 @@ - - outs: - type: file - description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list -- pattern: "${meta.id}/outs/*" -+ pattern: "outs/*" - - versions: - type: file - description: File containing software versions - ---- modules/nf-core/spaceranger/count/main.nf -+++ modules/nf-core/spaceranger/count/main.nf -@@ -10,7 +10,7 @@ - path(probeset) - - output: -- tuple val(meta), path("**/outs/**"), emit: outs -+ tuple val(meta), path("outs/**"), emit: outs - path "versions.yml", emit: versions - - when: -@@ -46,6 +46,7 @@ - $alignment \\ - $slidefile \\ - $args -+ mv ${prefix}/outs outs - - cat <<-END_VERSIONS > versions.yml - "${task.process}": -@@ -58,10 +59,9 @@ - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." - } -- def prefix = task.ext.prefix ?: "${meta.id}" - """ -- mkdir -p "${prefix}/outs/" -- touch ${prefix}/outs/fake_file.txt -+ mkdir -p outs/ -+ touch outs/fake_file.txt - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - -************************************************************ diff --git a/modules/nf-core/spaceranger/count/tests/main.nf.test b/modules/nf-core/spaceranger/count/tests/main.nf.test new file mode 100644 index 0000000..b751b07 --- /dev/null +++ b/modules/nf-core/spaceranger/count/tests/main.nf.test @@ -0,0 +1,226 @@ +nextflow_process { + + name "Test Process SPACERANGER_COUNT" + script "../main.nf" + process "SPACERANGER_COUNT" + + tag "modules" + tag "modules_nfcore" + tag "spaceranger" + tag "spaceranger/count" + tag "spaceranger/mkgtf" + tag "spaceranger/mkref" + + test("spaceranger v1 - homo_sapiens - fasta - gtf - fastq - tif - csv") { + + setup { + run("SPACERANGER_MKGTF") { + script "../../mkgtf/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + """ + } + } + } + + setup { + run("SPACERANGER_MKREF") { + script "../../mkref/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_fasta'], checkIfExists: true) + input[1] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + input[2] = 'homo_sapiens_chr22_reference' + """ + } + } + } + + when { + process { + """ + input[0] = [ + [ + id: 'Visium_FFPE_Human_Ovarian_Cancer', + slide: 'V10L13-020', + area: 'D1' + ], // Meta map + [ + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_fastq_1_gz']), + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_fastq_2_gz']) + ], // Reads + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_image']), // Image + [], // Cytaimage + [], // Darkimage + [], // Colorizedimage + [], // Manual alignment (default: automatic alignment) + [], // Slide specification (default: automatic download) + ] + input[1] = SPACERANGER_MKREF.out.reference // Reference + input[2] = [] // Probeset (default: use the one included with Space Ranger) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'web_summary.html', + 'scalefactors_json.json', + 'barcodes.tsv.gz', + 'features.tsv.gz', + 'matrix.mtx.gz' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'web_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'scalefactors_json.json' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'barcodes.tsv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'features.tsv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'matrix.mtx.gz' }).exists() } + ) + } + } + + test("spaceranger v1 (stub) - homo_sapiens - fasta - gtf - fastq - tif - csv") { + + setup { + run("SPACERANGER_MKGTF") { + script "../../mkgtf/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + """ + } + } + } + + setup { + run("SPACERANGER_MKREF") { + script "../../mkref/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_fasta'], checkIfExists: true) + input[1] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + input[2] = 'homo_sapiens_chr22_reference' + """ + } + } + } + + options "-stub" + + when { + process { + """ + input[0] = [ + [ + id: 'Visium_FFPE_Human_Ovarian_Cancer', + slide: 'V10L13-020', + area: 'D1' + ], // Meta map + [ + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_fastq_1_gz']), + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_fastq_2_gz']) + ], // Reads + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_v1_image']), // Image + [], // Cytaimage + [], // Darkimage + [], // Colorizedimage + [], // Manual alignment (default: automatic alignment) + [], // Slide specification (default: automatic download) + ] + input[1] = SPACERANGER_MKREF.out.reference // Reference + input[2] = [] // Probeset (default: use the one included with Space Ranger) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } + + test("spaceranger v2 - homo_sapiens - fasta - gtf - fastq - tif - csv") { + setup { + run("SPACERANGER_MKGTF") { + script "../../mkgtf/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + """ + } + } + } + + setup { + run("SPACERANGER_MKREF") { + script "../../mkref/main.nf" + process { + """ + input[0] = file(params.test_data['homo_sapiens']['genome']['genome_fasta'], checkIfExists: true) + input[1] = file(params.test_data['homo_sapiens']['genome']['genome_gtf'], checkIfExists: true) + input[2] = 'homo_sapiens_chr22_reference' + """ + } + } + } + + when { + process { + """ + input[0] = [ + [ + id: 'CytAssist_11mm_FFPE_Human_Glioblastoma_2', + slide: 'V52Y10-317', + area: 'B1' + ], // Meta map + [ + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_cytassist_fastq_1_gz']), + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_cytassist_fastq_2_gz']) + ], // Reads + [], // Image + file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_cytassist_image']), // Cytaimage + [], // Darkimage + [], // Colorizedimage + [], // Manual alignment (default: automatic alignment) + file('https://s3.us-west-2.amazonaws.com/10x.spatial-slides/gpr/V52Y10/V52Y10-317.gpr') // Slide specification (default: automatic download) + ] + input[1] = SPACERANGER_MKREF.out.reference // Reference + input[2] = file(params.test_data['homo_sapiens']['10xgenomics']['spaceranger']['test_10x_ffpe_cytassist_probeset']) // Probeset (default: use the one included with Space Ranger) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'web_summary.html', + 'scalefactors_json.json', + 'molecule_info.h5', + 'barcodes.tsv.gz', + 'features.tsv.gz', + 'matrix.mtx.gz' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'web_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'scalefactors_json.json' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'molecule_info.h5' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'barcodes.tsv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'features.tsv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'matrix.mtx.gz' }).exists() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/spaceranger/count/tests/main.nf.test.snap b/modules/nf-core/spaceranger/count/tests/main.nf.test.snap new file mode 100644 index 0000000..ece665f --- /dev/null +++ b/modules/nf-core/spaceranger/count/tests/main.nf.test.snap @@ -0,0 +1,54 @@ +{ + "spaceranger v1 (stub) - homo_sapiens - fasta - gtf - fastq - tif - csv": { + "content": [ + [ + "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + ] + ], + "timestamp": "2024-01-09T15:09:24.723008" + }, + "spaceranger v2 - homo_sapiens - fasta - gtf - fastq - tif - csv": { + "content": [ + [ + "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + ], + [ + "filtered_feature_bc_matrix.h5:md5,509e18ed6b218850e5095124ecc771c1", + "metrics_summary.csv:md5,412caff0fcd9f39cb54671147058de2f", + "possorted_genome_bam.bam:md5,23cd192fcc217d835b8c0afee0619f40", + "possorted_genome_bam.bam.bai:md5,baf623d3e554ba5008304f32414c9fb2", + "probe_set.csv:md5,5bfb8f12319be1b2b6c14142537c3804", + "raw_feature_bc_matrix.h5:md5,2263d2c756785f86dc28f6b76fd61b73", + "raw_probe_bc_matrix.h5:md5,3d5e711d0891ca2caaf301a2c1fbda91", + "aligned_fiducials.jpg:md5,51dcc3a32d3d5ca4704f664c8ede81ef", + "cytassist_image.tiff:md5,0fb04a55e5658f4d158d986a334b034d", + "detected_tissue_image.jpg:md5,64d9adb4844ab91506131476d93b28dc", + "tissue_hires_image.png:md5,1c0f1e94522a886c19f56a629227e097", + "tissue_lowres_image.png:md5,8c1fcb378f7f886301f49ffc4f84360a", + "tissue_positions.csv:md5,1b2df34f9e762e9e64aa226226b96c4b" + ] + ], + "timestamp": "2024-01-11T17:49:27.776247" + }, + "spaceranger v1 - homo_sapiens - fasta - gtf - fastq - tif - csv": { + "content": [ + [ + "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + ], + [ + "filtered_feature_bc_matrix.h5:md5,f444a4816bf40d377271a6157b320556", + "metrics_summary.csv:md5,5e36f2f9b6987791e0b5eb2736d25115", + "molecule_info.h5:md5,b3d14dfbfc167bb8fc9b158f083efdb6", + "possorted_genome_bam.bam:md5,6ed7f3bb2f17322113f940989a3771ff", + "possorted_genome_bam.bam.bai:md5,08ce9ffd30d9b82091932b744873610b", + "raw_feature_bc_matrix.h5:md5,7e937b4863a98b0d3784f4e21c07c326", + "aligned_fiducials.jpg:md5,f6217ddd707bb189e665f56b130c3da8", + "detected_tissue_image.jpg:md5,4a26b91db5aca179d627b86f352006e2", + "tissue_hires_image.png:md5,d91f8f176ae35ab824ede87117ac0889", + "tissue_lowres_image.png:md5,475a04208d193191c84d7a3b5d4eb287", + "tissue_positions.csv:md5,37d288d0e29e8572ea4c5bef292de4b6" + ] + ], + "timestamp": "2024-01-11T20:34:18.669838" + } +} \ No newline at end of file diff --git a/modules/nf-core/spaceranger/count/tests/tags.yml b/modules/nf-core/spaceranger/count/tests/tags.yml new file mode 100644 index 0000000..ad51f67 --- /dev/null +++ b/modules/nf-core/spaceranger/count/tests/tags.yml @@ -0,0 +1,2 @@ +spaceranger/count: + - "modules/nf-core/spaceranger/count/**" From e38037af5aa699d55957acd0c6d194ec5360414d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 13:01:04 +0100 Subject: [PATCH 262/410] Update `spaceranger/count` module (new outputs) --- modules.json | 2 +- modules/nf-core/spaceranger/count/main.nf | 8 ++++---- modules/nf-core/spaceranger/count/meta.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules.json b/modules.json index 968eaf0..64ed110 100644 --- a/modules.json +++ b/modules.json @@ -22,7 +22,7 @@ }, "spaceranger/count": { "branch": "master", - "git_sha": "e89a3110141cdf77abd4fbc85cd0d7cb8f01b49b", + "git_sha": "3bd057bfdfb64578636ff3ae7f7cb8eeab3c0cb6", "installed_by": ["modules"] }, "untar": { diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index c0f10e5..cac83e0 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -10,7 +10,7 @@ process SPACERANGER_COUNT { path(probeset) output: - tuple val(meta), path("**/outs/**"), emit: outs + tuple val(meta), path("outs/**"), emit: outs path "versions.yml", emit: versions when: @@ -46,6 +46,7 @@ process SPACERANGER_COUNT { $alignment \\ $slidefile \\ $args + mv ${prefix}/outs outs cat <<-END_VERSIONS > versions.yml "${task.process}": @@ -58,10 +59,9 @@ process SPACERANGER_COUNT { if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { error "SPACERANGER_COUNT module does not support Conda. Please use Docker / Singularity / Podman instead." } - def prefix = task.ext.prefix ?: "${meta.id}" """ - mkdir -p "${prefix}/outs/" - touch ${prefix}/outs/fake_file.txt + mkdir -p outs/ + touch outs/fake_file.txt cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/nf-core/spaceranger/count/meta.yml b/modules/nf-core/spaceranger/count/meta.yml index 123c96c..167ac8c 100644 --- a/modules/nf-core/spaceranger/count/meta.yml +++ b/modules/nf-core/spaceranger/count/meta.yml @@ -1,7 +1,7 @@ --- # yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/yaml-schema.json name: "spaceranger_count" -description: Module to use the 10x Spaceranger pipeline to proces 10x spatial transcriptomics data +description: Module to use the 10x Space Ranger pipeline to process 10x spatial transcriptomics data keywords: - align - count @@ -70,7 +70,7 @@ input: pattern: "*.json" - reference: type: directory - description: Folder containing all the reference indices needed by Spaceranger + description: Folder containing all the reference indices needed by Space Ranger - probeset: type: file description: OPTIONAL - Probe set specification. @@ -83,8 +83,8 @@ output: e.g. [ id:'test', single_end:false ] - outs: type: file - description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list - pattern: "${meta.id}/outs/*" + description: Files containing the outputs of Space Ranger, see official 10X Genomics documentation for a complete list + pattern: "outs/*" - versions: type: file description: File containing software versions From 1826d0137d980dfc0d33ee448ff535a1f48755e2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 15 Jan 2024 16:22:28 +0100 Subject: [PATCH 263/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 4 ++-- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 58aa1a3..45bd56e 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-12-18T16:46:26.313038" + "timestamp": "2024-01-15T16:00:03.485826" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 9bdda01..8be642f 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-29T12:46:05+0000" + "timestamp": "2024-01-15T13:44:40.789425" } } \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 94b2a26..25954a9 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.12.0, yaml=6.0.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], - "timestamp": "2023-06-29T12:48:09+0000" + "timestamp": "2024-01-15T15:42:52.651007" } } \ No newline at end of file From c190b7a723ac757cad69fdf40d6199e5493d1504 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 15:58:44 +0100 Subject: [PATCH 264/410] Add nf-core Quarto template as asset --- assets/_extensions/nf-core/_extension.yml | 21 +++ assets/_extensions/nf-core/nf-core.scss | 194 ++++++++++++++++++++ assets/_extensions/nf-core/nf-core.theme | 211 ++++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 assets/_extensions/nf-core/_extension.yml create mode 100644 assets/_extensions/nf-core/nf-core.scss create mode 100644 assets/_extensions/nf-core/nf-core.theme diff --git a/assets/_extensions/nf-core/_extension.yml b/assets/_extensions/nf-core/_extension.yml new file mode 100644 index 0000000..2089122 --- /dev/null +++ b/assets/_extensions/nf-core/_extension.yml @@ -0,0 +1,21 @@ +title: nf-core Quarto Extension +author: Erik Fasterius +version: 1.0.0 +quarto-required: ">=1.2.0" +contributes: + formats: + html: + code-fold: true + df-print: paged + embed-resources: true + highlight-style: nf-core.theme + smooth-scroll: true + theme: [default, nf-core.scss] + toc-location: left + toc: true + revealjs: + code-line-numbers: false + embed-resources: true + slide-level: 2 + slide-number: false + theme: [default, nf-core.scss] diff --git a/assets/_extensions/nf-core/nf-core.scss b/assets/_extensions/nf-core/nf-core.scss new file mode 100644 index 0000000..788a029 --- /dev/null +++ b/assets/_extensions/nf-core/nf-core.scss @@ -0,0 +1,194 @@ +/*-- scss:defaults --*/ + +$theme: "nf-core" !default; + +// Colours +$green: #24B064 !default; +$blue: #3073AF !default; +$red: #E0191A !default; +$yellow: #DABC25 !default; + +// Greyscale +$white: #FFFFFF !default; +$grey-100: #F5F5F5 !default; +$grey-90: #E5E5E5 !default; +$grey-80: #CCCCCC !default; +$grey-70: #B2B2B2 !default; +$grey-60: #999999 !default; +$grey-50: #7F7F7F !default; +$grey-40: #666666 !default; +$grey-30: #4C4C4C !default; +$grey-20: #333333 !default; +$grey-10: #191919 !default; +$black: #000000 !default; + +// Theme +$primary: $green !default; +$secondary: $blue !default; +$tertiary: $red !default; +$light: $grey-100 !default; +$dark: $grey-30 !default; +$success: $green !default; +$info: $blue !default; +$warning: $yellow !default; +$danger: $red !default; + +// Code +$code-color: $primary !default; +$code-bg: $light !default; +$code-block-bg: $light !default; + +// Links +$link-color: $primary !default; + +// Popover +$popover: $light !default; + +// Dropdowns +$dropdown-link-color: $grey-30 !default; +$dropdown-link-hover-color: $white !default; +$dropdown-link-hover-bg: $primary !default; + +// Font +@import "https://fonts.googleapis.com/css2?family=Maven+Pro:wght@300;400;500;600"; +$font-family-sans-serif: "Maven Pro"; + +// Font size for headers +$h1-font-size: 1.75rem !default; +$h2-font-size: 1.50rem !default; +$h3-font-size: 1.25rem !default; + +// Font size base for reveal.js presentations +$presentation-font-size-root: 35px !default; + +// Tables +$table-bg-scale: 0 !default; + +// Navs +$nav-link-padding-y: .5rem !default !default; +$nav-link-padding-x: 2rem !default; +$nav-link-disabled-color: $grey-40 !default !default; +$nav-tabs-border-color: $grey-80 !default; + +// Navbar +$navbar-padding-y: 1rem !default; +$navbar-light-bg: $primary !default; +$navbar-light-color: $white !default; +$navbar-light-hover-color: $success !default; +$navbar-light-active-color: $success !default; +$navbar-light-brand-color: $white !default; +$navbar-light-brand-hover-color: $navbar-light-brand-color !default; +$navbar-dark-color: $white !default; +$navbar-dark-hover-color: $primary !default; +$navbar-dark-active-color: $primary !default; +$navbar-dark-brand-color: $white !default; +$navbar-dark-brand-hover-color: $navbar-dark-brand-color !default; + +// Pagination +$pagination-color: $white !default; +$pagination-bg: $success !default; +$pagination-border-width: 0 !default; +$pagination-border-color: transparent !default; +$pagination-hover-color: $white !default; +$pagination-hover-bg: darken($success, 15%) !default; +$pagination-hover-border-color: transparent !default; +$pagination-active-bg: $pagination-hover-bg !default; +$pagination-active-border-color: transparent !default; +$pagination-disabled-color: $grey-80 !default; +$pagination-disabled-bg: lighten($success, 15%) !default; +$pagination-disabled-border-color: transparent !default; + +// List group +$list-group-hover-bg: $grey-80 !default; +$list-group-disabled-bg: $grey-80 !default; + +// Close +$btn-close-color: $white !default; +$btn-close-opacity: .4 !default; +$btn-close-hover-opacity: 1 !default; + +/*-- scss:rules --*/ + +// Variables +$web-font-path: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap" !default; +@if $web-font-path { + @import url($web-font-path); +} + +// Navbar +.bg-primary { + .navbar-nav .show > .nav-link, + .navbar-nav .nav-link.active, + .navbar-nav .nav-link:hover, + .navbar-nav .nav-link:focus { + color: $success !important; + } +} + +// Navs +.nav-tabs { + .nav-link.active, + .nav-link.active:focus, + .nav-link.active:hover, + .nav-item.open .nav-link, + .nav-item.open .nav-link:focus, + .nav-item.open .nav-link:hover { + color: $primary; + } +} + +// Pagination +.pagination { + a:hover { + text-decoration: none; + } +} + +// Blockquotes +.blockquote { + color: $primary; + border-left-color: $primary; +} + +// Cell Output +.cell-output-error > pre > code { + color: $red; +} +.cell-output-stderr > pre > code { + color: $yellow; +} + +// Horizontally center level 1 headers +.center h1 { + text-align: center +} + +// Text justification +.justify-right { + text-align: right +} +.justify-center { + text-align: center +} + +// Custom colours +.green { + color: $green; + font-weight: bold; +} +.blue { + color: $blue; + font-weight: bold; +} +.red { + color: $red; + font-weight: bold; +} +.yellow { + color: $yellow; + font-weight: bold; +} +.grey { + color: $grey-70; + font-weight: bold; +} diff --git a/assets/_extensions/nf-core/nf-core.theme b/assets/_extensions/nf-core/nf-core.theme new file mode 100644 index 0000000..1039cfc --- /dev/null +++ b/assets/_extensions/nf-core/nf-core.theme @@ -0,0 +1,211 @@ +{ + "text-color": null, + "background-color": null, + "line-number-color": "#aaaaaa", + "line-number-background-color": null, + "text-styles": { + "Other": { + "text-color": "#9e9370", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Attribute": { + "text-color": "#000000", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "SpecialString": { + "text-color": "#bb6688", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Annotation": { + "text-color": "#005c86", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Function": { + "text-color": "#3073AF", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "String": { + "text-color": "#24B064 ", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "ControlFlow": { + "text-color": "#9e9370", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "Operator": { + "text-color": "#666666", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Error": { + "text-color": "#BB5454 ", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "BaseN": { + "text-color": "#DABC25", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Alert": { + "text-color": "#BB5454 ", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "Variable": { + "text-color": "#005c86", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "BuiltIn": { + "text-color": "#005c86", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Extension": { + "text-color": null, + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Preprocessor": { + "text-color": "#BBBB54 ", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Information": { + "text-color": "#005c86", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "VerbatimString": { + "text-color": "#005c86", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Warning": { + "text-color": "#BBBB54 ", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Documentation": { + "text-color": "#BB5454 ", + "background-color": null, + "bold": false, + "italic": true, + "underline": false + }, + "Import": { + "text-color": "#BB5454 ", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "Char": { + "text-color": "#af75a7 ", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "DataType": { + "text-color": "#9e9370", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Float": { + "text-color": "#DABC25 ", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Comment": { + "text-color": "#B2B2B2 ", + "background-color": null, + "bold": false, + "italic": true, + "underline": false + }, + "CommentVar": { + "text-color": "#B2B2B2 ", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Constant": { + "text-color": "#B2B2B2", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "SpecialChar": { + "text-color": "#B2B2B2", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "DecVal": { + "text-color": "#DABC25 ", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Keyword": { + "text-color": "#BB5454 ", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + } + } +} From 812f462c857951b73d5d2eb330711841fbda66c2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 15:59:04 +0100 Subject: [PATCH 265/410] Use nf-core Quarto template in reports --- bin/st_clustering.qmd | 5 +---- bin/st_qc_and_normalisation.qmd | 5 +---- bin/st_spatial_de.qmd | 5 +---- modules/local/st_clustering.nf | 1 + modules/local/st_qc_and_normalisation.nf | 1 + modules/local/st_spatial_de.nf | 1 + subworkflows/local/st_postprocess.nf | 3 +++ subworkflows/local/st_preprocess.nf | 2 ++ 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 8a6da8e..a51da68 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -1,10 +1,7 @@ --- title: "Clustering Spatial Transcriptomics data" format: - html: - embed-resources: true - page-layout: full - code-fold: true + nf-core-html: default execute: keep-ipynb: true jupyter: python3 diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index d526d7d..5f8f788 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -1,10 +1,7 @@ --- title: "Pre-processing and quality control of Spatial Transcriptomics data" format: - html: - embed-resources: true - page-layout: full - code-fold: true + nf-core-html: default jupyter: python3 --- diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index d8a407a..82ace38 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -5,10 +5,7 @@ TODO: Make report more complete, add descriptions, etc. --- title: "Differential Gene Expression and spatially variable genes" format: - html: - embed-resources: true - page-layout: full - code-fold: true + nf-core-html: default jupyter: python3 --- diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 0da7fbb..22cc6a3 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -22,6 +22,7 @@ process ST_CLUSTERING { input: path(report) + path(report_template) tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index e268ffb..7f67c00 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -22,6 +22,7 @@ process ST_QC_AND_NORMALISATION { input: path(report) + path(report_template) tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") output: diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 6f0f617..40197db 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -21,6 +21,7 @@ process ST_SPATIAL_DE { } input: path(report) + path(report_template) tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: diff --git a/subworkflows/local/st_postprocess.nf b/subworkflows/local/st_postprocess.nf index defa6d6..7ad4475 100644 --- a/subworkflows/local/st_postprocess.nf +++ b/subworkflows/local/st_postprocess.nf @@ -19,12 +19,14 @@ workflow ST_POSTPROCESS { // report_clustering = file("${projectDir}/bin/st_clustering.qmd") report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") + report_template = Channel.fromPath("${projectDir}/assets/_extensions") // // Clustering // ST_CLUSTERING ( report_clustering, + report_template, st_adata_norm ) ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) @@ -34,6 +36,7 @@ workflow ST_POSTPROCESS { // ST_SPATIAL_DE ( report_spatial_de, + report_template, ST_CLUSTERING.out.st_adata_processed ) ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index 3827392..623d3f2 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -17,12 +17,14 @@ workflow ST_PREPROCESS { // Report files // report = file("${projectDir}/bin/st_qc_and_normalisation.qmd") + report_template = Channel.fromPath("${projectDir}/assets/_extensions") // // Spatial pre-processing // ST_QC_AND_NORMALISATION ( report, + report_template, st_raw ) ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) From 662e68ccc3a290dc0335d55606c3d2ab0f6384fc Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 17:54:48 +0100 Subject: [PATCH 266/410] Fix broken QC report link --- bin/st_qc_and_normalisation.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 5f8f788..eb4cb79 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -7,7 +7,7 @@ jupyter: python3 ## Introduction -Spatial Transcriptomics data analysis involves several steps, including quality control (QC) and pre-processing, to ensure the reliability of downstream analyses. This report outlines the QC and pre-processing steps for Visium Spatial Transcriptomics data using the (https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html)[anndata format]. +Spatial Transcriptomics data analysis involves several steps, including quality control (QC) and pre-processing, to ensure the reliability of downstream analyses. This report outlines the QC and pre-processing steps for Visium Spatial Transcriptomics data using the [anndata format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html). Quality control is an essential step in spatial transcriptomics to identify and filter out spots and genes that may introduce noise or bias into the analysis. From 41e601d8cebcab3412250f9152448edc9b9971eb Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 17:55:09 +0100 Subject: [PATCH 267/410] Echo QC report import code chunk --- bin/st_qc_and_normalisation.qmd | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index eb4cb79..57e81b2 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -32,7 +32,6 @@ nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` ```{python} -#| echo: false import scanpy as sc import scipy import pandas as pd From 3ecbe91e03f6c023d8b6dd429770f629e038377b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 18:28:28 +0100 Subject: [PATCH 268/410] Formatting and clean-up of QC report --- bin/st_qc_and_normalisation.qmd | 94 +++++++++++++++++---------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 57e81b2..36104ca 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -1,5 +1,6 @@ --- -title: "Pre-processing and quality control of Spatial Transcriptomics data" +title: "nf-core/spatialtranscriptomics" +subtitle: "Pre-processing and quality controls" format: nf-core-html: default jupyter: python3 @@ -7,10 +8,18 @@ jupyter: python3 ## Introduction -Spatial Transcriptomics data analysis involves several steps, including quality control (QC) and pre-processing, to ensure the reliability of downstream analyses. This report outlines the QC and pre-processing steps for Visium Spatial Transcriptomics data using the [anndata format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html). - -Quality control is an essential step in spatial transcriptomics to identify and filter out spots and genes that may introduce noise or bias into the analysis. +Spatial Transcriptomics data analysis involves several steps, including quality +controls (QC) and pre-processing, to ensure the reliability of downstream +analyses. This is an essential step in spatial transcriptomics to +identify and filter out spots and genes that may introduce noise and/or bias +into the analysis. +This report outlines the QC and pre-processing steps for Visium Spatial +Transcriptomics data using the [anndata format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html). +The anndata format is utilized to organize and store the Spatial Transcriptomics +data. It includes information about counts, features, observations, and +additional metadata. The anndata format ensures compatibility with various +analysis tools and facilitates seamless integration into existing workflows. ```{python} #| tags: [parameters] @@ -38,39 +47,40 @@ import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from IPython.display import display, Markdown - plt.rcParams["figure.figsize"] = (6, 6) ``` -## Anndata object - -The anndata format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and additional metadata. The anndata format ensures compatibility with various analysis tools and facilitates seamless integration into existing workflows. - ```{python} +# Read the data st_adata = sc.read("./" + rawAdata) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) -# Get mitochondrial percentages +# Calculate mitochondrial and haemoglobin percentages st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') st_adata.var['hb'] = st_adata.var_names.str.contains(("^Hb.*-")) - sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "hb"], inplace=True) +# Save a copy of data as a restore-point if filtering results in 0 spots left st_adata_before_filtering = st_adata.copy() +# Print the anndata object for inspection print ("Content of the anndata object:") st_adata ``` -## Quality Control +## Quality Controls -The following plots show the distribution of the number of genes per counts and counts per spot, as well as the percentage of counts from mitochondrial genes and hemoglobin genes. +The following plots show the distribution of the number of genes per counts and +counts per spot, as well as the percentage of counts from mitochondrial genes +and haemoglobin genes. ```{python} -sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) -sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) +sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], + multi_panel=True, jitter=0.4, rotation= 45) +sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], + multi_panel=True, jitter=0.4, rotation= 45) ``` The same can be plotted on top of the tissue: @@ -84,16 +94,17 @@ sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_hb"]) ### In-tissue Filtering -Spots outside the tissue are removed: +Spots outside the tissue are not needed and are thus removed. ```{python} # Create a string observation "obs/in_tissue_str" with "In tissue" and "Outside tissue": st_adata.obs["in_tissue_str"] = ["In tissue" if x == 1 else "Outside tissue" for x in st_adata.obs["in_tissue"]] +# Plot spots inside tissue sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue") del st_adata.obs["in_tissue_str"] -# Remove spots outside tissue +# Remove spots outside tissue and print results Number_spots = st_adata.shape[0] st_adata = st_adata[st_adata.obs["in_tissue"] == 1] Number_spots_in_tissue = st_adata.shape[0] @@ -103,6 +114,7 @@ print (f"{Number_spots_in_tissue} spots in tissue on {Number_spots} spots in tot Distribution of counts per spots and genes per counts. ```{python} +#| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." fig, axs = plt.subplots(2, 2, figsize=(8, 7)) p = sns.histplot( st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] @@ -128,9 +140,6 @@ p = sns.histplot( fig.tight_layout() ``` -_Top row: Distribution of the number of counts per spots._ -_Bottom row: Distribution of the number of genes with at least 1 count in a spot._ - ### Cell and Gene Filtering We filter cells based on minimum counts and genes, and filter genes based on minimum cells. @@ -158,6 +167,7 @@ print (f"Removed {Number_genes - Number_genes_filtered_minCells} genes expressed print (f"{Number_spots_filtered_minGenes} out of {Number_spots} spots remaining after filtering.") print (f"{Number_genes_filtered_minCells} out of {Number_genes} genes remaining after filtering.") +# Restore non-filtered data if filtering results in 0 spots left if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): st_adata = st_adata_before_filtering display( @@ -165,9 +175,15 @@ if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): ::: {.callout-important .content-visible when-format="html"} ## Issue: No Spots Remain After Filtering -An anomaly has been detected in the data: following the filtering process, all spots have been excluded. It is imperative to assess the data quality and carefully review the Analysis Options outlined in `docs/usage.md`. +An anomaly has been detected in the data: following the filtering process, +all spots have been excluded. It is imperative to assess the data quality +and carefully review the Analysis Options outlined in `docs/usage.md`. -To ensure the smooth progression of downstream analysis, the exported AnnData will, for the time being, remain unfiltered. This precautionary measure is implemented to facilitate continued analysis while investigating and resolving the cause of the unexpected removal of all spots during filtering. +To ensure the smooth progression of downstream analysis, the exported +AnnData will, for the time being, remain unfiltered. This precautionary +measure is implemented to facilitate continued analysis while +investigating and resolving the cause of the unexpected removal of all +spots during filtering. :::""" ) ) @@ -176,6 +192,7 @@ To ensure the smooth progression of downstream analysis, the exported AnnData wi Distributions after filtering: ```{python} +#| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." fig, axs = plt.subplots(2, 2, figsize=(8, 7)) p = sns.histplot( st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] @@ -201,18 +218,11 @@ p = sns.histplot( fig.tight_layout() ``` -_Top row: Distribution of the number of counts per spots._ -_Bottom row: Distribution of the number of genes with at least 1 count in a spot._ - -```{python} -# Write pre-processed data to files -st_adata.write(nameDataPlain) -print (f"Non-normalized Anndata object saved to data/{nameDataPlain}.") -``` - -### Normalization +## Normalization -We proceed to normalize Visium counts data using the built-in `normalize_total` method from Scanpy followed by a log-transformation. +We proceed to normalize Visium counts data using the built-in `normalize_total` +method from [Scanpy](https://scanpy.readthedocs.io/en/stable/) followed by a +log-transformation. ```{python} sc.pp.normalize_total(st_adata, inplace=True) @@ -222,32 +232,26 @@ sc.pp.log1p(st_adata) ### Top expressed genes ```{python} +# Plot top 20 most highly expressed genes sc.pl.highest_expr_genes(st_adata, n_top=20) ``` ### Highly variable genes -Highly variable genes are detected for subsequent analysis. +The top 2000 highly variable genes are detected for use in subsequent analyses. ```{python} +# Find top HVGs and print results sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) var_genes_all = st_adata.var.highly_variable print("Extracted highly variable genes: %d"%sum(var_genes_all)) -``` - -Plot of highly variable genes dispersion: -```{python} -# plot the table of highly variable genes +# Plot the HVGs sc.pl.highly_variable_genes(st_adata) ``` -### Anndata export - -All anndata files saved can be found in the `data` directory. - ```{python} -# Write normalized data to files +#| echo: false +# Write normalized data to disk st_adata.write(nameDataNorm) -print (f"Normalized Anndata object saved to data/{nameDataPlain}.") ``` From 7b4c9d0663abd8bda6ffd0e93a7980f74eddcf46 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 19:10:52 +0100 Subject: [PATCH 269/410] Remove unused `st_data_plain` QC report output --- bin/st_qc_and_normalisation.qmd | 3 +-- modules/local/st_qc_and_normalisation.nf | 2 -- subworkflows/local/st_preprocess.nf | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 36104ca..8bb6aa1 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -36,7 +36,6 @@ minCells = 1 # Min cells per gene histplotQCmaxTotalCounts = 10000 # Max total counts histplotQCminGeneCounts = 4000 # Min gene counts histplotQCbins = 40 # Number of bins -nameDataPlain = "st_adata_plain.h5ad" # Name of the raw data save file nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` @@ -218,7 +217,7 @@ p = sns.histplot( fig.tight_layout() ``` -## Normalization +### Normalization We proceed to normalize Visium counts data using the built-in `normalize_total` method from [Scanpy](https://scanpy.readthedocs.io/en/stable/) followed by a diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 7f67c00..992d43b 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -27,7 +27,6 @@ process ST_QC_AND_NORMALISATION { output: tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm - tuple val(meta), path("st_adata_plain.h5ad") , emit: st_data_plain tuple val(meta), path("st_qc_and_normalisation.html") , emit: html path("versions.yml") , emit: versions @@ -45,7 +44,6 @@ process ST_QC_AND_NORMALISATION { -P histplotQCmaxTotalCounts:${params.st_preprocess_hist_qc_max_total_counts} \ -P histplotQCminGeneCounts:${params.st_preprocess_hist_qc_min_gene_counts} \ -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ - -P nameDataPlain:st_adata_plain.h5ad \ -P nameDataNorm:st_adata_norm.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index 623d3f2..499c144 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -31,7 +31,6 @@ workflow ST_PREPROCESS { emit: st_data_norm = ST_QC_AND_NORMALISATION.out.st_data_norm // channel: [ val(sample), h5ad ] - st_data_plain = ST_QC_AND_NORMALISATION.out.st_data_plain // channel: [ val(sample), h5ad ] versions = ch_versions // channel: [ version.yml ] } From 7842683f02adc8912b1284f405698eecba966926 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Jan 2024 19:13:26 +0100 Subject: [PATCH 270/410] Do not check SCSS files with Prettier --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index bd2d59c..dd856fc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,4 @@ testing* bin/ test-datasets/ .nf-test/ +*.scss From 1924ce728bb92a41306c63748ce06e4d034b5bd3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 17 Jan 2024 11:30:44 +0100 Subject: [PATCH 271/410] Fix QC report figure sizes --- bin/st_qc_and_normalisation.qmd | 103 ++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 8bb6aa1..2096a76 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -76,6 +76,7 @@ counts per spot, as well as the percentage of counts from mitochondrial genes and haemoglobin genes. ```{python} +#| layout-nrow: 2 sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], @@ -85,6 +86,7 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], The same can be plotted on top of the tissue: ```{python} +#| layout-nrow: 2 sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_hb"]) ``` @@ -115,27 +117,29 @@ Distribution of counts per spots and genes per counts. ```{python} #| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -p = sns.histplot( - st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] -).set(title=f"Total counts") -p = sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], - kde=True, - bins=histplotQCbins, - ax=axs[0, 1] -).set(title=f"Total counts < {histplotQCmaxTotalCounts}") -p = sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, - ax=axs[1, 0] -).set(title=f"Genes by counts") -p = sns.histplot( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], - kde=True, - bins=histplotQCbins, - ax=axs[1, 1] -).set(title=f"Genes by counts < {histplotQCminGeneCounts}") +sns.histplot( + st_adata.obs["total_counts"], + kde=True, + ax=axs[0, 0] + ).set(title=f"Total counts") +sns.histplot( + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + kde=True, + bins=histplotQCbins, + ax=axs[0, 1] + ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") +sns.histplot( + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, + ax=axs[1, 0] + ).set(title=f"Genes by counts") +sns.histplot( + st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], + kde=True, + bins=histplotQCbins, + ax=axs[1, 1] + ).set(title=f"Genes by counts < {histplotQCminGeneCounts}") fig.tight_layout() ``` @@ -165,12 +169,15 @@ print (f"Removed {Number_genes - Number_genes_filtered_minCells} genes expressed print (f"{Number_spots_filtered_minGenes} out of {Number_spots} spots remaining after filtering.") print (f"{Number_genes_filtered_minCells} out of {Number_genes} genes remaining after filtering.") +``` +```{python} +#| echo: false # Restore non-filtered data if filtering results in 0 spots left if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): - st_adata = st_adata_before_filtering - display( - Markdown(""" + st_adata = st_adata_before_filtering + display( + Markdown(""" ::: {.callout-important .content-visible when-format="html"} ## Issue: No Spots Remain After Filtering @@ -184,8 +191,8 @@ measure is implemented to facilitate continued analysis while investigating and resolving the cause of the unexpected removal of all spots during filtering. :::""" + ) ) - ) ``` Distributions after filtering: @@ -193,27 +200,29 @@ Distributions after filtering: ```{python} #| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -p = sns.histplot( - st_adata.obs["total_counts"], kde=True, ax=axs[0, 0] -).set(title=f"Total counts") -p = sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], - kde=True, - bins=histplotQCbins, - ax=axs[0, 1] -).set(title=f"Total counts < {histplotQCmaxTotalCounts}") -p = sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, - ax=axs[1, 0] -).set(title=f"Genes by counts") -p = sns.histplot( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], - kde=True, - bins=histplotQCbins, - ax=axs[1, 1] -).set(title=f"Genes by counts < {histplotQCminGeneCounts}") +sns.histplot( + st_adata.obs["total_counts"], + kde=True, + ax=axs[0, 0] + ).set(title=f"Total counts") +sns.histplot( + st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], + kde=True, + bins=histplotQCbins, + ax=axs[0, 1] + ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") +sns.histplot( + st_adata.obs["n_genes_by_counts"], + kde=True, + bins=histplotQCbins, + ax=axs[1, 0] + ).set(title=f"Genes by counts") +sns.histplot( + st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], + kde=True, + bins=histplotQCbins, + ax=axs[1, 1] + ).set(title=f"Genes by counts < {histplotQCminGeneCounts}") fig.tight_layout() ``` @@ -240,12 +249,14 @@ sc.pl.highest_expr_genes(st_adata, n_top=20) The top 2000 highly variable genes are detected for use in subsequent analyses. ```{python} +# layout-nrow: 1 # Find top HVGs and print results sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) var_genes_all = st_adata.var.highly_variable print("Extracted highly variable genes: %d"%sum(var_genes_all)) # Plot the HVGs +plt.rcParams["figure.figsize"] = (4.5, 4.5) sc.pl.highly_variable_genes(st_adata) ``` From e256687fcb0f182d18216c97f59ef9d7d1f25908 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 17 Jan 2024 12:01:42 +0100 Subject: [PATCH 272/410] Add parameter for number of highly variable genes --- bin/st_qc_and_normalisation.qmd | 5 +++-- docs/usage.md | 1 + modules/local/st_qc_and_normalisation.nf | 1 + nextflow.config | 1 + nextflow_schema.json | 6 ++++++ 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 2096a76..80957f0 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -36,6 +36,7 @@ minCells = 1 # Min cells per gene histplotQCmaxTotalCounts = 10000 # Max total counts histplotQCminGeneCounts = 4000 # Min gene counts histplotQCbins = 40 # Number of bins +nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` @@ -246,12 +247,12 @@ sc.pl.highest_expr_genes(st_adata, n_top=20) ### Highly variable genes -The top 2000 highly variable genes are detected for use in subsequent analyses. +The top highly variable genes are detected for use in subsequent analyses. ```{python} # layout-nrow: 1 # Find top HVGs and print results -sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=2000) +sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=nHighlyVariableGenes) var_genes_all = st_adata.var.highly_variable print("Extracted highly variable genes: %d"%sum(var_genes_all)) diff --git a/docs/usage.md b/docs/usage.md index 13a6921..f83340b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -144,6 +144,7 @@ The following parameters are exposed for preprocessing: - `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. - `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. +- `--st_preprocess_n_hvgs`: Number of top highly variable genes to use for the analyses. ### Parameters for Clustering : diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 992d43b..0b0eed9 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -44,6 +44,7 @@ process ST_QC_AND_NORMALISATION { -P histplotQCmaxTotalCounts:${params.st_preprocess_hist_qc_max_total_counts} \ -P histplotQCminGeneCounts:${params.st_preprocess_hist_qc_min_gene_counts} \ -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ + -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ -P nameDataNorm:st_adata_norm.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/nextflow.config b/nextflow.config index 58fb589..65e401c 100644 --- a/nextflow.config +++ b/nextflow.config @@ -24,6 +24,7 @@ params { st_preprocess_hist_qc_max_total_counts = 10000 st_preprocess_hist_qc_min_gene_counts = 4000 st_preprocess_hist_qc_bins = 40 + st_preprocess_n_hvgs = 2000 // Clustering st_cluster_resolution = 1 diff --git a/nextflow_schema.json b/nextflow_schema.json index 8fce53e..953e707 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -131,6 +131,12 @@ "description": "The number of bins for the QC histogram plots.", "fa_icon": "fas fa-chart-simple" }, + "st_preprocess_n_hvgs": { + "type": "integer", + "default": 2000, + "description": "The number of top highly variable genes to use for the analyses.", + "fa_icon": "fas fa-hashtag" + }, "st_cluster_resolution": { "type": "number", "default": 0.4, From 211639559bf07a85704a2aa69e14cd3f50f622b8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 17 Jan 2024 12:04:48 +0100 Subject: [PATCH 273/410] Fix QC report header capitalisation --- bin/st_qc_and_normalisation.qmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 80957f0..e1d754f 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -70,7 +70,7 @@ print ("Content of the anndata object:") st_adata ``` -## Quality Controls +## Quality controls The following plots show the distribution of the number of genes per counts and counts per spot, as well as the percentage of counts from mitochondrial genes @@ -94,7 +94,7 @@ sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_hb"]) ## Filtering -### In-tissue Filtering +### In-tissue filtering Spots outside the tissue are not needed and are thus removed. @@ -144,7 +144,7 @@ sns.histplot( fig.tight_layout() ``` -### Cell and Gene Filtering +### Cell and gene filtering We filter cells based on minimum counts and genes, and filter genes based on minimum cells. From 7df9db4b3e0671c53d4cb7ac0ef6ccbe48436b5c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 17 Jan 2024 12:26:40 +0100 Subject: [PATCH 274/410] Formatting and clean-up of clustering report --- bin/st_clustering.qmd | 52 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index a51da68..41b490f 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -1,5 +1,6 @@ --- -title: "Clustering Spatial Transcriptomics data" +title: "nf-core/spatialtranscriptomics" +subtitle: "Dimensionality reduction and clustering" format: nf-core-html: default execute: @@ -16,36 +17,37 @@ resolution = 1 saveFileST = None ``` +# Reading the data + +The data has already been filtered (see the `st_qc_and_normalisation` report) +and is saved in AnnData format. + ```{python} -#| echo: false -# Hide warnings in output html. +#| warning: false import scanpy as sc import numpy as np import pandas as pd from umap import UMAP from matplotlib import pyplot as plt import seaborn as sns -import warnings -warnings.filterwarnings("ignore") sc.settings.verbosity = 0 sc.set_figure_params(dpi_save=300, facecolor="white") ``` -## Reading the data -The data has already been filtered (see `st_qc_and_normalisation` report) and is saved in AnnData format. - ```{python} -#| echo: true -#| code-fold: false st_adata = sc.read("./" + fileNameST) print (f"Loading {fileNameST}:") st_adata ``` -## Manifold embedding and clustering based on transcriptional similarity +# Manifold embedding and clustering -To uncover the underlying structure of the transcriptional landscape, we perform manifold embedding and clustering based on transcriptional similarity. Principal Component Analysis (PCA) is applied to reduce dimensionality, and UMAP (Uniform Manifold Approximation and Projection) is used for visualization. The Leiden algorithm is employed for clustering with a givent resolution. +To uncover the underlying structure of the transcriptional landscape, we perform +manifold embedding and clustering based on transcriptional similarity. Principal +Component Analysis (PCA) is applied to reduce dimensionality, and UMAP (Uniform +Manifold Approximation and Projection) is used for visualization. The Leiden +algorithm is employed for clustering with a given resolution. ```{python} sc.pp.pca(st_adata) @@ -55,9 +57,8 @@ print (f"Resolution for Leiden clustering: {resolution}") sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) ``` -## Exploratory Data Analysis - -We generate UMAP plots to visualize the spatial distribution of clusters and investigate potential associations with total counts and detected genes. +We then generate UMAP plots to visualize the spatial distribution of clusters +and investigate potential associations with total counts and detected genes. ```{python} # Make plots of UMAP of ST spots clusters @@ -69,9 +70,10 @@ sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") sc.pl.embedding_density(st_adata, groupby="clusters", ncols=4) ``` -## Visualization in spatial coordinates +# Visualization in spatial coordinates -Next, we examine how total counts and the number of detected genes behave in spatial coordinates. We overlay the circular spots on the Hematoxylin and Eosin stain (H&E) image to provide a spatial context. +Next, we examine how total counts and the number of detected genes behave in +spatial coordinates by overlaying the spots on the image itself. ```{python} plt.rcParams["figure.figsize"] = (10, 10) @@ -80,9 +82,11 @@ sc.pl.spatial( ) ``` -## Spatial Clustering Visualization - -To gain insights into tissue organization and potential inter-cellular communication, we visualize the spatial distribution of clusters on the H&E image. +To gain insights into tissue organization and potential inter-cellular +communication, we visualize the spatial distribution of clusters on the image. +Spots belonging to the same cluster in gene expression space often co-occur in +spatial dimensions, providing valuable information about the spatial +organization of cells. ```{python} plt.rcParams["figure.figsize"] = (10, 10) @@ -91,20 +95,14 @@ sc.pl.spatial( ) ``` -Spots belonging to the same cluster in gene expression space often co-occur in spatial dimensions, providing valuable information about the spatial organization of cells. - ```{python} #| echo: false # Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 st_adata.uns['log1p']['base'] = None ``` -## Data Saving - -To facilitate future analyses and reproducibility, the processed AnnData object is saved in the `data` directory. - ```{python} +#| echo: false if saveFileST is not None: st_adata.write(saveFileST) - print (f"Anndata object saved to data/{saveFileST}.") ``` From adbd9ca9d1cd072cfe2d347403138f283bff2260 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 17 Jan 2024 13:16:02 +0100 Subject: [PATCH 275/410] Formatting and clean-up of DE report --- bin/st_spatial_de.qmd | 45 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 82ace38..e60f8a6 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -1,9 +1,6 @@ - - --- -title: "Differential Gene Expression and spatially variable genes" +title: "nf-core/spatialtranscriptomics" +subtitle: "Differential gene expression" format: nf-core-html: default jupyter: python3 @@ -12,7 +9,6 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false - fileNameST = "" numberOfColumns = 5 saveDEFileName = "" @@ -21,7 +17,6 @@ plotTopHVG = 15 ``` ```{python} -# Load packages import scanpy as sc import pandas as pd import SpatialDE @@ -33,32 +28,39 @@ st_adata = sc.read(fileNameST) st_adata ``` -## Differential Gene Expression +# Differential gene expression + +We first find differentially expressed genes across the different clusters found +in the dataset. ```{python} +#| warning: false plt.rcParams["figure.figsize"] = (5, 5) st_adata.uns['log1p']['base'] = None - sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") ``` -```{python} -sc.tl.rank_genes_groups(st_adata, 'clusters', method='wilcoxon') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") -``` - -## Spatially variable genes +# Spatially variable genes -Spatial transcriptomics allows researchers to investigate how gene expression trends varies in space, thus identifying spatial patterns of gene expression. For this purpose, we use SpatialDE [Svensson18](https://www.nature.com/articles/nmeth.4636) ([code](https://github.com/Teichlab/SpatialDE)), a Gaussian process-based statistical framework that aims to identify spatially variable genes. +Spatial transcriptomics allows researchers to investigate how gene expression +trends varies in space, thus identifying spatial patterns of gene expression. +For this purpose, we use SpatialDE +[Svensson18](https://www.nature.com/articles/nmeth.4636) +([code](https://github.com/Teichlab/SpatialDE)), a Gaussian process-based +statistical framework that aims to identify spatially variable genes. -First, we convert normalized counts and coordinates to pandas dataframe, needed for inputs to spatialDE. +First, we convert normalized counts and coordinates to pandas dataframe, needed +for inputs to spatialDE. ```{python} results = SpatialDE.run(st_adata.obsm["spatial"], st_adata.to_df()) ``` -We concatenate the results with the DataFrame of annotations of variables: `st_adata.var`. +We concatenate the results with the DataFrame of annotations of variables: +`st_adata.var`. We can then inspect significant genes that varies in space and +visualize them with `sc.pl.spatial` function. + ```{python} results.set_index("g", inplace=True) @@ -66,11 +68,7 @@ results.set_index("g", inplace=True) results = results.loc[~results.index.duplicated(keep="first")] st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, :]], axis=1) -``` - -Then we can inspect significant genes that varies in space and visualize them with `sc.pl.spatial` function. -```{python} results_tab = st_adata.var.sort_values("qval", ascending=True) results_tab.to_csv(saveSpatialDEFileName) results_tab.head(plotTopHVG) @@ -78,5 +76,6 @@ results_tab.head(plotTopHVG) ```{python} symbols = results_tab.iloc[: plotTopHVG]["gene_symbol"] -sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, ncols=numberOfColumns, title=symbols) +sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, + ncols=numberOfColumns, title=symbols) ``` From 29d8becc5112b79553e27fed11faec44af05ef61 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 12:35:25 +0100 Subject: [PATCH 276/410] Update tests --- tests/pipeline/test_downstream.nf.test | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 11d9d3a..3f9ad4f 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -28,9 +28,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 0421158..66b3562 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -26,9 +26,9 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, // DEGs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index e9ee204..738a70b 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -22,9 +22,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("Distributions after filtering") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("AnnData object is saved in the") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("Spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, From d5bd0e0587a78649b42f645c3072f45ef4821c16 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 12:36:20 +0100 Subject: [PATCH 277/410] Update `untar` module --- modules.json | 2 +- modules/nf-core/untar/tests/main.nf.test | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules.json b/modules.json index 64ed110..07454f7 100644 --- a/modules.json +++ b/modules.json @@ -27,7 +27,7 @@ }, "untar": { "branch": "master", - "git_sha": "8e6960947b6647952b97667173805b34b40f25d6", + "git_sha": "e719354ba77df0a1bd310836aa2039b45c29d620", "installed_by": ["modules"] } } diff --git a/modules/nf-core/untar/tests/main.nf.test b/modules/nf-core/untar/tests/main.nf.test index f57234f..679e83c 100644 --- a/modules/nf-core/untar/tests/main.nf.test +++ b/modules/nf-core/untar/tests/main.nf.test @@ -16,7 +16,7 @@ nextflow_process { } process { """ - input[0] = [ [], file(params.testdata_base_path + 'modules/genomics/sarscov2/genome/db/kraken2.tar.gz', checkIfExists: true) ] + input[0] = [ [], file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/db/kraken2.tar.gz', checkIfExists: true) ] """ } } @@ -38,7 +38,7 @@ nextflow_process { } process { """ - input[0] = [ [], file(params.testdata_base_path + 'modules/generic/tar/hello.tar.gz', checkIfExists: true) ] + input[0] = [ [], file(params.modules_testdata_base_path + 'generic/tar/hello.tar.gz', checkIfExists: true) ] """ } } From e7934f04dff540c2a3c3c47195a77f4af0c040d1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 16:02:46 +0100 Subject: [PATCH 278/410] Also check ribosomal content in QC report --- bin/st_qc_and_normalisation.qmd | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index e1d754f..8e46827 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -15,7 +15,8 @@ identify and filter out spots and genes that may introduce noise and/or bias into the analysis. This report outlines the QC and pre-processing steps for Visium Spatial -Transcriptomics data using the [anndata format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html). +Transcriptomics data using the [AnnData format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html) +and the [`scanpy` Python package](https://scanpy.readthedocs.io/en/stable/). The anndata format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and additional metadata. The anndata format ensures compatibility with various @@ -57,14 +58,6 @@ st_adata = sc.read("./" + rawAdata) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) -# Calculate mitochondrial and haemoglobin percentages -st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') -st_adata.var['hb'] = st_adata.var_names.str.contains(("^Hb.*-")) -sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "hb"], inplace=True) - -# Save a copy of data as a restore-point if filtering results in 0 spots left -st_adata_before_filtering = st_adata.copy() - # Print the anndata object for inspection print ("Content of the anndata object:") st_adata @@ -73,23 +66,35 @@ st_adata ## Quality controls The following plots show the distribution of the number of genes per counts and -counts per spot, as well as the percentage of counts from mitochondrial genes -and haemoglobin genes. +counts per spot, as well as the percentage of counts from mitochondrial, +ribosomal and haemoglobin genes. ```{python} #| layout-nrow: 2 + +# Calculate mitochondrial, ribosomal and haemoglobin percentages +st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') +st_adata.var['ribo'] = st_adata.var_names.str.contains(("^RP[LS]")) +st_adata.var['hb'] = st_adata.var_names.str.contains(("^HB[AB]")) +sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "ribo", "hb"], + inplace=True, log1p=False) + +# Save a copy of data as a restore-point if filtering results in 0 spots left +st_adata_before_filtering = st_adata.copy() + +# Plot QC metrics sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) -sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_hb'], +sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` -The same can be plotted on top of the tissue: +The same quality metrics can also be plotted on top of the tissue: ```{python} #| layout-nrow: 2 sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) -sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_hb"]) +sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"]) ``` ## Filtering From eb9eaf92343c4509285b536f62628d3550a34c00 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 18:13:30 +0100 Subject: [PATCH 279/410] Add A vs B QC metrics plots --- bin/st_qc_and_normalisation.qmd | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 8e46827..cd14421 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -65,9 +65,10 @@ st_adata ## Quality controls -The following plots show the distribution of the number of genes per counts and -counts per spot, as well as the percentage of counts from mitochondrial, -ribosomal and haemoglobin genes. +There are several different quality metrics that are normally computed for +spatial data. The following plots show the distribution of the number of genes +per counts and counts per spot, as well as the percentage of counts from +mitochondrial, ribosomal and haemoglobin genes: ```{python} #| layout-nrow: 2 @@ -89,7 +90,8 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` -The same quality metrics can also be plotted on top of the tissue: +The same quality metrics can also be plotted on top of the tissue so that +spatial patterns may be discerned: ```{python} #| layout-nrow: 2 @@ -97,6 +99,16 @@ sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"]) ``` +It is also useful to some of these quality metrics against each other, such as +mitochondrial versus ribosomal content and the total counts versus the number of +genes: + +```{python} +#| layout-ncol: 2 +sc.pl.scatter(st_adata, x='pct_counts_ribo', y='pct_counts_mt') +sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') +``` + ## Filtering ### In-tissue filtering From f99bfc6d2eb712c1e3e6da38bbcd42cc2f683796 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 18:14:02 +0100 Subject: [PATCH 280/410] Fix QC report header levels --- bin/st_qc_and_normalisation.qmd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index cd14421..6ca2e84 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -63,7 +63,7 @@ print ("Content of the anndata object:") st_adata ``` -## Quality controls +# Quality controls There are several different quality metrics that are normally computed for spatial data. The following plots show the distribution of the number of genes @@ -109,9 +109,9 @@ sc.pl.scatter(st_adata, x='pct_counts_ribo', y='pct_counts_mt') sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') ``` -## Filtering +# Filtering -### In-tissue filtering +## In-tissue filtering Spots outside the tissue are not needed and are thus removed. @@ -161,7 +161,7 @@ sns.histplot( fig.tight_layout() ``` -### Cell and gene filtering +## Cell and gene filtering We filter cells based on minimum counts and genes, and filter genes based on minimum cells. @@ -244,7 +244,7 @@ sns.histplot( fig.tight_layout() ``` -### Normalization +# Normalization We proceed to normalize Visium counts data using the built-in `normalize_total` method from [Scanpy](https://scanpy.readthedocs.io/en/stable/) followed by a @@ -255,14 +255,14 @@ sc.pp.normalize_total(st_adata, inplace=True) sc.pp.log1p(st_adata) ``` -### Top expressed genes +# Top expressed genes ```{python} # Plot top 20 most highly expressed genes sc.pl.highest_expr_genes(st_adata, n_top=20) ``` -### Highly variable genes +# Highly variable genes The top highly variable genes are detected for use in subsequent analyses. From 2af8d6c38120edd011344b173c7ae1a28aa305fe Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 18:23:33 +0100 Subject: [PATCH 281/410] Add QC sub-headers and more explanations --- bin/st_qc_and_normalisation.qmd | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 6ca2e84..0e86ff8 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -66,8 +66,15 @@ st_adata # Quality controls There are several different quality metrics that are normally computed for -spatial data. The following plots show the distribution of the number of genes -per counts and counts per spot, as well as the percentage of counts from +spatial data. Common metrics include the number of genes with a least 1 count +(`n_genes_by_counts`), counts per spot (`total_counts`) as well as the +percentage of counts from mitochondrial, ribosomal and haemoglobin genes +(`pct_counts_[mt/ribo/hb]`). + +## Violin plots + +The following violin plots show the distribution of the number of +genes per counts and counts per spot, as well as the percentage of counts from mitochondrial, ribosomal and haemoglobin genes: ```{python} @@ -90,6 +97,8 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` +## Spatial distributions + The same quality metrics can also be plotted on top of the tissue so that spatial patterns may be discerned: @@ -99,9 +108,11 @@ sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"]) ``` -It is also useful to some of these quality metrics against each other, such as -mitochondrial versus ribosomal content and the total counts versus the number of -genes: +## Scatter plots + +It is also useful to some of these quality metrics against each other in scatter +plots, such as mitochondrial versus ribosomal content and the total counts +versus the number of genes: ```{python} #| layout-ncol: 2 From e02baee57f4e27b1d3f1a1233fb70abbe52a267c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 18 Jan 2024 18:29:52 +0100 Subject: [PATCH 282/410] Print all warnings in QC report --- bin/st_qc_and_normalisation.qmd | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 0e86ff8..320e464 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -177,8 +177,6 @@ fig.tight_layout() We filter cells based on minimum counts and genes, and filter genes based on minimum cells. ```{python} -#| warning: false - # Filter cells based on counts Number_spots = st_adata.shape[0] Number_genes = st_adata.shape[1] From 0e4f0c6d1c9e1a44050650bd7a39389d52252c9d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 09:39:12 +0100 Subject: [PATCH 283/410] Remove redundant QC plots Remove the redundant total counts/genes by counts histogram plots in the QC report, as violin plots show the same thing. Also replace the post-filtering plots with additional violin plots for consistency. --- bin/st_qc_and_normalisation.qmd | 82 +++++------------------- docs/usage.md | 3 - modules/local/st_qc_and_normalisation.nf | 3 - nextflow.config | 3 - nextflow_schema.json | 18 ------ 5 files changed, 16 insertions(+), 93 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 320e464..e7686eb 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -34,9 +34,6 @@ rawAdata = None # Name of the h5ad file minCounts = 500 # Min counts per spot minGenes = 250 # Min genes per spot minCells = 1 # Min cells per gene -histplotQCmaxTotalCounts = 10000 # Max total counts -histplotQCminGeneCounts = 4000 # Min gene counts -histplotQCbins = 40 # Number of bins nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` @@ -122,9 +119,10 @@ sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') # Filtering -## In-tissue filtering +## Non-tissue spots -Spots outside the tissue are not needed and are thus removed. +The following plot indicates which spots are outside of the tissue. These spots +are uninformative and are thus removed. ```{python} # Create a string observation "obs/in_tissue_str" with "In tissue" and "Outside tissue": @@ -138,43 +136,15 @@ del st_adata.obs["in_tissue_str"] Number_spots = st_adata.shape[0] st_adata = st_adata[st_adata.obs["in_tissue"] == 1] Number_spots_in_tissue = st_adata.shape[0] -print (f"{Number_spots_in_tissue} spots in tissue on {Number_spots} spots in total.") -``` - -Distribution of counts per spots and genes per counts. - -```{python} -#| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." -fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -sns.histplot( - st_adata.obs["total_counts"], - kde=True, - ax=axs[0, 0] - ).set(title=f"Total counts") -sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], - kde=True, - bins=histplotQCbins, - ax=axs[0, 1] - ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") -sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, - ax=axs[1, 0] - ).set(title=f"Genes by counts") -sns.histplot( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], - kde=True, - bins=histplotQCbins, - ax=axs[1, 1] - ).set(title=f"Genes by counts < {histplotQCminGeneCounts}") -fig.tight_layout() +Markdown(f"""A total of {Number_spots_in_tissue} spots are situated inside the +tissue, out of {Number_spots} spots in total.""") ``` ## Cell and gene filtering -We filter cells based on minimum counts and genes, and filter genes based on minimum cells. +We filter cells based on minimum counts and genes, but also filter genes based +on minimum cells; exactly what filtering criteria is reasonable is up to you and +your knowledge of the specific tissue at hand. ```{python} # Filter cells based on counts @@ -206,7 +176,7 @@ if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): display( Markdown(""" ::: {.callout-important .content-visible when-format="html"} -## Issue: No Spots Remain After Filtering +## Issue: no spots remain after filtering An anomaly has been detected in the data: following the filtering process, all spots have been excluded. It is imperative to assess the data quality @@ -222,35 +192,15 @@ spots during filtering. ) ``` -Distributions after filtering: +Following the filtering we can look at the same violin plots as above to see the +new distributions: ```{python} -#| fig-cap: "Top row: Distribution of the number of counts per spots. Bottom row: Distribution of the number of genes with at least 1 count in a spot." -fig, axs = plt.subplots(2, 2, figsize=(8, 7)) -sns.histplot( - st_adata.obs["total_counts"], - kde=True, - ax=axs[0, 0] - ).set(title=f"Total counts") -sns.histplot( - st_adata.obs["total_counts"][st_adata.obs["total_counts"] < histplotQCmaxTotalCounts], - kde=True, - bins=histplotQCbins, - ax=axs[0, 1] - ).set(title=f"Total counts < {histplotQCmaxTotalCounts}") -sns.histplot( - st_adata.obs["n_genes_by_counts"], - kde=True, - bins=histplotQCbins, - ax=axs[1, 0] - ).set(title=f"Genes by counts") -sns.histplot( - st_adata.obs["n_genes_by_counts"][st_adata.obs["n_genes_by_counts"] < histplotQCminGeneCounts], - kde=True, - bins=histplotQCbins, - ax=axs[1, 1] - ).set(title=f"Genes by counts < {histplotQCminGeneCounts}") -fig.tight_layout() +#| layout-nrow: 2 +sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], + multi_panel=True, jitter=0.4, rotation= 45) +sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], + multi_panel=True, jitter=0.4, rotation= 45) ``` # Normalization diff --git a/docs/usage.md b/docs/usage.md index f83340b..79fad0b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -141,9 +141,6 @@ The following parameters are exposed for preprocessing: - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. - `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. -- `--st_preprocess_hist_qc_max_total_counts`: Maximum total counts for the histogram plot in quality control. -- `--st_preprocess_hist_qc_min_gene_counts`: Minimum gene counts for the histogram plot in quality control. -- `--st_preprocess_hist_qc_bins`: Number of bins for the histogram plot in quality control. - `--st_preprocess_n_hvgs`: Number of top highly variable genes to use for the analyses. ### Parameters for Clustering : diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index 0b0eed9..b907049 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -41,9 +41,6 @@ process ST_QC_AND_NORMALISATION { -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ -P minCells:${params.st_preprocess_min_cells} \ - -P histplotQCmaxTotalCounts:${params.st_preprocess_hist_qc_max_total_counts} \ - -P histplotQCminGeneCounts:${params.st_preprocess_hist_qc_min_gene_counts} \ - -P histplotQCbins:${params.st_preprocess_hist_qc_bins} \ -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ -P nameDataNorm:st_adata_norm.h5ad diff --git a/nextflow.config b/nextflow.config index 65e401c..4f1b470 100644 --- a/nextflow.config +++ b/nextflow.config @@ -21,9 +21,6 @@ params { st_preprocess_min_counts = 500 st_preprocess_min_genes = 250 st_preprocess_min_cells = 1 - st_preprocess_hist_qc_max_total_counts = 10000 - st_preprocess_hist_qc_min_gene_counts = 4000 - st_preprocess_hist_qc_bins = 40 st_preprocess_n_hvgs = 2000 // Clustering diff --git a/nextflow_schema.json b/nextflow_schema.json index 953e707..23b89ea 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -113,24 +113,6 @@ "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_preprocess_hist_qc_max_total_counts": { - "type": "integer", - "default": 10000, - "description": "Max total counts cutoff for histogram QC plot.", - "fa_icon": "fas fa-hashtag" - }, - "st_preprocess_hist_qc_min_gene_counts": { - "type": "integer", - "default": 4000, - "description": "Min total gene counts cutoff for histogram QC plot.", - "fa_icon": "fas fa-hashtag" - }, - "st_preprocess_hist_qc_bins": { - "type": "integer", - "default": 40, - "description": "The number of bins for the QC histogram plots.", - "fa_icon": "fas fa-chart-simple" - }, "st_preprocess_n_hvgs": { "type": "integer", "default": 2000, From ae566cae7b072c79c768cf481b59d88573d2f894 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 10:06:29 +0100 Subject: [PATCH 284/410] Prettify filtering output --- bin/st_qc_and_normalisation.qmd | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index e7686eb..f07eb56 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -136,36 +136,40 @@ del st_adata.obs["in_tissue_str"] Number_spots = st_adata.shape[0] st_adata = st_adata[st_adata.obs["in_tissue"] == 1] Number_spots_in_tissue = st_adata.shape[0] -Markdown(f"""A total of {Number_spots_in_tissue} spots are situated inside the -tissue, out of {Number_spots} spots in total.""") +Markdown(f"""A total of `{Number_spots_in_tissue}` spots are situated inside the +tissue, out of `{Number_spots}` spots in total.""") ``` ## Cell and gene filtering -We filter cells based on minimum counts and genes, but also filter genes based -on minimum cells; exactly what filtering criteria is reasonable is up to you and +We filter spots based on minimum counts and genes, but also filter genes based +on minimum spots; exactly what filtering criteria is reasonable is up to you and your knowledge of the specific tissue at hand. ```{python} +#| warning: false # Filter cells based on counts Number_spots = st_adata.shape[0] Number_genes = st_adata.shape[1] sc.pp.filter_cells(st_adata, min_counts=minCounts) Number_spots_filtered_minCounts = st_adata.shape[0] -print (f"Removed {Number_spots - Number_spots_filtered_minCounts} spots with less than {minCounts} total counts.") # Filter cells based on genes sc.pp.filter_cells(st_adata, min_genes=minGenes) Number_spots_filtered_minGenes = st_adata.shape[0] -print (f"Removed {Number_spots_filtered_minCounts - Number_spots_filtered_minGenes} spots with less than {minGenes} genes expressed.") # Filter genes based on cells sc.pp.filter_genes(st_adata, min_cells=minCells) Number_genes_filtered_minCells = st_adata.shape[1] -print (f"Removed {Number_genes - Number_genes_filtered_minCells} genes expressed in less than {minCells} spots.") -print (f"{Number_spots_filtered_minGenes} out of {Number_spots} spots remaining after filtering.") -print (f"{Number_genes_filtered_minCells} out of {Number_genes} genes remaining after filtering.") +# Print results +Markdown(f""" +- Removed `{Number_spots - Number_spots_filtered_minCounts}` spots with less than `{minCounts}` total counts. +- Removed `{Number_spots_filtered_minCounts - Number_spots_filtered_minGenes}` spots with less than `{minGenes}` genes expressed. +- Removed `{Number_genes - Number_genes_filtered_minCells}` genes expressed in less than `{minCells}` spots. +- A total of `{Number_spots_filtered_minGenes}` spots out of `{Number_spots}` remain after filtering. +- A total of `{Number_genes_filtered_minCells}` genes out of `{Number_genes}` remain after filtering. +""") ``` ```{python} From 7b4b43c88d15ba405bb475e5c4785bf1e4d44f8c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 10:10:41 +0100 Subject: [PATCH 285/410] Change `cell` variable/params to `spot` Change `cell` variables and params to `spot`, as that is actually what is being done in the spatial data analysis. Previously the usage was mixed in the reports and parameters, and should now be unified to use `spot`. --- bin/st_qc_and_normalisation.qmd | 20 ++++++++++---------- docs/usage.md | 2 +- modules/local/st_qc_and_normalisation.nf | 2 +- nextflow.config | 2 +- nextflow_schema.json | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index f07eb56..5b8043f 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -33,7 +33,7 @@ saveFileST = None rawAdata = None # Name of the h5ad file minCounts = 500 # Min counts per spot minGenes = 250 # Min genes per spot -minCells = 1 # Min cells per gene +minSpots = 1 # Min spots per gene nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` @@ -140,7 +140,7 @@ Markdown(f"""A total of `{Number_spots_in_tissue}` spots are situated inside the tissue, out of `{Number_spots}` spots in total.""") ``` -## Cell and gene filtering +## Counts, genes and spots We filter spots based on minimum counts and genes, but also filter genes based on minimum spots; exactly what filtering criteria is reasonable is up to you and @@ -148,34 +148,34 @@ your knowledge of the specific tissue at hand. ```{python} #| warning: false -# Filter cells based on counts +# Filter spots based on counts Number_spots = st_adata.shape[0] Number_genes = st_adata.shape[1] sc.pp.filter_cells(st_adata, min_counts=minCounts) Number_spots_filtered_minCounts = st_adata.shape[0] -# Filter cells based on genes +# Filter spots based on genes sc.pp.filter_cells(st_adata, min_genes=minGenes) Number_spots_filtered_minGenes = st_adata.shape[0] -# Filter genes based on cells -sc.pp.filter_genes(st_adata, min_cells=minCells) -Number_genes_filtered_minCells = st_adata.shape[1] +# Filter genes based on spots +sc.pp.filter_genes(st_adata, min_cells=minSpots) +Number_genes_filtered_minSpots = st_adata.shape[1] # Print results Markdown(f""" - Removed `{Number_spots - Number_spots_filtered_minCounts}` spots with less than `{minCounts}` total counts. - Removed `{Number_spots_filtered_minCounts - Number_spots_filtered_minGenes}` spots with less than `{minGenes}` genes expressed. -- Removed `{Number_genes - Number_genes_filtered_minCells}` genes expressed in less than `{minCells}` spots. +- Removed `{Number_genes - Number_genes_filtered_minSpots}` genes expressed in less than `{minSpots}` spots. - A total of `{Number_spots_filtered_minGenes}` spots out of `{Number_spots}` remain after filtering. -- A total of `{Number_genes_filtered_minCells}` genes out of `{Number_genes}` remain after filtering. +- A total of `{Number_genes_filtered_minSpots}` genes out of `{Number_genes}` remain after filtering. """) ``` ```{python} #| echo: false # Restore non-filtered data if filtering results in 0 spots left -if (Number_genes_filtered_minCells == 0 or Number_spots_filtered_minGenes == 0): +if (Number_genes_filtered_minSpots == 0 or Number_spots_filtered_minGenes == 0): st_adata = st_adata_before_filtering display( Markdown(""" diff --git a/docs/usage.md b/docs/usage.md index 79fad0b..f893cc9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -140,7 +140,7 @@ The following parameters are exposed for preprocessing: - `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. - `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. -- `--st_preprocess_min_cells`: Minimum number of spots expressing a gene for the gene to be considered. +- `--st_preprocess_min_spots`: Minimum number of spots expressing a gene for the gene to be considered. - `--st_preprocess_n_hvgs`: Number of top highly variable genes to use for the analyses. ### Parameters for Clustering : diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index b907049..a478dbe 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -40,7 +40,7 @@ process ST_QC_AND_NORMALISATION { -P rawAdata:${st_raw} \ -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ - -P minCells:${params.st_preprocess_min_cells} \ + -P minCells:${params.st_preprocess_min_spots} \ -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ -P nameDataNorm:st_adata_norm.h5ad diff --git a/nextflow.config b/nextflow.config index 4f1b470..731fd9e 100644 --- a/nextflow.config +++ b/nextflow.config @@ -20,7 +20,7 @@ params { // Preprocessing, QC and normalisation st_preprocess_min_counts = 500 st_preprocess_min_genes = 250 - st_preprocess_min_cells = 1 + st_preprocess_min_spots = 1 st_preprocess_n_hvgs = 2000 // Clustering diff --git a/nextflow_schema.json b/nextflow_schema.json index 23b89ea..fac95c3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -107,7 +107,7 @@ "description": "The minimum number of expressed genes in a spot needed for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_preprocess_min_cells": { + "st_preprocess_min_spots": { "type": "integer", "default": 1, "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", From c2bfaad96675b21f941216629cf4f91d851a043c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 11:42:07 +0100 Subject: [PATCH 286/410] Move top expressed genes section to pre-filtering --- bin/st_qc_and_normalisation.qmd | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 5b8043f..731a790 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -117,6 +117,15 @@ sc.pl.scatter(st_adata, x='pct_counts_ribo', y='pct_counts_mt') sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') ``` +# Top expressed genes + +It can also be informative to see which genes are the most expressed in the +dataset; the following figure shows the top 20 most expressed genes. + +```{python} +sc.pl.highest_expr_genes(st_adata, n_top=20) +``` + # Filtering ## Non-tissue spots @@ -218,13 +227,6 @@ sc.pp.normalize_total(st_adata, inplace=True) sc.pp.log1p(st_adata) ``` -# Top expressed genes - -```{python} -# Plot top 20 most highly expressed genes -sc.pl.highest_expr_genes(st_adata, n_top=20) -``` - # Highly variable genes The top highly variable genes are detected for use in subsequent analyses. From bcfd236c3134d75983cb03c662c6b6c8dc13d8ec Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 12:47:18 +0100 Subject: [PATCH 287/410] Move normalisation and HVG to cluster report --- bin/st_clustering.qmd | 46 +++++++++++++++++++----- bin/st_qc_and_normalisation.qmd | 29 --------------- modules/local/st_clustering.nf | 1 + modules/local/st_qc_and_normalisation.nf | 1 - 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 41b490f..24b7ceb 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -13,14 +13,15 @@ jupyter: python3 #| echo: false fileNameST = None -resolution = 1 +resolution = 1 # Resolution for clustering +nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses saveFileST = None ``` -# Reading the data +# Input data -The data has already been filtered (see the `st_qc_and_normalisation` report) -and is saved in AnnData format. +The data has already been filtered in the _quality controls_ reports and is +saved in the AnnData format: ```{python} #| warning: false @@ -30,18 +31,45 @@ import pandas as pd from umap import UMAP from matplotlib import pyplot as plt import seaborn as sns - -sc.settings.verbosity = 0 -sc.set_figure_params(dpi_save=300, facecolor="white") +from IPython.display import display, Markdown ``` ```{python} st_adata = sc.read("./" + fileNameST) -print (f"Loading {fileNameST}:") st_adata ``` -# Manifold embedding and clustering +# Normalization + +Before we can continue working on the data it needs to be normalized. We here +use the built-in `normalize_total` method from [Scanpy](https://scanpy.readthedocs.io/en/stable/) +followed by a log-transformation. + +```{python} +sc.pp.normalize_total(st_adata, inplace=True) +sc.pp.log1p(st_adata) +``` + +# Feature selection + +Not all features (genes, in this case) are informative, and selecting for a +subset of the total features is commonly done prior to clustering. By selecting +the most variable genes in a dataset we can capture those most important in +regards to yielding a good separation of clusters. + +```{python} +# layout-nrow: 1 +# Find top HVGs and print results +sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=nHighlyVariableGenes) +var_genes_all = st_adata.var.highly_variable +print("Extracted highly variable genes: %d"%sum(var_genes_all)) + +# Plot the HVGs +plt.rcParams["figure.figsize"] = (4.5, 4.5) +sc.pl.highly_variable_genes(st_adata) +``` + +# Clustering To uncover the underlying structure of the transcriptional landscape, we perform manifold embedding and clustering based on transcriptional similarity. Principal diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_qc_and_normalisation.qmd index 731a790..e02522a 100644 --- a/bin/st_qc_and_normalisation.qmd +++ b/bin/st_qc_and_normalisation.qmd @@ -27,14 +27,12 @@ analysis tools and facilitates seamless integration into existing workflows. #| echo: false fileNameST = None -resolution = 1 saveFileST = None rawAdata = None # Name of the h5ad file minCounts = 500 # Min counts per spot minGenes = 250 # Min genes per spot minSpots = 1 # Min spots per gene -nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file ``` @@ -216,33 +214,6 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` -# Normalization - -We proceed to normalize Visium counts data using the built-in `normalize_total` -method from [Scanpy](https://scanpy.readthedocs.io/en/stable/) followed by a -log-transformation. - -```{python} -sc.pp.normalize_total(st_adata, inplace=True) -sc.pp.log1p(st_adata) -``` - -# Highly variable genes - -The top highly variable genes are detected for use in subsequent analyses. - -```{python} -# layout-nrow: 1 -# Find top HVGs and print results -sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=nHighlyVariableGenes) -var_genes_all = st_adata.var.highly_variable -print("Extracted highly variable genes: %d"%sum(var_genes_all)) - -# Plot the HVGs -plt.rcParams["figure.figsize"] = (4.5, 4.5) -sc.pl.highly_variable_genes(st_adata) -``` - ```{python} #| echo: false # Write normalized data to disk diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 22cc6a3..2d29642 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -39,6 +39,7 @@ process ST_CLUSTERING { --output "st_clustering.html" \ -P fileNameST:${st_adata_norm} \ -P resolution:${params.st_cluster_resolution} \ + -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ -P saveFileST:st_adata_processed.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_qc_and_normalisation.nf index a478dbe..a86bdf5 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_qc_and_normalisation.nf @@ -41,7 +41,6 @@ process ST_QC_AND_NORMALISATION { -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ -P minCells:${params.st_preprocess_min_spots} \ - -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ -P nameDataNorm:st_adata_norm.h5ad cat <<-END_VERSIONS > versions.yml From 8050b564e73680d6dca39508b616b1bccb653d8f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 12:47:45 +0100 Subject: [PATCH 288/410] Prettify and format clustering figures --- bin/st_clustering.qmd | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 24b7ceb..d6a6999 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -81,21 +81,39 @@ algorithm is employed for clustering with a given resolution. sc.pp.pca(st_adata) sc.pp.neighbors(st_adata) sc.tl.umap(st_adata) -print (f"Resolution for Leiden clustering: {resolution}") sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) +Markdown(f"Resolution for Leiden clustering: `{resolution}`") ``` -We then generate UMAP plots to visualize the spatial distribution of clusters -and investigate potential associations with total counts and detected genes. +## All clusters + +We then generate UMAP plots to visualize the distribution of clusters: + +```{python} +#| warning: false +plt.rcParams["figure.figsize"] = (7, 7) +sc.pl.umap(st_adata, color="clusters") +``` + +## Counts and genes + +We can also visualise the total counts and the genes with at least 1 count in +the UMAP: ```{python} # Make plots of UMAP of ST spots clusters -plt.rcParams["figure.figsize"] = (4, 4) -sc.pl.umap( - st_adata, color=["total_counts", "n_genes_by_counts", "clusters"], wspace=0.4 -) +plt.rcParams["figure.figsize"] = (3.5, 3.5) +sc.pl.umap(st_adata, color=["total_counts", "n_genes_by_counts"]) +``` + +## Individual clusters + +An additional visualisation is to show where the various spots are in each +individual cluster while ignoring all other cluster: + +```{python} sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") -sc.pl.embedding_density(st_adata, groupby="clusters", ncols=4) +sc.pl.embedding_density(st_adata, groupby="clusters", ncols=2) ``` # Visualization in spatial coordinates From 086b836561a59476b535d03a5cbeff85a7834e9d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 13:01:29 +0100 Subject: [PATCH 289/410] Format spatial visualisations in cluster report --- bin/st_clustering.qmd | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index d6a6999..3f29f1d 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -18,8 +18,6 @@ nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses saveFileST = None ``` -# Input data - The data has already been filtered in the _quality controls_ reports and is saved in the AnnData format: @@ -116,16 +114,16 @@ sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") sc.pl.embedding_density(st_adata, groupby="clusters", ncols=2) ``` -# Visualization in spatial coordinates +# Spatial visualisation Next, we examine how total counts and the number of detected genes behave in -spatial coordinates by overlaying the spots on the image itself. +spatial coordinates by overlaying the spots on the tissue image itself. ```{python} -plt.rcParams["figure.figsize"] = (10, 10) -sc.pl.spatial( - st_adata, img_key="hires", color=["total_counts", "n_genes_by_counts"] -) +#| layout-nrow: 2 +plt.rcParams["figure.figsize"] = (8, 8) +sc.pl.spatial(st_adata, img_key="hires", color="total_counts") +sc.pl.spatial(st_adata, img_key="hires", color="n_genes_by_counts") ``` To gain insights into tissue organization and potential inter-cellular @@ -135,10 +133,8 @@ spatial dimensions, providing valuable information about the spatial organization of cells. ```{python} -plt.rcParams["figure.figsize"] = (10, 10) -sc.pl.spatial( - st_adata, img_key="hires", color=["clusters"] -) +plt.rcParams["figure.figsize"] = (7, 7) +sc.pl.spatial(st_adata, img_key="hires", color="clusters") ``` ```{python} From 2dd37b46ffe7fa3e13e0fc5c03a9603602161882 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 13:09:58 +0100 Subject: [PATCH 290/410] Rename files and processes with new report content --- ...c_and_normalisation.qmd => st_quality_controls.qmd} | 0 conf/modules.config | 2 +- docs/output.md | 4 ++-- ..._qc_and_normalisation.nf => st_quality_controls.nf} | 8 ++++---- subworkflows/local/st_preprocess.nf | 10 +++++----- tests/pipeline/test_downstream.nf.test | 2 +- tests/pipeline/test_downstream.nf.test.snap | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 2 +- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 4 ++-- .../test_spaceranger_ffpe_v2_cytassist.nf.test | 2 +- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 ++-- 11 files changed, 21 insertions(+), 21 deletions(-) rename bin/{st_qc_and_normalisation.qmd => st_quality_controls.qmd} (100%) rename modules/local/{st_qc_and_normalisation.nf => st_quality_controls.nf} (83%) diff --git a/bin/st_qc_and_normalisation.qmd b/bin/st_quality_controls.qmd similarity index 100% rename from bin/st_qc_and_normalisation.qmd rename to bin/st_quality_controls.qmd diff --git a/conf/modules.config b/conf/modules.config index 6cffaed..30a52b1 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -68,7 +68,7 @@ process { ] } - withName: 'ST_READ_DATA|ST_QC_AND_NORMALISATION|ST_CLUSTERING|ST_SPATIAL_DE' { + withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SPATIAL_DE' { publishDir = [ [ path: { "${params.outdir}/${meta.id}/reports" }, diff --git a/docs/output.md b/docs/output.md index d5623ba..8461794 100644 --- a/docs/output.md +++ b/docs/output.md @@ -76,8 +76,8 @@ the data in an interactive way. Output files - `/reports/` - - `st_qc_and_normalisation.html`: HTML report. - - `st_qc_and_normalisation_files/`: Data needed for the HTML report. + - `st_quality_controls.html`: HTML report. + - `st_quality_controls_files/`: Data needed for the HTML report. diff --git a/modules/local/st_qc_and_normalisation.nf b/modules/local/st_quality_controls.nf similarity index 83% rename from modules/local/st_qc_and_normalisation.nf rename to modules/local/st_quality_controls.nf index a86bdf5..3c2da5d 100644 --- a/modules/local/st_qc_and_normalisation.nf +++ b/modules/local/st_quality_controls.nf @@ -1,7 +1,7 @@ // // Spatial data pre-processing // -process ST_QC_AND_NORMALISATION { +process ST_QUALITY_CONTROLS { // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 @@ -16,7 +16,7 @@ process ST_QC_AND_NORMALISATION { if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { architecture = System.getProperty("os.arch") if (architecture == "arm64" || architecture == "aarch64") { - exit 1, "The ST_QC_AND_NORMALISATION module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." + exit 1, "The ST_QUALITY_CONTROLS module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." } } @@ -27,7 +27,7 @@ process ST_QC_AND_NORMALISATION { output: tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm - tuple val(meta), path("st_qc_and_normalisation.html") , emit: html + tuple val(meta), path("st_quality_controls.html") , emit: html path("versions.yml") , emit: versions when: @@ -36,7 +36,7 @@ process ST_QC_AND_NORMALISATION { script: """ quarto render ${report} \ - --output st_qc_and_normalisation.html \ + --output st_quality_controls.html \ -P rawAdata:${st_raw} \ -P minCounts:${params.st_preprocess_min_counts} \ -P minGenes:${params.st_preprocess_min_genes} \ diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index 499c144..a99026e 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -2,7 +2,7 @@ // Pre-processing of ST data // -include { ST_QC_AND_NORMALISATION } from '../../modules/local/st_qc_and_normalisation' +include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' workflow ST_PREPROCESS { @@ -16,21 +16,21 @@ workflow ST_PREPROCESS { // // Report files // - report = file("${projectDir}/bin/st_qc_and_normalisation.qmd") + report = file("${projectDir}/bin/st_quality_controls.qmd") report_template = Channel.fromPath("${projectDir}/assets/_extensions") // // Spatial pre-processing // - ST_QC_AND_NORMALISATION ( + ST_QUALITY_CONTROLS ( report, report_template, st_raw ) - ch_versions = ch_versions.mix(ST_QC_AND_NORMALISATION.out.versions) + ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) emit: - st_data_norm = ST_QC_AND_NORMALISATION.out.st_data_norm // channel: [ val(sample), h5ad ] + st_data_norm = ST_QUALITY_CONTROLS.out.st_data_norm // channel: [ val(sample), h5ad ] versions = ch_versions // channel: [ version.yml ] } diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 3f9ad4f..9abef85 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -28,7 +28,7 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 45bd56e..75f12d8 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2024-01-15T16:00:03.485826" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 66b3562..b826c67 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -26,7 +26,7 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 8be642f..441c711 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2024-01-15T13:44:40.789425" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 738a70b..0b10d90 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -22,7 +22,7 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_qc_and_normalisation.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 25954a9..1dad690 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,8 +1,8 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QC_AND_NORMALISATION={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "timestamp": "2024-01-15T15:42:52.651007" } -} \ No newline at end of file +} From fb92ccb37500eefc8d5e18972b80f2990101bb25 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 13:12:28 +0100 Subject: [PATCH 291/410] Add nf-core TODO-string --- bin/st_clustering.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 3f29f1d..18b82b3 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -133,6 +133,7 @@ spatial dimensions, providing valuable information about the spatial organization of cells. ```{python} +# TODO nf-core: Can the colour bar on this figure be fit to the figure? plt.rcParams["figure.figsize"] = (7, 7) sc.pl.spatial(st_adata, img_key="hires", color="clusters") ``` From b9c8ed9bdc2332e3d545988309a5f32831c6eedc Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 13:47:01 +0100 Subject: [PATCH 292/410] Increase spatial plot point size --- bin/st_clustering.qmd | 6 +++--- bin/st_quality_controls.qmd | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 18b82b3..7745d46 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -122,8 +122,8 @@ spatial coordinates by overlaying the spots on the tissue image itself. ```{python} #| layout-nrow: 2 plt.rcParams["figure.figsize"] = (8, 8) -sc.pl.spatial(st_adata, img_key="hires", color="total_counts") -sc.pl.spatial(st_adata, img_key="hires", color="n_genes_by_counts") +sc.pl.spatial(st_adata, img_key="hires", color="total_counts", size=1.25) +sc.pl.spatial(st_adata, img_key="hires", color="n_genes_by_counts", size=1.25) ``` To gain insights into tissue organization and potential inter-cellular @@ -135,7 +135,7 @@ organization of cells. ```{python} # TODO nf-core: Can the colour bar on this figure be fit to the figure? plt.rcParams["figure.figsize"] = (7, 7) -sc.pl.spatial(st_adata, img_key="hires", color="clusters") +sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ``` ```{python} diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index e02522a..881f3f0 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -99,8 +99,8 @@ spatial patterns may be discerned: ```{python} #| layout-nrow: 2 -sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"]) -sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"]) +sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"], size=1.25) +sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"], size=1.25) ``` ## Scatter plots @@ -136,7 +136,7 @@ are uninformative and are thus removed. st_adata.obs["in_tissue_str"] = ["In tissue" if x == 1 else "Outside tissue" for x in st_adata.obs["in_tissue"]] # Plot spots inside tissue -sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue") +sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue", size=1.25) del st_adata.obs["in_tissue_str"] # Remove spots outside tissue and print results From c12b455ed7d4216d0b107f77065f918fcff263c2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 13:53:52 +0100 Subject: [PATCH 293/410] Do not print number of FASTQ files found for SR --- subworkflows/local/input_check.nf | 2 -- 1 file changed, 2 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index 8d9630d..baff0e8 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -107,8 +107,6 @@ def create_channel_spaceranger(LinkedHashMap meta) { if(!fastq_files.size()) { error "No `fastq_dir` specified or no samples found in folder." - } else { - log.info "${fastq_files.size()} FASTQ files found for sample ${meta['id']}." } check_optional_files = ["manual_alignment", "slidefile", "image", "cytaimage", "colorizedimage", "darkimage"] From 5a13c5e6231d8bc78b230f8e7ad14b9c54a8ae8b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 19 Jan 2024 14:02:19 +0100 Subject: [PATCH 294/410] Move scanpy issue solution to DE report --- bin/st_clustering.qmd | 6 ------ bin/st_spatial_de.qmd | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 7745d46..345c105 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -138,12 +138,6 @@ plt.rcParams["figure.figsize"] = (7, 7) sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ``` -```{python} -#| echo: false -# Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 -st_adata.uns['log1p']['base'] = None -``` - ```{python} #| echo: false if saveFileST is not None: diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index e60f8a6..3a6a628 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -24,8 +24,12 @@ from matplotlib import pyplot as plt ``` ```{python} +# Read data st_adata = sc.read(fileNameST) st_adata + +# Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 +st_adata.uns['log1p']['base'] = None ``` # Differential gene expression @@ -36,7 +40,6 @@ in the dataset. ```{python} #| warning: false plt.rcParams["figure.figsize"] = (5, 5) -st_adata.uns['log1p']['base'] = None sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") ``` From e44e975d2bbedd2e38ba4da1a32b63782d84e1e5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:01:36 +0100 Subject: [PATCH 295/410] Add info and callout of DEG figures --- bin/st_spatial_de.qmd | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 3a6a628..1011a9d 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -34,16 +34,30 @@ st_adata.uns['log1p']['base'] = None # Differential gene expression -We first find differentially expressed genes across the different clusters found -in the dataset. +Before we look for spatially variable genes we first find differentially +expressed genes across the different clusters found in the data. We can +visualize the top DEGs in a heatmap: ```{python} #| warning: false -plt.rcParams["figure.figsize"] = (5, 5) sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') -sc.pl.rank_genes_groups(st_adata, n_genes=25, sharey=False, gene_symbols="gene_symbol") +sc.pl.rank_genes_groups_heatmap(st_adata, n_genes=5, groupby="clusters") ``` +A different but similar visualization of the DEGs is the dot plot, where we can +also include the gene names: + +```{python} +#| warning: false +sc.pl.rank_genes_groups_dotplot(st_adata, n_genes=5, groupby="clusters") +``` + +::: {.callout-note} +Please note that you may need to scroll sidewise in these figures, as their +height and width depends on the number of clusters as well as the number and +intersection of the DEGs that are being plotted. +::: + # Spatially variable genes Spatial transcriptomics allows researchers to investigate how gene expression From 3f388bc20a6800b5010d927a5bf90777a0463d0b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:02:08 +0100 Subject: [PATCH 296/410] Suppress scanpy warnings; fix spelling errors --- bin/st_spatial_de.qmd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 1011a9d..378eb54 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -30,6 +30,9 @@ st_adata # Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 st_adata.uns['log1p']['base'] = None + +# Suppress scanpy-specific warnings +sc.settings.verbosity = 0 ``` # Differential gene expression @@ -68,13 +71,13 @@ For this purpose, we use SpatialDE statistical framework that aims to identify spatially variable genes. First, we convert normalized counts and coordinates to pandas dataframe, needed -for inputs to spatialDE. +for inputs to SpatialDE. ```{python} results = SpatialDE.run(st_adata.obsm["spatial"], st_adata.to_df()) ``` -We concatenate the results with the DataFrame of annotations of variables: +We concatenate the results with the dataframe of annotations of variables: `st_adata.var`. We can then inspect significant genes that varies in space and visualize them with `sc.pl.spatial` function. From 278c1fefe0a92322c3b7b97eb993c7ad373f1287 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:25:07 +0100 Subject: [PATCH 297/410] Fix spatial DEG figure; remove spatial ncol param --- bin/st_spatial_de.qmd | 30 ++++++++++++++---------------- docs/usage.md | 1 - modules/local/st_spatial_de.nf | 1 - nextflow.config | 1 - nextflow_schema.json | 7 ------- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 378eb54..4784fd4 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -10,7 +10,6 @@ jupyter: python3 #| tags: [parameters] #| echo: false fileNameST = "" -numberOfColumns = 5 saveDEFileName = "" saveSpatialDEFileName = "" plotTopHVG = 15 @@ -61,41 +60,40 @@ height and width depends on the number of clusters as well as the number and intersection of the DEGs that are being plotted. ::: -# Spatially variable genes +# Spatial gene expression -Spatial transcriptomics allows researchers to investigate how gene expression -trends varies in space, thus identifying spatial patterns of gene expression. -For this purpose, we use SpatialDE -[Svensson18](https://www.nature.com/articles/nmeth.4636) -([code](https://github.com/Teichlab/SpatialDE)), a Gaussian process-based -statistical framework that aims to identify spatially variable genes. - -First, we convert normalized counts and coordinates to pandas dataframe, needed -for inputs to SpatialDE. +Spatial transcriptomics data can give insight into how genes are expressed in +different areas in a tissue, allowing identification of spatial gene expression +patterns. Here we use [SpatialDE](https://www.nature.com/articles/nmeth.4636) to +identify such patterns. ```{python} +#| output: false results = SpatialDE.run(st_adata.obsm["spatial"], st_adata.to_df()) ``` -We concatenate the results with the dataframe of annotations of variables: -`st_adata.var`. We can then inspect significant genes that varies in space and -visualize them with `sc.pl.spatial` function. - +We can then inspect the spatial DEGs in a table: ```{python} results.set_index("g", inplace=True) # workaround for https://github.com/Teichlab/SpatialDE/issues/36 results = results.loc[~results.index.duplicated(keep="first")] +# Add annotations st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, :]], axis=1) +# Print results table results_tab = st_adata.var.sort_values("qval", ascending=True) results_tab.to_csv(saveSpatialDEFileName) results_tab.head(plotTopHVG) ``` +We can also plot the top spatially variable genes on top of the tissue image +itself to visualize the patterns: + ```{python} symbols = results_tab.iloc[: plotTopHVG]["gene_symbol"] +plt.rcParams["figure.figsize"] = (3.5, 4) sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, - ncols=numberOfColumns, title=symbols) + ncols=2, title=symbols, size=1.25) ``` diff --git a/docs/usage.md b/docs/usage.md index f893cc9..c47425a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -149,7 +149,6 @@ The following parameters are exposed for preprocessing: ### Parameters for Spatial Differential Expression : -- `st_spatial_de_ncols`: Number of columns in the output figure. ## Running the pipeline diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 40197db..14f0c9f 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -37,7 +37,6 @@ process ST_SPATIAL_DE { quarto render ${report} \ --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ - -P numberOfColumns:${params.st_spatial_de_ncols} \ -P plotTopHVG:${params.st_spatial_de_top_hvg} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv diff --git a/nextflow.config b/nextflow.config index 731fd9e..c41c44d 100644 --- a/nextflow.config +++ b/nextflow.config @@ -28,7 +28,6 @@ params { // Spatial differential expression st_spatial_de_top_hvg = 15 - st_spatial_de_ncols = 5 // MultiQC options multiqc_config = null diff --git a/nextflow_schema.json b/nextflow_schema.json index fac95c3..5e0e89e 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -131,13 +131,6 @@ "default": 15, "description": "The number of top spatially highly variable genes to plot.", "fa_icon": "fas fa-hashtag" - }, - "st_spatial_de_ncols": { - "type": "integer", - "default": 5, - "description": "Number of columns to group gene plots into.", - "help_text": "The default, 5, will plot the top spatially highly variable genes into groups of 5 plots per row. This, in combinationation with the default number of top HVGs to plot (15) will yield three rows with 5 plots each.", - "fa_icon": "fas fa-hashtag" } } }, From c9ed0b708ba29d9a6f6d32d3a8fbf3377be12f8f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:29:49 +0100 Subject: [PATCH 298/410] Rename top N spatial DEGs parameter --- bin/st_spatial_de.qmd | 6 +++--- modules/local/st_spatial_de.nf | 2 +- nextflow.config | 2 +- nextflow_schema.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 4784fd4..a98401d 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -12,7 +12,7 @@ jupyter: python3 fileNameST = "" saveDEFileName = "" saveSpatialDEFileName = "" -plotTopHVG = 15 +nTopSpatialDEGs = 14 ``` ```{python} @@ -85,14 +85,14 @@ st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, : # Print results table results_tab = st_adata.var.sort_values("qval", ascending=True) results_tab.to_csv(saveSpatialDEFileName) -results_tab.head(plotTopHVG) +results_tab.head(nTopSpatialDEGs) ``` We can also plot the top spatially variable genes on top of the tissue image itself to visualize the patterns: ```{python} -symbols = results_tab.iloc[: plotTopHVG]["gene_symbol"] +symbols = results_tab.iloc[: nTopSpatialDEGs]["gene_symbol"] plt.rcParams["figure.figsize"] = (3.5, 4) sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, ncols=2, title=symbols, size=1.25) diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 14f0c9f..5c08cdc 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -37,7 +37,7 @@ process ST_SPATIAL_DE { quarto render ${report} \ --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ - -P plotTopHVG:${params.st_spatial_de_top_hvg} \ + -P nTopSpatialDEGs:${params.st_n_top_spatial_degs} \ -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv diff --git a/nextflow.config b/nextflow.config index c41c44d..896271e 100644 --- a/nextflow.config +++ b/nextflow.config @@ -27,7 +27,7 @@ params { st_cluster_resolution = 1 // Spatial differential expression - st_spatial_de_top_hvg = 15 + st_n_top_spatial_degs = 14 // MultiQC options multiqc_config = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 5e0e89e..b7f59ba 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -126,9 +126,9 @@ "help_text": "The resolution controls the coarseness of the clustering, where a higher resolution leads to more clusters.", "fa_icon": "fas fa-circle-nodes" }, - "st_spatial_de_top_hvg": { + "st_n_top_spatial_degs": { "type": "integer", - "default": 15, + "default": 14, "description": "The number of top spatially highly variable genes to plot.", "fa_icon": "fas fa-hashtag" } From 200891ff9336effa447d65d2d0075399f66b1fd1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:42:24 +0100 Subject: [PATCH 299/410] Remove unused report parameters --- bin/st_quality_controls.qmd | 3 --- bin/st_spatial_de.qmd | 1 - modules/local/st_spatial_de.nf | 1 - 3 files changed, 5 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 881f3f0..0dca2d4 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -26,9 +26,6 @@ analysis tools and facilitates seamless integration into existing workflows. #| tags: [parameters] #| echo: false -fileNameST = None -saveFileST = None - rawAdata = None # Name of the h5ad file minCounts = 500 # Min counts per spot minGenes = 250 # Min genes per spot diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index a98401d..e7f4b78 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -10,7 +10,6 @@ jupyter: python3 #| tags: [parameters] #| echo: false fileNameST = "" -saveDEFileName = "" saveSpatialDEFileName = "" nTopSpatialDEGs = 14 ``` diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 5c08cdc..fac36b5 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -38,7 +38,6 @@ process ST_SPATIAL_DE { --output "st_spatial_de.html" \ -P fileNameST:${st_adata_norm} \ -P nTopSpatialDEGs:${params.st_n_top_spatial_degs} \ - -P saveDEFileName:st_gde.csv \ -P saveSpatialDEFileName:st_spatial_de.csv cat <<-END_VERSIONS > versions.yml From 31dbc4fa7c05406ffaa3ae38b3b4e6defd0a2fa2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 10:32:50 +0100 Subject: [PATCH 300/410] Minor formatting; remove TODOs --- bin/st_clustering.qmd | 2 +- bin/st_quality_controls.qmd | 4 ++-- docs/usage.md | 1 - modules/local/st_clustering.nf | 3 +-- modules/local/st_quality_controls.nf | 9 ++++----- modules/local/st_spatial_de.nf | 7 +++---- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 345c105..b0c6be0 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -133,7 +133,7 @@ spatial dimensions, providing valuable information about the spatial organization of cells. ```{python} -# TODO nf-core: Can the colour bar on this figure be fit to the figure? +# TODO: Can the colour bar on this figure be fit to the figure? plt.rcParams["figure.figsize"] = (7, 7) sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 0dca2d4..43fbbbf 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -65,8 +65,8 @@ percentage of counts from mitochondrial, ribosomal and haemoglobin genes ## Violin plots -The following violin plots show the distribution of the number of -genes per counts and counts per spot, as well as the percentage of counts from +The following violin plots show the distribution of the number of genes per +counts and counts per spot, as well as the percentage of counts from mitochondrial, ribosomal and haemoglobin genes: ```{python} diff --git a/docs/usage.md b/docs/usage.md index c47425a..0ff5da7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -149,7 +149,6 @@ The following parameters are exposed for preprocessing: ### Parameters for Spatial Differential Expression : - ## Running the pipeline The typical command for running the pipeline is as follows: diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 2d29642..98cf21c 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -1,9 +1,8 @@ // -// Clustering etc. +// Dimensionality reduction and clustering // process ST_CLUSTERING { - // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 3c2da5d..1e1c89e 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -1,9 +1,8 @@ // -// Spatial data pre-processing +// Quality controls and filtering // process ST_QUALITY_CONTROLS { - // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" @@ -26,9 +25,9 @@ process ST_QUALITY_CONTROLS { tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") output: - tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm - tuple val(meta), path("st_quality_controls.html") , emit: html - path("versions.yml") , emit: versions + tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm + tuple val(meta), path("st_quality_controls.html"), emit: html + path("versions.yml") , emit: versions when: task.ext.when == null || task.ext.when diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index fac36b5..a9b7412 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -3,7 +3,6 @@ // process ST_SPATIAL_DE { - // TODO: Add a better description // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" @@ -25,9 +24,9 @@ process ST_SPATIAL_DE { tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") output: - tuple val(meta), path("*.csv") , emit: degs - tuple val(meta), path("st_spatial_de.html") , emit: html - path("versions.yml") , emit: versions + tuple val(meta), path("*.csv") , emit: degs + tuple val(meta), path("st_spatial_de.html"), emit: html + path("versions.yml") , emit: versions when: task.ext.when == null || task.ext.when From ff3e181845cd9e0e2f8bca13485b90472482df06 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 11:03:04 +0100 Subject: [PATCH 301/410] Rename Python variables to adhere to PEP8 --- bin/st_clustering.qmd | 20 +++++------ bin/st_quality_controls.qmd | 50 ++++++++++++++-------------- bin/st_spatial_de.qmd | 14 ++++---- modules/local/st_clustering.nf | 8 ++--- modules/local/st_quality_controls.nf | 10 +++--- modules/local/st_spatial_de.nf | 9 +++-- 6 files changed, 55 insertions(+), 56 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index b0c6be0..61cae89 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -12,10 +12,10 @@ jupyter: python3 #| tags: [parameters] #| echo: false -fileNameST = None -resolution = 1 # Resolution for clustering -nHighlyVariableGenes = 2000 # Number of HVGs to use for analyses -saveFileST = None +input_adata = None +cluster_resolution = 1 # Resolution for Leiden clustering +n_hvgs = 2000 # Number of HVGs to use for analyses +output_anndata = None ``` The data has already been filtered in the _quality controls_ reports and is @@ -33,7 +33,7 @@ from IPython.display import display, Markdown ``` ```{python} -st_adata = sc.read("./" + fileNameST) +st_adata = sc.read("./" + input_adata) st_adata ``` @@ -58,7 +58,7 @@ regards to yielding a good separation of clusters. ```{python} # layout-nrow: 1 # Find top HVGs and print results -sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=nHighlyVariableGenes) +sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=n_hvgs) var_genes_all = st_adata.var.highly_variable print("Extracted highly variable genes: %d"%sum(var_genes_all)) @@ -79,8 +79,8 @@ algorithm is employed for clustering with a given resolution. sc.pp.pca(st_adata) sc.pp.neighbors(st_adata) sc.tl.umap(st_adata) -sc.tl.leiden(st_adata, key_added="clusters", resolution=resolution) -Markdown(f"Resolution for Leiden clustering: `{resolution}`") +sc.tl.leiden(st_adata, key_added="clusters", resolution=cluster_resolution) +Markdown(f"Resolution for Leiden clustering: `{cluster_resolution}`") ``` ## All clusters @@ -140,6 +140,6 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false -if saveFileST is not None: - st_adata.write(saveFileST) +if output_anndata is not None: + st_adata.write(output_anndata) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 43fbbbf..54ffd6d 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -26,11 +26,11 @@ analysis tools and facilitates seamless integration into existing workflows. #| tags: [parameters] #| echo: false -rawAdata = None # Name of the h5ad file -minCounts = 500 # Min counts per spot -minGenes = 250 # Min genes per spot -minSpots = 1 # Min spots per gene -nameDataNorm = "st_adata_norm.h5ad" # Name of the normalized data save file +input_adata = None # Name of the h5ad file +min_counts = 500 # Min counts per spot +min_genes = 250 # Min genes per spot +min_spots = 1 # Min spots per gene +output_adata = "st_adata_norm.h5ad" # Name of the normalized data save file ``` ```{python} @@ -45,7 +45,7 @@ plt.rcParams["figure.figsize"] = (6, 6) ```{python} # Read the data -st_adata = sc.read("./" + rawAdata) +st_adata = sc.read("./" + input_adata) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) @@ -137,11 +137,11 @@ sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue", size=1 del st_adata.obs["in_tissue_str"] # Remove spots outside tissue and print results -Number_spots = st_adata.shape[0] +n_spots = st_adata.shape[0] st_adata = st_adata[st_adata.obs["in_tissue"] == 1] -Number_spots_in_tissue = st_adata.shape[0] -Markdown(f"""A total of `{Number_spots_in_tissue}` spots are situated inside the -tissue, out of `{Number_spots}` spots in total.""") +n_spots_in_tissue = st_adata.shape[0] +Markdown(f"""A total of `{n_spots_in_tissue}` spots are situated inside the +tissue, out of `{n_spots}` spots in total.""") ``` ## Counts, genes and spots @@ -153,33 +153,33 @@ your knowledge of the specific tissue at hand. ```{python} #| warning: false # Filter spots based on counts -Number_spots = st_adata.shape[0] -Number_genes = st_adata.shape[1] -sc.pp.filter_cells(st_adata, min_counts=minCounts) -Number_spots_filtered_minCounts = st_adata.shape[0] +n_spots = st_adata.shape[0] +n_genes = st_adata.shape[1] +sc.pp.filter_cells(st_adata, min_counts=min_counts) +n_spots_filtered_min_counts = st_adata.shape[0] # Filter spots based on genes -sc.pp.filter_cells(st_adata, min_genes=minGenes) -Number_spots_filtered_minGenes = st_adata.shape[0] +sc.pp.filter_cells(st_adata, min_genes=min_genes) +n_spots_filtered_min_genes = st_adata.shape[0] # Filter genes based on spots -sc.pp.filter_genes(st_adata, min_cells=minSpots) -Number_genes_filtered_minSpots = st_adata.shape[1] +sc.pp.filter_genes(st_adata, min_cells=min_spots) +n_genes_filtered_min_spots = st_adata.shape[1] # Print results Markdown(f""" -- Removed `{Number_spots - Number_spots_filtered_minCounts}` spots with less than `{minCounts}` total counts. -- Removed `{Number_spots_filtered_minCounts - Number_spots_filtered_minGenes}` spots with less than `{minGenes}` genes expressed. -- Removed `{Number_genes - Number_genes_filtered_minSpots}` genes expressed in less than `{minSpots}` spots. -- A total of `{Number_spots_filtered_minGenes}` spots out of `{Number_spots}` remain after filtering. -- A total of `{Number_genes_filtered_minSpots}` genes out of `{Number_genes}` remain after filtering. +- Removed `{n_spots - n_spots_filtered_min_counts}` spots with less than `{min_counts}` total counts. +- Removed `{n_spots - n_spots_filtered_min_genes}` spots with less than `{min_genes}` genes expressed. +- Removed `{n_genes - n_genes_filtered_min_spots}` genes expressed in less than `{min_spots}` spots. +- A total of `{n_spots_filtered_min_genes}` spots out of `{n_spots}` remain after filtering. +- A total of `{n_genes_filtered_min_spots}` genes out of `{n_genes}` remain after filtering. """) ``` ```{python} #| echo: false # Restore non-filtered data if filtering results in 0 spots left -if (Number_genes_filtered_minSpots == 0 or Number_spots_filtered_minGenes == 0): +if (n_genes_filtered_min_spots == 0 or n_spots_filtered_min_genes == 0): st_adata = st_adata_before_filtering display( Markdown(""" @@ -214,5 +214,5 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ```{python} #| echo: false # Write normalized data to disk -st_adata.write(nameDataNorm) +st_adata.write(output_adata) ``` diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index e7f4b78..e0e4e77 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -9,9 +9,9 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -fileNameST = "" -saveSpatialDEFileName = "" -nTopSpatialDEGs = 14 +input_adata_processed = "st_adata_processed.h5ad" +output_spatial_degs = "st_spatial_de.csv" +n_top_spatial_degs = 14 ``` ```{python} @@ -23,7 +23,7 @@ from matplotlib import pyplot as plt ```{python} # Read data -st_adata = sc.read(fileNameST) +st_adata = sc.read(input_adata_processed) st_adata # Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 @@ -83,15 +83,15 @@ st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, : # Print results table results_tab = st_adata.var.sort_values("qval", ascending=True) -results_tab.to_csv(saveSpatialDEFileName) -results_tab.head(nTopSpatialDEGs) +results_tab.to_csv(output_spatial_degs) +results_tab.head(n_top_spatial_degs) ``` We can also plot the top spatially variable genes on top of the tissue image itself to visualize the patterns: ```{python} -symbols = results_tab.iloc[: nTopSpatialDEGs]["gene_symbol"] +symbols = results_tab.iloc[: n_top_spatial_degs]["gene_symbol"] plt.rcParams["figure.figsize"] = (3.5, 4) sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, ncols=2, title=symbols, size=1.25) diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 98cf21c..d4d50eb 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -36,10 +36,10 @@ process ST_CLUSTERING { """ quarto render ${report} \ --output "st_clustering.html" \ - -P fileNameST:${st_adata_norm} \ - -P resolution:${params.st_cluster_resolution} \ - -P nHighlyVariableGenes:${params.st_preprocess_n_hvgs} \ - -P saveFileST:st_adata_processed.h5ad + -P input_adata:${st_adata_norm} \ + -P cluster_resolution:${params.st_cluster_resolution} \ + -P n_hvgs:${params.st_preprocess_n_hvgs} \ + -P output_anndata:st_adata_processed.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 1e1c89e..38aca93 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -36,11 +36,11 @@ process ST_QUALITY_CONTROLS { """ quarto render ${report} \ --output st_quality_controls.html \ - -P rawAdata:${st_raw} \ - -P minCounts:${params.st_preprocess_min_counts} \ - -P minGenes:${params.st_preprocess_min_genes} \ - -P minCells:${params.st_preprocess_min_spots} \ - -P nameDataNorm:st_adata_norm.h5ad + -P input_adata:${st_raw} \ + -P min_counts:${params.st_preprocess_min_counts} \ + -P min_genes:${params.st_preprocess_min_genes} \ + -P min_spots:${params.st_preprocess_min_spots} \ + -P output_adata:st_adata_norm.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index a9b7412..249fda1 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -21,7 +21,7 @@ process ST_SPATIAL_DE { input: path(report) path(report_template) - tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") + tuple val(meta), path(st_adata_processed) output: tuple val(meta), path("*.csv") , emit: degs @@ -34,10 +34,9 @@ process ST_SPATIAL_DE { script: """ quarto render ${report} \ - --output "st_spatial_de.html" \ - -P fileNameST:${st_adata_norm} \ - -P nTopSpatialDEGs:${params.st_n_top_spatial_degs} \ - -P saveSpatialDEFileName:st_spatial_de.csv + -P input_adata_processed:${st_adata_processed} \ + -P n_top_spatial_degs:${params.st_n_top_spatial_degs} \ + -P output_spatial_degs:st_spatial_de.csv cat <<-END_VERSIONS > versions.yml "${task.process}": From 7e6cdc877d58885ac3320caf72b4fa33e42faf3a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 15:16:41 +0100 Subject: [PATCH 302/410] Harmonise input/output AnnData file/variable names --- bin/st_clustering.qmd | 9 ++++----- bin/st_quality_controls.qmd | 11 +++++------ modules/local/st_clustering.nf | 7 +++---- modules/local/st_quality_controls.nf | 9 ++++----- modules/local/st_read_data.nf | 2 +- subworkflows/local/st_postprocess.nf | 14 +++++++++----- subworkflows/local/st_preprocess.nf | 8 ++++---- workflows/spatialtranscriptomics.nf | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 61cae89..4fef792 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -12,10 +12,10 @@ jupyter: python3 #| tags: [parameters] #| echo: false -input_adata = None +input_adata_filtered = "st_adata_filtered.h5ad" # Name of the input anndata file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses -output_anndata = None +output_adata_processed = "st_adata_processed.h5ad" # Name of the output anndata file ``` The data has already been filtered in the _quality controls_ reports and is @@ -33,7 +33,7 @@ from IPython.display import display, Markdown ``` ```{python} -st_adata = sc.read("./" + input_adata) +st_adata = sc.read("./" + input_adata_filtered) st_adata ``` @@ -140,6 +140,5 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false -if output_anndata is not None: - st_adata.write(output_anndata) +st_adata.write(output_adata_processed) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 54ffd6d..75df8c0 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -25,12 +25,11 @@ analysis tools and facilitates seamless integration into existing workflows. ```{python} #| tags: [parameters] #| echo: false - -input_adata = None # Name of the h5ad file +input_adata_raw = "st_adata_raw.h5ad" # Name of the input anndata file min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene -output_adata = "st_adata_norm.h5ad" # Name of the normalized data save file +output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file ``` ```{python} @@ -45,7 +44,7 @@ plt.rcParams["figure.figsize"] = (6, 6) ```{python} # Read the data -st_adata = sc.read("./" + input_adata) +st_adata = sc.read("./" + input_adata_raw) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) @@ -213,6 +212,6 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ```{python} #| echo: false -# Write normalized data to disk -st_adata.write(output_adata) +# Write filtered data to disc +st_adata.write(output_adata_filtered) ``` diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index d4d50eb..b3c0974 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -22,7 +22,7 @@ process ST_CLUSTERING { input: path(report) path(report_template) - tuple val(meta), path(st_adata_norm, stageAs: "adata_norm.h5ad") + tuple val(meta), path(st_adata_filtered) output: tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed @@ -35,11 +35,10 @@ process ST_CLUSTERING { script: """ quarto render ${report} \ - --output "st_clustering.html" \ - -P input_adata:${st_adata_norm} \ + -P input_adata_filtered:${st_adata_filtered} \ -P cluster_resolution:${params.st_cluster_resolution} \ -P n_hvgs:${params.st_preprocess_n_hvgs} \ - -P output_anndata:st_adata_processed.h5ad + -P output_adata_processed:st_adata_processed.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 38aca93..e85135e 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -22,10 +22,10 @@ process ST_QUALITY_CONTROLS { input: path(report) path(report_template) - tuple val(meta), path(st_raw, stageAs: "adata_raw.h5ad") + tuple val(meta), path(st_adata_raw) output: - tuple val(meta), path("st_adata_norm.h5ad") , emit: st_data_norm + tuple val(meta), path("st_adata_filtered.h5ad") , emit: st_adata_filtered tuple val(meta), path("st_quality_controls.html"), emit: html path("versions.yml") , emit: versions @@ -35,12 +35,11 @@ process ST_QUALITY_CONTROLS { script: """ quarto render ${report} \ - --output st_quality_controls.html \ - -P input_adata:${st_raw} \ + -P input_adata_raw:${st_adata_raw} \ -P min_counts:${params.st_preprocess_min_counts} \ -P min_genes:${params.st_preprocess_min_genes} \ -P min_spots:${params.st_preprocess_min_spots} \ - -P output_adata:st_adata_norm.h5ad + -P output_adata_filtered:st_adata_filtered.h5ad cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index d482cc5..a39c9c3 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -15,7 +15,7 @@ process ST_READ_DATA { tuple val (meta), path("${meta.id}/*") output: - tuple val(meta), path("st_adata_raw.h5ad"), emit: st_raw + tuple val(meta), path("st_adata_raw.h5ad"), emit: st_adata_raw path("versions.yml") , emit: versions when: diff --git a/subworkflows/local/st_postprocess.nf b/subworkflows/local/st_postprocess.nf index 7ad4475..7843610 100644 --- a/subworkflows/local/st_postprocess.nf +++ b/subworkflows/local/st_postprocess.nf @@ -8,7 +8,7 @@ include { ST_CLUSTERING } from '../../modules/local/st_clustering' workflow ST_POSTPROCESS { take: - st_adata_norm + st_adata_filtered main: @@ -22,12 +22,12 @@ workflow ST_POSTPROCESS { report_template = Channel.fromPath("${projectDir}/assets/_extensions") // - // Clustering + // Normalisation, dimensionality reduction and clustering // ST_CLUSTERING ( report_clustering, report_template, - st_adata_norm + st_adata_filtered ) ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) @@ -42,7 +42,11 @@ workflow ST_POSTPROCESS { ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) emit: - spatial_degs = ST_SPATIAL_DE.out.degs // channel: [ val(sample), csv ] + st_adata_processed = ST_CLUSTERING.out.st_adata_processed // channel: [ meta, h5ad] + html = ST_CLUSTERING.out.html // channel: [ html ] - versions = ch_versions // channel: [ versions.yml ] + degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] + html = ST_SPATIAL_DE.out.html // channel: [ html ] + + versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf index a99026e..b762f4f 100644 --- a/subworkflows/local/st_preprocess.nf +++ b/subworkflows/local/st_preprocess.nf @@ -7,7 +7,7 @@ include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' workflow ST_PREPROCESS { take: - st_raw + st_adata_raw main: @@ -25,12 +25,12 @@ workflow ST_PREPROCESS { ST_QUALITY_CONTROLS ( report, report_template, - st_raw + st_adata_raw ) ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) emit: - st_data_norm = ST_QUALITY_CONTROLS.out.st_data_norm // channel: [ val(sample), h5ad ] + st_data_norm = ST_QUALITY_CONTROLS.out.st_adata_filtered // channel: [ meta, h5ad ] - versions = ch_versions // channel: [ version.yml ] + versions = ch_versions // channel: [ version.yml ] } diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 76757da..90f5a7d 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -125,7 +125,7 @@ workflow ST { // SUBWORKFLOW: Pre-processing of ST data // ST_PREPROCESS ( - ST_READ_DATA.out.st_raw + ST_READ_DATA.out.st_adata_raw ) ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) From 14cbc2ed43b358136095a067e269f76eb8616870 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 15:50:14 +0100 Subject: [PATCH 303/410] Merge downstream subworkflows into one Merge the downstream spatial subworkflows (previously known as `pre-` and `post-processing`) into one called just `downstream`, as there was no reason to keep them separate when they shared a common structure of input, outputs and Quarto template. --- subworkflows/local/st_downstream.nf | 67 ++++++++++++++++++++++++++++ subworkflows/local/st_postprocess.nf | 52 --------------------- subworkflows/local/st_preprocess.nf | 36 --------------- workflows/spatialtranscriptomics.nf | 17 ++----- 4 files changed, 71 insertions(+), 101 deletions(-) create mode 100644 subworkflows/local/st_downstream.nf delete mode 100644 subworkflows/local/st_postprocess.nf delete mode 100644 subworkflows/local/st_preprocess.nf diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf new file mode 100644 index 0000000..1852712 --- /dev/null +++ b/subworkflows/local/st_downstream.nf @@ -0,0 +1,67 @@ +// +// Subworkflow for downstream analyses of ST data +// + +include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' +include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' +include { ST_CLUSTERING } from '../../modules/local/st_clustering' + +workflow ST_DOWNSTREAM { + + take: + st_adata_raw + + main: + + ch_versions = Channel.empty() + + // + // Report files + // + report_quality_controls = file("${projectDir}/bin/st_quality_controls.qmd") + report_clustering = file("${projectDir}/bin/st_clustering.qmd") + report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") + report_template = Channel.fromPath("${projectDir}/assets/_extensions") + + // + // Quality controls and filtering + // + ST_QUALITY_CONTROLS ( + report_quality_controls, + report_template, + st_adata_raw + ) + ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) + + // + // Normalisation, dimensionality reduction and clustering + // + ST_CLUSTERING ( + report_clustering, + report_template, + ST_QUALITY_CONTROLS.out.st_adata_filtered + ) + ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) + + // + // Spatial differential expression + // + ST_SPATIAL_DE ( + report_spatial_de, + report_template, + ST_CLUSTERING.out.st_adata_processed + ) + ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) + + emit: + st_data_norm = ST_QUALITY_CONTROLS.out.st_adata_filtered // channel: [ meta, h5ad ] + html = ST_QUALITY_CONTROLS.out.html // channel: [ html ] + + st_adata_processed = ST_CLUSTERING.out.st_adata_processed // channel: [ meta, h5ad] + html = ST_CLUSTERING.out.html // channel: [ html ] + + degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] + html = ST_SPATIAL_DE.out.html // channel: [ html ] + + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/local/st_postprocess.nf b/subworkflows/local/st_postprocess.nf deleted file mode 100644 index 7843610..0000000 --- a/subworkflows/local/st_postprocess.nf +++ /dev/null @@ -1,52 +0,0 @@ -// -// Spatial differential expression and reporting -// - -include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' -include { ST_CLUSTERING } from '../../modules/local/st_clustering' - -workflow ST_POSTPROCESS { - - take: - st_adata_filtered - - main: - - ch_versions = Channel.empty() - - // - // Report files - // - report_clustering = file("${projectDir}/bin/st_clustering.qmd") - report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") - report_template = Channel.fromPath("${projectDir}/assets/_extensions") - - // - // Normalisation, dimensionality reduction and clustering - // - ST_CLUSTERING ( - report_clustering, - report_template, - st_adata_filtered - ) - ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) - - // - // Spatial differential expression - // - ST_SPATIAL_DE ( - report_spatial_de, - report_template, - ST_CLUSTERING.out.st_adata_processed - ) - ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) - - emit: - st_adata_processed = ST_CLUSTERING.out.st_adata_processed // channel: [ meta, h5ad] - html = ST_CLUSTERING.out.html // channel: [ html ] - - degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] - html = ST_SPATIAL_DE.out.html // channel: [ html ] - - versions = ch_versions // channel: [ versions.yml ] -} diff --git a/subworkflows/local/st_preprocess.nf b/subworkflows/local/st_preprocess.nf deleted file mode 100644 index b762f4f..0000000 --- a/subworkflows/local/st_preprocess.nf +++ /dev/null @@ -1,36 +0,0 @@ -// -// Pre-processing of ST data -// - -include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' - -workflow ST_PREPROCESS { - - take: - st_adata_raw - - main: - - ch_versions = Channel.empty() - - // - // Report files - // - report = file("${projectDir}/bin/st_quality_controls.qmd") - report_template = Channel.fromPath("${projectDir}/assets/_extensions") - - // - // Spatial pre-processing - // - ST_QUALITY_CONTROLS ( - report, - report_template, - st_adata_raw - ) - ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) - - emit: - st_data_norm = ST_QUALITY_CONTROLS.out.st_adata_filtered // channel: [ meta, h5ad ] - - versions = ch_versions // channel: [ version.yml ] -} diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 90f5a7d..c3c34f9 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -58,8 +58,7 @@ include { ST_READ_DATA } from '../modules/local/st_read_data' // include { INPUT_CHECK } from '../subworkflows/local/input_check' include { SPACERANGER } from '../subworkflows/local/spaceranger' -include { ST_PREPROCESS } from '../subworkflows/local/st_preprocess' -include { ST_POSTPROCESS } from '../subworkflows/local/st_postprocess' +include { ST_DOWNSTREAM } from '../subworkflows/local/st_downstream' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -122,20 +121,12 @@ workflow ST { ch_versions = ch_versions.mix(ST_READ_DATA.out.versions) // - // SUBWORKFLOW: Pre-processing of ST data + // SUBWORKFLOW: Downstream analyses of ST data // - ST_PREPROCESS ( + ST_DOWNSTREAM ( ST_READ_DATA.out.st_adata_raw ) - ch_versions = ch_versions.mix(ST_PREPROCESS.out.versions) - - // - // SUBWORKFLOW: Post-processing and reporting - // - ST_POSTPROCESS ( - ST_PREPROCESS.out.st_data_norm - ) - ch_versions = ch_versions.mix(ST_POSTPROCESS.out.versions) + ch_versions = ch_versions.mix(ST_DOWNSTREAM.out.versions) // // MODULE: Pipeline reporting From d3ed894d79fe7dd1ff17bb196e5d135ce24e2f4e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 15:57:38 +0100 Subject: [PATCH 304/410] Remove old directory listing from documentation --- docs/output.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/output.md b/docs/output.md index 8461794..56f439b 100644 --- a/docs/output.md +++ b/docs/output.md @@ -70,14 +70,13 @@ the data in an interactive way. ## Reports -### Quality controls and normalisation +### Quality controls and filtering
    Output files - `/reports/` - `st_quality_controls.html`: HTML report. - - `st_quality_controls_files/`: Data needed for the HTML report.
    @@ -93,7 +92,6 @@ you can find more details in the report itself. - `/reports/` - `st_clustering.html`: HTML report. - - `st_clustering_files/`: Data needed for the HTML report. @@ -108,7 +106,6 @@ option; you can find more details in the report itself. - `/reports/` - `st_spatial_de.html`: HTML report. - - `st_spatial_de_files/`: Data needed for the HTML report. - `/degs/` - `st_spatial_de.csv`: List of spatially varying genes. From dfba65b02663e065841d93e9cc560fdaf75948e6 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 16:20:53 +0100 Subject: [PATCH 305/410] Minor formatting; remove TODO --- docs/output.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/docs/output.md b/docs/output.md index 56f439b..23a88d8 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,10 +1,5 @@ # nf-core/spatialtranscriptomics: Output - - - - - ## Introduction This document describes the output produced by the pipeline. Most of the output @@ -80,10 +75,9 @@ the data in an interactive way. -Report containing analyses related to quality controls, filtering and -normalisation of spatial data. Spots are filtered based on total counts, -number of expressed genes, mitochondrial content as well as presence in tissue; -you can find more details in the report itself. +Report containing analyses related to quality controls and filtering of spatial +data. Spots are filtered based on total counts, number of expressed genes as +well as presence in tissue; you can find more details in the report itself. ### Clustering @@ -95,8 +89,8 @@ you can find more details in the report itself. -Report containing analyses related to dimensionality reduction, clustering and -spatial visualisation. Leiden clustering is currently the only clustering +Report containing analyses related to normalisation, dimensionality reduction, +clustering and spatial visualisation. Leiden clustering is currently the only option; you can find more details in the report itself. ### Differential expression @@ -107,7 +101,7 @@ option; you can find more details in the report itself. - `/reports/` - `st_spatial_de.html`: HTML report. - `/degs/` - - `st_spatial_de.csv`: List of spatially varying genes. + - `st_spatial_de.csv`: List of spatially differentially expressed genes. From 909ebdf30b61f9c890f3848f3ead4ce69a0e8d78 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 16:33:48 +0100 Subject: [PATCH 306/410] Update schema variable doc --- nextflow_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index b7f59ba..2fe9cb0 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -129,7 +129,7 @@ "st_n_top_spatial_degs": { "type": "integer", "default": 14, - "description": "The number of top spatially highly variable genes to plot.", + "description": "The number of top spatial differentially expressed genes to plot.", "fa_icon": "fas fa-hashtag" } } From ffd10c57c192f25bb0a073c5d6312688b8314cb0 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 16:34:12 +0100 Subject: [PATCH 307/410] Remove redundant `analysis` usage doc section --- docs/usage.md | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 0ff5da7..cdfc0ab 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -114,7 +114,7 @@ gene expression matrices as well as spatial information. ## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with -raw spatial data. Space Ranger requieres a lot of memory +raw spatial data. Space Ranger requires a lot of memory (64 GB) and several threads (8) to be able to run. You can find the Space Ranger documentation at the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). @@ -125,30 +125,11 @@ path to its directory (or another link from the 10X website above) using the default human reference for you automatically. > [!NOTE] -> For FFPE and Cytassist experiments, you need to manually supply the appropriate probset using the `--spaceranger_probeset` parameter -> Please refer to the [Spaceranger Downloads page](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) +> For FFPE and Cytassist experiments, you need to manually supply the +> appropriate probeset using the `--spaceranger_probeset` parameter Please refer +> to the [Space Ranger Downloads page](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) > to obtain the correct probeset. -## Analysis options - -The pipeline uses Python and the `scverse` tools to do the downstream analysis -(quality control, filtering, clustering, spatial differential equations). - -### Parameters for Quality Control and Filtering: - -The following parameters are exposed for preprocessing: - -- `--st_preprocess_min_counts`: Minimum number of counts for a spot to be considered in the analysis. -- `--st_preprocess_min_genes`: Minimum number of genes expressed in a spot for the spot to be considered. -- `--st_preprocess_min_spots`: Minimum number of spots expressing a gene for the gene to be considered. -- `--st_preprocess_n_hvgs`: Number of top highly variable genes to use for the analyses. - -### Parameters for Clustering : - -- `--st_cluster_resolution`: Resolution parameter for the clustering algorithm, controlling granularity. - -### Parameters for Spatial Differential Expression : - ## Running the pipeline The typical command for running the pipeline is as follows: From 48ca093181d73ba9d6773afea4c140bce97dfdee Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 22 Jan 2024 16:37:44 +0100 Subject: [PATCH 308/410] Update FastQC module --- modules.json | 2 +- modules/nf-core/fastqc/tests/main.nf.test | 90 +++++++++---------- .../nf-core/fastqc/tests/main.nf.test.snap | 4 +- 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/modules.json b/modules.json index 07454f7..f29c0dc 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "fastqc": { "branch": "master", - "git_sha": "617777a807a1770f73deb38c80004bac06807eef", + "git_sha": "c9488585ce7bd35ccd2a30faa2371454c8112fb9", "installed_by": ["modules"] }, "multiqc": { diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index ad9bc54..1f21c66 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -13,12 +13,10 @@ nextflow_process { when { process { """ - input[0] = [ - [ id: 'test', single_end:true ], - [ - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) - ] - ] + input[0] = Channel.of([ + [ id: 'test', single_end:true ], + [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) ] + ]) """ } } @@ -44,15 +42,13 @@ nextflow_process { when { process { - """ - input[0] = [ - [id: 'test', single_end: false], // meta map - [ - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), - file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true) - ] - ] - """ + """ + 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) ] + ]) + """ } } @@ -76,11 +72,11 @@ nextflow_process { when { process { - """ - input[0] = [ - [id: 'test', single_end: false], // meta map - file(params.test_data['sarscov2']['illumina']['test_interleaved_fastq_gz'], checkIfExists: true) - ] + """ + 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) + ]) """ } } @@ -102,12 +98,12 @@ nextflow_process { when { process { - """ - input[0] = [ - [id: 'test', single_end: false], // meta map - file(params.test_data['sarscov2']['illumina']['test_paired_end_sorted_bam'], checkIfExists: true) - ] - """ + """ + 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) + ]) + """ } } @@ -128,17 +124,15 @@ nextflow_process { when { process { - """ - input[0] = [ - [id: 'test', single_end: false], // meta map - [ - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true), - file(params.test_data['sarscov2']['illumina']['test_2_fastq_gz'], checkIfExists: true), - file(params.test_data['sarscov2']['illumina']['test2_1_fastq_gz'], checkIfExists: true), - file(params.test_data['sarscov2']['illumina']['test2_2_fastq_gz'], checkIfExists: true) - ] - ] - """ + """ + 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) ] + ]) + """ } } @@ -168,12 +162,12 @@ nextflow_process { when { process { - """ - input[0] = [ - [ id:'mysample', single_end:true ], // meta map - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) - ] - """ + """ + 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) + ]) + """ } } @@ -197,12 +191,10 @@ nextflow_process { when { process { """ - input[0] = [ - [ id: 'test', single_end:true ], - [ - file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz'], checkIfExists: true) - ] - ] + input[0] = Channel.of([ + [ id: 'test', single_end:true ], + [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) ] + ]) """ } } diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap index 5ef5afb..5d624bb 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -7,7 +7,7 @@ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], - "timestamp": "2023-12-29T02:48:05.126117287" + "timestamp": "2024-01-17T18:40:57.254299" }, "versions": { "content": [ @@ -15,6 +15,6 @@ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], - "timestamp": "2023-12-29T02:46:49.507942667" + "timestamp": "2024-01-17T18:36:50.033627" } } \ No newline at end of file From 5457f29b97df805de1fb7ed096689fc566edef95 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 23 Jan 2024 09:07:01 +0100 Subject: [PATCH 309/410] Change Quarto template files into a value channel --- subworkflows/local/st_downstream.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 1852712..91b5e74 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -21,7 +21,7 @@ workflow ST_DOWNSTREAM { report_quality_controls = file("${projectDir}/bin/st_quality_controls.qmd") report_clustering = file("${projectDir}/bin/st_clustering.qmd") report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") - report_template = Channel.fromPath("${projectDir}/assets/_extensions") + report_template = Channel.fromPath("${projectDir}/assets/_extensions").collect() // // Quality controls and filtering From 4416fcb8244fef97197cf36a3ff2408eeddb8713 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 23 Jan 2024 09:28:09 +0100 Subject: [PATCH 310/410] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af81800..ee3eb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ compatible with further downstream analyses and/or exploration in _e.g._ ### `Added` +- Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialtranscriptomics/pull/64)] - Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) - Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] - Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] From 4af9545c940d5ea1c04256bfb13ec266059bda83 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 23 Jan 2024 11:46:24 +0100 Subject: [PATCH 311/410] Update tests --- tests/pipeline/test_downstream.nf.test | 9 +++++++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 4 ++-- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 9abef85..317e662 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -26,14 +26,19 @@ nextflow_pipeline { // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_spatial_de.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index b826c67..a46b7f5 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -26,9 +26,9 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, // DEGs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 0b10d90..a43487e 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -22,9 +22,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("top highly variable genes are detected") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("significant genes that varies in space") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, From d3cd6c29c4eeac9808778636aad4c479d5d3bbda Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 23 Jan 2024 12:15:56 +0100 Subject: [PATCH 312/410] Prettier fixes --- .devcontainer/devcontainer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4ecfbfe..4a9bc5c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,11 +18,11 @@ "python.linting.flake8Path": "/opt/conda/bin/flake8", "python.linting.pycodestylePath": "/opt/conda/bin/pycodestyle", "python.linting.pydocstylePath": "/opt/conda/bin/pydocstyle", - "python.linting.pylintPath": "/opt/conda/bin/pylint" + "python.linting.pylintPath": "/opt/conda/bin/pylint", }, // Add the IDs of extensions you want installed when the container is created. - "extensions": ["ms-python.python", "ms-python.vscode-pylance", "nf-core.nf-core-extensionpack"] - } - } + "extensions": ["ms-python.python", "ms-python.vscode-pylance", "nf-core.nf-core-extensionpack"], + }, + }, } From d94db35bd9843fa85c3c77c62170afe72bab82a5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 08:21:43 +0100 Subject: [PATCH 313/410] Properly print all AnnData objects in reports --- bin/st_clustering.qmd | 3 ++- bin/st_quality_controls.qmd | 4 ++-- bin/st_spatial_de.qmd | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index 4fef792..c37fbbe 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -34,7 +34,8 @@ from IPython.display import display, Markdown ```{python} st_adata = sc.read("./" + input_adata_filtered) -st_adata +print("Content of the AnnData object:") +print(st_adata) ``` # Normalization diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 75df8c0..086971e 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -50,8 +50,8 @@ st_adata = sc.read("./" + input_adata_raw) st_adata.X = scipy.sparse.csc_matrix(st_adata.X) # Print the anndata object for inspection -print ("Content of the anndata object:") -st_adata +print("Content of the AnnData object:") +print(st_adata) ``` # Quality controls diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index e0e4e77..82d8e81 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -24,7 +24,8 @@ from matplotlib import pyplot as plt ```{python} # Read data st_adata = sc.read(input_adata_processed) -st_adata +print("Content of the AnnData object:") +print(st_adata) # Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 st_adata.uns['log1p']['base'] = None From 04cee1664942fbf0061001c94873495f35c926b3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 08:22:02 +0100 Subject: [PATCH 314/410] Store the raw data in the AnnData object --- bin/st_quality_controls.qmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 086971e..99a8a58 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -49,6 +49,9 @@ st_adata = sc.read("./" + input_adata_raw) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) +# Store the raw data so that it can be used for analyses from scratch if desired +st_adata.layers['raw'] = st_adata.X.copy() + # Print the anndata object for inspection print("Content of the AnnData object:") print(st_adata) From c719bea5bacceec434afece92034a3449d77be70 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 15:54:57 +0100 Subject: [PATCH 315/410] Fix header levels --- bin/st_quality_controls.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 99a8a58..7824ff5 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -6,7 +6,7 @@ format: jupyter: python3 --- -## Introduction +# Introduction Spatial Transcriptomics data analysis involves several steps, including quality controls (QC) and pre-processing, to ensure the reliability of downstream @@ -114,7 +114,7 @@ sc.pl.scatter(st_adata, x='pct_counts_ribo', y='pct_counts_mt') sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') ``` -# Top expressed genes +## Top expressed genes It can also be informative to see which genes are the most expressed in the dataset; the following figure shows the top 20 most expressed genes. From 8a5607d68b5f1ffba7d8e4ce7941c5e8a02616e3 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 15:55:07 +0100 Subject: [PATCH 316/410] Clarify filtering fallback if clause --- bin/st_quality_controls.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 7824ff5..0fb72b3 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -181,7 +181,7 @@ Markdown(f""" ```{python} #| echo: false # Restore non-filtered data if filtering results in 0 spots left -if (n_genes_filtered_min_spots == 0 or n_spots_filtered_min_genes == 0): +if (st_adata.shape[0] == 0 or st_adata.shape[1] == 0): st_adata = st_adata_before_filtering display( Markdown(""" From 87cbb971de0349baad7fdf487a205b886aceb1b2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 16:01:09 +0100 Subject: [PATCH 317/410] Move quality controls code chunk --- bin/st_quality_controls.qmd | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 0fb72b3..0b9c01f 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -65,15 +65,7 @@ spatial data. Common metrics include the number of genes with a least 1 count percentage of counts from mitochondrial, ribosomal and haemoglobin genes (`pct_counts_[mt/ribo/hb]`). -## Violin plots - -The following violin plots show the distribution of the number of genes per -counts and counts per spot, as well as the percentage of counts from -mitochondrial, ribosomal and haemoglobin genes: - ```{python} -#| layout-nrow: 2 - # Calculate mitochondrial, ribosomal and haemoglobin percentages st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') st_adata.var['ribo'] = st_adata.var_names.str.contains(("^RP[LS]")) @@ -83,8 +75,16 @@ sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "ribo", "hb"], # Save a copy of data as a restore-point if filtering results in 0 spots left st_adata_before_filtering = st_adata.copy() +``` -# Plot QC metrics +## Violin plots + +The following violin plots show the distribution of the number of genes per +counts and counts per spot, as well as the percentage of counts from +mitochondrial, ribosomal and haemoglobin genes: + +```{python} +#| layout-nrow: 2 sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], @@ -202,6 +202,8 @@ spots during filtering. ) ``` +## Violin plots + Following the filtering we can look at the same violin plots as above to see the new distributions: From cf03cab993795fe5fafcdecbccfef706e83ae6bd Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 16:37:14 +0100 Subject: [PATCH 318/410] Add mitochondrial content filtering --- bin/st_quality_controls.qmd | 17 +++++++++++++++++ modules/local/st_quality_controls.nf | 1 + nextflow.config | 1 + nextflow_schema.json | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 0b9c01f..9aa093a 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -29,6 +29,7 @@ input_adata_raw = "st_adata_raw.h5ad" # Name of the input anndata file min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene +mito_threshold = 20 # Mitochondrial content threshold (%) output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file ``` @@ -178,6 +179,22 @@ Markdown(f""" """) ``` +## Mito, ribo and Hb + +We can also filter for mitochondrial, ribosomal and haemoglobin content of the +cells: + +```{python} +# Filter spots +st_adata = st_adata[st_adata.obs["pct_counts_mt"] < mito_threshold] +n_spots_filtered_mito = st_adata.shape[0] + +# Print results +Markdown(f""" +- Removed `{n_spots - n_spots_filtered_mito}` spots with less than `{mito_threshold}%` mitochondrial content. +""") +``` + ```{python} #| echo: false # Restore non-filtered data if filtering results in 0 spots left diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index e85135e..8670112 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -39,6 +39,7 @@ process ST_QUALITY_CONTROLS { -P min_counts:${params.st_preprocess_min_counts} \ -P min_genes:${params.st_preprocess_min_genes} \ -P min_spots:${params.st_preprocess_min_spots} \ + -P mito_threshold:${params.st_qc_mito_threshold} \ -P output_adata_filtered:st_adata_filtered.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/nextflow.config b/nextflow.config index 896271e..566ef0a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -21,6 +21,7 @@ params { st_preprocess_min_counts = 500 st_preprocess_min_genes = 250 st_preprocess_min_spots = 1 + st_qc_mito_threshold = 20 st_preprocess_n_hvgs = 2000 // Clustering diff --git a/nextflow_schema.json b/nextflow_schema.json index 2fe9cb0..dea6d2b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -113,6 +113,12 @@ "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", "fa_icon": "fas fa-hashtag" }, + "st_qc_mito_threshold": { + "type": "integer", + "default": 20, + "description": "The maximum proportion of mitochondrial content that a spot is allowed to have to pass the filtering.", + "fa_icon": "fas fa-hashtag" + }, "st_preprocess_n_hvgs": { "type": "integer", "default": 2000, From 965b643d33cb6583253958daba10216573d1c4e2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 24 Jan 2024 16:43:46 +0100 Subject: [PATCH 319/410] Update parameter names for clarity --- conf/test.config | 4 ++-- conf/test_downstream.config | 4 ++-- conf/test_spaceranger_v1.config | 4 ++-- modules/local/st_clustering.nf | 2 +- modules/local/st_quality_controls.nf | 6 +++--- nextflow.config | 16 ++++++++-------- nextflow_schema.json | 8 ++++---- tests/pipeline/test_downstream.nf.test | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/conf/test.config b/conf/test.config index b707385..ce4909c 100644 --- a/conf/test.config +++ b/conf/test.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_downstream.config b/conf/test_downstream.config index 971f16b..89a56b2 100644 --- a/conf/test_downstream.config +++ b/conf/test_downstream.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index 67cf02a..2fca10e 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index b3c0974..0819c7a 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -37,7 +37,7 @@ process ST_CLUSTERING { quarto render ${report} \ -P input_adata_filtered:${st_adata_filtered} \ -P cluster_resolution:${params.st_cluster_resolution} \ - -P n_hvgs:${params.st_preprocess_n_hvgs} \ + -P n_hvgs:${params.st_cluster_n_hvgs} \ -P output_adata_processed:st_adata_processed.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 8670112..469c8ad 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -36,9 +36,9 @@ process ST_QUALITY_CONTROLS { """ quarto render ${report} \ -P input_adata_raw:${st_adata_raw} \ - -P min_counts:${params.st_preprocess_min_counts} \ - -P min_genes:${params.st_preprocess_min_genes} \ - -P min_spots:${params.st_preprocess_min_spots} \ + -P min_counts:${params.st_qc_min_counts} \ + -P min_genes:${params.st_qc_min_genes} \ + -P min_spots:${params.st_qc_min_spots} \ -P mito_threshold:${params.st_qc_mito_threshold} \ -P output_adata_filtered:st_adata_filtered.h5ad diff --git a/nextflow.config b/nextflow.config index 566ef0a..36d0927 100644 --- a/nextflow.config +++ b/nextflow.config @@ -17,18 +17,18 @@ params { spaceranger_probeset = null spaceranger_save_reference = false - // Preprocessing, QC and normalisation - st_preprocess_min_counts = 500 - st_preprocess_min_genes = 250 - st_preprocess_min_spots = 1 - st_qc_mito_threshold = 20 - st_preprocess_n_hvgs = 2000 + // Quality controls and filtering + st_qc_min_counts = 500 + st_qc_min_genes = 250 + st_qc_min_spots = 1 + st_qc_mito_threshold = 20 // Clustering - st_cluster_resolution = 1 + st_cluster_n_hvgs = 2000 + st_cluster_resolution = 1 // Spatial differential expression - st_n_top_spatial_degs = 14 + st_n_top_spatial_degs = 14 // MultiQC options multiqc_config = null diff --git a/nextflow_schema.json b/nextflow_schema.json index dea6d2b..5caba3a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -95,19 +95,19 @@ "fa_icon": "fas fa-magnifying-glass-chart", "description": "Options related to the downstream analyses performed by the pipeline.", "properties": { - "st_preprocess_min_counts": { + "st_qc_min_counts": { "type": "integer", "default": 500, "description": "The minimum number of UMIs needed in a spot for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_preprocess_min_genes": { + "st_qc_min_genes": { "type": "integer", "default": 250, "description": "The minimum number of expressed genes in a spot needed for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_preprocess_min_spots": { + "st_qc_min_spots": { "type": "integer", "default": 1, "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", @@ -119,7 +119,7 @@ "description": "The maximum proportion of mitochondrial content that a spot is allowed to have to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_preprocess_n_hvgs": { + "st_cluster_n_hvgs": { "type": "integer", "default": 2000, "description": "The number of top highly variable genes to use for the analyses.", diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 317e662..53b401c 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -12,8 +12,8 @@ nextflow_pipeline { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = "$outputDir" } } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index a46b7f5..6cf1f4b 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -9,8 +9,8 @@ nextflow_pipeline { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" - st_preprocess_min_counts = 5 - st_preprocess_min_genes = 3 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = "$outputDir" } } From 482cab256b01e3bb375c8ac2ca5b88a0b6e07ebd Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 10:09:16 +0100 Subject: [PATCH 320/410] Add ribosomal filtering --- bin/st_quality_controls.qmd | 9 +++++++-- modules/local/st_quality_controls.nf | 1 + nextflow.config | 1 + nextflow_schema.json | 7 +++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 9aa093a..0f0569c 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -30,6 +30,7 @@ min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene mito_threshold = 20 # Mitochondrial content threshold (%) +ribo_threshold = 0 # Ribosomal content threshold (%) output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file ``` @@ -182,16 +183,20 @@ Markdown(f""" ## Mito, ribo and Hb We can also filter for mitochondrial, ribosomal and haemoglobin content of the -cells: +cells; exactly which filtering threshold should be used are, again, up to you +and your biological knowledge of the sample at hand. ```{python} # Filter spots st_adata = st_adata[st_adata.obs["pct_counts_mt"] < mito_threshold] n_spots_filtered_mito = st_adata.shape[0] +st_adata = st_adata[st_adata.obs["pct_counts_ribo"] >= ribo_threshold] +n_spots_filtered_ribo = st_adata.shape[0] # Print results Markdown(f""" -- Removed `{n_spots - n_spots_filtered_mito}` spots with less than `{mito_threshold}%` mitochondrial content. +- Removed `{n_spots - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. +- Removed `{n_spots - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. """) ``` diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 469c8ad..9094491 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -40,6 +40,7 @@ process ST_QUALITY_CONTROLS { -P min_genes:${params.st_qc_min_genes} \ -P min_spots:${params.st_qc_min_spots} \ -P mito_threshold:${params.st_qc_mito_threshold} \ + -P ribo_threshold:${params.st_qc_ribo_threshold} \ -P output_adata_filtered:st_adata_filtered.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/nextflow.config b/nextflow.config index 36d0927..f4c7d72 100644 --- a/nextflow.config +++ b/nextflow.config @@ -22,6 +22,7 @@ params { st_qc_min_genes = 250 st_qc_min_spots = 1 st_qc_mito_threshold = 20 + st_qc_ribo_threshold = 0 // Clustering st_cluster_n_hvgs = 2000 diff --git a/nextflow_schema.json b/nextflow_schema.json index 5caba3a..dd2e8d9 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -117,6 +117,13 @@ "type": "integer", "default": 20, "description": "The maximum proportion of mitochondrial content that a spot is allowed to have to pass the filtering.", + "help_text": "If you do not wish to filter based on mitochondrial content, set this parameter to `100`.", + "fa_icon": "fas fa-hashtag" + }, + "st_qc_ribo_threshold": { + "type": "integer", + "default": 0, + "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by defeault).", "fa_icon": "fas fa-hashtag" }, "st_cluster_n_hvgs": { From 007e17e6605c86d8ef936ce2d43e5a87c4b9c321 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 10:36:47 +0100 Subject: [PATCH 321/410] Clarify filtering fallback callout --- bin/st_quality_controls.qmd | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 0f0569c..59be8e2 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -41,6 +41,7 @@ import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from IPython.display import display, Markdown +from textwrap import dedent plt.rcParams["figure.figsize"] = (6, 6) ``` @@ -206,21 +207,24 @@ Markdown(f""" if (st_adata.shape[0] == 0 or st_adata.shape[1] == 0): st_adata = st_adata_before_filtering display( - Markdown(""" -::: {.callout-important .content-visible when-format="html"} -## Issue: no spots remain after filtering - -An anomaly has been detected in the data: following the filtering process, -all spots have been excluded. It is imperative to assess the data quality -and carefully review the Analysis Options outlined in `docs/usage.md`. - -To ensure the smooth progression of downstream analysis, the exported -AnnData will, for the time being, remain unfiltered. This precautionary -measure is implemented to facilitate continued analysis while -investigating and resolving the cause of the unexpected removal of all -spots during filtering. -:::""" - ) + Markdown(dedent( + """ + ::: {.callout-important .content-visible when-format="html"} + ## Issue: no spots remain after filtering + + An anomaly has been detected in the data: following the filtering + process, all spots have been excluded. It is imperative to assess + the data quality and carefully review the values of the filtering + parameters. + + To ensure the smooth progression of downstream analysis, the + exported AnnData will, for the time being, remain unfiltered. This + precautionary measure is implemented to facilitate continued + analysis while investigating and resolving the cause of the + unexpected removal of all spots during filtering. + ::: + """ + )) ) ``` From ef0e69e7eba10c82c24b0a98efda8b3fdaca0e30 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 10:52:16 +0100 Subject: [PATCH 322/410] Fix counting of spots during filtering --- bin/st_quality_controls.qmd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 59be8e2..b223d5e 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -174,10 +174,8 @@ n_genes_filtered_min_spots = st_adata.shape[1] # Print results Markdown(f""" - Removed `{n_spots - n_spots_filtered_min_counts}` spots with less than `{min_counts}` total counts. -- Removed `{n_spots - n_spots_filtered_min_genes}` spots with less than `{min_genes}` genes expressed. +- Removed `{n_spots_filtered_min_counts - n_spots_filtered_min_genes}` spots with less than `{min_genes}` genes expressed. - Removed `{n_genes - n_genes_filtered_min_spots}` genes expressed in less than `{min_spots}` spots. -- A total of `{n_spots_filtered_min_genes}` spots out of `{n_spots}` remain after filtering. -- A total of `{n_genes_filtered_min_spots}` genes out of `{n_genes}` remain after filtering. """) ``` @@ -196,8 +194,10 @@ n_spots_filtered_ribo = st_adata.shape[0] # Print results Markdown(f""" -- Removed `{n_spots - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. -- Removed `{n_spots - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. +- Removed `{st_adata.shape[0] - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. +- Removed `{n_spots_filtered_mito - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. +- A total of `{n_spots_filtered_min_genes}` spots out of `{n_spots}` remain after filtering. +- A total of `{n_genes_filtered_min_spots}` genes out of `{n_genes}` remain after filtering. """) ``` From 7faa00ae31add9fac362d33c0fd6aba7f8efa43f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 11:16:13 +0100 Subject: [PATCH 323/410] Add haemoglobin filtering --- bin/st_quality_controls.qmd | 9 +++++++-- modules/local/st_quality_controls.nf | 1 + nextflow.config | 1 + nextflow_schema.json | 6 ++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index b223d5e..44a0aac 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -31,6 +31,7 @@ min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) +hb_threshold = 100 # content threshold (%) output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file ``` @@ -183,19 +184,23 @@ Markdown(f""" We can also filter for mitochondrial, ribosomal and haemoglobin content of the cells; exactly which filtering threshold should be used are, again, up to you -and your biological knowledge of the sample at hand. +and your biological knowledge of the sample at hand. Please note that neither +ribosomal nor haemoglobin content is filtered by default. ```{python} # Filter spots -st_adata = st_adata[st_adata.obs["pct_counts_mt"] < mito_threshold] +st_adata = st_adata[st_adata.obs["pct_counts_mt"] <= mito_threshold] n_spots_filtered_mito = st_adata.shape[0] st_adata = st_adata[st_adata.obs["pct_counts_ribo"] >= ribo_threshold] n_spots_filtered_ribo = st_adata.shape[0] +st_adata = st_adata[st_adata.obs["pct_counts_hb"] <= hb_threshold] +n_spots_filtered_hb = st_adata.shape[0] # Print results Markdown(f""" - Removed `{st_adata.shape[0] - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. - Removed `{n_spots_filtered_mito - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. +- Removed `{n_spots_filtered_ribo - n_spots_filtered_hb}` spots with more than `{hb_threshold}%` haemoglobin content. - A total of `{n_spots_filtered_min_genes}` spots out of `{n_spots}` remain after filtering. - A total of `{n_genes_filtered_min_spots}` genes out of `{n_genes}` remain after filtering. """) diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index 9094491..f52b6bb 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -41,6 +41,7 @@ process ST_QUALITY_CONTROLS { -P min_spots:${params.st_qc_min_spots} \ -P mito_threshold:${params.st_qc_mito_threshold} \ -P ribo_threshold:${params.st_qc_ribo_threshold} \ + -P hb_threshold:${params.st_qc_hb_threshold} \ -P output_adata_filtered:st_adata_filtered.h5ad cat <<-END_VERSIONS > versions.yml diff --git a/nextflow.config b/nextflow.config index f4c7d72..a8212c9 100644 --- a/nextflow.config +++ b/nextflow.config @@ -23,6 +23,7 @@ params { st_qc_min_spots = 1 st_qc_mito_threshold = 20 st_qc_ribo_threshold = 0 + st_qc_hb_threshold = 100 // Clustering st_cluster_n_hvgs = 2000 diff --git a/nextflow_schema.json b/nextflow_schema.json index dd2e8d9..b3e1af1 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -126,6 +126,12 @@ "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by defeault).", "fa_icon": "fas fa-hashtag" }, + "st_qc_hb_threshold": { + "type": "integer", + "default": 100, + "description": "The maximum proportion of haemoglobin content that a spot is allowed to have to pass the filtering (no filtering is done by defeault).", + "fa_icon": "fas fa-hashtag" + }, "st_cluster_n_hvgs": { "type": "integer", "default": 2000, From b65e1e5a158e01b9caba6796ce35fcdc424a03ea Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 11:25:14 +0100 Subject: [PATCH 324/410] Rename header; minor formatting --- bin/st_quality_controls.qmd | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 44a0aac..13885c2 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -201,8 +201,6 @@ Markdown(f""" - Removed `{st_adata.shape[0] - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. - Removed `{n_spots_filtered_mito - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. - Removed `{n_spots_filtered_ribo - n_spots_filtered_hb}` spots with more than `{hb_threshold}%` haemoglobin content. -- A total of `{n_spots_filtered_min_genes}` spots out of `{n_spots}` remain after filtering. -- A total of `{n_genes_filtered_min_spots}` genes out of `{n_genes}` remain after filtering. """) ``` @@ -233,10 +231,17 @@ if (st_adata.shape[0] == 0 or st_adata.shape[1] == 0): ) ``` -## Violin plots +## Filtering results + +```{python} +# Print filtering results +Markdown(f""" +The final results of all the filtering is as follows: -Following the filtering we can look at the same violin plots as above to see the -new distributions: +- A total of `{st_adata.shape[0]}` spots out of `{n_spots}` remain after filtering. +- A total of `{st_adata.shape[1]}` genes out of `{n_genes}` remain after filtering. +""") +``` ```{python} #| layout-nrow: 2 @@ -248,6 +253,6 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ```{python} #| echo: false -# Write filtered data to disc +# Write filtered data to disk st_adata.write(output_adata_filtered) ``` From 0d8364bf34bf2bc75ec3b69d98a1e10f09d1dcf8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 25 Jan 2024 12:50:46 +0100 Subject: [PATCH 325/410] Update tests --- tests/pipeline/test_downstream.nf.test | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 2 +- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 53b401c..dbfc38a 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -29,10 +29,10 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 6cf1f4b..fc02eaf 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -26,7 +26,7 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index a43487e..ab7a967 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -22,7 +22,7 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("the same violin plots as above") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, From a5691c66c999f962d6be99f7e6f37481071a571b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 26 Jan 2024 09:04:21 +0100 Subject: [PATCH 326/410] Fix grammar --- bin/st_quality_controls.qmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 13885c2..e209089 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -108,9 +108,9 @@ sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts ## Scatter plots -It is also useful to some of these quality metrics against each other in scatter -plots, such as mitochondrial versus ribosomal content and the total counts -versus the number of genes: +It is also useful to compare some of these quality metrics against each other in +scatter plots, such as mitochondrial versus ribosomal content and the total +counts versus the number of genes: ```{python} #| layout-ncol: 2 From ecffa90471a58a9bd3c11d5a0a2412b627187bec Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 26 Jan 2024 09:25:10 +0100 Subject: [PATCH 327/410] Change QC threshold type from integer to number --- nextflow_schema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index b3e1af1..5db2fe4 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -114,20 +114,20 @@ "fa_icon": "fas fa-hashtag" }, "st_qc_mito_threshold": { - "type": "integer", + "type": "number", "default": 20, "description": "The maximum proportion of mitochondrial content that a spot is allowed to have to pass the filtering.", "help_text": "If you do not wish to filter based on mitochondrial content, set this parameter to `100`.", "fa_icon": "fas fa-hashtag" }, "st_qc_ribo_threshold": { - "type": "integer", + "type": "number", "default": 0, "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by defeault).", "fa_icon": "fas fa-hashtag" }, "st_qc_hb_threshold": { - "type": "integer", + "type": "number", "default": 100, "description": "The maximum proportion of haemoglobin content that a spot is allowed to have to pass the filtering (no filtering is done by defeault).", "fa_icon": "fas fa-hashtag" From db5fb4e50f5a89ae18bf5ec3b787ab9c9db2db09 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 30 Jan 2024 08:31:03 +0100 Subject: [PATCH 328/410] Fix prettier --- .devcontainer/devcontainer.json | 8 ++++---- .github/workflows/download_pipeline.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4a9bc5c..4ecfbfe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,11 +18,11 @@ "python.linting.flake8Path": "/opt/conda/bin/flake8", "python.linting.pycodestylePath": "/opt/conda/bin/pycodestyle", "python.linting.pydocstylePath": "/opt/conda/bin/pydocstyle", - "python.linting.pylintPath": "/opt/conda/bin/pylint", + "python.linting.pylintPath": "/opt/conda/bin/pylint" }, // Add the IDs of extensions you want installed when the container is created. - "extensions": ["ms-python.python", "ms-python.vscode-pylance", "nf-core.nf-core-extensionpack"], - }, - }, + "extensions": ["ms-python.python", "ms-python.vscode-pylance", "nf-core.nf-core-extensionpack"] + } + } } diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index 8611458..8a33004 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -64,4 +64,4 @@ jobs: env: NXF_SINGULARITY_CACHEDIR: ./ NXF_SINGULARITY_HOME_MOUNT: true - run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -stub -profile test,singularity --outdir ./results + run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -stub -profile test,singularity --outdir ./results From 977b7feaafbcf5e3230c23c3ad2e3aa541d55daf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 30 Jan 2024 09:01:22 +0100 Subject: [PATCH 329/410] Fix linting --- ...core-spatialtranscriptomics_logo_light.png | Bin 72982 -> 72211 bytes ...-core-spatialtranscriptomics_logo_dark.png | Bin 21843 -> 21637 bytes ...core-spatialtranscriptomics_logo_light.png | Bin 18378 -> 18162 bytes nextflow.config | 8 ++++---- nextflow_schema.json | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/nf-core-spatialtranscriptomics_logo_light.png b/assets/nf-core-spatialtranscriptomics_logo_light.png index 25d303ca6baf0b4d2c1a3c6b977fca63343f757f..34da382b6a78091237f8ca325032f48989619456 100644 GIT binary patch literal 72211 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 Date: Tue, 30 Jan 2024 09:04:51 +0100 Subject: [PATCH 330/410] Fix black --- bin/read_st_data.py | 42 +++++++++++++++---- .../templates/dumpsoftwareversions.py | 4 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index f83301b..7c542af 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -114,19 +114,26 @@ def read_visium( for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") + logg.warning( + f"You seem to be missing an image file.\n" + f"Could not find '{f}'." + ) else: raise OSError(f"Could not find '{f}'") adata.uns["spatial"][library_id]["images"] = dict() for res in ["hires", "lowres"]: try: - adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) + adata.uns["spatial"][library_id]["images"][res] = imread( + str(files[f"{res}_image"]) + ) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) + adata.uns["spatial"][library_id]["scalefactors"] = json.loads( + files["scalefactors_json_file"].read_bytes() + ) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -150,7 +157,9 @@ def read_visium( adata.obs = adata.obs.join(positions, how="left") - adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() + adata.obsm["spatial"] = adata.obs[ + ["pxl_row_in_fullres", "pxl_col_in_fullres"] + ].to_numpy() adata.obs.drop( columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], inplace=True, @@ -160,7 +169,9 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + source_image_path + ) return adata @@ -171,13 +182,28 @@ def read_visium( description="Load spatial transcriptomics data from MTX matrices and aligned images." ) parser.add_argument( - "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." + "--SRCountDir", + metavar="SRCountDir", + type=str, + default=None, + help="Input directory with Spaceranger data.", + ) + parser.add_argument( + "--outAnnData", + metavar="outAnnData", + type=str, + default=None, + help="Output h5ad file path.", ) - parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") args = parser.parse_args() # Read Visium data - st_adata = read_visium(args.SRCountDir, count_file="raw_feature_bc_matrix.h5", library_id=None, load_images=True) + st_adata = read_visium( + args.SRCountDir, + count_file="raw_feature_bc_matrix.h5", + library_id=None, + load_images=True, + ) # Write raw anndata to file st_adata.write(args.outAnnData) diff --git a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py index da03340..4a99360 100755 --- a/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py +++ b/modules/nf-core/custom/dumpsoftwareversions/templates/dumpsoftwareversions.py @@ -58,7 +58,9 @@ def main(): } with open("$versions") as f: - versions_by_process = yaml.load(f, Loader=yaml.BaseLoader) | versions_this_module + versions_by_process = ( + yaml.load(f, Loader=yaml.BaseLoader) | versions_this_module + ) # aggregate versions by the module name (derived from fully-qualified process name) versions_by_module = {} From 7374953b3c46801444f6a45f800cf6b9cdc6c42d Mon Sep 17 00:00:00 2001 From: Alexander Peltzer Date: Tue, 30 Jan 2024 09:26:17 +0100 Subject: [PATCH 331/410] unchange logos --- .nf-core.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.nf-core.yml b/.nf-core.yml index bc2e3d4..da5010a 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -5,3 +5,6 @@ lint: - conf/igenomes.config files_unchanged: - .gitattributes + - assets/nf-core-spatialtranscriptomics_logo_light.png + - docs/images/nf-core-spatialtranscriptomics_logo_light.png + - docs/images/nf-core-spatialtranscriptomics_logo_dark.png From 0dc36ec59025424c93362371807a5573a0b79898 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 30 Jan 2024 14:01:59 +0100 Subject: [PATCH 332/410] Fix editorconfig linting errors --- bin/st_clustering.qmd | 4 ++-- bin/st_quality_controls.qmd | 12 ++++++------ bin/st_spatial_de.qmd | 4 ++-- subworkflows/local/input_check.nf | 4 ++-- workflows/spatialtranscriptomics.nf | 16 +++++++++------- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index c37fbbe..be8e496 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -2,9 +2,9 @@ title: "nf-core/spatialtranscriptomics" subtitle: "Dimensionality reduction and clustering" format: - nf-core-html: default + nf-core-html: default execute: - keep-ipynb: true + keep-ipynb: true jupyter: python3 --- diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index e209089..a841d8f 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -2,7 +2,7 @@ title: "nf-core/spatialtranscriptomics" subtitle: "Pre-processing and quality controls" format: - nf-core-html: default + nf-core-html: default jupyter: python3 --- @@ -75,7 +75,7 @@ st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') st_adata.var['ribo'] = st_adata.var_names.str.contains(("^RP[LS]")) st_adata.var['hb'] = st_adata.var_names.str.contains(("^HB[AB]")) sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "ribo", "hb"], - inplace=True, log1p=False) + inplace=True, log1p=False) # Save a copy of data as a restore-point if filtering results in 0 spots left st_adata_before_filtering = st_adata.copy() @@ -90,9 +90,9 @@ mitochondrial, ribosomal and haemoglobin genes: ```{python} #| layout-nrow: 2 sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], - multi_panel=True, jitter=0.4, rotation= 45) + multi_panel=True, jitter=0.4, rotation= 45) sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], - multi_panel=True, jitter=0.4, rotation= 45) + multi_panel=True, jitter=0.4, rotation= 45) ``` ## Spatial distributions @@ -246,9 +246,9 @@ The final results of all the filtering is as follows: ```{python} #| layout-nrow: 2 sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], - multi_panel=True, jitter=0.4, rotation= 45) + multi_panel=True, jitter=0.4, rotation= 45) sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], - multi_panel=True, jitter=0.4, rotation= 45) + multi_panel=True, jitter=0.4, rotation= 45) ``` ```{python} diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 82d8e81..0e1e211 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -2,7 +2,7 @@ title: "nf-core/spatialtranscriptomics" subtitle: "Differential gene expression" format: - nf-core-html: default + nf-core-html: default jupyter: python3 --- @@ -95,5 +95,5 @@ itself to visualize the patterns: symbols = results_tab.iloc[: n_top_spatial_degs]["gene_symbol"] plt.rcParams["figure.figsize"] = (3.5, 4) sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, - ncols=2, title=symbols, size=1.25) + ncols=2, title=symbols, size=1.25) ``` diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index baff0e8..abd2391 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -92,8 +92,8 @@ def create_channel_spaceranger(LinkedHashMap meta) { // Convert a path in `meta` to a file object and return it. If `key` is not contained in `meta` // return an empty list which is recognized as 'no file' by nextflow. def get_file_from_meta = {key -> - v = meta.remove(key); - return v ? file(v) : [] + v = meta.remove(key); + return v ? file(v) : [] } fastq_dir = meta.remove("fastq_dir") diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 47f529f..37047b9 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -17,13 +17,15 @@ WorkflowSpatialtranscriptomics.initialise(params, log) // Check input path parameters to see if they exist log.info """\ - Project directory: ${projectDir} - """ - .stripIndent() - -def checkPathParamList = [ params.input, - params.spaceranger_reference, - params.spaceranger_probeset ] + Project directory: ${projectDir} + """ + .stripIndent() + +def checkPathParamList = [ + params.input, + params.spaceranger_reference, + params.spaceranger_probeset +] for (param in checkPathParamList) { if (param) { file(param, checkIfExists: true) } } // Check mandatory parameters From e13cf660a3d30ed10e60b38b412059f9d6de929c Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 31 Jan 2024 09:34:31 +0100 Subject: [PATCH 333/410] Use spatialdata zarr objects instead of AnnData Replace all reads and writes with SpatialData zarr, and use a "legacy anndata" conversion function to add images and scales, in order to plot with scanpy. --- bin/read_st_data.py | 174 ++------------------------- bin/st_clustering.qmd | 37 +++++- bin/st_quality_controls.qmd | 46 +++++-- bin/st_spatial_de.qmd | 32 ++++- env/reports/Dockerfile | 2 +- env/reports/environment.yml | 11 +- env/st_spatial_de/environment.yml | 12 +- modules/local/st_clustering.nf | 10 +- modules/local/st_quality_controls.nf | 10 +- modules/local/st_read_data.nf | 8 +- modules/local/st_spatial_de.nf | 6 +- subworkflows/local/st_downstream.nf | 11 +- workflows/spatialtranscriptomics.nf | 2 +- 13 files changed, 152 insertions(+), 209 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index f83301b..f1b1547 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -2,168 +2,7 @@ # Load packages import argparse -from scanpy import read_10x_h5 -from pathlib import Path -from typing import Union, Optional - -import json -import pandas as pd -from matplotlib.image import imread -from anndata import AnnData - -from scanpy import logging as logg - - -def read_visium( - path: Union[str, Path], - genome: Optional[str] = None, - *, - count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str = None, - load_images: Optional[bool] = True, - source_image_path: Optional[Union[str, Path]] = None, -) -> AnnData: - """\ - Read 10x-Genomics-formatted visum dataset. - - In addition to reading regular 10x output, - this looks for the `spatial` folder and loads images, - coordinates and scale factors. - Based on the `Space Ranger output docs`_. - - See :func:`~scanpy.pl.spatial` for a compatible plotting function. - - .. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview - - Parameters - ---------- - path - Path to directory for visium datafiles. - genome - Filter expression to genes within this genome. - count_file - Which file in the passed directory to use as the count file. Typically would be one of: - 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. - library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. - source_image_path - Path to the high-resolution tissue image. Path will be included in - `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. - - Returns - ------- - Annotated data matrix, where observations/cells are named by their - barcode and variables/genes by gene name. Stores the following information: - - :attr:`~anndata.AnnData.X` - The data matrix is stored - :attr:`~anndata.AnnData.obs_names` - Cell names - :attr:`~anndata.AnnData.var_names` - Gene names for a feature barcode matrix, probe names for a probe bc matrix - :attr:`~anndata.AnnData.var`\\ `['gene_ids']` - Gene IDs - :attr:`~anndata.AnnData.var`\\ `['feature_types']` - Feature types - :attr:`~anndata.AnnData.obs`\\ `[filtered_barcodes]` - filtered barcodes if present in the matrix - :attr:`~anndata.AnnData.var` - Any additional metadata present in /matrix/features is read in. - :attr:`~anndata.AnnData.uns`\\ `['spatial']` - Dict of spaceranger output files with 'library_id' as key - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['images']` - Dict of images (`'hires'` and `'lowres'`) - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['scalefactors']` - Scale factors for the spots - :attr:`~anndata.AnnData.uns`\\ `['spatial'][library_id]['metadata']` - Files metadata: 'chemistry_description', 'software_version', 'source_image_path' - :attr:`~anndata.AnnData.obsm`\\ `['spatial']` - Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. - """ - path = Path(path) - adata = read_10x_h5(path / "raw_feature_bc_matrix.h5") - # use ensemble IDs as index, because they are unique - adata.var["gene_symbol"] = adata.var_names - adata.var.set_index("gene_ids", inplace=True) - - adata.uns["spatial"] = dict() - - from h5py import File - - with File(path / count_file, mode="r") as f: - attrs = dict(f.attrs) - if library_id is None: - library_id = str(attrs.pop("library_ids")[0], "utf-8") - - adata.uns["spatial"][library_id] = dict() - - if load_images: - tissue_positions_file = ( - path / "spatial/tissue_positions.csv" - if (path / "spatial/tissue_positions.csv").exists() - else path / "spatial/tissue_positions_list.csv" - ) - files = dict( - tissue_positions_file=tissue_positions_file, - scalefactors_json_file=path / "spatial/scalefactors_json.json", - hires_image=path / "spatial/tissue_hires_image.png", - lowres_image=path / "spatial/tissue_lowres_image.png", - ) - - # check if files exists, continue if images are missing - for f in files.values(): - if not f.exists(): - if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") - else: - raise OSError(f"Could not find '{f}'") - - adata.uns["spatial"][library_id]["images"] = dict() - for res in ["hires", "lowres"]: - try: - adata.uns["spatial"][library_id]["images"][res] = imread(str(files[f"{res}_image"])) - except Exception: - raise OSError(f"Could not find '{res}_image'") - - # read json scalefactors - adata.uns["spatial"][library_id]["scalefactors"] = json.loads(files["scalefactors_json_file"].read_bytes()) - - adata.uns["spatial"][library_id]["metadata"] = { - k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) - for k in ("chemistry_description", "software_version") - if k in attrs - } - - # read coordinates - positions = pd.read_csv( - files["tissue_positions_file"], - header=0 if tissue_positions_file.name == "tissue_positions.csv" else None, - index_col=0, - ) - positions.columns = [ - "in_tissue", - "array_row", - "array_col", - "pxl_col_in_fullres", - "pxl_row_in_fullres", - ] - - adata.obs = adata.obs.join(positions, how="left") - - adata.obsm["spatial"] = adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]].to_numpy() - adata.obs.drop( - columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], - inplace=True, - ) - - # put image path in uns - if source_image_path is not None: - # get an absolute path - source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) - - return adata - +import spatialdata_io if __name__ == "__main__": # Parse command-line arguments @@ -173,11 +12,14 @@ def read_visium( parser.add_argument( "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." ) - parser.add_argument("--outAnnData", metavar="outAnnData", type=str, default=None, help="Output h5ad file path.") + parser.add_argument("--output_sdata", metavar="output_sdata", type=str, default=None, help="Output spatialdata zarr path.") + #TODO Add argument with meta.id for dataset_id + args = parser.parse_args() # Read Visium data - st_adata = read_visium(args.SRCountDir, count_file="raw_feature_bc_matrix.h5", library_id=None, load_images=True) + st_spatialdata = spatialdata_io.visium(args.SRCountDir, counts_file ="raw_feature_bc_matrix.h5", dataset_id ="visium") - # Write raw anndata to file - st_adata.write(args.outAnnData) + # Write raw spatialdata to file + st_spatialdata.write(args.output_sdata, overwrite=True) + \ No newline at end of file diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index c37fbbe..eef9dfd 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -12,10 +12,11 @@ jupyter: python3 #| tags: [parameters] #| echo: false -input_adata_filtered = "st_adata_filtered.h5ad" # Name of the input anndata file +input_sdata = "st_sdata_filtered.zarr" # Name of the input anndata file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses output_adata_processed = "st_adata_processed.h5ad" # Name of the output anndata file +output_sdata = "st_sdata_processed.zarr" # Name of the input anndata file ``` The data has already been filtered in the _quality controls_ reports and is @@ -23,9 +24,11 @@ saved in the AnnData format: ```{python} #| warning: false +import spatialdata import scanpy as sc import numpy as np import pandas as pd +from anndata import AnnData from umap import UMAP from matplotlib import pyplot as plt import seaborn as sns @@ -33,7 +36,33 @@ from IPython.display import display, Markdown ``` ```{python} -st_adata = sc.read("./" + input_adata_filtered) +# Make sure we can use scanpy plots with the AnnData object exported from sdata.table +# This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ +# Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). +def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: + adata = sdata.table + for dataset_id in adata.uns["spatial"]: + adata.uns["spatial"][dataset_id]["images"] = { + "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), + "lowres": np.array(sdata.images[f"{dataset_id}_lowres_image"]).transpose([1, 2, 0]), + } + adata.uns["spatial"][dataset_id]["scalefactors"] = { + "tissue_hires_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_hires" + ).scale[0], + "tissue_lowres_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_lowres" + ).scale[0], + "spot_diameter_fullres": sdata.shapes[dataset_id]["radius"][0] * 2, + } + return adata +``` + +```{python} +st_sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) + +st_adata = to_legacy_anndata(st_sdata) + print("Content of the AnnData object:") print(st_adata) ``` @@ -143,3 +172,7 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) #| echo: false st_adata.write(output_adata_processed) ``` + +```{python} +st_sdata.write("./" + output_sdata) +``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index e209089..ce3f0ce 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -1,13 +1,11 @@ --- title: "nf-core/spatialtranscriptomics" subtitle: "Pre-processing and quality controls" -format: - nf-core-html: default jupyter: python3 --- # Introduction - + Spatial Transcriptomics data analysis involves several steps, including quality controls (QC) and pre-processing, to ensure the reliability of downstream analyses. This is an essential step in spatial transcriptomics to @@ -25,30 +23,59 @@ analysis tools and facilitates seamless integration into existing workflows. ```{python} #| tags: [parameters] #| echo: false -input_adata_raw = "st_adata_raw.h5ad" # Name of the input anndata file +input_sdata = "st_sdata_raw.zarr" # Name of the input anndata file min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) hb_threshold = 100 # content threshold (%) -output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file +output_sdata = "st_sdata_filtered.zarr" # Name of the input anndata file ``` ```{python} +import spatialdata import scanpy as sc import scipy import pandas as pd import matplotlib.pyplot as plt +import numpy as np import seaborn as sns +from anndata import AnnData from IPython.display import display, Markdown from textwrap import dedent plt.rcParams["figure.figsize"] = (6, 6) ``` +```{python} +# Make sure we can use scanpy plots with the AnnData object exported from sdata.table +# This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ +# Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). +def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: + adata = sdata.table + for dataset_id in adata.uns["spatial"]: + adata.uns["spatial"][dataset_id]["images"] = { + "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), + "lowres": np.array(sdata.images[f"{dataset_id}_lowres_image"]).transpose([1, 2, 0]), + } + adata.uns["spatial"][dataset_id]["scalefactors"] = { + "tissue_hires_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_hires" + ).scale[0], + "tissue_lowres_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_lowres" + ).scale[0], + "spot_diameter_fullres": sdata.shapes[dataset_id]["radius"][0] * 2, + } + return adata +``` + ```{python} # Read the data -st_adata = sc.read("./" + input_adata_raw) + +st_sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) + +st_adata = to_legacy_anndata(st_sdata) # Convert X matrix from csr to csc dense matrix for output compatibility: st_adata.X = scipy.sparse.csc_matrix(st_adata.X) @@ -252,7 +279,8 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ``` ```{python} -#| echo: false -# Write filtered data to disk -st_adata.write(output_adata_filtered) +st_sdata.write("./" + output_sdata) ``` + + + diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 82d8e81..44da8ef 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -9,7 +9,7 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -input_adata_processed = "st_adata_processed.h5ad" +input_sdata = "st_sdata.zarr" output_spatial_degs = "st_spatial_de.csv" n_top_spatial_degs = 14 ``` @@ -18,12 +18,40 @@ n_top_spatial_degs = 14 import scanpy as sc import pandas as pd import SpatialDE +import numpy as np +import spatialdata +from anndata import AnnData from matplotlib import pyplot as plt ``` +```{python} +# Make sure we can use scanpy plots with the AnnData object exported from sdata.table +# This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ +# Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). +def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: + adata = sdata.table + for dataset_id in adata.uns["spatial"]: + adata.uns["spatial"][dataset_id]["images"] = { + "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), + "lowres": np.array(sdata.images[f"{dataset_id}_lowres_image"]).transpose([1, 2, 0]), + } + adata.uns["spatial"][dataset_id]["scalefactors"] = { + "tissue_hires_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_hires" + ).scale[0], + "tissue_lowres_scalef": spatialdata.transformations.get_transformation( + sdata.shapes[dataset_id], to_coordinate_system="downscaled_lowres" + ).scale[0], + "spot_diameter_fullres": sdata.shapes[dataset_id]["radius"][0] * 2, + } + return adata +``` + ```{python} # Read data -st_adata = sc.read(input_adata_processed) +st_sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) + +st_adata = to_legacy_anndata(st_sdata) print("Content of the AnnData object:") print(st_adata) diff --git a/env/reports/Dockerfile b/env/reports/Dockerfile index e1cb40e..6c82239 100644 --- a/env/reports/Dockerfile +++ b/env/reports/Dockerfile @@ -2,7 +2,7 @@ FROM jdutant/quarto-minimal:1.3.313 as quarto # Second stage: multi-platform Mamba image -FROM condaforge/mambaforge:23.1.0-1 +FROM condaforge/mambaforge:23.11.0-0 # Copy Quarto installation from first stage and add to PATH COPY --from=quarto /opt/quarto /opt/quarto diff --git a/env/reports/environment.yml b/env/reports/environment.yml index f8f1923..6166d60 100644 --- a/env/reports/environment.yml +++ b/env/reports/environment.yml @@ -2,10 +2,17 @@ channels: - conda-forge - bioconda dependencies: + - python=3.10 - jupyter=1.0.0 - leidenalg=0.9.1 - papermill=2.3.4 - pip=23.0.1 - - scanpy=1.9.6 + - gcc=13.2.0 + - libgdal=3.8.3 + - gxx=13.2.0 + - imagecodecs=2024.1.1 - pip: - - SpatialDE==1.1.3 + - scanpy==1.9.8 + - spatialdata==0.0.15 + - spatialdata-io==0.0.9 + - spatialdata-plot==0.1.0 \ No newline at end of file diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml index 28457d1..ad72fe9 100644 --- a/env/st_spatial_de/environment.yml +++ b/env/st_spatial_de/environment.yml @@ -1,13 +1,19 @@ channels: - conda-forge - bioconda - - defaults dependencies: - - quarto=1.3.353 + - python=3.10 - jupyter=1.0.0 - leidenalg=0.9.1 - papermill=2.3.4 - pip=23.0.1 - - scanpy=1.9.3 + - gcc=13.2.0 + - libgdal=3.8.3 + - gxx=13.2.0 + - imagecodecs=2024.1.1 - pip: + - scanpy==1.9.8 - SpatialDE==1.1.3 + - spatialdata==0.0.15 + - spatialdata-io==0.0.9 + - spatialdata-plot==0.1.0 \ No newline at end of file diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf index 0819c7a..ceb49ff 100644 --- a/modules/local/st_clustering.nf +++ b/modules/local/st_clustering.nf @@ -9,7 +9,7 @@ process ST_CLUSTERING { label 'process_low' conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0 conda-forge::leidenalg=0.9.1" - container "docker.io/erikfas/spatialtranscriptomics" + container "docker.io/cavenel/spatialtranscriptomics" // Exit if running this module with -profile conda / -profile mamba on ARM64 if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { @@ -22,10 +22,11 @@ process ST_CLUSTERING { input: path(report) path(report_template) - tuple val(meta), path(st_adata_filtered) + tuple val(meta), path(st_sdata) output: tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed + tuple val(meta), path("st_sdata_processed.zarr"), emit: st_sdata_processed tuple val(meta), path("st_clustering.html") , emit: html path("versions.yml") , emit: versions @@ -35,10 +36,11 @@ process ST_CLUSTERING { script: """ quarto render ${report} \ - -P input_adata_filtered:${st_adata_filtered} \ + -P input_sdata:${st_sdata} \ -P cluster_resolution:${params.st_cluster_resolution} \ -P n_hvgs:${params.st_cluster_n_hvgs} \ - -P output_adata_processed:st_adata_processed.h5ad + -P output_adata_processed:st_adata_processed.h5ad \ + -P output_sdata:st_sdata_processed.zarr \ cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf index f52b6bb..a57c89b 100644 --- a/modules/local/st_quality_controls.nf +++ b/modules/local/st_quality_controls.nf @@ -9,7 +9,7 @@ process ST_QUALITY_CONTROLS { label 'process_low' conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0" - container "docker.io/erikfas/spatialtranscriptomics" + container "docker.io/cavenel/spatialtranscriptomics" // Exit if running this module with -profile conda / -profile mamba on ARM64 if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { @@ -22,10 +22,10 @@ process ST_QUALITY_CONTROLS { input: path(report) path(report_template) - tuple val(meta), path(st_adata_raw) + tuple val(meta), path(st_sdata_raw) output: - tuple val(meta), path("st_adata_filtered.h5ad") , emit: st_adata_filtered + tuple val(meta), path("st_sdata_filtered.zarr") , emit: st_sdata_filtered tuple val(meta), path("st_quality_controls.html"), emit: html path("versions.yml") , emit: versions @@ -35,14 +35,14 @@ process ST_QUALITY_CONTROLS { script: """ quarto render ${report} \ - -P input_adata_raw:${st_adata_raw} \ + -P input_sdata:${st_sdata_raw} \ -P min_counts:${params.st_qc_min_counts} \ -P min_genes:${params.st_qc_min_genes} \ -P min_spots:${params.st_qc_min_spots} \ -P mito_threshold:${params.st_qc_mito_threshold} \ -P ribo_threshold:${params.st_qc_ribo_threshold} \ -P hb_threshold:${params.st_qc_hb_threshold} \ - -P output_adata_filtered:st_adata_filtered.h5ad + -P output_sdata:st_sdata_filtered.zarr cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_read_data.nf b/modules/local/st_read_data.nf index a39c9c3..ce56d04 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/st_read_data.nf @@ -7,15 +7,13 @@ process ST_READ_DATA { label 'process_low' conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" - 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' }" + container "docker.io/cavenel/spatialtranscriptomics" input: tuple val (meta), path("${meta.id}/*") output: - tuple val(meta), path("st_adata_raw.h5ad"), emit: st_adata_raw + tuple val(meta), path("st_sdata.zarr"), emit: st_sdata_raw path("versions.yml") , emit: versions when: @@ -32,7 +30,7 @@ process ST_READ_DATA { read_st_data.py \\ --SRCountDir "${meta.id}" \\ - --outAnnData st_adata_raw.h5ad + --output_sdata st_sdata.zarr cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf index 249fda1..c02c0e0 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_spatial_de.nf @@ -9,7 +9,7 @@ process ST_SPATIAL_DE { label 'process_medium' conda "env/st_spatial_de/environment.yml" - container "docker.io/erikfas/spatialtranscriptomics" + container "docker.io/cavenel/spatialtranscriptomics" // Exit if running this module with -profile conda / -profile mamba on ARM64 if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { @@ -21,7 +21,7 @@ process ST_SPATIAL_DE { input: path(report) path(report_template) - tuple val(meta), path(st_adata_processed) + tuple val(meta), path(st_sdata) output: tuple val(meta), path("*.csv") , emit: degs @@ -34,7 +34,7 @@ process ST_SPATIAL_DE { script: """ quarto render ${report} \ - -P input_adata_processed:${st_adata_processed} \ + -P input_sdata:${st_sdata} \ -P n_top_spatial_degs:${params.st_n_top_spatial_degs} \ -P output_spatial_degs:st_spatial_de.csv diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 91b5e74..9bb64ea 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -9,7 +9,7 @@ include { ST_CLUSTERING } from '../../modules/local/st_clustering' workflow ST_DOWNSTREAM { take: - st_adata_raw + st_sdata_raw main: @@ -29,7 +29,7 @@ workflow ST_DOWNSTREAM { ST_QUALITY_CONTROLS ( report_quality_controls, report_template, - st_adata_raw + st_sdata_raw ) ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) @@ -39,7 +39,7 @@ workflow ST_DOWNSTREAM { ST_CLUSTERING ( report_clustering, report_template, - ST_QUALITY_CONTROLS.out.st_adata_filtered + ST_QUALITY_CONTROLS.out.st_sdata_filtered ) ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) @@ -49,15 +49,14 @@ workflow ST_DOWNSTREAM { ST_SPATIAL_DE ( report_spatial_de, report_template, - ST_CLUSTERING.out.st_adata_processed + ST_CLUSTERING.out.st_sdata_processed ) ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) emit: - st_data_norm = ST_QUALITY_CONTROLS.out.st_adata_filtered // channel: [ meta, h5ad ] html = ST_QUALITY_CONTROLS.out.html // channel: [ html ] - st_adata_processed = ST_CLUSTERING.out.st_adata_processed // channel: [ meta, h5ad] + st_sdata_processed = ST_CLUSTERING.out.st_sdata_processed // channel: [ meta, h5ad] html = ST_CLUSTERING.out.html // channel: [ html ] degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index c3c34f9..8b65559 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -124,7 +124,7 @@ workflow ST { // SUBWORKFLOW: Downstream analyses of ST data // ST_DOWNSTREAM ( - ST_READ_DATA.out.st_adata_raw + ST_READ_DATA.out.st_sdata_raw ) ch_versions = ch_versions.mix(ST_DOWNSTREAM.out.versions) From d866f378cbc103f246166d8ceae230f141b495e1 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 31 Jan 2024 09:34:57 +0100 Subject: [PATCH 334/410] Fix typo --- nextflow_schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 5db2fe4..aa93d47 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -123,13 +123,13 @@ "st_qc_ribo_threshold": { "type": "number", "default": 0, - "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by defeault).", + "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by default).", "fa_icon": "fas fa-hashtag" }, "st_qc_hb_threshold": { "type": "number", "default": 100, - "description": "The maximum proportion of haemoglobin content that a spot is allowed to have to pass the filtering (no filtering is done by defeault).", + "description": "The maximum proportion of haemoglobin content that a spot is allowed to have to pass the filtering (no filtering is done by default).", "fa_icon": "fas fa-hashtag" }, "st_cluster_n_hvgs": { From aec833ff3ea98906e0630f543a32b8a1a085eb9d Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Wed, 31 Jan 2024 11:23:56 +0100 Subject: [PATCH 335/410] Fix black and prettier --- bin/read_st_data.py | 11 ++++++----- env/reports/environment.yml | 2 +- env/st_spatial_de/environment.yml | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/read_st_data.py b/bin/read_st_data.py index f1b1547..610d89a 100755 --- a/bin/read_st_data.py +++ b/bin/read_st_data.py @@ -12,14 +12,15 @@ parser.add_argument( "--SRCountDir", metavar="SRCountDir", type=str, default=None, help="Input directory with Spaceranger data." ) - parser.add_argument("--output_sdata", metavar="output_sdata", type=str, default=None, help="Output spatialdata zarr path.") - #TODO Add argument with meta.id for dataset_id - + parser.add_argument( + "--output_sdata", metavar="output_sdata", type=str, default=None, help="Output spatialdata zarr path." + ) + # TODO Add argument with meta.id for dataset_id + args = parser.parse_args() # Read Visium data - st_spatialdata = spatialdata_io.visium(args.SRCountDir, counts_file ="raw_feature_bc_matrix.h5", dataset_id ="visium") + st_spatialdata = spatialdata_io.visium(args.SRCountDir, counts_file="raw_feature_bc_matrix.h5", dataset_id="visium") # Write raw spatialdata to file st_spatialdata.write(args.output_sdata, overwrite=True) - \ No newline at end of file diff --git a/env/reports/environment.yml b/env/reports/environment.yml index 6166d60..b7da2be 100644 --- a/env/reports/environment.yml +++ b/env/reports/environment.yml @@ -15,4 +15,4 @@ dependencies: - scanpy==1.9.8 - spatialdata==0.0.15 - spatialdata-io==0.0.9 - - spatialdata-plot==0.1.0 \ No newline at end of file + - spatialdata-plot==0.1.0 diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml index ad72fe9..984e420 100644 --- a/env/st_spatial_de/environment.yml +++ b/env/st_spatial_de/environment.yml @@ -16,4 +16,4 @@ dependencies: - SpatialDE==1.1.3 - spatialdata==0.0.15 - spatialdata-io==0.0.9 - - spatialdata-plot==0.1.0 \ No newline at end of file + - spatialdata-plot==0.1.0 From e6a6c64ce49407b5d87beff951332b98c9a52ee8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 2 Feb 2024 14:32:48 +0100 Subject: [PATCH 336/410] Update FastQC module --- modules.json | 2 +- modules/nf-core/fastqc/tests/main.nf.test | 14 ++-- .../nf-core/fastqc/tests/main.nf.test.snap | 76 ++++++++++++++++++- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/modules.json b/modules.json index f29c0dc..fc07cf9 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ }, "fastqc": { "branch": "master", - "git_sha": "c9488585ce7bd35ccd2a30faa2371454c8112fb9", + "git_sha": "f4ae1d942bd50c5c0b9bd2de1393ce38315ba57c", "installed_by": ["modules"] }, "multiqc": { diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index 1f21c66..70edae4 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -33,7 +33,7 @@ nextflow_process { { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("

    *qku_+;?>w8;OtNN-yvnjlX!O~y&Or7 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 G|Nj9%iy=<{ diff --git a/docs/images/nf-core-spatialtranscriptomics_logo_dark.png b/docs/images/nf-core-spatialtranscriptomics_logo_dark.png index bbc14562b249b945b7deb98f1185134f9dcec2dd..18665a3b84fb37e5d02a66b583bc73f3674e2025 100644 GIT binary patch literal 21637 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

    ") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_single") } ) } } @@ -63,7 +63,7 @@ nextflow_process { { assert path(process.out.html[0][1][0]).text.contains("") }, { assert path(process.out.html[0][1][1]).text.contains("") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_paired") } ) } } @@ -89,7 +89,7 @@ nextflow_process { { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_interleaved") } ) } } @@ -115,7 +115,7 @@ nextflow_process { { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_bam") } ) } } @@ -153,7 +153,7 @@ nextflow_process { { assert path(process.out.html[0][1][2]).text.contains("") }, { assert path(process.out.html[0][1][3]).text.contains("") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_multiple") } ) } } @@ -179,7 +179,7 @@ nextflow_process { { assert process.out.zip[0][1] ==~ ".*/mysample_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("") }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("fastqc_versions_custom_prefix") } ) } } @@ -204,7 +204,7 @@ nextflow_process { { 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() } + process.out.versions ).match("fastqc_stub") } ) } } diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap index 5d624bb..86f7c31 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -1,5 +1,17 @@ { - "sarscov2 single-end [fastq] - stub": { + "fastqc_versions_interleaved": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:40:07.293713" + }, + "fastqc_stub": { "content": [ [ "test.html", @@ -7,14 +19,70 @@ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], - "timestamp": "2024-01-17T18:40:57.254299" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:31:01.425198" + }, + "fastqc_versions_multiple": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:40:55.797907" + }, + "fastqc_versions_bam": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:40:26.795862" + }, + "fastqc_versions_single": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:39:27.043675" + }, + "fastqc_versions_paired": { + "content": [ + [ + "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:39:47.584191" }, - "versions": { + "fastqc_versions_custom_prefix": { "content": [ [ "versions.yml:md5,e1cc25ca8af856014824abd842e93978" ] ], - "timestamp": "2024-01-17T18:36:50.033627" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:41:14.576531" } } \ No newline at end of file From 42bb23cbabd573f51fd69fd5a17fc23e216d1972 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 2 Feb 2024 14:33:16 +0100 Subject: [PATCH 337/410] Update MultiQC module --- modules.json | 2 +- modules/nf-core/multiqc/tests/main.nf.test | 13 +++++---- .../nf-core/multiqc/tests/main.nf.test.snap | 28 ++++++++++++++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/modules.json b/modules.json index fc07cf9..c114074 100644 --- a/modules.json +++ b/modules.json @@ -17,7 +17,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "8ec825f465b9c17f9d83000022995b4f7de6fe93", + "git_sha": "9e71d8519dfbfc328c078bba14d4bd4c99e39a94", "installed_by": ["modules"] }, "spaceranger/count": { diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test index d0438ed..f1c4242 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_process { name "Test Process MULTIQC" script "../main.nf" process "MULTIQC" + tag "modules" tag "modules_nfcore" tag "multiqc" @@ -12,7 +13,7 @@ nextflow_process { when { process { """ - input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) input[1] = [] input[2] = [] input[3] = [] @@ -25,7 +26,7 @@ nextflow_process { { assert process.success }, { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("multiqc_versions_single") } ) } @@ -36,7 +37,7 @@ nextflow_process { when { process { """ - input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) 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] = [] @@ -49,7 +50,7 @@ nextflow_process { { assert process.success }, { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.versions).match("versions") } + { assert snapshot(process.out.versions).match("multiqc_versions_config") } ) } } @@ -61,7 +62,7 @@ nextflow_process { when { process { """ - input[0] = Channel.of([file(params.test_data['sarscov2']['illumina']['test_1_fastq_gz_fastqc_zip'], checkIfExists: true)]) + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) input[1] = [] input[2] = [] input[3] = [] @@ -75,7 +76,7 @@ nextflow_process { { assert snapshot(process.out.report.collect { file(it).getName() } + process.out.data.collect { file(it).getName() } + process.out.plots.collect { file(it).getName() } + - process.out.versions ).match() } + process.out.versions ).match("multiqc_stub") } ) } diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index d37e730..549ba79 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -1,13 +1,17 @@ { - "versions": { + "multiqc_versions_single": { "content": [ [ "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" ] ], - "timestamp": "2024-01-09T23:02:49.911994" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:43:40.529579" }, - "sarscov2 single-end [fastqc] - stub": { + "multiqc_stub": { "content": [ [ "multiqc_report.html", @@ -16,6 +20,22 @@ "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" ] ], - "timestamp": "2024-01-09T23:03:14.524346" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:45:09.605359" + }, + "multiqc_versions_config": { + "content": [ + [ + "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-01-31T17:44:53.535994" } } \ No newline at end of file From 12ca8836b63ba6a5f587bee4efe79e6520623e2e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 10:45:43 +0100 Subject: [PATCH 338/410] Update Conda environment and Dockerfile --- env/{reports => }/Dockerfile | 24 +++++++++++++++++++----- env/{reports => }/environment.yml | 3 ++- env/st_spatial_de/environment.yml | 13 ------------- 3 files changed, 21 insertions(+), 19 deletions(-) rename env/{reports => }/Dockerfile (53%) rename env/{reports => }/environment.yml (84%) delete mode 100644 env/st_spatial_de/environment.yml diff --git a/env/reports/Dockerfile b/env/Dockerfile similarity index 53% rename from env/reports/Dockerfile rename to env/Dockerfile index e1cb40e..ad89aec 100644 --- a/env/reports/Dockerfile +++ b/env/Dockerfile @@ -1,10 +1,24 @@ -# First stage: multi-platform Quarto image -FROM jdutant/quarto-minimal:1.3.313 as quarto +# +# First stage: Quarto installation +# +FROM ubuntu:20.04 as quarto +ARG QUARTO_VERSION=1.4.549 +ARG TARGETARCH +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && apt-get clean -# Second stage: multi-platform Mamba image -FROM condaforge/mambaforge:23.1.0-1 +RUN mkdir -p /opt/quarto \ + && curl -o quarto.tar.gz -L "https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-${TARGETARCH}.tar.gz" \ + && tar -zxvf quarto.tar.gz -C /opt/quarto/ --strip-components=1 \ + && rm quarto.tar.gz -# Copy Quarto installation from first stage and add to PATH +# +# Second stage: Conda environment +# +FROM condaforge/mambaforge:23.11.0-0 COPY --from=quarto /opt/quarto /opt/quarto ENV PATH="${PATH}:/opt/quarto/bin" diff --git a/env/reports/environment.yml b/env/environment.yml similarity index 84% rename from env/reports/environment.yml rename to env/environment.yml index f8f1923..45d6be9 100644 --- a/env/reports/environment.yml +++ b/env/environment.yml @@ -1,11 +1,12 @@ channels: - conda-forge - bioconda + - defaults dependencies: - jupyter=1.0.0 - leidenalg=0.9.1 - papermill=2.3.4 - pip=23.0.1 - - scanpy=1.9.6 + - scanpy=1.9.8 - pip: - SpatialDE==1.1.3 diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml deleted file mode 100644 index 28457d1..0000000 --- a/env/st_spatial_de/environment.yml +++ /dev/null @@ -1,13 +0,0 @@ -channels: - - conda-forge - - bioconda - - defaults -dependencies: - - quarto=1.3.353 - - jupyter=1.0.0 - - leidenalg=0.9.1 - - papermill=2.3.4 - - pip=23.0.1 - - scanpy=1.9.3 - - pip: - - SpatialDE==1.1.3 From 3ced22c695b05c014c88b67514765db15c67c674 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 12:43:25 +0100 Subject: [PATCH 339/410] Add QUARTONOTEBOOK nf-core module --- modules.json | 6 + modules/nf-core/quartonotebook/Dockerfile | 38 ++ .../nf-core/quartonotebook/environment.yml | 12 + modules/nf-core/quartonotebook/main.nf | 107 +++++ modules/nf-core/quartonotebook/meta.yml | 83 ++++ modules/nf-core/quartonotebook/parametrize.nf | 36 ++ .../quartonotebook/quartonotebook.diff | 14 + .../nf-core/quartonotebook/tests/main.nf.test | 212 +++++++++ .../quartonotebook/tests/main.nf.test.snap | 433 ++++++++++++++++++ .../tests/no-parametrization.config | 9 + modules/nf-core/quartonotebook/tests/tags.yml | 2 + .../tests/with-parametrization.config | 5 + 12 files changed, 957 insertions(+) create mode 100644 modules/nf-core/quartonotebook/Dockerfile create mode 100644 modules/nf-core/quartonotebook/environment.yml create mode 100644 modules/nf-core/quartonotebook/main.nf create mode 100644 modules/nf-core/quartonotebook/meta.yml create mode 100644 modules/nf-core/quartonotebook/parametrize.nf create mode 100644 modules/nf-core/quartonotebook/quartonotebook.diff create mode 100644 modules/nf-core/quartonotebook/tests/main.nf.test create mode 100644 modules/nf-core/quartonotebook/tests/main.nf.test.snap create mode 100644 modules/nf-core/quartonotebook/tests/no-parametrization.config create mode 100644 modules/nf-core/quartonotebook/tests/tags.yml create mode 100644 modules/nf-core/quartonotebook/tests/with-parametrization.config diff --git a/modules.json b/modules.json index c114074..87dcd88 100644 --- a/modules.json +++ b/modules.json @@ -20,6 +20,12 @@ "git_sha": "9e71d8519dfbfc328c078bba14d4bd4c99e39a94", "installed_by": ["modules"] }, + "quartonotebook": { + "branch": "master", + "git_sha": "07ecae35e5675ac4c1e2d84cf22021490f8b7947", + "installed_by": ["modules"], + "patch": "modules/nf-core/quartonotebook/quartonotebook.diff" + }, "spaceranger/count": { "branch": "master", "git_sha": "3bd057bfdfb64578636ff3ae7f7cb8eeab3c0cb6", diff --git a/modules/nf-core/quartonotebook/Dockerfile b/modules/nf-core/quartonotebook/Dockerfile new file mode 100644 index 0000000..0acc6f0 --- /dev/null +++ b/modules/nf-core/quartonotebook/Dockerfile @@ -0,0 +1,38 @@ +# +# First stage: Quarto installation +# +FROM ubuntu:20.04 as quarto +ARG QUARTO_VERSION=1.3.433 +ARG TARGETARCH +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && apt-get clean + +RUN mkdir -p /opt/quarto \ + && curl -o quarto.tar.gz -L "https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-${TARGETARCH}.tar.gz" \ + && tar -zxvf quarto.tar.gz -C /opt/quarto/ --strip-components=1 \ + && rm quarto.tar.gz + +# +# Second stage: Conda environment +# +FROM condaforge/mambaforge:23.11.0-0 +COPY --from=quarto /opt/quarto /opt/quarto +ENV PATH="${PATH}:/opt/quarto/bin" + +# Install packages using Mamba; also remove static libraries, python bytecode +# files and javascript source maps that are not required for execution +COPY environment.yml ./ +RUN mamba env update --name base --file environment.yml \ + && mamba clean --all --force-pkgs-dirs --yes \ + && find /opt/conda -follow -type f -name '*.a' -delete \ + && find /opt/conda -follow -type f -name '*.pyc' -delete \ + && find /opt/conda -follow -type f -name '*.js.map' -delete + +CMD /bin/bash + +LABEL \ + authors = "Erik Fasterius" \ + description = "Dockerfile for the quartonotebook nf-core module" diff --git a/modules/nf-core/quartonotebook/environment.yml b/modules/nf-core/quartonotebook/environment.yml new file mode 100644 index 0000000..1084ec0 --- /dev/null +++ b/modules/nf-core/quartonotebook/environment.yml @@ -0,0 +1,12 @@ +name: quartonotebook + +channels: + - conda-forge + - bioconda + - defaults + +dependencies: + - conda-forge::jupyter=1.0.0 + - conda-forge::matplotlib=3.4.3 + - conda-forge::papermill=2.4.0 + - conda-forge::r-rmarkdown=2.25 diff --git a/modules/nf-core/quartonotebook/main.nf b/modules/nf-core/quartonotebook/main.nf new file mode 100644 index 0000000..aca349e --- /dev/null +++ b/modules/nf-core/quartonotebook/main.nf @@ -0,0 +1,107 @@ +include { dumpParamsYaml; indentCodeBlock } from "./parametrize" + +process QUARTONOTEBOOK { + tag "$meta.id" + label 'process_low' + + // NB: You'll likely want to override this with a container containing all + // required dependencies for your analyses. You'll at least need Quarto + // itself, Papermill and whatever language you are running your analyses on; + // you can see an example in this module's Dockerfile. + container "docker.io/erikfas/spatialtranscriptomics" + + input: + tuple val(meta), path(notebook) + val parameters + path input_files + path extensions + + output: + tuple val(meta), path("*.html") , emit: html + tuple val(meta), path("${notebook}"), emit: notebook + tuple val(meta), path("artifacts/*"), emit: artifacts, optional: true + tuple val(meta), path("params.yml") , emit: params_yaml, optional: true + tuple val(meta), path("_extensions"), emit: extensions, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Exit if running this module with -profile conda / -profile mamba + // This is because of issues with getting a homogenous environment across + // both AMD64 and ARM64 architectures; please find more information at + // https://github.com/nf-core/modules/pull/4876#discussion_r1483541037. + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + exit 1, "The QUARTONOTEBOOK module does not support Conda/Mamba, please use Docker / Singularity / Podman instead." + } + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def parametrize = (task.ext.parametrize == null) ? true : task.ext.parametrize + def implicit_params = (task.ext.implicit_params == null) ? true : task.ext.implicit_params + def meta_params = (task.ext.meta_params == null) ? true : task.ext.meta_params + + // Dump parameters to yaml file. + // Using a YAML file over using the CLI params because + // - No issue with escaping + // - Allows passing nested maps instead of just single values + // - Allows running with the language-agnostic `--execute-params` + def params_cmd = "" + def render_args = "" + if (parametrize) { + nb_params = [:] + if (implicit_params) { + nb_params["cpus"] = task.cpus + nb_params["artifact_dir"] = "artifacts" + nb_params["input_dir"] = "./" + } + if (meta_params) { + nb_params["meta"] = meta + } + nb_params += parameters + params_cmd = dumpParamsYaml(nb_params) + render_args = "--execute-params params.yml" + } + """ + # Dump .params.yml heredoc (section will be empty if parametrization is disabled) + ${indentCodeBlock(params_cmd, 4)} + + # Create output directory + mkdir artifacts + + # Set environment variables needed for Quarto rendering + export XDG_CACHE_HOME="./.xdg_cache_home" + export XDG_DATA_HOME="./.xdg_data_home" + + # Set parallelism for BLAS/MKL etc. to avoid over-booking of resources + export MKL_NUM_THREADS="$task.cpus" + export OPENBLAS_NUM_THREADS="$task.cpus" + export OMP_NUM_THREADS="$task.cpus" + export NUMBA_NUM_THREADS="$task.cpus" + + # Render notebook + quarto render \\ + ${notebook} \\ + ${render_args} \\ + ${args} \\ + --output ${prefix}.html + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + quarto: \$(quarto -v) + papermill: \$(papermill --version | cut -f1 -d' ') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}.html + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + quarto: \$(quarto -v) + END_VERSIONS + """ +} diff --git a/modules/nf-core/quartonotebook/meta.yml b/modules/nf-core/quartonotebook/meta.yml new file mode 100644 index 0000000..5d95e8b --- /dev/null +++ b/modules/nf-core/quartonotebook/meta.yml @@ -0,0 +1,83 @@ +name: "quartonotebook" +description: Render a Quarto notebook, including parametrization. +keywords: + - quarto + - notebook + - reports + - python + - r +tools: + - quartonotebook: + description: An open-source scientific and technical publishing system. + homepage: https://quarto.org/ + documentation: https://quarto.org/docs/reference/ + tool_dev_url: https://github.com/quarto-dev/quarto-cli + licence: ["MIT"] + - papermill: + description: Parameterize, execute, and analyze notebooks + homepage: https://github.com/nteract/papermill + documentation: http://papermill.readthedocs.io/en/latest/ + tool_dev_url: https://github.com/nteract/papermill + licence: ["BSD 3-clause"] + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]`. + - notebook: + type: file + description: The Quarto notebook to be rendered. + pattern: "*.{qmd}" + - parameters: + type: map + description: | + Groovy map with notebook parameters which will be passed to Quarto to + generate parametrized reports. + - input_files: + type: file + description: One or multiple files serving as input data for the notebook. + pattern: "*" + - extensions: + type: file + description: | + A quarto `_extensions` directory with custom template(s) to be + available for rendering. + pattern: "*" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]`. + - html: + type: file + description: HTML report generated by Quarto. + pattern: "*.html" + - notebook: + type: file + description: The original, un-rendered notebook. + pattern: "*.[qmd,ipynb,rmd]" + - artifacts: + type: file + description: Artifacts generated during report rendering. + pattern: "*" + - params_yaml: + type: file + description: Parameters used during report rendering. + pattern: "*" + - extensions: + type: file + description: Quarto extensions used during report rendering. + pattern: "*" + - versions: + type: file + description: File containing software versions. + pattern: "versions.yml" + +authors: + - "@fasterius" +maintainers: + - "@fasterius" diff --git a/modules/nf-core/quartonotebook/parametrize.nf b/modules/nf-core/quartonotebook/parametrize.nf new file mode 100644 index 0000000..b3d8cea --- /dev/null +++ b/modules/nf-core/quartonotebook/parametrize.nf @@ -0,0 +1,36 @@ +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.DumperOptions + + +/** + * Multiline code blocks need to have the same indentation level + * as the `script:` section. This function re-indents code to the specified level. + */ +def indentCodeBlock(code, n_spaces) { + def indent_str = " ".multiply(n_spaces) + return code.stripIndent().split("\n").join("\n" + indent_str) +} + +/** + * Create a config YAML file from a groovy map + * + * @params task The process' `task` variable + * @returns a line to be inserted in the bash script. + */ +def dumpParamsYaml(params) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + def yaml = new Yaml(options) + def yaml_str = yaml.dump(params) + + // Writing the params.yml file directly as follows does not work. + // It only works in 'exec:', but not if there is a `script:` section: + // task.workDir.resolve('params.yml').text = yaml_str + + // Therefore, we inject it into the bash script: + return """\ + cat <<"END_PARAMS_SECTION" > ./params.yml + ${indentCodeBlock(yaml_str, 8)} + END_PARAMS_SECTION + """ +} diff --git a/modules/nf-core/quartonotebook/quartonotebook.diff b/modules/nf-core/quartonotebook/quartonotebook.diff new file mode 100644 index 0000000..29b5704 --- /dev/null +++ b/modules/nf-core/quartonotebook/quartonotebook.diff @@ -0,0 +1,14 @@ +Changes in module 'nf-core/quartonotebook' +--- modules/nf-core/quartonotebook/main.nf ++++ modules/nf-core/quartonotebook/main.nf +@@ -8,7 +8,7 @@ + // required dependencies for your analyses. You'll at least need Quarto + // itself, Papermill and whatever language you are running your analyses on; + // you can see an example in this module's Dockerfile. +- container "docker.io/erikfas/quartonotebook" ++ container "docker.io/erikfas/spatialtranscriptomics" + + input: + tuple val(meta), path(notebook) + +************************************************************ diff --git a/modules/nf-core/quartonotebook/tests/main.nf.test b/modules/nf-core/quartonotebook/tests/main.nf.test new file mode 100644 index 0000000..aeec8b1 --- /dev/null +++ b/modules/nf-core/quartonotebook/tests/main.nf.test @@ -0,0 +1,212 @@ +nextflow_process { + + name "Test Process QUARTONOTEBOOK" + script "../main.nf" + process "QUARTONOTEBOOK" + + tag "modules" + tag "modules_nfcore" + tag "quartonotebook" + + test("test notebook - [qmd:r]") { + + config "./no-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['quarto_r'], checkIfExists: true) // Notebook + ] + input[1] = [:] // Parameters + input[2] = [] // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() }, + ) + } + + } + + test("test notebook - [qmd:python]") { + + config "./no-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['quarto_python'], checkIfExists: true) // Notebook + ] + input[1] = [] // Parameters + input[2] = [] // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.artifacts, + process.out.params_yaml, + ).match() }, + { assert path(process.out.html[0][1]).readLines().any { it.contains('Hello world') } } + ) + } + + } + + test("test notebook - parametrized - [qmd:r]") { + + config "./with-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['quarto_r'], checkIfExists: true) // Notebook + ] + input[1] = [input_filename: "hello.txt", n_iter: 12] // parameters + input[2] = file(params.test_data['generic']['txt']['hello'], checkIfExists: true) // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() }, + ) + } + + } + + test("test notebook - parametrized - [qmd:python]") { + + config "./with-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['quarto_python'], checkIfExists: true) // Notebook + ] + input[1] = [input_filename: "hello.txt", n_iter: 12] // parameters + input[2] = file(params.test_data['generic']['txt']['hello'], checkIfExists: true) // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.artifacts, + process.out.params_yaml, + ).match() }, + { assert path(process.out.html[0][1]).readLines().any { it.contains('Hello world') } } + ) + } + + } + + test("test notebook - parametrized - [rmd]") { + + config "./with-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['rmarkdown'], checkIfExists: true) // notebook + ] + input[1] = [input_filename: "hello.txt", n_iter: 12] // parameters + input[2] = file(params.test_data['generic']['txt']['hello'], checkIfExists: true) // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() }, + ) + } + + } + + test("test notebook - parametrized - [ipynb]") { + + config "./with-parametrization.config" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['ipython_ipynb'], checkIfExists: true) // notebook + ] + input[1] = [input_filename: "hello.txt", n_iter: 12] // parameters + input[2] = file(params.test_data['generic']['txt']['hello'], checkIfExists: true) // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() }, + ) + } + + } + + test("test notebook - stub - [qmd:r]") { + + config "./no-parametrization.config" + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test' ], // meta map + file(params.test_data['generic']['notebooks']['quarto_r'], checkIfExists: true) // Notebook + ] + input[1] = [:] // Parameters + input[2] = [] // Input files + input[3] = [] // Extensions + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() }, + ) + } + + } + +} \ No newline at end of file diff --git a/modules/nf-core/quartonotebook/tests/main.nf.test.snap b/modules/nf-core/quartonotebook/tests/main.nf.test.snap new file mode 100644 index 0000000..f0f04cb --- /dev/null +++ b/modules/nf-core/quartonotebook/tests/main.nf.test.snap @@ -0,0 +1,433 @@ +{ + "test notebook - stub - [qmd:r]": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + "versions.yml:md5,93481281b24bb1b44ecc4387e0957a0e" + ], + "artifacts": [ + + ], + "extensions": [ + + ], + "html": [ + [ + { + "id": "test" + }, + "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "notebook": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "params_yaml": [ + + ], + "versions": [ + "versions.yml:md5,93481281b24bb1b44ecc4387e0957a0e" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T11:06:33.408525" + }, + "test notebook - [qmd:r]": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.html:md5,f09282296a5eee0154665975d842c07e" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + "artifacts": [ + + ], + "extensions": [ + + ], + "html": [ + [ + { + "id": "test" + }, + "test.html:md5,f09282296a5eee0154665975d842c07e" + ] + ], + "notebook": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "params_yaml": [ + + ], + "versions": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T11:05:50.985424" + }, + "test notebook - parametrized - [qmd:python]": { + "content": [ + [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + [ + [ + { + "id": "test" + }, + "artifact.txt:md5,8ddd8be4b179a529afa5f2ffae4b9858" + ] + ], + [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T09:21:18.194591" + }, + "test notebook - parametrized - [rmd]": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.html:md5,2b2026646ed8b59d49fdcbd54cb3a463" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "rmarkdown_notebook.Rmd:md5,1f5e4efbb41fd499b23c5bea2fc32e68" + ] + ], + "2": [ + [ + { + "id": "test" + }, + "artifact.txt:md5,b10a8db164e0754105b7a99be72e3fe5" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "4": [ + + ], + "5": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + "artifacts": [ + [ + { + "id": "test" + }, + "artifact.txt:md5,b10a8db164e0754105b7a99be72e3fe5" + ] + ], + "extensions": [ + + ], + "html": [ + [ + { + "id": "test" + }, + "test.html:md5,2b2026646ed8b59d49fdcbd54cb3a463" + ] + ], + "notebook": [ + [ + { + "id": "test" + }, + "rmarkdown_notebook.Rmd:md5,1f5e4efbb41fd499b23c5bea2fc32e68" + ] + ], + "params_yaml": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "versions": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T11:06:25.046249" + }, + "test notebook - parametrized - [ipynb]": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.html:md5,d7378ec0d1fd83b44424a68bf03a8fc3" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "ipython_notebook.ipynb:md5,02a206bf6c66396827dd310e7443926d" + ] + ], + "2": [ + + ], + "3": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "4": [ + + ], + "5": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + "artifacts": [ + + ], + "extensions": [ + + ], + "html": [ + [ + { + "id": "test" + }, + "test.html:md5,d7378ec0d1fd83b44424a68bf03a8fc3" + ] + ], + "notebook": [ + [ + { + "id": "test" + }, + "ipython_notebook.ipynb:md5,02a206bf6c66396827dd310e7443926d" + ] + ], + "params_yaml": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "versions": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T11:06:30.278412" + }, + "test notebook - [qmd:python]": { + "content": [ + [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + [ + + ], + [ + + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T09:21:00.324109" + }, + "test notebook - parametrized - [qmd:r]": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.html:md5,a25cdff28851a163d28669d4e62655af" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "2": [ + [ + { + "id": "test" + }, + "artifact.txt:md5,b10a8db164e0754105b7a99be72e3fe5" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "4": [ + + ], + "5": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ], + "artifacts": [ + [ + { + "id": "test" + }, + "artifact.txt:md5,b10a8db164e0754105b7a99be72e3fe5" + ] + ], + "extensions": [ + + ], + "html": [ + [ + { + "id": "test" + }, + "test.html:md5,a25cdff28851a163d28669d4e62655af" + ] + ], + "notebook": [ + [ + { + "id": "test" + }, + "quarto_r.qmd:md5,b3fa8b456efae62495c0b278a4f7694c" + ] + ], + "params_yaml": [ + [ + { + "id": "test" + }, + "params.yml:md5,efd62bc975f429e8749ba787a93042dd" + ] + ], + "versions": [ + "versions.yml:md5,55e1f767fbd72aae14cbbfb638e38a90" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-09T11:06:08.013103" + } +} \ No newline at end of file diff --git a/modules/nf-core/quartonotebook/tests/no-parametrization.config b/modules/nf-core/quartonotebook/tests/no-parametrization.config new file mode 100644 index 0000000..f686514 --- /dev/null +++ b/modules/nf-core/quartonotebook/tests/no-parametrization.config @@ -0,0 +1,9 @@ +profiles { + docker { + docker.runOptions = '-u $(id -u):$(id -g)' + } +} + +process { + ext.parametrize = false +} diff --git a/modules/nf-core/quartonotebook/tests/tags.yml b/modules/nf-core/quartonotebook/tests/tags.yml new file mode 100644 index 0000000..638b0ce --- /dev/null +++ b/modules/nf-core/quartonotebook/tests/tags.yml @@ -0,0 +1,2 @@ +quartonotebook: + - "modules/nf-core/quartonotebook/**" diff --git a/modules/nf-core/quartonotebook/tests/with-parametrization.config b/modules/nf-core/quartonotebook/tests/with-parametrization.config new file mode 100644 index 0000000..ab7df66 --- /dev/null +++ b/modules/nf-core/quartonotebook/tests/with-parametrization.config @@ -0,0 +1,5 @@ +profiles { + docker { + docker.runOptions = '-u $(id -u):$(id -g)' + } +} From 2e595b7ce5e6edce3b017ac6bba6e446b3470a2c Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 12:43:42 +0100 Subject: [PATCH 340/410] Use QUARTONOTEBOOK module in pipeline --- bin/st_clustering.qmd | 4 +- bin/st_quality_controls.qmd | 4 +- bin/st_spatial_de.qmd | 8 ++- subworkflows/local/st_downstream.nf | 104 +++++++++++++++++++++------- 4 files changed, 92 insertions(+), 28 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index be8e496..47bebfe 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -15,6 +15,7 @@ jupyter: python3 input_adata_filtered = "st_adata_filtered.h5ad" # Name of the input anndata file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses +artifact_dir = "artifacts" output_adata_processed = "st_adata_processed.h5ad" # Name of the output anndata file ``` @@ -23,6 +24,7 @@ saved in the AnnData format: ```{python} #| warning: false +import os import scanpy as sc import numpy as np import pandas as pd @@ -141,5 +143,5 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false -st_adata.write(output_adata_processed) +st_adata.write(os.path.join(artifact_dir, output_adata_processed)) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index a841d8f..06d6dc3 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -32,10 +32,12 @@ min_spots = 1 # Min spots per gene mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) hb_threshold = 100 # content threshold (%) +artifact_dir = "artifacts" output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file ``` ```{python} +import os import scanpy as sc import scipy import pandas as pd @@ -254,5 +256,5 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ```{python} #| echo: false # Write filtered data to disk -st_adata.write(output_adata_filtered) +st_adata.write(os.path.join(artifact_dir, output_adata_filtered)) ``` diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd index 0e1e211..600d588 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_spatial_de.qmd @@ -10,11 +10,13 @@ jupyter: python3 #| tags: [parameters] #| echo: false input_adata_processed = "st_adata_processed.h5ad" -output_spatial_degs = "st_spatial_de.csv" n_top_spatial_degs = 14 +artifact_dir = "artifacts/" +output_spatial_degs = "st_spatial_de.csv" ``` ```{python} +import os import scanpy as sc import pandas as pd import SpatialDE @@ -84,8 +86,10 @@ st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, : # Print results table results_tab = st_adata.var.sort_values("qval", ascending=True) -results_tab.to_csv(output_spatial_degs) results_tab.head(n_top_spatial_degs) + +# Write results to file +results_tab.to_csv(os.path.join(artifact_dir, output_spatial_degs)) ``` We can also plot the top spatially variable genes on top of the tissue image diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 91b5e74..9d2b5cb 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -2,9 +2,9 @@ // Subworkflow for downstream analyses of ST data // -include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' -include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' -include { ST_CLUSTERING } from '../../modules/local/st_clustering' +include { QUARTONOTEBOOK as ST_QUALITY_CONTROLS } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as ST_SPATIAL_DE } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as ST_CLUSTERING } from '../../modules/nf-core/quartonotebook/main' workflow ST_DOWNSTREAM { @@ -16,52 +16,108 @@ workflow ST_DOWNSTREAM { ch_versions = Channel.empty() // - // Report files + // Quarto reports and extension files // - report_quality_controls = file("${projectDir}/bin/st_quality_controls.qmd") - report_clustering = file("${projectDir}/bin/st_clustering.qmd") - report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") - report_template = Channel.fromPath("${projectDir}/assets/_extensions").collect() + quality_controls_file = file("${projectDir}/bin/st_quality_controls.qmd", checkIfExists: true) + clustering_file = file("${projectDir}/bin/st_clustering.qmd", checkIfExists: true) + spatial_de_file = file("${projectDir}/bin/st_spatial_de.qmd", checkIfExists: true) + extensions = Channel.fromPath("${projectDir}/assets/_extensions").collect() + + // input: + // tuple val(meta), path(notebook) + // val parameters + // path input_files + // path extensions + + // output: + // tuple val(meta), path("*.html") , emit: html + // tuple val(meta), path("${notebook}"), emit: notebook + // tuple val(meta), path("artifacts/*"), emit: artifacts, optional: true + // tuple val(meta), path("params.yml") , emit: params_yaml, optional: true + // tuple val(meta), path("_extensions"), emit: extensions, optional: true + // path "versions.yml" , emit: versions // // Quality controls and filtering // + ch_quality_controls_input_data = st_adata_raw + .map { it -> it[1] } + ch_quality_controls_notebook = st_adata_raw + .map { tuple(it[0], quality_controls_file) } + quality_controls_params = [ + input_raw_data: "st_adata_raw.h5ad", + min_counts: params.st_qc_min_counts, + min_genes: params.st_qc_min_genes, + min_spots: params.st_qc_min_spots, + mito_threshold: params.st_qc_mito_threshold, + ribo_threshold: params.st_qc_ribo_threshold, + hb_threshold: params.st_qc_hb_threshold, + output_adata_filtered: "st_adata_filtered.h5ad" + ] ST_QUALITY_CONTROLS ( - report_quality_controls, - report_template, - st_adata_raw + ch_quality_controls_notebook, + quality_controls_params, + ch_quality_controls_input_data, + extensions ) ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) // // Normalisation, dimensionality reduction and clustering // + ch_clustering_input_data = ST_QUALITY_CONTROLS.out.artifacts + .map { it -> it[1] } + ch_clustering_notebook = ST_QUALITY_CONTROLS.out.artifacts + .map { tuple(it[0], clustering_file) } + clustering_params = [ + input_adata_filtered: "st_adata_filtered.h5ad", + cluster_resolution: params.st_cluster_resolution, + n_hvgs: params.st_cluster_n_hvgs, + output_adata_processed: "st_adata_processed.h5ad" + ] ST_CLUSTERING ( - report_clustering, - report_template, - ST_QUALITY_CONTROLS.out.st_adata_filtered + ch_clustering_notebook, + clustering_params, + ch_clustering_input_data, + extensions ) ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) // // Spatial differential expression // + ch_spatial_de_input_data = ST_CLUSTERING.out.artifacts + .map { it -> it[1] } + ch_spatial_de_notebook = ST_CLUSTERING.out.artifacts + .map { tuple(it[0], spatial_de_file) } + spatial_de_params = [ + input_adata_processed: "st_adata_processed.h5ad", + n_top_spatial_degs: params.st_n_top_spatial_degs, + output_spatial_degs: "st_spatial_de.csv" + ] ST_SPATIAL_DE ( - report_spatial_de, - report_template, - ST_CLUSTERING.out.st_adata_processed + ch_spatial_de_notebook, + spatial_de_params, + ch_spatial_de_input_data, + extensions ) ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) emit: - st_data_norm = ST_QUALITY_CONTROLS.out.st_adata_filtered // channel: [ meta, h5ad ] - html = ST_QUALITY_CONTROLS.out.html // channel: [ html ] + st_qc_html = ST_QUALITY_CONTROLS.out.html // channel: [ meta, html ] + st_adata_filtered = ST_QUALITY_CONTROLS.out.artifacts // channel: [ meta, h5ad ] + st_qc_notebook = ST_QUALITY_CONTROLS.out.notebook // channel: [ meta, qmd ] + st_qc_params = ST_QUALITY_CONTROLS.out.params_yaml // channel: [ meta, yml ] - st_adata_processed = ST_CLUSTERING.out.st_adata_processed // channel: [ meta, h5ad] - html = ST_CLUSTERING.out.html // channel: [ html ] + st_clustering_html = ST_CLUSTERING.out.html // channel: [ html ] + st_adata_processed = ST_CLUSTERING.out.artifacts // channel: [ meta, h5ad] + st_clustering_notebook = ST_CLUSTERING.out.notebook // channel: [ meta, qmd ] + st_clustering_params = ST_CLUSTERING.out.params_yaml // channel: [ meta, yml ] - degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] - html = ST_SPATIAL_DE.out.html // channel: [ html ] + st_spatial_html = ST_SPATIAL_DE.out.html // channel: [ meta, html ] + st_degs = ST_SPATIAL_DE.out.artifacts // channel: [ meta, csv ] + st_spatial_notebook = ST_SPATIAL_DE.out.notebook // channel: [ meta, qmd ] + st_spatial_params = ST_SPATIAL_DE.out.params_yaml // channel: [ meta, yml ] - versions = ch_versions // channel: [ versions.yml ] + versions = ch_versions // channel: [ versions.yml ] } From 508c0b25af38d2dfc46f9db4b87e575f3f7b9ed5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 12:44:22 +0100 Subject: [PATCH 341/410] Remove old local modules --- modules/local/st_clustering.nf | 49 ------------------------- modules/local/st_quality_controls.nf | 53 ---------------------------- modules/local/st_spatial_de.nf | 49 ------------------------- 3 files changed, 151 deletions(-) delete mode 100644 modules/local/st_clustering.nf delete mode 100644 modules/local/st_quality_controls.nf delete mode 100644 modules/local/st_spatial_de.nf diff --git a/modules/local/st_clustering.nf b/modules/local/st_clustering.nf deleted file mode 100644 index 0819c7a..0000000 --- a/modules/local/st_clustering.nf +++ /dev/null @@ -1,49 +0,0 @@ -// -// Dimensionality reduction and clustering -// -process ST_CLUSTERING { - - // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 - - tag "${meta.id}" - label 'process_low' - - conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0 conda-forge::leidenalg=0.9.1" - container "docker.io/erikfas/spatialtranscriptomics" - - // Exit if running this module with -profile conda / -profile mamba on ARM64 - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - architecture = System.getProperty("os.arch") - if (architecture == "arm64" || architecture == "aarch64") { - exit 1, "The ST_CLUSTERING module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." - } - } - - input: - path(report) - path(report_template) - tuple val(meta), path(st_adata_filtered) - - output: - tuple val(meta), path("st_adata_processed.h5ad"), emit: st_adata_processed - tuple val(meta), path("st_clustering.html") , emit: html - path("versions.yml") , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - quarto render ${report} \ - -P input_adata_filtered:${st_adata_filtered} \ - -P cluster_resolution:${params.st_cluster_resolution} \ - -P n_hvgs:${params.st_cluster_n_hvgs} \ - -P output_adata_processed:st_adata_processed.h5ad - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - quarto: \$(quarto -v) - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - END_VERSIONS - """ -} diff --git a/modules/local/st_quality_controls.nf b/modules/local/st_quality_controls.nf deleted file mode 100644 index f52b6bb..0000000 --- a/modules/local/st_quality_controls.nf +++ /dev/null @@ -1,53 +0,0 @@ -// -// Quality controls and filtering -// -process ST_QUALITY_CONTROLS { - - // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 - - tag "${meta.id}" - label 'process_low' - - conda "conda-forge::quarto=1.3.353 conda-forge::scanpy=1.9.3 conda-forge::papermill=2.3.4 conda-forge::jupyter=1.0.0" - container "docker.io/erikfas/spatialtranscriptomics" - - // Exit if running this module with -profile conda / -profile mamba on ARM64 - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - architecture = System.getProperty("os.arch") - if (architecture == "arm64" || architecture == "aarch64") { - exit 1, "The ST_QUALITY_CONTROLS module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." - } - } - - input: - path(report) - path(report_template) - tuple val(meta), path(st_adata_raw) - - output: - tuple val(meta), path("st_adata_filtered.h5ad") , emit: st_adata_filtered - tuple val(meta), path("st_quality_controls.html"), emit: html - path("versions.yml") , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - quarto render ${report} \ - -P input_adata_raw:${st_adata_raw} \ - -P min_counts:${params.st_qc_min_counts} \ - -P min_genes:${params.st_qc_min_genes} \ - -P min_spots:${params.st_qc_min_spots} \ - -P mito_threshold:${params.st_qc_mito_threshold} \ - -P ribo_threshold:${params.st_qc_ribo_threshold} \ - -P hb_threshold:${params.st_qc_hb_threshold} \ - -P output_adata_filtered:st_adata_filtered.h5ad - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - quarto: \$(quarto -v) - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - END_VERSIONS - """ -} diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_spatial_de.nf deleted file mode 100644 index 249fda1..0000000 --- a/modules/local/st_spatial_de.nf +++ /dev/null @@ -1,49 +0,0 @@ -// -// Spatial differential expression -// -process ST_SPATIAL_DE { - - // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 - - tag "${meta.id}" - label 'process_medium' - - conda "env/st_spatial_de/environment.yml" - container "docker.io/erikfas/spatialtranscriptomics" - - // Exit if running this module with -profile conda / -profile mamba on ARM64 - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - architecture = System.getProperty("os.arch") - if (architecture == "arm64" || architecture == "aarch64") { - exit 1, "The ST_SPATIAL_DE module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." - } - } - input: - path(report) - path(report_template) - tuple val(meta), path(st_adata_processed) - - output: - tuple val(meta), path("*.csv") , emit: degs - tuple val(meta), path("st_spatial_de.html"), emit: html - path("versions.yml") , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - quarto render ${report} \ - -P input_adata_processed:${st_adata_processed} \ - -P n_top_spatial_degs:${params.st_n_top_spatial_degs} \ - -P output_spatial_degs:st_spatial_de.csv - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - quarto: \$(quarto -v) - leidenalg: \$(python -c "import leidenalg; print(leidenalg.version)") - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - SpatialDE: \$(python -c "from importlib.metadata import version; print(version('SpatialDE'))") - END_VERSIONS - """ -} From 393bc3cdbff97a0b35e851fccec3480e0c40aae9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 12:56:46 +0100 Subject: [PATCH 342/410] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3eb02..07df750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ compatible with further downstream analyses and/or exploration in _e.g._ ### `Added` +- Use the QUARTONOTEBOOK nf-core module instead of local Quarto-based modules [[#68](https://github.com/nf-core/spatialtranscriptomics/pull/68)] - Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialtranscriptomics/pull/64)] - Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) - Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] From 64d332761b2cc678159bb966b2f2eabb20654981 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 16:21:29 +0100 Subject: [PATCH 343/410] Remove XDG paths, which are now in QUARTONOTEBOOK --- conf/modules.config | 6 ------ 1 file changed, 6 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 30a52b1..ceb78b1 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -10,12 +10,6 @@ ---------------------------------------------------------------------------------------- */ -// Environment specification needed for Quarto -env { - XDG_CACHE_HOME = "./.xdg_cache_home" - XDG_DATA_HOME = "./.xdg_data_home" -} - process { publishDir = [ From fe7a7dce3d14dfd0c4c6795879787d1ac2053f0a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 16:21:58 +0100 Subject: [PATCH 344/410] Lower test thresholds --- conf/test.config | 4 ++-- conf/test_downstream.config | 4 ++-- conf/test_spaceranger_v1.config | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conf/test.config b/conf/test.config index ce4909c..6bc2be8 100644 --- a/conf/test.config +++ b/conf/test.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + st_qc_min_counts = 1 + st_qc_min_genes = 1 outdir = 'results' } diff --git a/conf/test_downstream.config b/conf/test_downstream.config index 89a56b2..d8f951f 100644 --- a/conf/test_downstream.config +++ b/conf/test_downstream.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + st_qc_min_counts = 1 + st_qc_min_genes = 1 outdir = 'results' } diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index 2fca10e..b08d492 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + st_qc_min_counts = 1 + st_qc_min_genes = 1 outdir = 'results' } From 548bc6778c2367100c308669182e9e50af7caa2d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 16:29:27 +0100 Subject: [PATCH 345/410] Revert "Lower test thresholds" This reverts commit fe7a7dce3d14dfd0c4c6795879787d1ac2053f0a. Lowering the test thresholds was part of troubleshooting the spatial DE module, but did solve the issue. --- conf/test.config | 4 ++-- conf/test_downstream.config | 4 ++-- conf/test_spaceranger_v1.config | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conf/test.config b/conf/test.config index 6bc2be8..ce4909c 100644 --- a/conf/test.config +++ b/conf/test.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 1 - st_qc_min_genes = 1 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_downstream.config b/conf/test_downstream.config index d8f951f..89a56b2 100644 --- a/conf/test_downstream.config +++ b/conf/test_downstream.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 1 - st_qc_min_genes = 1 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index b08d492..2fca10e 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 1 - st_qc_min_genes = 1 + st_qc_min_counts = 5 + st_qc_min_genes = 3 outdir = 'results' } From 66e32bca56a81583a5542f5d8c9600a8af3f3081 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 16:39:08 +0100 Subject: [PATCH 346/410] Fix report module output names and paths --- conf/modules.config | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index ceb78b1..c3a628e 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -63,16 +63,24 @@ process { } withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SPATIAL_DE' { + ext.prefix = { "${notebook.baseName}" } publishDir = [ [ path: { "${params.outdir}/${meta.id}/reports" }, mode: params.publish_dir_mode, - pattern: "*{.html,_files}" + pattern: "*{.html,.qmd,_extensions}" + ], + [ + path: { "${params.outdir}/${meta.id}/reports" }, + mode: params.publish_dir_mode, + pattern: "params.yml", + saveAs: { "${notebook.baseName}.yml" } ], [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "st_adata_processed.h5ad" + pattern: "artifacts/st_adata_processed.h5ad", + saveAs: { "st_adata_processed.h5ad" } ], [ path: { "${params.outdir}/${meta.id}/degs" }, From 522639895b791a3103a7a4d135c5fbd2125fd01d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 12 Feb 2024 16:42:02 +0100 Subject: [PATCH 347/410] Update Conda environment --- env/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/env/environment.yml b/env/environment.yml index 45d6be9..6e06a99 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -8,5 +8,6 @@ dependencies: - papermill=2.3.4 - pip=23.0.1 - scanpy=1.9.8 + - seaborn=0.12.2 - pip: - SpatialDE==1.1.3 From 91ca58b582629741fcdfd76c69fceadbc6c896cf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 1 Mar 2024 10:44:24 +0100 Subject: [PATCH 348/410] Remove redundant environment specification --- env/{reports => }/Dockerfile | 0 env/{reports => }/environment.yml | 0 env/st_spatial_de/environment.yml | 13 ------------- 3 files changed, 13 deletions(-) rename env/{reports => }/Dockerfile (100%) rename env/{reports => }/environment.yml (100%) delete mode 100644 env/st_spatial_de/environment.yml diff --git a/env/reports/Dockerfile b/env/Dockerfile similarity index 100% rename from env/reports/Dockerfile rename to env/Dockerfile diff --git a/env/reports/environment.yml b/env/environment.yml similarity index 100% rename from env/reports/environment.yml rename to env/environment.yml diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml deleted file mode 100644 index 28457d1..0000000 --- a/env/st_spatial_de/environment.yml +++ /dev/null @@ -1,13 +0,0 @@ -channels: - - conda-forge - - bioconda - - defaults -dependencies: - - quarto=1.3.353 - - jupyter=1.0.0 - - leidenalg=0.9.1 - - papermill=2.3.4 - - pip=23.0.1 - - scanpy=1.9.3 - - pip: - - SpatialDE==1.1.3 From 9539d6e80c8276d81019dddc1a72754b16e60ff4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 1 Mar 2024 10:50:35 +0100 Subject: [PATCH 349/410] Update MultiQC module --- modules.json | 2 +- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 4 ++-- modules/nf-core/multiqc/tests/main.nf.test.snap | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules.json b/modules.json index c114074..39ea71b 100644 --- a/modules.json +++ b/modules.json @@ -17,7 +17,7 @@ }, "multiqc": { "branch": "master", - "git_sha": "9e71d8519dfbfc328c078bba14d4bd4c99e39a94", + "git_sha": "b7ebe95761cd389603f9cc0e0dc384c0f663815a", "installed_by": ["modules"] }, "spaceranger/count": { diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index 7625b75..ca39fb6 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -4,4 +4,4 @@ channels: - bioconda - defaults dependencies: - - bioconda::multiqc=1.19 + - bioconda::multiqc=1.21 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 1b9f7c4..47ac352 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.19--pyhdfd78af_0' : - 'biocontainers/multiqc:1.19--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.21--pyhdfd78af_0' : + 'biocontainers/multiqc:1.21--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 549ba79..bfebd80 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,14e9a2661241abd828f4f06a7b5c222d" + "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" ] ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-01-31T17:43:40.529579" + "timestamp": "2024-02-29T08:48:55.657331" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" + "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" ] ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-01-31T17:45:09.605359" + "timestamp": "2024-02-29T08:49:49.071937" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,14e9a2661241abd828f4f06a7b5c222d" + "versions.yml:md5,21f35ee29416b9b3073c28733efe4b7d" ] ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-01-31T17:44:53.535994" + "timestamp": "2024-02-29T08:49:25.457567" } } \ No newline at end of file From 1a42abfd81eb6d542cf89684862525fca795f756 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 1 Mar 2024 11:08:48 +0100 Subject: [PATCH 350/410] Use Space Ranger output for MultiQC --- workflows/spatialtranscriptomics.nf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 37047b9..1afd25d 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -149,7 +149,8 @@ workflow ST { ch_multiqc_files = ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml').mix( ch_methods_description.collectFile(name: 'methods_description_mqc.yaml'), CUSTOM_DUMPSOFTWAREVERSIONS.out.mqc_yml.collect(), - FASTQC.out.zip.collect{ meta, qcfile -> qcfile } + FASTQC.out.zip.collect{ meta, qcfile -> qcfile }, + SPACERANGER.out.sr_dir.collect{ meta, sr_dir -> sr_dir }, ) MULTIQC ( From a09571330f5d4ecd8bdb313a14a5c5e1ff01b0f2 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 1 Mar 2024 11:17:24 +0100 Subject: [PATCH 351/410] Add missing MultiQC output documentation --- docs/output.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/output.md b/docs/output.md index 23a88d8..b400143 100644 --- a/docs/output.md +++ b/docs/output.md @@ -122,6 +122,9 @@ details in the report itself. - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. - Parameters used by the pipeline run: `params.json`. +- `multiqc/` + - Report generated by MultiQC: `multiqc_report.html`. + - Data and plots generated by MultiQC: `multiqc_data/` and `multiqc_plots/`. From f8c28632e14291920d9d2d9aef23356b15b632de Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 1 Mar 2024 11:19:43 +0100 Subject: [PATCH 352/410] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3eb02..56cc753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ compatible with further downstream analyses and/or exploration in _e.g._ ### `Added` +- Add MultiQC support for Space Ranger outputs [[#70](https://github.com/nf-core/spatialtranscriptomics/pull/70)] - Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialtranscriptomics/pull/64)] - Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) - Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] From 01eefccda58e4dde25497e61aa0ee35c833acc0d Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Mon, 4 Mar 2024 15:39:54 +0100 Subject: [PATCH 353/410] Fix tables writing in SpatialData objects --- bin/st_clustering.qmd | 5 ++--- bin/st_quality_controls.qmd | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index eef9dfd..5c48926 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -171,8 +171,7 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false st_adata.write(output_adata_processed) -``` - -```{python} +del st_sdata.table +st_sdata.table = st_adata st_sdata.write("./" + output_sdata) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index ce3f0ce..0a23c03 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -279,6 +279,8 @@ sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ``` ```{python} +del st_sdata.table +st_sdata.table = st_adata st_sdata.write("./" + output_sdata) ``` From d1bf3ecf5294dfe539853222a0e3693f4637e4b6 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Mon, 4 Mar 2024 15:40:19 +0100 Subject: [PATCH 354/410] Fix quarto formatting --- bin/st_quality_controls.qmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 0a23c03..5ab24d9 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -1,6 +1,8 @@ --- title: "nf-core/spatialtranscriptomics" subtitle: "Pre-processing and quality controls" +format: + nf-core-html: default jupyter: python3 --- From c606f176c3eeec3cccaef4fda7b6683c2d5599ff Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Mon, 4 Mar 2024 15:40:50 +0100 Subject: [PATCH 355/410] Move from SpatialDE to Squidpy for SVG computation --- bin/{st_spatial_de.qmd => st_svg.qmd} | 60 +++++++++++-------- docs/output.md | 8 +-- env/reports/environment.yml | 1 + env/st_spatial_de/environment.yml | 19 ------ modules/local/{st_spatial_de.nf => st_svg.nf} | 18 +++--- subworkflows/local/st_downstream.nf | 15 +++-- tests/pipeline/test_downstream.nf.test | 8 +-- 7 files changed, 61 insertions(+), 68 deletions(-) rename bin/{st_spatial_de.qmd => st_svg.qmd} (68%) delete mode 100644 env/st_spatial_de/environment.yml rename modules/local/{st_spatial_de.nf => st_svg.nf} (64%) diff --git a/bin/st_spatial_de.qmd b/bin/st_svg.qmd similarity index 68% rename from bin/st_spatial_de.qmd rename to bin/st_svg.qmd index 44da8ef..1a648ff 100644 --- a/bin/st_spatial_de.qmd +++ b/bin/st_svg.qmd @@ -1,6 +1,6 @@ --- title: "nf-core/spatialtranscriptomics" -subtitle: "Differential gene expression" +subtitle: "Neighborhood enrichment analysis and Spatially variable genes" format: nf-core-html: default jupyter: python3 @@ -10,14 +10,16 @@ jupyter: python3 #| tags: [parameters] #| echo: false input_sdata = "st_sdata.zarr" -output_spatial_degs = "st_spatial_de.csv" +output_adata_svg = "st_adata_svg.h5ad" # Name of the output anndata file +output_sdata = "st_sdata_svg.zarr" # Name of the input anndata file +output_spatial_degs = "st_svg.csv" n_top_spatial_degs = 14 ``` ```{python} import scanpy as sc import pandas as pd -import SpatialDE +import squidpy as sq import numpy as np import spatialdata from anndata import AnnData @@ -88,40 +90,46 @@ height and width depends on the number of clusters as well as the number and intersection of the DEGs that are being plotted. ::: -# Spatial gene expression +# Neighborhood enrichment analysis -Spatial transcriptomics data can give insight into how genes are expressed in -different areas in a tissue, allowing identification of spatial gene expression -patterns. Here we use [SpatialDE](https://www.nature.com/articles/nmeth.4636) to -identify such patterns. +We can perform a neighborhood enrichment analysis to find out which +genes are enriched in the neighborhood of each cluster: ```{python} -#| output: false -results = SpatialDE.run(st_adata.obsm["spatial"], st_adata.to_df()) +sq.gr.spatial_neighbors(st_adata, coord_type="generic") +sq.gr.nhood_enrichment(st_adata, cluster_key="clusters") +sq.pl.nhood_enrichment(st_adata, cluster_key="clusters", method="ward", vmin=-100, vmax=100) ``` -We can then inspect the spatial DEGs in a table: +We visualize the interaction matrix between the different clusters: ```{python} -results.set_index("g", inplace=True) -# workaround for https://github.com/Teichlab/SpatialDE/issues/36 -results = results.loc[~results.index.duplicated(keep="first")] +sq.gr.interaction_matrix(st_adata, cluster_key="clusters") +sq.pl.interaction_matrix(st_adata, cluster_key="clusters", method="ward", vmax=20000) +``` -# Add annotations -st_adata.var = pd.concat([st_adata.var, results.loc[st_adata.var.index.values, :]], axis=1) +# Spatially variable genes with spatial autocorrelation statistics + +Spatial transcriptomics data can give insight into how genes are expressed in +different areas in a tissue, allowing identification of spatial gene expression +patterns. Here we use [Moran's I](https://en.wikipedia.org/wiki/Moran%27s_I) autocorrelation score to identify such patterns. -# Print results table -results_tab = st_adata.var.sort_values("qval", ascending=True) -results_tab.to_csv(output_spatial_degs) -results_tab.head(n_top_spatial_degs) +```{python} +st_adata.var_names_make_unique() +sq.gr.spatial_autocorr(st_adata, mode="moran") +st_adata.uns["moranI"].head(n_top_spatial_degs) +#[TODO] add gearyC as optional mode ``` -We can also plot the top spatially variable genes on top of the tissue image -itself to visualize the patterns: ```{python} -symbols = results_tab.iloc[: n_top_spatial_degs]["gene_symbol"] -plt.rcParams["figure.figsize"] = (3.5, 4) -sc.pl.spatial(st_adata, img_key="hires", color=symbols.index, alpha=0.7, - ncols=2, title=symbols, size=1.25) +#| echo: false +st_adata.write(output_adata_svg) +del st_sdata.table +st_sdata.table = st_adata +st_sdata.write("./" + output_sdata) ``` + +```{python} +st_adata.uns["moranI"].to_csv(output_spatial_degs) +``` \ No newline at end of file diff --git a/docs/output.md b/docs/output.md index 23a88d8..76cd4df 100644 --- a/docs/output.md +++ b/docs/output.md @@ -99,15 +99,15 @@ option; you can find more details in the report itself. Output files - `/reports/` - - `st_spatial_de.html`: HTML report. + - `st_svg.html`: HTML report. - `/degs/` - - `st_spatial_de.csv`: List of spatially differentially expressed genes. + - `st_svg.csv`: List of spatially variable genes. Report containing analyses related to differential expression testing and -spatially varying genes. The [SpatialDE](https://github.com/Teichlab/SpatialDE) -package is currently the only option for spatial testing; you can find more +spatially varying genes. The [Moran 1](https://en.wikipedia.org/wiki/Moran%27s_I) +score is currently the only option for spatial testing; you can find more details in the report itself. ## Workflow reporting diff --git a/env/reports/environment.yml b/env/reports/environment.yml index b7da2be..386e5ce 100644 --- a/env/reports/environment.yml +++ b/env/reports/environment.yml @@ -13,6 +13,7 @@ dependencies: - imagecodecs=2024.1.1 - pip: - scanpy==1.9.8 + - squidpy==1.4.1 - spatialdata==0.0.15 - spatialdata-io==0.0.9 - spatialdata-plot==0.1.0 diff --git a/env/st_spatial_de/environment.yml b/env/st_spatial_de/environment.yml deleted file mode 100644 index 984e420..0000000 --- a/env/st_spatial_de/environment.yml +++ /dev/null @@ -1,19 +0,0 @@ -channels: - - conda-forge - - bioconda -dependencies: - - python=3.10 - - jupyter=1.0.0 - - leidenalg=0.9.1 - - papermill=2.3.4 - - pip=23.0.1 - - gcc=13.2.0 - - libgdal=3.8.3 - - gxx=13.2.0 - - imagecodecs=2024.1.1 - - pip: - - scanpy==1.9.8 - - SpatialDE==1.1.3 - - spatialdata==0.0.15 - - spatialdata-io==0.0.9 - - spatialdata-plot==0.1.0 diff --git a/modules/local/st_spatial_de.nf b/modules/local/st_svg.nf similarity index 64% rename from modules/local/st_spatial_de.nf rename to modules/local/st_svg.nf index c02c0e0..41c75f0 100644 --- a/modules/local/st_spatial_de.nf +++ b/modules/local/st_svg.nf @@ -1,21 +1,21 @@ // // Spatial differential expression // -process ST_SPATIAL_DE { +process ST_SVG { // TODO: Update Conda directive when Quarto/Pandoc works on ARM64 tag "${meta.id}" label 'process_medium' - conda "env/st_spatial_de/environment.yml" + conda "env/ST_SVG/environment.yml" container "docker.io/cavenel/spatialtranscriptomics" // Exit if running this module with -profile conda / -profile mamba on ARM64 if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { architecture = System.getProperty("os.arch") if (architecture == "arm64" || architecture == "aarch64") { - exit 1, "The ST_SPATIAL_DE module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." + exit 1, "The ST_SVG module does not support Conda on ARM64. Please use Docker / Singularity / Podman instead." } } input: @@ -25,8 +25,10 @@ process ST_SPATIAL_DE { output: tuple val(meta), path("*.csv") , emit: degs - tuple val(meta), path("st_spatial_de.html"), emit: html - path("versions.yml") , emit: versions + tuple val(meta), path("st_adata_svg.h5ad"), emit: st_adata_svg + tuple val(meta), path("st_sdata_svg.zarr"), emit: st_sdata_svg + tuple val(meta), path("st_svg.html") , emit: html + path("versions.yml") , emit: versions when: task.ext.when == null || task.ext.when @@ -36,14 +38,16 @@ process ST_SPATIAL_DE { quarto render ${report} \ -P input_sdata:${st_sdata} \ -P n_top_spatial_degs:${params.st_n_top_spatial_degs} \ - -P output_spatial_degs:st_spatial_de.csv + -P output_spatial_degs:st_svg.csv \ + -P output_adata_processed:st_adata_svg.h5ad \ + -P output_sdata:st_sdata_svg.zarr cat <<-END_VERSIONS > versions.yml "${task.process}": quarto: \$(quarto -v) leidenalg: \$(python -c "import leidenalg; print(leidenalg.version)") scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") - SpatialDE: \$(python -c "from importlib.metadata import version; print(version('SpatialDE'))") + squidpy: \$(python -c "import squidpy; print(squidpy.__version__)") END_VERSIONS """ } diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 9bb64ea..8baaae5 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -3,7 +3,7 @@ // include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' -include { ST_SPATIAL_DE } from '../../modules/local/st_spatial_de' +include { ST_SVG } from '../../modules/local/st_svg' include { ST_CLUSTERING } from '../../modules/local/st_clustering' workflow ST_DOWNSTREAM { @@ -20,7 +20,7 @@ workflow ST_DOWNSTREAM { // report_quality_controls = file("${projectDir}/bin/st_quality_controls.qmd") report_clustering = file("${projectDir}/bin/st_clustering.qmd") - report_spatial_de = file("${projectDir}/bin/st_spatial_de.qmd") + report_svg = file("${projectDir}/bin/st_svg.qmd") report_template = Channel.fromPath("${projectDir}/assets/_extensions").collect() // @@ -46,21 +46,20 @@ workflow ST_DOWNSTREAM { // // Spatial differential expression // - ST_SPATIAL_DE ( - report_spatial_de, + ST_SVG ( + report_svg, report_template, ST_CLUSTERING.out.st_sdata_processed ) - ch_versions = ch_versions.mix(ST_SPATIAL_DE.out.versions) + ch_versions = ch_versions.mix(ST_SVG.out.versions) emit: html = ST_QUALITY_CONTROLS.out.html // channel: [ html ] - st_sdata_processed = ST_CLUSTERING.out.st_sdata_processed // channel: [ meta, h5ad] + st_sdata_svg = ST_SVG.out.st_sdata_svg // channel: [ meta, h5ad] html = ST_CLUSTERING.out.html // channel: [ html ] - degs = ST_SPATIAL_DE.out.degs // channel: [ meta, csv ] - html = ST_SPATIAL_DE.out.html // channel: [ html ] + html = ST_SVG.out.html // channel: [ html ] versions = ch_versions // channel: [ versions.yml ] } diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index dbfc38a..881516f 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -31,14 +31,14 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_spatial_de.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_svg.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } From 52e4c211164d0ad736b4ae620c7d1f5638acac31 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 5 Mar 2024 10:55:51 +0100 Subject: [PATCH 356/410] Add util-subworkflows and move most lib/ content --- lib/NfcoreTemplate.groovy | 356 -------------- lib/Utils.groovy | 40 -- lib/WorkflowMain.groovy | 65 --- lib/WorkflowSpatialtranscriptomics.groovy | 107 ----- lib/commons-csv-1.9.0.jar | Bin 51322 -> 0 bytes main.nf | 31 +- modules.json | 19 + .../main.nf | 264 +++++++++++ .../nf-core/utils_nextflow_pipeline/main.nf | 126 +++++ .../nf-core/utils_nextflow_pipeline/meta.yml | 38 ++ .../tests/main.function.nf.test | 54 +++ .../tests/main.function.nf.test.snap | 20 + .../tests/main.workflow.nf.test | 111 +++++ .../tests/nextflow.config | 9 + .../utils_nextflow_pipeline/tests/tags.yml | 2 + .../nf-core/utils_nfcore_pipeline/main.nf | 440 ++++++++++++++++++ .../nf-core/utils_nfcore_pipeline/meta.yml | 24 + .../tests/main.function.nf.test | 134 ++++++ .../tests/main.function.nf.test.snap | 166 +++++++ .../tests/main.workflow.nf.test | 29 ++ .../tests/main.workflow.nf.test.snap | 19 + .../tests/nextflow.config | 9 + .../utils_nfcore_pipeline/tests/tags.yml | 2 + .../nf-core/utils_nfvalidation_plugin/main.nf | 62 +++ .../utils_nfvalidation_plugin/meta.yml | 44 ++ .../tests/main.nf.test | 200 ++++++++ .../tests/nextflow_schema.json | 96 ++++ .../utils_nfvalidation_plugin/tests/tags.yml | 2 + 28 files changed, 1875 insertions(+), 594 deletions(-) delete mode 100755 lib/NfcoreTemplate.groovy delete mode 100755 lib/WorkflowMain.groovy delete mode 100755 lib/WorkflowSpatialtranscriptomics.groovy delete mode 100644 lib/commons-csv-1.9.0.jar create mode 100644 subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/meta.yml create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/meta.yml create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml create mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/main.nf create mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml create mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test create mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json create mode 100644 subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml diff --git a/lib/NfcoreTemplate.groovy b/lib/NfcoreTemplate.groovy deleted file mode 100755 index e248e4c..0000000 --- a/lib/NfcoreTemplate.groovy +++ /dev/null @@ -1,356 +0,0 @@ -// -// This file holds several functions used within the nf-core pipeline template. -// - -import org.yaml.snakeyaml.Yaml -import groovy.json.JsonOutput -import nextflow.extension.FilesEx - -class NfcoreTemplate { - - // - // Check AWS Batch related parameters have been specified correctly - // - public static void awsBatch(workflow, params) { - if (workflow.profile.contains('awsbatch')) { - // Check params.awsqueue and params.awsregion have been set if running on AWSBatch - assert (params.awsqueue && params.awsregion) : "Specify correct --awsqueue and --awsregion parameters on AWSBatch!" - // Check outdir paths to be S3 buckets if running on AWSBatch - assert params.outdir.startsWith('s3:') : "Outdir not on S3 - specify S3 Bucket to run on AWSBatch!" - } - } - - // - // Warn if a -profile or Nextflow config has not been provided to run the pipeline - // - public static void checkConfigProvided(workflow, log) { - 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 " - } - } - - // - // Generate version string - // - public static String version(workflow) { - String version_string = "" - - if (workflow.manifest.version) { - def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' - version_string += "${prefix_v}${workflow.manifest.version}" - } - - if (workflow.commitId) { - def git_shortsha = workflow.commitId.substring(0, 7) - version_string += "-g${git_shortsha}" - } - - return version_string - } - - // - // Construct and send completion email - // - public static void email(workflow, params, summary_params, projectDir, log, multiqc_report=[]) { - - // Set up the e-mail variables - def subject = "[$workflow.manifest.name] Successful: $workflow.runName" - if (!workflow.success) { - subject = "[$workflow.manifest.name] FAILED: $workflow.runName" - } - - def summary = [:] - for (group in summary_params.keySet()) { - 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 - misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp - - def email_fields = [:] - email_fields['version'] = NfcoreTemplate.version(workflow) - email_fields['runName'] = workflow.runName - email_fields['success'] = workflow.success - email_fields['dateComplete'] = workflow.complete - email_fields['duration'] = workflow.duration - email_fields['exitStatus'] = workflow.exitStatus - email_fields['errorMessage'] = (workflow.errorMessage ?: 'None') - email_fields['errorReport'] = (workflow.errorReport ?: 'None') - email_fields['commandLine'] = workflow.commandLine - email_fields['projectDir'] = workflow.projectDir - email_fields['summary'] = summary << misc_fields - - // On success try attach the multiqc report - def mqc_report = null - try { - if (workflow.success) { - 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" - } - mqc_report = mqc_report[0] - } - } - } catch (all) { - if (multiqc_report) { - log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" - } - } - - // Check if we are only sending emails on failure - def email_address = params.email - if (!params.email && params.email_on_fail && !workflow.success) { - email_address = params.email_on_fail - } - - // Render the TXT template - def engine = new groovy.text.GStringTemplateEngine() - def tf = new File("$projectDir/assets/email_template.txt") - def txt_template = engine.createTemplate(tf).make(email_fields) - def email_txt = txt_template.toString() - - // Render the HTML template - def hf = new File("$projectDir/assets/email_template.html") - def html_template = engine.createTemplate(hf).make(email_fields) - def email_html = html_template.toString() - - // 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: "$projectDir", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ] - def sf = new File("$projectDir/assets/sendmail_template.txt") - def sendmail_template = engine.createTemplate(sf).make(smail_fields) - def sendmail_html = sendmail_template.toString() - - // Send the HTML e-mail - Map colors = logColours(params.monochrome_logs) - if (email_address) { - try { - if (params.plaintext_email) { throw 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) { - // Catch failures and try with plaintext - def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] - if ( mqc_report != null && mqc_report.size() <= max_multiqc_email_size.toBytes() ) { - mail_cmd += [ '-A', mqc_report ] - } - mail_cmd.execute() << email_html - 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 } - FilesEx.copyTo(output_hf.toPath(), "${params.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(), "${params.outdir}/pipeline_info/pipeline_report.txt"); - output_tf.delete() - } - - // - // Construct and send a notification to a web server as JSON - // e.g. Microsoft Teams and Slack - // - public static void IM_notification(workflow, params, summary_params, projectDir, log) { - def hook_url = params.hook_url - - def summary = [:] - for (group in summary_params.keySet()) { - 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 - - def msg_fields = [:] - msg_fields['version'] = NfcoreTemplate.version(workflow) - msg_fields['runName'] = workflow.runName - msg_fields['success'] = workflow.success - msg_fields['dateComplete'] = workflow.complete - msg_fields['duration'] = workflow.duration - msg_fields['exitStatus'] = workflow.exitStatus - msg_fields['errorMessage'] = (workflow.errorMessage ?: 'None') - msg_fields['errorReport'] = (workflow.errorReport ?: 'None') - msg_fields['commandLine'] = workflow.commandLine.replaceFirst(/ +--hook_url +[^ ]+/, "") - msg_fields['projectDir'] = workflow.projectDir - msg_fields['summary'] = summary << misc_fields - - // Render the JSON template - def engine = new groovy.text.GStringTemplateEngine() - // Different JSON depending on the service provider - // Defaults to "Adaptive Cards" (https://adaptivecards.io), except Slack which has its own format - def json_path = hook_url.contains("hooks.slack.com") ? "slackreport.json" : "adaptivecard.json" - def hf = new File("$projectDir/assets/${json_path}") - def json_template = engine.createTemplate(hf).make(msg_fields) - def json_message = json_template.toString() - - // POST - 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()); - } - } - - // - // Dump pipeline parameters in a json file - // - public static void dump_parameters(workflow, params) { - 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) - - FilesEx.copyTo(temp_pf.toPath(), "${params.outdir}/pipeline_info/params_${timestamp}.json") - temp_pf.delete() - } - - // - // Print pipeline summary on completion - // - public static void summary(workflow, params, log) { - Map colors = logColours(params.monochrome_logs) - 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}-" - } - } else { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-" - } - } - - // - // ANSII Colours used for terminal logging - // - public static Map logColours(Boolean monochrome_logs) { - Map colorcodes = [:] - - // Reset / Meta - colorcodes['reset'] = monochrome_logs ? '' : "\033[0m" - colorcodes['bold'] = monochrome_logs ? '' : "\033[1m" - colorcodes['dim'] = monochrome_logs ? '' : "\033[2m" - colorcodes['underlined'] = monochrome_logs ? '' : "\033[4m" - colorcodes['blink'] = monochrome_logs ? '' : "\033[5m" - colorcodes['reverse'] = monochrome_logs ? '' : "\033[7m" - 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" - - // 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" - - // 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" - - // 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" - - // 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" - - return colorcodes - } - - // - // Does what is says on the tin - // - public static String dashedLine(monochrome_logs) { - Map colors = logColours(monochrome_logs) - return "-${colors.dim}----------------------------------------------------${colors.reset}-" - } - - // - // nf-core logo - // - public static String logo(workflow, monochrome_logs) { - Map colors = logColours(monochrome_logs) - String workflow_version = NfcoreTemplate.version(workflow) - String.format( - """\n - ${dashedLine(monochrome_logs)} - ${colors.green},--.${colors.black}/${colors.green},-.${colors.reset} - ${colors.blue} ___ __ __ __ ___ ${colors.green}/,-._.--~\'${colors.reset} - ${colors.blue} |\\ | |__ __ / ` / \\ |__) |__ ${colors.yellow}} {${colors.reset} - ${colors.blue} | \\| | \\__, \\__/ | \\ |___ ${colors.green}\\`-._,-`-,${colors.reset} - ${colors.green}`._,._,\'${colors.reset} - ${colors.purple} ${workflow.manifest.name} ${workflow_version}${colors.reset} - ${dashedLine(monochrome_logs)} - """.stripIndent() - ) - } -} diff --git a/lib/Utils.groovy b/lib/Utils.groovy index 9d9d635..1150d58 100644 --- a/lib/Utils.groovy +++ b/lib/Utils.groovy @@ -2,8 +2,6 @@ // This file holds several Groovy functions that could be useful for any Nextflow pipeline // -import org.yaml.snakeyaml.Yaml - class Utils { public static List DOWNSTREAM_REQUIRED_SPACERANGER_FILES = [ @@ -14,42 +12,4 @@ class Utils { "tissue_lowres_image.png" ] - // - // When running with -profile conda, warn if channels have not been set-up appropriately - // - public static void checkCondaChannels(log) { - Yaml parser = new Yaml() - def channels = [] - 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 - } - - // 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 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])) - } - - 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" + - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" - } - } } diff --git a/lib/WorkflowMain.groovy b/lib/WorkflowMain.groovy deleted file mode 100755 index 85469a7..0000000 --- a/lib/WorkflowMain.groovy +++ /dev/null @@ -1,65 +0,0 @@ -// -// This file holds several functions specific to the main.nf workflow in the nf-core/spatialtranscriptomics pipeline -// - -import nextflow.Nextflow - -class WorkflowMain { - - // - // Citation string for pipeline - // - public static String citation(workflow) { - return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + - // TODO nf-core: Add Zenodo DOI for pipeline after first release - //"* The pipeline\n" + - //" https://doi.org/10.5281/zenodo.XXXXXXX\n\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" - } - - // - // Validate parameters and print summary to screen - // - public static void initialise(workflow, params, log, args) { - - // Print workflow version and exit on --version - if (params.version) { - String workflow_version = NfcoreTemplate.version(workflow) - log.info "${workflow.manifest.name} ${workflow_version}" - System.exit(0) - } - - // Check that a -profile or Nextflow config has been provided to run the pipeline - NfcoreTemplate.checkConfigProvided(workflow, log) - // Check that the profile doesn't contain spaces and doesn't end with a trailing comma - checkProfile(workflow.profile, args, log) - - // Check that conda channels are set-up correctly - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - Utils.checkCondaChannels(log) - } - - // Check AWS batch settings - NfcoreTemplate.awsBatch(workflow, params) - - // Check input has been provided - if (!params.input) { - Nextflow.error("Please provide an input samplesheet to the pipeline e.g. '--input samplesheet.csv'") - } - } - - // - // Exit pipeline if --profile contains spaces - // - private static void checkProfile(profile, args, log) { - if (profile.endsWith(',')) { - Nextflow.error "Profile cannot end with a trailing comma. Please remove the comma from the end of the profile string.\nHint: A common mistake is to provide multiple values to `-profile` separated by spaces. Please use commas to separate profiles instead,e.g., `-profile docker,test`." - } - if (args[0]) { - log.warn "nf-core pipelines do not accept positional arguments. The positional argument `${args[0]}` has been detected.\n Hint: A common mistake is to provide multiple values to `-profile` separated by spaces. Please use commas to separate profiles instead,e.g., `-profile docker,test`." - } - } -} diff --git a/lib/WorkflowSpatialtranscriptomics.groovy b/lib/WorkflowSpatialtranscriptomics.groovy deleted file mode 100755 index 99fb1ad..0000000 --- a/lib/WorkflowSpatialtranscriptomics.groovy +++ /dev/null @@ -1,107 +0,0 @@ -// -// This file holds several functions specific to the workflow/spatialtranscriptomics.nf in the nf-core/spatialtranscriptomics pipeline -// - -import nextflow.Nextflow -import groovy.text.SimpleTemplateEngine - -class WorkflowSpatialtranscriptomics { - - // - // Check and validate parameters - // - public static void initialise(params, log) { - - // if (!params.fasta) { - // Nextflow.error "Genome fasta file not specified with e.g. '--fasta genome.fa' or via a detectable config file." - // } - } - - // - // Get workflow summary for MultiQC - // - public static String paramsSummaryMultiqc(workflow, summary) { - String summary_section = '' - for (group in summary.keySet()) { - def group_params = summary.get(group) // This gets the parameters of that particular group - if (group_params) { - summary_section += "

    $group

    \n" - summary_section += "
    \n" - } - } - - String yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" - 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 - } - - // - // Generate methods description for MultiQC - // - - public static String toolCitationText(params) { - - def citation_text = [ - "Tools used in the workflow included:", - "AnnData (Virshup et al. 2021),", - "FastQC (Andrews 2010),", - "MultiQC (Ewels et al. 2016),", - "Quarto (Allaire et al. 2022),", - "Scanpy (Wolf et al. 2018),", - "Space Ranger (10x Genomics) and", - "SpatialDE (Svensson et al. 2018)." - ].join(' ').trim() - - return citation_text - } - - public static String toolBibliographyText(params) { - - def reference_text = [ - "
  • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: 10.1101/2021.12.16.473007
  • ", - "
  • Andrews S, (2010) FastQC, URL: bioinformatics.babraham.ac.uk.
  • ", - "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: 10.1093/bioinformatics/btw354
  • ", - "
  • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
  • ", - "
  • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
  • ", - "
  • 10x Genomics Space Ranger 2.1.0, URL: 10xgenomics.com/support/software/space-ranger
  • ", - "
  • Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: 10.1038/nmeth.4636
  • ", - ].join(' ').trim() - - return reference_text - } - - public static String methodsDescriptionText(run_workflow, mqc_methods_yaml, params) { - // Convert to a named map so can be used as with familar NXF ${workflow} variable syntax in the MultiQC YML file - def meta = [:] - meta.workflow = run_workflow.toMap() - meta["manifest_map"] = run_workflow.manifest.toMap() - - // Pipeline DOI - meta["doi_text"] = meta.manifest_map.doi ? "(doi: ${meta.manifest_map.doi})" : "" - meta["nodoi_text"] = meta.manifest_map.doi ? "": "
  • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
  • " - - // Tool references - meta["tool_citations"] = "" - meta["tool_bibliography"] = "" - meta["tool_citations"] = toolCitationText(params).replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") - meta["tool_bibliography"] = toolBibliographyText(params) - - - def methods_text = mqc_methods_yaml.text - - def engine = new SimpleTemplateEngine() - def description_html = engine.createTemplate(methods_text).make(meta) - - return description_html - } - -} diff --git a/lib/commons-csv-1.9.0.jar b/lib/commons-csv-1.9.0.jar deleted file mode 100644 index 0e3f67850c92980c43db49464317cc11e49bf099..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51322 zcma&N1CS;`)TZ0EZQFMDv~AnAZQIkfZEM=L?XPWjPvg$N`|ob-y?0R+8C8*WPR7ZI zH?s0PbxKhN6buar2nq^F(u+e8Y>#Tk00ao=3=9b9UoViHn5q!Hl)N~joRGYfxR{D6 zgPeGrijKo36PmxoWI%&whPIu&u?SU(u(nL4C}fSbW~+utzG5};tS#!~pL!8#={0q0 zeH5GAxVl`3q z5k-c)_>mXO=rW?;U(;-e3Hx4E2AkQ{x)@q+RhL%%>TSH;jO;~p0Ej|nBP+QnjSv?T zzqfc-D1v1S`1EKDEMKMp-SeXa zs(+FV;j8x9S@~L5NC0jnaKiRm>2%2sx4SvHU*MyvIfg5rZN4wB*Smw~kaSwCg2Lox zKk@zF@^ZU+c7K}GwzsrUzI$<$<1NHH=}OkQuiy96;q14IM3nV^DBc-d)$xU z3j>byu-$u|VKUzNF)W?SvbTvUe^No zx~&h;T-<2H$d{Du;F`KYVt)o`*e`!oAnacwti-%PSQIIDo8ip2I)}bk!MATrDY460 zp?+AGhbe2tR)s`<#`DtPD5EG#ONn~tZx5s&e9b*Gx)))}Q}q~ngJbOJetoR3_C7bF z@`9T0m*0TKVd=RG&lc>FlaVQL9=D{IrP9qo@R7*661ka|o>mkE4zbF1M~XUe#`5;w z7W%{kll>Lp0bz0;)kb=AUb9__{he5x1=}^lL7VyK^^#ts{uxg3pCDoW2@>eP2=bo> z|N8;`@9w_~@_%jiPr5)3&K8XSYYW)_v@misGO;xK-x#9)&xR%rc6JW-F8><`%>RD} z6BqaYjT7QOr~f`LJ0o{9`~QtG$^X~*-((s7>r@#2&k-{GU-|#%IQ0K5@c*$ww6q+C z+x?4JpYH#)LdZ&qh{>ynF}QlV?r7_{Y>lG(zt*oim#J7ml;yHn;hsh{9*5V8Sc!Jp zQ=~+YHxRW$G>kJ4MSnf>r}qYxn0kDu%ayT9U2QaP;r&mjl{x+Sny4BOeg;JoV9@fdj&&R{%`GDSEZ(c=X>iKKuAp-FY9{SCi zp-#1ymp{Oo!4`BWlqVy&k2t(b%;LN&R_M)g;OQ zQy)!0$BI+47yI6aIJd%4MlEVas77r@7(U`O6P^YH_-T6X78z&O;MAgEicNCqW7%gw7+8N*WS{z`ORz31de&K%LZ%aMdMyJ^ePBC2FvRp<{73!%*cATFOOM{*9$wm(2Cse1UY|)WTN(&vYrn?Dy0xdDLh&9C0wc5$!u9)??VeHPi zg_x@pOI1z7oy z{=Dxj;KkdmvsuPq1B~ z1+lCYZvk{m(!qOFZR8$Vz|Dqr5-Pf=*(Sk}UvYm1d&Z&F!-Lt1{`6s{xjx3)U#O`D zN%q9RRUjZ@s`wX-H8)X#(ODe>LyTi_jR{4|@_|xQ;=kR88_H;nw0of6GCaT} z7Z8}7g49CV>P;l*&XAlHs{kYfb`MoeVd<)*Z)o>l>H_ApNj3m+Mi#70Q6Y1?7F@?| zXIE8#)mYly;?j6RsAnZx<(OgUxt0jupptI=)Iuyma{KQWRN;KND=I|>z*8IPRfM7_ zVUp3DI7#q|E%-|s$=u5O6kPo-gaPQ!u3L#G32Bz+Ayl3xR@k@^y%Lc zGS`cC4PMKd2MfInL^^(<&TkZF6L0VDI<0C6$7aQgjGR=6u(N(3A zuqBB?180X1BQb%vLsdYAE?0okpbsGD*HnGtbc;sa`@OF)a4B69XB<7X zbini3kh&pNND}i^UG{zs!V9;8(GyMhNAIB*$+>RF;A_BXg$rKdLIo4{0%vut@%@gX z#CF>OY`=_|lK?6BbEz(a81D0gtdKj>U^_pcx4Qi-D~K6}^UiQVBtgOPxJ5Dxk6Ns^ zzqzNIs>PnnXj295}gt0hsQ|K8J=K zHhiw!n%H%K8ZB<+!0xB5J68h<^h7PLQ*+3bo4mH%hL(+JzO_N#8|Dqlrr{E` z0W$D+V3Mf1!c;tFD5z7!Wi@@p9GOE^&Yp2X9aO75LmKLnu9B5 zZ&{*K+}9Y@29ox+(av;JCO&a1@neNgY(7RY(pURsP=Zz^9R?7?#sc3%Fo}vcynZD) z!;xHv)|4C;?o*_A^v?wp6>w(Rk+LK#Diup{zp_wqQa}cySCE>MTR)oEZJ1^f15z7I zAKUKA7_oU^EoKFBL?)QhXIp5v5wBrBa|%#|a5(Cw;& zNGNUY=eQl>?_4}pWk0SPvc7T_oR@7=pND-4C9A9AhJe$;_z|Sw7a&| zq$FsV1>(}|U$_ZGvu7@Fm2c@8xWyKULX&FYQ1q(Ud=yzJtg{G}j`vdXGSADFE9AlI zPk$Uf=U19nX1*S-pW%{korru)HsARZoh|+zZ~PYO&y?kEqZ&Y(V9 z)@ml9lFE5BH|DEAX1NnOgBnR+XGe9}9kjTX9)MuMA;)i1c~9MCl-ht9hT`x;T}~Am zGrcq|lv3@qaC;Mge3VKAc=6*4LhDiH3?z5Tq2H{y90!LNj%66$JR!P7bC#I_xqi>}z0Rbtqm#p~bU_o@w%Cvd-M_F;>jDmQe>$!< znv^5H%45`Nf#r*Ho!iM`%)FtOR5@pA;x7YxtG) zxPvmdC3l%?{4`|cnK(s+oGLT}Fe4mOhI?G*?tNftP3AUHowIYQJ46AI3R0DXs%fIU zKu(JYn1gTTGC z#!Epdn0l|6*%+N(u*`S;a%U&iizYPO> zBxhu@g3%|81YY%&pF!M43J08GvVO%KO+~pdI@N(E{?e%#app(;MYlTT#&k0V_)6d; zycroA+1W96`_rQzlimHP{e6ulpU{F^Vcw^ZI>_2>7Aw#(kN~^{}A%`vMqKxf$ z0VKze8I42Y1Jh_Y{TJ}{_Y$P0&%0B%w_B&~+xu(j`JxZ$m)>6Q7jld4XM^vbo}WzN zvz-Tjx89RANDTea>q7**mxHfE`ECD>?p`eemsY^mAcO;k;12=EitD8yC+jx4{17;w zg`5NXq6?2B{6biQ;wSm=%t=SM;v9QFXof84_E&X5`XkXC&nfs@&Ka;ooBqV}a@Dz?5 z?%yb&PW3it-rR$ui#usSZ|?qQnA_FnEelC1qBh^(fneblU7p0JVGyCCnYd?hN7hpw zCz>NBwWB&mN8;%<+~Tp=@3=c&7UmsU3UCa0y{L%V2aUKxz}@U+o0k;%`2N8~wjcV3 zsYu)dkuTF9Ufr%13bPoz_`VWX}Z0ye$m z)$eEcaUh4#JLeG0IqbU~T{`I+Xv?s`o{mQn^b0y_ft$oGg{9}Wrn`;HwtnHMTxLG- zQmND9rd;#7{w-@UPY^{3oXI*>-^jeN0)j6DG>@w5yOm@%B$H+iArB*txO`z`2d}V( zkm>Y#?R~xs#=bv`z(OV=_r)6~l*6^AY*-tts5!F|4lS;7?zkx2?SC)|iR*rFg4hPA z&ShjNcZ$eF6;g`m;67!sd?@r-1xyBA`cmvJEVHFo=IicY3`bWo7dl($iIs@f7NV(h zC2W@LGV}33DMU2lXJ`!h2xB}}nG&D8ldLbnK zFxopDBmsWy6~J0)14Vk@)iWN~-yArUObYCh5A1zmGEjZmpeDNS;Wcz;ydQ-}vS|ciuTt3z)@KOSnkfzRX3ISh%q_!UWSqpkC*Coj(XV{#yHe zp{SOT8w}3p9};}$vZAHLEz9pY4$6dl29;yI{Yd@+y{Bld{hd{T6r5uTKMaRgyA_Y* zCJb z&i$qv5eA8nfaY`SJCq=)lRG-t$-V_~JwPmot++uXq5|p{I=cS!Fza9CfYz&&oo8t^QHbkjp|T2JW2r-o*T)Fi^bpI{j~+dOI(epig)YfPRE zG`$535Hp>6D*Yk{h2Pyk?0I07){OZ8A##iSnAN3YY4?!5s2ToBJtesshU1dj|L44{-nj#y{VP_5G%+>h<{sPbOfg}9OHh>&UNaAF1nX~A~z z=&J0Nn)8l`?C`JmfktZKSip#o;q*C;yteQ9^A6%kk!)9zoFdT?%2!3B4e}zm79zN( zBDicV_ubHb=|VOUg_!Ih1Y`QsidpFAPw{-5MpOo=CwmM5cnqEM;ev zbB;>WJ`9(JfnYR>c8RiyrO_GwpxJ)NeFaZjyqhT`PPqw?yt0h&9c;s)4}QYIhEBr8 zgodI0GC~m39dKKuUGK*@x4SxEKBufW0zaqFA0W;hA-5)F%vSO@%)ElDooZPv=u@GJ zdBzkn(ok8mQ3V@U!WJwqRVMk(CLLz*KWpkmjdCv!4hTq#0to0oSl9niQ^Iamwx(vz z|5aI~8ql6PBglD4X1`?o`>uz%olqcY+d_^=F}d+Kh#|JPbGU|yQ!U<&V#bpfkR_p! zv|*l(ZK>%C=}SvXTkymXh>^}lb{0c+UkCj5=0SOywSVzT-I6Qg24pVWY`t5&?CoUk zNbLPNg#v0hGRIUM2%#e|zGsS&(BzvuWX4Eneq)ZwZ}1ErQPBE~9huYm3?50L^Pf1> zp!1(Oghie!(eRm%p9SG>6!ds=j=gnXjtRyd`|;$z#QbY*95!mG6L@zGx(k`_*R4Z zIT;OrIq>834J_yuRTo_-bY14F(K}To!%X`z<+4h<6_j(%4w= zd9i4`F#jP)#}ci01sfN4K44jov0EE~T;(&n_+mK&OwYA=g}*YG#aI=gkvP*paF_62 z%vUzZR`v^r!V^caO2T)u!+xD8H8wi22O9FY-MRdCVKSEZ-qa`QfTHFt4sOED^*!XX zsfMw$K7G+r4~pJbqazm@ECQOgtlH`8!YqLpV1$Pe10`s;7&Jl`UZ=q3e z?iMOc0-FsOdle@ub)jL&2Ij_6kC!u?0!z(b;e!KzzZ}D2V)3}EmjueeEs{RsREeRi zUxElcDXH8OM_9r^j5SNz3ngnKpFu|7MedLxY^{M9QFnM*e6Uv__b-Swh+$(bmbq+Y zfjRofzSK)An7Fu$LprU6K+jKJ@zBjAlnUtl#Q7Ll^Mn5GM7 z-)GjL?;FFM{ljq|QEB12!ZU=2`A}|Jj#5Jw;!lG13r5vT6c(vLmQr#BM^M*+C8tV<{NlIsE_&YAo%w*5jNTzLzpgqfs@a{I^b{lvBY zZ-nk9BX;c7@{n5F7OXK+y|2`s7E3x=!!K;qcIK(5_*N8lXZHfH7=kG9qA%}w${qB) zmSSw3LM34J2I&E7lGaPzd;x-I)VyF7Bo58a+m&MNa*P#c*y3?Yg!uGiK5%6Md5Pi> zA2x%QJYYLX#Cdzoqedz|?h$2s+jUcR8L=HM*R-3dq%&z)pbK(?9qcfs4V${Kr8xZ< zXH8kcnU%W4&7ur)gx3?(>t;rbiNkcAMxsyp^U~z<`Fth%>Ix4V^8r;+&b4Wh8HuH+ z<<>R^ZL$Z&RVTWJE_<$7J<~y3lj-;$!qbO`M*WQXrU(I2(l*rX z7$=9`^QOIY;5MC|Vsx20^wji?bT-cS)?V;zcbm%dTPoVr%?>h|ep`#F>XWvc%H~T9 z=0`V(BZC5dz4@pkiGZ z-Ufvc@{ZKHfC=_%1h6M$`gvr6D5p9$X|TbE+G6ioU#Dg9B5f{hUWyz3*1m~m)yt*e z0mT+~$@&#*YU;CA5m$@d8UOUgB>;^s5N4Gp$zoZh3Ez>Ku}v4@3o+Zn`tUypvWuo$ zc}P>@W3Ba}uI3mQZPO?c7PIe76GBrb!pK=y8eCya>0a4EprGso>R#>1k|)fV<_c%x z}@p-OUPL)QBeoA8zGBYRom>Rx-hzAD1Y zJ~!0v*v`jucuY5OVP)hrOsORLt(iv}&AlNH61Cj?{o{3>;Znn!d7T{R?jll>U!%_u zB2`Ro9$I=b%Hl#wF4hWFlx}`>UNO-^POjG8Qr4o3VkaYAWG5xnLQfslMozxaHcy7O z9ooPh0poa;M1Od%Vb%EA!u@(jA8?cj#r~lBx7+MkE~8uSM|1c5eGg1yRVU+n`Hc`%g%E*MgF_f$pQZ-F49?_~RREY-(tYz= zrr8Ye!ud4M2;BaP7rDy$6xZ%j-AiWK45-33P5fkIe@h53y3lnChNy_%D%lYq0)No) z!>jN;gr9{noO#)iXGydKarWhn8gy`4l7L^k)(eCXJTk)fV1bjZjgeyivFh?{X%L`O z#VH{_$Z=0=k)s_&)ClR-r9;I5Z*ROwt+_}M?8{6iDl>js1o9e-vBHdJCnrU%wb8=J z=NzsMt+}|6nCBiYw5Gb4kU?*MJQz(v!U*5hXD7#|mto?B=U^@_P0nH9TP6y2pcS%w z5K2C|6z}k~SFH@_IU#&LM7cYVmZ6vi6#Za%&rFPO^rCMy?Si$)tF@>|P9hkGK><%# zhVb?Sl{*Ao(A&Z1yF4N*>0l@>+(Bbx1gNgWZxYk={q9^o*55Zn^Bu*&h{7s32ayzm zJ1DTk5I$ssI8q&pg0aRTi`z3hnqco0S%XTkj%bY`+&L~a1JSJg)=;)t@ay8x?se>% z`Vi}UAl1#lTVw+-gAwEz8o}u7(;c`N^8L_TtUx;bVYL+fB%lqE2mmh>$9%A5%kswx zoK4i3HKBVW&6eUK08z{C-u$>u%EBX@R z_Bgjzsc7F`+Iwl{biUMd!aCC*xzRsAXVZ+?v47Y|znMk|3*Su8eh9MP+DlmjkbOfT z8oMc>$?~6!uk3u0vT!I3-$5OX_XbH{2cma-$Y-rj2T3`F)lko^EiS=ouKuh?aR%H8 zi!6cF9BSr}q;)DvHtHfE^nk$JNmC6}7r@kqZ+ODf5$(jTMXzs8Y^X+`6{n)+C<>Dw znK(vC4#mWIVh)u$VuPO-`2KaGfkb!<%~3P{qy)UlP?wXLtHH#-8dZVlgUb)Ujt`WN zp!kYUKRI(Ru>z!(`uvcVM@0k~GqVSxP#Y*Ftc=@t%nutEBdbyB0%=RftoF}T_Nx%r z5v{=M(mrtBYKXuOTn7KPF8&3R+n#s#3_@>i*@PXW%^l`)x%vXdx?$ftwdI(#66Lxeo!|DI^@- zw8kBEF)8R5^Rl`{vCIP68hf#lPIjRLG85>^RXCpHfO-}Pg?2Et8l^J>L>u8N5hi!I zON{!eQ8NI;(r=EkPKVnd1npkTt|VC`4rk`_)EVs z(?C6G!}N_n_zAxkIh%%0xQ1$|D{EY)e6a2joaszp_rQMp2H+z|kTmqc_I-8eRl5G5 z;4WC2C5Mmp42Hb-JG*e=quYKvj8XIr>^%ezO{{ba3*Yk6)26G~KQ>mnS-JOmNbO7v zo$Rzn&@xAdSFwoNIMx45?K)@0HfG)vOB;E4k9_tU!WwD+UFqXsn_2tjl9|YP2jewy zFikCd|CBoUyN@Dj;_x=Je&>}s>AR2awd-0)xZD2pFS42+{9~Ak*L|v$sw+6H@+ff2$pU;Pyo*Z8cpF zyy&=@4*FpkTn`|A=^rPhqq*`X4Noxyo4stS&W{nq8How~5{Wia8vqKKZ74HLB%W!g zGmIo2YA7_!I6x1t2}Tab3n?^A9Wd=EHHoW5nEHKw7oeLZ)lW)GM6E~2`7gi3>M;d67S7VQOJ_F zrmUqW{G6jjRrbi^HDWs_K>0*(J;+Q5jqnuzBv~ODO48yir%G7}7)GeDmAE`KwI!~R zKI?OujHl&nwT)`8DzE)&v9S|!`JGiAg*=z$OzSOBSnJr0fXvoMVbnz5R%mD4S7Tn{ zXU&|?y!id1QdSw0Nlo8f0bs@3kv3v&$(c3zfmYxLXAv*sVlX<+xLQczZ1GDv#9U)- zp_OM(;b<|YR_J7C`}$Kl+oB(mA+R^4;CW-2+DNt$OyBPqNh>+8-#ki{bmP4QQDvCHg)GNh4d zkH(3enV*p2_U1DSG+xw`nt4Iy)dKSi#*+f)T@TpwFWr{I2JjSuw)=R~P#S*YRfX9Y z-DSINApOOgCD1N~fc3CD7jqP~Bos<(A|Rm}?i_YJLGd0tDHMsH)=Ty~^dthg@dQy2 zgI3qAEuPUR01S679dYL0-e@;BtU5B`^{L(=l>T<99+8ysbu51{dh?6nw0tVjOjm;??V$V`B!Lq`30#7h`NtSAHdCZgk8V>!WKRLd2Qu#)12jn{{d}9+%{Mk2o z*WgPG81irx`1W(5K^bHykjO#rx;(EBb11qe{GXu?gBN863q24}iVG0XKd$qCggXBT zbNHZrQ%9a1`bi4sAfG=Rd$wIqqih`WJ%7<&+x^Y(#@ zg4;HoGk^|*+JaYBSFLL|y`LM}tnF&s(yBi9vTwI`960wdg9APRGxsw-k0&$PoVPR6 zAA@TNzrcY|0k|E@bD$w5-(bFA&f8GV(8tK*luUvBf*{~v5Xe}vhEPF;VByGE@`hkR z!XTv3Z_sGSR1{2;q;YAZjvTrsCj5(7K12#xf_@55lBbg7soBG&E|njAG^xr% z6RJ7MV7R4~U}eZ$d!YH!OVFuahH-2|p!re&Fmsd)v4Y@!TM!p0Rg?@FLxLb0kQFd% zsAZH41w-JVFt7_KI+BJAPc30X>L9ZmGsntZP{6WNXG=~$3DR^nCuJ&or`yw0zyO&|%vaBKhN=?atJb6RVg?2~F0_d|cZTe=%@=eh1 zMT!ru8B#Y%6F%igK}8ss$|X{}v^GUVole&z$l%2h-L6oXbngPGu4&NAk}39cn0D7v zSM>C{lSTI7Ee>NFazGuqBYw~&^tRx>I0z?lM`k}gTirkJY>7>V9eH*Sq>hySb|~L$ znZ5gd`b7_D-#L0egH53oUg&MHeS70Cg2^2jL(Ft{pM8pR0fuYh^BsjSNUB#Nz>D&w zpX$F+iG{r~`VpZ%6T=@OaleWAF9x;~I-X4AHWfg41Pu_p3)H*?QxL2cO|q6trFKGH zHB;ghy;V~P)=ErL{e)BUN}j0Yyt5XMvYuio_+|LNMN|Bx7yaZ@mTDEM!KL>7VN zJ~AnK3#1sW7w4qx0t%%Vtri*C&oy+tH$WAnSd&W(Wa=#!tFxYPvp(0LKGXUy!FGx6 zD1!Eo-}r;tp?c)@@j-4;JQMl}p+1xP^PxU7`~QM{3-5!11d!ixf)Jv-jfVvPWH<*< z-bGP-4Z+KPlvDUgPu&@S36S1#`+ai)zD4$7L4H%5J1Lw|c?XBS44ed{AqMJt4VzA)q ztS>b=Ddq_*En(uVEVnuuYuVY#h+Dj5y>^nL+v4Mb?o1&rYRmx5RahnJtE$TkHFdge z)=qM2b{nz~nb7RjNO3~SZgw+Qo0+UA(lHe!>T7HCg4%VPVP*ieW~`Fw`dS?}p98uY z_RVr_8jKn3aM_12bO7pDE^nHUHr4t9o~0Jb6%>1WD?^1qYd{U>8E0$vpy-8Rbg4v*y0pZ zbCQJRc8$zuHc}+^#^1?+XSuG3Z^>D`<3@EQ*wGC72(E(h`=x}tHjzmJ z=d3R*xAx#9Zh(BI(0rKD#?B5Fam4A?3}Qk39W7m~3Ryj|^qAS?SM!&M~} zfK(}6udr=49^zOS7912GUJg*vS%YVWKR|vzeFg==Q2?=pM2sUfv#Ks#NHmRu^~Bh+ z;`MQ54{y&52N7Zvxq#W7jAzg4O@Da?rMsLy5zhd<8XNH25=XcQf}BMlsl98zY}U|5 z#PLD09MO^um(C0?-$kYscnv9D&K|mr#MS}!U^VUwmc2@Es$f_?!!Ie=rk0vxE%jve zRvTsB3va{#m3-PPjXxlw&uIJ`@;ff}m@wQc^c~l(V zw#BIPK7s5Yev>6jXGs50`kLq*M;((j=;g(bHY-18&>6OoMMI|)}f8w(9HPfLZ4wT!K$^~f1}6OwL`@t9;DMOyPFX(dcX!(OLZ zz`r^=OQ@B88||u}DR2O4Ys9LZ1Ax#tMnw47D6aWT+{Rds8)IXF_9BJV;(- zmwRv~y01~(UW(shBP)(c(ChIu#~d$onOlD|z9>~WWV#`TIR2}CCokl&O0$Y7~{3ct?lGehSmv-T|qX__S zDf(GCM;iDa!3Pe^0b#?MmZ#)avFN#>Csg1&^WyED9eSzp{lVS}#@8N_oLO zgRCoUp!)2G)@;eZ2Mbr(nnV4}B#aZ_ye3(|oGFC*T@6TcBp` znsT)|XyG4znUiv$l18f~38r_qv}y;ZKWK`P$*pJ%9$q|gOlkBNRYhykR5^gGASV4C zhFA~{h}el))(p<^ix?OefC=+d&|2zrw?hiK4m;VzJ|fG|(iw`xvduw`5Cj%N2!rpcXz2IkP~zbG~PLSQ$4sX(2EL7U8lpTnQ<7Jxg}w6Ve>>bz`d5Ygilk-&Bg^a z8UjkDTwIRq&^EE~@lt6p0tHtNE}eCBnabd4G!7qLlRfWjm(89wixc|BIBrb;^3XhZ z5a?v+RN`^hacz3T*(zSD%xRFyKSgSMqUIy^#&EgaKvARL{;E`5*hXe?YokNka1(2m zJjm(zK6fm0@bigH3{bbBq_OZu#5s#6UzMY7T1MJM`(evLgn0CB*q?7vh|RT8hPAUE z2ousV+rhq4KwH^CwjyK~yI1YFGH2SU3%M}lVq!%?c&aUaiWlX3g7PVzG*m8(D_<_3 zWiejDe=MWe;GOa#N*6o!<}psQuBJ@Gt!{G~Q9iTTW}Z!pyk=_MIt*3gg2-hitWh8x zCmZDdWMdA@i_cO}H9&)bzE%qZFQ1Zbi8WdBiQ`F{%k$i&`DzR>fLP|4tiAl&(Re(1JbHhCNjx zrlu%tEIIT$o8b72T*StdaP!r*64tdc);-VZP?plQQr4A%Gak^wE6?c!a5VyCbjFOf z7t>dn?MD+@A2{@5^|Ke}jq^S{FNP=Zxf?rNi^;@Oz?fGZFDCik5zcKvvMZWh);oE! zr%ujgP3&{uHLGTmsW=n+fk5;@-oX~N!e?2apNieRz?#lNA_O^t*wSiPa|IEmR-$s?dp{r>!LD~cKP5n-mXidu(|>MfX1Luil#<1b>kW`P{7)?K?U~i zn~R@Ve(9XaCz3w35n~x;qRU`?VYwxYP|YxxY9S^tlHbIHg(Xs9YXw~gyFVM$r_%!N z#u{UgzeLX38)r+7l&Lu7&oYRcnebNJP^x|s z0J_#$vWBvt7#EPLFhGN}iE$a$E*^ZnfR#4F;-HTxub>D01{a%|DfINI*3NCidLpM{ z+*aur7dGOS7kaONbH6gK*8rs~C>^P87gz58J~Jv=perTHG8tm+2dZjyWBI)ywF;Z* z2EoEYM&Nh_`4X?xE&^8GfX5mnEw&=VY&P7w@B~4L>XUF?JCk-$IG;z|U|}qrY#eQP zt%VvI1#R`gNh}%YiK;5RZLHjri*(+XsV#xvBeI&L7M5Z)qul>gc3iYI%bB9lQ+BET z^{ZMjAwW1NrF=tPzLB<0*FtiskdCWv6wlR@%XfzH_Ec!Z&`^Rk5Gr~^w+bXM6e==M zk$w;jpk?1>q$4k$4@7QM(wj7NOaRwdZLCYd(NPD?i^&ZHpD`;5i3Hlmz^sD7f`E5; zfWfX}y|SIEG@reD2#E}nQ$w|IV1(jJD*8bURR#|tcT~nNp7U3VI{L&e1`T&)aTMn(4u~(01uG(LxrW1PZ+RzMBA`d2 z<~(Uu@KGXJ4_bnfn&{aV1)-zJhzDvQb*2qX*v1Y|f{$B|iIsw{xAGJfbfUjDn|t*# zI1tSX8y0U%7s#b=QMXS3XlS1b!}Dbv_5!4)M%#Hn{+J5Oc3CI!~ZNC{M*)x1?uY74N=p4 zC-kS^5!$vliKo{~WCI&0?W}>xlM1TD1tRY+GqjyYb$^}2#POyNSiIC?q2)FzUBQD* z_SV}QhE|3S5u9F6qezgC8BLLD(Ik;Lg8=DL4cbgPRa40zEMRnntBF=~(b zpoYcWPAy(RbgPTHUh@k3{kwO2H_fk@RwG7Qmg7c--P zkC@xW61$Y@urRB+XgvapRa$_dABParFb)%VRcl9)vE48qSAv$ICI<R6WZ#Za!*mAOZ1tvGM{>j>ja zNB4Q7pEnY6#G$pIMleFeM}fAVmKC$O+C2yG4u6+KDXSzK-~DTOlFQ6)Tr^;lhu)le zJ_P-l}Y* z>gI6sUOn{%I}t54H3tXxLvYOUlFWN#EaqU9{9T-Fl&ByQCzBzq-J)7aO9I#%5*dg; z!ydxCP`#rSFs?O2(D;XrHfZpl6bU#=b%M+s`f%$3hkp^@KmUd89o?F*9AW2({z zl3fO~S6=Wv!|Qg`g;V;Q=5`!!>zi)vEv+w-dUmW0vK?8}NZg=NO@Vk0=#_ZcPVPl>QYWhbkGF+l>9manl$Tk6df4Fgzx_~e*Ng!izy#284t6)r^QL(kdCID%q2dt zx;iSt&U~Jq>>VFXo=Lb3{{D;)xtm@(RXc~E+4$j{pHs2qFjeo0(~!)^KO5)@!N<0F z+o87k+o9e*&ZyA=&aBtj?<35!Es5^}WAmg_+HUS}vnOfei)!NICrSz|xi*@N3BoUP zUcyPIeCMn-m~$0bt=0pFn_|uCzvrlRB6E%~nqllltFlJa)R+(;25=1TpwR*o1~q_W z|1d56WSvv9e&uKSTa_7Pp0AmEZ%R_9qEn+$gX~|n)lgzL!(KYw3$u`flVa+6WEHK*eyvanHDL zO*yb+o~iKhkV@VaKrH_DAKiw3j}5dFUZQb;6~^?@Zmla$UyAE+#KJ=wk{(*aG@6s0 za7$`~OEABM2}Wx+3WdfEE{kx2W|Y@qhJ^n7qnxbH@q#x_<5Sn+pWT79e+82W2N;{e zg06#uY(Ycf&_znNxi|>UmBwf!wU)5mnS%YM{+03uWI?j>;_+-) zVVeaMqY7<;JBQO*<_}5BZo0^BVsl&N>Fv)-$?RSWJ`2IFz;Qrh$1AUps?M=js%Jg^ zs|*D`=fVU{5DY?X|4Z-MoM_)9g$bS%WV&<3r*55`3$InA_05c>ny^SPeV0#%oA^x~ zVKnZ~Y{dLAr*(E(+(xoju;3Rd<;qJ%WWs~zCSmsXOMxHSOYLEq??=UlsF`XmpED0X zsL*YTtnKz`Ozdy*HliRtFaTUJ6?35U6T}rjKCbK!4a$UH)kEh^FE~lf)vL0>#tayO z&lRzEUVrZph^1D5*my>FL||Q~z9Vq~-Ed9m{6zw+om~AjC%B6s`B3cdHq`L-T`UP( zb}aw99B_IKT>tBbDmIa*cHiI8Ath-kp`t7Qmx5J`Z=(Dy?^pY$kKokQ4k-2aXHT&r z6{vjlFAln2o{pM-1}41Wm4B}h>v(~F&^idmKlo57?*=g?ncmka?WE!t?D-M_&$zz1bLFRq1)4-Z+e6i2D&eAwWTQqZ@1de5A6~8C+ zkK5l-&Yp~=Z7ge+Y6_z0;o!Wjhc)8eJ`#qL1S}v^-3T)q@>Id5Q?s{u%_cbg4^FYV?{lE&5SBQGcyS`_}>TS&MQH5p(W(Io&CNp*qWWB|9MoSq3X zaz;w*cedojIk|`XLai)$kOy-7pr#|HkkI25DbZatltj+B;UhWn1J5NXwy}bqZ>H&| zUl1EROjD;f;V`Uw@Xb=@yKs~JbJ1(LO4P8Yrz zB@BU;7s3^amxhI1P-&Gh2f}X9(hk%$8tFpa+K3(%QwKg5FdZ^_)MW14iDIh89x-z9 zQQN+3ipk>(;^Z;gAT=tY_QWTuNyT=vR7*>uh=er1t(c7c^75<13N|GIJ*oLlrP3UV&iNkUsnPKf^umv?i$QcWirTFE97 z;6!)fu(dK<*Ebls?%NxkW{%UF%OKfMUsq^03%fSfNMMRd6fTy@d~iR-xQ8#x&DF2oz`Ixp-|G$0ih# zuftY7NG?FDzSvhV4E})>`b|j9gAh`s3_7SDbna0wXN-d3OBsG@o4`%JrFHc8qLami*KcI}0w&*^jmho)3+KkI# z!U5Wh-E9PFuELZ}BB>`>4K1@#q#8rcnAYPnlI&u%N-mIUNR2L2S)>|SN=8S19qv2b z#eBINclpgc&s6>FiaeUqSEUhBr4Opxi_x@lS@6dj^thVyaIzbEwSsd9a6x}p;f>o< zWp6=!5yxz2Sh+wgV^+OxofGc+mI~e=&%!DByS#z%fOU4YK*kY$^9#bnoJ<*Ao9&e_ z0Nc>@*=#n z)OQ&g>r6#n@TJ0Dvkj3&hCTmqsmthHU|$t*^jAhLU%TpUxm;$4U)!glb!>6-%XNlX zQ|s3SlkOdxb`ba(BZG*?N=*D31S>r0UyXitNBlI-esiVO0%-SrnDxL!Ws%8ZtBMPG zT^Wxcjb2>mWuF4UUd*z}ZbO7BJ=8MXI~}Y?-W7h~L6?R#kc=^$g~JLac=0H`jWCWw zZwyl#rEgv*K&^?KvEDABUivGn2KW(mM+|%O^NH*>uvEDbN{1uKJokw$s0DqF-Ej@N zAq6jdXTkI5@EL1xoYaF=4l{dU*c%(KUSz5G zof(&5n{XIpPhaIhzGg0K%602R3{%4XheMahINooh_YcXmd@1YjWK2k}UK)oJt4Cz^ zO<|c*;J2Xe7^#i}RF5RH+qJ1F=CbdD0<;^rX;_^`QX($cVcL2UoNK?cHsLDpM1Aax z=T+9Fz196+v?iTLRn~|+kmC|8<7%3Pr$vRP>xH|AozoSNjmKF=(cLnxm`%uu-mT(l zj8@iud!WRTuM+9lOgNhp-J@z8;#h(uCsNd^HIK5b6Lp}(Sy;u{)C(^o$6fpcfRO>) zzrp!eWbYpW>%ElYi;aG6Qr>%bpvof9V{-!PZwa-<+GHrMpxEc5vN)4o=E{_Abmol=8d*CKOg52A-NGWvla9tneF z*-;HXU^to5W{r|bCxi~I8_O<}8=KGFu9ecj zB_-39=>>ylE_T>0rPnfARC^POSd{67aiO^RHG&oGi%H)Oo=r~(qD2#Sl-WNMxedQA z%D5mFzjL7#AGzJJHY;J5-|{Z`=)5{V;hg_`n|69-=#thq$&88`Hsb*6|4;Z-yjqHx zQG~{>iJTJ*-P}u#bd2HTM+}g+$l^^hEvvRxRfI9+&bZ0UJ)c+mq3mO{NlEz zto!s2GX_EMKj*AYGd1yUQ*z$RLta80f{lS|8OZ)?BLvJXsHwbLR!v7_08@HT*Y4P= zY&cPl7oz$HQ7z)IZ#kJf$T%t*4+xHnZ?xPzfa*lQ%?To_1UF=P|6^d*`4`^5CwVY~ zaw)FgV)o z_(C4EzJsAxd~~}nU{rTG!6dYF`rTJl5beEaNotWP9FvFYl7I#C+xAVZ#hF76qQt2t z@lk=Gk4*JR;x;1qTOt$UY;+-~#(&W=2}($P$K!5?Co+QC%{_Ek=5U0q3RE7uw+Th9 zqSn--$OH1Y`vHHWfMu|hEDxXi*gt050^p0oMck4GYTXE6d&_Al415@du{ECMEMU!| z`-Q`iqBQoIr)R^Ge}_%P4x^18(P55qytIKIv_cf$lcbJ0l^#>pMwSQ{KpOs`3U+^r zr3`jQ4b`(g5Bh5uN9;FScaHp2E1OPIT9=wZBHQDyIV{g8&jXeI1foEubX-PrE?W0N zqUi}rcNQ#x(wK%&Z|WeP*oJC&7~>aHd^-e38!mwSs8J1HW7{w&B# z@1NI+3AmH{6BiSF=1HUYV>x76T$=-$WTgok6;8Bl&ND1VDJLr_HZHZR_elI4#Z?I_ z6|@im?EU^J07Lxf^zqM&c^c*48QcfKn^+T(8U8?m)mep5mJ%T+hL+YvQYIN3%#acH zRro%bFV?D`j+_Vr>6q5p1mqjwkw|+*2G};nnw6omR_jBo@+U-@)J7LFOJ*nQd@SOR z-eH`?(D$b?U3#%;Qcrj7JQt9*(ZwaPU?#KbPPT%h@q{kavQjxitz0TPq~M|(nWy$8 zj>=(Lg7%p9n+0%3NoeH#PQ=THAtL^56r*>?Y-jzNFVs`3kUM0ir*e2Hi5vKEN{fy;(K2xB`z)09(6CU@U%xu%^dwfEGYpa z=|-9|q^?eO9zD*OC3D(k4$2F4>F4ldS}&lrCG3I3ejS<}rVpC>U~G-lf!}yRvjyqF zzTREwmG-8+++XP%_;7a(-1)Wj0dP&<0i7|}lK5bC4cdXPx#QY}`?7Kk@qzdnhATPJ z|4~3vpqs~np7eU{&o00*?MW|3Swu^l@I{snhxB*p6Om6--}Us=4+sP)y6S8?QpCdA zG!XaS8x7d3$~ksx#=`XI$H0tOQM^^d)FDWfE}7nD4pX(rCPT}k53_R(*T&TBau3hP z&#^t^fum7=gR#)^M`iz6I<Cj?MxgLunGJtr;vzW2Fk10krd`eKJa84V2VdEbz;GpT?+$zQfVqCc z&`56vVTnYw`|!S53bJKOzNVIggK^bTh#ZuT6Mjl4{3wJvckpb!db=?QRU~9uD4*}) zG~e&%e}*5`d{%0AGn{DCddfjteUkd71rZ?o>74(z6?E=GUwE%In{xv&@FIX6ksf}4 z(8Jz=X?W#r4djE}+2afBcp<(;^g;c}B0wQtyxIE^a?|kwQ9ckmdpq`w5{Ga;C_1MI ze%LJxFM)hO-^VijD<5{nw-+^ZzB{WGnhT31;IFmYv=B;ogH$0d`i?SgSyYP9Q zV$4Nz2XlmRQ9usG_jUY8M+P90M-oPAmts$-R60J%hEzJd4r$|TxZRA22w8qU_2y5^ zBzmmMMIWr1lQA7zxa764bTV+M3vyrIA)*1?vEHEcXt0nrpL5FCbS2;S&Ni0=;8Ama<}RmJ$Jp0ZFQTpS%6ek8Q4sbt}TGN|@rDT&ya3!&FOmzP54JrjBw4mt&8grb*F^rE`r&A_rd2WYVi9g?V#v#BEB+41+!L zYEJX{YP{&^aeYH#ER5W0eL>Nc&gimz<38Qf>+`_bU zVUjYX92<+JhZI?e(Wpr11vDwLNN{7kreDnt*Yd2ZHml&w;4hQaiZmiS3uO#u&rPGj*dc&}zvn4j0#jZ>-{Nr0 z@1F{IvhIKDPs6q_n7s9*GJWbiF~3tDD?WPUAWkfTS!>2N3<^k!+yr~-!`a7T(<-pR z8a_uDo%;zyik=wsi2X^@n#eXdY)fgNUWH1I0HUE?fF9+I60a7?gmt(GwZ=EPq`nMj zNbp`_%A%}RRIgD`{D%rFIwQ5x9SBs(N4FK2WN}S?30z5>mQ5Z3XW2+!X8<)g>nUIWV|(H`YlBr;#dXD*u)WAq9l;ViZE=-B3r&B(8fw|Yz~tL zs0cV(CxSGM(93EDO>c5vEv)jTU8ial(tumBfV0h5HidF&7Ge9mk_++dFmqwrlRuCH zIpl{VcjFGPKwO6MDDNjxCA%z}hy>L-(6yLJ+)q8`I2;c#L)FkzP zDnlb+!qr#8;Wx6bS}==-F;MCUs0Mfe0u(PO@YjK|^CLs*!o~RziwM$-M1;=dsL+f) zM*m8m2NTeJ#K-gk`B7EF>E|F|=QuquogZJWihWBpDw#^AKA)x z07oq)-DW$pc;;$>IEE9!bX$Ji}hoyp9NVObW?|A|V1pvX4p zm#Q_FiWsCP`G&(TyvjA-{ksG0G>CRh1J^F**+!XmB?rsU);s@Mmo@Vih=QIsdy{Ux z#pDih%&yK~Ki@AIT>?NTynv6t$Ty9a;0GNbUk1)CYv<%;;cP91N17Zw&RuG;mye9r zQ=(5eBd9lf5K43)gxk^AEa3}PG+|iJ;|KGsBkXcq$+;@LBb~1RCFkK8o|tNtN^Dtn zKe2bnat$w=%hXOZlCQz7D_kNT$C%Fmro~r{Wc$6PSNbez7wdFtQw9M~t!~h|-6709 zpYw}70=I@q$;r~`i4NFiq12jB)b4e`rQZSd3y#K-Ny!RDI(nu(wHyhx9i^UWpX-I3Tb@^>rN`?EUJLe@xgxafFFumLX}R8N*zG?RaNc~* zh*y3>E0KRp5Y;@LeWbcJ= zpTHt-WAKkY7o*<~tdAlbvodyv-%cWLd<}Uz=YM_l=pVBVAN=7y*P{O`04QEmNL9^{ zyxk`}?mr4Ee&vif5yLNUV)!3xgSC6G6t;uJ?sd=BV={WE!P?aC*EC8=|Y8u$D5A}^L9^N9^a1Nm#4_Vr_0Zg z4)+Ozd&Y%zBeTkfd-}Z`{N)bu_L#qx{l$lO^w19XDS>->!ZCi56aDZW4EL#lcg8h! zQxdKI?SS*<@=xx9&uP(}s~myDHtVMJ-*>pZSM?Z67iYE$=yqeu!~uG@g8SY(lZe^J zbntl?ulT`FxGLwg9t6%Ve&8Td?PDYOoyBYa*+#tLtjK4->z>QAVutY{yG0cq5Mt0G2KvMGcR3t#c`L` zLCaX%CSM-LjS{(Y-ufXN!q1@_Vbxh`Ck&Sv;YH}p7|cN_B@O|#HWvD|zVNny`wDwG zJHv&-y6vta+?S8 zMxG|3y2mZt0dkw^e4g$q_+&v@KQVgsG`h$QHs=`eqt7(U3rDl`bI&fFZZ?TZv&1IvU^i#9Z801&?gRi&m;RAV*t_( z6?LpYuaO&^Y}`RFoEu^MkSdi{MKDPdq{*I^COKmd*o}vI6em~A+7Ip?uu8H6i!s*H zi*{dSTjhzfNn;aob$?}B;tAoYvyISk1X8QQ3mG#SvYY#X={TWfi^}1t?hPhAUeiBw zw9{L2xU#GC+HxEG!P!aM1LPCG6WBA_6A*KxaNG95$4=@4r8A+!ApDiycCwluqSQ8W{7mZIr4@!jl9%P2V zIOY>QT3~)b1@G%Ae&1!1X)ztFT!j%c)2{r6KYen zU=@nb7EKkkUvN#`_*+lDec8_Y zY|Mlz$TEpipr4H!-}k5DP}ZUu;hbgPe#nwJ(l|ozpH3W;Tf5uJDOtUwbfmDUs2-9; zAeO$upcsg@4^W1;0x#i{oafz4EXe~;VF8vDveaamBr`_|W&V9;aaghOBM7qS0ogU; zvFzVC+l1pznHN|Hb`oY=*oZ&T7m5IRRFn)N3HQilICW~-?NQXCI|%*}>nTke1?0c^ z%)S6>3DzLQG*q4{sYIP4Z{Yt|+`m2f{NSH@mp>#4AZL?k` zHkoi=S=S2r0^WAryYChLeJ3xZ5hp$vhaJ`_7q%0KxN}Jt9aaF2YQ+68m3EVP!)Wk* zw^Fe4OVWTej3HCO7V`vca$gC`0a-i2mY=`p58kjaSc)v_#EQ`*b?8C8>TGN;&i*9Y zo<4uxUU?v!Q6g%gofJT=Ki@y{mNEDOc{|=#P#}n(QN4P?DrZl1%rvN($+;xr*1T!jw9Gw$!P`kViXfY9~0gglukg$ zGxp1m^LgSUn!x%`qzU<#L_DS$4(-^>wk6oI5>6rZ)S#>geN2)5FL4$w#^{e@EWb?B33W7X&pi~b*x zW8tMK+{h6&5V5ojaZ53p^Zr;Cr1iRX#hRmL&I)=4(bnK>p1d7EH2v*+YczIUORXKP ztEkBJx#9|ikdSX_`}*7??V8^e{@_Z;&V)wEiFV#aw8y*#lN!m)5s1-hS!#A_L8AX` zmhB!1!U6rf3oft!%2oKHHE8e*g+nzu`;2r;EBuD1=ozE;@%QzEK{hX#x1*JGaEbp2 zm?v}JUFdKpmO3sr4gR$keSYF>Pb$(9W?4QO$>)oKQ9wM5j8Iqc*Zl<^EjfkpE}hvc z6!A-1xkt?Xx38#opID^=EB-zWYdaMSiWv^^CRRBgj3U__+dVmR*!=J7zkvsTt_kFU6l9;Q!8&`R|cQ)JX3KCFAfP`%#v>mJA9%D$fG? za&i)psPIR5V2FCy1;}1eUmsA}zlic1zE zahgwZdt|w?hrFyDoqyLZr89BLyjuR2JtHF@&l1CN%8Usa?@ky<9W|$iGUAR^?s!A4 zG$fXC*xp;DV)`D%H0r36R5)qhlCVS_4TBY-f=sw$XD~x7HsNpKbEUQ_zkE8Ph+?g# z_DuaycH4RD;p!awY-L(`6SEflC(c!Ol=d_6>xkJfeNzoQDF`M-kO5O?<3aV=AQ)pC zP(U*VK1yD&{E7gff89oZfs|0SyGLwgsVc8LGgN+`{F^c{c`<1}%xKW{36=l(fm`%6 z%`r{@mBayE8ISog{EutQ+xC|H8qo~h;Ial}p)%Gf4TUDJud`3}vAiK$rt*(N%+4@# zM@&Z~%XQf<(sHfa|G;VC8M(U@{&2Lue`weLyYcKlJ>WlIzDo_t6ZL5An=e7ixB*K5 zS@bueUKsxX86pQLYO#q9PQgEB4E>E<|#F{3$j`TOK>slDObWeGFz90-IxK* z!|k#nubgPDf3okH+>A1%542KErJ%D~mxNJF^CiO_IYmpv=uj67F^Xd~&`VAgFZ&4{ zYbC`&4;8WSh++hfC9M;K}6gY7LY7e=}#J3|f6{RAyRE65q6a^&? zSL>uXrAyTNx)+5<6tXTCVJ_h4)BzJmcK*^)bj)%IvkNpaC_ib9Ky-EDoLIkXgfEvG zg&OIsbeuzl(bb})y5LTHh)SzEHBJ@L3@cVzTC49m*x!%{&N1>|(g|mrSs&8p@oo*W z1h(?CK~9Mk3N4c|DIydJH-S#!>}4Xy7h0!C*T(a(S*(Wpj_VW&Ope^t`y2|nUK%Ag zt*11c8^R~EE>4^(B@d2o_zedj8&-XI4yt+~ZZ&>T{;e;DT07NKA)O zpn`#sPr9WOIE1JxV1jsP+txM~C2Dinm*>#n{b*Ht|Ij?m!Z;gz^_&2DZUFY~O&sWq zJY*bt9f{`*94P>+S*wIo{S1VI8)Ire$NLC#AU*gIbXgLoPHNgj8P`oc#fH$Hw==q%292j6f+oz5ZL& z22OL<=JYKj0u+JMum*LU)9bL>NGY2YpPU049b6njK2oGWQL{`$y>|UJw)xlZ+-Pfn zAZ7+Mhn}#eVbpldmx3vi0JP?$$UY<_c@n%+;5X`eEim~oE2!`$1~APr3eLsXd%w&D z)T_ZT-q5%r{u#^*5e!s_56WGQZV`afYcDRqA~M)*=hw)5kw59i#4P5pa?t|vJ{djj>=|(XN8bcxzFEJx)o*?0KQ+j@B5I-fP!*ky-{TUZ zOx3<^48zx=L@m>y-dt7(GmHn5b=(>OexWNEkRlkPBR%TVzm{Jqq8TdWv1|Y)8Kk*$ z0fPgkmG5xdbYqjjNgf5F(D4J;SX*9@+f{0oCnvJgNXE5@yC66a|3!u(rF66n%=vdv^YH#(aSu@U#=7F`sC?C|_uRwkcqw!AVkwjdeH6Oz6RfdHjJ?@Jm!Ove;es3l)Cr>lc5RXt26 zG;wO5&5a1{=pw*=Bz_|R$*4^mO3G)hi}&~II2*A)NAG_*l-E;h!BSne8oSXCpv#ju z+WZIl{N+ypGQ@NFB3GFlybtM*}Z z4ZjYC#+(9Tbh_#%_GkKU^+TH z_x|MAz5`naSQl(KwFj&fad8abGzO{VcYxl!9ZNI0v!pX^X}f61wCt>mr)l<8IAX}a zVZfA@?O|Q7JGU$f`5Shv+*ll>Vc9+hO1V^)r`7dgoK@bx2jHNK0Fja;&YLl{s^VSN zS&B3&m2|+hhcsg?is%7D1>>jUK(jb^5Eoty6}3{hGmg{LN#y z>!xYEOON^IRByZ2BtK9@Xe?*+RPHTGx&wAyDhBJZl{*`8i;vPbJAQ8AF_OQh$OP^U zkEdn<2E0p@9%~hz?C}{hv^}Zys~+^oDDjPhjoPSlGMX=H$Sh38DdxJ?%{>Oolttv$ z316twPgM19ClNyNDT{mdAGgpuTmI1_ZU3Xp(Ih1{i8V_HY7A$QR?afT#ZF#**SgH{ zT((}=9x)%pwr-G`$c*;WiwXMe<0Ui)#X2i5+OD0d6AP6svu>dwx$f4 z>1cj%G-2LqM|M+U%r)e9l;i@IRj=VMs-)GHN(RAoTEgl!$8VPqt~gE{%31{xU<(&t zd+8L4@^Nw#63ix?SoOR(ucvevA~uF z32%0=I^|c8pn}e-RZrqIDzaKjiVelhd1b|UWzB8Q!c*x+Q4to{Mf71AK2Eg}AkkVf z=*h*~NB~9yE#FxNTBm|W?e?WwVc7*voo73-ttC)xCAHjbaljJ4kBmyMH5Fs$mL;pF z`5O@0=AAy1iR@B;Ej+DOl7MU(#dJ7X+kq6iw_IYcqFK1D5cD`as@!#KaCB4kU{j^m zzJS$!mFdKYyUNz5H<284XPxMw?kIJ<0F%VljK^|FhvX93#*U%~uvQB*k@5lYB558wlF9tAJjf&y$wef;Ye2pUrs z0v*f(k)c`Ggr_8b8OzVlmzilSR2OXZwYDI0@R0kA)lM!fuq`TfawvPYzAV z2RAlF#!i+bU)%M36t&qc+3UKg5GFJq3mn|UP%zZ0;W0Fe?Nbj-y zHCmOLkUbYy^=R?9iJ+Zg?j+Z42%9aQoOOCHRIaS5!mW`xhWf-)q7TP~i1`RxY(O}LR%9*q>53HRbFm3lk^6_vaz z)O))r&c43Zr9f60itQjr*jY0mtCxd-+ZlMZC6d7w_TAYGD`RVMKA+-AcXv2}>ObOM zzE$|L%PC&2Y24X#y6-?t+eFnFdhgj)Lz#aeh`ksqxSCaVe~VRC?#7Fzg`T8;J=cgI;$*G|X~&hB*NSISNytN}Wu z&93)m$FeZSwP@z-NU6IPWCe*apzDWgmD8gO*P{~;b8~=oP)BW`25KO`@;0;zA^eEO z4othxQ}FvJu|q!2uzdj1>mC~Jj9n3OuLd%trV|;tGaU6w&)daNEwQDxR=CxIrh@Z# z?|^ecn_qez%kIS0E2Ny>ovMU{AZ!eyxVVQ_T zrXsX^cNkSC8!a`Tr)$jfM5t572<3r408(XqS;F|lGD>daFU!*kqk#sNohr*C7$|=4 zH(Y$=$_m|z6B z$VHFEDoD(Qn)jqu_)il9?}mYA^M>cRC@_CIwwS4?%&u0#SB6OcyEE?q^nm^h5Bn*; z>LcngWIYx`#28S6H(5y@e|R;de#!mh!%I|ZrXOt8rL6@0r5Q=DLYm_ zILO5in)4A7c$S7Rnb}|EXS_pxBmGzhx(wGtzZ4RKh;0cnN`uANL7yKe;JA|Himv$M zq9LO9cJNVe6J+>8LoDIwj?!-V(kav1Vd&ic?cK8TC(Hayx;LkkUt5X#YpYj8lsJoTHUsZ2y4H0`X705 zwa!)}Jue;^x@^av#?O+X1xDyPV_DgF__}869yqzRPe)~Pl+xr<>tNP`uqt(ml(Oi# z=8w-`-RZ|^e=B9ka{CHjo-a+V?RTd0(oi~Dj?{e^Q=g64Awb4<*7rx^l`bKI>`%Hp z?bmvGp=@;eH#(veLl$-(EZm4_NvP`q6e0xu%-5seD00nUDRLZy2jFQW2FC4`6I+QC zwqlO{9r+7KUVb>Pciz};AMJVzd>SW-VP8PZF3J!4mAXaDXH)DlCf3601OsYnL%N^? zv<2b2rh(4jB1_op5W9i`&a9rAU1a%h^w7Ivh|fS2=LpswzWmkSAg3M~l;_AH=StX9 zcn6LiIjZyMXJvc_5*}ST#lCyF9!bB1JZ>4hqIeop;Oq#D%!CZih|_b8W=W(<%GAl+ z8flDRuW(!7gFV+gGrR23JoZs6Pp}~E*gfOB{+_fpV$0l(j)CogZhP=nQw+%n_X4QnAk z;j8-7hnLBU5x8>ee#uL^t;0a!V-8W$`3;zqm_hO;MY;9*(rCjuYZP{qO6#fzAvdFl z&=B(*l(%BlT*t*+*E}?T_#>8)6U)5Cnym?sW503t2elb^&%KqDpVX8 z*e9t}q)P=Ia*Mv*yoYN&Fjl|YSi6M)?j!(QT1g^%bGzQbz@xP4E^fkp6p++N1|;e( zIeNo@XLfE+pOZYuI5wU557}J(oIN`+MUA)JZHN5G4yhvi??tczSJn`YKpeW5=t_bm z?AmDG`z7)qBSkO7cxr)vv+~yp>!yII&I7W>C2u5udi@UHePqkNvYFi{`IlEH--f1y zXVRowWzvN-?>Mwf1@@|dblL^Q0ccA*hTChjwjr7wJN1DrK*Lb|j~5#RCmLl#IBT85 zt+X2Va)o0_)&Qt4-s;o7GsG;>KWmCv?IqUdWB~sI4Zy^zLroMb1G@GnmS1WB6JUeDl_Zd}>zF z(D%W~PkKEaZ9N?&ucfGvwMf8Ko9wr&vDtE8G1(#dxDmpt))0I=UZuH3S>^JW=P*nQ zdk1Z_EL)z|-(&WXG1g-qHkaM#{la=6A0yaBUw@sw9MN!<^bbT8Ln6vsI_wfu%M0=l ztl+Hf9f&%pj5xDY^N6FGH&8iwgsSRR9LrkR5Qmes!0;CjL+AyVe&RM9^!6~sC0MuZ zK=&B>&8CHoKv7B1kDd8wKVB}}?p(8Vs#KDoH;opbqZMsu+waCylOao~)YDs>Z@OBd zcJ~)qy=mL;w=~W_%N8AW%}*rN zOWDJ4>rAWpJQ3bk9NhUlF(oY00_zelk{(zdO)6tWT>yiol`>V2fbzzcd~NY&OYTAJ%2P~aF!JA2>`27^Ras^|)7i|7;^elhydgT9m4a(lk`lsylpM}08 z6*o`CCA6<=OM0dUQGtFEP=9}b6d9D9T%)fjY;K;ItbU$uRX1#mp)Mne34g6Rnp{Qm z(t0~+N9)q+y0V!ONuz8nty0C7(1vXuQHlFCi0MJsG;gaUMe1zQnJq)jp|m+=$2wXas$9LT+dR<1~o+cL!Kp^~EqthHA9 z!1p^}PyEC)Q%u@bv|+)QN>5&LU|xcLjN4k-7*kip0RsVE{9%kcAxr~T`BM6^lL_o> z=mSYlVa#r-oEkG%bwb0)$kef@;lKTgr59_KmMome0Q=g41udOQ#MMM`1RS*>1$X(g zK6rz{P`EU_y2V>tsGZWcd=Zl%r#mD!}4cQ(B>dA^kL)&Yemoz857j5 zE_t*TXr5YSQOxz_Dh{tqLnQ3c4=G@-JolRHKlcY8PVhrX1VlS zs52CqZ}9H)q!!DP32y2>%b`|hEC8B5P^keXY-fuX&N+=bTH|QL1Mbp29Oy4qqNF@! zn+7A>2XL&94CJ;+L_JLAh6)EzJ7b55JKv|6Q0*X5O8E*1)(i$5kZ)z;xQzYehN@hR zkdD*PS5~a(8zRA>my3P+X`L2-bn47nFim}IoGIqJG8a(8YB9%32BtQl=AFhV$YL0U zAgrTQW^ReT4;6=Bkf@{&NM+}NBP2*7}R_%onRj^N6M>J zL5uf;05ReWi3B@kj;(&m^NaKv3Iq?Uw12>w%_@=K29111S95!+RB<@c&PDcPmBF2Z zow$<9-hmRbCgT4}?9Wo$80oB)(3tGHaaB@1Iul9bSmM_rqlZrC?s#eDZ!@F2Fjws8 z&>0P!E(h40GD9AmJYn9QJbzg@;Ub=Iu$LPA(s1k~=;DMQYee9IUZ5>FD2t{Ov+NUB z6=zcxx8J}?*>l6&iE9W>z18xR8PLPq8G)g}!lU0Oh2xvOW7M9z6Z{YziMcBeCg#*0 zxZw0A8^(HRi`LnN8zAfF4-4-!23}>Hd3k`v6id@ z*Kbv9UH9ud!(H`MmKbgg6Q)FwTjJK2IqKQTm!lCG+pX``PEsu#v$VOcUrLa^+lK}A zzZ$(sr_sq*V|&&ujyeaCv9_SJ5*IgMtyA7SRzxyaCfA38YH8;Sw);qU4rXI;Lci1A zU}bq^wON`MZP{#WucgFyTO{d8aUZ14TNJjWn!z-hhi5iYZB;CORud9i4PIvCH%~HF z#l&e_v{qlAu_Q;d&I1gfVGbdMq*NIM0gjUpc=^w2^$z;1yqZ%eC# zJEMgJfWU!9tx{B|H-5!qxu%TuPSx=n0m*}Bs+ZY_-_-z?n=tpm+0ZD{{@>~hfk&YR zXd@DFy&-E|2cmld+2Qw#_Mo+}Ss(M>nt^n^`GYn_a{Fx|p1qc?9PyyChKrF)WGf$MULP)>+V2dR0oLS@n6zBqDW8JH7zLMQ0V2g(vo zS+tA)x`I~TiOP{BR)(t5kZ#stC8KjMY7{f{49!R?rWugTcAOGFkt_aqi1Tu zMBW%DPvtk5{34gAEpw6*J#{IXM zmNar{$LwA!y-uUc4yv%*f9d2o1fKN`F^?-U*)nb`g2sC{x!`PiS=h8H#w(#S} zSjr1QT0N5^&o_=-8a%`G7#1`@aT#yZpL|pO{#6l0-%ZuMdH!OY5j2oG!?r=x*&Es= zdZVMC)D?Mg3vJyiW6R_n{(eh^--pBiF=`m_rd`Ov0Z+M@KQ4xXSene+ps3m=DO z=BW+xW&o`c!1Yc9sZ8Vu`>@E6nP;5sjha^gYe)Iw8N;F14XZ7oD7fhSVVKXv7UP!S z4lujx3QF@_Kr7hXU7zixvr9rb`t___S3~jR0xECM%+IDv2OZ&-u3O(;G<<{Te<4RX z*CD0k?2?RnSQGa~@*l8}^B0+PJkmZ&-7C8k+dHs;h2*2h%lm0*!k3Cp<~vc~}~H83lOPmhh$qPaP# z`oQNGAVMy}{|D1*D}w3XG#i-ZqFrDq{b2gWiwD)13Cct!5Ta=TS&<9NzpF?^_PP=BqqZ6LXo*a zi0K)BmPT>ZhvN;8c>}egK5HxK(bLbd&gLw0cw}~aJbQMpk4UGn&jr#XI={Wo1<@q> zfM%%s)aI7q+u41MK3WtyT8dl85{osRE4M-eQI0Dutc6j$#Q^Ip=^Q@aEyd^`N8>)T zb}?oC8F4k9GEQoEg_b|33ZRmV(3K9bi#bFfC@Qt9_)-<|f?X6!`=Z9shmwHKvrO_z zpa{BY-o5TedezI=E0?Ik5}i*pUTxRxiH`bN9*3)l&|88pBzs4gk8j@~kSzd~QGuHg zG+ry_4!xpo8*hzJ;|XONy(KSDZp&9~@(la+KM15x@jVhjKl?_*bJIQvq zPUC*+-wXLwY0QEoWT${NJv3$13Px~Pfp9lc^>L^7^&$#NFl2?K?iI;ro$`|h62#=< zx+EXaU8dYZLFK?6C?H=S2YL{-V&p{ULc0KD!)8QNArE-FJNf))_MN96e)-_;o|C;E z=yJg8g)hL1KrIs=y>NHSM$_+0AbDcNh_hq&*?R^xcq{cAAPw3oVCo#)j10VR`uGq> z+Sni41l?JEy>lJ4BJCW-+}L%=h`P%6Ka+m==8ezSx2NOJ)+bwIr#Nf%_O@eDaY_!< zD-GbwE^yF7o)8$6N=KKgv6xw`@h;3;#p}h!AFrkwufon{XPsL&kyc?7(?W4o;1E~A z+AB+>uPr8TuEr)CL0%nG(FUjE6}1Xtu~^u3FL+E|ibkZ%B^0Q`wEPWKk6*q{jvoA* zv%9x>;Uw6yf$6S4^0W7p4Qo+Hhn+9stX?s5@Hqa~Ebi{cVqW7g4|4)4U6MbbM@zpK zf2ryf={W)uaX&n;ip=F-ITj(c^gboj6|(o}F2Ni{=syO8a%p0=<#kmm*+^IwGzZ2- zG+^8r$$(1AeJuhT;-~sRUj0&MItg63ptC{7@E~nP5Ni50=2O0T^TV~>8-@Is~$$B{{DKj-_LcZUGISv0DVUR*8OC8{X zuAQ-(S&8nAqzJ#?+?e#$E31_HV}y)k{w&U8IZajr`BcV@ll8NeMC7q5&mW2JuW?`T z_qd}iEixC3L_LI?kwYi#3xdCG5|xv)kjnya6@2)U001TG8z_#JtaH^CSCv^>4a9+V z;aW!Z@DjnWgYD^=e(~E#c-eN#6J%7zB~~#nEk6$}x-4>`u_)we2bqAh%q`(Q6@B5m z3_O`{Oc{*tW^MJBnj5W_>mwmHn|fLRG*UXof>_^fmdZug9jpk~d|JYCoRQG1w375uk>|`BKGZo_l_H&pQV7>6U zHA0-t#Wt2oGiagg`p!JuFlZ>SvNeP&YI)K!Y`A8|LP~SSGPyv7@!8h?3{r?U zSQdKtJ&Z+DV;0!F6O+~7Pl0x9%y1J`;KD75M~CLq%J(CJ6!Qnr z|LD38gie^TdWLRJlauZ^HR%tYUjkrpIIwJf-h-Rm0AbkZ18D~N)_I|^jBZJ?#yBRO zvVt*?Pib!^3HeJtsAv2I=ZkKSfblELN6b}wxaW=l>O08}_S-=9v`W|@C3(G4y#STN z8=8K!6}u+u_>M%wLt3Wfg@19Hy>0-nl$CkZHO^X5k%gIho6;PlWPw?E`I1vC&}&kp zQsp*rgOZ64W|U@>fRIcsgnQdi;7I}vmL+=)jj5*nETg1+RSw$=T0FJ-6q%Hk^SqZE zeX}jGo@RY*qs8iWe6abenifN=t*pmqVb?JnCQJHU?Sn3>f(O@ym(r~P4i%2QdVF|| z#C*4-(a!E?>&$zh{fxvp11hU*o&L$_SK33uU?&&x5`ImRfn`1K9&}h!CJdXO z^ye*m|F#d;@y&~@7%3uSW+N=cJZc%!a%UXqu^i-!Y?}@}d`w|Vz(HPyevsgmWI9U7 zC9d_(= zY`deo_J7WI_St>U+5dNMt+B?!81FOZta?|is+abuN8nCSHwfh)c5{pzJ=VKt8}O{z zvNJ!W^CA>avf}w*#}5t%R=!{;Jk^3*A@z@aK{%XaZHVo@_AxKQ!`3s$+i!?=w{!NJ zOBT9(yb=i8ggN#jrz07QGQUTCg`DI1%m;~H1B2^Nzxhg0c<=_ff$#wgx+4RD7+u5d zFW*V$3^eOAcs^q4Nf#tMeGeJG52>Le?V6Ubz&vV5ymFSm!6qoV`XeT4NRN021Pp~b zcAQ)lw@$C}%YnbTgPq_}-hJCuS2%Vs$(`gfkwHapH$PCcZ;v>1y16JxIWIgo}`t=mBV+SDmoMczoJ^EBk+b4 z3T@p;KL6fC6_h}e;X#wnD2kiG!RO1sWT-k96o&W0`+hxRGiAb_U*qz3AiqNt+z2QG z3YoHmfbnt%ey635+i4^&#g0~mz!!F&*-FNQWs%DZC3Jvnf^e7}z#aWAF<+Ibj)9mJ zCFn7g646Jn<#&XLE_a*s786|mwVs3^zD59yry+P)5KBg%wKa0e9YJ>F%T%8(XMsav zYbZ%|5uz>x+>%fU`=c|f#QnD%5Z90|bJClKu(~4TviQ&}`g##+O2Vl>6v{C3i8J7v z8qx~8J|_&l8|phk;`~D2*=0QVEb)jA_6`m_!qM{$CJ<>N+ruCg z10F4Rb&@pL=XIc0rpi{WH8eO#jIi!4*`zURaBPUfwL(lKrlMS|(7)zY@Mcn`oY9a$ zKNSdr2Hed}Vw#C`V22uhd^@qfCU4F)cui*^uEz>waCbnTy<9L20qFZa+cd6!%~D&iR9NNDyzN{v4>Dv_M|81!we}KV{a1(46plXMX2oEtqmp3Wd>v z`Y^huw)_npMKb8-gagG=yXp9<z(g*U~QYIpc7KIy3A0oo>lrJsblfXX`nozce|kT zkU2a=uYh$j)icU2=ouX>@jc??<;wT^9cYuX^2wg~qMu99q%u%N!Igd(X{ls7Ar#^o@v|?~ zUt@a7)jZo;07)&QfQ*B`i|Dkob#gYab^d28Z%DG1(wscv$VV7@<{~2?-@}0dqDYaWZAq9D0uT)zD>r96SQ&)KO5JFDxHqJGtOt>BMrFsh`>7 zt6M|I`D4(u9`Jg|z>XTAb9_9eD5O+-}b!_#Qaad^+@v=$Lj$Kwx zD_tM<@c7}|0s<)oR$l_fyK>XEN(R{DwapyzrLN)PGiS}FVN+QU%Ar<0X6?_sLo)@d z>ynJZDq8fC6pXOw6Oy`1a5_0eIaEFMd6G)^Gsz8N$beS5<3p66DT8n87LN}@#00<7BxZ1RV+%|@wVD64d0TwBu+*s< zbGvQO*q~y?EPXP$SbWxX^IFCK@Rf^;MS8{Va!gFgMv2IxuLi$Z^2_8#Dn6=}c0msg z%gDIY4*bQiK8ZoUq#-I+=-rkJxy6tzv-B(dIlkIt%z5wS%$aGK3S}nBQITJ#huGb@ zjd^zo8D0Z)qq>(j?PMBkh6StFS-XDD1DEJIz5qD#4oMEdNgvcsVK3#5FRU|%3^&D7 zSa=v1uLL*IU@lQ{b$oW8#r7-gwMG`8KJ5;pfLWHi;7^#rr2aR`ohT4JqHdZiv>l6W z-&A+epA~}*AbP|(DC%T67$+UF-a=PfB*kT+Y){DXho3c=cq~+pvY#Oz2%Wb|F1xSn zQ4yYMF5Q3})qnb-x-WNe<9PZzd)}aUJ|IPS3pnFcbhlsngiLJ4Tv7n2osR^KEkmCOdYX7Ih1CV2*tb+RfFu{@i z;H(+iJO`akgc0Hz%9JnPhE^n$APAE})a3vDc*26qm2+-Fy6(CUgXJ38XqMfh7Ea)W zAO_I_=Le$9T?qa+MqP216F&k-U#sckI}q;WLaUmF2i+Dxedv~!i;Y$&eF$9qSj|!a zGpzC-q&w-}pE0Vm2BU5~1cl?mlZbTdTOA~YYY^zQN`-U1@4{U*2eI&{eqBWhT6~|{ zHxhDH6>yi!d4QUq zopGi=|G8;bNR}lB3u~;<>4*D;ccTyYWg&`ezKykh?-(Z(J5t4{r3_(hJtUY?TE$PB zsDr}m_2WIl|0j#v610=4f1TgN(Fq=IW!&*F5Y#Mxc2*$TYEDk@|%%-VK1`e1%RgJiF6xB+r#8hKPtv za2+1%&=V@FJqQr8F&axI5Z^&;&Y4hWyTFx%hP_9~9bk+2R{M@L5T!7-cI-VBhlu?O znZFpmyK6ui^+x;QoSDQdAnTr5+&U@7AR!39jbBmc8>UvP;# zKA`mwoI5D8<>x-Z8msjb3WxjKv{3;AyrzLhuR-kCi2)S&^{*k1nY?dbOh`MExeN?p zzW)fke=VoY8UeQkZES{;W5`SdwwKFqc2dz+3yIOdG(ZRnJv5*LuQtr<1qQbb3c*Cd zo9}xAV`qOt--s6^ULp(LNDOO&3lFUsz$r}^4pOa@m^a_wB>66KEuEKaH5Lsyo|6~l zCZVQN`GP6rMlXTuf+7YjZktHdlD{H@yPyM7EZn1bRwRrwO$TL!-nXJ!$$273G}WDf zHZ>)b_3ZCZj$q2BYAVai!mM!tU$BPfKwu51gXWQZUQm02v5Q*lkh5uLkG%_eG)YMdOS)vw-_DJkvF_QyTV^jqcEj-(2A&HJr%o$V^8EBb>5XAJzg)15?IRmQL) z;#>H7zOiPOutbc(NydS8QpK9kTHW-PE?#|^>kD%tSpy>;juw-E)KheGtaLewaYUsmhP#TG!{2DN-|qL!>G~X?5^A~-@oNu7MbOJe7lY% zKx9FG;R{|!&TaTbExZH63$Y{~Prvfw(#_C-JBq89?(y)3_RId0!v(2B(%>7q-8PqY z@8c6BpLr#G!`h_zknWjLCFifkcF-&eWc_~94D%NqXYwmtR(dDB-rITDmCJtzIK_$!0(%J4q9$u>MqXDi$&CXlwHhw&Fz6k_@(R6zdb^yD z;~8XgIH>%E`nDbca0RNxO~f`-Xs8Cud*DDlBq*5)1DDIYH?0H?WgS|>l|bn4_+wGI z;&-_o3x0&qJvVP3F&iIKp0+5{e9Moy-mxDc`FP?7u;1IS3QA&plWj}__EOXe)NzGO$t zL7h}dX>Yy&X?yO6u&e9P;rE7J&6em#c*zzM#sE(aWnHcvzO0z}9qz^T_`Tuvnyilz zz$lI2V0A`FVUb*^hF+~Dc#%fAUD*bAVHjMR$@eG5B7lj|b@7S$^j9|x_Z5?0O zF87tM+ec)XKczaaG1(LGfh%ptHjpzcT?N@FxfV7VeNFA@7*8#3dhD%aUlJVIpk|!C z;nIUbg^78&31Gq7(yrSt#!L3>rtY$FYMZO;nk;oJb64Vb>XcU;h%rkGKZXyQ0f3b)zM9g*!X96P~2TQ0ke(70Af=R;-y#-x# zzhbiz(S#j`YCfv}^`Y&sHm~RN7lU<2Lrm1xtK0}TY6Z(3+Ys!=S6lrRR&q1M zA)GG(5&?7cpTEXhfma)?^iuA*B*Q7&5AdV0;^AXILArbbqY=K6!E1qKvHx0`d%U{d zYNwTKeB*D_wSqqm?5Qpmz6DuuvxogpgJTV z^UYJKfbLTmT?(;%8H2q}en0KYZS}c^YngBF^_H7FwLRuNaD5k&!*0|l;tW>rE8D?S zVT1QOR90{Rv3H(WiW@Amcg^(c?o6wrt8lS-hBla*&AVB6uJR~Lj|EtcV60vNhJTtm zHYPFWKq^bTZhYCCXl8-RW zWYZU2*$H~y(1$U!b0kxXq9qLUq`6i4871{;IV%K{=dcpLgMwm-1!qxGHjN{r9QTkz z>$)FQx_3 zw5h}udrvZJBg?he7yj5Qyb&}K6Gp9Pg4z!+>M`Dx!>x)&l2F%}8K2PJvE>O%WS?1K zyoJy{&wiv1_29@8Y}w_+eHB~OprHbrxaCJ4`z zSAT*bh&hPNa>2F9b%{4dH6pIi7EMjT2sTKS-9@XaqItu$W8GBox~X~N{2@)<@ke9*QVzC)pSMChMN09Z@1t1&SmJ~me8D&M(%exjK9_A)jCZBs z$BygvJ3gmfxZ{uUDAUg4;pq)`DYm!%n3kbYJ;_^Yc3;}058A8`;z_&0XT2Y1Ep)F_ zAzLWi+d643uIH9(9)x*?Z#SbU38BIyD@!vOG9yVoG7~f13x@VBjV`vEvzx1e-x&HZ zWJ#G<#F;v{5;12pSc}PbK3!MlkS#wj2ARrOW1W~hdy-*e&1;!Tqhls@zC`9Jt0Fv# zxwoLpz`-7kS(~vXP9an6_>3uQUgAoYC<%>WQZI5~Mx0=&?hb(V)U9$dr1Q*wmiz83 zpcywP925GW++IW2zy9VK1Zfh3lb=D2qlsX8Z!8^G2jAtX8F#LTv#jf8$DKXaT#QUp z#E}zi#*Wl(d5D5?vEId+@;Ti2>-$8|i7N{3wsJI_9)aW~F80xnIh}-@V7dTt5#orA z1ZVBxgF@mpe6u<9X*c-}UwJI~nF>@Q8XvnNs3~)z6ocaj%^#y{o(zD?p4|xv_2m*X zQrrZJk&_)|GP>JHr>||J*FOnlDkj;m5XVJt3Af>FA0qogVsNn(oChLf9ut}CpO z-8P%^g|)^c4B!6v#6=WTOb@-Ak!n$?pnaxqY`h?8#kBaNgO$$PTd@MFW`~l=u7Evf zCC`i){J{N?bPn=vh#3$Muge(Mf*=g;22YBtQwF#fOV;0~wz{os;9gK387-m6vz}4` z_f+IdKbORtS;_c%Ad+fmUe5KxJPy$+*S(9T=z*{>{hO5w^f`59uO-d(V!Eq{Ldm_4Q`YfLkb%c`O3&0n$8W%?7 zDGj7dNXk;pjEdO0POi*`MR8+$l*tprxm|+;$_5Bw+a?}><2okD5%ZySlnNIpQY~IF zYH*Cg(GJFon#?BemWrY&m;E3z&7-maQLa*mk&iN2i>W_nu>Em_TekzhUZx;l8aLig zE_?{BnZF%=q#z)*0=h0jp>iErg1yRFsXTOVkVD~!XtnPPDl*8_4{;iJy_b?8%#9w+ zbQQ$o`9P&32Y%1p3$gG9C7M|GO{I%PbIMep1TBlq674lby}!L(-)dx$U?A}g%P1hr z`@U`Oo+*%NB7fU;#Pa@an!3KCn5F8xOhreQ*R zXbuExu|jESGA{kxnIv7r|R!9I^_`bl28TCa4-=#I3E8+$K*MpkAUGXCM$w z$r}i{cBELPWzp`_UUHITOd2?AYB80S3}lrs8zyB72&%g-W|e`mO!|#aPMnRFe_A(8 zOwhnf6ji~KFjc{$42OlUuUJkoJD4qI9{=UM=^Vo4dS#xrKWWHSm%2x8nj2_4 z8pcGjk?xJZh;q$0CUaNc% zo*gvtNUzDqAeViTP7{|^d*f|54EzR2R@<_9(1upVXY`_@lT?)+qX(3B(&f#ID@!vt z^vi*Y89>+M0rx89g%E?EVnhuNc8wX}0gub?ilkF+EH=-L7co*N=<79ni;y4{QJjyNpXND!`CIjou$2IBxsxAVc~I>e^x#kDhm z$AUVv{{@JGDgc;Gh=TFQFpC=J#Zax22Z@#KVsFks|8YatdJt*HvJR0N^OJO2(5usx zH1nexfla$bSE8*1wbx*x>bHZA!+6lLW~WQ~+^!zGsW%PSj29eftHE5f5i=X!_?8t5 zn*$I#Jl+Acl!Atbk^}CnsFpTA2&bs=ND!J{H(0t6NL<$h!|9|%1;y`hvQm;r)x)fv zzE!i6#X1C)uU{An?qD98gjvb*Va{U2wj1NHAvwWIgv7Z%BpuZoN4o1YN0lB0?iXVE z)y|(v>yAZnVx-@VBlTEI97|W#ZMZx``@w%zn~|&od8i~ifK~k*nPBoYuNs>Cf}d+> z?VIS}4x`s3M?D}|c??Z68G&W!K23XBANc zf88ulN4$E8?MJ!F&AE)X+rv||z*FRy^G9avCEKD-1vV~K`2blbT_Co1$S%#GeF2oa zdP04tLfA4ec(dwGf(~x>q3H*cZ%9}~w`2L`#1>-d2$^&G)8Lw6F^wY>%Wr{$25UGn zlR3RricmLVmnxxFV4_QZBN4AJtFRdR-t45^XAy!GW14xlCtceoj^ zd77U?s$ns$lb*!|NI?(&wy#v9Ml zPq)?+*{kzk#1L;RX5$jL2XeD(W3e621-HQOf?mPUSC}5xsD6CBQoP=|kyli-y*RH9 zPt@-;-eFYsnLQ92uN+5*HGLwljIF^RRu+_1yLOpy$|XRKNFy(%-G{jcVbMuF>5|u2 z#n7Oil)f39G(6!34a>wlb$%cA)tuyS4?VB^hV}%)7l{k42>c{##E4W?WiO{+1k=(T zCGws!B=YV!9bMtK9JsE?s~WpTP;Cvne-$uknuRH0@@mQHhI!8YCg8v+raP+xe^^(c zx4qKyWl7^;4_A*?$|P3HX<1)TZIp-DTf(@{BAN_e8JXjoTPSQZ8EbjHY!>>!y4tj| zaR~5A6S%QSXn=ec0;m(GIRhJoXzH*$H&cce_M}(t@Nn1eva%y@ftvK>c3|S9^kJd+Ep=wTVgRxvNQ z7N0MF+U`g)0hf*dwuw2bP7%<;=rwno)Qb%)LdoDl;16&VQC-cEg~uq|4@J!)ePjO; z8 z7nt!_;LAf!z_4&2wKA-*HU2cpxe;`^6HBE55h*dmNmx-FcJ*uG(jkNz69!hgnMVhM zK%5gxr))Ie))WNY?5jFaq|7m?k0DQvY&+vFwvRTV$T&k8e4y5jc+ZOH3%*{6C4cT^ zy(192GVsm;gbsQp6T1fzKY_|BPI+3oIB~Yi=x9DP_yUtM1u2|Hr0G6%@LQfcoV;j+7Kt$dzTH9j2pH6r1-Mwj6tIT0C z)2$r%a$d!dIZ)`PNqjf%rmcsz>qgx|wv0Var_awny6b-FD$lEN8FHM-7cGsm?7ml7 zYVbh2@oY6_$7xxrNzAq&WwGIWw{e|c&Qdp^<{v$ndSR++0a2S?xTC;7+jBc%gekUN zCjYXgVlM3y;bTeBEZUbLaQ4U(5lcRE@L2v%#F2ButA6UP6<+p2pl9bdu&<;FNxx%4 z)&A|~_q$;4wexicfG02G?>%`6E_Ti)GIqu$|L|bMDB8%)0gBUIELQ1I+I<3-M%@!( zlRvTlIgZ{hXj)&0^8?X zu9v6Z$MZW3|K}l45^jY$a2-+2VI?$)J$H3AhGIp>Y&C8rhCap`f*}W?K9Ug`FeTjF zarZt%S}gE^PNYWc_WQB=INS~R%*kH6Q`Z+^zd1NgS=OhYsF*BNJf-K0@RnYkV%4ga zDV7o4;B*)j`zvr#XkMm5$E#aFL@_F_9d95YYAhg{k~^5s)9&k176#n#`P5*ha>NUh?bH#W z-~em+ctLsA7?q_@AX6d}md+@pI8i8~QcSRdx^ejW`nN`XGh)N}z2rqdcGG|p^|_4kbZi2mMRm3wmoDx2fGhJ5Lm*}PpjAl=Nw`%J z2e6q>nTFAzs~e)pnTj!ON5h%DsfhcoGNMTa4v`%}3LRbSDinQ5`73?6>%mG*rW=Ct zt(rAmrFyl85U?{nj5Q&eO`j)=FPm5@Tq+k{5t`FXSqG7;k87ky@H853f}+qnKgIR zn11rxCH|`FSI}ZxODQKkU2MLXw$twQh2pk;XuLDbjYM(6zCw^-C7$Ru#XguqbPEMkOxvC+9n5BYhVaOv!<%kj9{Q$$;Kpb?PplJ-=~5QO zxQp69wpB$UK_T9ccupcQM;kxNirg$RE5uWVa23DHWP43JgVswdjk@Cp^jFuia{a8m zFF>uC0BZd=7Rmpk*8eyUhbRIxS{{|h4(C)AIywUK(=%b|8fYZSD;N=j0`+A0ERBMZ z__402gO|=~Y_MICD8Y|Ggr8wdn`)uyh;@EhZkMNBkLz9+FVClI2tY6t6-Oh|*cubX zKSDUb+5 z@pBB(xC2do6W7|6fAz+5n60jE z9KugBK5q@h?}!$)n_^Fk7j+UA$s~c2VcgrtRR?brrWbBlItXZ`PwJuW=JT4J4sv%5 z`+8FAjBZnb3pmDO3?LGg38g0x3;MF?JI9KZcpDUwn5+|6CFRg3aSWlZu3$}6r)gBS<-e`1Z>;*+~$@r`A zD*!?S^A&cyJFF;&8Uj~^wFuBszv**nfn722Lq(pTJ z|KdPVs<*qgL;H-5|K88@j)%!8!QR8ZbAi>Z*%#aSmmhO9e4D)uI^;oodXoG1Q3-LI zC+59fcUfZ5&07+CbJ*~`u%zu5d?;@dq-|hgZX(_|^{{2`aALRQL53Yl&tO@iVQ=)o zeiGx+bP`_|A1_}|(c$Wm@LlK-(9T0!?sK4E>d`DQ1Y_9X(%sh4RL?;;{a!UeVQWF+ z5je75R&C)6V{8;&1brJNb{4KZ^d$!G+%ZB_l`$mMrx5aEkjAEBs%5esO1OlawmCpZ z`k*Rj%l+=w7>K=3I{|Q<1W~$ZI?pOiw19N{7y?fkINSJA@*D(@w}ix%f;4dEcUDB+ z#XiUwETkno<*Qwr|ZEYNF?L2%qiKjS@o<8>%uMe-kpu4-s$<_2mYYx>eo1s3@0%JU2 zic;`|ALWB*d@7#kBr}TB4cHPHwRSsOtu570oPi=nfuF+rrW8zbqZ6QtN{}@N%y+e+ z-X2?8GkS&z&JAfWc$tBR;w}l|(3w)cQNoOV6!~HAV{x9rb#+HES|GN4Fk@c$TpfIR ze&$>?orWRAe^<5*P9z!!M6>-Fio?CnhH?&jLL4U;m%-G_jmwSepuoNv{3n}FF4u$< zo(BQcUDYfmNQnL!!Dn&yQ#qoX;KL~(bJM-P7$14{t?vh@Br%3=(&w1qijYDICt9n? zfieY?`aMf6rhxJ@KR1M@(Hp4hvXqy&2N#a!K~5S3eqrWPA@4aqX>BE!ajEH!> zOkfJ)E$X8xsHsA!7Qs{0jVHk*MMCGF3c1GYg_AsY_z;A1TVz2kxxvu>^O2xi`HOuk%1W7 zttorn5sgCSIFTOKR~wFV%rCp)0#4lSe+SdAPnZnb)Kg2~IEDeKJ#ia(MGdUq2%XV0vzQdxF5iV-q z3O_U@0-iw=_o3p@UF<%v*l%+iR=~}sb~VTT+*0r7p;JKe_nLCnfd7 z$|r2Xc~yQ$wRVCqIZ(+b#-{>A4_lmF)Zhi|8t6ng838%*Rkb3nn;$r91VXGpXIYKt{KBzwwY06$o3Sd|b; zJ~U9V2c7}lNH-|*w#cv(8wGGYZRa|Rj`hX47;{uHe3)X^Kf5|*x?tBy9q1QGu-O=L z_BpCjK}F&%h=kWF4*YBjVspH)&<8$)7~0Xbaba7tR6!8T5qbiXcT_z>q{JU- z%v=<4P$t@#E{cNAo?oPd_S{R1YdhXkd0HNqo!-w zp|Fw(U&QNvBhe$$E8`4JC@ag&S@>m928b^Vj}MuUDwL=wN}_mInAU1=-7lXiv`1pz z8-^}&%8k5LWvV+(y)6knf^+{R4nS#uC&MlSX$JTl|D)eMX0Di~2DC~y|^dIqCG z*b1#W$M>w(nODu?n$!4?@CwO+N^8&W>_vQh@2PAM=0+Lwg&9HTpSHPkribfbGHYU6`=XFp$ z)yAf`4~rIvZ&}�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 diff --git a/main.nf b/main.nf index ae2df75..f65d684 100644 --- a/main.nf +++ b/main.nf @@ -11,30 +11,15 @@ nextflow.enable.dsl = 2 - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - VALIDATE & PRINT PARAMETER SUMMARY + IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS / WORKFLOWS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { validateParameters; paramsHelp } from 'plugin/nf-validation' - -// Print help message if needed -if (params.help) { - def logo = NfcoreTemplate.logo(workflow, params.monochrome_logs) - def citation = '\n' + WorkflowMain.citation(workflow) + '\n' - def String command = "nextflow run ${workflow.manifest.name} --input samplesheet.csv --genome GRCh37 -profile docker" - log.info logo + paramsHelp(command) + citation + NfcoreTemplate.dashedLine(params.monochrome_logs) - System.exit(0) -} - -// Validate input parameters -if (params.validate_params) { - validateParameters() -} - -WorkflowMain.initialise(workflow, params, log, args) +include { ST } from './workflows/spatialtranscriptomics' +include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_rnaseq_pipeline' +include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_rnaseq_pipeline' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -42,8 +27,6 @@ WorkflowMain.initialise(workflow, params, log, args) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { ST } from './workflows/spatialtranscriptomics' - // // WORKFLOW: Run main nf-core/spatialtranscriptomics analysis pipeline // @@ -53,14 +36,10 @@ workflow NFCORE_SPATIALTRANSCRIPTOMICS { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - RUN ALL WORKFLOWS + RUN MAIN WORKFLOW ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -// -// WORKFLOW: Execute a single named workflow for the pipeline -// See: https://github.com/nf-core/rnaseq/issues/619 -// workflow { NFCORE_SPATIALTRANSCRIPTOMICS () } diff --git a/modules.json b/modules.json index c114074..3e848ba 100644 --- a/modules.json +++ b/modules.json @@ -31,6 +31,25 @@ "installed_by": ["modules"] } } + }, + "subworkflows": { + "nf-core": { + "utils_nextflow_pipeline": { + "branch": "master", + "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "installed_by": ["subworkflows"] + }, + "utils_nfcore_pipeline": { + "branch": "master", + "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "installed_by": ["subworkflows"] + }, + "utils_nfvalidation_plugin": { + "branch": "master", + "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "installed_by": ["subworkflows"] + } + } } } } diff --git a/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf b/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf new file mode 100644 index 0000000..d7ca091 --- /dev/null +++ b/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf @@ -0,0 +1,264 @@ +// +// Subworkflow with functionality specific to the {{ name }} pipeline +// + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +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 { 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' + +/* +======================================================================================== + SUBWORKFLOW TO INITIALISE PIPELINE +======================================================================================== +*/ + +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 + outdir // string: The output directory where the results will be saved + input // string: Path to input samplesheet + + main: + + ch_versions = Channel.empty() + + // + // Print version and exit if required and dump pipeline parameters to JSON file + // + UTILS_NEXTFLOW_PIPELINE ( + version, + true, + outdir, + 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, + validate_params, + "nextflow_schema.json" + ) + + // + // Check config provided to the pipeline + // + UTILS_NFCORE_PIPELINE ( + nextflow_cli_args + ) + + {%- if igenomes %} + // + // Custom validation for pipeline parameters + // + validateInputParameters() + {%- endif %} + + // + // Create channel from input file provided through params.input + // + Channel + .fromSamplesheet("input") + .map { + meta, fastq_1, fastq_2 -> + if (!fastq_2) { + return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] + } else { + return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] + } + } + .groupTuple() + .map { + validateInputSamplesheet(it) + } + .map { + meta, fastqs -> + return [ meta, fastqs.flatten() ] + } + .set { ch_samplesheet } + + emit: + samplesheet = ch_samplesheet + versions = ch_versions +} + +/* +======================================================================================== + SUBWORKFLOW FOR PIPELINE COMPLETION +======================================================================================== +*/ + +workflow PIPELINE_COMPLETION { + + take: + 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") + + // + // Completion email and summary + // + workflow.onComplete { + if (email || email_on_fail) { + 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) + } + } +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +{%- if igenomes %} +// +// Check and validate pipeline parameters +// +def validateInputParameters() { + genomeExistsError() +} +{%- endif %} + +// +// Validate channels from input samplesheet +// +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 + 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}") + } + + return [ metas[0], fastqs ] +} + +{%- if igenomes %} +// +// Get attribute from genome config file e.g. fasta +// +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 ] + } + } + return null +} + +// +// Exit pipeline if incorrect --genome key provided +// +def genomeExistsError() { + if (params.genomes && params.genome && !params.genomes.containsKey(params.genome)) { + def error_string = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + " Genome '${params.genome}' not found in any config files provided to the pipeline.\n" + + " Currently, the available genome keys are:\n" + + " ${params.genomes.keySet().join(", ")}\n" + + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + error(error_string) + } +} +{%- endif %} + +// +// Generate methods description for MultiQC +// +def toolCitationText() { + + def citation_text = [ + "Tools used in the workflow included:", + "AnnData (Virshup et al. 2021),", + "FastQC (Andrews 2010),", + "MultiQC (Ewels et al. 2016),", + "Quarto (Allaire et al. 2022),", + "Scanpy (Wolf et al. 2018),", + "Space Ranger (10x Genomics) and", + "SpatialDE (Svensson et al. 2018)." + ].join(' ').trim() + + return citation_text +} + +def toolBibliographyText() { + + def reference_text = [ + "
  • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: 10.1101/2021.12.16.473007
  • ", + "
  • Andrews S, (2010) FastQC, URL: bioinformatics.babraham.ac.uk.
  • ", + "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: 10.1093/bioinformatics/btw354
  • ", + "
  • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
  • ", + "
  • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
  • ", + "
  • 10x Genomics Space Ranger 2.1.0, URL: 10xgenomics.com/support/software/space-ranger
  • ", + "
  • Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: 10.1038/nmeth.4636
  • ", + ].join(' ').trim() + + return reference_text +} + +def methodsDescriptionText(mqc_methods_yaml) { + // Convert to a named map so can be used as with familar NXF ${workflow} variable syntax in the MultiQC YML file + def meta = [:] + meta.workflow = workflow.toMap() + meta["manifest_map"] = workflow.manifest.toMap() + + // Pipeline DOI + meta["doi_text"] = meta.manifest_map.doi ? "(doi: ${meta.manifest_map.doi})" : "" + meta["nodoi_text"] = meta.manifest_map.doi ? "": "
  • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
  • " + + // Tool references + meta["tool_citations"] = "" + meta["tool_bibliography"] = "" + meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") + meta["tool_bibliography"] = toolBibliographyText() + + + def methods_text = mqc_methods_yaml.text + + def engine = new groovy.text.SimpleTemplateEngine() + def description_html = engine.createTemplate(methods_text).make(meta) + + return description_html.toString() +} + diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf new file mode 100644 index 0000000..ac31f28 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -0,0 +1,126 @@ +// +// 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 +======================================================================================== +*/ + +workflow UTILS_NEXTFLOW_PIPELINE { + + take: + print_version // boolean: print version + dump_parameters // boolean: dump parameters + outdir // path: base directory used to publish pipeline results + check_conda_channels // boolean: check conda channels + + main: + + // + // Print workflow version and exit on --version + // + if (print_version) { + log.info "${workflow.manifest.name} ${getWorkflowVersion()}" + System.exit(0) + } + + // + // Dump pipeline parameters to a JSON file + // + if (dump_parameters && outdir) { + dumpParametersToJSON(outdir) + } + + // + // When running with Conda, warn if channels have not been set-up appropriately + // + if (check_conda_channels) { + checkCondaChannels() + } + + emit: + dummy_emit = true +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Generate version string +// +def getWorkflowVersion() { + String version_string = "" + if (workflow.manifest.version) { + def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' + version_string += "${prefix_v}${workflow.manifest.version}" + } + + if (workflow.commitId) { + def git_shortsha = workflow.commitId.substring(0, 7) + version_string += "-g${git_shortsha}" + } + + return version_string +} + +// +// 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 = JsonOutput.toJson(params) + temp_pf.text = JsonOutput.prettyPrint(jsonStr) + + FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") + temp_pf.delete() +} + +// +// When running with -profile conda, warn if channels have not been set-up appropriately +// +def checkCondaChannels() { + Yaml parser = new Yaml() + def channels = [] + 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 + } + + // 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 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])) + } + + 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" + + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml b/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml new file mode 100644 index 0000000..e5c3a0a --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "UTILS_NEXTFLOW_PIPELINE" +description: Subworkflow with functionality that may be useful for any Nextflow pipeline +keywords: + - utility + - pipeline + - initialise + - version +components: [] +input: + - print_version: + type: boolean + description: | + Print the version of the pipeline and exit + - dump_parameters: + type: boolean + description: | + Dump the parameters of the pipeline to a JSON file + - output_directory: + type: directory + description: Path to output dir to write JSON file to. + pattern: "results/" + - check_conda_channel: + type: boolean + description: | + Check if the conda channel priority is correct. +output: + - dummy_emit: + type: boolean + description: | + Dummy emit to make nf-core subworkflows lint happy +authors: + - "@adamrtalbot" + - "@drpatelh" +maintainers: + - "@adamrtalbot" + - "@drpatelh" + - "@maxulysse" diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test new file mode 100644 index 0000000..68718e4 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test @@ -0,0 +1,54 @@ + +nextflow_function { + + name "Test Functions" + script "subworkflows/nf-core/utils_nextflow_pipeline/main.nf" + config "subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config" + tag 'subworkflows' + tag 'utils_nextflow_pipeline' + tag 'subworkflows/utils_nextflow_pipeline' + + test("Test Function getWorkflowVersion") { + + function "getWorkflowVersion" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function dumpParametersToJSON") { + + function "dumpParametersToJSON" + + when { + function { + """ + // define inputs of the function here. Example: + input[0] = "$outputDir" + """.stripIndent() + } + } + + then { + assertAll( + { assert function.success } + ) + } + } + + test("Test Function checkCondaChannels") { + + function "checkCondaChannels" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap new file mode 100644 index 0000000..e3f0baf --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap @@ -0,0 +1,20 @@ +{ + "Test Function getWorkflowVersion": { + "content": [ + "v9.9.9" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:05.308243" + }, + "Test Function checkCondaChannels": { + "content": null, + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:12.425833" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test new file mode 100644 index 0000000..ca964ce --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test @@ -0,0 +1,111 @@ +nextflow_workflow { + + name "Test Workflow UTILS_NEXTFLOW_PIPELINE" + script "../main.nf" + config "subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config" + workflow "UTILS_NEXTFLOW_PIPELINE" + tag 'subworkflows' + tag 'utils_nextflow_pipeline' + tag 'subworkflows/utils_nextflow_pipeline' + + test("Should run no inputs") { + + when { + workflow { + """ + print_version = false + dump_parameters = false + outdir = null + check_conda_channels = false + + input[0] = print_version + input[1] = dump_parameters + input[2] = outdir + input[3] = check_conda_channels + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should print version") { + + when { + workflow { + """ + print_version = true + dump_parameters = false + outdir = null + check_conda_channels = false + + input[0] = print_version + input[1] = dump_parameters + input[2] = outdir + input[3] = check_conda_channels + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert workflow.stdout.contains("nextflow_workflow v9.9.9") } + ) + } + } + + test("Should dump params") { + + when { + workflow { + """ + print_version = false + dump_parameters = true + outdir = 'results' + check_conda_channels = false + + input[0] = false + input[1] = true + input[2] = outdir + input[3] = false + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should not create params JSON if no output directory") { + + when { + workflow { + """ + print_version = false + dump_parameters = true + outdir = null + check_conda_channels = false + + input[0] = false + input[1] = true + input[2] = outdir + input[3] = false + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config new file mode 100644 index 0000000..d0a926b --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config @@ -0,0 +1,9 @@ +manifest { + name = 'nextflow_workflow' + author = """nf-core""" + homePage = 'https://127.0.0.1' + description = """Dummy pipeline""" + nextflowVersion = '!>=23.04.0' + version = '9.9.9' + doi = 'https://doi.org/10.5281/zenodo.5070524' +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml b/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml new file mode 100644 index 0000000..f847611 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/utils_nextflow_pipeline: + - subworkflows/nf-core/utils_nextflow_pipeline/** diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf new file mode 100644 index 0000000..a8b55d6 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -0,0 +1,440 @@ +// +// Subworkflow with utility functions specific to the nf-core pipeline template +// + +import org.yaml.snakeyaml.Yaml +import nextflow.extension.FilesEx + +/* +======================================================================================== + SUBWORKFLOW DEFINITION +======================================================================================== +*/ + +workflow UTILS_NFCORE_PIPELINE { + + take: + nextflow_cli_args + + main: + valid_config = checkConfigProvided() + checkProfileProvided(nextflow_cli_args) + + emit: + valid_config +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Warn if a -profile or Nextflow config has not been provided to run the pipeline +// +def checkConfigProvided() { + valid_config = true + 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 " + valid_config = false + } + return valid_config +} + +// +// Exit pipeline if --profile contains spaces +// +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" + } + 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" + } +} + +// +// Citation string for pipeline +// +def workflowCitation() { + return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + + "* The pipeline\n" + + " ${workflow.manifest.doi}\n\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" +} + +// +// Generate workflow version string +// +def getWorkflowVersion() { + String version_string = "" + if (workflow.manifest.version) { + def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' + version_string += "${prefix_v}${workflow.manifest.version}" + } + + if (workflow.commitId) { + def git_shortsha = workflow.commitId.substring(0, 7) + version_string += "-g${git_shortsha}" + } + + return version_string +} + +// +// 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 ] } + return yaml.dumpAsMap(versions).trim() +} + +// +// Get workflow version for pipeline +// +def workflowVersionToYAML() { + return """ + Workflow: + $workflow.manifest.name: ${getWorkflowVersion()} + Nextflow: $workflow.nextflow.version + """.stripIndent().trim() +} + +// +// Get channel of software versions used in pipeline in YAML format +// +def softwareVersionsToYAML(ch_versions) { + return ch_versions + .unique() + .map { processVersionsFromYAML(it) } + .unique() + .mix(Channel.of(workflowVersionToYAML())) +} + +// +// Get workflow summary for MultiQC +// +def paramsSummaryMultiqc(summary_params) { + def summary_section = '' + for (group in summary_params.keySet()) { + 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()) { + summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" + } + summary_section += "
    \n" + } + } + + String yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" + 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 +} + +// +// nf-core logo +// +def nfCoreLogo(monochrome_logs=true) { + Map colors = logColours(monochrome_logs) + String.format( + """\n + ${dashedLine(monochrome_logs)} + ${colors.green},--.${colors.black}/${colors.green},-.${colors.reset} + ${colors.blue} ___ __ __ __ ___ ${colors.green}/,-._.--~\'${colors.reset} + ${colors.blue} |\\ | |__ __ / ` / \\ |__) |__ ${colors.yellow}} {${colors.reset} + ${colors.blue} | \\| | \\__, \\__/ | \\ |___ ${colors.green}\\`-._,-`-,${colors.reset} + ${colors.green}`._,._,\'${colors.reset} + ${colors.purple} ${workflow.manifest.name} ${getWorkflowVersion()}${colors.reset} + ${dashedLine(monochrome_logs)} + """.stripIndent() + ) +} + +// +// Return dashed line +// +def dashedLine(monochrome_logs=true) { + Map colors = logColours(monochrome_logs) + return "-${colors.dim}----------------------------------------------------${colors.reset}-" +} + +// +// ANSII colours used for terminal logging +// +def logColours(monochrome_logs=true) { + Map colorcodes = [:] + + // Reset / Meta + colorcodes['reset'] = monochrome_logs ? '' : "\033[0m" + colorcodes['bold'] = monochrome_logs ? '' : "\033[1m" + colorcodes['dim'] = monochrome_logs ? '' : "\033[2m" + colorcodes['underlined'] = monochrome_logs ? '' : "\033[4m" + colorcodes['blink'] = monochrome_logs ? '' : "\033[5m" + colorcodes['reverse'] = monochrome_logs ? '' : "\033[7m" + 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" + + // 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" + + // 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" + + // 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" + + // 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" + + return colorcodes +} + +// +// Attach the multiqc report to email +// +def attachMultiqcReport(multiqc_report) { + def mqc_report = null + try { + if (workflow.success) { + 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" + } + mqc_report = mqc_report[0] + } + } + } catch (all) { + if (multiqc_report) { + log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" + } + } + return mqc_report +} + +// +// Construct and send completion email +// +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" + if (!workflow.success) { + subject = "[$workflow.manifest.name] FAILED: $workflow.runName" + } + + def summary = [:] + for (group in summary_params.keySet()) { + 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 + misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp + + def email_fields = [:] + email_fields['version'] = getWorkflowVersion() + email_fields['runName'] = workflow.runName + email_fields['success'] = workflow.success + email_fields['dateComplete'] = workflow.complete + email_fields['duration'] = workflow.duration + email_fields['exitStatus'] = workflow.exitStatus + email_fields['errorMessage'] = (workflow.errorMessage ?: 'None') + email_fields['errorReport'] = (workflow.errorReport ?: 'None') + email_fields['commandLine'] = workflow.commandLine + email_fields['projectDir'] = workflow.projectDir + email_fields['summary'] = summary << misc_fields + + // On success try attach the multiqc report + def mqc_report = attachMultiqcReport(multiqc_report) + + // Check if we are only sending emails on failure + def email_address = email + if (!email && email_on_fail && !workflow.success) { + email_address = email_on_fail + } + + // Render the TXT template + def engine = new groovy.text.GStringTemplateEngine() + def tf = new File("${workflow.projectDir}/assets/email_template.txt") + def txt_template = engine.createTemplate(tf).make(email_fields) + def email_txt = txt_template.toString() + + // Render the HTML template + def hf = new File("${workflow.projectDir}/assets/email_template.html") + def html_template = engine.createTemplate(hf).make(email_fields) + def email_html = html_template.toString() + + // 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 sf = new File("${workflow.projectDir}/assets/sendmail_template.txt") + def sendmail_template = engine.createTemplate(sf).make(smail_fields) + def sendmail_html = sendmail_template.toString() + + // Send the HTML e-mail + Map colors = logColours(monochrome_logs) + if (email_address) { + try { + if (plaintext_email) { throw 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) { + // Catch failures and try with plaintext + 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)-" + } + } + + // 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"); + 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"); + output_tf.delete() +} + +// +// Print pipeline summary on completion +// +def completionSummary(monochrome_logs=true) { + Map colors = logColours(monochrome_logs) + 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}-" + } + } else { + log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-" + } +} + +// +// Construct and send a notification to a web server as JSON e.g. Microsoft Teams and Slack +// +def imNotification(summary_params, hook_url) { + def summary = [:] + for (group in summary_params.keySet()) { + 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 + + def msg_fields = [:] + msg_fields['version'] = getWorkflowVersion() + msg_fields['runName'] = workflow.runName + msg_fields['success'] = workflow.success + msg_fields['dateComplete'] = workflow.complete + msg_fields['duration'] = workflow.duration + msg_fields['exitStatus'] = workflow.exitStatus + msg_fields['errorMessage'] = (workflow.errorMessage ?: 'None') + msg_fields['errorReport'] = (workflow.errorReport ?: 'None') + msg_fields['commandLine'] = workflow.commandLine.replaceFirst(/ +--hook_url +[^ ]+/, "") + msg_fields['projectDir'] = workflow.projectDir + msg_fields['summary'] = summary << misc_fields + + // Render the JSON template + def engine = new groovy.text.GStringTemplateEngine() + // Different JSON depending on the service provider + // Defaults to "Adaptive Cards" (https://adaptivecards.io), except Slack which has its own format + def json_path = hook_url.contains("hooks.slack.com") ? "slackreport.json" : "adaptivecard.json" + def hf = new File("${workflow.projectDir}/assets/${json_path}") + def json_template = engine.createTemplate(hf).make(msg_fields) + def json_message = json_template.toString() + + // POST + 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()); + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml b/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml new file mode 100644 index 0000000..d08d243 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "UTILS_NFCORE_PIPELINE" +description: Subworkflow with utility functions specific to the nf-core pipeline template +keywords: + - utility + - pipeline + - initialise + - version +components: [] +input: + - nextflow_cli_args: + type: list + description: | + Nextflow CLI positional arguments +output: + - success: + type: boolean + description: | + Dummy output to indicate success +authors: + - "@adamrtalbot" +maintainers: + - "@adamrtalbot" + - "@maxulysse" diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test new file mode 100644 index 0000000..1dc317f --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test @@ -0,0 +1,134 @@ + +nextflow_function { + + name "Test Functions" + script "../main.nf" + config "subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config" + tag "subworkflows" + tag "subworkflows_nfcore" + tag "utils_nfcore_pipeline" + tag "subworkflows/utils_nfcore_pipeline" + + test("Test Function checkConfigProvided") { + + function "checkConfigProvided" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function checkProfileProvided") { + + function "checkProfileProvided" + + when { + function { + """ + input[0] = [] + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function workflowCitation") { + + function "workflowCitation" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function nfCoreLogo") { + + function "nfCoreLogo" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function dashedLine") { + + function "dashedLine" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function without logColours") { + + function "logColours" + + when { + function { + """ + input[0] = true + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function with logColours") { + function "logColours" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap new file mode 100644 index 0000000..1037232 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap @@ -0,0 +1,166 @@ +{ + "Test Function checkProfileProvided": { + "content": null, + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:03.360873" + }, + "Test Function checkConfigProvided": { + "content": [ + true + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:59.729647" + }, + "Test Function nfCoreLogo": { + "content": [ + "\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nextflow_workflow v9.9.9\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:10.562934" + }, + "Test Function workflowCitation": { + "content": [ + "If you use nextflow_workflow for your analysis please cite:\n\n* The pipeline\n https://doi.org/10.5281/zenodo.5070524\n\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nextflow_workflow/blob/master/CITATIONS.md" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:07.019761" + }, + "Test Function without logColours": { + "content": [ + { + "reset": "", + "bold": "", + "dim": "", + "underlined": "", + "blink": "", + "reverse": "", + "hidden": "", + "black": "", + "red": "", + "green": "", + "yellow": "", + "blue": "", + "purple": "", + "cyan": "", + "white": "", + "bblack": "", + "bred": "", + "bgreen": "", + "byellow": "", + "bblue": "", + "bpurple": "", + "bcyan": "", + "bwhite": "", + "ublack": "", + "ured": "", + "ugreen": "", + "uyellow": "", + "ublue": "", + "upurple": "", + "ucyan": "", + "uwhite": "", + "iblack": "", + "ired": "", + "igreen": "", + "iyellow": "", + "iblue": "", + "ipurple": "", + "icyan": "", + "iwhite": "", + "biblack": "", + "bired": "", + "bigreen": "", + "biyellow": "", + "biblue": "", + "bipurple": "", + "bicyan": "", + "biwhite": "" + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:17.969323" + }, + "Test Function dashedLine": { + "content": [ + "-\u001b[2m----------------------------------------------------\u001b[0m-" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:14.366181" + }, + "Test Function with logColours": { + "content": [ + { + "reset": "\u001b[0m", + "bold": "\u001b[1m", + "dim": "\u001b[2m", + "underlined": "\u001b[4m", + "blink": "\u001b[5m", + "reverse": "\u001b[7m", + "hidden": "\u001b[8m", + "black": "\u001b[0;30m", + "red": "\u001b[0;31m", + "green": "\u001b[0;32m", + "yellow": "\u001b[0;33m", + "blue": "\u001b[0;34m", + "purple": "\u001b[0;35m", + "cyan": "\u001b[0;36m", + "white": "\u001b[0;37m", + "bblack": "\u001b[1;30m", + "bred": "\u001b[1;31m", + "bgreen": "\u001b[1;32m", + "byellow": "\u001b[1;33m", + "bblue": "\u001b[1;34m", + "bpurple": "\u001b[1;35m", + "bcyan": "\u001b[1;36m", + "bwhite": "\u001b[1;37m", + "ublack": "\u001b[4;30m", + "ured": "\u001b[4;31m", + "ugreen": "\u001b[4;32m", + "uyellow": "\u001b[4;33m", + "ublue": "\u001b[4;34m", + "upurple": "\u001b[4;35m", + "ucyan": "\u001b[4;36m", + "uwhite": "\u001b[4;37m", + "iblack": "\u001b[0;90m", + "ired": "\u001b[0;91m", + "igreen": "\u001b[0;92m", + "iyellow": "\u001b[0;93m", + "iblue": "\u001b[0;94m", + "ipurple": "\u001b[0;95m", + "icyan": "\u001b[0;96m", + "iwhite": "\u001b[0;97m", + "biblack": "\u001b[1;90m", + "bired": "\u001b[1;91m", + "bigreen": "\u001b[1;92m", + "biyellow": "\u001b[1;93m", + "biblue": "\u001b[1;94m", + "bipurple": "\u001b[1;95m", + "bicyan": "\u001b[1;96m", + "biwhite": "\u001b[1;97m" + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:21.714424" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test new file mode 100644 index 0000000..8940d32 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test @@ -0,0 +1,29 @@ +nextflow_workflow { + + name "Test Workflow UTILS_NFCORE_PIPELINE" + script "../main.nf" + config "subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config" + workflow "UTILS_NFCORE_PIPELINE" + tag "subworkflows" + tag "subworkflows_nfcore" + tag "utils_nfcore_pipeline" + tag "subworkflows/utils_nfcore_pipeline" + + test("Should run without failures") { + + when { + workflow { + """ + input[0] = [] + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap new file mode 100644 index 0000000..859d103 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap @@ -0,0 +1,19 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + true + ], + "valid_config": [ + true + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:25.726491" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config new file mode 100644 index 0000000..d0a926b --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config @@ -0,0 +1,9 @@ +manifest { + name = 'nextflow_workflow' + author = """nf-core""" + homePage = 'https://127.0.0.1' + description = """Dummy pipeline""" + 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/tests/tags.yml b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml new file mode 100644 index 0000000..ac8523c --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/utils_nfcore_pipeline: + - subworkflows/nf-core/utils_nfcore_pipeline/** diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf b/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf new file mode 100644 index 0000000..2585b65 --- /dev/null +++ b/subworkflows/nf-core/utils_nfvalidation_plugin/main.nf @@ -0,0 +1,62 @@ +// +// 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 new file mode 100644 index 0000000..3d4a6b0 --- /dev/null +++ b/subworkflows/nf-core/utils_nfvalidation_plugin/meta.yml @@ -0,0 +1,44 @@ +# 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 new file mode 100644 index 0000000..5784a33 --- /dev/null +++ b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/main.nf.test @@ -0,0 +1,200 @@ +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/nextflow_schema.json b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json new file mode 100644 index 0000000..7626c1c --- /dev/null +++ b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/nextflow_schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/./master/nextflow_schema.json", + "title": ". pipeline parameters", + "description": "", + "type": "object", + "definitions": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["outdir"], + "properties": { + "validate_params": { + "type": "boolean", + "description": "Validate parameters?", + "default": true, + "hidden": true + }, + "outdir": { + "type": "string", + "format": "directory-path", + "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", + "fa_icon": "fas fa-folder-open" + }, + "test_data_base": { + "type": "string", + "default": "https://raw.githubusercontent.com/nf-core/test-datasets/modules", + "description": "Base for test data directory", + "hidden": true + }, + "test_data": { + "type": "string", + "description": "Fake test data param", + "hidden": true + } + } + }, + "generic_options": { + "title": "Generic options", + "type": "object", + "fa_icon": "fas fa-file-import", + "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.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "logo": { + "type": "boolean", + "default": true, + "description": "Display nf-core logo in console output.", + "fa_icon": "fas fa-image", + "hidden": true + }, + "singularity_pull_docker_container": { + "type": "boolean", + "description": "Pull Singularity container from Docker?", + "hidden": true + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "description": "Method used to save pipeline results to output directory.", + "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", + "fa_icon": "fas fa-copy", + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "hidden": true + }, + "monochrome_logs": { + "type": "boolean", + "description": "Use monochrome_logs", + "hidden": true + } + } + } + }, + "allOf": [ + { + "$ref": "#/definitions/input_output_options" + }, + { + "$ref": "#/definitions/generic_options" + } + ] +} diff --git a/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml new file mode 100644 index 0000000..60b1cff --- /dev/null +++ b/subworkflows/nf-core/utils_nfvalidation_plugin/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/utils_nfvalidation_plugin: + - subworkflows/nf-core/utils_nfvalidation_plugin/** From 99d9a8fa3f242c4a6d80c2489ac5228a587d89aa Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 5 Mar 2024 18:04:15 +0100 Subject: [PATCH 357/410] Add missing versions export for INPUT_CHECK --- subworkflows/local/input_check.nf | 10 ++++++++-- workflows/spatialtranscriptomics.nf | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/subworkflows/local/input_check.nf b/subworkflows/local/input_check.nf index b51756b..06a9721 100644 --- a/subworkflows/local/input_check.nf +++ b/subworkflows/local/input_check.nf @@ -11,6 +11,9 @@ workflow INPUT_CHECK { samplesheet // file: samplesheet read in from --input main: + + ch_versions = Channel.empty() + ch_st = Channel.fromPath(samplesheet) .splitCsv ( header: true, sep: ',') .branch { @@ -30,6 +33,7 @@ workflow INPUT_CHECK { // Extract tarballed inputs UNTAR_SPACERANGER_INPUT ( ch_spaceranger.tar ) + ch_versions = ch_versions.mix(UNTAR_SPACERANGER_INPUT.out.versions) // Combine extracted and directory inputs into one channel ch_spaceranger_combined = UNTAR_SPACERANGER_INPUT.out.untar @@ -51,6 +55,7 @@ workflow INPUT_CHECK { // Extract tarballed inputs UNTAR_DOWNSTREAM_INPUT ( ch_downstream.tar ) + ch_versions = ch_versions.mix(UNTAR_DOWNSTREAM_INPUT.out.versions) // Combine extracted and directory inputs into one channel ch_downstream_combined = UNTAR_DOWNSTREAM_INPUT.out.untar @@ -61,8 +66,9 @@ workflow INPUT_CHECK { ch_downstream_input = ch_downstream_combined.map { create_channel_downstream(it) } emit: - ch_spaceranger_input // channel: [ val(meta), [ st data ] ] - ch_downstream_input // channel: [ val(meta), [ st data ] ] + ch_spaceranger_input // channel: [ val(meta), [ st data ] ] + ch_downstream_input // channel: [ val(meta), [ st data ] ] + versions = ch_versions // channel: [ versions.yml ] } // Function to get list of [ meta, [ spaceranger_dir ]] diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 3d66ad0..0e88145 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -37,6 +37,7 @@ workflow SPATIALTRANSCRIPTOMICS { INPUT_CHECK ( samplesheet ) + ch_versions = ch_versions.mix(INPUT_CHECK.out.versions) // // MODULE: FastQC From 45de379713a5cef9ab6d5bc13df559b7849b34c8 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 5 Mar 2024 18:14:10 +0100 Subject: [PATCH 358/410] Add missing FastQC input to MultiQC --- workflows/spatialtranscriptomics.nf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 0e88145..0caa945 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -46,7 +46,7 @@ workflow SPATIALTRANSCRIPTOMICS { INPUT_CHECK.out.ch_spaceranger_input.map{ it -> [it[0] /* meta */, it[1] /* reads */]} ) ch_versions = ch_versions.mix(FASTQC.out.versions) - // ch_multiqc_files = ch_multiqc_files.mix(FASTQC.out.zip.collect{it[1]}) + ch_multiqc_files = ch_multiqc_files.mix(FASTQC.out.zip.collect{it[1]}) // // SUBWORKFLOW: Space Ranger raw data processing @@ -82,7 +82,6 @@ workflow SPATIALTRANSCRIPTOMICS { ) ch_versions = ch_versions.mix(ST_DOWNSTREAM.out.versions) - // // Collate and save software versions // From 9b8e7d5b36baa58be9d54879bbf86c79e08bd749 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 5 Mar 2024 18:16:52 +0100 Subject: [PATCH 359/410] Update UNTAR module --- modules.json | 2 +- modules/nf-core/untar/tests/main.nf.test | 8 -------- modules/nf-core/untar/tests/main.nf.test.snap | 12 ++++++++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules.json b/modules.json index 39604d4..fa8e5ee 100644 --- a/modules.json +++ b/modules.json @@ -22,7 +22,7 @@ }, "untar": { "branch": "master", - "git_sha": "e719354ba77df0a1bd310836aa2039b45c29d620", + "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", "installed_by": ["modules"] } } diff --git a/modules/nf-core/untar/tests/main.nf.test b/modules/nf-core/untar/tests/main.nf.test index 679e83c..2a7c97b 100644 --- a/modules/nf-core/untar/tests/main.nf.test +++ b/modules/nf-core/untar/tests/main.nf.test @@ -3,17 +3,12 @@ nextflow_process { name "Test Process UNTAR" script "../main.nf" process "UNTAR" - tag "modules" tag "modules_nfcore" tag "untar" - test("test_untar") { when { - params { - outdir = "$outputDir" - } process { """ input[0] = [ [], file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/db/kraken2.tar.gz', checkIfExists: true) ] @@ -33,9 +28,6 @@ nextflow_process { test("test_untar_onlyfiles") { when { - params { - outdir = "$outputDir" - } process { """ input[0] = [ [], file(params.modules_testdata_base_path + 'generic/tar/hello.tar.gz', checkIfExists: true) ] diff --git a/modules/nf-core/untar/tests/main.nf.test.snap b/modules/nf-core/untar/tests/main.nf.test.snap index ace4257..6455029 100644 --- a/modules/nf-core/untar/tests/main.nf.test.snap +++ b/modules/nf-core/untar/tests/main.nf.test.snap @@ -12,7 +12,11 @@ ] ] ], - "timestamp": "2023-10-18T11:56:46.878844" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T11:49:41.320643" }, "test_untar": { "content": [ @@ -29,6 +33,10 @@ ] ] ], - "timestamp": "2023-10-18T11:56:08.16574" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T11:49:33.795172" } } \ No newline at end of file From c08398d32feac05849fc5f9e97c260583619d99b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 5 Mar 2024 20:04:18 +0100 Subject: [PATCH 360/410] Update nf-test with new downstream subworkflow --- modules/local/st_svg.nf | 2 +- subworkflows/local/st_downstream.nf | 2 +- tests/pipeline/lib/UTILS.groovy | 2 +- tests/pipeline/test_downstream.nf.test | 2 +- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/local/st_svg.nf b/modules/local/st_svg.nf index 41c75f0..8ec9bc9 100644 --- a/modules/local/st_svg.nf +++ b/modules/local/st_svg.nf @@ -24,7 +24,7 @@ process ST_SVG { tuple val(meta), path(st_sdata) output: - tuple val(meta), path("*.csv") , emit: degs + tuple val(meta), path("*.csv") , emit: degs tuple val(meta), path("st_adata_svg.h5ad"), emit: st_adata_svg tuple val(meta), path("st_sdata_svg.zarr"), emit: st_sdata_svg tuple val(meta), path("st_svg.html") , emit: html diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 8baaae5..70eb424 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -3,8 +3,8 @@ // include { ST_QUALITY_CONTROLS } from '../../modules/local/st_quality_controls' -include { ST_SVG } from '../../modules/local/st_svg' include { ST_CLUSTERING } from '../../modules/local/st_clustering' +include { ST_SVG } from '../../modules/local/st_svg' workflow ST_DOWNSTREAM { diff --git a/tests/pipeline/lib/UTILS.groovy b/tests/pipeline/lib/UTILS.groovy index 311403c..deacb58 100644 --- a/tests/pipeline/lib/UTILS.groovy +++ b/tests/pipeline/lib/UTILS.groovy @@ -2,7 +2,7 @@ class UTILS { public static String removeNextflowVersion(outputDir) { - def softwareVersions = path("$outputDir/pipeline_info/software_versions.yml").yaml + def softwareVersions = path("$outputDir/pipeline_info/nf_core_pipeline_software_mqc_versions.yml").yaml if (softwareVersions.containsKey("Workflow")) { softwareVersions.Workflow.remove("Nextflow") } diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 881516f..77a81f5 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -22,7 +22,7 @@ nextflow_pipeline { assertAll( // Workflow { assert workflow.success }, - { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index fc02eaf..bceb8a1 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -20,7 +20,7 @@ nextflow_pipeline { // Workflow { assert workflow.success }, - { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, @@ -28,10 +28,10 @@ nextflow_pipeline { // Reports { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, // DEGs - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatial_de.csv").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_svg.csv").exists() }, // Space Ranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index ab7a967..4dd8e8d 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -16,7 +16,7 @@ nextflow_pipeline { // Workflow { assert workflow.success }, - { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("software_versions") }, + { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, @@ -24,10 +24,10 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatial_de.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatial_de.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, // Space Ranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/web_summary.html").exists() }, From 370004da7d3b35772c0b9030178eb7e5bc6fc8ea Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 6 Mar 2024 14:04:51 +0100 Subject: [PATCH 361/410] Add missing Spaceranger MultiQC input --- workflows/spatialtranscriptomics.nf | 1 + 1 file changed, 1 insertion(+) diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index e50e68f..1e1fd5a 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -62,6 +62,7 @@ workflow SPATIALTRANSCRIPTOMICS { INPUT_CHECK.out.ch_spaceranger_input ) ch_versions = ch_versions.mix(SPACERANGER.out.versions) + ch_multiqc_files = ch_multiqc_files.mix(SPACERANGER.out.sr_dir.collect{it[1]}) ch_downstream_input = INPUT_CHECK.out.ch_downstream_input.concat(SPACERANGER.out.sr_dir).map{ meta, outs -> [meta, outs.findAll{ it -> DOWNSTREAM_REQUIRED_SPACERANGER_FILES.contains(it.name) }] } From 0600a1e24d650e0071144fc45a40cd1be0e3327a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 6 Mar 2024 14:08:43 +0100 Subject: [PATCH 362/410] Fix notebook name and zarr/h5ad output patterns --- conf/modules.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 3e27fe3..6114657 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -62,7 +62,7 @@ process { ] } - withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SPATIAL_DE' { + withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SVG' { ext.prefix = { "${notebook.baseName}" } publishDir = [ [ @@ -79,7 +79,7 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_adata_processed.h5ad", + pattern: "artifacts/st_{a,s}data_processed.{h5ad,zarr}", saveAs: { "st_sdata_processed.zarr" } ], [ From d8067c6bc03efe564cd2f41315ca4f01bfc5ba8b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 10:57:10 +0100 Subject: [PATCH 363/410] Fix SVG report test assertions --- tests/pipeline/test_downstream.nf.test | 7 ++++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 2 +- tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 77a81f5..456f187 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -1,4 +1,4 @@ -nextflow_pipeline { +extflow_pipeline { name "Test downstream workflow (excl. Space Ranger)" script "main.nf" tag "pipeline" @@ -31,10 +31,11 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, + { assert + file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index bceb8a1..e1ef7fa 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -28,7 +28,7 @@ nextflow_pipeline { // Reports { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_svg.csv").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 4dd8e8d..3be133d 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -24,7 +24,7 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("plot the top spatially variable genes") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, From bef1ce58a2033f144d1ec866307acf7f55957b2e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 10:59:56 +0100 Subject: [PATCH 364/410] Delete old spatial DE report --- bin/st_spatial_de.qmd | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bin/st_spatial_de.qmd diff --git a/bin/st_spatial_de.qmd b/bin/st_spatial_de.qmd deleted file mode 100644 index 8b13789..0000000 --- a/bin/st_spatial_de.qmd +++ /dev/null @@ -1 +0,0 @@ - From c49f8805528158b35ab179bac4b65550dd941777 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 12:13:36 +0100 Subject: [PATCH 365/410] Fix publishing of AnnData object --- bin/st_clustering.qmd | 2 +- conf/modules.config | 8 +++++++- subworkflows/local/st_downstream.nf | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index e98fc53..ba26fb9 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -173,8 +173,8 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false -st_adata.write(os.path.join(artifact_dir, output_adata_processed)) del st_sdata.table st_sdata.table = st_adata +st_adata.write(os.path.join(artifact_dir, output_adata_processed)) st_sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/conf/modules.config b/conf/modules.config index 6114657..9d7fc44 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -79,9 +79,15 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_{a,s}data_processed.{h5ad,zarr}", + pattern: "artifacts/st_sdata_processed.zarr", saveAs: { "st_sdata_processed.zarr" } ], + [ + path: { "${params.outdir}/${meta.id}/data" }, + mode: params.publish_dir_mode, + pattern: "artifacts/st_adata_processed.h5ad", + saveAs: { "st_adata_processed.h5ad" } + ], [ path: { "${params.outdir}/${meta.id}/degs" }, mode: params.publish_dir_mode, diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index d728476..4861f60 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -59,7 +59,8 @@ workflow ST_DOWNSTREAM { input_sdata_filtered: "st_adata_filtered.zarr", cluster_resolution: params.st_cluster_resolution, n_hvgs: params.st_cluster_n_hvgs, - output_sdata_processed: "st_sdata_processed.zarr" + output_adata_processed: "st_adata_processed.h5ad", + output_sdata: "st_sdata_processed.zarr" ] ST_CLUSTERING ( ch_clustering_notebook, From 3245e1723b63c48f2d2e105111ac6c9caf84bf82 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 13:02:09 +0100 Subject: [PATCH 366/410] Fix publishing of DEG CSV --- bin/st_svg.qmd | 18 ++++++++++-------- conf/modules.config | 3 ++- subworkflows/local/st_downstream.nf | 6 ++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bin/st_svg.qmd b/bin/st_svg.qmd index f3c401b..1951e4c 100644 --- a/bin/st_svg.qmd +++ b/bin/st_svg.qmd @@ -9,18 +9,20 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata.zarr" -output_adata_svg = "st_adata_svg.h5ad" # Name of the output anndata file -output_sdata = "st_sdata_svg.zarr" # Name of the input anndata file -output_spatial_degs = "st_svg.csv" -n_top_spatial_degs = 14 +input_sdata = "st_sdata_processed.zarr" # Input: SpatialData file +output_adata_svg = "st_adata_svg.h5ad" # Output: AnnData file +output_sdata = "st_sdata_svg.zarr" # Output: SpatialData file +output_svg = "st_svg.csv" # Output: spatially variable genes +n_top_spatial_degs = 14 # Parameter: number of SVG to plot in report +artifact_dir = "artifacts" ``` ```{python} -import scanpy as sc +import numpy as np +import os import pandas as pd +import scanpy as sc import squidpy as sq -import numpy as np import spatialdata from anndata import AnnData from matplotlib import pyplot as plt @@ -131,5 +133,5 @@ st_sdata.write("./" + output_sdata) ``` ```{python} -st_adata.uns["moranI"].to_csv(output_spatial_degs) +st_adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_svg)) ``` diff --git a/conf/modules.config b/conf/modules.config index 9d7fc44..7f01fe2 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -91,7 +91,8 @@ process { [ path: { "${params.outdir}/${meta.id}/degs" }, mode: params.publish_dir_mode, - pattern: "*.csv" + pattern: "artifacts/st_svg.csv", + saveAs: { "st_svg.csv" } ] ] } diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 4861f60..014e2c3 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -79,8 +79,10 @@ workflow ST_DOWNSTREAM { .map { tuple(it[0], svg_file) } svg_params = [ input_sdata: "st_sdata_processed.zarr", - n_top_spatial_degs: params.st_n_top_spatial_degs, - output_svg: "st_svg.csv" + output_adata_svg: "st_adata_svg.h5ad", + output_sdata: "st_sdata_svg.zarr", + output_svg: "st_svg.csv", + n_top_spatial_degs: params.st_n_top_spatial_degs ] ST_SVG ( ch_svg_notebook, From d5f16b439e0aacd653df140516d5c93f775f23c6 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 13:36:29 +0100 Subject: [PATCH 367/410] Add testing of SpatialData output --- tests/pipeline/test_downstream.nf.test | 8 +++++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test | 1 + tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 456f187..8c400aa 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -1,4 +1,4 @@ -extflow_pipeline { +nextflow_pipeline { name "Test downstream workflow (excl. Space Ranger)" script "main.nf" tag "pipeline" @@ -20,19 +20,21 @@ extflow_pipeline { then { assertAll( + // Workflow { assert workflow.success }, { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_sdata_processed.zarr").exists() }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_adata_processed.h5ad").exists() }, + { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_sdata_processed.zarr").exists() }, // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert - file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index e1ef7fa..25abe67 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -24,6 +24,7 @@ nextflow_pipeline { // Data { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_sdata_processed.zarr").exists() }, // Reports { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 3be133d..8b4638d 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -20,6 +20,7 @@ nextflow_pipeline { // Data { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_sdata_processed.zarr").exists() }, // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, From fcd540450bcc28be7739f7460cbf928535d19ecf Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 14:09:03 +0100 Subject: [PATCH 368/410] Minor formatting --- bin/st_clustering.qmd | 16 ++++++++-------- bin/st_quality_controls.qmd | 8 +++----- bin/st_svg.qmd | 8 ++++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index ba26fb9..ccb91ba 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -11,13 +11,12 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false - -input_sdata = "st_sdata_filtered.zarr" # Name of the input anndata file +input_sdata = "st_sdata_filtered.zarr" # Input: SpatialData file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses -artifact_dir = "artifacts" -output_adata_processed = "st_adata_processed.h5ad" # Name of the output anndata file -output_sdata = "st_sdata_processed.zarr" # Name of the input anndata file +artifact_dir = "artifacts" # Output directory +output_sdata = "st_sdata_processed.zarr" # Output: SpatialData file +output_adata_processed = "st_adata_processed.h5ad" # Output: AnnData file ``` The data has already been filtered in the _quality controls_ reports and is @@ -39,9 +38,10 @@ from IPython.display import display, Markdown ``` ```{python} -# Make sure we can use scanpy plots with the AnnData object exported from sdata.table -# This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ -# Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). +# Make sure we can use scanpy plots with the AnnData object exported from +# `sdata.table`. This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ +# Once that PR is merged into spatialdata-io, we should instead use +# `spatialdata_io.to_legacy_anndata(sdata)`. def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: adata = sdata.table for dataset_id in adata.uns["spatial"]: diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index 125beb2..ecd19a0 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -26,7 +26,7 @@ analysis tools and facilitates seamless integration into existing workflows. ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata_raw.zarr" # Name of the input anndata file +input_sdata = "st_sdata_raw.zarr" # Input: SpatialData file min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene @@ -34,8 +34,8 @@ mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) hb_threshold = 100 # content threshold (%) artifact_dir = "artifacts" -output_sdata = "st_sdata_filtered.zarr" # Name of the output zarr file -output_adata_filtered = "st_adata_filtered.h5ad" # Name of the output anndata file +output_sdata = "st_sdata_filtered.zarr" # Output: SpatialData file +output_adata_filtered = "st_adata_filtered.h5ad" # Output: AnnData file ``` ```{python} @@ -78,9 +78,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read the data - st_sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) - st_adata = to_legacy_anndata(st_sdata) # Convert X matrix from csr to csc dense matrix for output compatibility: diff --git a/bin/st_svg.qmd b/bin/st_svg.qmd index 1951e4c..e9f7b00 100644 --- a/bin/st_svg.qmd +++ b/bin/st_svg.qmd @@ -9,12 +9,12 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata_processed.zarr" # Input: SpatialData file +input_sdata = "st_sdata_processed.zarr" # Input: SpatialData file +n_top_spatial_degs = 14 # Number of SVG to plot in report +artifact_dir = "artifacts" # Output directory output_adata_svg = "st_adata_svg.h5ad" # Output: AnnData file output_sdata = "st_sdata_svg.zarr" # Output: SpatialData file -output_svg = "st_svg.csv" # Output: spatially variable genes -n_top_spatial_degs = 14 # Parameter: number of SVG to plot in report -artifact_dir = "artifacts" +output_svg = "st_svg.csv" # Output: spatially variable genes ``` ```{python} From 496a96ee4a8898cc8a9619fd1b5f58b36cf4d344 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 7 Mar 2024 14:28:36 +0100 Subject: [PATCH 369/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 12 ++++++++---- .../test_spaceranger_ffpe_v1.nf.test.snap | 16 +++++++++++++++- ...st_spaceranger_ffpe_v2_cytassist.nf.test.snap | 12 ++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 75f12d8..4587197 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,8 +1,12 @@ { - "software_versions": { + "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], - "timestamp": "2024-01-15T16:00:03.485826" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-03-07T13:23:56.226763" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 441c711..c36ecfc 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -3,6 +3,20 @@ "content": [ "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, "timestamp": "2024-01-15T13:44:40.789425" + }, + "nf_core_pipeline_software_mqc_versions.yml": { + "content": [ + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-03-07T13:55:24.407294" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 1dad690..02e6954 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,8 +1,12 @@ { - "software_versions": { + "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], - "timestamp": "2024-01-15T15:42:52.651007" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-03-07T14:28:20.163729" } -} +} \ No newline at end of file From 9e907df546b077df470927f763ea99e5049dfe81 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Mar 2024 14:37:49 +0100 Subject: [PATCH 370/410] Rename SVG to spatially variable genes --- bin/st_clustering.qmd | 7 +- bin/st_quality_controls.qmd | 2 +- ...vg.qmd => st_spatially_variable_genes.qmd} | 12 +-- conf/modules.config | 6 +- docs/output.md | 4 +- subworkflows/local/st_downstream.nf | 80 ++++++++++--------- tests/pipeline/test_downstream.nf.test | 8 +- tests/pipeline/test_downstream.nf.test.snap | 4 +- .../pipeline/test_spaceranger_ffpe_v1.nf.test | 4 +- .../test_spaceranger_ffpe_v1.nf.test.snap | 4 +- ...test_spaceranger_ffpe_v2_cytassist.nf.test | 4 +- ...spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 +- 12 files changed, 71 insertions(+), 68 deletions(-) rename bin/{st_svg.qmd => st_spatially_variable_genes.qmd} (93%) diff --git a/bin/st_clustering.qmd b/bin/st_clustering.qmd index ccb91ba..e7516c5 100644 --- a/bin/st_clustering.qmd +++ b/bin/st_clustering.qmd @@ -15,8 +15,8 @@ input_sdata = "st_sdata_filtered.zarr" # Input: SpatialData file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses artifact_dir = "artifacts" # Output directory +output_adata = "st_adata_processed.h5ad" # Output: AnnData file output_sdata = "st_sdata_processed.zarr" # Output: SpatialData file -output_adata_processed = "st_adata_processed.h5ad" # Output: AnnData file ``` The data has already been filtered in the _quality controls_ reports and is @@ -62,8 +62,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ``` ```{python} -st_sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) - +st_sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) st_adata = to_legacy_anndata(st_sdata) print("Content of the AnnData object:") @@ -175,6 +174,6 @@ sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) #| echo: false del st_sdata.table st_sdata.table = st_adata -st_adata.write(os.path.join(artifact_dir, output_adata_processed)) +st_adata.write(os.path.join(artifact_dir, output_adata)) st_sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/bin/st_quality_controls.qmd b/bin/st_quality_controls.qmd index ecd19a0..0586b88 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/st_quality_controls.qmd @@ -34,8 +34,8 @@ mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) hb_threshold = 100 # content threshold (%) artifact_dir = "artifacts" +output_adata = "st_adata_filtered.h5ad" # Output: AnnData file output_sdata = "st_sdata_filtered.zarr" # Output: SpatialData file -output_adata_filtered = "st_adata_filtered.h5ad" # Output: AnnData file ``` ```{python} diff --git a/bin/st_svg.qmd b/bin/st_spatially_variable_genes.qmd similarity index 93% rename from bin/st_svg.qmd rename to bin/st_spatially_variable_genes.qmd index e9f7b00..2e60664 100644 --- a/bin/st_svg.qmd +++ b/bin/st_spatially_variable_genes.qmd @@ -10,11 +10,11 @@ jupyter: python3 #| tags: [parameters] #| echo: false input_sdata = "st_sdata_processed.zarr" # Input: SpatialData file -n_top_spatial_degs = 14 # Number of SVG to plot in report +n_top_spatial_degs = 14 # Number of spatially variable genes to plot artifact_dir = "artifacts" # Output directory -output_adata_svg = "st_adata_svg.h5ad" # Output: AnnData file -output_sdata = "st_sdata_svg.zarr" # Output: SpatialData file -output_svg = "st_svg.csv" # Output: spatially variable genes +output_csv = "st_spatially_variable_genes.csv" # Output: gene list +output_adata = "st_adata_spatially_variable_genes.h5ad" # Output: AnnData file +output_sdata = "st_sdata.zarr" # Output: SpatialData file ``` ```{python} @@ -126,12 +126,12 @@ st_adata.uns["moranI"].head(n_top_spatial_degs) ```{python} #| echo: false -st_adata.write(output_adata_svg) +st_adata.write(output_adata) del st_sdata.table st_sdata.table = st_adata st_sdata.write("./" + output_sdata) ``` ```{python} -st_adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_svg)) +st_adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) ``` diff --git a/conf/modules.config b/conf/modules.config index 7f01fe2..5d48d0b 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -62,7 +62,7 @@ process { ] } - withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SVG' { + withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SPATIALLY_VARIABLE_GENES' { ext.prefix = { "${notebook.baseName}" } publishDir = [ [ @@ -91,8 +91,8 @@ process { [ path: { "${params.outdir}/${meta.id}/degs" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_svg.csv", - saveAs: { "st_svg.csv" } + pattern: "artifacts/st_spatially_variable_genes.csv", + saveAs: { "st_spatially_variable_genes.csv" } ] ] } diff --git a/docs/output.md b/docs/output.md index 875c683..98f17c9 100644 --- a/docs/output.md +++ b/docs/output.md @@ -99,9 +99,9 @@ option; you can find more details in the report itself. Output files - `/reports/` - - `st_svg.html`: HTML report. + - `st_spatially_variable_genes.html`: HTML report. - `/degs/` - - `st_svg.csv`: List of spatially variable genes. + - `st_spatially_variable_genes.csv`: List of spatially variable genes. diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf index 014e2c3..6cef709 100644 --- a/subworkflows/local/st_downstream.nf +++ b/subworkflows/local/st_downstream.nf @@ -2,9 +2,9 @@ // Subworkflow for downstream analyses of ST data // -include { QUARTONOTEBOOK as ST_QUALITY_CONTROLS } from '../../modules/nf-core/quartonotebook/main' -include { QUARTONOTEBOOK as ST_SVG } from '../../modules/nf-core/quartonotebook/main' -include { QUARTONOTEBOOK as ST_CLUSTERING } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as ST_QUALITY_CONTROLS } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as ST_SPATIALLY_VARIABLE_GENES } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as ST_CLUSTERING } from '../../modules/nf-core/quartonotebook/main' workflow ST_DOWNSTREAM { @@ -18,9 +18,9 @@ workflow ST_DOWNSTREAM { // // Quarto reports and extension files // - quality_controls_file = file("${projectDir}/bin/st_quality_controls.qmd", checkIfExists: true) - clustering_file = file("${projectDir}/bin/st_clustering.qmd", checkIfExists: true) - svg_file = file("${projectDir}/bin/st_svg.qmd", checkIfExists: true) + quality_controls_notebook = file("${projectDir}/bin/st_quality_controls.qmd", checkIfExists: true) + clustering_notebook = file("${projectDir}/bin/st_clustering.qmd", checkIfExists: true) + spatially_variable_genes_notebook = file("${projectDir}/bin/st_spatially_variable_genes.qmd", checkIfExists: true) extensions = Channel.fromPath("${projectDir}/assets/_extensions").collect() // @@ -29,7 +29,7 @@ workflow ST_DOWNSTREAM { ch_quality_controls_input_data = st_sdata_raw .map { it -> it[1] } ch_quality_controls_notebook = st_sdata_raw - .map { tuple(it[0], quality_controls_file) } + .map { tuple(it[0], quality_controls_notebook) } quality_controls_params = [ input_sdata: "st_sdata_raw.zarr", min_counts: params.st_qc_min_counts, @@ -38,7 +38,9 @@ workflow ST_DOWNSTREAM { mito_threshold: params.st_qc_mito_threshold, ribo_threshold: params.st_qc_ribo_threshold, hb_threshold: params.st_qc_hb_threshold, - output_sdata: "st_sdata_filtered.zarr" + artifact_dir: "artifacts", + output_adata: "st_adata_filtered.h5ad", + output_sdata: "st_sdata_filtered.zarr", ] ST_QUALITY_CONTROLS ( ch_quality_controls_notebook, @@ -54,13 +56,14 @@ workflow ST_DOWNSTREAM { ch_clustering_input_data = ST_QUALITY_CONTROLS.out.artifacts .map { it -> it[1] } ch_clustering_notebook = ST_QUALITY_CONTROLS.out.artifacts - .map { tuple(it[0], clustering_file) } + .map { tuple(it[0], clustering_notebook) } clustering_params = [ - input_sdata_filtered: "st_adata_filtered.zarr", + input_sdata: "st_sdata_filtered.zarr", cluster_resolution: params.st_cluster_resolution, n_hvgs: params.st_cluster_n_hvgs, - output_adata_processed: "st_adata_processed.h5ad", - output_sdata: "st_sdata_processed.zarr" + artifact_dir: "artifacts", + output_adata: "st_adata_processed.h5ad", + output_sdata: "st_sdata_processed.zarr", ] ST_CLUSTERING ( ch_clustering_notebook, @@ -73,40 +76,41 @@ workflow ST_DOWNSTREAM { // // Spatially variable genes // - ch_spatial_de_input_data = ST_CLUSTERING.out.artifacts + ch_spatially_variable_genes_input_data = ST_CLUSTERING.out.artifacts .map { it -> it[1] } - ch_svg_notebook = ST_CLUSTERING.out.artifacts - .map { tuple(it[0], svg_file) } - svg_params = [ + ch_spatially_variable_genes_notebook = ST_CLUSTERING.out.artifacts + .map { tuple(it[0], spatially_variable_genes_notebook) } + spatially_variable_genes_params = [ input_sdata: "st_sdata_processed.zarr", - output_adata_svg: "st_adata_svg.h5ad", - output_sdata: "st_sdata_svg.zarr", - output_svg: "st_svg.csv", - n_top_spatial_degs: params.st_n_top_spatial_degs + n_top_spatial_degs: params.st_n_top_spatial_degs, + artifact_dir: "artifacts", + output_csv: "st_spatially_variable_genes.csv", + output_adata: "st_adata_spatially_variable_genes.h5ad", + output_sdata: "st_sdata.zarr", ] - ST_SVG ( - ch_svg_notebook, - svg_params, - ch_spatial_de_input_data, + ST_SPATIALLY_VARIABLE_GENES ( + ch_spatially_variable_genes_notebook, + spatially_variable_genes_params, + ch_spatially_variable_genes_input_data, extensions ) - ch_versions = ch_versions.mix(ST_SVG.out.versions) + ch_versions = ch_versions.mix(ST_SPATIALLY_VARIABLE_GENES.out.versions) emit: - st_qc_html = ST_QUALITY_CONTROLS.out.html // channel: [ meta, html ] - st_sdata_filtered = ST_QUALITY_CONTROLS.out.artifacts // channel: [ meta, h5ad ] - st_qc_notebook = ST_QUALITY_CONTROLS.out.notebook // channel: [ meta, qmd ] - st_qc_params = ST_QUALITY_CONTROLS.out.params_yaml // channel: [ meta, yml ] + st_qc_html = ST_QUALITY_CONTROLS.out.html // channel: [ meta, html ] + st_sdata_filtered = ST_QUALITY_CONTROLS.out.artifacts // channel: [ meta, h5ad ] + st_qc_notebook = ST_QUALITY_CONTROLS.out.notebook // channel: [ meta, qmd ] + st_qc_params = ST_QUALITY_CONTROLS.out.params_yaml // channel: [ meta, yml ] - st_clustering_html = ST_CLUSTERING.out.html // channel: [ html ] - st_sdata_processed = ST_CLUSTERING.out.artifacts // channel: [ meta, h5ad] - st_clustering_notebook = ST_CLUSTERING.out.notebook // channel: [ meta, qmd ] - st_clustering_params = ST_CLUSTERING.out.params_yaml // channel: [ meta, yml ] + st_clustering_html = ST_CLUSTERING.out.html // channel: [ html ] + st_sdata_processed = ST_CLUSTERING.out.artifacts // channel: [ meta, h5ad] + st_clustering_notebook = ST_CLUSTERING.out.notebook // channel: [ meta, qmd ] + st_clustering_params = ST_CLUSTERING.out.params_yaml // channel: [ meta, yml ] - st_svg_html = ST_SVG.out.html // channel: [ meta, html ] - st_output = ST_SVG.out.artifacts // channel: [ meta, csv ] - st_svg_notebook = ST_SVG.out.notebook // channel: [ meta, qmd ] - st_svg_params = ST_SVG.out.params_yaml // channel: [ meta, yml ] + st_svg_html = ST_SPATIALLY_VARIABLE_GENES.out.html // channel: [ meta, html ] + st_output = ST_SPATIALLY_VARIABLE_GENES.out.artifacts // channel: [ meta, csv ] + st_svg_notebook = ST_SPATIALLY_VARIABLE_GENES.out.notebook // channel: [ meta, qmd ] + st_svg_params = ST_SPATIALLY_VARIABLE_GENES.out.params_yaml // channel: [ meta, yml ] - versions = ch_versions // channel: [ versions.yml ] + versions = ch_versions // channel: [ versions.yml ] } diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 8c400aa..51e405d 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -34,14 +34,14 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_svg.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_spatially_variable_genes.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 4587197..b918fbc 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-07T13:23:56.226763" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 25abe67..df375e6 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -29,10 +29,10 @@ nextflow_pipeline { // Reports { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_svg.csv").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index c36ecfc..f0c3b50 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -11,7 +11,7 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -19,4 +19,4 @@ }, "timestamp": "2024-03-07T13:55:24.407294" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 8b4638d..c2fba52 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -25,10 +25,10 @@ nextflow_pipeline { // Reports { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_svg.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_svg.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 02e6954..8aede30 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SVG={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-07T14:28:20.163729" } -} \ No newline at end of file +} From 8238a32b96e817992d1469892c26fb918786990d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Mar 2024 16:26:37 +0100 Subject: [PATCH 371/410] Remove `st_` prefix from names/variables/etc. --- bin/{st_clustering.qmd => clustering.qmd} | 52 ++++---- ...lity_controls.qmd => quality_controls.qmd} | 100 +++++++-------- bin/{read_st_data.py => read_data.py} | 5 +- ...genes.qmd => spatially_variable_genes.qmd} | 48 ++++---- conf/modules.config | 14 +-- conf/test.config | 4 +- conf/test_downstream.config | 4 +- conf/test_spaceranger_v1.config | 4 +- docs/output.md | 10 +- .../local/{st_read_data.nf => read_data.nf} | 8 +- nextflow.config | 18 +-- nextflow_schema.json | 18 +-- subworkflows/local/downstream.nf | 116 ++++++++++++++++++ subworkflows/local/spaceranger.nf | 4 +- subworkflows/local/st_downstream.nf | 116 ------------------ tests/pipeline/test_downstream.nf.test | 28 ++--- tests/pipeline/test_downstream.nf.test.snap | 2 +- .../pipeline/test_spaceranger_ffpe_v1.nf.test | 16 +-- .../test_spaceranger_ffpe_v1.nf.test.snap | 4 +- ...test_spaceranger_ffpe_v2_cytassist.nf.test | 12 +- ...spaceranger_ffpe_v2_cytassist.nf.test.snap | 2 +- workflows/spatialtranscriptomics.nf | 14 +-- 22 files changed, 300 insertions(+), 299 deletions(-) rename bin/{st_clustering.qmd => clustering.qmd} (77%) rename bin/{st_quality_controls.qmd => quality_controls.qmd} (73%) rename bin/{read_st_data.py => read_data.py} (89%) rename bin/{st_spatially_variable_genes.qmd => spatially_variable_genes.qmd} (71%) rename modules/local/{st_read_data.nf => read_data.nf} (87%) create mode 100644 subworkflows/local/downstream.nf delete mode 100644 subworkflows/local/st_downstream.nf diff --git a/bin/st_clustering.qmd b/bin/clustering.qmd similarity index 77% rename from bin/st_clustering.qmd rename to bin/clustering.qmd index e7516c5..e19cc6f 100644 --- a/bin/st_clustering.qmd +++ b/bin/clustering.qmd @@ -11,12 +11,12 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata_filtered.zarr" # Input: SpatialData file +input_sdata = "sdata_filtered.zarr" # Input: SpatialData file cluster_resolution = 1 # Resolution for Leiden clustering n_hvgs = 2000 # Number of HVGs to use for analyses artifact_dir = "artifacts" # Output directory -output_adata = "st_adata_processed.h5ad" # Output: AnnData file -output_sdata = "st_sdata_processed.zarr" # Output: SpatialData file +output_adata = "adata_processed.h5ad" # Output: AnnData file +output_sdata = "sdata_processed.zarr" # Output: SpatialData file ``` The data has already been filtered in the _quality controls_ reports and is @@ -62,11 +62,11 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ``` ```{python} -st_sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) -st_adata = to_legacy_anndata(st_sdata) +sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) +adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") -print(st_adata) +print(adata) ``` # Normalization @@ -76,8 +76,8 @@ use the built-in `normalize_total` method from [Scanpy](https://scanpy.readthedo followed by a log-transformation. ```{python} -sc.pp.normalize_total(st_adata, inplace=True) -sc.pp.log1p(st_adata) +sc.pp.normalize_total(adata, inplace=True) +sc.pp.log1p(adata) ``` # Feature selection @@ -90,13 +90,13 @@ regards to yielding a good separation of clusters. ```{python} # layout-nrow: 1 # Find top HVGs and print results -sc.pp.highly_variable_genes(st_adata, flavor="seurat", n_top_genes=n_hvgs) -var_genes_all = st_adata.var.highly_variable +sc.pp.highly_variable_genes(adata, flavor="seurat", n_top_genes=n_hvgs) +var_genes_all = adata.var.highly_variable print("Extracted highly variable genes: %d"%sum(var_genes_all)) # Plot the HVGs plt.rcParams["figure.figsize"] = (4.5, 4.5) -sc.pl.highly_variable_genes(st_adata) +sc.pl.highly_variable_genes(adata) ``` # Clustering @@ -108,10 +108,10 @@ Manifold Approximation and Projection) is used for visualization. The Leiden algorithm is employed for clustering with a given resolution. ```{python} -sc.pp.pca(st_adata) -sc.pp.neighbors(st_adata) -sc.tl.umap(st_adata) -sc.tl.leiden(st_adata, key_added="clusters", resolution=cluster_resolution) +sc.pp.pca(adata) +sc.pp.neighbors(adata) +sc.tl.umap(adata) +sc.tl.leiden(adata, key_added="clusters", resolution=cluster_resolution) Markdown(f"Resolution for Leiden clustering: `{cluster_resolution}`") ``` @@ -122,7 +122,7 @@ We then generate UMAP plots to visualize the distribution of clusters: ```{python} #| warning: false plt.rcParams["figure.figsize"] = (7, 7) -sc.pl.umap(st_adata, color="clusters") +sc.pl.umap(adata, color="clusters") ``` ## Counts and genes @@ -133,7 +133,7 @@ the UMAP: ```{python} # Make plots of UMAP of ST spots clusters plt.rcParams["figure.figsize"] = (3.5, 3.5) -sc.pl.umap(st_adata, color=["total_counts", "n_genes_by_counts"]) +sc.pl.umap(adata, color=["total_counts", "n_genes_by_counts"]) ``` ## Individual clusters @@ -142,8 +142,8 @@ An additional visualisation is to show where the various spots are in each individual cluster while ignoring all other cluster: ```{python} -sc.tl.embedding_density(st_adata, basis="umap", groupby="clusters") -sc.pl.embedding_density(st_adata, groupby="clusters", ncols=2) +sc.tl.embedding_density(adata, basis="umap", groupby="clusters") +sc.pl.embedding_density(adata, groupby="clusters", ncols=2) ``` # Spatial visualisation @@ -154,8 +154,8 @@ spatial coordinates by overlaying the spots on the tissue image itself. ```{python} #| layout-nrow: 2 plt.rcParams["figure.figsize"] = (8, 8) -sc.pl.spatial(st_adata, img_key="hires", color="total_counts", size=1.25) -sc.pl.spatial(st_adata, img_key="hires", color="n_genes_by_counts", size=1.25) +sc.pl.spatial(adata, img_key="hires", color="total_counts", size=1.25) +sc.pl.spatial(adata, img_key="hires", color="n_genes_by_counts", size=1.25) ``` To gain insights into tissue organization and potential inter-cellular @@ -167,13 +167,13 @@ organization of cells. ```{python} # TODO: Can the colour bar on this figure be fit to the figure? plt.rcParams["figure.figsize"] = (7, 7) -sc.pl.spatial(st_adata, img_key="hires", color="clusters", size=1.25) +sc.pl.spatial(adata, img_key="hires", color="clusters", size=1.25) ``` ```{python} #| echo: false -del st_sdata.table -st_sdata.table = st_adata -st_adata.write(os.path.join(artifact_dir, output_adata)) -st_sdata.write(os.path.join(artifact_dir, output_sdata)) +del sdata.table +sdata.table = adata +adata.write(os.path.join(artifact_dir, output_adata)) +sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/bin/st_quality_controls.qmd b/bin/quality_controls.qmd similarity index 73% rename from bin/st_quality_controls.qmd rename to bin/quality_controls.qmd index 0586b88..7c94f78 100644 --- a/bin/st_quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -26,7 +26,7 @@ analysis tools and facilitates seamless integration into existing workflows. ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata_raw.zarr" # Input: SpatialData file +input_sdata = "sdata_raw.zarr" # Input: SpatialData file min_counts = 500 # Min counts per spot min_genes = 250 # Min genes per spot min_spots = 1 # Min spots per gene @@ -34,8 +34,8 @@ mito_threshold = 20 # Mitochondrial content threshold (%) ribo_threshold = 0 # Ribosomal content threshold (%) hb_threshold = 100 # content threshold (%) artifact_dir = "artifacts" -output_adata = "st_adata_filtered.h5ad" # Output: AnnData file -output_sdata = "st_sdata_filtered.zarr" # Output: SpatialData file +output_adata = "adata_filtered.h5ad" # Output: AnnData file +output_sdata = "sdata_filtered.zarr" # Output: SpatialData file ``` ```{python} @@ -78,18 +78,18 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read the data -st_sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) -st_adata = to_legacy_anndata(st_sdata) +sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) +adata = to_legacy_anndata(sdata) # Convert X matrix from csr to csc dense matrix for output compatibility: -st_adata.X = scipy.sparse.csc_matrix(st_adata.X) +adata.X = scipy.sparse.csc_matrix(adata.X) # Store the raw data so that it can be used for analyses from scratch if desired -st_adata.layers['raw'] = st_adata.X.copy() +adata.layers['raw'] = adata.X.copy() # Print the anndata object for inspection print("Content of the AnnData object:") -print(st_adata) +print(adata) ``` # Quality controls @@ -102,14 +102,14 @@ percentage of counts from mitochondrial, ribosomal and haemoglobin genes ```{python} # Calculate mitochondrial, ribosomal and haemoglobin percentages -st_adata.var['mt'] = st_adata.var_names.str.startswith('MT-') -st_adata.var['ribo'] = st_adata.var_names.str.contains(("^RP[LS]")) -st_adata.var['hb'] = st_adata.var_names.str.contains(("^HB[AB]")) -sc.pp.calculate_qc_metrics(st_adata, qc_vars=["mt", "ribo", "hb"], +adata.var['mt'] = adata.var_names.str.startswith('MT-') +adata.var['ribo'] = adata.var_names.str.contains(("^RP[LS]")) +adata.var['hb'] = adata.var_names.str.contains(("^HB[AB]")) +sc.pp.calculate_qc_metrics(adata, qc_vars=["mt", "ribo", "hb"], inplace=True, log1p=False) # Save a copy of data as a restore-point if filtering results in 0 spots left -st_adata_before_filtering = st_adata.copy() +adata_before_filtering = adata.copy() ``` ## Violin plots @@ -120,9 +120,9 @@ mitochondrial, ribosomal and haemoglobin genes: ```{python} #| layout-nrow: 2 -sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], +sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) -sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], +sc.pl.violin(adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` @@ -133,8 +133,8 @@ spatial patterns may be discerned: ```{python} #| layout-nrow: 2 -sc.pl.spatial(st_adata, color = ["total_counts", "n_genes_by_counts"], size=1.25) -sc.pl.spatial(st_adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"], size=1.25) +sc.pl.spatial(adata, color = ["total_counts", "n_genes_by_counts"], size=1.25) +sc.pl.spatial(adata, color = ["pct_counts_mt", "pct_counts_ribo", "pct_counts_hb"], size=1.25) ``` ## Scatter plots @@ -145,8 +145,8 @@ counts versus the number of genes: ```{python} #| layout-ncol: 2 -sc.pl.scatter(st_adata, x='pct_counts_ribo', y='pct_counts_mt') -sc.pl.scatter(st_adata, x='total_counts', y='n_genes_by_counts') +sc.pl.scatter(adata, x='pct_counts_ribo', y='pct_counts_mt') +sc.pl.scatter(adata, x='total_counts', y='n_genes_by_counts') ``` ## Top expressed genes @@ -155,7 +155,7 @@ It can also be informative to see which genes are the most expressed in the dataset; the following figure shows the top 20 most expressed genes. ```{python} -sc.pl.highest_expr_genes(st_adata, n_top=20) +sc.pl.highest_expr_genes(adata, n_top=20) ``` # Filtering @@ -167,16 +167,16 @@ are uninformative and are thus removed. ```{python} # Create a string observation "obs/in_tissue_str" with "In tissue" and "Outside tissue": -st_adata.obs["in_tissue_str"] = ["In tissue" if x == 1 else "Outside tissue" for x in st_adata.obs["in_tissue"]] +adata.obs["in_tissue_str"] = ["In tissue" if x == 1 else "Outside tissue" for x in adata.obs["in_tissue"]] # Plot spots inside tissue -sc.pl.spatial(st_adata, color=["in_tissue_str"], title="Spots in tissue", size=1.25) -del st_adata.obs["in_tissue_str"] +sc.pl.spatial(adata, color=["in_tissue_str"], title="Spots in tissue", size=1.25) +del adata.obs["in_tissue_str"] # Remove spots outside tissue and print results -n_spots = st_adata.shape[0] -st_adata = st_adata[st_adata.obs["in_tissue"] == 1] -n_spots_in_tissue = st_adata.shape[0] +n_spots = adata.shape[0] +adata = adata[adata.obs["in_tissue"] == 1] +n_spots_in_tissue = adata.shape[0] Markdown(f"""A total of `{n_spots_in_tissue}` spots are situated inside the tissue, out of `{n_spots}` spots in total.""") ``` @@ -190,18 +190,18 @@ your knowledge of the specific tissue at hand. ```{python} #| warning: false # Filter spots based on counts -n_spots = st_adata.shape[0] -n_genes = st_adata.shape[1] -sc.pp.filter_cells(st_adata, min_counts=min_counts) -n_spots_filtered_min_counts = st_adata.shape[0] +n_spots = adata.shape[0] +n_genes = adata.shape[1] +sc.pp.filter_cells(adata, min_counts=min_counts) +n_spots_filtered_min_counts = adata.shape[0] # Filter spots based on genes -sc.pp.filter_cells(st_adata, min_genes=min_genes) -n_spots_filtered_min_genes = st_adata.shape[0] +sc.pp.filter_cells(adata, min_genes=min_genes) +n_spots_filtered_min_genes = adata.shape[0] # Filter genes based on spots -sc.pp.filter_genes(st_adata, min_cells=min_spots) -n_genes_filtered_min_spots = st_adata.shape[1] +sc.pp.filter_genes(adata, min_cells=min_spots) +n_genes_filtered_min_spots = adata.shape[1] # Print results Markdown(f""" @@ -220,16 +220,16 @@ ribosomal nor haemoglobin content is filtered by default. ```{python} # Filter spots -st_adata = st_adata[st_adata.obs["pct_counts_mt"] <= mito_threshold] -n_spots_filtered_mito = st_adata.shape[0] -st_adata = st_adata[st_adata.obs["pct_counts_ribo"] >= ribo_threshold] -n_spots_filtered_ribo = st_adata.shape[0] -st_adata = st_adata[st_adata.obs["pct_counts_hb"] <= hb_threshold] -n_spots_filtered_hb = st_adata.shape[0] +adata = adata[adata.obs["pct_counts_mt"] <= mito_threshold] +n_spots_filtered_mito = adata.shape[0] +adata = adata[adata.obs["pct_counts_ribo"] >= ribo_threshold] +n_spots_filtered_ribo = adata.shape[0] +adata = adata[adata.obs["pct_counts_hb"] <= hb_threshold] +n_spots_filtered_hb = adata.shape[0] # Print results Markdown(f""" -- Removed `{st_adata.shape[0] - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. +- Removed `{adata.shape[0] - n_spots_filtered_mito}` spots with more than `{mito_threshold}%` mitochondrial content. - Removed `{n_spots_filtered_mito - n_spots_filtered_ribo}` spots with less than `{ribo_threshold}%` ribosomal content. - Removed `{n_spots_filtered_ribo - n_spots_filtered_hb}` spots with more than `{hb_threshold}%` haemoglobin content. """) @@ -238,8 +238,8 @@ Markdown(f""" ```{python} #| echo: false # Restore non-filtered data if filtering results in 0 spots left -if (st_adata.shape[0] == 0 or st_adata.shape[1] == 0): - st_adata = st_adata_before_filtering +if (adata.shape[0] == 0 or adata.shape[1] == 0): + adata = adata_before_filtering display( Markdown(dedent( """ @@ -269,21 +269,21 @@ if (st_adata.shape[0] == 0 or st_adata.shape[1] == 0): Markdown(f""" The final results of all the filtering is as follows: -- A total of `{st_adata.shape[0]}` spots out of `{n_spots}` remain after filtering. -- A total of `{st_adata.shape[1]}` genes out of `{n_genes}` remain after filtering. +- A total of `{adata.shape[0]}` spots out of `{n_spots}` remain after filtering. +- A total of `{adata.shape[1]}` genes out of `{n_genes}` remain after filtering. """) ``` ```{python} #| layout-nrow: 2 -sc.pl.violin(st_adata, ['n_genes_by_counts', 'total_counts'], +sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts'], multi_panel=True, jitter=0.4, rotation= 45) -sc.pl.violin(st_adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], +sc.pl.violin(adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], multi_panel=True, jitter=0.4, rotation= 45) ``` ```{python} -del st_sdata.table -st_sdata.table = st_adata -st_sdata.write(os.path.join(artifact_dir, output_sdata)) +del sdata.table +sdata.table = adata +sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/bin/read_st_data.py b/bin/read_data.py similarity index 89% rename from bin/read_st_data.py rename to bin/read_data.py index 8379243..fd27753 100755 --- a/bin/read_st_data.py +++ b/bin/read_data.py @@ -2,6 +2,7 @@ # Load packages import argparse + import spatialdata_io if __name__ == "__main__": @@ -28,9 +29,9 @@ args = parser.parse_args() # Read Visium data - st_spatialdata = spatialdata_io.visium( + spatialdata = spatialdata_io.visium( args.SRCountDir, counts_file="raw_feature_bc_matrix.h5", dataset_id="visium" ) # Write raw spatialdata to file - st_spatialdata.write(args.output_sdata, overwrite=True) + spatialdata.write(args.output_sdata, overwrite=True) diff --git a/bin/st_spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd similarity index 71% rename from bin/st_spatially_variable_genes.qmd rename to bin/spatially_variable_genes.qmd index 2e60664..c9421b9 100644 --- a/bin/st_spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -9,12 +9,12 @@ jupyter: python3 ```{python} #| tags: [parameters] #| echo: false -input_sdata = "st_sdata_processed.zarr" # Input: SpatialData file +input_sdata = "sdata_processed.zarr" # Input: SpatialData file n_top_spatial_degs = 14 # Number of spatially variable genes to plot artifact_dir = "artifacts" # Output directory -output_csv = "st_spatially_variable_genes.csv" # Output: gene list -output_adata = "st_adata_spatially_variable_genes.h5ad" # Output: AnnData file -output_sdata = "st_sdata.zarr" # Output: SpatialData file +output_csv = "spatially_variable_genes.csv" # Output: gene list +output_adata = "adata_spatially_variable_genes.h5ad" # Output: AnnData file +output_sdata = "sdata.zarr" # Output: SpatialData file ``` ```{python} @@ -53,14 +53,14 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read data -st_sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) +sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) -st_adata = to_legacy_anndata(st_sdata) +adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") -print(st_adata) +print(adata) # Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 -st_adata.uns['log1p']['base'] = None +adata.uns['log1p']['base'] = None # Suppress scanpy-specific warnings sc.settings.verbosity = 0 @@ -74,8 +74,8 @@ visualize the top DEGs in a heatmap: ```{python} #| warning: false -sc.tl.rank_genes_groups(st_adata, 'clusters', method='t-test') -sc.pl.rank_genes_groups_heatmap(st_adata, n_genes=5, groupby="clusters") +sc.tl.rank_genes_groups(adata, 'clusters', method='t-test') +sc.pl.rank_genes_groups_heatmap(adata, n_genes=5, groupby="clusters") ``` A different but similar visualization of the DEGs is the dot plot, where we can @@ -83,7 +83,7 @@ also include the gene names: ```{python} #| warning: false -sc.pl.rank_genes_groups_dotplot(st_adata, n_genes=5, groupby="clusters") +sc.pl.rank_genes_groups_dotplot(adata, n_genes=5, groupby="clusters") ``` ::: {.callout-note} @@ -98,16 +98,16 @@ We can perform a neighborhood enrichment analysis to find out which genes are enriched in the neighborhood of each cluster: ```{python} -sq.gr.spatial_neighbors(st_adata, coord_type="generic") -sq.gr.nhood_enrichment(st_adata, cluster_key="clusters") -sq.pl.nhood_enrichment(st_adata, cluster_key="clusters", method="ward", vmin=-100, vmax=100) +sq.gr.spatial_neighbors(adata, coord_type="generic") +sq.gr.nhood_enrichment(adata, cluster_key="clusters") +sq.pl.nhood_enrichment(adata, cluster_key="clusters", method="ward", vmin=-100, vmax=100) ``` We visualize the interaction matrix between the different clusters: ```{python} -sq.gr.interaction_matrix(st_adata, cluster_key="clusters") -sq.pl.interaction_matrix(st_adata, cluster_key="clusters", method="ward", vmax=20000) +sq.gr.interaction_matrix(adata, cluster_key="clusters") +sq.pl.interaction_matrix(adata, cluster_key="clusters", method="ward", vmax=20000) ``` # Spatially variable genes with spatial autocorrelation statistics @@ -117,21 +117,21 @@ different areas in a tissue, allowing identification of spatial gene expression patterns. Here we use [Moran's I](https://en.wikipedia.org/wiki/Moran%27s_I) autocorrelation score to identify such patterns. ```{python} -st_adata.var_names_make_unique() -sq.gr.spatial_autocorr(st_adata, mode="moran") -st_adata.uns["moranI"].head(n_top_spatial_degs) +adata.var_names_make_unique() +sq.gr.spatial_autocorr(adata, mode="moran") +adata.uns["moranI"].head(n_top_spatial_degs) #[TODO] add gearyC as optional mode ``` ```{python} #| echo: false -st_adata.write(output_adata) -del st_sdata.table -st_sdata.table = st_adata -st_sdata.write("./" + output_sdata) +adata.write(output_adata) +del sdata.table +sdata.table = adata +sdata.write("./" + output_sdata) ``` ```{python} -st_adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) +adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) ``` diff --git a/conf/modules.config b/conf/modules.config index 5d48d0b..e8e3e63 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -62,7 +62,7 @@ process { ] } - withName: 'ST_READ_DATA|ST_QUALITY_CONTROLS|ST_CLUSTERING|ST_SPATIALLY_VARIABLE_GENES' { + withName: 'READ_DATA|QUALITY_CONTROLS|CLUSTERING|SPATIALLY_VARIABLE_GENES' { ext.prefix = { "${notebook.baseName}" } publishDir = [ [ @@ -79,20 +79,20 @@ process { [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_sdata_processed.zarr", - saveAs: { "st_sdata_processed.zarr" } + pattern: "artifacts/sdata_processed.zarr", + saveAs: { "sdata_processed.zarr" } ], [ path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_adata_processed.h5ad", - saveAs: { "st_adata_processed.h5ad" } + pattern: "artifacts/adata_processed.h5ad", + saveAs: { "adata_processed.h5ad" } ], [ path: { "${params.outdir}/${meta.id}/degs" }, mode: params.publish_dir_mode, - pattern: "artifacts/st_spatially_variable_genes.csv", - saveAs: { "st_spatially_variable_genes.csv" } + pattern: "artifacts/spatially_variable_genes.csv", + saveAs: { "spatially_variable_genes.csv" } ] ] } diff --git a/conf/test.config b/conf/test.config index ce4909c..97b801b 100644 --- a/conf/test.config +++ b/conf/test.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + qc_min_counts = 5 + qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_downstream.config b/conf/test_downstream.config index ddb52b1..51a6dc9 100644 --- a/conf/test_downstream.config +++ b/conf/test_downstream.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + qc_min_counts = 5 + qc_min_genes = 3 outdir = 'results' } diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index 2fca10e..5bce856 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -25,7 +25,7 @@ params { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + qc_min_counts = 5 + qc_min_genes = 3 outdir = 'results' } diff --git a/docs/output.md b/docs/output.md index 98f17c9..d5ea2f6 100644 --- a/docs/output.md +++ b/docs/output.md @@ -53,7 +53,7 @@ information about these files at the [10X website](https://support.10xgenomics.c Output files - `/data/` - - `st_adata_processed.h5ad`: Filtered, normalised and clustered adata. + - `adata_processed.h5ad`: Filtered, normalised and clustered adata. @@ -71,7 +71,7 @@ the data in an interactive way. Output files - `/reports/` - - `st_quality_controls.html`: HTML report. + - `quality_controls.html`: HTML report. @@ -85,7 +85,7 @@ well as presence in tissue; you can find more details in the report itself. Output files - `/reports/` - - `st_clustering.html`: HTML report. + - `clustering.html`: HTML report. @@ -99,9 +99,9 @@ option; you can find more details in the report itself. Output files - `/reports/` - - `st_spatially_variable_genes.html`: HTML report. + - `spatially_variable_genes.html`: HTML report. - `/degs/` - - `st_spatially_variable_genes.csv`: List of spatially variable genes. + - `spatially_variable_genes.csv`: List of spatially variable genes. diff --git a/modules/local/st_read_data.nf b/modules/local/read_data.nf similarity index 87% rename from modules/local/st_read_data.nf rename to modules/local/read_data.nf index 9f162e5..3e15428 100644 --- a/modules/local/st_read_data.nf +++ b/modules/local/read_data.nf @@ -1,7 +1,7 @@ // // Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file // -process ST_READ_DATA { +process READ_DATA { tag "${meta.id}" label 'process_low' @@ -13,7 +13,7 @@ process ST_READ_DATA { tuple val (meta), path("${meta.id}/*") output: - tuple val(meta), path("st_sdata_raw.zarr"), emit: st_sdata_raw + tuple val(meta), path("sdata_raw.zarr"), emit: sdata_raw path("versions.yml") , emit: versions when: @@ -34,9 +34,9 @@ process ST_READ_DATA { export XDG_DATA_HOME="./.xdg_data_home" # Execute read data script - read_st_data.py \\ + read_data.py \\ --SRCountDir "${meta.id}" \\ - --output_sdata st_sdata_raw.zarr + --output_sdata sdata_raw.zarr cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/nextflow.config b/nextflow.config index bf66982..70fa1c0 100644 --- a/nextflow.config +++ b/nextflow.config @@ -18,19 +18,19 @@ params { spaceranger_save_reference = false // Quality controls and filtering - st_qc_min_counts = 500 - st_qc_min_genes = 250 - st_qc_min_spots = 1 - st_qc_mito_threshold = 20.0 - st_qc_ribo_threshold = 0.0 - st_qc_hb_threshold = 100.0 + qc_min_counts = 500 + qc_min_genes = 250 + qc_min_spots = 1 + qc_mito_threshold = 20.0 + qc_ribo_threshold = 0.0 + qc_hb_threshold = 100.0 // Clustering - st_cluster_n_hvgs = 2000 - st_cluster_resolution = 1.0 + cluster_n_hvgs = 2000 + cluster_resolution = 1.0 // Spatial differential expression - st_n_top_spatial_degs = 14 + n_top_spatial_degs = 14 // MultiQC options multiqc_config = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 17e9cde..64b7551 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -96,57 +96,57 @@ "fa_icon": "fas fa-magnifying-glass-chart", "description": "Options related to the downstream analyses performed by the pipeline.", "properties": { - "st_qc_min_counts": { + "qc_min_counts": { "type": "integer", "default": 500, "description": "The minimum number of UMIs needed in a spot for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_qc_min_genes": { + "qc_min_genes": { "type": "integer", "default": 250, "description": "The minimum number of expressed genes in a spot needed for that spot to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_qc_min_spots": { + "qc_min_spots": { "type": "integer", "default": 1, "description": "The minimum number of spots in which a gene is expressed for that gene to pass the filtering.", "fa_icon": "fas fa-hashtag" }, - "st_qc_mito_threshold": { + "qc_mito_threshold": { "type": "number", "default": 20, "description": "The maximum proportion of mitochondrial content that a spot is allowed to have to pass the filtering.", "help_text": "If you do not wish to filter based on mitochondrial content, set this parameter to `100`.", "fa_icon": "fas fa-hashtag" }, - "st_qc_ribo_threshold": { + "qc_ribo_threshold": { "type": "number", "default": 0, "description": "The minimum proportion of ribosomal content that a spot is needs to have to pass the filtering (no filtering is done by default).", "fa_icon": "fas fa-hashtag" }, - "st_qc_hb_threshold": { + "qc_hb_threshold": { "type": "number", "default": 100, "description": "The maximum proportion of haemoglobin content that a spot is allowed to have to pass the filtering (no filtering is done by default).", "fa_icon": "fas fa-hashtag" }, - "st_cluster_n_hvgs": { + "cluster_n_hvgs": { "type": "integer", "default": 2000, "description": "The number of top highly variable genes to use for the analyses.", "fa_icon": "fas fa-hashtag" }, - "st_cluster_resolution": { + "cluster_resolution": { "type": "number", "default": 1, "description": "The resolution for the clustering of the spots.", "help_text": "The resolution controls the coarseness of the clustering, where a higher resolution leads to more clusters.", "fa_icon": "fas fa-circle-nodes" }, - "st_n_top_spatial_degs": { + "n_top_spatial_degs": { "type": "integer", "default": 14, "description": "The number of top spatial differentially expressed genes to plot.", diff --git a/subworkflows/local/downstream.nf b/subworkflows/local/downstream.nf new file mode 100644 index 0000000..decbcbe --- /dev/null +++ b/subworkflows/local/downstream.nf @@ -0,0 +1,116 @@ +// +// Subworkflow for downstream analyses of ST data +// + +include { QUARTONOTEBOOK as QUALITY_CONTROLS } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as SPATIALLY_VARIABLE_GENES } from '../../modules/nf-core/quartonotebook/main' +include { QUARTONOTEBOOK as CLUSTERING } from '../../modules/nf-core/quartonotebook/main' + +workflow DOWNSTREAM { + + take: + sdata_raw + + main: + + ch_versions = Channel.empty() + + // + // Quarto reports and extension files + // + quality_controls_notebook = file("${projectDir}/bin/quality_controls.qmd", checkIfExists: true) + clustering_notebook = file("${projectDir}/bin/clustering.qmd", checkIfExists: true) + spatially_variable_genes_notebook = file("${projectDir}/bin/spatially_variable_genes.qmd", checkIfExists: true) + extensions = Channel.fromPath("${projectDir}/assets/_extensions").collect() + + // + // Quality controls and filtering + // + ch_quality_controls_input_data = sdata_raw + .map { it -> it[1] } + ch_quality_controls_notebook = sdata_raw + .map { tuple(it[0], quality_controls_notebook) } + quality_controls_params = [ + input_sdata: "sdata_raw.zarr", + min_counts: params.qc_min_counts, + min_genes: params.qc_min_genes, + min_spots: params.qc_min_spots, + mito_threshold: params.qc_mito_threshold, + ribo_threshold: params.qc_ribo_threshold, + hb_threshold: params.qc_hb_threshold, + artifact_dir: "artifacts", + output_adata: "adata_filtered.h5ad", + output_sdata: "sdata_filtered.zarr", + ] + QUALITY_CONTROLS ( + ch_quality_controls_notebook, + quality_controls_params, + ch_quality_controls_input_data, + extensions + ) + ch_versions = ch_versions.mix(QUALITY_CONTROLS.out.versions) + + // + // Normalisation, dimensionality reduction and clustering + // + ch_clustering_input_data = QUALITY_CONTROLS.out.artifacts + .map { it -> it[1] } + ch_clustering_notebook = QUALITY_CONTROLS.out.artifacts + .map { tuple(it[0], clustering_notebook) } + clustering_params = [ + input_sdata: "sdata_filtered.zarr", + cluster_resolution: params.cluster_resolution, + n_hvgs: params.cluster_n_hvgs, + artifact_dir: "artifacts", + output_adata: "adata_processed.h5ad", + output_sdata: "sdata_processed.zarr", + ] + CLUSTERING ( + ch_clustering_notebook, + clustering_params, + ch_clustering_input_data, + extensions + ) + ch_versions = ch_versions.mix(CLUSTERING.out.versions) + + // + // Spatially variable genes + // + ch_spatially_variable_genes_input_data = CLUSTERING.out.artifacts + .map { it -> it[1] } + ch_spatially_variable_genes_notebook = CLUSTERING.out.artifacts + .map { tuple(it[0], spatially_variable_genes_notebook) } + spatially_variable_genes_params = [ + input_sdata: "sdata_processed.zarr", + n_top_spatial_degs: params.n_top_spatial_degs, + artifact_dir: "artifacts", + output_csv: "spatially_variable_genes.csv", + output_adata: "adata_spatially_variable_genes.h5ad", + output_sdata: "sdata.zarr", + ] + SPATIALLY_VARIABLE_GENES ( + ch_spatially_variable_genes_notebook, + spatially_variable_genes_params, + ch_spatially_variable_genes_input_data, + extensions + ) + ch_versions = ch_versions.mix(SPATIALLY_VARIABLE_GENES.out.versions) + + emit: + qc_html = QUALITY_CONTROLS.out.html // channel: [ meta, html ] + qc_sdata = QUALITY_CONTROLS.out.artifacts // channel: [ meta, h5ad ] + qc_nb = QUALITY_CONTROLS.out.notebook // channel: [ meta, qmd ] + qc_params = QUALITY_CONTROLS.out.params_yaml // channel: [ meta, yml ] + + clustering_html = CLUSTERING.out.html // channel: [ html ] + clustering_sdata = CLUSTERING.out.artifacts // channel: [ meta, h5ad] + clustering_nb = CLUSTERING.out.notebook // channel: [ meta, qmd ] + clustering_params = CLUSTERING.out.params_yaml // channel: [ meta, yml ] + + svg_html = SPATIALLY_VARIABLE_GENES.out.html // channel: [ meta, html ] + svg_csv = SPATIALLY_VARIABLE_GENES.out.artifacts // channel: [ meta, csv ] + svg_nb = SPATIALLY_VARIABLE_GENES.out.notebook // channel: [ meta, qmd ] + svg_params = SPATIALLY_VARIABLE_GENES.out.params_yaml // channel: [ meta, yml ] + + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/local/spaceranger.nf b/subworkflows/local/spaceranger.nf index 8816074..3dab2bf 100644 --- a/subworkflows/local/spaceranger.nf +++ b/subworkflows/local/spaceranger.nf @@ -8,7 +8,7 @@ include { SPACERANGER_COUNT } from '../../modules/nf-core/spa workflow SPACERANGER { take: - ch_st_data // channel: [ val(meta), [ raw st data ] ] + ch_data // channel: [ val(meta), [ raw st data ] ] main: @@ -44,7 +44,7 @@ workflow SPACERANGER { // Run Space Ranger count // SPACERANGER_COUNT ( - ch_st_data, + ch_data, ch_reference, ch_probeset ) diff --git a/subworkflows/local/st_downstream.nf b/subworkflows/local/st_downstream.nf deleted file mode 100644 index 6cef709..0000000 --- a/subworkflows/local/st_downstream.nf +++ /dev/null @@ -1,116 +0,0 @@ -// -// Subworkflow for downstream analyses of ST data -// - -include { QUARTONOTEBOOK as ST_QUALITY_CONTROLS } from '../../modules/nf-core/quartonotebook/main' -include { QUARTONOTEBOOK as ST_SPATIALLY_VARIABLE_GENES } from '../../modules/nf-core/quartonotebook/main' -include { QUARTONOTEBOOK as ST_CLUSTERING } from '../../modules/nf-core/quartonotebook/main' - -workflow ST_DOWNSTREAM { - - take: - st_sdata_raw - - main: - - ch_versions = Channel.empty() - - // - // Quarto reports and extension files - // - quality_controls_notebook = file("${projectDir}/bin/st_quality_controls.qmd", checkIfExists: true) - clustering_notebook = file("${projectDir}/bin/st_clustering.qmd", checkIfExists: true) - spatially_variable_genes_notebook = file("${projectDir}/bin/st_spatially_variable_genes.qmd", checkIfExists: true) - extensions = Channel.fromPath("${projectDir}/assets/_extensions").collect() - - // - // Quality controls and filtering - // - ch_quality_controls_input_data = st_sdata_raw - .map { it -> it[1] } - ch_quality_controls_notebook = st_sdata_raw - .map { tuple(it[0], quality_controls_notebook) } - quality_controls_params = [ - input_sdata: "st_sdata_raw.zarr", - min_counts: params.st_qc_min_counts, - min_genes: params.st_qc_min_genes, - min_spots: params.st_qc_min_spots, - mito_threshold: params.st_qc_mito_threshold, - ribo_threshold: params.st_qc_ribo_threshold, - hb_threshold: params.st_qc_hb_threshold, - artifact_dir: "artifacts", - output_adata: "st_adata_filtered.h5ad", - output_sdata: "st_sdata_filtered.zarr", - ] - ST_QUALITY_CONTROLS ( - ch_quality_controls_notebook, - quality_controls_params, - ch_quality_controls_input_data, - extensions - ) - ch_versions = ch_versions.mix(ST_QUALITY_CONTROLS.out.versions) - - // - // Normalisation, dimensionality reduction and clustering - // - ch_clustering_input_data = ST_QUALITY_CONTROLS.out.artifacts - .map { it -> it[1] } - ch_clustering_notebook = ST_QUALITY_CONTROLS.out.artifacts - .map { tuple(it[0], clustering_notebook) } - clustering_params = [ - input_sdata: "st_sdata_filtered.zarr", - cluster_resolution: params.st_cluster_resolution, - n_hvgs: params.st_cluster_n_hvgs, - artifact_dir: "artifacts", - output_adata: "st_adata_processed.h5ad", - output_sdata: "st_sdata_processed.zarr", - ] - ST_CLUSTERING ( - ch_clustering_notebook, - clustering_params, - ch_clustering_input_data, - extensions - ) - ch_versions = ch_versions.mix(ST_CLUSTERING.out.versions) - - // - // Spatially variable genes - // - ch_spatially_variable_genes_input_data = ST_CLUSTERING.out.artifacts - .map { it -> it[1] } - ch_spatially_variable_genes_notebook = ST_CLUSTERING.out.artifacts - .map { tuple(it[0], spatially_variable_genes_notebook) } - spatially_variable_genes_params = [ - input_sdata: "st_sdata_processed.zarr", - n_top_spatial_degs: params.st_n_top_spatial_degs, - artifact_dir: "artifacts", - output_csv: "st_spatially_variable_genes.csv", - output_adata: "st_adata_spatially_variable_genes.h5ad", - output_sdata: "st_sdata.zarr", - ] - ST_SPATIALLY_VARIABLE_GENES ( - ch_spatially_variable_genes_notebook, - spatially_variable_genes_params, - ch_spatially_variable_genes_input_data, - extensions - ) - ch_versions = ch_versions.mix(ST_SPATIALLY_VARIABLE_GENES.out.versions) - - emit: - st_qc_html = ST_QUALITY_CONTROLS.out.html // channel: [ meta, html ] - st_sdata_filtered = ST_QUALITY_CONTROLS.out.artifacts // channel: [ meta, h5ad ] - st_qc_notebook = ST_QUALITY_CONTROLS.out.notebook // channel: [ meta, qmd ] - st_qc_params = ST_QUALITY_CONTROLS.out.params_yaml // channel: [ meta, yml ] - - st_clustering_html = ST_CLUSTERING.out.html // channel: [ html ] - st_sdata_processed = ST_CLUSTERING.out.artifacts // channel: [ meta, h5ad] - st_clustering_notebook = ST_CLUSTERING.out.notebook // channel: [ meta, qmd ] - st_clustering_params = ST_CLUSTERING.out.params_yaml // channel: [ meta, yml ] - - st_svg_html = ST_SPATIALLY_VARIABLE_GENES.out.html // channel: [ meta, html ] - st_output = ST_SPATIALLY_VARIABLE_GENES.out.artifacts // channel: [ meta, csv ] - st_svg_notebook = ST_SPATIALLY_VARIABLE_GENES.out.notebook // channel: [ meta, qmd ] - st_svg_params = ST_SPATIALLY_VARIABLE_GENES.out.params_yaml // channel: [ meta, yml ] - - versions = ch_versions // channel: [ versions.yml ] -} diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 51e405d..404cc1f 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -12,8 +12,8 @@ nextflow_pipeline { spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters - st_qc_min_counts = 5 - st_qc_min_genes = 3 + qc_min_counts = 5 + qc_min_genes = 3 outdir = "$outputDir" } } @@ -26,22 +26,22 @@ nextflow_pipeline { { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, - { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_sdata_processed.zarr").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_adata_processed.h5ad").exists() }, - { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/st_sdata_processed.zarr").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/adata_processed.h5ad").exists() }, + { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/sdata_processed.zarr").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/adata_processed.h5ad").exists() }, + { assert path("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/sdata_processed.zarr").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/quality_controls.html").text.contains("final results of all the filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/quality_controls.html").text.contains("final results of all the filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatially_variable_genes.csv").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/st_spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/spatially_variable_genes.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index b918fbc..eba2f1a 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index df375e6..1aa7bd5 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -9,8 +9,8 @@ nextflow_pipeline { input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" - st_qc_min_counts = 5 - st_qc_min_genes = 3 + qc_min_counts = 5 + qc_min_genes = 3 outdir = "$outputDir" } } @@ -23,16 +23,16 @@ nextflow_pipeline { { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/st_sdata_processed.zarr").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/adata_processed.h5ad").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/sdata_processed.zarr").exists() }, // Reports - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/quality_controls.html").text.contains("final results of all the filtering") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/st_spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index f0c3b50..b8f5beb 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,7 +1,7 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, ST_CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, ST_QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, ST_READ_DATA={scanpy=1.7.2}, ST_SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, READ_DATA={scanpy=1.7.2}, SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -11,7 +11,7 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index c2fba52..74ad1af 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -19,16 +19,16 @@ nextflow_pipeline { { assert snapshot(UTILS.removeNextflowVersion("$outputDir")).match("nf_core_pipeline_software_mqc_versions.yml") }, // Data - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_adata_processed.h5ad").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/st_sdata_processed.zarr").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/adata_processed.h5ad").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/sdata_processed.zarr").exists() }, // Reports - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_quality_controls.html").text.contains("final results of all the filtering") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_clustering.html").text.contains("spatial distribution of clusters") }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/st_spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/quality_controls.html").text.contains("final results of all the filtering") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/clustering.html").text.contains("spatial distribution of clusters") }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/st_spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 8aede30..2dfeb4c 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, ST_CLUSTERING={quarto=1.4.549, papermill=null}, ST_QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, ST_READ_DATA={scanpy=1.9.8}, ST_SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index 1e1fd5a..c90bfb8 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -4,13 +4,13 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { ST_READ_DATA } from '../modules/local/st_read_data' +include { READ_DATA } from '../modules/local/read_data' include { FASTQC } from '../modules/nf-core/fastqc/main' include { MULTIQC } from '../modules/nf-core/multiqc/main' include { paramsSummaryMap } from 'plugin/nf-validation' include { INPUT_CHECK } from '../subworkflows/local/input_check' include { SPACERANGER } from '../subworkflows/local/spaceranger' -include { ST_DOWNSTREAM } from '../subworkflows/local/st_downstream' +include { DOWNSTREAM } from '../subworkflows/local/downstream' 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_spatialtranscriptomics_pipeline' @@ -70,18 +70,18 @@ workflow SPATIALTRANSCRIPTOMICS { // // MODULE: Read ST data and save as `anndata` // - ST_READ_DATA ( + READ_DATA ( ch_downstream_input ) - ch_versions = ch_versions.mix(ST_READ_DATA.out.versions) + ch_versions = ch_versions.mix(READ_DATA.out.versions) // // SUBWORKFLOW: Downstream analyses of ST data // - ST_DOWNSTREAM ( - ST_READ_DATA.out.st_sdata_raw + DOWNSTREAM ( + READ_DATA.out.sdata_raw ) - ch_versions = ch_versions.mix(ST_DOWNSTREAM.out.versions) + ch_versions = ch_versions.mix(DOWNSTREAM.out.versions) // // Collate and save software versions From 4143983d9dfe3ea04c09f8591b0fefe94ab3964e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 19 Mar 2024 18:51:53 +0100 Subject: [PATCH 372/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 6 +++--- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 6 +++--- .../test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index eba2f1a..d7127c3 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,12 +1,12 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-03-07T13:23:56.226763" + "timestamp": "2024-03-19T18:46:59.035976" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index b8f5beb..2a097bc 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -11,12 +11,12 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.4.549, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-03-07T13:55:24.407294" + "timestamp": "2024-03-19T18:31:18.231368" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 2dfeb4c..76bba1a 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,12 +1,12 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.4.549, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", "nextflow": "23.10.1" }, - "timestamp": "2024-03-07T14:28:20.163729" + "timestamp": "2024-03-19T18:45:01.126974" } -} +} \ No newline at end of file From 354f3af8f88f612c8de45f26e471c984b9862595 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Mar 2024 09:28:12 +0100 Subject: [PATCH 373/410] Update citations --- CITATIONS.md | 10 +++++++--- assets/methods_description_template.yml | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index b6b79a1..a6d8b27 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -32,11 +32,15 @@ - [Space Ranger](https://www.10xgenomics.com/support/software/space-ranger) - > 10x Genomics Space Ranger 2.1.0 + > 10x Genomics Space Ranger 2.1.0 [Online] -- [SpatialDE](https://github.com/Teichlab/SpatialDE) +- [SpatialData](https://www.biorxiv.org/content/10.1101/2023.05.05.539647v1) - > Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: https://doi.org/10.1038/nmeth.4636 + > Marconato L, Palla G, Yamauchi K, Virshup I, Heidari E, Treis T, Toth M, Shrestha R, Vöhringer H, Huber W, Gerstung M, Moore J, Theis F, Stegle O. SpatialData: an open and universal data framework for spatial omics. bioRxiv 2023.05.05.539647; doi: https://doi.org/10.1101/2023.05.05.539647 + +- [Squipy](https://www.nature.com/articles/s41592-021-01358-2) + + > Palla G, Spitzer H, Klein M et al. Squidpy: a scalable framework for spatial omics analysis. Nat Methods 19, 171–178 (2022). doi: https://doi.org/10.1038/s41592-021-01358-2 ## Software packaging/containerisation tools diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index d8e566d..a18700e 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -3,7 +3,6 @@ description: "Suggested text and references to use when describing pipeline usag section_name: "nf-core/spatialtranscriptomics Methods Description" section_href: "https://github.com/nf-core/spatialtranscriptomics" plot_type: "html" -## TODO nf-core: Update the HTML below to your preferred methods description, e.g. add publication citation for this pipeline ## You inject any metadata in the Nextflow '${workflow}' object data: |

    Methods

    @@ -17,6 +16,14 @@ data: |
  • Ewels, P. A., Peltzer, A., Fillinger, S., Patel, H., Alneberg, J., Wilm, A., Garcia, M. U., Di Tommaso, P., & Nahnsen, S. (2020). The nf-core framework for community-curated bioinformatics pipelines. Nature Biotechnology, 38(3), 276-278. doi: 10.1038/s41587-020-0439-x
  • GrĂĽning, B., Dale, R., Sjödin, A., Chapman, B. A., Rowe, J., Tomkins-Tinch, C. H., Valieris, R., Köster, J., & Bioconda Team. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods, 15(7), 475–476. doi: 10.1038/s41592-018-0046-7
  • da Veiga Leprevost, F., GrĂĽning, B. A., Alves Aflitos, S., Röst, H. L., Uszkoreit, J., Barsnes, H., Vaudel, M., Moreno, P., Gatto, L., Weber, J., Bai, M., Jimenez, R. C., Sachsenberg, T., Pfeuffer, J., Vera Alvarez, R., Griss, J., Nesvizhskii, A. I., & Perez-Riverol, Y. (2017). BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics (Oxford, England), 33(16), 2580–2582. doi: 10.1093/bioinformatics/btx192
  • +
  • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007. doi: 10.1101/2021.12.16.473007
  • +
  • Andrews S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]: bioinformatics.babraham.ak.uk/project/fastqc
  • +
  • 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. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. doi: 10.1093/bioinformatics/btw354
  • +
  • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
  • +
  • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
  • +
  • 10x Genomics Space Ranger 2.1.0 [Online]10xgenomics.com/support/software/space-ranger
  • +
  • Marconato L, Palla G, Yamauchi K, Virshup I, Heidari E, Treis T, Toth M, Shrestha R, Vöhringer H, Huber W, Gerstung M, Moore J, Theis F, Stegle O. SpatialData: an open and universal data framework for spatial omics. bioRxiv 2023.05.05.539647; doi: 10.1101/2023.05.05.539647
  • +
  • Palla G, Spitzer H, Klein M et al. Squidpy: a scalable framework for spatial omics analysis. Nat Methods 19, 171–178 (2022). doi: 10.1038/s41592-021-01358-2
  • ${tool_bibliography}
    From 2a9fb02a3a2c26d2c95ebdd52d55f9d28af0bc58 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Wed, 20 Mar 2024 09:48:35 +0100 Subject: [PATCH 374/410] Fix duplicate citation entries --- assets/methods_description_template.yml | 16 +----------- .../main.nf | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index a18700e..c65a0c4 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -11,21 +11,7 @@ data: |
    ${workflow.commandLine}

    ${tool_citations}

    References

    -
      -
    • Di Tommaso, P., Chatzou, M., Floden, E. W., Barja, P. P., Palumbo, E., & Notredame, C. (2017). Nextflow enables reproducible computational workflows. Nature Biotechnology, 35(4), 316-319. doi: 10.1038/nbt.3820
    • -
    • Ewels, P. A., Peltzer, A., Fillinger, S., Patel, H., Alneberg, J., Wilm, A., Garcia, M. U., Di Tommaso, P., & Nahnsen, S. (2020). The nf-core framework for community-curated bioinformatics pipelines. Nature Biotechnology, 38(3), 276-278. doi: 10.1038/s41587-020-0439-x
    • -
    • GrĂĽning, B., Dale, R., Sjödin, A., Chapman, B. A., Rowe, J., Tomkins-Tinch, C. H., Valieris, R., Köster, J., & Bioconda Team. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods, 15(7), 475–476. doi: 10.1038/s41592-018-0046-7
    • -
    • da Veiga Leprevost, F., GrĂĽning, B. A., Alves Aflitos, S., Röst, H. L., Uszkoreit, J., Barsnes, H., Vaudel, M., Moreno, P., Gatto, L., Weber, J., Bai, M., Jimenez, R. C., Sachsenberg, T., Pfeuffer, J., Vera Alvarez, R., Griss, J., Nesvizhskii, A. I., & Perez-Riverol, Y. (2017). BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics (Oxford, England), 33(16), 2580–2582. doi: 10.1093/bioinformatics/btx192
    • -
    • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007. doi: 10.1101/2021.12.16.473007
    • -
    • Andrews S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]: bioinformatics.babraham.ak.uk/project/fastqc
    • -
    • 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. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. doi: 10.1093/bioinformatics/btw354
    • -
    • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
    • -
    • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
    • -
    • 10x Genomics Space Ranger 2.1.0 [Online]10xgenomics.com/support/software/space-ranger
    • -
    • Marconato L, Palla G, Yamauchi K, Virshup I, Heidari E, Treis T, Toth M, Shrestha R, Vöhringer H, Huber W, Gerstung M, Moore J, Theis F, Stegle O. SpatialData: an open and universal data framework for spatial omics. bioRxiv 2023.05.05.539647; doi: 10.1101/2023.05.05.539647
    • -
    • Palla G, Spitzer H, Klein M et al. Squidpy: a scalable framework for spatial omics analysis. Nat Methods 19, 171–178 (2022). doi: 10.1038/s41592-021-01358-2
    • - ${tool_bibliography} -
    +
      ${tool_bibliography}
    Notes:
      diff --git a/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf b/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf index 90f1e6a..c037da1 100644 --- a/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf @@ -132,8 +132,9 @@ def toolCitationText() { "MultiQC (Ewels et al. 2016),", "Quarto (Allaire et al. 2022),", "Scanpy (Wolf et al. 2018),", - "Space Ranger (10x Genomics) and", - "SpatialDE (Svensson et al. 2018)." + "Space Ranger (10x Genomics)", + "SpatialData (Marconato et al. 2023) and", + "Squidpy (Palla et al. 2022)" ].join(' ').trim() return citation_text @@ -142,13 +143,18 @@ def toolCitationText() { def toolBibliographyText() { def reference_text = [ - "
    • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: 10.1101/2021.12.16.473007
    • ", - "
    • Andrews S, (2010) FastQC, URL: bioinformatics.babraham.ac.uk.
    • ", - "
    • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: 10.1093/bioinformatics/btw354
    • ", - "
    • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
    • ", - "
    • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
    • ", - "
    • 10x Genomics Space Ranger 2.1.0, URL: 10xgenomics.com/support/software/space-ranger
    • ", - "
    • Svensson V, Teichmann S, Stegle O. SpatialDE: identification of spatially variable genes. Nat Methods 15, 343–346 (2018). doi: 10.1038/nmeth.4636
    • ", + '
    • Di Tommaso, P., Chatzou, M., Floden, E. W., Barja, P. P., Palumbo, E., & Notredame, C. (2017). Nextflow enables reproducible computational workflows. Nature Biotechnology, 35(4), 316-319. doi: 10.1038/nbt.3820
    • ', + '
    • Ewels, P. A., Peltzer, A., Fillinger, S., Patel, H., Alneberg, J., Wilm, A., Garcia, M. U., Di Tommaso, P., & Nahnsen, S. (2020). The nf-core framework for community-curated bioinformatics pipelines. Nature Biotechnology, 38(3), 276-278. doi: 10.1038/s41587-020-0439-x
    • ', + '
    • GrĂĽning, B., Dale, R., Sjödin, A., Chapman, B. A., Rowe, J., Tomkins-Tinch, C. H., Valieris, R., Köster, J., & Bioconda Team. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods, 15(7), 475–476. doi: 10.1038/s41592-018-0046-7
    • ', + '
    • da Veiga Leprevost, F., GrĂĽning, B. A., Alves Aflitos, S., Röst, H. L., Uszkoreit, J., Barsnes, H., Vaudel, M., Moreno, P., Gatto, L., Weber, J., Bai, M., Jimenez, R. C., Sachsenberg, T., Pfeuffer, J., Vera Alvarez, R., Griss, J., Nesvizhskii, A. I., & Perez-Riverol, Y. (2017). BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics (Oxford, England), 33(16), 2580–2582. doi: 10.1093/bioinformatics/btx192
    • ', + '
    • Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007. doi: 10.1101/2021.12.16.473007
    • ', + '
    • Andrews S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]: bioinformatics.babraham.ak.uk/project/fastqc
    • ', + '
    • 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. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. doi: 10.1093/bioinformatics/btw354
    • ', + '
    • Allaire J, Teague C, Scheidegger C, Xie Y, Dervieux C. Quarto (2022). doi: 10.5281/zenodo.5960048
    • ', + '
    • Wolf F, Angerer P, Theis F. SCANPY: large-scale single-cell gene expression data analysis. Genome Biol 19, 15 (2018). doi: 10.1186/s13059-017-1382-0
    • ', + '
    • 10x Genomics Space Ranger 2.1.0 [Online]: 10xgenomics.com/support/software/space-ranger
    • ', + '
    • Marconato L, Palla G, Yamauchi K, Virshup I, Heidari E, Treis T, Toth M, Shrestha R, Vöhringer H, Huber W, Gerstung M, Moore J, Theis F, Stegle O. SpatialData: an open and universal data framework for spatial omics. bioRxiv 2023.05.05.539647; doi: 10.1101/2023.05.05.539647
    • ', + '
    • Palla G, Spitzer H, Klein M et al. Squidpy: a scalable framework for spatial omics analysis. Nat Methods 19, 171–178 (2022). doi: 10.1038/s41592-021-01358-2
    • ', ].join(' ').trim() return reference_text @@ -165,8 +171,6 @@ def methodsDescriptionText(mqc_methods_yaml) { meta["nodoi_text"] = meta.manifest_map.doi ? "": "
    • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
    • " // Tool references - meta["tool_citations"] = "" - meta["tool_bibliography"] = "" meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") meta["tool_bibliography"] = toolBibliographyText() From c0c19ad7a4e14ac611ab35f3dd8f24c7c9c7ba9e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 21 Mar 2024 11:18:35 +0100 Subject: [PATCH 375/410] Add pipeline logo to Quarto report ToCs --- assets/_extensions/nf-core/_extension.yml | 6 +++++- .../nf-core/nf-core-spatialtranscriptomics_logo_light.png | 1 + assets/_extensions/nf-core/toc.html | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 120000 assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png create mode 100644 assets/_extensions/nf-core/toc.html diff --git a/assets/_extensions/nf-core/_extension.yml b/assets/_extensions/nf-core/_extension.yml index 2089122..69579df 100644 --- a/assets/_extensions/nf-core/_extension.yml +++ b/assets/_extensions/nf-core/_extension.yml @@ -11,11 +11,15 @@ contributes: highlight-style: nf-core.theme smooth-scroll: true theme: [default, nf-core.scss] - toc-location: left toc: true + toc-image: nf-core-spatialtranscriptomics_logo_light.png + toc-location: left + template-partials: + - toc.html revealjs: code-line-numbers: false embed-resources: true + logo: nf-core-spatialtranscriptomics_logo_light.png slide-level: 2 slide-number: false theme: [default, nf-core.scss] diff --git a/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png b/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png new file mode 120000 index 0000000..a64fe77 --- /dev/null +++ b/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png @@ -0,0 +1 @@ +../../nf-core-spatialtranscriptomics_logo_light.png \ No newline at end of file diff --git a/assets/_extensions/nf-core/toc.html b/assets/_extensions/nf-core/toc.html new file mode 100644 index 0000000..b402642 --- /dev/null +++ b/assets/_extensions/nf-core/toc.html @@ -0,0 +1,7 @@ + From e2c05d146088108ac29b9e24a413dfad4862582d Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 21 Mar 2024 15:32:36 +0100 Subject: [PATCH 376/410] Fix schema validation for spaceranger reference Needs to be `path` instead of `file-path` because it can be either directory or file. Before it failed for me with ``` ERROR ~ ERROR: Validation of pipeline parameters failed! -- Check '.nextflow.log' file for details The following invalid input values have been detected: * --spaceranger_reference: '//refdata-gex-GRCh38-2020-A' is not a file, but a directory (/refdata-gex-GRCh38-2020-A)` ``` --- nextflow_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 2f9a6f4..17d3244 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -59,7 +59,7 @@ }, "spaceranger_reference": { "type": "string", - "format": "file-path", + "format": "path", "description": "Location of Space Ranger reference directory. May be packed as `tar.gz` file.", "help_text": "Please see the [10x website](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) to download either of the supported human or mouse references. If not specified the GRCh38 human reference is automatically downladed and used.", "fa_icon": "fas fa-folder-open", From dbca699946f2acec2228962f6a01d6b76ef0d2aa Mon Sep 17 00:00:00 2001 From: Gregor Sturm Date: Thu, 21 Mar 2024 15:35:04 +0100 Subject: [PATCH 377/410] Update nextflow_schema.json --- nextflow_schema.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 17d3244..aa701fb 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -55,7 +55,8 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Location of Space Ranger probeset file.", - "fa_icon": "fas fa-file-csv" + "fa_icon": "fas fa-file-csv", + "exists": true }, "spaceranger_reference": { "type": "string", @@ -63,7 +64,8 @@ "description": "Location of Space Ranger reference directory. May be packed as `tar.gz` file.", "help_text": "Please see the [10x website](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest) to download either of the supported human or mouse references. If not specified the GRCh38 human reference is automatically downladed and used.", "fa_icon": "fas fa-folder-open", - "default": "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz" + "default": "https://cf.10xgenomics.com/supp/spatial-exp/refdata-gex-GRCh38-2020-A.tar.gz", + "exists": true } } }, From b275bc7620afe0c183a54a4c2abb8fc4d3c92668 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Apr 2024 14:27:10 +0200 Subject: [PATCH 378/410] Downgrade Quarto to 1.3 due to figure formatting Downgrade the Quarto version to 1.3 due to some odd interaction between the Python figures and the `layout-ncol` Quarto chunk option in 1.4, which made the figures come out horizontally squashed. --- env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/Dockerfile b/env/Dockerfile index 43f18cb..ad1c06f 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -2,7 +2,7 @@ # First stage: Quarto installation # FROM ubuntu:20.04 as quarto -ARG QUARTO_VERSION=1.4.549 +ARG QUARTO_VERSION=1.3.450 ARG TARGETARCH RUN apt-get update \ && apt-get install -y --no-install-recommends \ From d994410218d45aff12caeae7ddaa33513575e506 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 2 Apr 2024 14:37:51 +0200 Subject: [PATCH 379/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 4 ++-- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index d7127c3..824113d 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.4.549, papermill=null}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-19T18:46:59.035976" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 2a097bc..063d565 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -11,7 +11,7 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.4.549, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -19,4 +19,4 @@ }, "timestamp": "2024-03-19T18:31:18.231368" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 76bba1a..032f4ef 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.4.549, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.4.549, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.4.549, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-19T18:45:01.126974" } -} \ No newline at end of file +} From 5e64cd24db45fbab6eb732abbc3a6dd01d9bae7e Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Tue, 2 Apr 2024 15:14:36 +0200 Subject: [PATCH 380/410] Include SpatialData description in all outputs --- CITATIONS.md | 6 +++++- bin/clustering.qmd | 4 ++-- bin/quality_controls.qmd | 2 +- modules/local/read_data.nf | 3 ++- workflows/spatialtranscriptomics.nf | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index a6d8b27..beb3be8 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,10 +10,14 @@ ## Pipeline tools -- [AnnData](https://github.com/theislab/anndata) +- [AnnData](https://github.com/scverse/anndata) > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: https://doi.org/10.1101/2021.12.16.473007 +- [SpatialData](https://github.com/scverse/spatialdata) + + > Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and universal data framework for spatial omics. Nat Methods (2024); https://doi.org/10.1038/s41592-024-02212-x + - [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) > Andrews, S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. diff --git a/bin/clustering.qmd b/bin/clustering.qmd index e19cc6f..cecc9da 100644 --- a/bin/clustering.qmd +++ b/bin/clustering.qmd @@ -20,7 +20,7 @@ output_sdata = "sdata_processed.zarr" # Output: SpatialData file ``` The data has already been filtered in the _quality controls_ reports and is -saved in the AnnData format: +saved in the SpatialData format: ```{python} #| warning: false @@ -65,7 +65,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) adata = to_legacy_anndata(sdata) -print("Content of the AnnData object:") +print("Content of the SpatialData table object:") print(adata) ``` diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index 7c94f78..fd09221 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -21,7 +21,7 @@ and the [`scanpy` Python package](https://scanpy.readthedocs.io/en/stable/). The anndata format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and additional metadata. The anndata format ensures compatibility with various -analysis tools and facilitates seamless integration into existing workflows. +analysis tools and facilitates seamless integration into existing workflows. The AnnData object is saved in a zarr [SpatialData object](https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions). ```{python} #| tags: [parameters] diff --git a/modules/local/read_data.nf b/modules/local/read_data.nf index 3e15428..7778ded 100644 --- a/modules/local/read_data.nf +++ b/modules/local/read_data.nf @@ -1,11 +1,12 @@ // -// Read ST 10x visium and SC 10x data with Scanpy and save to `anndata` file +// Read ST 10x visium and SC 10x data with spatialdata_io and save to `SpatialData` file // process READ_DATA { tag "${meta.id}" label 'process_low' + // TODO fix conda environment to include spatialdata_io instead of scanpy conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" container "docker.io/erikfas/spatialtranscriptomics" diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialtranscriptomics.nf index c90bfb8..3b2fb75 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialtranscriptomics.nf @@ -68,7 +68,7 @@ workflow SPATIALTRANSCRIPTOMICS { } // - // MODULE: Read ST data and save as `anndata` + // MODULE: Read ST data and save as `SpatialData` // READ_DATA ( ch_downstream_input From 668d712cc80f2a71d0d0efb46ca0855024b3074c Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Tue, 2 Apr 2024 16:21:14 +0200 Subject: [PATCH 381/410] Clean Spatially Variable Genes quarto markdown --- bin/spatially_variable_genes.qmd | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index c9421b9..35dd79c 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -69,7 +69,7 @@ sc.settings.verbosity = 0 # Differential gene expression Before we look for spatially variable genes we first find differentially -expressed genes across the different clusters found in the data. We can +expressed genes (DEG) across the different clusters found in the data. We can visualize the top DEGs in a heatmap: ```{python} @@ -98,6 +98,7 @@ We can perform a neighborhood enrichment analysis to find out which genes are enriched in the neighborhood of each cluster: ```{python} +#| warning: false sq.gr.spatial_neighbors(adata, coord_type="generic") sq.gr.nhood_enrichment(adata, cluster_key="clusters") sq.pl.nhood_enrichment(adata, cluster_key="clusters", method="ward", vmin=-100, vmax=100) @@ -106,8 +107,9 @@ sq.pl.nhood_enrichment(adata, cluster_key="clusters", method="ward", vmin=-100, We visualize the interaction matrix between the different clusters: ```{python} +#| warning: false sq.gr.interaction_matrix(adata, cluster_key="clusters") -sq.pl.interaction_matrix(adata, cluster_key="clusters", method="ward", vmax=20000) +sq.pl.interaction_matrix(adata, cluster_key="clusters", method="ward") ``` # Spatially variable genes with spatial autocorrelation statistics @@ -123,15 +125,17 @@ adata.uns["moranI"].head(n_top_spatial_degs) #[TODO] add gearyC as optional mode ``` +```{python} +#| echo: false +# Save the spatially variable genes to a CSV file: +adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) +``` ```{python} #| echo: false +#| info: false adata.write(output_adata) del sdata.table sdata.table = adata sdata.write("./" + output_sdata) ``` - -```{python} -adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) -``` From e103fc4ec4b1fac7e6eb73b2381bee9e2c58f757 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 08:38:10 +0200 Subject: [PATCH 382/410] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae362f..066d74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ compatible with further downstream analyses and/or exploration in _e.g._ - Add MultiQC support for Space Ranger outputs [[#70](https://github.com/nf-core/spatialtranscriptomics/pull/70)] - Use the QUARTONOTEBOOK nf-core module instead of local Quarto-based modules [[#68](https://github.com/nf-core/spatialtranscriptomics/pull/68)] +- Add support for SpatialData [[$67](https://github.com/nf-core/spatialtranscriptomics/pull/67)] - Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialtranscriptomics/pull/64)] - Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) - Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] From 4f12ac8bc8023c7bddb5bbe191349515002ea51d Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 08:39:38 +0200 Subject: [PATCH 383/410] Remove duplicate citation for SpatialData --- CITATIONS.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index beb3be8..d82d84d 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -14,10 +14,6 @@ > Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA. bioRxiv 2021.12.16.473007; doi: https://doi.org/10.1101/2021.12.16.473007 -- [SpatialData](https://github.com/scverse/spatialdata) - - > Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and universal data framework for spatial omics. Nat Methods (2024); https://doi.org/10.1038/s41592-024-02212-x - - [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) > Andrews, S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. From f82267d08a17eb7eb11176e17855d863d47baa3a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 09:21:04 +0200 Subject: [PATCH 384/410] Minor formatting --- bin/quality_controls.qmd | 2 +- modules/local/read_data.nf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index fd09221..cb2df55 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -81,7 +81,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) adata = to_legacy_anndata(sdata) -# Convert X matrix from csr to csc dense matrix for output compatibility: +# Convert X matrix from CSR to CSC dense matrix for output compatibility adata.X = scipy.sparse.csc_matrix(adata.X) # Store the raw data so that it can be used for analyses from scratch if desired diff --git a/modules/local/read_data.nf b/modules/local/read_data.nf index 7778ded..dfc7563 100644 --- a/modules/local/read_data.nf +++ b/modules/local/read_data.nf @@ -15,7 +15,7 @@ process READ_DATA { output: tuple val(meta), path("sdata_raw.zarr"), emit: sdata_raw - path("versions.yml") , emit: versions + path("versions.yml") , emit: versions when: task.ext.when == null || task.ext.when From 8ae8c266e05b66c736b6d0641acda280d283a48f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 09:34:44 +0200 Subject: [PATCH 385/410] Update quartonotebook module --- modules.json | 2 +- modules/nf-core/quartonotebook/Dockerfile | 2 +- modules/nf-core/quartonotebook/quartonotebook.diff | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules.json b/modules.json index 243013f..86c77ee 100644 --- a/modules.json +++ b/modules.json @@ -17,7 +17,7 @@ }, "quartonotebook": { "branch": "master", - "git_sha": "07ecae35e5675ac4c1e2d84cf22021490f8b7947", + "git_sha": "93b7e1bf63944488fe77ad490a9de62a73959bed", "installed_by": ["modules"], "patch": "modules/nf-core/quartonotebook/quartonotebook.diff" }, diff --git a/modules/nf-core/quartonotebook/Dockerfile b/modules/nf-core/quartonotebook/Dockerfile index 0acc6f0..78d2ab2 100644 --- a/modules/nf-core/quartonotebook/Dockerfile +++ b/modules/nf-core/quartonotebook/Dockerfile @@ -18,7 +18,7 @@ RUN mkdir -p /opt/quarto \ # # Second stage: Conda environment # -FROM condaforge/mambaforge:23.11.0-0 +FROM condaforge/mambaforge:24.1.2-0@sha256:64c45c1a743737f61cf201f54cae974b5c853be94f9c1a84f5e82e0e854f0407 COPY --from=quarto /opt/quarto /opt/quarto ENV PATH="${PATH}:/opt/quarto/bin" diff --git a/modules/nf-core/quartonotebook/quartonotebook.diff b/modules/nf-core/quartonotebook/quartonotebook.diff index f2f640e..018e646 100644 --- a/modules/nf-core/quartonotebook/quartonotebook.diff +++ b/modules/nf-core/quartonotebook/quartonotebook.diff @@ -4,7 +4,7 @@ Changes in module 'nf-core/quartonotebook' @@ -4,11 +4,7 @@ tag "$meta.id" label 'process_low' - + - // NB: You'll likely want to override this with a container containing all - // required dependencies for your analyses. You'll at least need Quarto - // itself, Papermill and whatever language you are running your analyses on; From d4861bd5dacda27029b5b45ca93cae9a3f912c34 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 09:30:57 +0200 Subject: [PATCH 386/410] Update spaceranger/count module --- conf/modules.config | 1 + modules.json | 2 +- modules/nf-core/spaceranger/count/main.nf | 2 +- .../spaceranger/count/tests/main.nf.test | 4 +- .../spaceranger/count/tests/main.nf.test.snap | 80 ++++++++++++++----- .../spaceranger/count/tests/nextflow.config | 5 ++ tests/pipeline/test_downstream.nf.test.snap | 2 +- .../test_spaceranger_ffpe_v1.nf.test.snap | 8 +- ...spaceranger_ffpe_v2_cytassist.nf.test.snap | 8 +- 9 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 modules/nf-core/spaceranger/count/tests/nextflow.config diff --git a/conf/modules.config b/conf/modules.config index e8e3e63..40b2dbc 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -55,6 +55,7 @@ process { } withName: SPACERANGER_COUNT { + ext.args = '--create-bam false' publishDir = [ path: { "${params.outdir}/${meta.id}/spaceranger" }, mode: params.publish_dir_mode, diff --git a/modules.json b/modules.json index 86c77ee..e071f8c 100644 --- a/modules.json +++ b/modules.json @@ -23,7 +23,7 @@ }, "spaceranger/count": { "branch": "master", - "git_sha": "3bd057bfdfb64578636ff3ae7f7cb8eeab3c0cb6", + "git_sha": "2f0ef0cd414ea43e33625023c72b6af936dce63d", "installed_by": ["modules"] }, "untar": { diff --git a/modules/nf-core/spaceranger/count/main.nf b/modules/nf-core/spaceranger/count/main.nf index cac83e0..4f766cb 100644 --- a/modules/nf-core/spaceranger/count/main.nf +++ b/modules/nf-core/spaceranger/count/main.nf @@ -2,7 +2,7 @@ process SPACERANGER_COUNT { tag "$meta.id" label 'process_high' - container "docker.io/nfcore/spaceranger:2.1.0" + container "nf-core/spaceranger:3.0.0" input: tuple val(meta), path(reads), path(image), path(cytaimage), path(darkimage), path(colorizedimage), path(alignment), path(slidefile) diff --git a/modules/nf-core/spaceranger/count/tests/main.nf.test b/modules/nf-core/spaceranger/count/tests/main.nf.test index b751b07..7631d85 100644 --- a/modules/nf-core/spaceranger/count/tests/main.nf.test +++ b/modules/nf-core/spaceranger/count/tests/main.nf.test @@ -2,6 +2,7 @@ nextflow_process { name "Test Process SPACERANGER_COUNT" script "../main.nf" + config "./nextflow.config" process "SPACERANGER_COUNT" tag "modules" @@ -210,7 +211,8 @@ nextflow_process { 'molecule_info.h5', 'barcodes.tsv.gz', 'features.tsv.gz', - 'matrix.mtx.gz' + 'matrix.mtx.gz', + 'cloupe.cloupe' ]} ).match() }, diff --git a/modules/nf-core/spaceranger/count/tests/main.nf.test.snap b/modules/nf-core/spaceranger/count/tests/main.nf.test.snap index ece665f..c13496e 100644 --- a/modules/nf-core/spaceranger/count/tests/main.nf.test.snap +++ b/modules/nf-core/spaceranger/count/tests/main.nf.test.snap @@ -2,53 +2,89 @@ "spaceranger v1 (stub) - homo_sapiens - fasta - gtf - fastq - tif - csv": { "content": [ [ - "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + "versions.yml:md5,1539e8a9a3d63ce3653920721d1af509" ] ], - "timestamp": "2024-01-09T15:09:24.723008" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-04-02T09:29:02.205153668" }, "spaceranger v2 - homo_sapiens - fasta - gtf - fastq - tif - csv": { "content": [ [ - "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + "versions.yml:md5,1539e8a9a3d63ce3653920721d1af509" ], [ - "filtered_feature_bc_matrix.h5:md5,509e18ed6b218850e5095124ecc771c1", - "metrics_summary.csv:md5,412caff0fcd9f39cb54671147058de2f", - "possorted_genome_bam.bam:md5,23cd192fcc217d835b8c0afee0619f40", - "possorted_genome_bam.bam.bai:md5,baf623d3e554ba5008304f32414c9fb2", + "clusters.csv:md5,2cc2d0c94ec0af69f03db235f9ea6932", + "clusters.csv:md5,46c12f3845e28f27f2cd580cb004c0ea", + "clusters.csv:md5,4e5f082240b9c9903168842d1f9dbe34", + "clusters.csv:md5,e626eb7049baf591ea49f5d8c305621c", + "clusters.csv:md5,65cfb24fc937e4df903a742c1adf8b08", + "clusters.csv:md5,819a71787618945dacfa2d5301b953b1", + "clusters.csv:md5,5ae17ed02cdb9f61d7ceb0cd6922c9d4", + "clusters.csv:md5,641550bec22e02fff3611087f7fd6e07", + "clusters.csv:md5,9fbe5c79035175bc1899e9a7fc80f7ac", + "clusters.csv:md5,ed0c2dcca15c14a9983407ff9af0daaf", + "differential_expression.csv:md5,d37a8ef21699372ec4a4bdf0c43d71b7", + "differential_expression.csv:md5,ac3181524385c88d38a0fc17d3bdd526", + "differential_expression.csv:md5,557d6dfec7421c392aa6443725608cd1", + "differential_expression.csv:md5,1437fad68d701c97a4a46318aee45575", + "differential_expression.csv:md5,7a2f3d0e90782055580f4903617a7d27", + "differential_expression.csv:md5,41756e9570d07aee6aed710e6a965846", + "differential_expression.csv:md5,62ea7651c3f195d3c960c6c688dca477", + "differential_expression.csv:md5,b630542266c4abb71f4205922340498d", + "differential_expression.csv:md5,0deb97f0be7e72ad73e456092db31e6d", + "differential_expression.csv:md5,3bba8490f753507e7e2e29be759f218b", + "components.csv:md5,568bb9bcb6ee913356fcb4be3fea1911", + "dispersion.csv:md5,e2037b1db404f6e5d8b3144629f2500d", + "features_selected.csv:md5,3ba6d1315ae594963b306d94ba1180e7", + "projection.csv:md5,aef5d71381678d5245e471f3d5a8ab67", + "variance.csv:md5,475a95e51ce66e639ae21d801c455e2b", + "projection.csv:md5,928c0f68a9c773fba590941d3d5af7ca", + "projection.csv:md5,216dcc5589a083fcc27d981aa90fa2ab", + "filtered_feature_bc_matrix.h5:md5,f1a8f225c113974b47efffe08e70f367", + "metrics_summary.csv:md5,faa17487b479eab361050d3266da2efb", "probe_set.csv:md5,5bfb8f12319be1b2b6c14142537c3804", - "raw_feature_bc_matrix.h5:md5,2263d2c756785f86dc28f6b76fd61b73", + "raw_feature_bc_matrix.h5:md5,6e40ae93a116c6fc0adbe707b0eb415f", "raw_probe_bc_matrix.h5:md5,3d5e711d0891ca2caaf301a2c1fbda91", "aligned_fiducials.jpg:md5,51dcc3a32d3d5ca4704f664c8ede81ef", "cytassist_image.tiff:md5,0fb04a55e5658f4d158d986a334b034d", - "detected_tissue_image.jpg:md5,64d9adb4844ab91506131476d93b28dc", - "tissue_hires_image.png:md5,1c0f1e94522a886c19f56a629227e097", + "detected_tissue_image.jpg:md5,1d3ccc1e12c4fee091b006e48b9cc16a", + "spatial_enrichment.csv:md5,1117792553e82feb2b4b3934907a0136", + "tissue_hires_image.png:md5,834706fff299024fab48e6366afc9cb9", "tissue_lowres_image.png:md5,8c1fcb378f7f886301f49ffc4f84360a", - "tissue_positions.csv:md5,1b2df34f9e762e9e64aa226226b96c4b" + "tissue_positions.csv:md5,425601ef21661ec0126000f905ef044f" ] ], - "timestamp": "2024-01-11T17:49:27.776247" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-04-02T10:13:00.787792273" }, "spaceranger v1 - homo_sapiens - fasta - gtf - fastq - tif - csv": { "content": [ [ - "versions.yml:md5,038e17e049a72dd3d417d0e221dce732" + "versions.yml:md5,1539e8a9a3d63ce3653920721d1af509" ], [ - "filtered_feature_bc_matrix.h5:md5,f444a4816bf40d377271a6157b320556", - "metrics_summary.csv:md5,5e36f2f9b6987791e0b5eb2736d25115", - "molecule_info.h5:md5,b3d14dfbfc167bb8fc9b158f083efdb6", - "possorted_genome_bam.bam:md5,6ed7f3bb2f17322113f940989a3771ff", - "possorted_genome_bam.bam.bai:md5,08ce9ffd30d9b82091932b744873610b", - "raw_feature_bc_matrix.h5:md5,7e937b4863a98b0d3784f4e21c07c326", + "filtered_feature_bc_matrix.h5:md5,7e09d1cd2e1f497a698c5efde9e4af84", + "metrics_summary.csv:md5,07a6fcc2e20f854f8d3fcde2457a2f9a", + "molecule_info.h5:md5,1f2e0fd31d15509e7916e84f22632c9c", + "raw_feature_bc_matrix.h5:md5,5a4184a3bfaf722eec8d1a763a45906e", "aligned_fiducials.jpg:md5,f6217ddd707bb189e665f56b130c3da8", - "detected_tissue_image.jpg:md5,4a26b91db5aca179d627b86f352006e2", + "detected_tissue_image.jpg:md5,c1c7e8741701a576c1ec103c1aaf98ea", "tissue_hires_image.png:md5,d91f8f176ae35ab824ede87117ac0889", "tissue_lowres_image.png:md5,475a04208d193191c84d7a3b5d4eb287", - "tissue_positions.csv:md5,37d288d0e29e8572ea4c5bef292de4b6" + "tissue_positions.csv:md5,748bf590c445db409d7dbdf5a08e72e8" ] ], - "timestamp": "2024-01-11T20:34:18.669838" + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-04-02T09:37:13.128424153" } } \ No newline at end of file diff --git a/modules/nf-core/spaceranger/count/tests/nextflow.config b/modules/nf-core/spaceranger/count/tests/nextflow.config new file mode 100644 index 0000000..fe9d61a --- /dev/null +++ b/modules/nf-core/spaceranger/count/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: SPACERANGER_COUNT { + ext.args = '--create-bam false' + } +} diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index 824113d..c848af7 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-19T18:46:59.035976" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 063d565..ee3d586 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -11,12 +11,12 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nextflow": "24.02.0" }, - "timestamp": "2024-03-19T18:31:18.231368" + "timestamp": "2024-04-04T10:08:16.975105" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 032f4ef..92e16a3 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,12 +1,12 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nextflow": "24.02.0" }, - "timestamp": "2024-03-19T18:45:01.126974" + "timestamp": "2024-04-04T10:42:54.76102" } -} +} \ No newline at end of file From f253f562f433ccd7da634a106c661a973b3562ff Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 4 Apr 2024 21:16:34 +0200 Subject: [PATCH 387/410] Remove TODO --- bin/quality_controls.qmd | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index cb2df55..995558f 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -8,7 +8,6 @@ jupyter: python3 # Introduction - Spatial Transcriptomics data analysis involves several steps, including quality controls (QC) and pre-processing, to ensure the reliability of downstream analyses. This is an essential step in spatial transcriptomics to identify and From 51d5918f4b43e341739aad96ff09d05a8f32304f Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 12:37:19 +0200 Subject: [PATCH 388/410] Update SpatialData to v0.1.2 --- bin/clustering.qmd | 10 +++++----- bin/quality_controls.qmd | 12 +++++------- bin/spatially_variable_genes.qmd | 9 ++++----- env/environment.yml | 8 ++++---- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/bin/clustering.qmd b/bin/clustering.qmd index cecc9da..7266cab 100644 --- a/bin/clustering.qmd +++ b/bin/clustering.qmd @@ -39,11 +39,11 @@ from IPython.display import display, Markdown ```{python} # Make sure we can use scanpy plots with the AnnData object exported from -# `sdata.table`. This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ +# `sdata.tables`. This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ # Once that PR is merged into spatialdata-io, we should instead use # `spatialdata_io.to_legacy_anndata(sdata)`. def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: - adata = sdata.table + adata = sdata.tables["table"] for dataset_id in adata.uns["spatial"]: adata.uns["spatial"][dataset_id]["images"] = { "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), @@ -62,7 +62,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ``` ```{python} -sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) +sdata = spatialdata.read_zarr(input_sdata, ["images", "tables", "shapes"]) adata = to_legacy_anndata(sdata) print("Content of the SpatialData table object:") @@ -172,8 +172,8 @@ sc.pl.spatial(adata, img_key="hires", color="clusters", size=1.25) ```{python} #| echo: false -del sdata.table -sdata.table = adata +del sdata.tables["table"] +sdata.tables["table"] = adata adata.write(os.path.join(artifact_dir, output_adata)) sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index 995558f..d552d79 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -1,8 +1,6 @@ --- title: "nf-core/spatialtranscriptomics" subtitle: "Pre-processing and quality controls" -format: - nf-core-html: default jupyter: python3 --- @@ -53,11 +51,11 @@ plt.rcParams["figure.figsize"] = (6, 6) ``` ```{python} -# Make sure we can use scanpy plots with the AnnData object exported from sdata.table +# Make sure we can use scanpy plots with the AnnData object exported from sdata.tables # This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ # Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: - adata = sdata.table + adata = sdata.tables["table"] for dataset_id in adata.uns["spatial"]: adata.uns["spatial"][dataset_id]["images"] = { "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), @@ -77,8 +75,8 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read the data -sdata = spatialdata.read_zarr(input_sdata, ["images", "table", "shapes"]) adata = to_legacy_anndata(sdata) +sdata = spatialdata.read_zarr(input_sdata, ["images", "tables", "shapes"]) # Convert X matrix from CSR to CSC dense matrix for output compatibility adata.X = scipy.sparse.csc_matrix(adata.X) @@ -282,7 +280,7 @@ sc.pl.violin(adata, ['pct_counts_mt', 'pct_counts_ribo', 'pct_counts_hb'], ``` ```{python} -del sdata.table -sdata.table = adata +del sdata.tables["table"] +sdata.tables["table"] = adata sdata.write(os.path.join(artifact_dir, output_sdata)) ``` diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index 35dd79c..c25bbdb 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -29,11 +29,11 @@ from matplotlib import pyplot as plt ``` ```{python} -# Make sure we can use scanpy plots with the AnnData object exported from sdata.table +# Make sure we can use scanpy plots with the AnnData object exported from sdata.tables # This code is taken from the early version of https://github.com/scverse/spatialdata-io/pull/102/ # Once the PR will be merged in spatialdata-io, we should use spatialdata_io.to_legacy_anndata(sdata). def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: - adata = sdata.table + adata = sdata.tables["table"] for dataset_id in adata.uns["spatial"]: adata.uns["spatial"][dataset_id]["images"] = { "hires": np.array(sdata.images[f"{dataset_id}_hires_image"]).transpose([1, 2, 0]), @@ -53,7 +53,6 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read data -sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "table", "shapes"]) adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") @@ -135,7 +134,7 @@ adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) #| echo: false #| info: false adata.write(output_adata) -del sdata.table -sdata.table = adata +del sdata.tables["table"] +sdata.tables["table"] = adata sdata.write("./" + output_sdata) ``` diff --git a/env/environment.yml b/env/environment.yml index 386e5ce..c376fb2 100644 --- a/env/environment.yml +++ b/env/environment.yml @@ -12,8 +12,8 @@ dependencies: - gxx=13.2.0 - imagecodecs=2024.1.1 - pip: - - scanpy==1.9.8 + - scanpy==1.10.0 - squidpy==1.4.1 - - spatialdata==0.0.15 - - spatialdata-io==0.0.9 - - spatialdata-plot==0.1.0 + - spatialdata==0.1.2 + - spatialdata-io==0.1.2 + - spatialdata-plot==0.2.1 From a9a55705eca90389ff3137b522c050746c5563f5 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 12:37:53 +0200 Subject: [PATCH 389/410] Remove temporary fix for scanpy issue --- bin/spatially_variable_genes.qmd | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index c25bbdb..aea5b35 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -58,9 +58,6 @@ adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") print(adata) -# Fix for scanpy issue https://github.com/scverse/scanpy/issues/2181 -adata.uns['log1p']['base'] = None - # Suppress scanpy-specific warnings sc.settings.verbosity = 0 ``` From b1397559b67d15f2efad1c30537ec4c44ef3174e Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 12:38:29 +0200 Subject: [PATCH 390/410] Add back missing line in SVG quarto --- bin/spatially_variable_genes.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index aea5b35..4ad198c 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -53,6 +53,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read data +sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "tables", "shapes"]) adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") From d2a490b8fdf6441bcca9e7b7a02ce1cfe153bfc1 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 12:39:16 +0200 Subject: [PATCH 391/410] Fix spelling of AnnData --- bin/quality_controls.qmd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index d552d79..744bd0a 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -15,10 +15,11 @@ analysis. This report outlines the QC and pre-processing steps for Visium Spatial Transcriptomics data using the [AnnData format](https://anndata.readthedocs.io/en/latest/tutorials/notebooks/getting-started.html) and the [`scanpy` Python package](https://scanpy.readthedocs.io/en/stable/). -The anndata format is utilized to organize and store the Spatial Transcriptomics +The AnnData format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and -additional metadata. The anndata format ensures compatibility with various -analysis tools and facilitates seamless integration into existing workflows. The AnnData object is saved in a zarr [SpatialData object](https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions). +additional metadata. The AnnData format ensures compatibility with various +analysis tools and facilitates seamless integration into existing workflows. +The AnnData object is saved in the `Tables` element of a zarr [SpatialData object](https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions). ```{python} #| tags: [parameters] From 184dec84387f370d51da93e23f8d291928d13d1a Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 13:00:04 +0200 Subject: [PATCH 392/410] Clean read_zarr local path --- bin/spatially_variable_genes.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index 4ad198c..a3be687 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -53,7 +53,7 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read data -sdata = spatialdata.read_zarr("./" + input_sdata, ["images", "tables", "shapes"]) +sdata = spatialdata.read_zarr(input_sdata, ["images", "tables", "shapes"]) adata = to_legacy_anndata(sdata) print("Content of the AnnData object:") From c364e3eb281b5b04945e7b6865c94a0edf41e4ad Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 13:30:15 +0200 Subject: [PATCH 393/410] Fix qmd headers --- bin/clustering.qmd | 2 -- bin/quality_controls.qmd | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/clustering.qmd b/bin/clustering.qmd index 7266cab..3985af8 100644 --- a/bin/clustering.qmd +++ b/bin/clustering.qmd @@ -3,8 +3,6 @@ title: "nf-core/spatialtranscriptomics" subtitle: "Dimensionality reduction and clustering" format: nf-core-html: default -execute: - keep-ipynb: true jupyter: python3 --- diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index 744bd0a..d664ecf 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -1,6 +1,8 @@ --- title: "nf-core/spatialtranscriptomics" subtitle: "Pre-processing and quality controls" +format: + nf-core-html: default jupyter: python3 --- From 6754c6f26fe9de5c7095f638ed90521a2827150b Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 13:29:36 +0200 Subject: [PATCH 394/410] Move `degs/` output to `data/` folder --- bin/spatially_variable_genes.qmd | 4 +- conf/modules.config | 2 +- docs/output.md | 37 +++++++++++++------ nextflow.config | 2 +- nextflow_schema.json | 2 +- subworkflows/local/downstream.nf | 2 +- tests/pipeline/test_downstream.nf.test | 6 +-- .../pipeline/test_spaceranger_ffpe_v1.nf.test | 4 +- ...test_spaceranger_ffpe_v2_cytassist.nf.test | 4 +- 9 files changed, 39 insertions(+), 24 deletions(-) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index 4ad198c..2a30a99 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -10,7 +10,7 @@ jupyter: python3 #| tags: [parameters] #| echo: false input_sdata = "sdata_processed.zarr" # Input: SpatialData file -n_top_spatial_degs = 14 # Number of spatially variable genes to plot +n_top_svgs = 14 # Number of spatially variable genes to plot artifact_dir = "artifacts" # Output directory output_csv = "spatially_variable_genes.csv" # Output: gene list output_adata = "adata_spatially_variable_genes.h5ad" # Output: AnnData file @@ -118,7 +118,7 @@ patterns. Here we use [Moran's I](https://en.wikipedia.org/wiki/Moran%27s_I) aut ```{python} adata.var_names_make_unique() sq.gr.spatial_autocorr(adata, mode="moran") -adata.uns["moranI"].head(n_top_spatial_degs) +adata.uns["moranI"].head(n_top_svgs) #[TODO] add gearyC as optional mode ``` diff --git a/conf/modules.config b/conf/modules.config index 40b2dbc..274e825 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -90,7 +90,7 @@ process { saveAs: { "adata_processed.h5ad" } ], [ - path: { "${params.outdir}/${meta.id}/degs" }, + path: { "${params.outdir}/${meta.id}/data" }, mode: params.publish_dir_mode, pattern: "artifacts/spatially_variable_genes.csv", saveAs: { "spatially_variable_genes.csv" } diff --git a/docs/output.md b/docs/output.md index d5ea2f6..3d6d220 100644 --- a/docs/output.md +++ b/docs/output.md @@ -53,25 +53,38 @@ information about these files at the [10X website](https://support.10xgenomics.c Output files - `/data/` - - `adata_processed.h5ad`: Filtered, normalised and clustered adata. + - `sdata_processed.zarr`: Processed data in SpatialData format. + - `adata_processed.h5ad`: Processed data in AnnData format. + - `spatially_variable_genes.csv`: List of spatially variable genes. -Data in `.h5ad` format as processed by the pipeline, which can be used for -further downstream analyses if desired; unprocessed data is also present in this -file. It can also be used by the [TissUUmaps](https://tissuumaps.github.io/) +Data in `.zarr` and `.h5ad` formats as processed by the pipeline, which can be +used for further downstream analyses if desired; unprocessed data is also +present in these files. It can also be used by the [TissUUmaps](https://tissuumaps.github.io/) browser-based tool for visualisation and exploration, allowing you to delve into -the data in an interactive way. +the data in an interactive way. The list of spatially variable genes are added +as a convenience if you want to explore them in _e.g._ Excel. ## Reports +
      +Output files + +- `/reports/` + - `_extensions/`: Quarto nf-core extension, common to all reports. + +
      + ### Quality controls and filtering
      Output files - `/reports/` - - `quality_controls.html`: HTML report. + - `quality_controls.html`: Rendered HTML report. + - `quality_controls.yml`: YAML file containing parameters used in the report. + - `quality_controls.qmd`: Quarto document used for rendering the report.
      @@ -85,7 +98,9 @@ well as presence in tissue; you can find more details in the report itself. Output files - `/reports/` - - `clustering.html`: HTML report. + - `clustering.html`: Rendered HTML report. + - `clustering.yml`: YAML file containing parameters used in the report. + - `clustering.qmd`: Quarto document used for rendering the report. @@ -93,15 +108,15 @@ Report containing analyses related to normalisation, dimensionality reduction, clustering and spatial visualisation. Leiden clustering is currently the only option; you can find more details in the report itself. -### Differential expression +### Spatially variable genes
      Output files - `/reports/` - - `spatially_variable_genes.html`: HTML report. -- `/degs/` - - `spatially_variable_genes.csv`: List of spatially variable genes. + - `spatially_variable_genes.html`: Rendered HTML report. + - `spatially_variable_genes.yml`: YAML file containing parameters used in the report. + - `spatially_variable_genes.qmd`: Quarto document used for rendering the report.
      diff --git a/nextflow.config b/nextflow.config index 70fa1c0..c622d42 100644 --- a/nextflow.config +++ b/nextflow.config @@ -30,7 +30,7 @@ params { cluster_resolution = 1.0 // Spatial differential expression - n_top_spatial_degs = 14 + n_top_svgs = 14 // MultiQC options multiqc_config = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 64b7551..ad63b5b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -146,7 +146,7 @@ "help_text": "The resolution controls the coarseness of the clustering, where a higher resolution leads to more clusters.", "fa_icon": "fas fa-circle-nodes" }, - "n_top_spatial_degs": { + "n_top_svgs": { "type": "integer", "default": 14, "description": "The number of top spatial differentially expressed genes to plot.", diff --git a/subworkflows/local/downstream.nf b/subworkflows/local/downstream.nf index decbcbe..77269ee 100644 --- a/subworkflows/local/downstream.nf +++ b/subworkflows/local/downstream.nf @@ -82,7 +82,7 @@ workflow DOWNSTREAM { .map { tuple(it[0], spatially_variable_genes_notebook) } spatially_variable_genes_params = [ input_sdata: "sdata_processed.zarr", - n_top_spatial_degs: params.n_top_spatial_degs, + n_top_svgs: params.n_top_svgs, artifact_dir: "artifacts", output_csv: "spatially_variable_genes.csv", output_adata: "adata_spatially_variable_genes.h5ad", diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 404cc1f..6233756 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -39,9 +39,9 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, - // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/spatially_variable_genes.csv").exists() }, - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/degs/spatially_variable_genes.csv").exists() }, + // Spatially variable genes + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/spatially_variable_genes.csv").exists() }, + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2.2/data/spatially_variable_genes.csv").exists() }, // MultiQC { assert file("$outputDir/multiqc/multiqc_report.html").exists() } diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 1aa7bd5..3ee63d2 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -31,8 +31,8 @@ nextflow_pipeline { { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, - // DEGs - { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/degs/spatially_variable_genes.csv").exists() }, + // Spatially variable genes + { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/data/spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/Visium_FFPE_Human_Ovarian_Cancer/spaceranger/outs/web_summary.html").exists() }, diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test index 74ad1af..124ebe3 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test @@ -27,8 +27,8 @@ nextflow_pipeline { { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/clustering.html").text.contains("spatial distribution of clusters") }, { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/reports/spatially_variable_genes.html").text.contains("Spatial transcriptomics data can give insight") }, - // DEGs - { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/degs/spatially_variable_genes.csv").exists() }, + // Spatially variable genes + { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/data/spatially_variable_genes.csv").exists() }, // Space Ranger { assert file("$outputDir/CytAssist_11mm_FFPE_Human_Glioblastoma_2/spaceranger/outs/web_summary.html").exists() }, From 72e07a042618f18542d1df6bb247ee1f1139b04e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 13:30:21 +0200 Subject: [PATCH 395/410] Add fix for pip install in Dockerfile --- env/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/env/Dockerfile b/env/Dockerfile index ad1c06f..8c44eed 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -21,6 +21,10 @@ RUN mkdir -p /opt/quarto \ FROM condaforge/mambaforge:23.11.0-0 COPY --from=quarto /opt/quarto /opt/quarto ENV PATH="${PATH}:/opt/quarto/bin" +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + pkg-config \ + && apt-get clean # Install packages using Mamba; also remove static libraries, python bytecode # files and javascript source maps that are not required for execution From de3c8c171be3063808e757827ac0698456420245 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 13:31:40 +0200 Subject: [PATCH 396/410] Fix adata import order --- bin/quality_controls.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index d664ecf..185714d 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -78,8 +78,8 @@ def to_legacy_anndata(sdata: spatialdata.SpatialData) -> AnnData: ```{python} # Read the data -adata = to_legacy_anndata(sdata) sdata = spatialdata.read_zarr(input_sdata, ["images", "tables", "shapes"]) +adata = to_legacy_anndata(sdata) # Convert X matrix from CSR to CSC dense matrix for output compatibility adata.X = scipy.sparse.csc_matrix(adata.X) From a465e0fe25d7beb55964f6556bbdc457f0bbc446 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Fri, 12 Apr 2024 13:38:37 +0200 Subject: [PATCH 397/410] Remove trailing whitespace --- bin/quality_controls.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index 185714d..4afabcc 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -20,7 +20,7 @@ and the [`scanpy` Python package](https://scanpy.readthedocs.io/en/stable/). The AnnData format is utilized to organize and store the Spatial Transcriptomics data. It includes information about counts, features, observations, and additional metadata. The AnnData format ensures compatibility with various -analysis tools and facilitates seamless integration into existing workflows. +analysis tools and facilitates seamless integration into existing workflows. The AnnData object is saved in the `Tables` element of a zarr [SpatialData object](https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions). ```{python} From 7e1756f5efa195c43cd054cbe80217583a360feb Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 14:02:42 +0200 Subject: [PATCH 398/410] Export `spatialdata_io` version --- modules/local/read_data.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/local/read_data.nf b/modules/local/read_data.nf index dfc7563..209373a 100644 --- a/modules/local/read_data.nf +++ b/modules/local/read_data.nf @@ -41,7 +41,7 @@ process READ_DATA { cat <<-END_VERSIONS > versions.yml "${task.process}": - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + spatialdata_io: \$(python -c "import spatialdata_io; print(spatialdata_io.__version__)") END_VERSIONS """ } From 7ec707c576b2718287235eef3fe13c358da6624f Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 14:10:06 +0200 Subject: [PATCH 399/410] Remove Conda profile for READ_DATA process --- modules/local/read_data.nf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/local/read_data.nf b/modules/local/read_data.nf index 209373a..613efd9 100644 --- a/modules/local/read_data.nf +++ b/modules/local/read_data.nf @@ -6,8 +6,6 @@ process READ_DATA { tag "${meta.id}" label 'process_low' - // TODO fix conda environment to include spatialdata_io instead of scanpy - conda "conda-forge::scanpy=1.7.2 conda-forge::matplotlib=3.6.3 conda-forge::pandas=1.5.3" container "docker.io/erikfas/spatialtranscriptomics" input: @@ -21,6 +19,9 @@ process READ_DATA { task.ext.when == null || task.ext.when script: + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + exit 1, "The READ_DATA module does not support Conda/Mamba, please use Docker / Singularity / Podman instead." + } """ # Fix required directory structure mkdir "${meta.id}/spatial" From 509f547c4675de0d66de334996e3b26558948827 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 14:12:09 +0200 Subject: [PATCH 400/410] Update snapshots --- tests/pipeline/test_downstream.nf.test.snap | 4 ++-- tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap | 4 ++-- .../pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index c848af7..cdacf62 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-19T18:46:59.035976" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index ee3d586..11b5370 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -11,7 +11,7 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -19,4 +19,4 @@ }, "timestamp": "2024-04-04T10:08:16.975105" } -} \ No newline at end of file +} diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index 92e16a3..be972cc 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={scanpy=1.9.8}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-04-04T10:42:54.76102" } -} \ No newline at end of file +} From 0bb2062cad18b8c4e6cff21b39fbf92be44f4de4 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Fri, 12 Apr 2024 14:45:42 +0200 Subject: [PATCH 401/410] Remove TODO --- bin/read_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/read_data.py b/bin/read_data.py index fd27753..c0ed0f4 100755 --- a/bin/read_data.py +++ b/bin/read_data.py @@ -24,7 +24,6 @@ default=None, help="Output spatialdata zarr path.", ) - # TODO Add argument with meta.id for dataset_id args = parser.parse_args() From 4d64794f5d6fc1fffe278cef1ab7165406363378 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Apr 2024 14:06:19 +0200 Subject: [PATCH 402/410] Add Geary's C autocorrelation method to SVG report --- bin/spatially_variable_genes.qmd | 12 ++++++++---- nextflow.config | 1 + nextflow_schema.json | 7 +++++++ subworkflows/local/downstream.nf | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index e5762ea..71afdc3 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -10,6 +10,7 @@ jupyter: python3 #| tags: [parameters] #| echo: false input_sdata = "sdata_processed.zarr" # Input: SpatialData file +svg_autocorr_method = "moran" # Parameter: SVG autocorrelation method n_top_svgs = 14 # Number of spatially variable genes to plot artifact_dir = "artifacts" # Output directory output_csv = "spatially_variable_genes.csv" # Output: gene list @@ -117,15 +118,18 @@ patterns. Here we use [Moran's I](https://en.wikipedia.org/wiki/Moran%27s_I) aut ```{python} adata.var_names_make_unique() -sq.gr.spatial_autocorr(adata, mode="moran") -adata.uns["moranI"].head(n_top_svgs) -#[TODO] add gearyC as optional mode +sq.gr.spatial_autocorr(adata, mode=svg_autocorr_method) +if svg_autocorr_method == "moran": + svg_autocorr_method_string = "moranI" +else: + svg_autocorr_method_string = "gearyC" +adata.uns[svg_autocorr_method_string].head(n_top_svgs) ``` ```{python} #| echo: false # Save the spatially variable genes to a CSV file: -adata.uns["moranI"].to_csv(os.path.join(artifact_dir, output_csv)) +adata.uns[svg_autocorr_method_string].to_csv(os.path.join(artifact_dir, output_csv)) ``` ```{python} diff --git a/nextflow.config b/nextflow.config index c622d42..945353c 100644 --- a/nextflow.config +++ b/nextflow.config @@ -30,6 +30,7 @@ params { cluster_resolution = 1.0 // Spatial differential expression + svg_autocorr_method = "moran" n_top_svgs = 14 // MultiQC options diff --git a/nextflow_schema.json b/nextflow_schema.json index 6bd574c..c5f4714 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -148,6 +148,13 @@ "help_text": "The resolution controls the coarseness of the clustering, where a higher resolution leads to more clusters.", "fa_icon": "fas fa-circle-nodes" }, + "svg_autocorr_method": { + "type": "string", + "default": "moran", + "description": "The method to use for spatially variable gene autocorrelation.", + "enum": ["moran", "geary"], + "fa_icon": "fas fa-circle-nodes" + }, "n_top_svgs": { "type": "integer", "default": 14, diff --git a/subworkflows/local/downstream.nf b/subworkflows/local/downstream.nf index 77269ee..a477bbc 100644 --- a/subworkflows/local/downstream.nf +++ b/subworkflows/local/downstream.nf @@ -82,6 +82,7 @@ workflow DOWNSTREAM { .map { tuple(it[0], spatially_variable_genes_notebook) } spatially_variable_genes_params = [ input_sdata: "sdata_processed.zarr", + svg_autocorr_method: params.svg_autocorr_method, n_top_svgs: params.n_top_svgs, artifact_dir: "artifacts", output_csv: "spatially_variable_genes.csv", From d8324129659f6246b0fe911c1528a3f098a75d1e Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Apr 2024 14:07:59 +0200 Subject: [PATCH 403/410] Update FastQC module --- modules.json | 2 +- modules/nf-core/fastqc/main.nf | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules.json b/modules.json index e071f8c..bb3e8b3 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "fastqc": { "branch": "master", - "git_sha": "f4ae1d942bd50c5c0b9bd2de1393ce38315ba57c", + "git_sha": "285a50500f9e02578d90b3ce6382ea3c30216acd", "installed_by": ["modules"] }, "multiqc": { diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf index 9e19a74..d79f1c8 100644 --- a/modules/nf-core/fastqc/main.nf +++ b/modules/nf-core/fastqc/main.nf @@ -25,6 +25,11 @@ process FASTQC { def old_new_pairs = reads instanceof Path || reads.size() == 1 ? [[ reads, "${prefix}.${reads.extension}" ]] : reads.withIndex().collect { entry, index -> [ entry, "${prefix}_${index + 1}.${entry.extension}" ] } 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') + // FastQC memory value allowed range (100 - 10000) + def fastqc_memory = memory_in_mb > 10000 ? 10000 : (memory_in_mb < 100 ? 100 : memory_in_mb) + """ printf "%s %s\\n" $rename_to | while read old_name new_name; do [ -f "\${new_name}" ] || ln -s \$old_name \$new_name @@ -33,6 +38,7 @@ process FASTQC { fastqc \\ $args \\ --threads $task.cpus \\ + --memory $fastqc_memory \\ $renamed_files cat <<-END_VERSIONS > versions.yml From 5c033e34cbfa6608d12c59464f9c887f5e1f9c3a Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Tue, 16 Apr 2024 16:31:33 +0200 Subject: [PATCH 404/410] Change name to `nf-core/spatialvi` As per discussions in the nf-core community, this commit changes the name of the pipeline from `nf-core/spatialtranscriptomics` to `nf-core/spatialvi`. The old name is descriptive in regards to what it does, but not specific enough, as it analyses 10X Visium data exclusively. It was determined that spatial nf-core pipelines should be named according to the `spatialXX` pattern; another pipeline named `nf-core/spatialxe` for Xenium data already exists. --- .github/CONTRIBUTING.md | 20 ++++---- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/ISSUE_TEMPLATE/config.yml | 6 +-- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 10 ++-- .github/workflows/branch.yml | 4 +- CHANGELOG.md | 46 +++++++++--------- CITATIONS.md | 2 +- README.md | 32 ++++++------ assets/_extensions/nf-core/_extension.yml | 4 +- ...core-spatialtranscriptomics_logo_light.png | 1 - .../nf-core/nf-core-spatialvi_logo_light.png | 1 + assets/adaptivecard.json | 2 +- assets/email_template.html | 14 +++--- assets/email_template.txt | 10 ++-- assets/methods_description_template.yml | 8 +-- assets/multiqc_config.yml | 8 +-- ...core-spatialtranscriptomics_logo_light.png | Bin 72211 -> 0 bytes assets/nf-core-spatialvi_logo_light.png | Bin 0 -> 75828 bytes assets/schema_input.json | 4 +- assets/sendmail_template.txt | 6 +-- assets/slackreport.json | 2 +- bin/clustering.qmd | 2 +- bin/quality_controls.qmd | 2 +- bin/spatially_variable_genes.qmd | 2 +- conf/base.config | 2 +- conf/test.config | 8 +-- conf/test_downstream.config | 8 +-- conf/test_full.config | 4 +- conf/test_spaceranger_v1.config | 8 +-- docs/README.md | 4 +- ...-core-spatialtranscriptomics_logo_dark.png | Bin 21637 -> 0 bytes ...core-spatialtranscriptomics_logo_light.png | Bin 18162 -> 0 bytes docs/images/nf-core-spatialvi_logo_dark.png | Bin 0 -> 28955 bytes docs/images/nf-core-spatialvi_logo_light.png | Bin 0 -> 24680 bytes docs/output.md | 2 +- docs/usage.md | 18 +++---- env/Dockerfile | 2 +- main.nf | 24 ++++----- modules.json | 4 +- modules/local/read_data.nf | 2 +- modules/nf-core/quartonotebook/main.nf | 2 +- .../quartonotebook/quartonotebook.diff | 2 +- nextflow.config | 16 +++--- nextflow_schema.json | 10 ++-- .../main.nf | 2 +- tests/pipeline/test_downstream.nf.test | 6 +-- tests/pipeline/test_downstream.nf.test.snap | 4 +- .../pipeline/test_spaceranger_ffpe_v1.nf.test | 6 +-- .../test_spaceranger_ffpe_v1.nf.test.snap | 4 +- ...spaceranger_ffpe_v2_cytassist.nf.test.snap | 4 +- ...spatialtranscriptomics.nf => spatialvi.nf} | 4 +- 52 files changed, 169 insertions(+), 169 deletions(-) delete mode 120000 assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png create mode 120000 assets/_extensions/nf-core/nf-core-spatialvi_logo_light.png delete mode 100644 assets/nf-core-spatialtranscriptomics_logo_light.png create mode 100644 assets/nf-core-spatialvi_logo_light.png delete mode 100644 docs/images/nf-core-spatialtranscriptomics_logo_dark.png delete mode 100644 docs/images/nf-core-spatialtranscriptomics_logo_light.png create mode 100644 docs/images/nf-core-spatialvi_logo_dark.png create mode 100644 docs/images/nf-core-spatialvi_logo_light.png rename subworkflows/local/{utils_nfcore_spatialtranscriptomics_pipeline => utils_nfcore_spatialvi_pipeline}/main.nf (99%) rename workflows/{spatialtranscriptomics.nf => spatialvi.nf} (98%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 90cd7e8..518d733 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,23 +1,23 @@ -# nf-core/spatialtranscriptomics: Contributing Guidelines +# nf-core/spatialvi: Contributing Guidelines Hi there! -Many thanks for taking an interest in improving nf-core/spatialtranscriptomics. +Many thanks for taking an interest in improving nf-core/spatialvi. -We try to manage the required tasks for nf-core/spatialtranscriptomics using GitHub issues, you probably came to this page when creating one. +We try to manage the required tasks for nf-core/spatialvi using GitHub issues, you probably came to this page when creating one. Please use the pre-filled template to save time. However, don't be put off by this template - other more general issues and suggestions are welcome! Contributions to the code are even more welcome ;) > [!NOTE] -> If you need help using or modifying nf-core/spatialtranscriptomics then the best place to ask is on the nf-core Slack [#spatialtranscriptomics](https://nfcore.slack.com/channels/spatialtranscriptomics) channel ([join our Slack here](https://nf-co.re/join/slack)). +> If you need help using or modifying nf-core/spatialvi then the best place to ask is on the nf-core Slack [#spatialvi](https://nfcore.slack.com/channels/spatialvi) channel ([join our Slack here](https://nf-co.re/join/slack)). ## Contribution workflow -If you'd like to write some code for nf-core/spatialtranscriptomics, the standard workflow is as follows: +If you'd like to write some code for nf-core/spatialvi, the standard workflow is as follows: -1. Check that there isn't already an issue about your idea in the [nf-core/spatialtranscriptomics issues](https://github.com/nf-core/spatialtranscriptomics/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/spatialtranscriptomics repository](https://github.com/nf-core/spatialtranscriptomics) to your GitHub account +1. Check that there isn't already an issue about your idea in the [nf-core/spatialvi issues](https://github.com/nf-core/spatialvi/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/spatialvi repository](https://github.com/nf-core/spatialvi) 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). 5. Submit a Pull Request against the `dev` branch and wait for the code to be reviewed and merged @@ -61,11 +61,11 @@ These tests are run both with the latest available version of `Nextflow` and als ## Getting help -For further information/help, please consult the [nf-core/spatialtranscriptomics documentation](https://nf-co.re/spatialtranscriptomics/usage) and don't hesitate to get in touch on the nf-core Slack [#spatialtranscriptomics](https://nfcore.slack.com/channels/spatialtranscriptomics) channel ([join our Slack here](https://nf-co.re/join/slack)). +For further information/help, please consult the [nf-core/spatialvi documentation](https://nf-co.re/spatialvi/usage) and don't hesitate to get in touch on the nf-core Slack [#spatialvi](https://nfcore.slack.com/channels/spatialvi) channel ([join our Slack here](https://nf-co.re/join/slack)). ## Pipeline contribution conventions -To make the nf-core/spatialtranscriptomics code and processing logic more understandable for new contributors and to ensure quality, we semi-standardise the way the code and other contributions are written. +To make the nf-core/spatialvi code and processing logic more understandable for new contributors and to ensure quality, we semi-standardise the way the code and other contributions are written. ### Adding a new step @@ -115,7 +115,7 @@ This repo includes a devcontainer configuration which will create a GitHub Codes To get started: -- Open the repo in [Codespaces](https://github.com/nf-core/spatialtranscriptomics/codespaces) +- Open the repo in [Codespaces](https://github.com/nf-core/spatialvi/codespaces) - Tools installed - nf-core - Nextflow diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 936ae84..2993ab2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: Before you post this issue, please check the documentation: - [nf-core website: troubleshooting](https://nf-co.re/usage/troubleshooting) - - [nf-core/spatialtranscriptomics pipeline documentation](https://nf-co.re/spatialtranscriptomics/usage) + - [nf-core/spatialvi pipeline documentation](https://nf-co.re/spatialvi/usage) - type: textarea id: description @@ -47,4 +47,4 @@ body: * Executor _(eg. slurm, local, awsbatch)_ * Container engine: _(e.g. Docker, Singularity, Conda, Podman, Shifter, Charliecloud, or Apptainer)_ * OS _(eg. CentOS Linux, macOS, Linux Mint)_ - * Version of nf-core/spatialtranscriptomics _(eg. 1.1, 1.5, 1.8.2)_ + * Version of nf-core/spatialvi _(eg. 1.1, 1.5, 1.8.2)_ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index dcad003..c9a6fa4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,6 +2,6 @@ contact_links: - name: Join nf-core url: https://nf-co.re/join about: Please join the nf-core community here - - name: "Slack #spatialtranscriptomics channel" - url: https://nfcore.slack.com/channels/spatialtranscriptomics - about: Discussion about the nf-core/spatialtranscriptomics pipeline + - name: "Slack #spatialvi channel" + url: https://nfcore.slack.com/channels/spatialvi + about: Discussion about the nf-core/spatialvi pipeline diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 181d3e1..1b23a3d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Suggest an idea for the nf-core/spatialtranscriptomics pipeline +description: Suggest an idea for the nf-core/spatialvi pipeline labels: enhancement body: - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d90b4d8..560b815 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,22 @@ ## PR checklist - [ ] This comment contains a description of changes (with reason). - [ ] 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/spatialtranscriptomics/tree/master/.github/CONTRIBUTING.md) -- [ ] If necessary, also make a PR on the nf-core/spatialtranscriptomics _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. +- [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/nf-core/spatialvi/tree/master/.github/CONTRIBUTING.md) +- [ ] If necessary, also make a PR on the nf-core/spatialvi _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. - [ ] Make sure your code lints (`nf-core lint`). - [ ] Ensure the test suite passes (`nf-test test main.nf.test -profile test,docker`). - [ ] Check for unexpected warnings in debug mode (`nextflow run . -profile debug,test,docker --outdir `). diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 9ab998b..405362d 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -11,9 +11,9 @@ jobs: steps: # PRs to the nf-core repo master branch are only ok if coming from the nf-core repo `dev` or any `patch` branches - name: Check PRs - if: github.repository == 'nf-core/spatialtranscriptomics' + if: github.repository == 'nf-core/spatialvi' run: | - { [[ ${{github.event.pull_request.head.repo.full_name }} == nf-core/spatialtranscriptomics ]] && [[ $GITHUB_HEAD_REF == "dev" ]]; } || [[ $GITHUB_HEAD_REF == "patch" ]] + { [[ ${{github.event.pull_request.head.repo.full_name }} == nf-core/spatialvi ]] && [[ $GITHUB_HEAD_REF == "dev" ]]; } || [[ $GITHUB_HEAD_REF == "patch" ]] # If the above check failed, post a comment on the PR explaining the failure # NOTE - this doesn't currently work if the PR is coming from a fork, due to limitations in GitHub actions secrets diff --git a/CHANGELOG.md b/CHANGELOG.md index 066d74d..84cc97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ -# nf-core/spatialtranscriptomics: Changelog +# nf-core/spatialvi: Changelog The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -Initial release of nf-core/spatialtranscriptomics, created with the +Initial release of nf-core/spatialvi, created with the [nf-core](https://nf-co.re/) template. This marks the point at which the pipeline development was moved to nf-core and NBIS. The pipeline has undergone several iterations regarding its functionality and content; there are a @@ -18,29 +18,29 @@ compatible with further downstream analyses and/or exploration in _e.g._ ### `Added` -- Add MultiQC support for Space Ranger outputs [[#70](https://github.com/nf-core/spatialtranscriptomics/pull/70)] -- Use the QUARTONOTEBOOK nf-core module instead of local Quarto-based modules [[#68](https://github.com/nf-core/spatialtranscriptomics/pull/68)] -- Add support for SpatialData [[$67](https://github.com/nf-core/spatialtranscriptomics/pull/67)] -- Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialtranscriptomics/pull/64)] +- Add MultiQC support for Space Ranger outputs [[#70](https://github.com/nf-core/spatialvi/pull/70)] +- Use the QUARTONOTEBOOK nf-core module instead of local Quarto-based modules [[#68](https://github.com/nf-core/spatialvi/pull/68)] +- Add support for SpatialData [[$67](https://github.com/nf-core/spatialvi/pull/67)] +- Add a custom nf-core Quarto template for the downstream analysis reports [[#64](https://github.com/nf-core/spatialvi/pull/64)] - Allow input directories `fastq_dir` and `spaceranger_dir` to be specified as tar archives (`.tar.gz`) -- Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialtranscriptomics/issues/46)] -- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialtranscriptomics/pull/42)] -- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialtranscriptomics/pull/44)] -- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialtranscriptomics/pull/43)] -- Use a samplesheet for input specification [[#30](https://github.com/nf-core/spatialtranscriptomics/pull/30), [#31](https://github.com/nf-core/spatialtranscriptomics/pull/31) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] -- Add Space Ranger pre-processing as an optional pipeline step using the `spaceranger` nf-core module [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#45](https://github.com/nf-core/spatialtranscriptomics/pull/45)] -- Add `env/` directory with pipeline-specific container and Conda environment specifications [[#17](https://github.com/nf-core/spatialtranscriptomics/pull/17) and [#28](https://github.com/nf-core/spatialtranscriptomics/pull/28)] -- Use a more standardised practice to find mitochondrial genes [[#30](https://github.com/nf-core/spatialtranscriptomics/pull/30)] -- Make pipeline output compatible with TissUUmaps [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] -- Add custom Quarto-based reports for all downstream processing [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] -- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialtranscriptomics/pull/43)] +- Add a check to make sure that there are spots left after filtering [[#46](https://github.com/nf-core/spatialvi/issues/46)] +- Implement tests with nf-test [[#42](https://github.com/nf-core/spatialvi/pull/42)] +- Replace custom code to download reference with `untar` module [[#44](https://github.com/nf-core/spatialvi/pull/44)] +- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialvi/pull/43)] +- Use a samplesheet for input specification [[#30](https://github.com/nf-core/spatialvi/pull/30), [#31](https://github.com/nf-core/spatialvi/pull/31) and [#45](https://github.com/nf-core/spatialvi/pull/45)] +- Add Space Ranger pre-processing as an optional pipeline step using the `spaceranger` nf-core module [[#17](https://github.com/nf-core/spatialvi/pull/17) and [#45](https://github.com/nf-core/spatialvi/pull/45)] +- Add `env/` directory with pipeline-specific container and Conda environment specifications [[#17](https://github.com/nf-core/spatialvi/pull/17) and [#28](https://github.com/nf-core/spatialvi/pull/28)] +- Use a more standardised practice to find mitochondrial genes [[#30](https://github.com/nf-core/spatialvi/pull/30)] +- Make pipeline output compatible with TissUUmaps [[#31](https://github.com/nf-core/spatialvi/pull/31)] +- Add custom Quarto-based reports for all downstream processing [[#31](https://github.com/nf-core/spatialvi/pull/31)] +- Embed resources in quarto reports [[#43](https://github.com/nf-core/spatialvi/pull/43)] ### `Fixed` -- [#51](https://github.com/nf-core/spatialtranscriptomics/issues/51): Fix version export of `leidenalg` and `SpatialDE` Python modules -- [#38](https://github.com/nf-core/spatialtranscriptomics/issues/38): Specify manual alignment files in samplesheet -- [#20](https://github.com/nf-core/spatialtranscriptomics/issues/20) and [#22](https://github.com/nf-core/spatialtranscriptomics/issues/22): Add missing Groovy module -- [#53](https://github.com/nf-core/spatialtranscriptomics/pull/53): Use ensemble IDs as index in adata.var and fix related +- [#51](https://github.com/nf-core/spatialvi/issues/51): Fix version export of `leidenalg` and `SpatialDE` Python modules +- [#38](https://github.com/nf-core/spatialvi/issues/38): Specify manual alignment files in samplesheet +- [#20](https://github.com/nf-core/spatialvi/issues/20) and [#22](https://github.com/nf-core/spatialvi/issues/22): Add missing Groovy module +- [#53](https://github.com/nf-core/spatialvi/pull/53): Use ensemble IDs as index in adata.var and fix related issue with SpatialDE ### `Dependencies` @@ -60,10 +60,10 @@ versions of the same tool. ### `Removed` -- Streamline pipeline for basic ST data processing; remove SC processing and deconvolution (for now) [[#31](https://github.com/nf-core/spatialtranscriptomics/pull/31)] +- Streamline pipeline for basic ST data processing; remove SC processing and deconvolution (for now) [[#31](https://github.com/nf-core/spatialvi/pull/31)] ## v0.1.0 - 2023-03-31 -Initial release of nf-core/spatialtranscriptomics, created with the +Initial release of nf-core/spatialvi, created with the [nf-core](https://nf-co.re/) template by the Jackson Laboratory contributors (see `README.md` for details). diff --git a/CITATIONS.md b/CITATIONS.md index d82d84d..5f048a5 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -1,4 +1,4 @@ -# nf-core/spatialtranscriptomics: Citations +# nf-core/spatialvi: Citations ## [nf-core](https://pubmed.ncbi.nlm.nih.gov/32055031/) diff --git a/README.md b/README.md index 0322b77..cf0980f 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,25 @@

      - - nf-core/spatialtranscriptomics + + nf-core/spatialvi

      -[![GitHub Actions CI Status](https://github.com/nf-core/spatialtranscriptomics/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/spatialtranscriptomics/actions/workflows/ci.yml) -[![GitHub Actions Linting Status](https://github.com/nf-core/spatialtranscriptomics/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialtranscriptomics/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/spatialtranscriptomics/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) +[![GitHub Actions CI Status](https://github.com/nf-core/spatialvi/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/spatialvi/actions/workflows/ci.yml) +[![GitHub Actions Linting Status](https://github.com/nf-core/spatialvi/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialvi/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/spatialvi/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/) [![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) [![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/) [![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/) -[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://tower.nf/launch?pipeline=https://github.com/nf-core/spatialtranscriptomics) +[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://tower.nf/launch?pipeline=https://github.com/nf-core/spatialvi) -[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23spatialtranscriptomics-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/spatialtranscriptomics)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) +[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23spatialvi-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/spatialvi)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) ## Introduction -**nf-core/spatialtranscriptomics** is a bioinformatics analysis pipeline for +**nf-core/spatialvi** is a bioinformatics analysis pipeline for Spatial Transcriptomics. It can process and analyse 10X spatial data either directly from raw data by running [Space Ranger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) or data already processed by Space Ranger. The pipeline currently consists of the @@ -46,7 +46,7 @@ full-sized dataset on the AWS cloud infrastructure. This ensures that the pipeline runs on AWS, has sensible resource allocation defaults set to run on real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from -the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialtranscriptomics/results). +the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialvi/results). ## Usage @@ -56,7 +56,7 @@ the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spat You can run the pipeline using: ```bash -nextflow run nf-core/spatialtranscriptomics \ +nextflow run nf-core/spatialvi \ -profile \ --input samplesheet.csv \ --outdir @@ -65,18 +65,18 @@ nextflow run nf-core/spatialtranscriptomics \ > [!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). -For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialtranscriptomics/usage) and the [parameter documentation](https://nf-co.re/spatialtranscriptomics/parameters). +For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialvi/usage) and the [parameter documentation](https://nf-co.re/spatialvi/parameters). ## Pipeline output -To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/spatialtranscriptomics/results) tab on the nf-core website pipeline page. +To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/spatialvi/results) tab on the nf-core website pipeline page. For more details about the output files and reports, please refer to the -[output documentation](https://nf-co.re/spatialtranscriptomics/output). +[output documentation](https://nf-co.re/spatialvi/output). ## Credits -nf-core/spatialtranscriptomics was originally developed by the Jackson -Laboratory1, up to the [0.1.0](https://github.com/nf-core/spatialtranscriptomics/releases/tag/0.1.0) +nf-core/spatialvi was originally developed by the Jackson +Laboratory1, up to the [0.1.0](https://github.com/nf-core/spatialvi/releases/tag/0.1.0) tag. It was further developed in a collaboration between the [National Bioinformatics Infrastructure Sweden](https://nbis.se/) and [National Genomics Infrastructure](https://ngisweden.scilifelab.se/) within [SciLifeLab](https://scilifelab.se/); @@ -96,12 +96,12 @@ Chuang and Dr. Anuj Srivastava._ If you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md). -For further information or help, don't hesitate to get in touch on the [Slack `#spatialtranscriptomics` channel](https://nfcore.slack.com/channels/spatialtranscriptomics) (you can join with [this invite](https://nf-co.re/join/slack)). +For further information or help, don't hesitate to get in touch on the [Slack `#spatialvi` channel](https://nfcore.slack.com/channels/spatialvi) (you can join with [this invite](https://nf-co.re/join/slack)). ## Citations - + An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. diff --git a/assets/_extensions/nf-core/_extension.yml b/assets/_extensions/nf-core/_extension.yml index 69579df..95759fc 100644 --- a/assets/_extensions/nf-core/_extension.yml +++ b/assets/_extensions/nf-core/_extension.yml @@ -12,14 +12,14 @@ contributes: smooth-scroll: true theme: [default, nf-core.scss] toc: true - toc-image: nf-core-spatialtranscriptomics_logo_light.png + toc-image: nf-core-spatialvi_logo_light.png toc-location: left template-partials: - toc.html revealjs: code-line-numbers: false embed-resources: true - logo: nf-core-spatialtranscriptomics_logo_light.png + logo: nf-core-spatialvi_logo_light.png slide-level: 2 slide-number: false theme: [default, nf-core.scss] diff --git a/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png b/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png deleted file mode 120000 index a64fe77..0000000 --- a/assets/_extensions/nf-core/nf-core-spatialtranscriptomics_logo_light.png +++ /dev/null @@ -1 +0,0 @@ -../../nf-core-spatialtranscriptomics_logo_light.png \ No newline at end of file diff --git a/assets/_extensions/nf-core/nf-core-spatialvi_logo_light.png b/assets/_extensions/nf-core/nf-core-spatialvi_logo_light.png new file mode 120000 index 0000000..b20f5fe --- /dev/null +++ b/assets/_extensions/nf-core/nf-core-spatialvi_logo_light.png @@ -0,0 +1 @@ +../../nf-core-spatialvi_logo_light.png \ No newline at end of file diff --git a/assets/adaptivecard.json b/assets/adaptivecard.json index fd842be..f985020 100644 --- a/assets/adaptivecard.json +++ b/assets/adaptivecard.json @@ -17,7 +17,7 @@ "size": "Large", "weight": "Bolder", "color": "<% if (success) { %>Good<% } else { %>Attention<%} %>", - "text": "nf-core/spatialtranscriptomics v${version} - ${runName}", + "text": "nf-core/spatialvi v${version} - ${runName}", "wrap": true }, { diff --git a/assets/email_template.html b/assets/email_template.html index 0a69d64..da9592c 100644 --- a/assets/email_template.html +++ b/assets/email_template.html @@ -4,21 +4,21 @@ - - nf-core/spatialtranscriptomics Pipeline Report + + nf-core/spatialvi Pipeline Report
      -

      nf-core/spatialtranscriptomics ${version}

      +

      nf-core/spatialvi ${version}

      Run Name: $runName

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

      nf-core/spatialtranscriptomics execution completed unsuccessfully!

      +

      nf-core/spatialvi execution completed unsuccessfully!

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

      The full error message was:

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

      nf-core/spatialtranscriptomics executi } else { out << """
      - nf-core/spatialtranscriptomics execution completed successfully! + nf-core/spatialvi execution completed successfully!
      """ } @@ -44,8 +44,8 @@

      Pipeline Configuration:

    \n" - for (param in group_params.keySet()) { - summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" - } - summary_section += "
    Process Name \\", + " \\ Software Version
    CUSTOM_DUMPSOFTWAREVERSIONSpython3.11.7
    yaml5.4.1
    TOOL1tool10.11.9
    TOOL2tool21.9
    WorkflowNextflow
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    File typeConventional base calls
    -

    nf-core/spatialtranscriptomics

    -

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

    +

    nf-core/spatialvi

    +

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

    diff --git a/assets/email_template.txt b/assets/email_template.txt index 2a7dd66..ef4affb 100644 --- a/assets/email_template.txt +++ b/assets/email_template.txt @@ -4,15 +4,15 @@ |\\ | |__ __ / ` / \\ |__) |__ } { | \\| | \\__, \\__/ | \\ |___ \\`-._,-`-, `._,._,' - nf-core/spatialtranscriptomics ${version} + nf-core/spatialvi ${version} ---------------------------------------------------- Run Name: $runName <% if (success){ - out << "## nf-core/spatialtranscriptomics execution completed successfully! ##" + out << "## nf-core/spatialvi execution completed successfully! ##" } else { out << """#################################################### -## nf-core/spatialtranscriptomics execution completed unsuccessfully! ## +## nf-core/spatialvi execution completed unsuccessfully! ## #################################################### The exit status of the task that caused the workflow execution to fail was: $exitStatus. The full error message was: @@ -35,5 +35,5 @@ Pipeline Configuration: <% out << summary.collect{ k,v -> " - $k: $v" }.join("\n") %> -- -nf-core/spatialtranscriptomics -https://github.com/nf-core/spatialtranscriptomics +nf-core/spatialvi +https://github.com/nf-core/spatialvi diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index c65a0c4..11102b9 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -1,12 +1,12 @@ -id: "nf-core-spatialtranscriptomics-methods-description" +id: "nf-core-spatialvi-methods-description" description: "Suggested text and references to use when describing pipeline usage within the methods section of a publication." -section_name: "nf-core/spatialtranscriptomics Methods Description" -section_href: "https://github.com/nf-core/spatialtranscriptomics" +section_name: "nf-core/spatialvi Methods Description" +section_href: "https://github.com/nf-core/spatialvi" plot_type: "html" ## You inject any metadata in the Nextflow '${workflow}' object data: |

    Methods

    -

    Data was processed using nf-core/spatialtranscriptomics v${workflow.manifest.version} ${doi_text} of the nf-core collection of workflows (Ewels et al., 2020), utilising reproducible software environments from the Bioconda (GrĂĽning et al., 2018) and Biocontainers (da Veiga Leprevost et al., 2017) projects.

    +

    Data was processed using nf-core/spatialvi v${workflow.manifest.version} ${doi_text} of the nf-core collection of workflows (Ewels et al., 2020), utilising reproducible software environments from the Bioconda (GrĂĽning et al., 2018) and Biocontainers (da Veiga Leprevost et al., 2017) projects.

    The pipeline was executed with Nextflow v${workflow.nextflow.version} (Di Tommaso et al., 2017) with the following command:

    ${workflow.commandLine}

    ${tool_citations}

    diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 2ea9059..2449bbe 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -1,13 +1,13 @@ report_comment: > - This report has been generated by the nf-core/spatialtranscriptomics + This report has been generated by the nf-core/spatialvi analysis pipeline. For information about how to interpret these results, please see the - documentation. + documentation. report_section_order: - "nf-core-spatialtranscriptomics-methods-description": + "nf-core-spatialvi-methods-description": order: -1000 software_versions: order: -1001 - "nf-core-spatialtranscriptomics-summary": + "nf-core-spatialvi-summary": order: -1002 export_plots: true diff --git a/assets/nf-core-spatialtranscriptomics_logo_light.png b/assets/nf-core-spatialtranscriptomics_logo_light.png deleted file mode 100644 index 34da382b6a78091237f8ca325032f48989619456..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72211 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 -Content-Disposition: inline; filename="nf-core-spatialtranscriptomics_logo_light.png" +Content-Disposition: inline; filename="nf-core-spatialvi_logo_light.png" -<% out << new File("$projectDir/assets/nf-core-spatialtranscriptomics_logo_light.png"). +<% out << new File("$projectDir/assets/nf-core-spatialvi_logo_light.png"). bytes. encodeBase64(). toString(). diff --git a/assets/slackreport.json b/assets/slackreport.json index 4bd4bfb..ac5eea3 100644 --- a/assets/slackreport.json +++ b/assets/slackreport.json @@ -3,7 +3,7 @@ { "fallback": "Plain-text summary of the attachment.", "color": "<% if (success) { %>good<% } else { %>danger<%} %>", - "author_name": "nf-core/spatialtranscriptomics ${version} - ${runName}", + "author_name": "nf-core/spatialvi ${version} - ${runName}", "author_icon": "https://www.nextflow.io/docs/latest/_static/favicon.ico", "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors<% } %>", "fields": [ diff --git a/bin/clustering.qmd b/bin/clustering.qmd index 3985af8..671471c 100644 --- a/bin/clustering.qmd +++ b/bin/clustering.qmd @@ -1,5 +1,5 @@ --- -title: "nf-core/spatialtranscriptomics" +title: "nf-core/spatialvi" subtitle: "Dimensionality reduction and clustering" format: nf-core-html: default diff --git a/bin/quality_controls.qmd b/bin/quality_controls.qmd index 4afabcc..6bfe9c3 100644 --- a/bin/quality_controls.qmd +++ b/bin/quality_controls.qmd @@ -1,5 +1,5 @@ --- -title: "nf-core/spatialtranscriptomics" +title: "nf-core/spatialvi" subtitle: "Pre-processing and quality controls" format: nf-core-html: default diff --git a/bin/spatially_variable_genes.qmd b/bin/spatially_variable_genes.qmd index 71afdc3..6d52c92 100644 --- a/bin/spatially_variable_genes.qmd +++ b/bin/spatially_variable_genes.qmd @@ -1,5 +1,5 @@ --- -title: "nf-core/spatialtranscriptomics" +title: "nf-core/spatialvi" subtitle: "Neighborhood enrichment analysis and Spatially variable genes" format: nf-core-html: default diff --git a/conf/base.config b/conf/base.config index ba62d19..a0a554a 100644 --- a/conf/base.config +++ b/conf/base.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/spatialtranscriptomics Nextflow base config file + nf-core/spatialvi Nextflow base config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A 'blank slate' config file, appropriate for general use on most high performance compute environments. Assumes that all software is installed and available on diff --git a/conf/test.config b/conf/test.config index 97b801b..ffebede 100644 --- a/conf/test.config +++ b/conf/test.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test, --outdir + nextflow run nf-core/spatialvi -profile test, --outdir ---------------------------------------------------------------------------------------- */ @@ -20,9 +20,9 @@ params { max_time = '2.h' // Input and output - input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + input = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_spaceranger.csv" + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters qc_min_counts = 5 diff --git a/conf/test_downstream.config b/conf/test_downstream.config index 51a6dc9..263b0b5 100644 --- a/conf/test_downstream.config +++ b/conf/test_downstream.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test_downstream, --outdir + nextflow run nf-core/spatialvi -profile test_downstream, --outdir ---------------------------------------------------------------------------------------- */ @@ -20,9 +20,9 @@ params { max_time = '2.h' // Input and output - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters qc_min_counts = 5 diff --git a/conf/test_full.config b/conf/test_full.config index db19919..770a747 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a full size pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test_full, --outdir + nextflow run nf-core/spatialvi -profile test_full, --outdir ---------------------------------------------------------------------------------------- */ @@ -15,5 +15,5 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data for full size test - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/test-dataset/samplesheet.csv' + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/test-dataset/samplesheet.csv' } diff --git a/conf/test_spaceranger_v1.config b/conf/test_spaceranger_v1.config index 5bce856..5b36146 100644 --- a/conf/test_spaceranger_v1.config +++ b/conf/test_spaceranger_v1.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/spatialtranscriptomics -profile test_spaceranger_v1, --outdir + nextflow run nf-core/spatialvi -profile test_spaceranger_v1, --outdir ---------------------------------------------------------------------------------------- */ @@ -20,9 +20,9 @@ params { max_time = '2.h' // Input and output - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' - spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' + spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters qc_min_counts = 5 diff --git a/docs/README.md b/docs/README.md index fe1cec7..65464b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# nf-core/spatialtranscriptomics: Documentation +# nf-core/spatialvi: Documentation -The nf-core/spatialtranscriptomics documentation is split into the following pages: +The nf-core/spatialvi documentation is split into the following pages: - [Usage](usage.md) - An overview of how the pipeline works, how to run it and a description of all of the different command-line flags. diff --git a/docs/images/nf-core-spatialtranscriptomics_logo_dark.png b/docs/images/nf-core-spatialtranscriptomics_logo_dark.png deleted file mode 100644 index 18665a3b84fb37e5d02a66b583bc73f3674e2025..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21637 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 B>rnsz diff --git a/docs/images/nf-core-spatialvi_logo_dark.png b/docs/images/nf-core-spatialvi_logo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7bdd03d3dfc131cd6f427afced45650645ff18f8 GIT binary patch literal 28955 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 by%U#vZ;l1kU)}_l9zskmU4vH|I^O#q{+$Er literal 0 HcmV?d00001 diff --git a/docs/images/nf-core-spatialvi_logo_light.png b/docs/images/nf-core-spatialvi_logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..533c426a9c2b4b269b31289cde9ca10060cfa967 GIT binary patch literal 24680 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 T5{&@=K>)&_ERA2CagF{j5Q_Wp literal 0 HcmV?d00001 diff --git a/docs/output.md b/docs/output.md index 3d6d220..ee2eed5 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,4 +1,4 @@ -# nf-core/spatialtranscriptomics: Output +# nf-core/spatialvi: Output ## Introduction diff --git a/docs/usage.md b/docs/usage.md index cdfc0ab..5afa5c9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,6 @@ -# nf-core/spatialtranscriptomics: Usage +# nf-core/spatialvi: Usage -## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/spatialtranscriptomics/usage](https://nf-co.re/spatialtranscriptomics/usage) +## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/spatialvi/usage](https://nf-co.re/spatialvi/usage) > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ @@ -114,9 +114,9 @@ gene expression matrices as well as spatial information. ## Space Ranger The pipeline exposes several of Space Ranger's parameters when executing with -raw spatial data. Space Ranger requires a lot of memory -(64 GB) and several threads (8) to be able to run. You can find the Space Ranger -documentation at the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). +raw spatial data. Space Ranger requires a lot of memory (64 GB) and several +threads (8) to be able to run. You can find the Space Ranger documentation at +the [10X website](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger). You are only able to run Space Ranger on the [officially supported organisms](https://support.10xgenomics.com/spatial-gene-expression/software/downloads/latest): human and mouse. If you have already downloaded a reference you may supply the @@ -136,7 +136,7 @@ The typical command for running the pipeline is as follows: ```bash nextflow run \ - nf-core/spatialtranscriptomics \ + nf-core/spatialvi \ --input \ --outdir \ -profile docker @@ -164,7 +164,7 @@ Do not use `-c ` to specify parameters as this will result in errors. Cust The above pipeline run specified with a params file in yaml format: ```bash -nextflow run nf-core/spatialtranscriptomics -profile docker -params-file params.yaml +nextflow run nf-core/spatialvi -profile docker -params-file params.yaml ``` with `params.yaml` containing: @@ -182,14 +182,14 @@ You can also generate such `YAML`/`JSON` files via [nf-core/launch](https://nf-c When you run the above command, Nextflow automatically pulls the pipeline code from GitHub and stores it as a cached version. When running the pipeline after this, it will always use the cached version if available - even if the pipeline has been updated since. To make sure that you're running the latest version of the pipeline, make sure that you regularly update the cached version of the pipeline: ```bash -nextflow pull nf-core/spatialtranscriptomics +nextflow pull nf-core/spatialvi ``` ### Reproducibility It is a good idea to specify a pipeline version when running the pipeline on your data. This ensures that a specific version of the pipeline code and software are used when you run your pipeline. If you keep using the same tag, you'll be running the same version of the pipeline, even if there have been changes to the code since. -First, go to the [nf-core/spatialtranscriptomics releases page](https://github.com/nf-core/spatialtranscriptomics/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. +First, go to the [nf-core/spatialvi releases page](https://github.com/nf-core/spatialvi/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. diff --git a/env/Dockerfile b/env/Dockerfile index 8c44eed..f6b50ae 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -42,4 +42,4 @@ CMD /bin/bash LABEL \ authors = "Erik Fasterius, Christophe Avenel" \ - description = "Dockerfile for nf-core/spatialtranscriptomics report modules" + description = "Dockerfile for nf-core/spatialvi report modules" diff --git a/main.nf b/main.nf index 8afe21f..d750244 100644 --- a/main.nf +++ b/main.nf @@ -1,11 +1,11 @@ #!/usr/bin/env nextflow /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/spatialtranscriptomics + nf-core/spatialvi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Github : https://github.com/nf-core/spatialtranscriptomics - Website: https://nf-co.re/spatialtranscriptomics - Slack : https://nfcore.slack.com/channels/spatialtranscriptomics + Github : https://github.com/nf-core/spatialvi + Website: https://nf-co.re/spatialvi + Slack : https://nfcore.slack.com/channels/spatialvi ---------------------------------------------------------------------------------------- */ @@ -17,9 +17,9 @@ nextflow.enable.dsl = 2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { SPATIALTRANSCRIPTOMICS } from './workflows/spatialtranscriptomics' -include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline' -include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline' +include { SPATIALVI } from './workflows/spatialvi' +include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_spatialvi_pipeline' +include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_spatialvi_pipeline' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -30,7 +30,7 @@ include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_spat // // WORKFLOW: Run main analysis pipeline depending on type of input // -workflow NFCORE_SPATIALTRANSCRIPTOMICS { +workflow NFCORE_SPATIALVI { take: samplesheet // file: samplesheet read in from --input @@ -40,12 +40,12 @@ workflow NFCORE_SPATIALTRANSCRIPTOMICS { // // WORKFLOW: Run pipeline // - SPATIALTRANSCRIPTOMICS ( + SPATIALVI ( samplesheet ) emit: - multiqc_report = SPATIALTRANSCRIPTOMICS.out.multiqc_report // channel: /path/to/multiqc_report.html + multiqc_report = SPATIALVI.out.multiqc_report // channel: /path/to/multiqc_report.html } /* @@ -74,7 +74,7 @@ workflow { // // WORKFLOW: Run main workflow // - NFCORE_SPATIALTRANSCRIPTOMICS ( + NFCORE_SPATIALVI ( params.input ) @@ -88,7 +88,7 @@ workflow { params.outdir, params.monochrome_logs, params.hook_url, - NFCORE_SPATIALTRANSCRIPTOMICS.out.multiqc_report + NFCORE_SPATIALVI.out.multiqc_report ) } diff --git a/modules.json b/modules.json index bb3e8b3..1e84bdd 100644 --- a/modules.json +++ b/modules.json @@ -1,6 +1,6 @@ { - "name": "nf-core/spatialtranscriptomics", - "homePage": "https://github.com/nf-core/spatialtranscriptomics", + "name": "nf-core/spatialvi", + "homePage": "https://github.com/nf-core/spatialvi", "repos": { "https://github.com/nf-core/modules.git": { "modules": { diff --git a/modules/local/read_data.nf b/modules/local/read_data.nf index 613efd9..d0f3d30 100644 --- a/modules/local/read_data.nf +++ b/modules/local/read_data.nf @@ -6,7 +6,7 @@ process READ_DATA { tag "${meta.id}" label 'process_low' - container "docker.io/erikfas/spatialtranscriptomics" + container "docker.io/erikfas/spatialvi" input: tuple val (meta), path("${meta.id}/*") diff --git a/modules/nf-core/quartonotebook/main.nf b/modules/nf-core/quartonotebook/main.nf index 2766dc5..c21abf7 100644 --- a/modules/nf-core/quartonotebook/main.nf +++ b/modules/nf-core/quartonotebook/main.nf @@ -4,7 +4,7 @@ process QUARTONOTEBOOK { tag "$meta.id" label 'process_low' - container "docker.io/erikfas/spatialtranscriptomics" + container "docker.io/erikfas/spatialvi" input: tuple val(meta), path(notebook) diff --git a/modules/nf-core/quartonotebook/quartonotebook.diff b/modules/nf-core/quartonotebook/quartonotebook.diff index 018e646..61538bd 100644 --- a/modules/nf-core/quartonotebook/quartonotebook.diff +++ b/modules/nf-core/quartonotebook/quartonotebook.diff @@ -10,7 +10,7 @@ Changes in module 'nf-core/quartonotebook' - // itself, Papermill and whatever language you are running your analyses on; - // you can see an example in this module's Dockerfile. - container "docker.io/erikfas/quartonotebook" -+ container "docker.io/erikfas/spatialtranscriptomics" ++ container "docker.io/erikfas/spatialvi" input: tuple val(meta), path(notebook) diff --git a/nextflow.config b/nextflow.config index 945353c..d605bfd 100644 --- a/nextflow.config +++ b/nextflow.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/spatialtranscriptomics Nextflow config file + nf-core/spatialvi Nextflow config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default config options for all compute environments ---------------------------------------------------------------------------------------- @@ -29,7 +29,7 @@ params { cluster_n_hvgs = 2000 cluster_resolution = 1.0 - // Spatial differential expression + // Spatially variable genes svg_autocorr_method = "moran" n_top_svgs = 14 @@ -87,12 +87,12 @@ try { System.err.println("WARNING: Could not load nf-core/config profiles: ${params.custom_config_base}/nfcore_custom.config") } -// Load nf-core/spatialtranscriptomics custom profiles from different institutions. +// Load nf-core/spatialvi custom profiles from different institutions. // Warning: Uncomment only if a pipeline-specific institutional config already exists on nf-core/configs! // try { -// includeConfig "${params.custom_config_base}/pipeline/spatialtranscriptomics.config" +// includeConfig "${params.custom_config_base}/pipeline/spatialvi.config" // } catch (Exception e) { -// System.err.println("WARNING: Could not load nf-core/config/spatialtranscriptomics profiles: ${params.custom_config_base}/pipeline/spatialtranscriptomics.config") +// System.err.println("WARNING: Could not load nf-core/config/spatialvi profiles: ${params.custom_config_base}/pipeline/spatialvi.config") // } profiles { debug { @@ -240,10 +240,10 @@ dag { } manifest { - name = 'nf-core/spatialtranscriptomics' + name = 'nf-core/spatialvi' author = """Erik Fasterius, Christophe Avenel, Sergii Domanskyi, Jeffrey Chuang, Anuj Srivastava""" - homePage = 'https://github.com/nf-core/spatialtranscriptomics' - description = """Spatial Transcriptomics""" + homePage = 'https://github.com/nf-core/spatialvi' + description = """10X Visium Spatial Transcriptomics""" mainScript = 'main.nf' nextflowVersion = '!>=23.04.0' version = '1.0dev' diff --git a/nextflow_schema.json b/nextflow_schema.json index c5f4714..648809a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://raw.githubusercontent.com/nf-core/spatialtranscriptomics/master/nextflow_schema.json", - "title": "nf-core/spatialtranscriptomics pipeline parameters", - "description": "Spatial Transcriptomics", + "$id": "https://raw.githubusercontent.com/nf-core/spatialvi/master/nextflow_schema.json", + "title": "nf-core/spatialvi pipeline parameters", + "description": "10X Visium Spatial Transcriptomics", "type": "object", "definitions": { "input_output_options": { @@ -20,7 +20,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Path to comma-separated file containing information about the samples in the experiment.", - "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline, use this parameter to specify its location. It has to be a comma-separated file with 2 or 5 columns, plus a header row. See [usage docs](https://nf-co.re/spatialtranscriptomics/usage#samplesheet-input).", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline, use this parameter to specify its location. It has to be a comma-separated file with 2 or 5 columns, plus a header row. See [usage docs](https://nf-co.re/spatialvi/usage#samplesheet-input).", "fa_icon": "fas fa-file-csv" }, "outdir": { @@ -158,7 +158,7 @@ "n_top_svgs": { "type": "integer", "default": 14, - "description": "The number of top spatial differentially expressed genes to plot.", + "description": "The number of top spatially variable genes to plot.", "fa_icon": "fas fa-hashtag" } } diff --git a/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf b/subworkflows/local/utils_nfcore_spatialvi_pipeline/main.nf similarity index 99% rename from subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf rename to subworkflows/local/utils_nfcore_spatialvi_pipeline/main.nf index c037da1..5f5695a 100644 --- a/subworkflows/local/utils_nfcore_spatialtranscriptomics_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_spatialvi_pipeline/main.nf @@ -1,5 +1,5 @@ // -// Subworkflow with functionality specific to the nf-core/spatialtranscriptomics pipeline +// Subworkflow with functionality specific to the nf-core/spatialvi pipeline // /* diff --git a/tests/pipeline/test_downstream.nf.test b/tests/pipeline/test_downstream.nf.test index 6233756..9ad57a3 100644 --- a/tests/pipeline/test_downstream.nf.test +++ b/tests/pipeline/test_downstream.nf.test @@ -7,9 +7,9 @@ nextflow_pipeline { when { params { // Input and output - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' - spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/samplesheet_downstream.csv' + spaceranger_probeset = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-brain-cancer-11-mm-capture-area-ffpe-2-standard_v2_ffpe_cytassist/outs/probe_set.csv" + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/homo_sapiens_chr22_reference.tar.gz" // Parameters qc_min_counts = 5 diff --git a/tests/pipeline/test_downstream.nf.test.snap b/tests/pipeline/test_downstream.nf.test.snap index cdacf62..8e1a3d2 100644 --- a/tests/pipeline/test_downstream.nf.test.snap +++ b/tests/pipeline/test_downstream.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_DOWNSTREAM_INPUT={untar=1.3}, Workflow={nf-core/spatialvi=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-03-19T18:46:59.035976" } -} +} \ No newline at end of file diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test index 3ee63d2..21ef046 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test @@ -6,9 +6,9 @@ nextflow_pipeline { test("Space Ranger FFPE v1 Standard") { when { params { - input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' - spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' - spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialtranscriptomics/testdata/homo_sapiens_chr22_reference.tar.gz" + input = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-ovarian-cancer-1-standard_v1_ffpe/samplesheet_spaceranger.csv' + spaceranger_probeset = 'https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/human-ovarian-cancer-1-standard_v1_ffpe/Visium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv' + spaceranger_reference = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialvi/testdata/homo_sapiens_chr22_reference.tar.gz" qc_min_counts = 5 qc_min_genes = 3 outdir = "$outputDir" diff --git a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap index 11b5370..bc10f4c 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v1.nf.test.snap @@ -1,7 +1,7 @@ { "software_versions": { "content": [ - "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, READ_DATA={scanpy=1.7.2}, SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialtranscriptomics=1.0dev}}" + "{CUSTOM_DUMPSOFTWAREVERSIONS={python=3.11.7, yaml=5.4.1}, FASTQC={fastqc=0.12.1}, SPACERANGER_COUNT={spaceranger=2.1.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.30}, CLUSTERING={quarto=1.3.302, scanpy=1.9.3}, QUALITY_CONTROLS={quarto=1.3.302, scanpy=1.9.3}, READ_DATA={scanpy=1.7.2}, SPATIAL_DE={SpatialDE=1.1.3, leidenalg=0.9.1, quarto=1.3.302, scanpy=1.9.3}, Workflow={nf-core/spatialvi=1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -11,7 +11,7 @@ }, "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialvi=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", diff --git a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap index be972cc..8a82bd8 100644 --- a/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap +++ b/tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test.snap @@ -1,7 +1,7 @@ { "nf_core_pipeline_software_mqc_versions.yml": { "content": [ - "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialtranscriptomics=v1.0dev}}" + "{CLUSTERING={quarto=1.3.450, papermill=null}, FASTQC={fastqc=0.12.1}, QUALITY_CONTROLS={quarto=1.3.450, papermill=null}, READ_DATA={spatialdata_io=0.1.2}, SPACERANGER_COUNT={spaceranger=3.0.0}, SPACERANGER_UNTAR_REFERENCE={untar=1.3}, SPATIALLY_VARIABLE_GENES={quarto=1.3.450, papermill=null}, UNTAR_SPACERANGER_INPUT={untar=1.3}, Workflow={nf-core/spatialvi=v1.0dev}}" ], "meta": { "nf-test": "0.8.4", @@ -9,4 +9,4 @@ }, "timestamp": "2024-04-04T10:42:54.76102" } -} +} \ No newline at end of file diff --git a/workflows/spatialtranscriptomics.nf b/workflows/spatialvi.nf similarity index 98% rename from workflows/spatialtranscriptomics.nf rename to workflows/spatialvi.nf index 3b2fb75..1bad7a6 100644 --- a/workflows/spatialtranscriptomics.nf +++ b/workflows/spatialvi.nf @@ -13,7 +13,7 @@ include { SPACERANGER } from '../subworkflows/local/spaceranger' include { DOWNSTREAM } from '../subworkflows/local/downstream' 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_spatialtranscriptomics_pipeline' +include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_spatialvi_pipeline' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -21,7 +21,7 @@ include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_spat ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -workflow SPATIALTRANSCRIPTOMICS { +workflow SPATIALVI { take: samplesheet // file: samplesheet read in from --input From da2b3325982a1dcd9e5084c57f7f54f52cac0f07 Mon Sep 17 00:00:00 2001 From: Christophe Avenel Date: Thu, 18 Apr 2024 11:42:45 +0200 Subject: [PATCH 405/410] Adding subway image --- README.md | 4 ++++ docs/images/spatialvi_subway.png | Bin 0 -> 175620 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/images/spatialvi_subway.png diff --git a/README.md b/README.md index cf0980f..33a4ce5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialvi/results). +

    + +

    + ## Usage > [!NOTE] diff --git a/docs/images/spatialvi_subway.png b/docs/images/spatialvi_subway.png new file mode 100644 index 0000000000000000000000000000000000000000..978148fa8564ee4e6a4e725998cca500ee9dfd3e GIT binary patch literal 175620 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 literal 0 HcmV?d00001 From 1c7fd3606194b13ee6255e51296518eaf9263ed1 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 6 May 2024 10:20:26 +0200 Subject: [PATCH 406/410] Clarify that the pipeline analyses Visium data --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 33a4ce5..8534a2c 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ ## Introduction -**nf-core/spatialvi** is a bioinformatics analysis pipeline for -Spatial Transcriptomics. It can process and analyse 10X spatial data either -directly from raw data by running [Space Ranger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) -or data already processed by Space Ranger. The pipeline currently consists of the -following steps: +**nf-core/spatialvi** is a bioinformatics analysis pipeline for Visium spatial +transcriptomics data from 10x Genomics. It can process and analyse spatial data +either directly from raw data by running [Space Ranger](https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/what-is-space-ranger) +or data already processed by Space Ranger. The pipeline currently consists of +the following steps: 0. Raw data processing with Space Ranger (optional) 1. Quality controls and filtering From b9820752c31b67a87183603abc5e45d6c31fb420 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 6 May 2024 10:20:55 +0200 Subject: [PATCH 407/410] Move metro map to after first README paragraph --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8534a2c..6d164bc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ either directly from raw data by running [Space Ranger](https://support.10xgenom or data already processed by Space Ranger. The pipeline currently consists of the following steps: +

    + +

    + 0. Raw data processing with Space Ranger (optional) 1. Quality controls and filtering 2. Normalisation @@ -48,10 +52,6 @@ real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialvi/results). -

    - -

    - ## Usage > [!NOTE] From 649a7c2767f004c9a564c93a481bd68c8d634cf5 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 6 May 2024 10:22:53 +0200 Subject: [PATCH 408/410] Update the `utils_nfcore_pipeline` subworkflow --- modules.json | 2 +- subworkflows/nf-core/utils_nfcore_pipeline/main.nf | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules.json b/modules.json index 1e84bdd..d9db588 100644 --- a/modules.json +++ b/modules.json @@ -42,7 +42,7 @@ }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "5caf7640a9ef1d18d765d55339be751bb0969dfa", + "git_sha": "92de218a329bfc9a9033116eb5f65fd270e72ba3", "installed_by": ["subworkflows"] }, "utils_nfvalidation_plugin": { diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index a8b55d6..14558c3 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -65,9 +65,15 @@ def checkProfileProvided(nextflow_cli_args) { // Citation string for pipeline // def workflowCitation() { + def temp_doi_ref = "" + String[] 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" return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + "* The pipeline\n" + - " ${workflow.manifest.doi}\n\n" + + temp_doi_ref + "\n" + "* The nf-core framework\n" + " https://doi.org/10.1038/s41587-020-0439-x\n\n" + "* Software dependencies\n" + From f8851e1b26528b6d38f3aa097c7237c5dedca2e9 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Thu, 16 May 2024 15:30:17 +0200 Subject: [PATCH 409/410] Update dark logo image from TEMPLATE branch --- docs/images/nf-core-spatialvi_logo_dark.png | Bin 28955 -> 29006 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/nf-core-spatialvi_logo_dark.png b/docs/images/nf-core-spatialvi_logo_dark.png index 7bdd03d3dfc131cd6f427afced45650645ff18f8..095b5eb3672f08a5ff1f6437c1ef98709a3ba58c 100644 GIT binary patch delta 28390 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 literal 28955 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 by%U#vZ;l1kU)}_l9zskmU4vH|I^O#q{+$Er From 7b5bc2a81009ed9f6e9538415b69c64307dbfdf7 Mon Sep 17 00:00:00 2001 From: Erik Fasterius Date: Mon, 20 May 2024 11:09:14 +0200 Subject: [PATCH 410/410] Fix CI issue --- .github/workflows/ci.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0802e2b..6972735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,20 +21,6 @@ concurrency: cancel-in-progress: true jobs: - define_nxf_versions: - name: Choose nextflow versions to test against depending on target branch - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.nxf_versions.outputs.matrix }} - steps: - - id: nxf_versions - run: | - if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "dev" && "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then - echo matrix='["latest-everything"]' | tee -a $GITHUB_OUTPUT - else - echo matrix='["latest-everything", "23.04.0"]' | tee -a $GITHUB_OUTPUT - fi - test: name: Run pipeline with test data # Only run on push if this is the nf-core dev branch (merged PRs) @@ -43,7 +29,9 @@ jobs: strategy: fail-fast: false matrix: - NXF_VER: ${{ fromJson(needs.define_nxf_versions.outputs.matrix) }} + NXF_VER: + - "23.04.0" + - "latest-everything" test: - tests/pipeline/test_spaceranger_ffpe_v1.nf.test - tests/pipeline/test_spaceranger_ffpe_v2_cytassist.nf.test
    \n" + for (param in group_params.keySet()) { + summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" + } + summary_section += "