From d8d8a791af10877fcaf9d54baeb8c95547fd25ed Mon Sep 17 00:00:00 2001 From: Neil Horner Date: Fri, 24 Feb 2023 14:42:57 +0000 Subject: [PATCH] Glue --- .gitignore | 6 +- .pre-commit-config.yaml | 8 +- README.md | 14 ++- bin/check_sample_sheet.py | 40 ------- bin/ping.py | 65 ---------- bin/workflow-glue | 7 ++ bin/workflow_glue/__init__.py | 62 ++++++++++ bin/workflow_glue/check_sample_sheet.py | 81 +++++++++++++ .../compute_cluster_quality.py | 97 ++++++++------- bin/{ => workflow_glue}/de_plots.py | 0 .../generate_pychopper_stats.py | 18 ++- .../generate_tracking_summary.py | 17 ++- bin/{ => workflow_glue}/merge_count_tsvs.py | 50 ++++---- bin/{ => workflow_glue}/report.py | 112 +++++++++--------- bin/{ => workflow_glue}/run_isonclust2.py | 21 +++- bin/workflow_glue/tests/__init__.py | 1 + bin/workflow_glue/tests/test_test.py | 10 ++ bin/workflow_glue/util.py | 52 ++++++++ lib/Pinguscript.groovy | 4 +- lib/fastqingress.nf | 2 +- main.nf | 11 +- nextflow.config | 6 +- nextflow_schema.json | 5 - subworkflows/denovo_assembly.nf | 4 +- subworkflows/differential_expression.nf | 6 +- 25 files changed, 413 insertions(+), 286 deletions(-) delete mode 100755 bin/check_sample_sheet.py delete mode 100755 bin/ping.py create mode 100755 bin/workflow-glue create mode 100755 bin/workflow_glue/__init__.py create mode 100755 bin/workflow_glue/check_sample_sheet.py rename bin/{ => workflow_glue}/compute_cluster_quality.py (98%) rename bin/{ => workflow_glue}/de_plots.py (100%) rename bin/{ => workflow_glue}/generate_pychopper_stats.py (77%) rename bin/{ => workflow_glue}/generate_tracking_summary.py (87%) rename bin/{ => workflow_glue}/merge_count_tsvs.py (52%) rename bin/{ => workflow_glue}/report.py (99%) rename bin/{ => workflow_glue}/run_isonclust2.py (92%) create mode 100755 bin/workflow_glue/tests/__init__.py create mode 100755 bin/workflow_glue/tests/test_test.py create mode 100755 bin/workflow_glue/util.py diff --git a/.gitignore b/.gitignore index cad64e1..be27172 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,5 @@ nextflow template-workflow .*.swp .*.swo -.DS_STORE -output/** -.idea/** -**/__pycache__ +*.pyc +*.pyo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a330bb..f5d1036 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: rev: 3.7.9 hooks: - id: flake8 + pass_filenames: false additional_dependencies: - flake8-rst-docstrings - flake8-docstrings @@ -31,4 +32,9 @@ repos: - flake8-builtins - flake8-absolute-import - flake8-print - entry: flake8 bin --import-order-style google --statistics + args: [ + "bin", + "--import-order-style=google", + "--statistics", + "--max-line-length=88", + ] diff --git a/README.md b/README.md index 68060d5..bc35fa5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ It has been adapted from two existing Snakemake pipelines: * https://github.com/nanoporetech/pipeline-nanopore-ref-isoforms * https://github.com/nanoporetech/pipeline-nanopore-denovo-isoforms + + + + ## Introduction This workflow identifies RNA isoforms using either cDNA or direct RNA (dRNA) @@ -80,6 +84,10 @@ Differential gene expression is sensitive to the input data quantity and quality - Reference genome in fasta format (required for reference-based assembly). - Optional reference annotation in GFF2/3 format (required for differential expression analysis `--de_analysis`). - For fusion detection, JAFFAL reference files (see Quickstart) + + + + ## Quickstart The workflow uses [nextflow](https://www.nextflow.io/) to manage compute and @@ -284,7 +292,11 @@ in `${out_dir}/jaffal_output_${sample_id}` you will find: * Nowicka, Malgorzata, and Mark D. Robinson. 2016. “DRIMSeq: A Dirichlet-Multinomial Framework for Multivariate Count Outcomes in Genomics [Version 2; Referees: 2 Approved].” F1000Research 5 (1356). https://doi.org/10.12688/f1000research.8900.2. * Patro, Robert, Geet Duggal, Michael I Love, Rafael A Irizarry, and Carl Kingsford. 2017. “Salmon Provides Fast and Bias-Aware Quantification of Transcript Expression.” Nature Methods 14 (March). https://doi.org/10.1038/nmeth.4197. * Robinson, Mark D, Davis J McCarthy, and Gordon K Smyth. 2010. “EdgeR: A Bioconductor Package for Differential Expression Analysis of Digital Gene Expression Data.” Bioinformatics 26 (1): 139–40. -* Love, Michael I., et al. Swimming Downstream: Statistical Analysis of Differential Transcript Usage Following Salmon Quantification. 7:952, F1000Research, 14 Sept. 2018. f1000research.com, https://f1000research.com/articles/7-952## Useful links +* Love, Michael I., et al. Swimming Downstream: Statistical Analysis of Differential Transcript Usage Following Salmon Quantification. 7:952, F1000Research, 14 Sept. 2018. f1000research.com, https://f1000research.com/articles/7-952 + + + +## Useful links * [nextflow](https://www.nextflow.io/) * [docker](https://www.docker.com/products/docker-desktop) diff --git a/bin/check_sample_sheet.py b/bin/check_sample_sheet.py deleted file mode 100755 index 9c6a4ff..0000000 --- a/bin/check_sample_sheet.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -"""Script to check that sample sheet is well-formatted.""" -import argparse -import sys - -import pandas as pd - - -def main(): - """Run entry point.""" - parser = argparse.ArgumentParser() - parser.add_argument('sample_sheet') - parser.add_argument('output') - args = parser.parse_args() - - try: - samples = pd.read_csv(args.sample_sheet, sep=None) - if 'alias' in samples.columns: - if 'sample_id' in samples.columns: - sys.stderr.write( - "Warning: sample sheet contains both 'alias' and " - 'sample_id, using the former.') - samples['sample_id'] = samples['alias'] - if not set(['sample_id', 'barcode']).intersection(samples.columns): - raise IOError() - except Exception: - raise IOError( - "Could not parse sample sheet, it must contain two columns " - "named 'barcode' and 'sample_id' or 'alias'.") - # check duplicates - dup_bc = samples['barcode'].duplicated() - dup_sample = samples['sample_id'].duplicated() - if any(dup_bc) or any(dup_sample): - raise IOError( - "Sample sheet contains duplicate values.") - samples.to_csv(args.output, sep=",", index=False) - - -if __name__ == '__main__': - main() diff --git a/bin/ping.py b/bin/ping.py deleted file mode 100755 index de8c9a0..0000000 --- a/bin/ping.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python -"""Send workflow ping.""" - -import argparse -import json -import uuid - -from epi2melabs import ping - - -def get_uuid(val): - """Construct UUID from string.""" - return uuid.UUID(str(val)) - - -def main(): - """Run the entry point.""" - parser = argparse.ArgumentParser() - parser.add_argument( - "--hostname", required=True, default=None, - help="ping some meta") - parser.add_argument( - "--opsys", required=True, default=None, - help="ping some meta") - parser.add_argument( - "--session", default=None, - help="ping some meta") - parser.add_argument( - "--message", required=True, default=None, - help="message to include in the ping") - parser.add_argument( - "--meta", default=None, - help="JSON file of metadata to be included in the ping") - parser.add_argument( - "--revision", default='unknown', - help="git branch/tag of the executed workflow") - parser.add_argument( - "--commit", default='unknown', - help="git commit of the executed workflow") - parser.add_argument( - "--disable", action='store_true', - help="Run the script but don't send the ping") - args = parser.parse_args() - - meta = None - if args.meta: - with open(args.meta, "r") as json_file: - meta = json.load(json_file) - - if not args.disable: - ping.Pingu( - get_uuid(args.session), - hostname=args.hostname, - opsys=args.opsys - ).send_workflow_ping( - workflow='wf-transcriptomes', - message=args.message, - revision=args.revision, - commit=args.commit, - meta=meta - ) - - -if __name__ == "__main__": - main() diff --git a/bin/workflow-glue b/bin/workflow-glue new file mode 100755 index 0000000..88db349 --- /dev/null +++ b/bin/workflow-glue @@ -0,0 +1,7 @@ +#!/usr/bin/env python +"""Entry point for sc_tools (single_cell_tools).""" + +from workflow_glue import cli + +if __name__ == '__main__': + cli() diff --git a/bin/workflow_glue/__init__.py b/bin/workflow_glue/__init__.py new file mode 100755 index 0000000..ffc7c80 --- /dev/null +++ b/bin/workflow_glue/__init__.py @@ -0,0 +1,62 @@ +"""Workflow Python code.""" +import argparse +import glob +import importlib +import os + +from workflow_glue.util import _log_level, get_main_logger # noqa: ABS101 + + +__version__ = "0.0.1" +_package_name = "workflow_glue" + + +def get_components(): + """Find a list of workflow command scripts.""" + path = os.path.dirname(os.path.abspath(__file__)) + components = list() + for fname in glob.glob(os.path.join(path, "*.py")): + name = os.path.splitext(os.path.basename(fname))[0] + if name in ("__init__", "util"): + continue + mod = importlib.import_module(f"{_package_name}.{name}") + # if there's a main() and and argparser() that's good enough for us. + try: + req = "main", "argparser" + if all(callable(getattr(mod, x)) for x in req): + components.append(name) + except Exception: + pass + return components + + +def cli(): + """Run workflow entry points.""" + parser = argparse.ArgumentParser( + 'wf-glue', + parents=[_log_level()], + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + '-v', '--version', action='version', + version='%(prog)s {}'.format(__version__)) + + subparsers = parser.add_subparsers( + title='subcommands', description='valid commands', + help='additional help', dest='command') + subparsers.required = True + + # all component demos, plus some others + components = [ + f'{_package_name}.{comp}' for comp in get_components()] + for module in components: + mod = importlib.import_module(module) + p = subparsers.add_parser( + module.split(".")[-1], parents=[mod.argparser()]) + p.set_defaults(func=mod.main) + + logger = get_main_logger(_package_name) + args = parser.parse_args() + + logger.info("Starting entrypoint.") + args.func(args) diff --git a/bin/workflow_glue/check_sample_sheet.py b/bin/workflow_glue/check_sample_sheet.py new file mode 100755 index 0000000..0f7d17b --- /dev/null +++ b/bin/workflow_glue/check_sample_sheet.py @@ -0,0 +1,81 @@ +"""Check if a sample sheet is valid.""" +import csv +import sys + +from .util import get_named_logger, wf_parser # noqa: ABS101 + + +def main(args): + """Run the entry point.""" + logger = get_named_logger("checkSheet") + + barcodes = [] + aliases = [] + types = [] + + try: + with open(args.sample_sheet, "r") as f: + csv_reader = csv.DictReader(f) + n_row = 0 + for row in csv_reader: + n_row += 1 + if n_row == 1: + n_cols = len(row) + else: + # check we got the same number of fields + if len(row) != n_cols: + raise ValueError( + f"Unexpected number of cells in row number {n_row}." + ) + try: + barcodes.append(row["barcode"]) + except KeyError: + sys.stdout.write("'barcode' column missing") + exit() + try: + aliases.append(row["alias"]) + except KeyError: + sys.stdout.write("'alias' column missing") + exit() + try: + types.append(row["type"]) + except KeyError: + pass + except Exception as e: + sys.stdout.write(f"Parsing error: {e}") + exit() + + # check barcode and alias values are unique + if len(barcodes) > len(set(barcodes)): + sys.stdout.write("values in 'barcode' column not unique") + exit() + if len(aliases) > len(set(aliases)): + sys.stdout.write("values in 'alias' column not unique") + exit() + + if types: + # check if "type" column has unexpected values + unexp_type_vals = set(types) - set( + [ + "test_sample", + "positive_control", + "negative_control", + "no_template_control", + ] + ) + if unexp_type_vals: + sys.stdout.write( + f"found unexpected values in 'type' column: {unexp_type_vals}. " + "allowed values are: `['test_sample', 'positive_control', " + "'negative_control', 'no_template_control']`" + ) + exit() + + logger.info(f"Checked sample sheet {args.sample_sheet}.") + + +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("check_sample_sheet") + parser.add_argument("sample_sheet", help="Sample sheet to check") + return parser diff --git a/bin/compute_cluster_quality.py b/bin/workflow_glue/compute_cluster_quality.py similarity index 98% rename from bin/compute_cluster_quality.py rename to bin/workflow_glue/compute_cluster_quality.py index 3220677..adf3deb 100755 --- a/bin/compute_cluster_quality.py +++ b/bin/workflow_glue/compute_cluster_quality.py @@ -1,11 +1,9 @@ #!/usr/bin/env python - """Generate cluster quality data.""" # Adapted form script by Kristoffer Sahlin for # isONclust: https://github.com/ksahlin/isONclust -import argparse from collections import defaultdict import math from pathlib import Path @@ -19,9 +17,56 @@ import pysam from sklearn.metrics.cluster import ( adjusted_rand_score, completeness_score, homogeneity_score, v_measure_score) + +from .util import wf_parser # noqa: ABS101 + matplotlib.use('Agg') +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("compute_cluster_quality") + parser.add_argument( + '--clusters', + type=str, + help='Inferred clusters (tsv file)') + parser.add_argument( + '--classes', + type=str, + help='A sorted and indexed bam file.') + parser.add_argument( + '--ctsv', + default=None, + type=str, + help='Write true classes in this TSV file.') + parser.add_argument( + '--simulated', + action="store_true", + help='Simulated data, we can simply read correct classes ' + 'from the ref field.') + parser.add_argument( + '--ont', + action="store_true", + help='ONT data, parsing accessions differently.') + parser.add_argument( + '--modified_ont', + action="store_true", + help='ONT data preprocessed accessions, parsing ' + 'accessions differently.') + parser.add_argument('--outfile', type=str, help='Output file with results') + parser.add_argument( + '--report', + type=str, + help='Output PDF file with report') + parser.add_argument('--sizes', type=str, help='Cluster sizes') + parser.add_argument( + '--raw_data_out', + type=str, + help='dir to save raw data for plotting') + + return parser + + def parse_inferred_clusters_tsv(tsv_file, args): """parse_inferred_clusters_tsv.""" infile = open(tsv_file, "r") @@ -602,51 +647,3 @@ def main(args): plt.clf() pages.close() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="Align predicted transcripts to transcripts in ensembl " - "reference data base.") - parser.add_argument( - '--clusters', - type=str, - help='Inferred clusters (tsv file)') - parser.add_argument( - '--classes', - type=str, - help='A sorted and indexed bam file.') - parser.add_argument( - '--ctsv', - default=None, - type=str, - help='Write true classes in this TSV file.') - parser.add_argument( - '--simulated', - action="store_true", - help='Simulated data, we can simply read correct classes ' - 'from the ref field.') - parser.add_argument( - '--ont', - action="store_true", - help='ONT data, parsing accessions differently.') - parser.add_argument( - '--modified_ont', - action="store_true", - help='ONT data preprocessed accessions, parsing ' - 'accessions differently.') - parser.add_argument('--outfile', type=str, help='Output file with results') - parser.add_argument( - '--report', - type=str, - help='Output PDF file with report') - parser.add_argument('--sizes', type=str, help='Cluster sizes') - parser.add_argument( - '--raw_data_out', - type=str, - help='dir to save raw data for plotting') - args = parser.parse_args() - - sys.stdout("------------------------------------------------------------") - main(args) - sys.stdout("------------------------------------------------------------") diff --git a/bin/de_plots.py b/bin/workflow_glue/de_plots.py similarity index 100% rename from bin/de_plots.py rename to bin/workflow_glue/de_plots.py diff --git a/bin/generate_pychopper_stats.py b/bin/workflow_glue/generate_pychopper_stats.py similarity index 77% rename from bin/generate_pychopper_stats.py rename to bin/workflow_glue/generate_pychopper_stats.py index 149c555..8955986 100755 --- a/bin/generate_pychopper_stats.py +++ b/bin/workflow_glue/generate_pychopper_stats.py @@ -3,20 +3,20 @@ # -*- coding: utf-8 -*- -import argparse import os -import sys import pandas as pd +from .util import wf_parser # noqa: ABS101 -def parse_args(argv=sys.argv[1:]): - """Parse arguments.""" - description = """Script to run the isoform workflow """ - parser = argparse.ArgumentParser(description=description) + +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("generate_pychopper_stats") parser.add_argument("--data", required=True, help="") parser.add_argument("--output_dir", required=True, help="") - return parser.parse_args(argv) + + return parser def generate_pychopper_stats(tsv, output): @@ -37,7 +37,3 @@ def main(args): assert os.path.isfile(args.data) assert os.path.isdir(args.output_dir) generate_pychopper_stats(tsv=args.data, output=args.output_dir) - - -if __name__ == '__main__': - main(args=parse_args()) diff --git a/bin/generate_tracking_summary.py b/bin/workflow_glue/generate_tracking_summary.py similarity index 87% rename from bin/generate_tracking_summary.py rename to bin/workflow_glue/generate_tracking_summary.py index ec126cc..02a0eef 100755 --- a/bin/generate_tracking_summary.py +++ b/bin/workflow_glue/generate_tracking_summary.py @@ -1,21 +1,22 @@ #!/usr/bin/env python """Generate per-transcript class sumarrarry files from gffcompare.""" -import argparse import os import sys import pandas as pd +from .util import wf_parser # noqa: ABS101 -def parse_args(argv=sys.argv[1:]): - """Parse arguments.""" - description = """Script to run the isoform workflow """ - parser = argparse.ArgumentParser(description=description) + +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("generate_tracking_summary") parser.add_argument("--tracking", required=True, help="") parser.add_argument("--output_dir", required=True, help="") parser.add_argument("--annotation", required=False, default=None, help="") - return parser.parse_args(argv) + + return parser def generate_tracking_summary(tracking_file, output_dir, annotations=None): @@ -63,7 +64,3 @@ def main(args): os.path.isfile(args.annotation) generate_tracking_summary( args.tracking, output_dir=args.output_dir, annotations=args.annotation) - - -if __name__ == '__main__': - main(parse_args(sys.argv[1:])) diff --git a/bin/merge_count_tsvs.py b/bin/workflow_glue/merge_count_tsvs.py similarity index 52% rename from bin/merge_count_tsvs.py rename to bin/workflow_glue/merge_count_tsvs.py index 5732676..939ad1a 100755 --- a/bin/merge_count_tsvs.py +++ b/bin/workflow_glue/merge_count_tsvs.py @@ -1,36 +1,40 @@ #!/usr/bin/env python """Merge salmon output count files.""" -import argparse from functools import reduce import numpy as np import pandas as pd -# Parse command line arguments: -parser = argparse.ArgumentParser( - description="""Merge tab separated files on a given field using pandas.""") -parser.add_argument( - '-j', metavar='join', help="Join type (outer).", default="outer") -parser.add_argument( - '-f', metavar='field', - help="Join on this field (Reference).", default="Reference") -parser.add_argument( - '-o', metavar='out_tsv', - help="Output tsv (merge_tsvs.tsv).", default="merge_tsvs.tsv") -parser.add_argument( - '-z', action="store_true", - help="Fill NA values with zero.", default=False) -parser.add_argument( - '-tpm', type=bool, default=False, - help="TPM instead of counts") -parser.add_argument( - '-tsvs', metavar='input_tsvs', nargs='*', - help="Input tab separated files.") +from .util import wf_parser # noqa: ABS101 -if __name__ == '__main__': - args = parser.parse_args() +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("merge_count_tsvs") + parser.add_argument( + '-j', metavar='join', help="Join type (outer).", default="outer") + parser.add_argument( + '-f', metavar='field', + help="Join on this field (Reference).", default="Reference") + parser.add_argument( + '-o', metavar='out_tsv', + help="Output tsv (merge_tsvs.tsv).", default="merge_tsvs.tsv") + parser.add_argument( + '-z', action="store_true", + help="Fill NA values with zero.", default=False) + parser.add_argument( + '-tpm', type=bool, default=False, + help="TPM instead of counts") + parser.add_argument( + '-tsvs', metavar='input_tsvs', nargs='*', + help="Input tab separated files.") + + return parser + + +def main(args): + """Run entry point.""" dfs = {x: pd.read_csv(x, sep="\t") for x in args.tsvs} ndfs = [] diff --git a/bin/report.py b/bin/workflow_glue/report.py similarity index 99% rename from bin/report.py rename to bin/workflow_glue/report.py index 807d2aa..ee75d2d 100755 --- a/bin/report.py +++ b/bin/workflow_glue/report.py @@ -1,7 +1,6 @@ #!/usr/bin/env python """Create workflow report.""" -import argparse from collections import Counter, defaultdict, OrderedDict import math import os @@ -19,12 +18,66 @@ from bokeh.models.widgets import DataTable, TableColumn from bokeh.palettes import Category10_10 from bokeh.plotting import figure from bokeh.transform import dodge -import de_plots import gffutils import numpy as np import pandas as pd import sigfig +from . import de_plots # noqa: ABS101 +from .util import wf_parser # noqa: ABS101 + + +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("report") + parser.add_argument("--report", help="Report output file") + parser.add_argument("--summaries", nargs='+', help="Read summary file.") + parser.add_argument( + "--versions", required=True, + help="directory containing CSVs containing name,version.") + parser.add_argument( + "--params", default=None, required=True, + help="A JSON file containing the workflow parameter key/values") + parser.add_argument( + "--revision", default='unknown', + help="git branch/tag of the executed workflow") + parser.add_argument( + "--commit", default='unknown', + help="git commit of the executed workflow") + parser.add_argument( + "--alignment_stats", required=False, default=None, nargs='*', + help="TSV summary file of alignment statistics") + parser.add_argument( + "--gff_annotation", required=False, nargs='+', + help="transcriptome annotation gff file") + parser.add_argument( + "--gffcompare_dir", required=False, default=None, nargs='*', + help="gffcompare outout dir") + parser.add_argument( + "--pychop_report", required=False, default=None, + help="TSV summary file of pychopper statistics") + parser.add_argument( + "--sample_ids", required=True, nargs='+', + help="List of sample ids") + parser.add_argument( + "--isoform_table_nrows", required=False, type=int, default=5000, + help="Maximum rows to display in isoforms table") + parser.add_argument( + "--cluster_qc_dirs", required=False, type=str, default=None, nargs='*', + help="Directory with various cluster quality csvs") + parser.add_argument( + "--jaffal_csv", required=False, type=str, default=None, + help="Path to JAFFAL results csv") + parser.add_argument( + "--de_report", required=False, type=str, default=None, + help="Differential expression report optional") + parser.add_argument( + "--de_stats", required=False, type=str, default=None, nargs='*', + help="Differential expression report optional") + parser.add_argument('--denovo', dest='denovo', action='store_true') + + return parser + def _parse_stat_line(sl): """Parse a stats line.""" @@ -846,57 +899,8 @@ def de_section(report): report=report) -def main(): +def main(args): """Run the entry point.""" - parser = argparse.ArgumentParser() - parser.add_argument("--report", help="Report output file") - parser.add_argument("--summaries", nargs='+', help="Read summary file.") - parser.add_argument( - "--versions", required=True, - help="directory containing CSVs containing name,version.") - parser.add_argument( - "--params", default=None, required=True, - help="A JSON file containing the workflow parameter key/values") - parser.add_argument( - "--revision", default='unknown', - help="git branch/tag of the executed workflow") - parser.add_argument( - "--commit", default='unknown', - help="git commit of the executed workflow") - parser.add_argument( - "--alignment_stats", required=False, default=None, nargs='*', - help="TSV summary file of alignment statistics") - parser.add_argument( - "--gff_annotation", required=False, nargs='+', - help="transcriptome annotation gff file") - parser.add_argument( - "--gffcompare_dir", required=False, default=None, nargs='*', - help="gffcompare outout dir") - parser.add_argument( - "--pychop_report", required=False, default=None, - help="TSV summary file of pychopper statistics") - parser.add_argument( - "--sample_ids", required=True, nargs='+', - help="List of sample ids") - parser.add_argument( - "--isoform_table_nrows", required=False, type=int, default=5000, - help="Maximum rows to display in isoforms table") - parser.add_argument( - "--cluster_qc_dirs", required=False, type=str, default=None, nargs='*', - help="Directory with various cluster quality csvs") - parser.add_argument( - "--jaffal_csv", required=False, type=str, default=None, - help="Path to JAFFAL results csv") - parser.add_argument( - "--de_report", required=False, type=str, default=None, - help="Differential expression report optional") - parser.add_argument( - "--de_stats", required=False, type=str, default=None, nargs='*', - help="Differential expression report optional") - parser.add_argument('--denovo', dest='denovo', action='store_true') - - args = parser.parse_args() - sample_ids = args.sample_ids report = WFReport( @@ -952,7 +956,3 @@ def main(): section=scomponents.params_table(args.params)) report.write(args.report) - - -if __name__ == "__main__": - main() diff --git a/bin/run_isonclust2.py b/bin/workflow_glue/run_isonclust2.py similarity index 92% rename from bin/run_isonclust2.py rename to bin/workflow_glue/run_isonclust2.py index fb16301..b9541f5 100755 --- a/bin/run_isonclust2.py +++ b/bin/workflow_glue/run_isonclust2.py @@ -3,10 +3,23 @@ from collections import OrderedDict from glob import glob from itertools import zip_longest +import os from pathlib import Path import re import subprocess as sub +from .util import wf_parser # noqa: ABS101 + + +def argparser(): + """Argument parser for entrypoint.""" + parser = wf_parser("report") + parser.add_argument( + "--workdir", help="directory containing batches/ dir [CWD]", + default=Path()) + + return parser + class Node: """Node.""" @@ -90,8 +103,9 @@ def build_job_tree(): return job_tree, levels -def main(): +def main(args): """Entry point.""" + os.chdir(args.workdir) Path('clusters').mkdir(exist_ok=True) job_tree, levels = build_job_tree() @@ -122,8 +136,3 @@ def main(): sub.call(( "ln -s `realpath clusters/isONcluster_{}.cer` " "isONcluster_ROOT.cer".format(n.Id)), shell=True) - - -if __name__ == '__main__': - # The cwd should be the process dir that contains 'batches/' - main() diff --git a/bin/workflow_glue/tests/__init__.py b/bin/workflow_glue/tests/__init__.py new file mode 100755 index 0000000..71bd80f --- /dev/null +++ b/bin/workflow_glue/tests/__init__.py @@ -0,0 +1 @@ +"""__init__.py for the tests.""" diff --git a/bin/workflow_glue/tests/test_test.py b/bin/workflow_glue/tests/test_test.py new file mode 100755 index 0000000..496bc2a --- /dev/null +++ b/bin/workflow_glue/tests/test_test.py @@ -0,0 +1,10 @@ +"""A dummy test.""" + +import argparse + +from workflow_glue import report + + +def test(): + """Just showing that we can import using the workflow-glue.""" + assert isinstance(report.argparser(), argparse.ArgumentParser) diff --git a/bin/workflow_glue/util.py b/bin/workflow_glue/util.py new file mode 100755 index 0000000..9359ac7 --- /dev/null +++ b/bin/workflow_glue/util.py @@ -0,0 +1,52 @@ +"""The odd helper function.""" + +import argparse +import logging + +_log_name = None + + +def get_main_logger(name): + """Create the top-level logger.""" + global _log_name + _log_name = name + logging.basicConfig( + format='[%(asctime)s - %(name)s] %(message)s', + datefmt='%H:%M:%S', level=logging.INFO) + return logging.getLogger(name) + + +def get_named_logger(name): + """Create a logger with a name. + + :param name: name of logger. + """ + name = name.ljust(10)[:10] # so logging is aligned + logger = logging.getLogger('{}.{}'.format(_log_name, name)) + return logger + + +def wf_parser(name): + """Make an argument parser for a workflow command.""" + return argparse.ArgumentParser( + name, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=False) + + +def _log_level(): + """Parser to set logging level and acquire software version/commit.""" + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + + modify_log_level = parser.add_mutually_exclusive_group() + modify_log_level.add_argument( + '--debug', action='store_const', + dest='log_level', const=logging.DEBUG, default=logging.INFO, + help='Verbose logging of debug information.') + modify_log_level.add_argument( + '--quiet', action='store_const', + dest='log_level', const=logging.WARNING, default=logging.INFO, + help='Minimal logging; warnings only.') + + return parser diff --git a/lib/Pinguscript.groovy b/lib/Pinguscript.groovy index e9d0751..a893263 100644 --- a/lib/Pinguscript.groovy +++ b/lib/Pinguscript.groovy @@ -8,7 +8,7 @@ class Pinguscript { def msgId = UUID.randomUUID().toString() def hosthash = null try { - hosthash = InetAddress.getLocalHost().getHostName().md5() + hosthash = InetAddress.getLocalHost().getHostName() } catch(Exception e) { hosthash = "Unavailable" } @@ -32,7 +32,7 @@ class Pinguscript { def meta = meta_json "error": errorMessage.toString(), "profile": profile.toString(), "agent": agent.toString() meta+=any_other_data - def ping_version = '2.0.1' + def ping_version = '2.0.2' def tracking_json = new JsonBuilder() def tracking_id = tracking_json "msg_id": msgId, "version": ping_version def data_json = new JsonBuilder() diff --git a/lib/fastqingress.nf b/lib/fastqingress.nf index d059ca1..13988c2 100644 --- a/lib/fastqingress.nf +++ b/lib/fastqingress.nf @@ -32,7 +32,7 @@ process checkSampleSheet { output: file "samples.txt" """ - check_sample_sheet.py sample_sheet.txt samples.txt + workflow-glue check_sample_sheet sample_sheet.txt samples.txt """ } diff --git a/main.nf b/main.nf index d21d8eb..ab4b7a1 100644 --- a/main.nf +++ b/main.nf @@ -30,6 +30,7 @@ process summariseConcatReads { tuple val(meta.sample_id), path('*.stats'), emit: summary script: """ + fastcat -s ${meta.sample_id} -r ${meta.sample_id}.stats -x ${directory} > ${meta.sample_id}.fastq """ } @@ -55,7 +56,7 @@ process getVersions { stringtie --version | sed 's/^/stringtie,/' >> versions.txt gffcompare --version | head -n 1 | sed 's/ /,/' >> versions.txt spoa --version | sed 's/^/spoa,/' >> versions.txt - isONclust2 version | sed 's/ version: /,/' >> versions.txt +# isONclust2 version | sed 's/ version: /,/' >> versions.txt """ } @@ -67,6 +68,8 @@ process getParams { path "params.json" script: def paramsJSON = new JsonBuilder(params).toPrettyString() + println('test') + println(params.workDir) """ # Output nextflow params object to JSON echo '$paramsJSON' > params.json @@ -91,7 +94,7 @@ process preprocess_reads { """ pychopper -t ${params.threads} ${params.pychopper_opts} ${input_reads} ${sample_id}_full_length_reads.fastq mv pychopper.tsv ${sample_id}_pychopper.tsv - generate_pychopper_stats.py --data ${sample_id}_pychopper.tsv --output . + workflow-glue generate_pychopper_stats --data ${sample_id}_pychopper.tsv --output . # Add sample id column sed "1s/\$/\tsample_id/; 1 ! s/\$/\t${sample_id}/" ${sample_id}_pychopper.tsv > tmp @@ -242,7 +245,7 @@ process run_gffcompare{ gffcompare -o ${out_dir}/str_merged -r ${ref_annotation} \ ${params.gffcompare_opts} ${query_annotation} - generate_tracking_summary.py --tracking $out_dir/str_merged.tracking \ + workflow-glue generate_tracking_summary --tracking $out_dir/str_merged.tracking \ --output_dir ${out_dir} --annotation ${ref_annotation} mv *.tmap $out_dir @@ -350,7 +353,7 @@ process makeReport { else OPT_PC_REPORT="--pychop_report pychopper_report/*" fi - report.py --report $report_name \ + workflow-glue report --report $report_name \ --versions $versions \ --params params.json \ \$OPT_ALN \ diff --git a/nextflow.config b/nextflow.config index b6e9bc3..1d5de7c 100644 --- a/nextflow.config +++ b/nextflow.config @@ -24,7 +24,6 @@ params { out_dir = "output" sample = null sample_sheet = null - wfversion = "v0.1.8" aws_image_prefix = null aws_queue = null process_label = "isoforms" @@ -34,7 +33,7 @@ params { monochrome_logs = false validate_params = true show_hidden_params = false - schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion,wf,process_label' + schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wf,process_label' // Process cDNA reads using pychopper, turn off for direct RNA: direct_rna = false @@ -189,14 +188,17 @@ profiles { timeline { enabled = true + overwrite = true file = "${params.out_dir}/execution/timeline.html" } report { enabled = true + overwrite = true file = "${params.out_dir}/execution/report.html" } trace { enabled = true + overwrite = true file = "${params.out_dir}/execution/trace.txt" } diff --git a/nextflow_schema.json b/nextflow_schema.json index 5624414..e30b4a8 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -339,11 +339,6 @@ "type": "string", "hidden": true }, - "wfversion": { - "type": "string", - "default": "v0.1.8", - "hidden": true - }, "monochrome_logs": { "type": "boolean" }, diff --git a/subworkflows/denovo_assembly.nf b/subworkflows/denovo_assembly.nf index 8c3571f..422b7e9 100644 --- a/subworkflows/denovo_assembly.nf +++ b/subworkflows/denovo_assembly.nf @@ -199,7 +199,7 @@ process clustering() { tuple val(sample_id), path('isONcluster_ROOT.cer'), emit: root_cluster script: """ - run_isonclust2.py $sorted_batches + workflow-glue run_isonclust2 $sorted_batches """ } @@ -228,7 +228,7 @@ process cluster_quality() { samtools view -q 2 -F 2304 -b - |\ samtools sort - -o $bam; samtools index $bam; - compute_cluster_quality.py --sizes $final_clusters_dir/clusters_info.tsv \ + workflow-glue compute_cluster_quality --sizes $final_clusters_dir/clusters_info.tsv \ --outfile ${qc_dir}/cluster_quality.csv --ont --clusters $final_clusters_dir/clusters.tsv \ --classes $bam --report ${qc_dir}/cluster_quality.pdf --raw_data_out $qc_dir_raw """ diff --git a/subworkflows/differential_expression.nf b/subworkflows/differential_expression.nf index eec5aaa..915f5df 100644 --- a/subworkflows/differential_expression.nf +++ b/subworkflows/differential_expression.nf @@ -24,7 +24,7 @@ process mergeCounts { output: path "all_counts.tsv" """ - merge_count_tsvs.py -z -o all_counts.tsv -tsvs ${counts} + workflow-glue merge_count_tsvs -z -o all_counts.tsv -tsvs ${counts} """ } @@ -35,7 +35,7 @@ process mergeTPM { output: path "tpm_counts.tsv" """ - merge_count_tsvs.py -o tpm_counts.tsv -z -tpm True -tsvs $counts + workflow-glue merge_count_tsvs -o tpm_counts.tsv -z -tpm True -tsvs $counts """ } @@ -51,7 +51,7 @@ process deAnalysis { path "merged/all_counts_filtered.tsv", emit: flt_counts path "merged/all_gene_counts.tsv", emit: gene_counts path "de_analysis/results_dge.tsv", emit: dge - path "de_analysis/results_dexseq.tsv", emit: dexseq + path "de_analysis/results_dexseq.tsv", emit: dexseq path "de_analysis", emit: de_analysis """