Merge branch 'CW-790' into 'dev'

template updates

Closes CW-790

See merge request epi2melabs/workflow-containers/wf-isoforms!57
This commit is contained in:
Neil Horner 2022-07-07 19:14:52 +00:00
commit 3efcf30a34
8 changed files with 219 additions and 27 deletions

76
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you running?
options:
- Windows 10
- Windows 11
- macOS
- ubuntu 18.04
- ubuntu 20.04
validations:
required: true
- type: dropdown
id: execution
attributes:
label: Workflow Execution
description: Where are you running the workflow?
options:
- EPI2ME Labs desktop application
- Command line
- EPI2ME
- Other (please describe)
validations:
required: true
- type: input
id: labs-version
attributes:
label: Workflow Execution - EPI2ME Labs Versions
description: If you're running using EPI2ME Labs please provide the version and the environment version (Click the blue help icon bottom left and select ":bout")?
validations:
required: false
- type: dropdown
id: profile
attributes:
label: Workflow Execution - Execution Profile
description: If you're using the CLI to run the workflow, what profile are you using?
options:
- Docker
- Singularity
- Conda
validations:
required: false
- type: input
id: version
attributes:
label: Workflow Version
description: What version of the workflow are you running?
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Nanopore customer support
url: https://nanoporetech.com/contact
about: For general support, including bioinformatics questions.

View File

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.1.4]
### Changed
- Args parser for fastqingress
- Set out_dir option type to ensure output is written to correct directory on Windows
## [v0.1.3] ## [v0.1.3]
### Changed ### Changed
- Better help text on cli - Better help text on cli

61
lib/ArgumentParser.groovy Normal file
View File

@ -0,0 +1,61 @@
/* Check arguments of a Nextflow function
*
* Nextflow script does not support the Groovy idiom:
*
* def function(Map args[:], arg1, arg2, ...)
*
* to support unordered kwargs. The methods here are designed
* to reduce boileplate while allowing Nextflow script to implement
*
* def function(Map args[:])
*
* with required and default values. This is similar to some Python
* libraries' (notably matplotlib) extensive use of things like:
*
* def function(*args, **kwargs)
*
* to implement generic APIs. Why do we want to do all this? Because
* we want to write library code with a clean set of required parameters
* but also extensible with non-required parameters with default values.
* This allows us to later add parameters without breaking existing code,
* and is very common practice elsewhere.
*/
import java.util.Set
class ArgumentParser {
Set args
Map kwargs
String name
/* Parse arguments, raising an error on unknown keys */
public Map parse_args(LinkedHashMap given_args) {
Set opt_keys = kwargs.keySet()
Set given_keys = given_args.keySet()
check_required(given_keys)
check_unknown(given_keys, opt_keys)
return kwargs + given_args
}
/* Parse arguments, without raising an error for extra keys */
public Map parse_known_args(LinkedHashMap given_args) {
Set opt_keys = kwargs.keySet()
Set given_keys = given_args.keySet()
check_required(given_keys)
return kwargs + given_args
}
private void check_required(Set given) {
Set missing_keys = args - given
if (!missing_keys.isEmpty()) {
throw new Exception("Missing arguments for function ${name}: ${missing_keys}")
}
}
private void check_unknown(Set given, Set kwargs_keys) {
Set extra_keys = given - (args + kwargs_keys)
if (!extra_keys.isEmpty()) {
throw new Exception("Unknown arguments provided to function ${name}: ${extra_keys}.")
}
}
}

View File

