add schema

This commit is contained in:
Chris Wright 2021-09-28 15:11:18 +01:00
parent 0d230e3930
commit e486b98a2c
7 changed files with 1023 additions and 28 deletions

541
lib/NfcoreSchema.groovy Normal file
View File

@ -0,0 +1,541 @@
//
// This file holds several functions used to perform JSON parameter validation, help and summary rendering for the nf-core pipeline template.
//
// MIT License
//
// Copyright (c) 2018 nf-core
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import org.everit.json.schema.Schema
import org.everit.json.schema.loader.SchemaLoader
import org.everit.json.schema.ValidationException
import org.json.JSONObject
import org.json.JSONTokener
import org.json.JSONArray
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
class NfcoreSchema {
//
// Resolve Schema path relative to main workflow directory
//
public static String getSchemaPath(workflow, schema_filename='nextflow_schema.json') {
return "${workflow.projectDir}/${schema_filename}"
}
//
// Function to loop over all parameters defined in schema and check
// whether the given parameters adhere to the specifications
//
/* groovylint-disable-next-line UnusedPrivateMethodParameter */
public static void validateParameters(workflow, params, log, schema_filename='nextflow_schema.json') {
def has_error = false
//=====================================================================//
// Check for nextflow core params and unexpected params
def json = new File(getSchemaPath(workflow, schema_filename=schema_filename)).text
def Map schemaParams = (Map) new JsonSlurper().parseText(json).get('definitions')
def nf_params = [
// Options for base `nextflow` command
'bg',
'c',
'C',
'config',
'd',
'D',
'dockerize',
'h',
'log',
'q',
'quiet',
'syslog',
'v',
'version',
// Options for `nextflow run` command
'ansi',
'ansi-log',
'bg',
'bucket-dir',
'c',
'cache',
'config',
'dsl2',
'dump-channels',
'dump-hashes',
'E',
'entry',
'latest',
'lib',
'main-script',
'N',
'name',
'offline',
'params-file',
'pi',
'plugins',
'poll-interval',
'pool-size',
'profile',
'ps',
'qs',
'queue-size',
'r',
'resume',
'revision',
'stdin',
'stub',
'stub-run',
'test',
'w',
'with-charliecloud',
'with-conda',
'with-dag',
'with-docker',
'with-mpi',
'with-notification',
'with-podman',
'with-report',
'with-singularity',
'with-timeline',
'with-tower',
'with-trace',
'with-weblog',
'without-docker',
'without-podman',
'work-dir'
]
def unexpectedParams = []
// Collect expected parameters from the schema
def expectedParams = []
for (group in schemaParams) {
for (p in group.value['properties']) {
expectedParams.push(p.key)
}
}
for (specifiedParam in params.keySet()) {
// nextflow params
if (nf_params.contains(specifiedParam)) {
log.error "ERROR: You used a core Nextflow option with two hyphens: '--${specifiedParam}'. Please resubmit with '-${specifiedParam}'"
has_error = true
}
// unexpected params
def params_ignore = params.schema_ignore_params.split(',') + 'schema_ignore_params'
def expectedParamsLowerCase = expectedParams.collect{ it.replace("-", "").toLowerCase() }
def specifiedParamLowerCase = specifiedParam.replace("-", "").toLowerCase()
def isCamelCaseBug = (specifiedParam.contains("-") && !expectedParams.contains(specifiedParam) && expectedParamsLowerCase.contains(specifiedParamLowerCase))
if (!expectedParams.contains(specifiedParam) && !params_ignore.contains(specifiedParam) && !isCamelCaseBug) {
// Temporarily remove camelCase/camel-case params #1035
def unexpectedParamsLowerCase = unexpectedParams.collect{ it.replace("-", "").toLowerCase()}
if (!unexpectedParamsLowerCase.contains(specifiedParamLowerCase)){
unexpectedParams.push(specifiedParam)
}
}
}
//=====================================================================//
// Validate parameters against the schema
InputStream input_stream = new File(getSchemaPath(workflow, schema_filename=schema_filename)).newInputStream()
JSONObject raw_schema = new JSONObject(new JSONTokener(input_stream))
// Remove anything that's in params.schema_ignore_params
raw_schema = removeIgnoredParams(raw_schema, params)
Schema schema = SchemaLoader.load(raw_schema)
// Clean the parameters
def cleanedParams = cleanParameters(params)
// Convert to JSONObject
def jsonParams = new JsonBuilder(cleanedParams)
JSONObject params_json = new JSONObject(jsonParams.toString())
// Validate
try {
schema.validate(params_json)
} catch (ValidationException e) {
println ''
log.error 'ERROR: Validation of pipeline parameters failed!'
JSONObject exceptionJSON = e.toJSON()
printExceptions(exceptionJSON, params_json, log)
println ''
has_error = true
}
// Check for unexpected parameters
if (unexpectedParams.size() > 0) {
Map colors = NfcoreTemplate.logColours(params.monochrome_logs)
println ''
def warn_msg = 'Found unexpected parameters:'
for (unexpectedParam in unexpectedParams) {
warn_msg = warn_msg + "\n* --${unexpectedParam}: ${params[unexpectedParam].toString()}"
}
log.warn warn_msg
log.info "- ${colors.dim}Ignore this warning: params.schema_ignore_params = \"${unexpectedParams.join(',')}\" ${colors.reset}"
println ''
}
if (has_error) {
System.exit(1)
}
}
//
// Beautify parameters for --help
//
public static String paramsHelp(workflow, params, command, schema_filename='nextflow_schema.json') {
Map colors = NfcoreTemplate.logColours(params.monochrome_logs)
Integer num_hidden = 0
String output = ''
output += 'Typical pipeline command:\n\n'
output += " ${colors.cyan}${command}${colors.reset}\n\n"
Map params_map = paramsLoad(getSchemaPath(workflow, schema_filename=schema_filename))
Integer max_chars = paramsMaxChars(params_map) + 1
Integer desc_indent = max_chars + 14
Integer dec_linewidth = 160 - desc_indent
for (group in params_map.keySet()) {
Integer num_params = 0
String group_output = colors.underlined + colors.bold + group + colors.reset + '\n'
def group_params = params_map.get(group) // This gets the parameters of that particular group
for (param in group_params.keySet()) {
if (group_params.get(param).hidden && !params.show_hidden_params) {
num_hidden += 1
continue;
}
def type = '[' + group_params.get(param).type + ']'
def description = group_params.get(param).description
def defaultValue = group_params.get(param).default ? " [default: " + group_params.get(param).default.toString() + "]" : ''
def description_default = description + colors.dim + defaultValue + colors.reset
// Wrap long description texts
// Loosely based on https://dzone.com/articles/groovy-plain-text-word-wrap
if (description_default.length() > dec_linewidth){
List olines = []
String oline = "" // " " * indent
description_default.split(" ").each() { wrd ->
if ((oline.size() + wrd.size()) <= dec_linewidth) {
oline += wrd + " "
} else {
olines += oline
oline = wrd + " "
}
}
olines += oline
description_default = olines.join("\n" + " " * desc_indent)
}
group_output += " --" + param.padRight(max_chars) + colors.dim + type.padRight(10) + colors.reset + description_default + '\n'
num_params += 1
}
group_output += '\n'
if (num_params > 0){
output += group_output
}
}
if (num_hidden > 0){
output += colors.dim + "!! Hiding $num_hidden params, use --show_hidden_params to show them !!\n" + colors.reset
}
output += NfcoreTemplate.dashedLine(params.monochrome_logs)
return output
}
//
// Groovy Map summarising parameters/workflow options used by the pipeline
//
public static LinkedHashMap paramsSummaryMap(workflow, params, schema_filename='nextflow_schema.json') {
// Get a selection of core Nextflow workflow options
def Map workflow_summary = [:]
if (workflow.revision) {
workflow_summary['revision'] = workflow.revision
}
workflow_summary['runName'] = workflow.runName
if (workflow.containerEngine) {
workflow_summary['containerEngine'] = workflow.containerEngine
}
if (workflow.container) {
workflow_summary['container'] = workflow.container
}
workflow_summary['launchDir'] = workflow.launchDir
workflow_summary['workDir'] = workflow.workDir
workflow_summary['projectDir'] = workflow.projectDir
workflow_summary['userName'] = workflow.userName
workflow_summary['profile'] = workflow.profile
workflow_summary['configFiles'] = workflow.configFiles.join(', ')
// Get pipeline parameters defined in JSON Schema
def Map params_summary = [:]
def blacklist = ['hostnames']
def params_map = paramsLoad(getSchemaPath(workflow, schema_filename=schema_filename))
for (group in params_map.keySet()) {
def sub_params = new LinkedHashMap()
def group_params = params_map.get(group) // This gets the parameters of that particular group
for (param in group_params.keySet()) {
if (params.containsKey(param) && !blacklist.contains(param)) {
def params_value = params.get(param)
def schema_value = group_params.get(param).default
def param_type = group_params.get(param).type
if (schema_value != null) {
if (param_type == 'string') {
if (schema_value.contains('$projectDir') || schema_value.contains('${projectDir}')) {
def sub_string = schema_value.replace('\$projectDir', '')
sub_string = sub_string.replace('\${projectDir}', '')
if (params_value.contains(sub_string)) {
schema_value = params_value
}
}
if (schema_value.contains('$params.outdir') || schema_value.contains('${params.outdir}')) {
def sub_string = schema_value.replace('\$params.outdir', '')
sub_string = sub_string.replace('\${params.outdir}', '')
if ("${params.outdir}${sub_string}" == params_value) {
schema_value = params_value
}
}
}
}
// We have a default in the schema, and this isn't it
if (schema_value != null && params_value != schema_value) {
sub_params.put(param, params_value)
}
// No default in the schema, and this isn't empty
else if (schema_value == null && params_value != "" && params_value != null && params_value != false) {
sub_params.put(param, params_value)
}
}
}
params_summary.put(group, sub_params)
}
return [ 'Core Nextflow options' : workflow_summary ] << params_summary
}
//
// Beautify parameters for summary and return as string
//
public static String paramsSummaryLog(workflow, params) {
Map colors = NfcoreTemplate.logColours(params.monochrome_logs)
String output = ''
def params_map = paramsSummaryMap(workflow, params)
def max_chars = paramsMaxChars(params_map)
for (group in params_map.keySet()) {
def group_params = params_map.get(group) // This gets the parameters of that particular group
if (group_params) {
output += colors.bold + group + colors.reset + '\n'
for (param in group_params.keySet()) {
output += " " + colors.blue + param.padRight(max_chars) + ": " + colors.green + group_params.get(param) + colors.reset + '\n'
}
output += '\n'
}
}
output += "!! Only displaying parameters that differ from the pipeline defaults !!\n"
output += NfcoreTemplate.dashedLine(params.monochrome_logs)
return output
}
//
// Loop over nested exceptions and print the causingException
//
private static void printExceptions(ex_json, params_json, log) {
def causingExceptions = ex_json['causingExceptions']
if (causingExceptions.length() == 0) {
def m = ex_json['message'] =~ /required key \[([^\]]+)\] not found/
// Missing required param
if (m.matches()) {
log.error "* Missing required parameter: --${m[0][1]}"
}
// Other base-level error
else if (ex_json['pointerToViolation'] == '#') {
log.error "* ${ex_json['message']}"
}
// Error with specific param
else {
def param = ex_json['pointerToViolation'] - ~/^#\//
def param_val = params_json[param].toString()
log.error "* --${param}: ${ex_json['message']} (${param_val})"
}
}
for (ex in causingExceptions) {
printExceptions(ex, params_json, log)
}
}
//
// Remove an element from a JSONArray
//
private static JSONArray removeElement(json_array, element) {
def list = []
int len = json_array.length()
for (int i=0;i<len;i++){
list.add(json_array.get(i).toString())
}
list.remove(element)
JSONArray jsArray = new JSONArray(list)
return jsArray
}
//
// Remove ignored parameters
//
private static JSONObject removeIgnoredParams(raw_schema, params) {
// Remove anything that's in params.schema_ignore_params
params.schema_ignore_params.split(',').each{ ignore_param ->
if(raw_schema.keySet().contains('definitions')){
raw_schema.definitions.each { definition ->
for (key in definition.keySet()){
if (definition[key].get("properties").keySet().contains(ignore_param)){
// Remove the param to ignore
definition[key].get("properties").remove(ignore_param)
// If the param was required, change this
if (definition[key].has("required")) {
def cleaned_required = removeElement(definition[key].required, ignore_param)
definition[key].put("required", cleaned_required)
}
}
}
}
}
if(raw_schema.keySet().contains('properties') && raw_schema.get('properties').keySet().contains(ignore_param)) {
raw_schema.get("properties").remove(ignore_param)
}
if(raw_schema.keySet().contains('required') && raw_schema.required.contains(ignore_param)) {
def cleaned_required = removeElement(raw_schema.required, ignore_param)
raw_schema.put("required", cleaned_required)
}
}
return raw_schema
}
//
// Clean and check parameters relative to Nextflow native classes
//
private static Map cleanParameters(params) {
def new_params = params.getClass().newInstance(params)
for (p in params) {
// remove anything evaluating to false
if (!p['value']) {
new_params.remove(p.key)
}
// Cast MemoryUnit to String
if (p['value'].getClass() == nextflow.util.MemoryUnit) {
new_params.replace(p.key, p['value'].toString())
}
// Cast Duration to String
if (p['value'].getClass() == nextflow.util.Duration) {
new_params.replace(p.key, p['value'].toString().replaceFirst(/d(?!\S)/, "day"))
}
// Cast LinkedHashMap to String
if (p['value'].getClass() == LinkedHashMap) {
new_params.replace(p.key, p['value'].toString())
}
}
return new_params
}
//
// This function tries to read a JSON params file
//
private static LinkedHashMap paramsLoad(String json_schema) {
def params_map = new LinkedHashMap()
try {
params_map = paramsRead(json_schema)
} catch (Exception e) {
println "Could not read parameters settings from JSON. $e"
params_map = new LinkedHashMap()
}
return params_map
}
//
// Method to actually read in JSON file using Groovy.
// Group (as Key), values are all parameters
// - Parameter1 as Key, Description as Value
// - Parameter2 as Key, Description as Value
// ....
// Group
// -
private static LinkedHashMap paramsRead(String json_schema) throws Exception {
def json = new File(json_schema).text
def Map schema_definitions = (Map) new JsonSlurper().parseText(json).get('definitions')
def Map schema_properties = (Map) new JsonSlurper().parseText(json).get('properties')
/* Tree looks like this in nf-core schema
* definitions <- this is what the first get('definitions') gets us
group 1
title
description
properties
parameter 1
type
description
parameter 2
type
description
group 2
title
description
properties
parameter 1
type
description
* properties <- parameters can also be ungrouped, outside of definitions
parameter 1
type
description
*/
// Grouped params
def params_map = new LinkedHashMap()
schema_definitions.each { key, val ->
def Map group = schema_definitions."$key".properties // Gets the property object of the group
def title = schema_definitions."$key".title
def sub_params = new LinkedHashMap()
group.each { innerkey, value ->
sub_params.put(innerkey, value)
}
params_map.put(title, sub_params)
}
// Ungrouped params
def ungrouped_params = new LinkedHashMap()
schema_properties.each { innerkey, value ->
ungrouped_params.put(innerkey, value)
}
params_map.put("Other parameters", ungrouped_params)
return params_map
}
//
// Get maximum number of characters across all parameter names
//
private static Integer paramsMaxChars(params_map) {
Integer max_chars = 0
for (group in params_map.keySet()) {
def group_params = params_map.get(group) // This gets the parameters of that particular group
for (param in group_params.keySet()) {
if (param.size() > max_chars) {
max_chars = param.size()
}
}
}
return max_chars
}
}

