diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12d201a5d51..7b6737e0cfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -662,6 +662,15 @@ jobs: path: subworkflows/nf-core/fastq_align_bwa - profile: conda path: subworkflows/nf-core/fasta_newick_epang_gappa + - profile: conda + path: modules/nf-core/xeniumranger/relabel + - profile: conda + path: modules/nf-core/xeniumranger/rename + - profile: conda + path: modules/nf-core/xeniumranger/resegment + - profile: conda + path: modules/nf-core/xeniumranger/import-segmentation + env: NXF_ANSI_LOG: false NFTEST_VER: "0.9.0" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 351a51a9b3a..1e4ceb6a30c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.1.0" + rev: "v4.0.0-alpha.8" hooks: - id: prettier additional_dependencies: diff --git a/modules/nf-core/xeniumranger/.gitignore b/modules/nf-core/xeniumranger/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/nf-core/xeniumranger/Dockerfile b/modules/nf-core/xeniumranger/Dockerfile new file mode 100644 index 00000000000..daa3c965c5e --- /dev/null +++ b/modules/nf-core/xeniumranger/Dockerfile @@ -0,0 +1,29 @@ +# Use a base image with the required operating system +FROM ubuntu:20.04 AS builder + +LABEL authors="Sameesh Kher " \ + description="Docker image containing Xenium Ranger" + +RUN apt-get update --allow-releaseinfo-change \ + && apt-get install -y \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# copy over xeniumranger +# the latest version of the xeniumranger tool has been downloaded from https://www.10xgenomics.com/support/software/xenium-ranger/downloads +COPY xeniumranger-3.0.1.tar.gz /xeniumranger-3.0.1.tar.gz + +# install xenium ranger +RUN tar -xzvf /xeniumranger-3.0.1.tar.gz && \ + rm /xeniumranger-3.0.1.tar.gz + +# Set environment variables +# ENV PATH="xeniumranger-xenium3.0/bin:$PATH" + +# multistage to reduce image size +FROM ubuntu:20.04 + +# Set environment variables +ENV PATH="/xeniumranger-xenium3.0/bin:$PATH" + +# copy over xenium from builder +COPY --from=builder /xeniumranger-xenium3.0 /xeniumranger-xenium3.0/ diff --git a/modules/nf-core/xeniumranger/README.md b/modules/nf-core/xeniumranger/README.md new file mode 100644 index 00000000000..9bbe56b01c2 --- /dev/null +++ b/modules/nf-core/xeniumranger/README.md @@ -0,0 +1,20 @@ +# Updating the docker container and making a new module release + +Xenium Ranger is a commercial tool from 10X Genomics. The container provided for the xeniumranger nf-core module is not provided nor supported by 10x Genomics. Updating the Xenium Ranger versions in the container and pushing the update to Dockerhub needs to be done manually. + +1. Navigate to the appropriate download page. - [Xenium Ranger](https://www.10xgenomics.com/support/software/xenium-ranger/downloads): download the tar ball of the desired Xenium Ranger version with `curl` or `wget`. Place this file in the same folder where the Dockerfile lies. + +2. Edit the Dockerfile. Update the Xenium Ranger versions in this line: + (current version for xenium ranger is fixated at [3.0.1](https://www.10xgenomics.com/support/software/xenium-ranger/downloads) in the dockerfile) + +3. Create and test the container: + +```bash +docker build . -t quay.io/nf-core/xeniumranger: +``` + +4. Access rights are needed to push the container to the Dockerhub nfcore organization, please ask a core team member to do so. + +```bash +docker push quay.io/nf-core/xeniumranger: +``` diff --git a/modules/nf-core/xeniumranger/import-segmentation/main.nf b/modules/nf-core/xeniumranger/import-segmentation/main.nf new file mode 100644 index 00000000000..50b17272a16 --- /dev/null +++ b/modules/nf-core/xeniumranger/import-segmentation/main.nf @@ -0,0 +1,81 @@ +process XENIUMRANGER_IMPORT_SEGMENTATION { + tag "$meta.id" + label 'process_high' + + container "nf-core/xeniumranger:3.0.1" + + input: + tuple val(meta), path(xenium_bundle) + val(expansion_distance) + path(coordinate_transform) + path(nuclei) + path(cells) + path(transcript_assignment) + path(viz_polygons) + + output: + tuple val(meta), path("**/outs/**"), emit: outs + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_IMPORT-SEGMENTATION module does not support Conda. Please use Docker / Singularity / Podman instead." + } + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + // image based segmentation options + def expansion_distance = expansion_distance ? "--expansion-distance=\"${expansion_distance}\"": "" // expansion distance (default - 5, range - 0 - 100) + def coordinate_transform = coordinate_transform ? "--coordinate-transform=\"${coordinate_transform}\"": "" + + def nuclei_detection = nuclei ? "--nuclei=\"${nuclei}\"": "" + def cells = cells ? "--cells=\"${cells}\"": "" + + // transcript based segmentation + def transcript_assignment = transcript_assignment ? "--transcript-assignment=\"${transcript_assignment}\"": "" + def viz_polygons = viz_polygons ? "--viz-polygons=\"${viz_polygons}\"":"" + + // shared argument + def units = coordinate_transform ? "--units=microns": "--units=pixels" + + """ + xeniumranger import-segmentation \\ + --id="${prefix}" \\ + --xenium-bundle="${xenium_bundle}" \\ + --localcores=${task.cpus} \\ + --localmem=${task.memory.toGiga()} \\ + ${coordinate_transform} \\ + ${nuclei_detection} \\ + ${cells} \\ + ${expansion_distance} \\ + ${transcript_assignment} \\ + ${viz_polygons} \\ + ${units} \\ + ${args} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_IMPORT-SEGMENTATION 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" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/xeniumranger/import-segmentation/meta.yml b/modules/nf-core/xeniumranger/import-segmentation/meta.yml new file mode 100644 index 00000000000..c3a34ec5339 --- /dev/null +++ b/modules/nf-core/xeniumranger/import-segmentation/meta.yml @@ -0,0 +1,84 @@ +name: xeniumranger_import_segmentation +description: The xeniumranger import-segmentation module allows you to specify 2D + nuclei and/or cell segmentation results for assigning transcripts to cells and recalculate + all Xenium Onboard Analysis (XOA) outputs that depend on segmentation. Segmentation + results can be generated by community-developed tools or prior Xenium segmentation + result. +keywords: + - spatial + - segmentation + - import segmentation + - nuclear segmentation + - cell segmentation + - xeniumranger + - imaging +tools: + - xeniumranger: + description: | + Xenium Ranger is a set of analysis pipelines that process Xenium In Situ Gene Expression data to relabel, resegment, or import new segmentation results from community-developed tools. Xenium Ranger provides flexible off-instrument reanalysis of Xenium In Situ data. Relabel transcripts, resegment cells with the latest 10x segmentation algorithms, or import your own segmentation data to assign transcripts to cells. + homepage: "https://www.10xgenomics.com/support/software/xenium-ranger/latest" + documentation: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/getting-started" + tool_dev_url: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/analysis" + licence: + - "10x Genomics EULA" + identifier: "" + +input: + - - meta: + type: map + description: | + Groovy Map containing run information + e.g. [id:'xenium_bundle_path'] + - xenium_bundle: + type: directory + description: Path to the xenium output bundle generated by the Xenium Onboard + Analysis pipeline + - - expansion_distance: + type: integer + description: Nuclei boundary expansion distance in µm. Only for use when nucleus + segmentation provided as input. Default-5 (accepted range 0 - 100) + - - coordinate_transform: + type: file + description: Image alignment file containing similarity transform matrix e.g., + the _imagealignment.csv file exported from Xenium Explorer + - - nuclei: + type: file + description: | + Label mask (TIFF or NPY), polygons of nucleus segmentations (GeoJSON FeatureCollection), or Xenium Onboard Analysis cells.zarr.zip (the nucleus masks as input). + --nuclei will use nucleusGeometry polygon if it exists in the GeoJSON (i.e., for QuPath-like GeoJSON files), + or geometry if it does not. Error if --transcript-assignment argument is used. + - - cells: + type: file + description: | + Label mask (TIFF or NPY), polygons of cell segmentations (GeoJSON FeatureCollection), or Xenium Onboard Analysis cells.zarr.zip (the cell masks as input). + Features with a non-cell objectType will be ignored. Error if --transcript-assignment argument is used. + In Xenium Ranger v2.0, --nuclei no longer needs to be used with --cells. + - - transcript_assignment: + type: file + description: | + Transcript CSV with cell assignment from Baysor v0.6. Error if --cells or --nuclei arguments are used. + - - viz_polygons: + type: file + description: | + Cell boundary polygons (GeoJSON) for visualization from Baysor v0.6. Required if --transcript-assignment argument used. Error if --cells or --nuclei arguments used. +output: + - outs: + - meta: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics + documentation for a complete list + pattern: "${meta.id}/outs/*" + - "**/outs/**": + type: file + description: Files containing the outputs of xenium ranger, see official 10X + Genomics documentation for a complete list of outputs + pattern: "${meta.id}/outs/*" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test b/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test new file mode 100644 index 00000000000..54d3ba00209 --- /dev/null +++ b/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test @@ -0,0 +1,314 @@ +nextflow_process { + + name "Test Process XENIUMRANGER_IMPORT_SEGMENTATION" + script "../main.nf" + process "XENIUMRANGER_IMPORT_SEGMENTATION" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "xeniumranger" + tag "xeniumranger/import-segmentation" + tag "unzip" + + setup { + run("UNZIP") { + script "modules/nf-core/unzip/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialxe/Xenium_Prime_Mouse_Ileum_tiny_outs.zip', checkIfExists: true)] + """ + } + } + } + + test("xeniumranger import-segmentation nuclei npy") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_import-segmentation"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = 0 + input[2] = [] + input[3] = UNZIP.out.unzipped_archive.map { it[1] } + "/segmentations/nuclei.npy" + input[4] = [] + input[5] = [] + input[6] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'analysis_summary.html', + 'metrics_summary.csv', + 'cell_boundaries.csv.gz', + 'cell_boundaries.parquet', + 'nucleus_boundaries.csv.gz', + 'nucleus_boundaries.parquet', + 'cells.csv.gz', + 'cells.parquet', + 'cells.zarr.zip', + 'transcripts.parquet', + 'transcripts.zarr.zip', + 'clusters.csv', + 'differential_expression.csv', + 'components.csv', + 'projection.csv', + 'variance.csv', + 'analysis.zarr.zip', + 'experiment.xenium', + 'cell_feature_matrix.zarr.zip' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'clusters.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'differential_expression.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'components.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'projection.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'variance.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() }, + ) + } + } + + + test("xeniumranger import-segmentation nuclei tif") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_import-segmentation"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = 0 + input[2] = [] + input[3] = UNZIP.out.unzipped_archive.map { it[1] } + "/segmentations/nuclei.npy" + input[4] = [] + input[5] = [] + input[6] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'analysis_summary.html', + 'metrics_summary.csv', + 'cell_boundaries.csv.gz', + 'cell_boundaries.parquet', + 'nucleus_boundaries.csv.gz', + 'nucleus_boundaries.parquet', + 'cells.csv.gz', + 'cells.parquet', + 'cells.zarr.zip', + 'transcripts.parquet', + 'transcripts.zarr.zip', + 'clusters.csv', + 'differential_expression.csv', + 'components.csv', + 'projection.csv', + 'variance.csv', + 'analysis.zarr.zip', + 'experiment.xenium', + 'cell_feature_matrix.zarr.zip' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'clusters.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'differential_expression.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'components.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'projection.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'variance.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() }, + ) + } + } + + test("xeniumranger import-segmentation segmentation csv") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_import-segmentation"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = 0 + input[2] = UNZIP.out.unzipped_archive.map { it[1] } + "/segmentations/imagealignment.csv" + input[3] = [] + input[4] = [] + input[5] = UNZIP.out.unzipped_archive.map { it[1] } + "/segmentations/segmentation.csv" + input[6] = UNZIP.out.unzipped_archive.map { it[1] } + "/segmentations/segmentation_polygons.json" + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'analysis_summary.html', + 'metrics_summary.csv', + 'cell_boundaries.csv.gz', + 'cell_boundaries.parquet', + 'nucleus_boundaries.csv.gz', + 'nucleus_boundaries.parquet', + 'cells.csv.gz', + 'cells.parquet', + 'cells.zarr.zip', + 'transcripts.parquet', + 'transcripts.zarr.zip', + 'clusters.csv', + 'differential_expression.csv', + 'components.csv', + 'projection.csv', + 'variance.csv', + 'analysis.zarr.zip', + 'experiment.xenium', + 'cell_feature_matrix.zarr.zip' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'clusters.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'differential_expression.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'components.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'projection.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'variance.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() }, + ) + } + } + + test("xeniumranger import-segmentation") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_import-segmentation"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = 0 + input[2] = [] + input[3] = UNZIP.out.unzipped_archive.map { it[1] } + "/cells.zarr.zip" + input[4] = [] + input[5] = [] + input[6] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'analysis_summary.html', + 'metrics_summary.csv', + 'cell_boundaries.csv.gz', + 'cell_boundaries.parquet', + 'nucleus_boundaries.csv.gz', + 'nucleus_boundaries.parquet', + 'cells.csv.gz', + 'cells.parquet', + 'cells.zarr.zip', + 'transcripts.parquet', + 'transcripts.zarr.zip', + 'clusters.csv', + 'differential_expression.csv', + 'components.csv', + 'projection.csv', + 'variance.csv', + 'analysis.zarr.zip', + 'experiment.xenium', + 'cell_feature_matrix.zarr.zip' + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'nucleus_boundaries.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'clusters.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'differential_expression.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'components.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'projection.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'variance.csv' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() }, + ) + } + } + + test("xeniumranger import-segmentation stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_import-segmentation"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = 0 + input[2] = [] + input[3] = UNZIP.out.unzipped_archive.map { it[1] } + "/cells.zarr.zip" + input[4] = [] + input[5] = [] + input[6] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test.snap b/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test.snap new file mode 100644 index 00000000000..1c312ae0f3b --- /dev/null +++ b/modules/nf-core/xeniumranger/import-segmentation/tests/main.nf.test.snap @@ -0,0 +1,127 @@ +{ + "xeniumranger import-segmentation": { + "content": [ + [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ], + [ + "dispersion.csv:md5,e8b1abb880ece8fb730ce34a15f958b4", + "features_selected.csv:md5,c5e32d69f001f938ed316d2108a21e00", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "barcodes.tsv.gz:md5,04ea06796d6b28517c288904ca043582", + "features.tsv.gz:md5,7862242129681900a9cc4086dc83b62e", + "matrix.mtx.gz:md5,489f86fbd8d65d6b973bb9cc7c5a76f1", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T00:13:13.575888" + }, + "xeniumranger import-segmentation nuclei npy": { + "content": [ + [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ], + [ + "dispersion.csv:md5,e8b1abb880ece8fb730ce34a15f958b4", + "features_selected.csv:md5,c5e32d69f001f938ed316d2108a21e00", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "barcodes.tsv.gz:md5,04ea06796d6b28517c288904ca043582", + "features.tsv.gz:md5,7862242129681900a9cc4086dc83b62e", + "matrix.mtx.gz:md5,489f86fbd8d65d6b973bb9cc7c5a76f1", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-29T23:03:26.726334" + }, + "xeniumranger import-segmentation segmentation csv": { + "content": [ + [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ], + [ + "dispersion.csv:md5,e8b1abb880ece8fb730ce34a15f958b4", + "features_selected.csv:md5,c5e32d69f001f938ed316d2108a21e00", + "cell_feature_matrix.h5:md5,5d74ea595561e0300b6c3e5ec8d06fff", + "barcodes.tsv.gz:md5,97496a9b448d9380cff0575b8e7a6f57", + "features.tsv.gz:md5,7862242129681900a9cc4086dc83b62e", + "matrix.mtx.gz:md5,f93ed82a2a74c154392fc6237642f1d2", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-29T23:22:58.158857" + }, + "xeniumranger import-segmentation stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_xeniumranger_import-segmentation" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ], + "outs": [ + [ + { + "id": "test_xeniumranger_import-segmentation" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T22:49:39.204133" + }, + "xeniumranger import-segmentation nuclei tif": { + "content": [ + [ + "versions.yml:md5,d76e870d71abf94ed9ae972a08b83f63" + ], + [ + "dispersion.csv:md5,e8b1abb880ece8fb730ce34a15f958b4", + "features_selected.csv:md5,c5e32d69f001f938ed316d2108a21e00", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "barcodes.tsv.gz:md5,04ea06796d6b28517c288904ca043582", + "features.tsv.gz:md5,7862242129681900a9cc4086dc83b62e", + "matrix.mtx.gz:md5,489f86fbd8d65d6b973bb9cc7c5a76f1", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-29T23:11:37.18721" + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/import-segmentation/tests/nextflow.config b/modules/nf-core/xeniumranger/import-segmentation/tests/nextflow.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/nf-core/xeniumranger/import-segmentation/tests/tags.yml b/modules/nf-core/xeniumranger/import-segmentation/tests/tags.yml new file mode 100644 index 00000000000..90c2b80563c --- /dev/null +++ b/modules/nf-core/xeniumranger/import-segmentation/tests/tags.yml @@ -0,0 +1,2 @@ +xeniumranger/import-segmentation: + - "modules/nf-core/xeniumranger/import-segmentation/**" diff --git a/modules/nf-core/xeniumranger/relabel/main.nf b/modules/nf-core/xeniumranger/relabel/main.nf new file mode 100644 index 00000000000..b06fe9ea13e --- /dev/null +++ b/modules/nf-core/xeniumranger/relabel/main.nf @@ -0,0 +1,56 @@ +process XENIUMRANGER_RELABEL { + tag "$meta.id" + label 'process_high' + + container "nf-core/xeniumranger:3.0.1" + + input: + tuple val(meta), path(xenium_bundle) + path(gene_panel) + + output: + tuple val(meta), path("**/outs/**"), emit: outs + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RELABEL module does not support Conda. Please use Docker / Singularity / Podman instead." + } + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + xeniumranger relabel \\ + --id="${prefix}" \\ + --xenium-bundle="${xenium_bundle}" \\ + --panel="${gene_panel}" \\ + --localcores=${task.cpus} \\ + --localmem=${task.memory.toGiga()} \\ + ${args} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RELABEL 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" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/xeniumranger/relabel/meta.yml b/modules/nf-core/xeniumranger/relabel/meta.yml new file mode 100644 index 00000000000..85c1dbfa3a1 --- /dev/null +++ b/modules/nf-core/xeniumranger/relabel/meta.yml @@ -0,0 +1,53 @@ +name: xeniumranger_relabel +description: The xeniumranger relabel module allows you to change the gene labels + applied to decoded transcripts. +keywords: + - spatial + - relabel + - gene labels + - transcripts + - xeniumranger +tools: + - xeniumranger: + description: | + Xenium Ranger is a set of analysis pipelines that process Xenium In Situ Gene Expression data to relabel, resegment, or import new segmentation results from community-developed tools. Xenium Ranger provides flexible off-instrument reanalysis of Xenium In Situ data. Relabel transcripts, resegment cells with the latest 10x segmentation algorithms, or import your own segmentation data to assign transcripts to cells. + homepage: "https://www.10xgenomics.com/support/software/xenium-ranger/latest" + documentation: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/getting-started" + tool_dev_url: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/analysis" + licence: + - 10x Genomics EULA + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing run information + e.g. [id:'xenium_bundle_path'] + - xenium_bundle: + type: directory + description: Path to the xenium output bundle generated by the Xenium Onboard + Analysis pipeline + - - gene_panel: + type: file + description: Gene panel JSON file to use for relabeling decoded transcripts +output: + - outs: + - meta: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics + documentation for a complete list + pattern: "${meta.id}/outs/*" + - "**/outs/**": + type: file + description: Files containing the outputs of xenium ranger, see official 10X + Genomics documentation for a complete list of outputs + pattern: "${meta.id}/outs/*" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/xeniumranger/relabel/tests/main.nf.test b/modules/nf-core/xeniumranger/relabel/tests/main.nf.test new file mode 100644 index 00000000000..d09627386c3 --- /dev/null +++ b/modules/nf-core/xeniumranger/relabel/tests/main.nf.test @@ -0,0 +1,93 @@ +nextflow_process { + + name "Test Process XENIUMRANGER_RELABEL" + script "../main.nf" + process "XENIUMRANGER_RELABEL" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "xeniumranger" + tag "xeniumranger/relabel" + tag "unzip" + + setup { + run("UNZIP") { + script "modules/nf-core/unzip/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialxe/Xenium_Prime_Mouse_Ileum_tiny_outs.zip', checkIfExists: true)] + """ + } + } + } + + test("xeniumranger relabel") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_relabel"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = UNZIP.out.unzipped_archive.map { it[1] } + "/gene_panel.json" + """ + } + } + then { + assertAll( + { assert process.success }, + { assert process.out.outs != null }, + { + assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + "analysis.zarr.zip", + "experiment.xenium", + "transcripts.zarr.zip", + "analysis_summary.html", + "cell_feature_matrix.zarr.zip", + "differential_expression.csv", + "components.csv", + "projection.csv", + "variance.csv", + "metrics_summary.csv", + "clusters.csv" + ]} + ).match() + }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.zarr.zip' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'differential_expression.csv' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'components.csv' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'projection.csv' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'variance.csv' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'clusters.csv' }).exists() }, + ) + } + } + + test("xeniumranger relabel stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_relabel"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = UNZIP.out.unzipped_archive.map { it[1] } + "/gene_panel.json" + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/relabel/tests/main.nf.test.snap b/modules/nf-core/xeniumranger/relabel/tests/main.nf.test.snap new file mode 100644 index 00000000000..7c70cfc28fa --- /dev/null +++ b/modules/nf-core/xeniumranger/relabel/tests/main.nf.test.snap @@ -0,0 +1,66 @@ +{ + "xeniumranger relabel": { + "content": [ + [ + "versions.yml:md5,ab2584177544560d5a9e9c36f7d24354" + ], + [ + "dispersion.csv:md5,e8b1abb880ece8fb730ce34a15f958b4", + "features_selected.csv:md5,c5e32d69f001f938ed316d2108a21e00", + "cell_boundaries.csv.gz:md5,8b4f2aa455a6fb14b2669a42db32ea7e", + "cell_boundaries.parquet:md5,e55d6a7fbec336103994baad8c8e4a9a", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "barcodes.tsv.gz:md5,04ea06796d6b28517c288904ca043582", + "features.tsv.gz:md5,7862242129681900a9cc4086dc83b62e", + "matrix.mtx.gz:md5,489f86fbd8d65d6b973bb9cc7c5a76f1", + "cells.csv.gz:md5,3cef2d7cc8cfba1d47bdb7c65c3d5d5f", + "cells.parquet:md5,9b30b35ab961d2d243a1426e8dc980fe", + "cells.zarr.zip:md5,556e47d5b14150239b10b2f801defa2b", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98", + "nucleus_boundaries.csv.gz:md5,e417b6e293298870956d42c7106cbd0c", + "nucleus_boundaries.parquet:md5,bacbfc3c2e956d899e1d8ccba5dd7c5e", + "transcripts.parquet:md5,c0f40d5c61b87404bc9efb84ff0563a8" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-29T21:06:09.082129" + }, + "xeniumranger relabel stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_xeniumranger_relabel" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,ab2584177544560d5a9e9c36f7d24354" + ], + "outs": [ + [ + { + "id": "test_xeniumranger_relabel" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,ab2584177544560d5a9e9c36f7d24354" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-22T15:22:34.353444" + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/relabel/tests/nextflow.config b/modules/nf-core/xeniumranger/relabel/tests/nextflow.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/nf-core/xeniumranger/relabel/tests/tags.yml b/modules/nf-core/xeniumranger/relabel/tests/tags.yml new file mode 100644 index 00000000000..1cb37a80de0 --- /dev/null +++ b/modules/nf-core/xeniumranger/relabel/tests/tags.yml @@ -0,0 +1,2 @@ +xeniumranger/relabel: + - "modules/nf-core/xeniumranger/relabel/**" diff --git a/modules/nf-core/xeniumranger/rename/main.nf b/modules/nf-core/xeniumranger/rename/main.nf new file mode 100644 index 00000000000..d273caf3276 --- /dev/null +++ b/modules/nf-core/xeniumranger/rename/main.nf @@ -0,0 +1,58 @@ +process XENIUMRANGER_RENAME { + tag "$meta.id" + label 'process_high' + + container "nf-core/xeniumranger:3.0.1" + + input: + tuple val(meta), path(xenium_bundle) + val(region_name) + val(cassette_name) + + output: + tuple val(meta), path("**/outs/**"), emit: outs + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RENAME module does not support Conda. Please use Docker / Singularity / Podman instead." + } + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + xeniumranger rename \\ + --id="${prefix}" \\ + --xenium-bundle="${xenium_bundle}" \\ + --region-name="${region_name}" \\ + --cassette-name="${cassette_name}" \\ + --localcores=${task.cpus} \\ + --localmem=${task.memory.toGiga()} \\ + ${args} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RENAME 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" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/xeniumranger/rename/meta.yml b/modules/nf-core/xeniumranger/rename/meta.yml new file mode 100644 index 00000000000..d7842b007a7 --- /dev/null +++ b/modules/nf-core/xeniumranger/rename/meta.yml @@ -0,0 +1,55 @@ +name: xeniumranger_rename +description: The xeniumranger rename module allows you to change the sample region_name + and cassette_name throughout all the Xenium Onboard Analysis output files that contain + this information. +keywords: + - spatial + - rename + - gene labels + - transcripts + - xeniumranger +tools: + - xeniumranger: + description: | + Xenium Ranger is a set of analysis pipelines that process Xenium In Situ Gene Expression data to relabel, resegment, or import new segmentation results from community-developed tools. Xenium Ranger provides flexible off-instrument reanalysis of Xenium In Situ data. Relabel transcripts, resegment cells with the latest 10x segmentation algorithms, or import your own segmentation data to assign transcripts to cells. + homepage: "https://www.10xgenomics.com/support/software/xenium-ranger/latest" + documentation: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/getting-started" + tool_dev_url: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/analysis" + licence: + - "10x Genomics EULA" + identifier: "" +input: + - - meta: + type: map + description: Groovy Map containing sample information e.g. [ id:'test' ] + - xenium_bundle: + type: directory + description: Path to the xenium output bundle generated by the Xenium Onboard + Analysis pipeline + - - region_name: + type: string + description: New region name + - - cassette_name: + type: string + description: New cassette name +output: + - outs: + - meta: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics + documentation for a complete list + pattern: "${meta.id}/outs/*" + - "**/outs/**": + type: file + description: Files containing the outputs of xenium ranger, see official 10X + Genomics documentation for a complete list of outputs + pattern: "${meta.id}/outs/*" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/xeniumranger/rename/tests/main.nf.test b/modules/nf-core/xeniumranger/rename/tests/main.nf.test new file mode 100644 index 00000000000..3819a310d19 --- /dev/null +++ b/modules/nf-core/xeniumranger/rename/tests/main.nf.test @@ -0,0 +1,77 @@ +nextflow_process { + + name "Test Process XENIUMRANGER_RENAME" + script "../main.nf" + process "XENIUMRANGER_RENAME" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "xeniumranger" + tag "xeniumranger/rename" + tag "unzip" + + setup { + run("UNZIP") { + script "modules/nf-core/unzip/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialxe/Xenium_Prime_Mouse_Ileum_tiny_outs.zip', checkIfExists: true)] + """ + } + } + } + + test("xeniumranger rename") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_rename"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = "test_region" + input[2] = "test_cassette" + """ + } + } + then { + assertAll( + { assert process.success }, + { assert process.out.outs != null }, + { + assert snapshot( + process.out.versions, + process.out.outs.get(0).get(1).findAll { file(it).name !in [ + 'analysis_summary.html', + 'experiment.xenium', + ]} + ).match() + }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'experiment.xenium' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'metrics_summary.csv' }).exists() } + ) + } + } + + test("xeniumranger rename stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_rename"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = "test_region" + input[2] = "test_cassette" + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/rename/tests/main.nf.test.snap b/modules/nf-core/xeniumranger/rename/tests/main.nf.test.snap new file mode 100644 index 00000000000..ea57ab7e0fd --- /dev/null +++ b/modules/nf-core/xeniumranger/rename/tests/main.nf.test.snap @@ -0,0 +1,65 @@ +{ + "xeniumranger rename stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_xeniumranger_rename" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,823917e7bc8f27cf314ef477fe7369eb" + ], + "outs": [ + [ + { + "id": "test_xeniumranger_rename" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,823917e7bc8f27cf314ef477fe7369eb" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T23:10:43.918492" + }, + "xeniumranger rename": { + "content": [ + [ + "versions.yml:md5,823917e7bc8f27cf314ef477fe7369eb" + ], + [ + "analysis.zarr.zip:md5,1ffb1b86586fe6c80ce1676b79137785", + "cell_boundaries.csv.gz:md5,8b4f2aa455a6fb14b2669a42db32ea7e", + "cell_boundaries.parquet:md5,e55d6a7fbec336103994baad8c8e4a9a", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "cell_feature_matrix.zarr.zip:md5,36f45a290cf4ee1232f2d1cd0fdbd820", + "cells.csv.gz:md5,3cef2d7cc8cfba1d47bdb7c65c3d5d5f", + "cells.parquet:md5,e1450c7eca3d7ce0d4911c95042b1303", + "cells.zarr.zip:md5,556e47d5b14150239b10b2f801defa2b", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "metrics_summary.csv:md5,54ad3944eb3ba6a4d7bda01bc2a6bb1c", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98", + "nucleus_boundaries.csv.gz:md5,e417b6e293298870956d42c7106cbd0c", + "nucleus_boundaries.parquet:md5,bacbfc3c2e956d899e1d8ccba5dd7c5e", + "transcripts.parquet:md5,203cb05ee7689805cc505ebda9557551", + "transcripts.zarr.zip:md5,807e63a2ef8340e085cd899507f45395" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T23:10:34.499597" + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/rename/tests/nextflow.config b/modules/nf-core/xeniumranger/rename/tests/nextflow.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/nf-core/xeniumranger/rename/tests/tags.yml b/modules/nf-core/xeniumranger/rename/tests/tags.yml new file mode 100644 index 00000000000..1f9026ac618 --- /dev/null +++ b/modules/nf-core/xeniumranger/rename/tests/tags.yml @@ -0,0 +1,2 @@ +xeniumranger/rename: + - "modules/nf-core/xeniumranger/rename/**" diff --git a/modules/nf-core/xeniumranger/resegment/main.nf b/modules/nf-core/xeniumranger/resegment/main.nf new file mode 100644 index 00000000000..5d28fa698a7 --- /dev/null +++ b/modules/nf-core/xeniumranger/resegment/main.nf @@ -0,0 +1,70 @@ +process XENIUMRANGER_RESEGMENT { + tag "$meta.id" + label 'process_high' + + container "nf-core/xeniumranger:3.0.1" + + input: + tuple val(meta), path(xenium_bundle) + val(expansion_distance) + val(dapi_filter) + val(boundary_stain) + val(interior_stain) + + output: + tuple val(meta), path("**/outs/**"), emit: outs + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RESEGMENT module does not support Conda. Please use Docker / Singularity / Podman instead." + } + def args = task.ext.args ?: "" + def prefix = task.ext.prefix ?: "${meta.id}" + + def expansion_distance = expansion_distance ? "--expansion-distance=\"${expansion_distance}\"": "" + def dapi_filter = dapi_filter ? "--dapi-filter=\"${dapi_filter}\"": "" + + // Do not use boundary stain in analysis, but keep default interior stain and DAPI + def boundary_stain = boundary_stain ? "--boundary-stain=disable": "" + // Do not use interior stain in analysis, but keep default boundary stain and DAPI + def interior_stain = interior_stain ? "--interior-stain=disable": "" + + """ + xeniumranger resegment \\ + --id="${prefix}" \\ + --xenium-bundle="${xenium_bundle}" \\ + ${expansion_distance} \\ + ${dapi_filter} \\ + ${boundary_stain} \\ + ${interior_stain} \\ + --localcores=${task.cpus} \\ + --localmem=${task.memory.toGiga()} \\ + ${args} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error "XENIUMRANGER_RESEGMENT 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" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + xeniumranger: \$(xeniumranger -V | sed -e "s/xeniumranger-/- /g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/xeniumranger/resegment/meta.yml b/modules/nf-core/xeniumranger/resegment/meta.yml new file mode 100644 index 00000000000..af0e88826b0 --- /dev/null +++ b/modules/nf-core/xeniumranger/resegment/meta.yml @@ -0,0 +1,58 @@ +name: xeniumranger_resegment +description: The xeniumranger resegment module allows you to generate a new segmentation of the morphology image space by rerunning the Xenium Onboard Analysis (XOA) segmentation algorithms with modified parameters. +keywords: + - spatial + - resegment + - morphology + - segmentation + - xeniumranger +tools: + - xeniumranger: + description: | + Xenium Ranger is a set of analysis pipelines that process Xenium In Situ Gene Expression data to relabel, resegment, or import new segmentation results from community-developed tools. Xenium Ranger provides flexible off-instrument reanalysis of Xenium In Situ data. Relabel transcripts, resegment cells with the latest 10x segmentation algorithms, or import your own segmentation data to assign transcripts to cells. + homepage: "https://www.10xgenomics.com/support/software/xenium-ranger/latest" + documentation: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/getting-started" + tool_dev_url: "https://www.10xgenomics.com/support/software/xenium-ranger/latest/analysis" + licence: + - "10x Genomics EULA" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing run information + e.g. [ id:'xenium_experiment' ] + - xenium_bundle: + type: directory + description: Path to the xenium output bundle generated by the Xenium Onboard Analysis pipeline + - - expansion_distance: + type: integer + description: Nuclei boundary expansion distance in µm. Only for use when nucleus segmentation provided as input. Default-5 (accepted range 0 - 100) + - - dapi_filter: + type: integer + description: Minimum intensity in photoelectrons to filter nuclei default-100 range of values is 0 to 99th percentile of image stack or 1000, whichever is larger + - - boundary_stain: + type: string + description: Specify the name of the boundary stain to use or disable possible options are default-ATP1A1/CD45/E-Cadherin or disable + - - interior_stain: + type: string + description: Specify the name of the interior stain to use or disable possible options are default-18S or disable +output: + - outs: + - meta: + type: file + description: Files containing the outputs of Cell Ranger, see official 10X Genomics documentation for a complete list + pattern: "${meta.id}/outs/*" + - "**/outs/**": + type: file + description: Files containing the outputs of xenium ranger, see official 10X Genomics documentation for a complete list of outputs + pattern: "${meta.id}/outs/*" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/xeniumranger/resegment/tests/main.nf.test b/modules/nf-core/xeniumranger/resegment/tests/main.nf.test new file mode 100644 index 00000000000..861c2414565 --- /dev/null +++ b/modules/nf-core/xeniumranger/resegment/tests/main.nf.test @@ -0,0 +1,77 @@ +nextflow_process { + + name "Test Process XENIUMRANGER_RESEGMENT" + script "../main.nf" + process "XENIUMRANGER_RESEGMENT" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "xeniumranger" + tag "xeniumranger/resegment" + tag "unzip" + + setup { + run("UNZIP") { + script "modules/nf-core/unzip/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialxe/Xenium_Prime_Mouse_Ileum_tiny_outs.zip', checkIfExists: true)] + """ + } + } + } + + test("xeniumranger resegment") { + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_resegment"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = [] + input[2] = [] + input[3] = [] + input[4] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert process.out.outs != null }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis_summary.html' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.csv.gz' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'cells.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.parquet' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'transcripts.zarr.zip' }).exists() }, + { assert file(process.out.outs.get(0).get(1).find { file(it).name == 'analysis.zarr.zip' }).exists() }, + { assert path(process.out.outs.get(0).get(1).find { file(it).name == 'cell_feature_matrix.zarr.zip' }).exists() } + ) + } + } + + test("xeniumranger resegment stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [id: "test_xeniumranger_resegment"], + ]).combine(UNZIP.out.unzipped_archive.map { it[1] }) + input[1] = [] + input[2] = [] + input[3] = [] + input[4] = [] + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/resegment/tests/main.nf.test.snap b/modules/nf-core/xeniumranger/resegment/tests/main.nf.test.snap new file mode 100644 index 00000000000..16f94c14ebf --- /dev/null +++ b/modules/nf-core/xeniumranger/resegment/tests/main.nf.test.snap @@ -0,0 +1,35 @@ +{ + "xeniumranger resegment stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_xeniumranger_resegment" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,4671141281357e0ce26d9cb35fed23a8" + ], + "outs": [ + [ + { + "id": "test_xeniumranger_resegment" + }, + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,4671141281357e0ce26d9cb35fed23a8" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T23:22:35.438329" + } +} \ No newline at end of file diff --git a/modules/nf-core/xeniumranger/resegment/tests/nextflow.config b/modules/nf-core/xeniumranger/resegment/tests/nextflow.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/nf-core/xeniumranger/resegment/tests/tags.yml b/modules/nf-core/xeniumranger/resegment/tests/tags.yml new file mode 100644 index 00000000000..99f47c82ae9 --- /dev/null +++ b/modules/nf-core/xeniumranger/resegment/tests/tags.yml @@ -0,0 +1,2 @@ +xeniumranger/resegment: + - "modules/nf-core/xeniumranger/resegment/**"