-
Notifications
You must be signed in to change notification settings - Fork 0
A draft of a simple module to discuss nf-core
#1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 16 commits
8efeb53
de529da
822a9b8
0891679
602d7e5
0948d95
460e82e
a10b328
f18c87f
fb2cfd5
24213a8
fa36589
292f451
1ddd9fe
0634482
80c5f4c
4e34040
9c207ec
e37e4c4
5ed50e3
77ac7ce
992558b
7353d82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| FROM python:3.11-slim | ||
|
|
||
|
|
||
| # non-interactive mode | ||
| ENV DEBIAN_FRONTEND=noninteractive | ||
| ENV VENV_PATH="/env" | ||
| ENV PATH="${VENV_PATH}/bin:$PATH" | ||
|
|
||
|
|
||
| # Update and install system dependencies | ||
| RUN apt-get update && apt-get install -y \ | ||
| git \ | ||
| build-essential \ | ||
| procps \ | ||
| && apt-get clean \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install uv package manager | ||
| RUN pip install uv | ||
|
|
||
| # Create a new environment with uv and install packages | ||
| RUN uv venv "${VENV_PATH}" && \ | ||
| uv pip install --no-cache-dir \ | ||
| scanpy \ | ||
| numpy \ | ||
| pandas \ | ||
| muon \ | ||
| scikit-network \ | ||
| jupyterlab \ | ||
| notebook \ | ||
| tqdm \ | ||
| ipywidgets \ | ||
| papermill | ||
|
|
||
| # Copy Dockerfile to the container | ||
| COPY Dockerfile /docker/ | ||
| RUN chmod -R 755 /docker |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
|
|
||
|
|
||
| process ANNDATAUTILS_TOH5AD { | ||
| tag "Converting ${sample_id}'s file to .h5ad" | ||
| container 'docker://quay.io/cellgeni/metacells-python:latest' | ||
|
|
||
| input: | ||
| tuple val(sample_id), path(input, name: 'input/*') | ||
| val(delimiter) | ||
| output: | ||
| tuple val(sample_id), path("${sample_id}.h5ad"), emit: 'h5ad' | ||
| path "versions.yml", emit: 'versions' | ||
| script: | ||
| """ | ||
| convert_to_h5ad.py \ | ||
| --input "${input}" \ | ||
| --sample_id "${sample_id}" \ | ||
| --delimiter "${delimiter}" \ | ||
| --output "${sample_id}.h5ad" | ||
|
|
||
| cat <<-END_VERSIONS > versions.yml | ||
| "${task.process}": | ||
| anndata: \$( python -c "import anndata; print(anndata.__version__)" ) | ||
| scanpy: \$( python -c "import scanpy; print(scanpy.__version__)" ) | ||
| END_VERSIONS | ||
| """ | ||
| stub: | ||
| """ | ||
| touch "${sample_id}.h5ad" | ||
| touch versions.yml | ||
| """ | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| --- | ||
| # yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json | ||
| name: "anndatautils_toh5ad" | ||
| description: A module to convert AnnData convertable files to AnnData object and save it as .h5ad file | ||
|
Claptar marked this conversation as resolved.
Outdated
|
||
| keywords: | ||
| - anndata | ||
| - h5ad | ||
| - conversion | ||
|
|
||
| input: | ||
| - - sample_id: | ||
| type: string | ||
| description: | | ||
| Sample ID to be used as obs_names in the AnnData object | ||
| - - input: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be a single dash. Otherwise, it means
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix dashes, thank you! Well, input can be either file or directory, so I struggled to come up with better name |
||
| type: [ "file" , "directory" ] | ||
| description: | | ||
| Input files to be converted to AnnData object. The first element of the tuple is the sample ID, and the second element is the file path. | ||
| - - delimiter: | ||
| type: string | ||
| description: | | ||
| Delimiter to be used to create obs_names in the format sample_id + delimiter + barcode | ||
|
|
||
| output: | ||
| - output: | ||
| - "*.h5ad": | ||
| type: file | ||
| description: .h5ad file containing the AnnData object | ||
| pattern: "*.h5ad" | ||
|
|
||
|
|
||
| authors: | ||
| - "@claptar" | ||
| maintainers: | ||
| - "@claptar" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| process { | ||
| withName: "*ANNDATAUTILS_TOH5AD" { | ||
| queue = 'normal' | ||
| cpus = 4 | ||
| memory = { 16.GB + 32.GB * (task.attempt - 1) } | ||
| publishDir = [ | ||
| mode : params.publish_mode, | ||
| path : "${outputDir}/adata/raw", | ||
| overwrite: true | ||
| ] | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+12
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import os | ||
| import sys | ||
| import logging | ||
| import argparse | ||
| import scanpy as sc | ||
|
|
||
|
|
||
| # Set up logging | ||
| logging.basicConfig( | ||
| level=logging.INFO, | ||
| format="%(levelname)s: %(message)s", | ||
| stream=sys.stdout, # Direct output to stdout instead of a file | ||
| ) | ||
|
|
||
|
|
||
| def init_parser() -> argparse.ArgumentParser: | ||
| """ | ||
| Initialise argument parser for the script | ||
| """ | ||
| parser = argparse.ArgumentParser( | ||
| description="Script validates sample and annotation tables and splits annotation table into separate celltypes" | ||
| ) | ||
| parser.add_argument( | ||
| "--input", | ||
| type=str, | ||
| metavar="<path>", | ||
| help="Specify a path to input file", | ||
| required=True, | ||
| ) | ||
| parser.add_argument( | ||
| "--sample_id", | ||
| type=str, | ||
| metavar="<str>", | ||
| default=None, | ||
| help="Specify sample name for the file", | ||
| ) | ||
| parser.add_argument( | ||
| "--output", | ||
| metavar="<path>", | ||
| type=str, | ||
| help="Specify a path to output .h5ad file", | ||
| default=10, | ||
| ) | ||
| parser.add_argument( | ||
| "--delimiter", | ||
| type=str, | ||
| metavar="<str>", | ||
| default=None, | ||
| help="Specify sample delimiter", | ||
| ) | ||
|
|
||
| return parser | ||
|
|
||
|
|
||
| def check_10x_mtx_files(directory: str) -> bool: | ||
| """ | ||
| Check if the directory contains the required 10x mtx files | ||
| """ | ||
| required_files = ["matrix.mtx", "barcodes.tsv", "features.tsv"] | ||
| for file in required_files: | ||
| filepath = os.path.join(directory, file) | ||
| if not os.path.isfile(filepath) and not os.path.isfile(filepath + ".gz"): | ||
| logging.error(f"Missing required file: {file}") | ||
| return False | ||
| return True | ||
|
|
||
|
|
||
| def main() -> None: | ||
| # Parse arguments | ||
| parser = init_parser() | ||
| args = parser.parse_args() | ||
|
|
||
| # Check if input is directory | ||
| if os.path.isdir(args.input): | ||
| # return error if any of the required files are missing | ||
| if not check_10x_mtx_files(args.input): | ||
| raise FileNotFoundError( | ||
| "The specified directory does not contain the required files: matrix.mtx, barcodes.tsv, and features.tsv" | ||
| ) | ||
|
|
||
| # load 10x mtx file | ||
| logging.info("Loading 10x mtx file to AnnData object") | ||
| adata = sc.read_10x_mtx( | ||
| args.input, | ||
| var_names="gene_symbols", | ||
| gex_only=True, | ||
| ) | ||
| else: | ||
| # get file extension | ||
| _, extension = os.path.splitext(args.input) | ||
|
|
||
| # read file based on extension using case match | ||
| match extension: | ||
| case ".h5": | ||
| logging.info("Loading .h5 file to AnnData object") | ||
| adata = sc.read_10x_h5(args.input, gex_only=True) | ||
| case ".mtx": | ||
| logging.info("Loading .mtx file to AnnData object") | ||
| adata = sc.read_mtx(args.input).T | ||
| case ".zarr": | ||
| logging.info("Loading .zarr file to AnnData object") | ||
| raise NotImplementedError( | ||
| "Loading .zarr file is not implemented yet. Please provide a path to .h5 file or .mtx file." | ||
| ) | ||
| case _: | ||
| raise ValueError( | ||
| "Unsupported file format. Please provide a path to .h5 file, .mtx file or .mtx file directory." | ||
| ) | ||
|
|
||
| # Add sample name to obs | ||
| logging.info("Adding sample name to obs") | ||
| adata.obs["sample"] = args.sample_id | ||
|
|
||
| # Add delimiter to obs index if specified | ||
| if args.delimiter: | ||
| logging.info("Adding delimiter to obs index") | ||
| adata.obs["barcode"] = adata.obs.index | ||
| adata.obs.index = adata.obs["barcode"] + args.delimiter + adata.obs["sample"] | ||
| adata.obs.index.name = "barcode_sample" | ||
|
|
||
| # Save adata abject | ||
| logging.info("Saving AnnData object to .h5ad file") | ||
| adata.write_h5ad(args.output) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| nextflow_process { | ||
|
|
||
| name "Test Process ANNDATAUTILS_TOH5AD" | ||
| script "../main.nf" | ||
| process "ANNDATAUTILS_TOH5AD" | ||
|
|
||
| tag "modules" | ||
| tag "modules_sanger" | ||
| tag "anndatautils" | ||
| tag "anndatautils/toh5ad" | ||
|
|
||
| // TODO nf-core: Change the test name preferably indicating the test-data and file-format used | ||
| test("Conversion of .h5 file to .h5ad") { | ||
| when { | ||
| process { | ||
| """ | ||
| input[0] = ['pbmc_1k_h5', file(params.test_data_base + "anndatautils/pbmc_1k.h5")] | ||
| input[1] = "___" | ||
| """ | ||
| } | ||
| } | ||
|
|
||
| then { | ||
| // basec run health | ||
| assert process.success | ||
| assert process.exitStatus == 0 | ||
|
|
||
| // h5ad files | ||
| assert process.out.h5ad | ||
| assert process.out.h5ad.get(0).get(0) ==~ "pbmc_1k_h5" | ||
| assert process.out.h5ad.get(0).get(1) ==~ ".*/pbmc_1k_h5.h5ad" | ||
|
|
||
| // versions.yml | ||
| assert process.out.versions | ||
| assert process.out.versions.get(0) ==~ ".*/versions.yml" | ||
|
|
||
| // Snapshot all output channels of a process | ||
| assert snapshot(process.out).match() | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
| test("Conversion of 10x .mtx file to .h5ad") { | ||
| when { | ||
| process { | ||
| """ | ||
| input[0] = ['pbmc_1k_mtx', file(params.test_data_base + "anndatautils/raw_feature_bc_matrix")] | ||
| input[1] = "___" | ||
| """ | ||
| } | ||
| } | ||
|
|
||
| then { | ||
| // basec run health | ||
| assert process.success | ||
| assert process.exitStatus == 0 | ||
|
|
||
| // h5ad files | ||
| assert process.out.h5ad | ||
| assert process.out.h5ad.get(0).get(0) ==~ "pbmc_1k_mtx" | ||
| assert process.out.h5ad.get(0).get(1) ==~ ".*/pbmc_1k_mtx.h5ad" | ||
|
|
||
| // versions.yml | ||
| assert process.out.versions | ||
| assert process.out.versions.get(0) ==~ ".*/versions.yml" | ||
|
|
||
| // Snapshot all output channels of a process | ||
| assert snapshot(process.out).match() | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
| test("Run -stub for .mtx and .h5 files") { | ||
|
|
||
| options "-stub" | ||
| when { | ||
| process { | ||
| """ | ||
| input[0] = Channel.fromList([ | ||
| ['pbmc_1k_h5', file(params.test_data_base + "anndatautils/pbmc_1k.h5")], | ||
| ['pbmc_1k_mtx', file(params.test_data_base + "anndatautils/raw_feature_bc_matrix")] | ||
| ]) | ||
| input[1] = Channel.fromList(["___", "___"]) | ||
| """ | ||
| } | ||
| } | ||
|
|
||
| then { | ||
| // basec run health | ||
| assert process.success | ||
| assert process.exitStatus == 0 | ||
| assert process.trace.succeeded().size() == 2 | ||
|
|
||
| // h5ad files | ||
| assert process.out.h5ad | ||
| assert process.out.h5ad.size() == 2 | ||
|
|
||
| // versions.yml | ||
| assert process.out.versions | ||
| with(process.out.versions) { | ||
| assert size() == 2 | ||
| assert get(0) ==~ ".*/versions.yml" | ||
| assert get(1) ==~ ".*/versions.yml" | ||
| } | ||
|
|
||
| // Snapshot all output channels of a process | ||
| assert snapshot(process.out).match() | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dockerfile goes to dockage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docker file for this module is already on dockage. I think it would still be good to reference Dockerfile somehow. Either by putting it in the module directory or by leaving a link to dockage in
meta.ymlorREADME.mdfile