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:
commit
3efcf30a34
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||||
@ -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
61
lib/ArgumentParser.groovy
Normal 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}.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
9
main.nf
9
main.nf
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user