@ -1,5 +1,7 @@
import ArgumentParser
process handleSingleFile { process handleSingleFile {
label "isoforms" label params.process_label
cpus 1 cpus 1
input: input:
file reads file reads
@ -16,7 +18,7 @@ process handleSingleFile {
process checkSampleSheet { process checkSampleSheet {
label "isoforms" label params.process_label
cpus 1 cpus 1
input: input:
file "sample_sheet.txt" file "sample_sheet.txt"
@ -73,7 +75,7 @@ def find_fastq(pattern, maxdepth)
* @param input_folder Top-level input directory. * @param input_folder Top-level input directory.
* @param staging Top-level output_directory. * @param staging Top-level output_directory.
* @return A File object representating the staging directory created * @return A File object representating the staging directory created
* under output_folder * under output
*/ */
def sanitize_fastq(input_folder, staging) def sanitize_fastq(input_folder, staging)
{ {
@ -111,8 +113,8 @@ def sanitize_fastq(input_folder, staging)
*/ */
def get_subdirectories(input_directory) def get_subdirectories(input_directory)
{ {
barcode_dirs = file("$input_directory/barcode*", type: 'dir', maxdepth: 1) barcode_dirs = file(input_directory.resolve("barcode*"), type: 'dir', maxdepth: 1)
all_dirs = file("$input_directory/*", type: 'dir', maxdepth: 1) all_dirs = file(input_directory.resolve("*"), type: 'dir', maxdepth: 1)
non_barcoded = ( all_dirs + barcode_dirs ) - all_dirs.intersect(barcode_dirs) non_barcoded = ( all_dirs + barcode_dirs ) - all_dirs.intersect(barcode_dirs)
return [barcode_dirs, non_barcoded] return [barcode_dirs, non_barcoded]
} }
@ -223,9 +225,11 @@ def handle_flat_dir(input_directory, sample_name)
* @param barcoded_dirs List of barcoded directories (barcodeXX,...) * @param barcoded_dirs List of barcoded directories (barcodeXX,...)
* @param sample_sheet List of tuples mapping barcode to sample name * @param sample_sheet List of tuples mapping barcode to sample name
* or a simple string for non-multiplexed data. * or a simple string for non-multiplexed data.
* @param min_barcode Minimum barcode to accept.
* @param max_barcode Maximum (inclusive) barcode to accept.
* @return Channel of tuples (path, sample_id, type) * @return Channel of tuples (path, sample_id, type)
*/ */
def handle_barcoded_dirs(barcoded_dirs, sample_sheet) def handle_barcoded_dirs(barcoded_dirs, sample_sheet, min_barcode, max_barcode)
{ {
valid_dirs = get_valid_directories(barcoded_dirs) valid_dirs = get_valid_directories(barcoded_dirs)
// link sample names to barcode through sample sheet // link sample names to barcode through sample sheet
@ -233,17 +237,36 @@ def handle_barcoded_dirs(barcoded_dirs, sample_sheet)
sample_sheet = Channel sample_sheet = Channel
.fromPath(valid_dirs) .fromPath(valid_dirs)
.filter(~/.*barcode[0-9]{1,3}$/) // up to 192 .filter(~/.*barcode[0-9]{1,3}$/) // up to 192
.filter { barcode_in_range(it, min_barcode, max_barcode) }
.map { path -> tuple(path.baseName, path.baseName, 'test_sample') } .map { path -> tuple(path.baseName, path.baseName, 'test_sample') }
} }
return Channel return Channel
.fromPath(valid_dirs) .fromPath(valid_dirs)
.filter(~/.*barcode[0-9]{1,3}$/) // up to 192 .filter(~/.*barcode[0-9]{1,3}$/) // up to 192
.filter { barcode_in_range(it, min_barcode, max_barcode) }
.map { path -> tuple(path.baseName, path) } .map { path -> tuple(path.baseName, path) }
.join(sample_sheet) .join(sample_sheet)
.map { barcode, path, sample, type -> tuple(path, sample, type) } .map { barcode, path, sample, type -> tuple(path, sample, type) }
} }
/**
* Determine if a barcode path is within a required numeric range
*
* @param path barcoded directory (barcodeXX).
* @param min_barcode Minimum barcode to accept.
* @param max_barcode Maximum (inclusive) barcode to accept.
*/
def barcode_in_range(path, min_barcode, max_barcode)
{
pattern = ~/barcode(\d+)/
matcher = "${path}" =~ pattern
value = matcher[0][1].toInteger()
valid = ((value >= min_barcode) && (value <= max_barcode))
return valid
}
/** /**
* Take a list of non-barcode directories to return a channel * Take a list of non-barcode directories to return a channel
* of named samples. Samples are named by directory baseName. * of named samples. Samples are named by directory baseName.
@ -264,33 +287,49 @@ def handle_non_barcoded_dirs(non_barcoded_dirs)
* Take an input (file or directory) and return a channel of * Take an input (file or directory) and return a channel of
* named samples. * named samples.
* *
*
* @param input Top level input file or folder to locate fastq data. * @param input Top level input file or folder to locate fastq data.
* @param sample string to name single sample data. * @param sample string to name single sample data.
* @param sample_sheet Path to sample sheet CSV file. * @param sample_sheet Path to sample sheet CSV file.
* @param sanitize regularize inputs from EPI2ME platform.
* @param output output location, required if sanitize==true
* @param min_barcode Minimum barcode to accept.
* @param max_barcode Maximum (inclusive) barcode to accept.
*
* @return Channel of tuples (path, sample_id, type) * @return Channel of tuples (path, sample_id, type)
*/ */
def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize) def fastq_ingress(Map arguments)
{ {
def parser = new ArgumentParser(
args:["input"],
kwargs:[
"sample":null, "sample_sheet":null, "sanitize":false, "output":null,
"min_barcode":0, "max_barcode":Integer.MAX_VALUE],
name:"fastq_ingress")
Map margs = parser.parse_args(arguments)
if (margs.sanitize && margs.output == null) {
throw new Exception("Argument 'output' required if 'sanitize' is true.")
}
println("Checking fastq input.") println("Checking fastq input.")
input = file(input); input = file(margs.input)
// Handle file input // Handle file input
if (input.isFile()) { if (input.isFile()) {
// Assume sample is a string at this point // Assume sample is a string at this point
println('Single file input detected.') println('Single file input detected.')
if (sample_sheet) { if (margs.sample_sheet) {
println('Warning: `--sample_sheet` given but single file input found. Ignoring.') println('Warning: `--sample_sheet` given but single file input found. Ignoring.')
} }
return handle_single_file(input, sample) return handle_single_file(input, margs.sample)
} }
// Handle directory input // Handle directory input
if (input.isDirectory()) { if (input.isDirectory()) {
// EPI2ME harness // EPI2ME harness
if (sanitize) { if (margs.sanitize) {
staging = file(output_folder).resolve("staging") staging = file(margs.output).resolve("staging")
input = sanitize_fastq(file(input), staging) input = sanitize_fastq(input, staging)
} }
// Get barcoded and non barcoded subdirectories // Get barcoded and non barcoded subdirectories
@ -299,13 +338,13 @@ def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize)
// Case 03: If no subdirectories, handle the single dir // Case 03: If no subdirectories, handle the single dir
if (!barcoded && !non_barcoded) { if (!barcoded && !non_barcoded) {
println("Single directory input detected.") println("Single directory input detected.")
if (sample_sheet) { if (margs.sample_sheet) {
println('Warning: `--sample_sheet` given but single non-barcode directory found. Ignoring.') println('Warning: `--sample_sheet` given but single non-barcode directory found. Ignoring.')
} }
return handle_flat_dir(input, sample) return handle_flat_dir(input, margs.sample)
} }
if (sample) { if (margs.sample) {
println('Warning: `--sample` given but multiple directories found, ignoring.') println('Warning: `--sample` given but multiple directories found, ignoring.')
} }
@ -314,16 +353,17 @@ def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize)
barcoded_samples = Channel.empty() barcoded_samples = Channel.empty()
if (barcoded) { if (barcoded) {
println("Barcoded directories detected.") println("Barcoded directories detected.")
if (sample_sheet) { sample_sheet = null
sample_sheet = get_sample_sheet(sample_sheet) if (margs.sample_sheet) {
sample_sheet = get_sample_sheet(margs.sample_sheet)
} }
barcoded_samples = handle_barcoded_dirs(barcoded, sample_sheet) barcoded_samples = handle_barcoded_dirs(barcoded, sample_sheet, margs.min_barcode, margs.max_barcode)
} }
non_barcoded_samples = Channel.empty() non_barcoded_samples = Channel.empty()
if (non_barcoded) { if (non_barcoded) {
println("Non barcoded directories detected.") println("Non barcoded directories detected.")
if (!barcoded && sample_sheet) { if (!barcoded && margs.sample_sheet) {
println('Warning: `--sample_sheet` given but no barcode directories found.') println('Warning: `--sample_sheet` given but no barcode directories found.')
} }
non_barcoded_samples = handle_non_barcoded_dirs(non_barcoded) non_barcoded_samples = handle_non_barcoded_dirs(non_barcoded)

View File

@ -502,9 +502,12 @@ workflow {
ref_annotation = file("$projectDir/data/OPTIONAL_FILE") ref_annotation = file("$projectDir/data/OPTIONAL_FILE")
} }
reads = fastq_ingress( reads = fastq_ingress([
params.fastq, params.out_dir, params.sample, params.sample_sheet, params.sanitize_fastq "input":params.fastq,
) "sample":params.sample,
"sample_sheet":params.sample_sheet,
"sanitize": params.sanitize_fastq,
"output":params.out_dir])
pipeline(reads, ref_genome, ref_annotation) pipeline(reads, ref_genome, ref_annotation)

View File

@ -23,15 +23,16 @@ params {
sample = null sample = null
sample_sheet = null sample_sheet = null
sanitize_fastq = false sanitize_fastq = false
wfversion = "v0.1.3" wfversion = "v0.1.4"
aws_image_prefix = null aws_image_prefix = null
aws_queue = null aws_queue = null
report_name = "report" report_name = "report"
process_label = "isoforms"
monochrome_logs = false monochrome_logs = false
validate_params = true validate_params = true
show_hidden_params = false show_hidden_params = false
schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion,wf' schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion,wf,process_label'
// Process cDNA reads using pychopper, turn off for direct RNA: // Process cDNA reads using pychopper, turn off for direct RNA:
direct_rna = false direct_rna = false

View File

@ -15,6 +15,7 @@
"out_dir": { "out_dir": {
"type": "string", "type": "string",
"default": "output", "default": "output",
"format": "path",
"description": "Directory for output of all user-facing files." "description": "Directory for output of all user-facing files."
}, },
"fastq": { "fastq": {
@ -280,7 +281,7 @@
}, },
"wfversion": { "wfversion": {
"type": "string", "type": "string",
"default": "v0.1.3", "default": "v0.1.4",
"hidden": true "hidden": true
}, },
"monochrome_logs": { "monochrome_logs": {