321
lib/NfcoreTemplate.groovy Normal file
View File

@ -0,0 +1,321 @@
//
// This file holds several functions used within the nf-core pipeline template.
//
// MIT License
//
// Copyright (c) 2018 nf-core
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import org.yaml.snakeyaml.Yaml
class NfcoreTemplate {
//
// Check AWS Batch related parameters have been specified correctly
//
public static void awsBatch(workflow, params) {
if (workflow.profile.contains('awsbatch')) {
// Check params.awsqueue and params.awsregion have been set if running on AWSBatch
assert (params.awsqueue && params.awsregion) : "Specify correct --awsqueue and --awsregion parameters on AWSBatch!"
// Check outdir paths to be S3 buckets if running on AWSBatch
assert params.outdir.startsWith('s3:') : "Outdir not on S3 - specify S3 Bucket to run on AWSBatch!"
}
}
//
// Check params.hostnames
//
public static void hostName(workflow, params, log) {
Map colors = logColours(params.monochrome_logs)
if (params.hostnames) {
try {
def hostname = "hostname".execute().text.trim()
params.hostnames.each { prof, hnames ->
hnames.each { hname ->
if (hostname.contains(hname) && !workflow.profile.contains(prof)) {
log.info "=${colors.yellow}====================================================${colors.reset}=\n" +
"${colors.yellow}WARN: You are running with `-profile $workflow.profile`\n" +
" but your machine hostname is ${colors.white}'$hostname'${colors.reset}.\n" +
" ${colors.yellow_bold}Please use `-profile $prof${colors.reset}`\n" +
"=${colors.yellow}====================================================${colors.reset}="
}
}
}
} catch (Exception e) {
log.warn "[$workflow.manifest.name] Could not determine 'hostname' - skipping check. Reason: ${e.message}."
}
}
}
//
// Construct and send completion email
//
public static void email(workflow, params, summary_params, projectDir, log, multiqc_report=[], fail_mapped_reads=[:]) {
// Set up the e-mail variables
def subject = "[$workflow.manifest.name] Successful: $workflow.runName"
if (fail_mapped_reads.size() > 0) {
subject = "[$workflow.manifest.name] Partially successful (${fail_mapped_reads.size()} skipped): $workflow.runName"
}
if (!workflow.success) {
subject = "[$workflow.manifest.name] FAILED: $workflow.runName"
}
def summary = [:]
for (group in summary_params.keySet()) {
summary << summary_params[group]
}
def misc_fields = [:]
misc_fields['Date Started'] = workflow.start
misc_fields['Date Completed'] = workflow.complete
misc_fields['Pipeline script file path'] = workflow.scriptFile
misc_fields['Pipeline script hash ID'] = workflow.scriptId
if (workflow.repository) misc_fields['Pipeline repository Git URL'] = workflow.repository
if (workflow.commitId) misc_fields['Pipeline repository Git Commit'] = workflow.commitId
if (workflow.revision) misc_fields['Pipeline Git branch/tag'] = workflow.revision
misc_fields['Nextflow Version'] = workflow.nextflow.version
misc_fields['Nextflow Build'] = workflow.nextflow.build
misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp
def email_fields = [:]
email_fields['version'] = workflow.manifest.version
email_fields['runName'] = workflow.runName
email_fields['success'] = workflow.success
email_fields['dateComplete'] = workflow.complete
email_fields['duration'] = workflow.duration
email_fields['exitStatus'] = workflow.exitStatus
email_fields['errorMessage'] = (workflow.errorMessage ?: 'None')
email_fields['errorReport'] = (workflow.errorReport ?: 'None')
email_fields['commandLine'] = workflow.commandLine
email_fields['projectDir'] = workflow.projectDir
email_fields['summary'] = summary << misc_fields
email_fields['fail_mapped_reads'] = fail_mapped_reads.keySet()
email_fields['min_mapped_reads'] = params.min_mapped_reads
// On success try attach the multiqc report
def mqc_report = null
try {
if (workflow.success && !params.skip_multiqc) {
mqc_report = multiqc_report.getVal()
if (mqc_report.getClass() == ArrayList && mqc_report.size() >= 1) {
if (mqc_report.size() > 1) {
log.warn "[$workflow.manifest.name] Found multiple reports from process 'MULTIQC', will use only one"
}
mqc_report = mqc_report[0]
}
}
} catch (all) {
if (multiqc_report) {
log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email"
}
}
// Check if we are only sending emails on failure
def email_address = params.email
if (!params.email && params.email_on_fail && !workflow.success) {
email_address = params.email_on_fail
}
// Render the TXT template
def engine = new groovy.text.GStringTemplateEngine()
def tf = new File("$projectDir/assets/email_template.txt")
def txt_template = engine.createTemplate(tf).make(email_fields)
def email_txt = txt_template.toString()
// Render the HTML template
def hf = new File("$projectDir/assets/email_template.html")
def html_template = engine.createTemplate(hf).make(email_fields)
def email_html = html_template.toString()
// Render the sendmail template
def max_multiqc_email_size = params.max_multiqc_email_size as nextflow.util.MemoryUnit
def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "$projectDir", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ]
def sf = new File("$projectDir/assets/sendmail_template.txt")
def sendmail_template = engine.createTemplate(sf).make(smail_fields)
def sendmail_html = sendmail_template.toString()
// Send the HTML e-mail
Map colors = logColours(params.monochrome_logs)
if (email_address) {
try {
if (params.plaintext_email) { throw GroovyException('Send plaintext e-mail, not HTML') }
// Try to send HTML e-mail using sendmail
[ 'sendmail', '-t' ].execute() << sendmail_html
log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (sendmail)-"
} catch (all) {
// Catch failures and try with plaintext
def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ]
if ( mqc_report.size() <= max_multiqc_email_size.toBytes() ) {
mail_cmd += [ '-A', mqc_report ]
}
mail_cmd.execute() << email_html
log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-"
}
}
// Write summary e-mail HTML to a file
def output_d = new File("${params.outdir}/pipeline_info/")
if (!output_d.exists()) {
output_d.mkdirs()
}
def output_hf = new File(output_d, "pipeline_report.html")
output_hf.withWriter { w -> w << email_html }
def output_tf = new File(output_d, "pipeline_report.txt")
output_tf.withWriter { w -> w << email_txt }
}
//
// Print pipeline summary on completion
//
public static void summary(workflow, params, log, fail_mapped_reads=[:], pass_mapped_reads=[:]) {
Map colors = logColours(params.monochrome_logs)
if (pass_mapped_reads.size() > 0) {
def idx = 0
def samp_aln = ''
def total_aln_count = pass_mapped_reads.size() + fail_mapped_reads.size()
for (samp in pass_mapped_reads) {
samp_aln += " ${samp.value}: ${samp.key}\n"
idx += 1
if (idx > 5) {
samp_aln += " ..see pipeline reports for full list\n"
break;
}
}
log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} ${pass_mapped_reads.size()}/$total_aln_count samples passed Bowtie2 ${params.min_mapped_reads} mapped read threshold:\n${samp_aln}${colors.reset}-"
}
if (fail_mapped_reads.size() > 0) {
def samp_aln = ''
for (samp in fail_mapped_reads) {
samp_aln += " ${samp.value}: ${samp.key}\n"
}
log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} ${fail_mapped_reads.size()} samples skipped since they failed Bowtie2 ${params.min_mapped_reads} mapped read threshold:\n${samp_aln}${colors.reset}-"
}
if (workflow.success) {
if (workflow.stats.ignoredCount == 0) {
log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Pipeline completed successfully${colors.reset}-"
} else {
log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed successfully, but with errored process(es) ${colors.reset}-"
}
} else {
hostName(workflow, params, log)
log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-"
}
}
//
// ANSII Colours used for terminal logging
//
public static Map logColours(Boolean monochrome_logs) {
Map colorcodes = [:]
// Reset / Meta
colorcodes['reset'] = monochrome_logs ? '' : "\033[0m"
colorcodes['bold'] = monochrome_logs ? '' : "\033[1m"
colorcodes['dim'] = monochrome_logs ? '' : "\033[2m"
colorcodes['underlined'] = monochrome_logs ? '' : "\033[4m"
colorcodes['blink'] = monochrome_logs ? '' : "\033[5m"
colorcodes['reverse'] = monochrome_logs ? '' : "\033[7m"
colorcodes['hidden'] = monochrome_logs ? '' : "\033[8m"
// Regular Colors
colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m"
colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m"
colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m"
colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m"
colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m"
colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m"
colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m"
colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m"
// Bold
colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m"
colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m"
colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m"
colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m"
colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m"
colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m"
colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m"
colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m"
// Underline
colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m"
colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m"
colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m"
colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m"
colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m"
colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m"
colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m"
colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m"
// High Intensity
colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m"
colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m"
colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m"
colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m"
colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m"
colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m"
colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m"
colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m"
// Bold High Intensity
colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m"
colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m"
colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m"
colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m"
colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m"
colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m"
colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m"
colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m"
return colorcodes
}
//
// Does what is says on the tin
//
public static String dashedLine(monochrome_logs) {
Map colors = logColours(monochrome_logs)
return "-${colors.dim}----------------------------------------------------${colors.reset}-"
}
//
// nf-core logo
//
public static String logo(workflow, monochrome_logs) {
Map colors = logColours(monochrome_logs)
String.format(
"""\n
${dashedLine(monochrome_logs)}
${colors.green},--.${colors.black}/${colors.green},-.${colors.reset}
${colors.blue} ___ __ __ __ ___ ${colors.green}/,-._.--~\'${colors.reset}
${colors.blue} |\\ | |__ __ / ` / \\ |__) |__ ${colors.yellow}} {${colors.reset}
${colors.blue} | \\| | \\__, \\__/ | \\ |___ ${colors.green}\\`-._,-`-,${colors.reset}
${colors.green}`._,._,\'${colors.reset}
${colors.purple} ${workflow.manifest.name} v${workflow.manifest.version}${colors.reset}
${dashedLine(monochrome_logs)}
""".stripIndent()
)
}
}

44
lib/WorkflowMain.groovy Normal file
View File

@ -0,0 +1,44 @@
class WorkflowMain {
// Citation string for pipeline
public static String citation(workflow) {
return "If you use wf-template for your analysis please cite:\n\n" +
"* The nf-core framework\n" +
" https://doi.org/10.1038/s41587-020-0439-x\n\n"
}
// Print help to screen
public static String help(workflow, params, log) {
def command = "nextflow run epi2me-labs/wf-template --fastq <input folder> -profile docker"
def help_string = ''
help_string += NfcoreSchema.paramsHelp(workflow, params, command)
help_string += '\n' + citation(workflow) + '\n'
return help_string
}
// Print parameter summary log to screen
public static String paramsSummaryLog(workflow, params, log) {
def summary_log = ''
summary_log += NfcoreSchema.paramsSummaryLog(workflow, params)
summary_log += '\n' + citation(workflow) + '\n'
return summary_log
}
// Validate parameters and print summary to screen
public static void initialise(workflow, params, log) {
// Print help to screen if required
if (params.help) {
log.info help(workflow, params, log)
System.exit(0)
}
// Validate workflow parameters via the JSON schema
if (params.validate_params) {
NfcoreSchema.validateParameters(workflow, params, log)
}
// Print parameter summary log to screen
log.info paramsSummaryLog(workflow, params, log)
}
}

Binary file not shown.

29
main.nf
View File

@ -15,22 +15,6 @@ nextflow.enable.dsl = 2
include { fastq_ingress } from './lib/fastqingress' include { fastq_ingress } from './lib/fastqingress'
def helpMessage(){
log.info """
Workflow template'
Usage:
nextflow run epi2melabs/wf-template [options]
Script Options:
--fastq DIR Path to FASTQ directory (required)
--samples FILE CSV file with columns named `barcode` and `sample_name`
(or simply a sample name for non-multiplexed data).
--out_dir DIR Path for output (default: $params.out_dir)
--report_name STR Optional report suffix (default: $params.report_name)
"""
}
process summariseReads { process summariseReads {
// concatenate fastq and fastq.gz in a dir // concatenate fastq and fastq.gz in a dir
@ -121,20 +105,9 @@ workflow pipeline {
} }
// entrypoint workflow // entrypoint workflow
WorkflowMain.initialise(workflow, params, log)
workflow { workflow {
if (params.help) {
helpMessage()
exit 1
}
if (!params.fastq) {
helpMessage()
println("")
println("`--fastq` is required")
exit 1
}
samples = fastq_ingress( samples = fastq_ingress(
params.fastq, params.out_dir, params.samples, params.sanitize_fastq) params.fastq, params.out_dir, params.samples, params.sanitize_fastq)

View File

@ -20,8 +20,22 @@ params {
aws_image_prefix = null aws_image_prefix = null
aws_queue = null aws_queue = null
report_name = "report" report_name = "report"
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'
} }
manifest {
name = 'epi2me-labs/wf-template'
author = 'Oxford Nanopore Technologies'
homePage = 'https://github.com/epi2me-labs/wf-template'
description = 'Template workflow'
mainScript = 'main.nf'
nextflowVersion = '>=20.10.0'
//version = 'v0.0.7' // TODO: do switch to this?
}
executor { executor {
$local { $local {

102
nextflow_schema.json Normal file
View File

@ -0,0 +1,102 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/./master/nextflow_schema.json",
"title": ". pipeline parameters",
"description": "",
"type": "object",
"definitions": {
"basic_input_output_options": {
"title": "Basic Input/Output Options",
"type": "object",
"fa_icon": "fas fa-terminal",
"description": "Define where the pipeline should find input data and save output data.",
"properties": {
"out_dir": {
"type": "string",
"default": "output",
"description": "Directory for output of all user-facing files."
},
"fastq": {
"type": "string",
"description": "Directory containing fastq input files. May contain fastq files directly or directories name barcodeXX relating to independent samples."
},
"samples": {
"type": "string",
"description": "CSV file with columns named `barcode` and `sample_name`. (or simply a sample name for non-multiplexed data)"
},
"sanitize_fastq": {
"type": "boolean",
"description": "Use additional heuristics to identify barcodes from file paths.",
"help_text": "Enabling this option will group together files into samples by the presence of strings of the form `barcodeXXX` present in filenames, rather than simply files grouped into directories (as output by MinKNOW and the Guppy basecaller)."
}
},
"required": [
"fastq"
]
},
"meta_data": {
"title": "Meta Data",
"type": "object",
"description": "",
"default": "",
"properties": {
"report_name": {
"type": "string",
"default": "report",
"description": "Output report filename suffix."
}
}
},
"generic_options": {
"title": "Generic options",
"type": "object",
"fa_icon": "far fa-question-circle",
"description": "Less common options for the pipeline, typically set in a config file.",
"help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.",
"properties": {
"help": {
"type": "boolean",
"description": "Display help text.",
"fa_icon": "fas fa-question-circle",
"hidden": true
}
}
}
},
"allOf": [
{
"$ref": "#/definitions/basic_input_output_options"
},
{
"$ref": "#/definitions/meta_data"
},
{
"$ref": "#/definitions/generic_options"
}
],
"properties": {
"aws_image_prefix": {
"type": "string",
"hidden": true
},
"aws_queue": {
"type": "string",
"hidden": true
},
"wfversion": {
"type": "string",
"default": "v0.3.3",
"hidden": true
},
"monochrome_logs": {
"type": "boolean"
},
"validate_params": {
"type": "boolean",
"default": true
},
"show_hidden_params": {
"type": "boolean"
}
}
}