[CW-7214] Tidy up CLI parsing

This commit is contained in:
Chris Wright 2026-05-14 17:41:21 +00:00
parent a08e23849d
commit 5af948dabf
10 changed files with 674 additions and 443 deletions

View File

@ -57,7 +57,7 @@ docker-run:
parallel: parallel:
matrix: matrix:
- MATRIX_NAME: [ - MATRIX_NAME: [
"de-poscounts-fallback", "discover", "igv", "discover", "igv",
"smoke_discover", "smoke_fixed", "smoke_direct_rna", "smoke_de", "smoke_discover", "smoke_fixed", "smoke_direct_rna", "smoke_de",
"no_annotation", "invalid_mode", "conflicting_flags" "no_annotation", "invalid_mode", "conflicting_flags"
] ]
@ -81,18 +81,6 @@ docker-run:
-c ${CI_PROJECT_NAME}/data/demo.nextflow.config " -c ${CI_PROJECT_NAME}/data/demo.nextflow.config "
ASSERT_NEXTFLOW_FAILURE: "1" ASSERT_NEXTFLOW_FAILURE: "1"
ASSERT_NEXTFLOW_FAILURE_REXP: "Missing required parameter: --ref_annotation" ASSERT_NEXTFLOW_FAILURE_REXP: "Missing required parameter: --ref_annotation"
- if: $MATRIX_NAME == "de-poscounts-fallback"
variables:
NF_BEFORE_SCRIPT: "mkdir -p ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/differential_expression.tar.gz https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/differential_expression.tar.gz && tar -xzvf ${CI_PROJECT_NAME}/data/differential_expression.tar.gz -C ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/demo.nextflow.config https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/demo.nextflow.config;"
NF_WORKFLOW_OPTS: "--fastq ${CI_PROJECT_NAME}/data/differential_expression/differential_expression_fastq \
--de_analysis \
--ref_genome ${CI_PROJECT_NAME}/data/differential_expression/hg38_chr20.fa \
--ref_annotation ${CI_PROJECT_NAME}/data/differential_expression/gencode.v22.annotation.chr20.gtf \
--direct_rna --minimap2_index_opts '-k 15' --sample_sheet ${CI_PROJECT_NAME}/data/differential_expression/sample_sheet.csv \
-c ${CI_PROJECT_NAME}/data/demo.nextflow.config "
NF_IGNORE_PROCESSES: faidx,gz_faidx,merge_transcriptomes,decompress_annotation,decompress_ref,decompress_transcriptome,preprocess_ref_transcriptome
AFTER_NEXTFLOW_CMD: >
grep -Eq '"deseq2_size_factor_method": "poscounts"' ${CI_PROJECT_NAME}/de_analysis/de_qc_stats.json
- if: $MATRIX_NAME == "only_differential_expression" - if: $MATRIX_NAME == "only_differential_expression"
variables: variables:
NF_BEFORE_SCRIPT: "mkdir -p ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/differential_expression.tar.gz https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/differential_expression.tar.gz && tar -xzvf ${CI_PROJECT_NAME}/data/differential_expression.tar.gz -C ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/demo.nextflow.config https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/demo.nextflow.config;" NF_BEFORE_SCRIPT: "mkdir -p ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/differential_expression.tar.gz https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/differential_expression.tar.gz && tar -xzvf ${CI_PROJECT_NAME}/data/differential_expression.tar.gz -C ${CI_PROJECT_NAME}/data/ && wget -nv -O ${CI_PROJECT_NAME}/data/demo.nextflow.config https://ont-exd-int-s3-euwst1-epi2me-labs.s3.amazonaws.com/wf-isoforms/demo.nextflow.config;"

View File

@ -4,23 +4,18 @@ export(bambu_discovery_enabled)
export(bambu_filter_transcripts) export(bambu_filter_transcripts)
export(bambu_normalise_tsv_df) export(bambu_normalise_tsv_df)
export(bambu_resolve_inputs) export(bambu_resolve_inputs)
export(bambu_resolve_ndr)
export(bambu_strip_alias) export(bambu_strip_alias)
export(bambu_validate_args)
export(bambu_write_outputs) export(bambu_write_outputs)
export(de_analysis_arg_parser) export(de_analysis_arg_parser)
export(de_build_contrast_name)
export(de_parse_covariates)
export(de_validate_inputs) export(de_validate_inputs)
export(main_run_bambu) export(main_run_bambu)
export(main_run_de_analysis) export(main_run_de_analysis)
export(run_bambu_cli) export(run_bambu_cli)
export(run_de_analysis_cli) export(run_de_analysis_cli)
export(workflow_glue_r_arg_parser_from_spec)
export(workflow_glue_r_cli) export(workflow_glue_r_cli)
export(workflow_glue_r_components) export(workflow_glue_r_components)
export(workflow_glue_r_arg_missing)
export(workflow_glue_r_empty_tsv) export(workflow_glue_r_empty_tsv)
export(workflow_glue_r_normalise_args)
export(workflow_glue_r_normalise_tsv_df) export(workflow_glue_r_normalise_tsv_df)
export(workflow_glue_r_parse_csv_list) export(workflow_glue_r_parse_csv_list)
export(workflow_glue_r_read_csv)
export(workflow_glue_r_require_args)

View File

@ -0,0 +1,213 @@
workflow_glue_r_parse_csv_list <- function(value) {
if (is.null(value) || length(value) == 0 || (length(value) == 1 && is.na(value))) {
return(character(0))
}
values <- trimws(strsplit(value, ",", fixed = TRUE)[[1]])
values[nzchar(values)]
}
workflow_glue_r_flag_present <- function(raw_argv, flag) {
if (is.null(raw_argv) || length(raw_argv) == 0) {
return(FALSE)
}
any(raw_argv == flag | startsWith(raw_argv, paste0(flag, "=")))
}
workflow_glue_r_arg_parser_from_spec <- function(description, arg_spec) {
parser <- argparser::arg_parser(description)
for (arg in arg_spec) {
add_args <- list(
parser = parser,
arg = arg$flag,
help = arg$help,
type = arg$type
)
if ("default" %in% names(arg)) {
add_args$default <- arg$default
}
parser <- do.call(argparser::add_argument, add_args)
}
parser
}
workflow_glue_r_arg_value_error <- function(arg, fallback) {
if ("value_error" %in% names(arg)) {
return(arg$value_error)
}
fallback
}
workflow_glue_r_scalar_arg <- function(value, arg) {
if (length(value) != 1) {
stop(
sprintf(
"%s must be a single %s value.",
arg$flag,
arg$type
),
call. = FALSE
)
}
value
}
workflow_glue_r_normalise_arg_value <- function(value, arg, flag_provided = FALSE) {
value_is_na <- length(value) == 1 && is.na(value)
value_is_absent <- is.null(value) ||
length(value) == 0 ||
(value_is_na && !isTRUE(flag_provided)) ||
(is.character(value) && length(value) == 1 && !nzchar(value))
if (value_is_absent) {
if ("default" %in% names(arg)) {
return(arg$default)
}
return(NULL)
}
value <- workflow_glue_r_scalar_arg(value, arg)
if (identical(arg$type, "character")) {
if (is.na(value)) {
stop(
workflow_glue_r_arg_value_error(arg, sprintf("%s must be a non-empty string.", arg$flag)),
call. = FALSE
)
}
return(as.character(value))
}
if (identical(arg$type, "integer")) {
numeric_value <- suppressWarnings(as.numeric(value))
integer_value <- suppressWarnings(as.integer(numeric_value))
if (
is.na(numeric_value) ||
is.na(integer_value) ||
!is.finite(numeric_value) ||
numeric_value != integer_value
) {
stop(
workflow_glue_r_arg_value_error(arg, sprintf("%s must be an integer.", arg$flag)),
call. = FALSE
)
}
return(integer_value)
}
if (identical(arg$type, "numeric")) {
numeric_value <- suppressWarnings(as.numeric(value))
if (is.na(numeric_value)) {
stop(
workflow_glue_r_arg_value_error(arg, sprintf("%s must be numeric.", arg$flag)),
call. = FALSE
)
}
return(numeric_value)
}
if (identical(arg$type, "logical")) {
logical_value <- suppressWarnings(as.logical(value))
if (length(logical_value) != 1 || is.na(logical_value)) {
stop(
workflow_glue_r_arg_value_error(arg, sprintf("%s must be true or false.", arg$flag)),
call. = FALSE
)
}
return(logical_value)
}
value
}
workflow_glue_r_validate_arg_value <- function(value, arg) {
if (is.null(value)) {
return(invisible(value))
}
if ("choices" %in% names(arg) && !value %in% arg$choices) {
stop(
sprintf(
"%s must be one of: %s",
arg$name,
paste(arg$choices, collapse = ", ")
),
call. = FALSE
)
}
if (
("min" %in% names(arg) && value < arg$min) ||
("max" %in% names(arg) && value > arg$max)
) {
stop(
workflow_glue_r_arg_value_error(
arg,
sprintf(
"%s must be between %s and %s.",
arg$flag,
if ("min" %in% names(arg)) arg$min else "-Inf",
if ("max" %in% names(arg)) arg$max else "Inf"
)
),
call. = FALSE
)
}
invisible(value)
}
workflow_glue_r_normalise_args <- function(argv, arg_spec, raw_argv = NULL) {
for (arg in arg_spec) {
argv[[arg$name]] <- workflow_glue_r_normalise_arg_value(
argv[[arg$name]],
arg,
flag_provided = workflow_glue_r_flag_present(raw_argv, arg$flag)
)
workflow_glue_r_validate_arg_value(argv[[arg$name]], arg)
}
required_args <- vapply(arg_spec, function(arg) {
isTRUE(arg$required)
}, logical(1))
required_arg_names <- vapply(arg_spec[required_args], `[[`, character(1), "name")
missing_args <- required_arg_names[vapply(required_arg_names, function(arg_name) {
is.null(argv[[arg_name]])
}, logical(1))]
if (length(missing_args) > 0) {
stop(
sprintf(
"Missing required arguments: %s",
paste(sprintf("--%s", missing_args), collapse = ", ")
),
call. = FALSE
)
}
xor_args <- vapply(arg_spec, function(arg) {
"xor_group" %in% names(arg)
}, logical(1))
xor_groups <- unique(vapply(arg_spec[xor_args], `[[`, character(1), "xor_group"))
for (xor_group in xor_groups) {
group_args <- arg_spec[vapply(arg_spec, function(arg) {
identical(arg$xor_group, xor_group)
}, logical(1))]
group_arg_names <- vapply(group_args, `[[`, character(1), "name")
present_args <- vapply(group_arg_names, function(arg_name) {
!is.null(argv[[arg_name]])
}, logical(1))
if (sum(present_args) != 1) {
group_flags <- vapply(group_args, `[[`, character(1), "flag")
stop(
sprintf(
"Provide exactly one of %s.",
paste(group_flags, collapse = " or ")
),
call. = FALSE
)
}
}
argv
}

View File

@ -1,66 +1,89 @@
bambu_arg_parser <- function() { bambu_arg_spec <- function() {
parser <- argparser::arg_parser("Run bambu transcript discovery and quantification.") list(
parser <- argparser::add_argument(parser, "--bams", help = "Comma-separated BAM paths.") list(
parser <- argparser::add_argument(parser, "--aliases", help = "Comma-separated aliases for --bams.") name = "bams",
parser <- argparser::add_argument(parser, "--sample_sheet", help = "Optional sample sheet CSV.") flag = "--bams",
parser <- argparser::add_argument(parser, "--annotation", help = "Reference annotation GTF/GFF.") help = "Comma-separated BAM paths.",
parser <- argparser::add_argument(parser, "--genome", help = "Reference genome FASTA.") type = "character",
parser <- argparser::add_argument( required = TRUE
parser,
"--transcriptome_mode",
help = "discover or fixed_annotation.",
default = "discover"
)
parser <- argparser::add_argument(
parser,
"--threads",
help = "Number of worker threads.",
type = "numeric",
default = 1
)
parser <- argparser::add_argument(
parser,
"--ndr",
help = "Optional novel discovery rate.",
type = "numeric"
)
argparser::add_argument(parser, "--out_dir", help = "Output directory.")
}
bambu_validate_args <- function(argv) {
workflow_glue_r_require_args(argv, c("annotation", "genome", "out_dir"))
if (workflow_glue_r_arg_missing(argv$bams)) {
stop("Missing required arguments: --bams", call. = FALSE)
}
if (workflow_glue_r_arg_missing(argv$aliases)) {
stop("Missing required arguments: --aliases", call. = FALSE)
}
if (!argv$transcriptome_mode %in% c("discover", "fixed_annotation")) {
stop(
sprintf(
"transcriptome_mode must be one of: %s",
paste(c("discover", "fixed_annotation"), collapse = ", ")
), ),
call. = FALSE list(
name = "aliases",
flag = "--aliases",
help = "Comma-separated aliases for --bams.",
type = "character",
required = TRUE
),
list(
name = "sample_sheet",
flag = "--sample_sheet",
help = "Optional sample sheet CSV.",
type = "character"
),
list(
name = "annotation",
flag = "--annotation",
help = "Reference annotation GTF/GFF.",
type = "character",
required = TRUE
),
list(
name = "genome",
flag = "--genome",
help = "Reference genome FASTA.",
type = "character",
required = TRUE
),
list(
name = "transcriptome_mode",
flag = "--transcriptome_mode",
help = "discover or fixed_annotation.",
type = "character",
default = "discover",
choices = c("discover", "fixed_annotation")
),
list(
name = "threads",
flag = "--threads",
help = "Number of worker threads.",
type = "integer",
default = 1L,
min = 1L
),
list(
name = "ndr",
flag = "--ndr",
help = "Optional novel discovery rate.",
type = "numeric",
min = 0,
max = 1,
value_error = "NDR (Novel Discovery Rate) must be between 0 and 1"
),
list(
name = "out_dir",
flag = "--out_dir",
help = "Output directory.",
type = "character",
required = TRUE
)
) )
}
if (!workflow_glue_r_arg_missing(argv$ndr) && (argv$ndr < 0 || argv$ndr > 1)) {
stop("NDR (Novel Discovery Rate) must be between 0 and 1", call. = FALSE)
}
invisible(argv)
} }
bambu_resolve_inputs <- function( bambu_arg_parser <- function() {
argv, workflow_glue_r_arg_parser_from_spec(
bamfile_list_ctor = Rsamtools::BamFileList "Run bambu transcript discovery and quantification.",
) { bambu_arg_spec()
)
}
bambu_resolve_inputs <- function(args) {
sample_df <- NULL sample_df <- NULL
if (!workflow_glue_r_arg_missing(argv$sample_sheet)) { if (!is.null(args$sample_sheet)) {
sample_df <- workflow_glue_r_read_csv(argv$sample_sheet) sample_df <- utils::read.csv(
args$sample_sheet,
check.names = FALSE,
stringsAsFactors = FALSE
)
if (!"alias" %in% names(sample_df)) { if (!"alias" %in% names(sample_df)) {
stop("Sample sheet must contain an 'alias' column.", call. = FALSE) stop("Sample sheet must contain an 'alias' column.", call. = FALSE)
} }
@ -76,8 +99,8 @@ bambu_resolve_inputs <- function(
} }
} }
bam_paths <- workflow_glue_r_parse_csv_list(argv$bams) bam_paths <- workflow_glue_r_parse_csv_list(args$bams)
aliases <- workflow_glue_r_parse_csv_list(argv$aliases) aliases <- workflow_glue_r_parse_csv_list(args$aliases)
if (length(bam_paths) < 1) { if (length(bam_paths) < 1) {
stop("No BAM files were provided in --bams.", call. = FALSE) stop("No BAM files were provided in --bams.", call. = FALSE)
@ -116,7 +139,7 @@ bambu_resolve_inputs <- function(
reads <- if (length(bam_paths) == 1) { reads <- if (length(bam_paths) == 1) {
bam_paths bam_paths
} else { } else {
bamfile_list_ctor(bam_paths, yieldSize = 1000000) Rsamtools::BamFileList(bam_paths, yieldSize = 250000L)
} }
list( list(
@ -127,34 +150,47 @@ bambu_resolve_inputs <- function(
) )
} }
bambu_discovery_enabled <- function(argv) { bambu_discovery_enabled <- function(args) {
identical(argv$transcriptome_mode, "discover") identical(args$transcriptome_mode, "discover")
} }
bambu_resolve_ndr <- function(argv, default_ndr = 0.1) { bambu_build_args <- function(args, reads, annotation_obj) {
if (workflow_glue_r_arg_missing(argv$ndr)) {
default_ndr
} else {
as.numeric(argv$ndr)
}
}
bambu_build_args <- function(argv, reads, annotation_obj) {
bambu_args <- list( bambu_args <- list(
reads = reads, reads = reads,
annotations = annotation_obj, annotations = annotation_obj,
genome = argv$genome, genome = args$genome,
ncore = as.integer(argv$threads), ncore = args$threads,
discovery = bambu_discovery_enabled(argv) discovery = bambu_discovery_enabled(args),
lowMemory = TRUE,
yieldSize = 250000L,
verbose = TRUE
) )
if (bambu_discovery_enabled(argv)) { if (bambu_discovery_enabled(args) && !is.null(args$ndr)) {
bambu_args$NDR <- bambu_resolve_ndr(argv) bambu_args$NDR <- args$ndr
} }
bambu_args bambu_args
} }
bambu_effective_threads <- function(args, bam_count) {
# bambu's low-memory mode can have issues with multiple BAMs and
# parallel threads due to BiocFileCache writes,
# so we enforce single-threading in that case.
threads <- as.integer(args$threads)
if (bam_count > 1 && threads > 1L) {
warning(
paste(
"Low-memory mode with multiple BAMs can fail in bambu due to",
"parallel BiocFileCache writes; forcing threads=1."
),
call. = FALSE
)
return(1L)
}
threads
}
bambu_filter_transcripts <- function(se) { bambu_filter_transcripts <- function(se) {
counts_mat <- SummarizedExperiment::assays(se)$counts counts_mat <- SummarizedExperiment::assays(se)$counts
full_length_mat <- SummarizedExperiment::assays(se)$fullLengthCounts full_length_mat <- SummarizedExperiment::assays(se)$fullLengthCounts
@ -194,85 +230,6 @@ bambu_matrix_to_df <- function(se_obj, assay_name, id_col, meta_df) {
merge(meta_df, assay_df, by.x = id_col, by.y = id_col, all.y = TRUE, sort = FALSE) merge(meta_df, assay_df, by.x = id_col, by.y = id_col, all.y = TRUE, sort = FALSE)
} }
bambu_extract_gtf_attribute <- function(attr_field, key) {
match <- regexec(sprintf('%s "([^"]*)";', key), attr_field, perl = TRUE)
captures <- regmatches(attr_field, match)[[1]]
if (length(captures) < 2) {
return(NULL)
}
captures[2]
}
bambu_normalise_gtf_attribute_value <- function(value) {
if (is.null(value)) {
return(NULL)
}
value <- gsub('[";]', "", value)
value <- trimws(gsub("\\s+", " ", value))
if (!nzchar(value)) {
return(NULL)
}
value
}
bambu_sanitise_gtf_file <- function(path) {
lines <- readLines(path, warn = FALSE)
cleaned_lines <- vapply(lines, function(line) {
if (!nzchar(line) || startsWith(line, "#")) {
return(line)
}
fields <- strsplit(line, "\t", fixed = TRUE)[[1]]
if (length(fields) < 9) {
return(line)
}
attr_field <- fields[9]
transcript_id <- bambu_normalise_gtf_attribute_value(
bambu_extract_gtf_attribute(attr_field, "transcript_id")
)
gene_id <- bambu_normalise_gtf_attribute_value(
bambu_extract_gtf_attribute(attr_field, "gene_id")
)
if (!is.null(gene_id) && identical(gene_id, "transcript_id")) {
warning(
sprintf(
"Replaced malformed gene_id 'transcript_id' with transcript_id '%s'.",
transcript_id
),
call. = FALSE
)
gene_id <- transcript_id
}
if (is.null(gene_id)) {
gene_id <- transcript_id
}
if (!is.null(gene_id)) {
attr_field <- sub(
'gene_id "([^"]*)";',
sprintf('gene_id "%s";', gene_id),
attr_field,
perl = TRUE
)
}
if (!is.null(transcript_id)) {
attr_field <- sub(
'transcript_id "([^"]*)";',
sprintf('transcript_id "%s";', transcript_id),
attr_field,
perl = TRUE
)
}
fields[9] <- attr_field
paste(fields, collapse = "\t")
}, character(1))
writeLines(cleaned_lines, path)
}
bambu_format_count <- function(value) { bambu_format_count <- function(value) {
if (length(value) == 0 || all(is.na(value))) { if (length(value) == 0 || all(is.na(value))) {
return("NA") return("NA")
@ -285,17 +242,30 @@ bambu_format_count <- function(value) {
) )
} }
bambu_write_outputs <- function(se, gene_se, sample_df, argv, qc_stats, write_gtf_fn = bambu::writeToGTF) { bambu_write_matrix_tsv <- function(se_obj, assay_name, id_col, meta_df, output_path) {
write_gtf_fn( table_df <- bambu_matrix_to_df(se_obj, assay_name, id_col, meta_df)
utils::write.table(
table_df,
file = output_path,
sep = "\t",
quote = FALSE,
row.names = FALSE
)
rm(table_df)
invisible(gc(verbose = FALSE))
}
bambu_write_outputs <- function(se, gene_se, sample_df, args, qc_stats) {
bambu::writeToGTF(
SummarizedExperiment::rowRanges(se), SummarizedExperiment::rowRanges(se),
file = file.path(argv$out_dir, "transcripts.gtf") file = file.path(args$out_dir, "transcripts.gtf")
) )
saveRDS(se, file.path(argv$out_dir, "bambu_transcripts.rds")) saveRDS(se, file.path(args$out_dir, "bambu_transcripts.rds"))
saveRDS(gene_se, file.path(argv$out_dir, "bambu_genes.rds")) saveRDS(gene_se, file.path(args$out_dir, "bambu_genes.rds"))
utils::write.csv( utils::write.csv(
sample_df, sample_df,
file.path(argv$out_dir, "samples.csv"), file.path(args$out_dir, "samples.csv"),
row.names = FALSE, row.names = FALSE,
quote = FALSE quote = FALSE
) )
@ -318,58 +288,51 @@ bambu_write_outputs <- function(se, gene_se, sample_df, argv, qc_stats, write_gt
utils::write.table( utils::write.table(
tx_meta, tx_meta,
file = file.path(argv$out_dir, "transcript_metadata.tsv"), file = file.path(args$out_dir, "transcript_metadata.tsv"),
sep = "\t", sep = "\t",
quote = FALSE, quote = FALSE,
row.names = FALSE row.names = FALSE
) )
utils::write.table( utils::write.table(
gene_meta, gene_meta,
file = file.path(argv$out_dir, "gene_metadata.tsv"), file = file.path(args$out_dir, "gene_metadata.tsv"),
sep = "\t", sep = "\t",
quote = FALSE, quote = FALSE,
row.names = FALSE row.names = FALSE
) )
tx_counts <- bambu_matrix_to_df(se, "counts", "TXNAME", tx_meta) bambu_write_matrix_tsv(
tx_cpm <- bambu_matrix_to_df(se, "CPM", "TXNAME", tx_meta) se,
gene_counts <- bambu_matrix_to_df(gene_se, "counts", "GENEID", gene_meta) "counts",
gene_cpm <- bambu_matrix_to_df(gene_se, "CPM", "GENEID", gene_meta) "TXNAME",
tx_meta,
utils::write.table( file.path(args$out_dir, "transcript_counts.tsv")
tx_counts,
file = file.path(argv$out_dir, "transcript_counts.tsv"),
sep = "\t",
quote = FALSE,
row.names = FALSE
) )
utils::write.table( bambu_write_matrix_tsv(
tx_cpm, se,
file = file.path(argv$out_dir, "transcript_cpm.tsv"), "CPM",
sep = "\t", "TXNAME",
quote = FALSE, tx_meta,
row.names = FALSE file.path(args$out_dir, "transcript_cpm.tsv")
) )
utils::write.table( bambu_write_matrix_tsv(
gene_counts, gene_se,
file = file.path(argv$out_dir, "gene_counts.tsv"), "counts",
sep = "\t", "GENEID",
quote = FALSE, gene_meta,
row.names = FALSE file.path(args$out_dir, "gene_counts.tsv")
) )
utils::write.table( bambu_write_matrix_tsv(
gene_cpm, gene_se,
file = file.path(argv$out_dir, "gene_cpm.tsv"), "CPM",
sep = "\t", "GENEID",
quote = FALSE, gene_meta,
row.names = FALSE file.path(args$out_dir, "gene_cpm.tsv")
) )
bambu_sanitise_gtf_file(file.path(argv$out_dir, "transcripts.gtf")) qc_stats$transcriptome_mode <- args$transcriptome_mode
qc_stats$ndr_used <- if (bambu_discovery_enabled(args)) {
qc_stats$transcriptome_mode <- argv$transcriptome_mode if (is.null(args$ndr)) "automatic" else args$ndr
qc_stats$ndr_used <- if (bambu_discovery_enabled(argv)) {
bambu_resolve_ndr(argv)
} else { } else {
"N/A" "N/A"
} }
@ -377,7 +340,7 @@ bambu_write_outputs <- function(se, gene_se, sample_df, argv, qc_stats, write_gt
jsonlite::write_json( jsonlite::write_json(
qc_stats, qc_stats,
file.path(argv$out_dir, "bambu_qc_stats.json"), file.path(args$out_dir, "bambu_qc_stats.json"),
pretty = TRUE, pretty = TRUE,
auto_unbox = TRUE auto_unbox = TRUE
) )
@ -387,8 +350,14 @@ bambu_write_outputs <- function(se, gene_se, sample_df, argv, qc_stats, write_gt
"================================", "================================",
"", "",
sprintf("Timestamp: %s", qc_stats$timestamp), sprintf("Timestamp: %s", qc_stats$timestamp),
sprintf("Mode: %s", argv$transcriptome_mode), sprintf("Mode: %s", args$transcriptome_mode),
if (bambu_discovery_enabled(argv)) sprintf("NDR: %.3f", bambu_resolve_ndr(argv)) else NULL, if (bambu_discovery_enabled(args)) {
if (is.null(args$ndr)) {
"NDR: automatic (bambu-selected)"
} else {
sprintf("NDR: %.3f", args$ndr)
}
} else NULL,
"", "",
"Sample Statistics:", "Sample Statistics:",
sprintf(" Samples analyzed: %s", bambu_format_count(qc_stats$samples)), sprintf(" Samples analyzed: %s", bambu_format_count(qc_stats$samples)),
@ -415,49 +384,51 @@ bambu_write_outputs <- function(se, gene_se, sample_df, argv, qc_stats, write_gt
"" ""
) )
writeLines(qc_summary, file.path(argv$out_dir, "bambu_qc_summary.txt")) writeLines(qc_summary, file.path(args$out_dir, "bambu_qc_summary.txt"))
writeLines(capture.output(sessionInfo()), file.path(argv$out_dir, "session_info.txt")) writeLines(capture.output(sessionInfo()), file.path(args$out_dir, "session_info.txt"))
} }
main_run_bambu <- function( main_run_bambu <- function(
argv, args,
analysis_fn = bambu::bambu, analysis_fn = bambu::bambu,
prepare_annotations_fn = bambu::prepareAnnotations, prepare_annotations_fn = bambu::prepareAnnotations,
gene_expression_fn = bambu::transcriptToGeneExpression, gene_expression_fn = bambu::transcriptToGeneExpression
write_gtf_fn = bambu::writeToGTF,
bamfile_list_ctor = Rsamtools::BamFileList
) { ) {
set.seed(42) set.seed(42)
suppressPackageStartupMessages({ # bambu's parallel worker code may rely on these being attached for generics
library(GenomicRanges) # such as seqlengths().
library(Rsamtools) suppressPackageStartupMessages(library(GenomicRanges))
}) suppressPackageStartupMessages(library(Rsamtools))
bambu_validate_args(argv) dir.create(args$out_dir, showWarnings = FALSE, recursive = TRUE)
dir.create(argv$out_dir, showWarnings = FALSE, recursive = TRUE)
inputs <- bambu_resolve_inputs( inputs <- bambu_resolve_inputs(args)
argv, args$threads <- bambu_effective_threads(args, length(inputs$bam_paths))
bamfile_list_ctor = bamfile_list_ctor annotation_obj <- prepare_annotations_fn(args$annotation)
)
annotation_obj <- prepare_annotations_fn(argv$annotation)
ndr_value <- bambu_resolve_ndr(argv)
if (!workflow_glue_r_arg_missing(argv$ndr)) { if (!is.null(args$ndr)) {
message(sprintf("Using user-specified NDR = %.3f", ndr_value)) message(sprintf("Using user-specified NDR = %.3f", args$ndr))
} else { } else if (bambu_discovery_enabled(args)) {
message(sprintf("Using default NDR = %.3f", ndr_value)) message("Using bambu automatic NDR selection.")
} }
if (bambu_discovery_enabled(argv)) { if (bambu_discovery_enabled(args)) {
message("Novel Discovery Rate (NDR) controls transcript discovery stringency:") message("Novel Discovery Rate (NDR) controls transcript discovery stringency:")
message(" Lower NDR (e.g., 0.05) = fewer false positive transcripts, may miss real ones") message(" Lower NDR (e.g., 0.05) = fewer false positive transcripts, may miss real ones")
message(" Higher NDR (e.g., 0.2) = more sensitive discovery, more false positives") message(" Higher NDR (e.g., 0.2) = more sensitive discovery, more false positives")
message(sprintf(" Current NDR = %.3f balances precision and recall", ndr_value)) if (is.null(args$ndr)) {
message(" Current NDR = automatic (selected by bambu from the data)")
} else {
message(sprintf(" Current NDR = %.3f balances precision and recall", args$ndr))
} }
}
if (length(inputs$bam_paths) > 1) {
message("Using BamFileList yieldSize = 250000")
}
message(sprintf("Running bambu with threads = %d", args$threads))
message("Running bambu...") message("Running bambu...")
se <- do.call(analysis_fn, bambu_build_args(argv, inputs$reads, annotation_obj)) se <- do.call(analysis_fn, bambu_build_args(args, inputs$reads, annotation_obj))
message("Bambu completed successfully") message("Bambu completed successfully")
colnames(se) <- inputs$aliases colnames(se) <- inputs$aliases
@ -509,9 +480,8 @@ main_run_bambu <- function(
se, se,
gene_se, gene_se,
inputs$sample_df, inputs$sample_df,
argv, args,
qc_stats, qc_stats
write_gtf_fn = write_gtf_fn
) )
invisible( invisible(
@ -526,5 +496,6 @@ main_run_bambu <- function(
run_bambu_cli <- function(argv = commandArgs(trailingOnly = TRUE)) { run_bambu_cli <- function(argv = commandArgs(trailingOnly = TRUE)) {
parsed <- argparser::parse_args(bambu_arg_parser(), argv = argv) parsed <- argparser::parse_args(bambu_arg_parser(), argv = argv)
main_run_bambu(parsed) args <- workflow_glue_r_normalise_args(parsed, bambu_arg_spec(), raw_argv = argv)
main_run_bambu(args)
} }

View File

@ -1,38 +1,3 @@
workflow_glue_r_arg_missing <- function(value) {
if (is.null(value) || length(value) == 0 || all(is.na(value))) {
return(TRUE)
}
if (is.character(value)) {
return(all(!nzchar(value)))
}
FALSE
}
workflow_glue_r_require_args <- function(argv, required_args) {
missing_args <- required_args[vapply(required_args, function(arg_name) {
workflow_glue_r_arg_missing(argv[[arg_name]])
}, logical(1))]
if (length(missing_args) > 0) {
stop(
sprintf(
"Missing required arguments: %s",
paste(sprintf("--%s", missing_args), collapse = ", ")
),
call. = FALSE
)
}
}
workflow_glue_r_parse_csv_list <- function(value) {
if (workflow_glue_r_arg_missing(value)) {
return(character(0))
}
values <- trimws(strsplit(value, ",", fixed = TRUE)[[1]])
values[nzchar(values)]
}
workflow_glue_r_is_r_formula_name <- function(name) { workflow_glue_r_is_r_formula_name <- function(name) {
is.character(name) && is.character(name) &&
length(name) == 1 && length(name) == 1 &&
@ -54,10 +19,6 @@ workflow_glue_r_validate_r_formula_names <- function(names, label = "Column") {
invisible(names) invisible(names)
} }
workflow_glue_r_read_csv <- function(path) {
utils::read.csv(path, check.names = FALSE, stringsAsFactors = FALSE)
}
workflow_glue_r_normalise_tsv_value <- function(value) { workflow_glue_r_normalise_tsv_value <- function(value) {
if (length(value) == 0 || all(is.na(value))) { if (length(value) == 0 || all(is.na(value))) {
return(NA_character_) return(NA_character_)

View File

@ -1,27 +1,64 @@
de_analysis_arg_parser <- function() { de_analysis_arg_spec <- function() {
parser <- argparser::arg_parser("Run DESeq2 and DEXSeq on bambu output.") list(
parser <- argparser::add_argument(parser, "--transcript_rds", help = "bambu transcript RDS.") list(
parser <- argparser::add_argument(parser, "--gene_rds", help = "bambu gene RDS.") name = "transcript_rds",
parser <- argparser::add_argument(parser, "--sample_sheet", help = "Sample sheet CSV.") flag = "--transcript_rds",
parser <- argparser::add_argument( help = "bambu transcript RDS.",
parser, type = "character",
"--condition_column", required = TRUE
),
list(
name = "gene_rds",
flag = "--gene_rds",
help = "bambu gene RDS.",
type = "character",
required = TRUE
),
list(
name = "sample_sheet",
flag = "--sample_sheet",
help = "Sample sheet CSV.",
type = "character",
required = TRUE
),
list(
name = "condition_column",
flag = "--condition_column",
help = "Primary condition column.", help = "Primary condition column.",
type = "character",
default = "condition" default = "condition"
),
list(
name = "covariates",
flag = "--covariates",
help = "Comma-separated nuisance covariates.",
type = "character"
),
list(
name = "reference_level",
flag = "--reference_level",
help = "Reference level for the condition column.",
type = "character"
),
list(
name = "out_dir",
flag = "--out_dir",
help = "Output directory.",
type = "character",
default = "de_analysis"
)
) )
parser <- argparser::add_argument(parser, "--covariates", help = "Comma-separated nuisance covariates.")
parser <- argparser::add_argument(parser, "--reference_level", help = "Reference level for the condition column.")
argparser::add_argument(parser, "--out_dir", help = "Output directory.", default = "de_analysis")
} }
de_parse_covariates <- function(value) { de_analysis_arg_parser <- function() {
workflow_glue_r_parse_csv_list(value) workflow_glue_r_arg_parser_from_spec(
"Run DESeq2 and DEXSeq on bambu output.",
de_analysis_arg_spec()
)
} }
de_validate_inputs <- function(tx_se, gene_se, sample_df, argv) { de_validate_inputs <- function(tx_se, gene_se, sample_df, argv) {
workflow_glue_r_require_args(argv, c("transcript_rds", "gene_rds", "sample_sheet")) covariates <- workflow_glue_r_parse_csv_list(argv$covariates)
covariates <- de_parse_covariates(argv$covariates)
workflow_glue_r_validate_r_formula_names( workflow_glue_r_validate_r_formula_names(
c(argv$condition_column, covariates), c(argv$condition_column, covariates),
label = "Design column" label = "Design column"
@ -107,7 +144,7 @@ de_validate_inputs <- function(tx_se, gene_se, sample_df, argv) {
} }
reference_level <- argv$reference_level reference_level <- argv$reference_level
if (workflow_glue_r_arg_missing(reference_level)) { if (is.null(reference_level)) {
if ("control" %in% condition_values) { if ("control" %in% condition_values) {
reference_level <- "control" reference_level <- "control"
} else { } else {
@ -127,8 +164,6 @@ de_validate_inputs <- function(tx_se, gene_se, sample_df, argv) {
} }
list( list(
tx_se = tx_se,
gene_se = gene_se,
sample_df = sample_df, sample_df = sample_df,
covariates = covariates, covariates = covariates,
condition_values = condition_values, condition_values = condition_values,
@ -136,19 +171,6 @@ de_validate_inputs <- function(tx_se, gene_se, sample_df, argv) {
) )
} }
de_build_contrast_name <- function(condition_column, target_level, reference_level) {
sprintf("%s_%s_vs_%s", condition_column, target_level, reference_level)
}
de_set_dispersions <- function(object, value) {
setter <- get("dispersions<-", envir = asNamespace("DESeq2"))
setter(object, value = value)
}
de_extract_disp_gene_est <- function(object) {
S4Vectors::mcols(object)$dispGeneEst
}
# DESeq2's default geometric-mean size-factor estimator is undefined when # DESeq2's default geometric-mean size-factor estimator is undefined when
# every gene has at least one zero across samples, so use poscounts then. # every gene has at least one zero across samples, so use poscounts then.
de_choose_size_factor_type <- function(count_mat, context_label = "Count matrix") { de_choose_size_factor_type <- function(count_mat, context_label = "Count matrix") {
@ -161,7 +183,6 @@ de_choose_size_factor_type <- function(count_mat, context_label = "Count matrix"
} }
"ratio" "ratio"
} }
de_run_deseq_with_fallback <- function( de_run_deseq_with_fallback <- function(
dds, dds,
contrast_name, contrast_name,
@ -204,7 +225,8 @@ de_run_deseq_with_fallback <- function(
dds <- DESeq2::estimateSizeFactors(dds, type = sf_type) dds <- DESeq2::estimateSizeFactors(dds, type = sf_type)
dds <- DESeq2::estimateDispersionsGeneEst(dds) dds <- DESeq2::estimateDispersionsGeneEst(dds)
dds <- de_set_dispersions(dds, de_extract_disp_gene_est(dds)) dispersions_setter <- get("dispersions<-", envir = asNamespace("DESeq2"))
dds <- dispersions_setter(dds, value = S4Vectors::mcols(dds)$dispGeneEst)
dispersion_values <- suppressWarnings(as.numeric(DESeq2::dispersions(dds))) dispersion_values <- suppressWarnings(as.numeric(DESeq2::dispersions(dds)))
dispersion_values <- dispersion_values[is.finite(dispersion_values)] dispersion_values <- dispersion_values[is.finite(dispersion_values)]
@ -279,8 +301,7 @@ de_estimate_dispersions_with_fallback <- function(
list( list(
object = DESeq2::estimateDispersions(object), object = DESeq2::estimateDispersions(object),
method_used = "parametric", method_used = "parametric",
fallback_applied = FALSE, fallback_applied = FALSE
reason = NULL
), ),
error = function(err) { error = function(err) {
if (!grepl( if (!grepl(
@ -291,7 +312,6 @@ de_estimate_dispersions_with_fallback <- function(
stop(err) stop(err)
} }
primary_reason <- conditionMessage(err)
message( message(
context_label, context_label,
" dispersion fitting failed; retrying with fitType='local'." " dispersion fitting failed; retrying with fitType='local'."
@ -300,8 +320,7 @@ de_estimate_dispersions_with_fallback <- function(
list( list(
object = DESeq2::estimateDispersions(object, fitType = "local"), object = DESeq2::estimateDispersions(object, fitType = "local"),
method_used = "local", method_used = "local",
fallback_applied = TRUE, fallback_applied = TRUE
reason = primary_reason
), ),
error = function(local_err) { error = function(local_err) {
if (!grepl( if (!grepl(
@ -320,8 +339,7 @@ de_estimate_dispersions_with_fallback <- function(
list( list(
object = DESeq2::estimateDispersions(object, fitType = "mean"), object = DESeq2::estimateDispersions(object, fitType = "mean"),
method_used = "mean", method_used = "mean",
fallback_applied = TRUE, fallback_applied = TRUE
reason = primary_reason
), ),
error = function(mean_err) { error = function(mean_err) {
if (!grepl( if (!grepl(
@ -340,12 +358,15 @@ de_estimate_dispersions_with_fallback <- function(
" mean-fit dispersion retry failed; falling back to gene-wise dispersion estimates." " mean-fit dispersion retry failed; falling back to gene-wise dispersion estimates."
) )
object <- DESeq2::estimateDispersionsGeneEst(object) object <- DESeq2::estimateDispersionsGeneEst(object)
object <- de_set_dispersions(object, de_extract_disp_gene_est(object)) dispersions_setter <- get("dispersions<-", envir = asNamespace("DESeq2"))
object <- dispersions_setter(
object,
value = S4Vectors::mcols(object)$dispGeneEst
)
list( list(
object = object, object = object,
method_used = "gene-wise", method_used = "gene-wise",
fallback_applied = TRUE, fallback_applied = TRUE
reason = primary_reason
) )
} }
) )
@ -404,7 +425,6 @@ de_run_deseq2_result <- function(
independentFiltering = TRUE independentFiltering = TRUE
) )
list( list(
dds = dds,
result = result, result = result,
deseq2_dispersion_fallback = deseq2_dispersion_fallback deseq2_dispersion_fallback = deseq2_dispersion_fallback
) )
@ -469,15 +489,8 @@ de_run_dexseq_result <- function(
"DEXSeq", "DEXSeq",
allow_gene_est = TRUE allow_gene_est = TRUE
) )
if (is.list(dispersion_result) && !is.null(dispersion_result$object)) {
dxd <- dispersion_result$object dxd <- dispersion_result$object
dispersion_method <- dispersion_result$method_used dispersion_method <- dispersion_result$method_used
dispersion_reason <- dispersion_result$reason
} else {
dxd <- dispersion_result
dispersion_method <- "parametric"
dispersion_reason <- NULL
}
dxd <- DEXSeq::testForDEU(dxd, reducedModel = reduced_formula) dxd <- DEXSeq::testForDEU(dxd, reducedModel = reduced_formula)
dxd <- DEXSeq::estimateExonFoldChanges(dxd, fitExpToVar = condition_column) dxd <- DEXSeq::estimateExonFoldChanges(dxd, fitExpToVar = condition_column)
dxr <- DEXSeq::DEXSeqResults(dxd, independentFiltering = FALSE) dxr <- DEXSeq::DEXSeqResults(dxd, independentFiltering = FALSE)
@ -485,7 +498,6 @@ de_run_dexseq_result <- function(
dxd = dxd, dxd = dxd,
dxr = dxr, dxr = dxr,
dexseq_dispersion_method = dispersion_method, dexseq_dispersion_method = dispersion_method,
dexseq_dispersion_reason = dispersion_reason,
dexseq_size_factor_type = dexseq_sf_type dexseq_size_factor_type = dexseq_sf_type
) )
}, error = function(err) { }, error = function(err) {
@ -545,14 +557,18 @@ de_extract_dtu_transcript_table <- function(dex_df, contrast_name) {
workflow_glue_r_normalise_tsv_df(tx_dtu) workflow_glue_r_normalise_tsv_df(tx_dtu)
} }
main_run_de_analysis <- function(argv) { main_run_de_analysis <- function(args) {
set.seed(42) set.seed(42)
dir.create(argv$out_dir, showWarnings = FALSE, recursive = TRUE) dir.create(args$out_dir, showWarnings = FALSE, recursive = TRUE)
tx_se <- readRDS(argv$transcript_rds) tx_se <- readRDS(args$transcript_rds)
gene_se <- readRDS(argv$gene_rds) gene_se <- readRDS(args$gene_rds)
sample_df <- workflow_glue_r_read_csv(argv$sample_sheet) sample_df <- utils::read.csv(
validated <- de_validate_inputs(tx_se, gene_se, sample_df, argv) args$sample_sheet,
check.names = FALSE,
stringsAsFactors = FALSE
)
validated <- de_validate_inputs(tx_se, gene_se, sample_df, args)
sample_df <- validated$sample_df sample_df <- validated$sample_df
covariates <- validated$covariates covariates <- validated$covariates
condition_values <- validated$condition_values condition_values <- validated$condition_values
@ -576,14 +592,14 @@ main_run_de_analysis <- function(argv) {
de_qc_stats <- list( de_qc_stats <- list(
timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"), timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
total_samples = nrow(sample_df), total_samples = nrow(sample_df),
condition_column = argv$condition_column, condition_column = args$condition_column,
reference_level = reference_level, reference_level = reference_level,
covariates = if (length(covariates) > 0) covariates else "none", covariates = if (length(covariates) > 0) covariates else "none",
num_contrasts = length(targets), num_contrasts = length(targets),
contrasts = list() contrasts = list()
) )
n_per_group <- table(sample_df[[argv$condition_column]]) n_per_group <- table(sample_df[[args$condition_column]])
de_qc_stats$samples_per_group <- as.list(n_per_group) de_qc_stats$samples_per_group <- as.list(n_per_group)
sample_size_warnings <- character(0) sample_size_warnings <- character(0)
@ -641,18 +657,23 @@ main_run_de_analysis <- function(argv) {
" 4. Consider using hierarchical testing procedures", " 4. Consider using hierarchical testing procedures",
"" ""
) )
writeLines(mt_content, file.path(argv$out_dir, "MULTIPLE_TESTING_WARNING.txt")) writeLines(mt_content, file.path(args$out_dir, "MULTIPLE_TESTING_WARNING.txt"))
} }
for (target_level in targets) { for (target_level in targets) {
contrast_name <- de_build_contrast_name(argv$condition_column, target_level, reference_level) contrast_name <- sprintf(
contrast_dir <- file.path(argv$out_dir, contrast_name) "%s_%s_vs_%s",
args$condition_column,
target_level,
reference_level
)
contrast_dir <- file.path(args$out_dir, contrast_name)
dir.create(contrast_dir, showWarnings = FALSE, recursive = TRUE) dir.create(contrast_dir, showWarnings = FALSE, recursive = TRUE)
keep_samples <- sample_df[[argv$condition_column]] %in% c(reference_level, target_level) keep_samples <- sample_df[[args$condition_column]] %in% c(reference_level, target_level)
contrast_samples <- droplevels(sample_df[keep_samples, , drop = FALSE]) contrast_samples <- droplevels(sample_df[keep_samples, , drop = FALSE])
contrast_samples[[argv$condition_column]] <- stats::relevel( contrast_samples[[args$condition_column]] <- stats::relevel(
factor(contrast_samples[[argv$condition_column]]), factor(contrast_samples[[args$condition_column]]),
ref = reference_level ref = reference_level
) )
@ -661,8 +682,8 @@ main_run_de_analysis <- function(argv) {
target_level = target_level, target_level = target_level,
reference_level = reference_level, reference_level = reference_level,
n_samples = nrow(contrast_samples), n_samples = nrow(contrast_samples),
n_target = sum(contrast_samples[[argv$condition_column]] == target_level), n_target = sum(contrast_samples[[args$condition_column]] == target_level),
n_reference = sum(contrast_samples[[argv$condition_column]] == reference_level), n_reference = sum(contrast_samples[[args$condition_column]] == reference_level),
deseq2_size_factor_method = "ratio", deseq2_size_factor_method = "ratio",
deseq2_dispersion_fallback = list( deseq2_dispersion_fallback = list(
applied = FALSE, applied = FALSE,
@ -693,9 +714,9 @@ main_run_de_analysis <- function(argv) {
contrast_samples, contrast_samples,
target_level, target_level,
reference_level, reference_level,
argv$condition_column, args$condition_column,
covariates, covariates,
argv$out_dir, args$out_dir,
contrast_name contrast_name
) )
if (!is.null(dge_run$deseq2_dispersion_fallback)) { if (!is.null(dge_run$deseq2_dispersion_fallback)) {
@ -749,7 +770,7 @@ main_run_de_analysis <- function(argv) {
tx_counts, tx_counts,
tx_meta, tx_meta,
contrast_samples, contrast_samples,
argv$condition_column, args$condition_column,
covariates covariates
), ),
error = function(err) { error = function(err) {
@ -776,9 +797,9 @@ main_run_de_analysis <- function(argv) {
sprintf( sprintf(
"Samples: %d (%d %s, %d %s)", "Samples: %d (%d %s, %d %s)",
nrow(contrast_samples), nrow(contrast_samples),
sum(contrast_samples[[argv$condition_column]] == target_level), sum(contrast_samples[[args$condition_column]] == target_level),
target_level, target_level,
sum(contrast_samples[[argv$condition_column]] == reference_level), sum(contrast_samples[[args$condition_column]] == reference_level),
reference_level reference_level
), ),
sprintf("Transcripts: %d", nrow(tx_counts)), sprintf("Transcripts: %d", nrow(tx_counts)),
@ -963,7 +984,7 @@ main_run_de_analysis <- function(argv) {
jsonlite::write_json( jsonlite::write_json(
de_qc_stats, de_qc_stats,
file.path(argv$out_dir, "de_qc_stats.json"), file.path(args$out_dir, "de_qc_stats.json"),
pretty = TRUE, pretty = TRUE,
auto_unbox = TRUE auto_unbox = TRUE
) )
@ -1008,13 +1029,14 @@ main_run_de_analysis <- function(argv) {
" - <contrast>/results_dtu_gene.tsv", " - <contrast>/results_dtu_gene.tsv",
"" ""
) )
writeLines(unlist(overall_summary), file.path(argv$out_dir, "de_overall_summary.txt")) writeLines(unlist(overall_summary), file.path(args$out_dir, "de_overall_summary.txt"))
writeLines(capture.output(sessionInfo()), file.path(argv$out_dir, "session_info.txt")) writeLines(capture.output(sessionInfo()), file.path(args$out_dir, "session_info.txt"))
invisible(list(qc = de_qc_stats)) invisible(list(qc = de_qc_stats))
} }
run_de_analysis_cli <- function(argv = commandArgs(trailingOnly = TRUE)) { run_de_analysis_cli <- function(argv = commandArgs(trailingOnly = TRUE)) {
parsed <- argparser::parse_args(de_analysis_arg_parser(), argv = argv) parsed <- argparser::parse_args(de_analysis_arg_parser(), argv = argv)
main_run_de_analysis(parsed) args <- workflow_glue_r_normalise_args(parsed, de_analysis_arg_spec(), raw_argv = argv)
main_run_de_analysis(args)
} }

View File

@ -118,6 +118,71 @@ make_test_tx_se <- function(include_geneid = TRUE, sample_names = NULL) {
) )
} }
#' Create bambu-style transcript row ranges for output-writing tests.
#'
#' `bambu::writeToGTF()` expects a `GRangesList` shaped like the object
#' returned by `bambu::prepareAnnotations()`. This helper writes a minimal GTF
#' and returns that object with metadata columns used by test assertions.
#'
#' @param out_dir Directory where temporary annotation fixture is written.
#' @return A GRangesList with tx1-tx4 transcript entries and metadata.
#' @export
make_test_bambu_row_ranges <- function(out_dir) {
gtf <- file.path(out_dir, "annotation.gtf")
writeLines(
c(
paste(
"chr1", "test", "transcript", "1", "50", ".", "+", ".",
'gene_id "gene1"; transcript_id "tx1";',
sep = "\t"
),
paste(
"chr1", "test", "exon", "1", "50", ".", "+", ".",
'gene_id "gene1"; transcript_id "tx1"; exon_number "1";',
sep = "\t"
),
paste(
"chr1", "test", "transcript", "101", "150", ".", "+", ".",
'gene_id "gene1"; transcript_id "tx2";',
sep = "\t"
),
paste(
"chr1", "test", "exon", "101", "150", ".", "+", ".",
'gene_id "gene1"; transcript_id "tx2"; exon_number "1";',
sep = "\t"
),
paste(
"chr1", "test", "transcript", "201", "250", ".", "+", ".",
'gene_id "gene2"; transcript_id "tx3";',
sep = "\t"
),
paste(
"chr1", "test", "exon", "201", "250", ".", "+", ".",
'gene_id "gene2"; transcript_id "tx3"; exon_number "1";',
sep = "\t"
),
paste(
"chr1", "test", "transcript", "301", "350", ".", "+", ".",
'gene_id "gene2"; transcript_id "tx4";',
sep = "\t"
),
paste(
"chr1", "test", "exon", "301", "350", ".", "+", ".",
'gene_id "gene2"; transcript_id "tx4"; exon_number "1";',
sep = "\t"
)
),
gtf
)
row_ranges <- bambu::prepareAnnotations(gtf)[c("tx1", "tx2", "tx3", "tx4")]
S4Vectors::mcols(row_ranges)$TXNAME <- names(row_ranges)
S4Vectors::mcols(row_ranges)$GENEID <- c("gene1", "gene1", "gene2", "gene2")
S4Vectors::mcols(row_ranges)$eqClassById <- IRanges::CharacterList(list(c("1", "2"), "3", "4", "5"))
row_ranges
}
#' Create a test gene-level SummarizedExperiment by aggregating a transcript-level SummarizedExperiment. #' Create a test gene-level SummarizedExperiment by aggregating a transcript-level SummarizedExperiment.
#' @param sample_names Optional vector of sample names to use as column names. #' @param sample_names Optional vector of sample names to use as column names.
#' @return A SummarizedExperiment object with synthetic gene-level data. #' @return A SummarizedExperiment object with synthetic gene-level data.

View File

@ -1,6 +1,6 @@
#' These tests cover the validation logic owned by supeRglue bambu before bambu #' These tests cover the validation logic owned by supeRglue bambu before bambu
#' itself is invoked: mutually exclusive BAM inputs, alias derivation, sample #' itself is invoked: BAM/alias argument checks, sample-sheet alignment,
#' sheet alignment, transcriptome mode selection, and NDR handling. #' transcriptome mode selection, and NDR handling.
#' #'
#' NOTE: Annotation/reference preparation is handled by Python #' NOTE: Annotation/reference preparation is handled by Python
#' (bin/workflow_glue/prepare_annotation_reference.py) with pytest coverage. #' (bin/workflow_glue/prepare_annotation_reference.py) with pytest coverage.
@ -20,18 +20,20 @@ testthat::test_that("BAM inputs required", {
) )
testthat::expect_error( testthat::expect_error(
bambu_validate_args(args), workflow_glue_r_normalise_args(args, bambu_arg_spec()),
"Missing required arguments: --bams" "Missing required arguments: --bams"
) )
args$bams <- "sampleA.bam" args$bams <- "sampleA.bam"
testthat::expect_error( testthat::expect_error(
bambu_validate_args(args), workflow_glue_r_normalise_args(args, bambu_arg_spec()),
"Missing required arguments: --aliases" "Missing required arguments: --aliases"
) )
args$aliases <- "sampleA" args$aliases <- "sampleA"
testthat::expect_silent(bambu_validate_args(args)) normalised <- NULL
testthat::expect_silent(normalised <- workflow_glue_r_normalise_args(args, bambu_arg_spec()))
testthat::expect_identical(normalised$threads, 1L)
}) })
# transcriptome_mode must be "discover" or "fixed_annotation". # transcriptome_mode must be "discover" or "fixed_annotation".
@ -48,27 +50,32 @@ testthat::test_that("invalid discovery settings rejected", {
) )
testthat::expect_error( testthat::expect_error(
bambu_validate_args(args), workflow_glue_r_normalise_args(args, bambu_arg_spec()),
"transcriptome_mode must be one of" "transcriptome_mode must be one of"
) )
args$transcriptome_mode <- "discover" args$transcriptome_mode <- "discover"
args$ndr <- -0.01 args$ndr <- -0.01
testthat::expect_error( testthat::expect_error(
bambu_validate_args(args), workflow_glue_r_normalise_args(args, bambu_arg_spec()),
"NDR .* must be between 0 and 1" "NDR .* must be between 0 and 1"
) )
args$ndr <- 1.01 args$ndr <- 1.01
testthat::expect_error( testthat::expect_error(
bambu_validate_args(args), workflow_glue_r_normalise_args(args, bambu_arg_spec()),
"NDR .* must be between 0 and 1" "NDR .* must be between 0 and 1"
) )
args$ndr <- 0 args$ndr <- 0
testthat::expect_silent(bambu_validate_args(args)) testthat::expect_silent(workflow_glue_r_normalise_args(args, bambu_arg_spec()))
args$ndr <- 1 args$ndr <- 1
testthat::expect_silent(bambu_validate_args(args)) testthat::expect_silent(workflow_glue_r_normalise_args(args, bambu_arg_spec()))
args$ndr <- NULL
args$threads <- "2"
normalised <- workflow_glue_r_normalise_args(args, bambu_arg_spec())
testthat::expect_identical(normalised$threads, 2L)
}) })
# Fail fast if --bams is empty rather than passing empty input to bambu. # Fail fast if --bams is empty rather than passing empty input to bambu.
@ -80,7 +87,7 @@ testthat::test_that("empty BAM list rejected", {
) )
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"No BAM files were provided in --bams" "No BAM files were provided in --bams"
) )
}) })
@ -94,13 +101,13 @@ testthat::test_that("unique sample aliases required", {
sample_sheet = NULL sample_sheet = NULL
) )
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"BAM aliases must be unique" "BAM aliases must be unique"
) )
args$aliases <- "sampleA" args$aliases <- "sampleA"
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"Provide one alias per BAM in --bams" "Provide one alias per BAM in --bams"
) )
missing_alias_sheet <- tempfile(fileext = ".csv") missing_alias_sheet <- tempfile(fileext = ".csv")
@ -118,7 +125,7 @@ testthat::test_that("unique sample aliases required", {
sample_sheet = missing_alias_sheet sample_sheet = missing_alias_sheet
) )
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"Sample sheet must contain an 'alias' column" "Sample sheet must contain an 'alias' column"
) )
@ -134,7 +141,7 @@ testthat::test_that("unique sample aliases required", {
) )
args$sample_sheet <- duplicate_alias_sheet args$sample_sheet <- duplicate_alias_sheet
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"Sample sheet aliases must be unique" "Sample sheet aliases must be unique"
) )
}) })
@ -159,13 +166,11 @@ testthat::test_that("sample sheet reordered to match BAMs", {
aliases = "sampleA,sampleB", aliases = "sampleA,sampleB",
sample_sheet = sample_sheet sample_sheet = sample_sheet
) )
resolved <- bambu_resolve_inputs( resolved <- bambu_resolve_inputs(args)
args,
bamfile_list_ctor = function(paths, yieldSize) paths
)
testthat::expect_equal(resolved$aliases, c("sampleA", "sampleB")) testthat::expect_equal(resolved$aliases, c("sampleA", "sampleB"))
testthat::expect_equal(resolved$sample_df$alias, c("sampleA", "sampleB")) testthat::expect_equal(resolved$sample_df$alias, c("sampleA", "sampleB"))
testthat::expect_s4_class(resolved$reads, "BamFileList")
bad_sheet <- tempfile(fileext = ".csv") bad_sheet <- tempfile(fileext = ".csv")
writeLines( writeLines(
@ -179,7 +184,7 @@ testthat::test_that("sample sheet reordered to match BAMs", {
args$sample_sheet <- bad_sheet args$sample_sheet <- bad_sheet
testthat::expect_error( testthat::expect_error(
bambu_resolve_inputs(args, bamfile_list_ctor = function(paths, yieldSize) paths), bambu_resolve_inputs(args),
"Sample sheet is missing alias rows" "Sample sheet is missing alias rows"
) )
}) })
@ -199,6 +204,19 @@ testthat::test_that("transcriptome mode mapped to bambu args", {
testthat::expect_true(discover$discovery) testthat::expect_true(discover$discovery)
testthat::expect_equal(discover$NDR, 0.2) testthat::expect_equal(discover$NDR, 0.2)
testthat::expect_equal(discover$ncore, 3L) testthat::expect_equal(discover$ncore, 3L)
testthat::expect_true(discover$lowMemory)
testthat::expect_equal(discover$yieldSize, 250000L)
auto_ndr_args <- list(
genome = "genome.fa",
threads = 2,
transcriptome_mode = "discover",
ndr = NULL
)
auto_ndr <- bambu_build_args(auto_ndr_args, reads = "sample.bam", annotation_obj = annotation_obj)
testthat::expect_true(auto_ndr$discovery)
testthat::expect_false("NDR" %in% names(auto_ndr))
testthat::expect_equal(auto_ndr$yieldSize, 250000L)
fixed_args <- list( fixed_args <- list(
genome = "genome.fa", genome = "genome.fa",
@ -209,6 +227,8 @@ testthat::test_that("transcriptome mode mapped to bambu args", {
fixed <- bambu_build_args(fixed_args, reads = "sample.bam", annotation_obj = annotation_obj) fixed <- bambu_build_args(fixed_args, reads = "sample.bam", annotation_obj = annotation_obj)
testthat::expect_false(fixed$discovery) testthat::expect_false(fixed$discovery)
testthat::expect_false("NDR" %in% names(fixed)) testthat::expect_false("NDR" %in% names(fixed))
testthat::expect_true(fixed$lowMemory)
testthat::expect_equal(fixed$yieldSize, 250000L)
}) })
# End-to-end unit test with mocked bambu analysis function. # End-to-end unit test with mocked bambu analysis function.
@ -235,19 +255,32 @@ testthat::test_that("bams input with discovery mode", {
) )
captured <- new.env(parent = emptyenv()) captured <- new.env(parent = emptyenv())
fake_bamfile_list <- function(paths, yieldSize) { fake_analysis <- function(
captured$bamfile_paths <- paths reads,
captured$yield_size <- yieldSize annotations,
structure(paths, class = "mockBamFileList") genome,
} ncore,
fake_analysis <- function(reads, annotations, genome, ncore, discovery, NDR) { discovery,
lowMemory,
NDR = NULL,
yieldSize = NULL,
...
) {
captured$reads <- reads captured$reads <- reads
captured$annotations <- annotations captured$annotations <- annotations
captured$genome <- genome captured$genome <- genome
captured$ncore <- ncore captured$ncore <- ncore
captured$discovery <- discovery captured$discovery <- discovery
captured$NDR <- NDR captured$NDR <- NDR
make_test_tx_se(sample_names = c("sampleA", "sampleB")) captured$low_memory <- lowMemory
captured$arg_yield_size <- yieldSize
captured$yield_size <- Rsamtools::yieldSize(reads[[1]])
base_se <- make_test_tx_se(sample_names = c("sampleA", "sampleB"))
SummarizedExperiment::SummarizedExperiment(
assays = SummarizedExperiment::assays(base_se),
rowRanges = make_test_bambu_row_ranges(fixture_dir)
)
} }
argv <- list( argv <- list(
@ -262,6 +295,7 @@ testthat::test_that("bams input with discovery mode", {
threads = 2 threads = 2
) )
argv <- workflow_glue_r_normalise_args(argv, bambu_arg_spec())
result <- suppressMessages(main_run_bambu( result <- suppressMessages(main_run_bambu(
argv, argv,
analysis_fn = fake_analysis, analysis_fn = fake_analysis,
@ -269,23 +303,17 @@ testthat::test_that("bams input with discovery mode", {
captured$annotation_path <- annotation captured$annotation_path <- annotation
structure(list(path = annotation), class = "mockAnnotation") structure(list(path = annotation), class = "mockAnnotation")
}, },
gene_expression_fn = function(se) make_test_gene_se(sample_names = colnames(se)), gene_expression_fn = function(se) make_test_gene_se(sample_names = colnames(se))
write_gtf_fn = function(row_ranges, file) {
writeLines(
'chr1\tsim\texon\t1\t50\t.\t+\t.\tgene_id "gene1"; transcript_id "tx1";',
file
)
},
bamfile_list_ctor = fake_bamfile_list
)) ))
testthat::expect_equal(captured$annotation_path, "annotation.gtf") testthat::expect_equal(captured$annotation_path, "annotation.gtf")
testthat::expect_equal(captured$genome, "genome.fa") testthat::expect_equal(captured$genome, "genome.fa")
testthat::expect_equal(captured$ncore, 2L) testthat::expect_equal(captured$ncore, 1L)
testthat::expect_true(captured$discovery) testthat::expect_true(captured$discovery)
testthat::expect_equal(captured$NDR, 0.25) testthat::expect_equal(captured$NDR, 0.25)
testthat::expect_equal(captured$yield_size, 1000000) testthat::expect_true(captured$low_memory)
testthat::expect_equal(captured$bamfile_paths, c(sample_a, sample_b)) testthat::expect_equal(captured$arg_yield_size, 250000L)
testthat::expect_equal(captured$yield_size, 250000)
testthat::expect_equal(result$sample_df$alias, c("sampleA", "sampleB")) testthat::expect_equal(result$sample_df$alias, c("sampleA", "sampleB"))
testthat::expect_equal(result$sample_df$condition, c("control", "treated")) testthat::expect_equal(result$sample_df$condition, c("control", "treated"))
testthat::expect_true(file.exists(file.path(argv$out_dir, "bambu_qc_stats.json"))) testthat::expect_true(file.exists(file.path(argv$out_dir, "bambu_qc_stats.json")))
@ -308,35 +336,19 @@ testthat::test_that("list columns flattened for TSV output", {
testthat::expect_equal(normalised$list_col, c("x;y", "z")) testthat::expect_equal(normalised$list_col, c("x;y", "z"))
}) })
# NCBI annotations may have gene_id="transcript_id" (literal string, not value reference).
# Replace with gene_id=<actual transcript_id value>, but leave gene_id="MYTRANSCRIPT_ID" alone.
testthat::test_that("malformed gene_id values sanitized", {
gtf_path <- tempfile(fileext = ".gtf")
writeLines(
c(
'chr1\tsim\texon\t1\t50\t.\t+\t.\tgene_id "MYTRANSCRIPT_ID"; transcript_id "tx_keep";',
'chr1\tsim\texon\t101\t150\t.\t+\t.\tgene_id "transcript_id"; transcript_id "tx_replace";'
),
gtf_path
)
testthat::expect_warning(
bambu_sanitise_gtf_file(gtf_path),
"Replaced malformed gene_id"
)
lines <- readLines(gtf_path, warn = FALSE)
testthat::expect_match(lines[[1]], 'gene_id "MYTRANSCRIPT_ID";', fixed = TRUE)
testthat::expect_match(lines[[2]], 'gene_id "tx_replace";', fixed = TRUE)
})
# Verify all expected output files are created with correct structure. # Verify all expected output files are created with correct structure.
testthat::test_that("bambu outputs written correctly", { testthat::test_that("bambu outputs written correctly", {
out_dir <- tempfile("bambu-write-") out_dir <- tempfile("bambu-write-")
dir.create(out_dir) dir.create(out_dir)
sample_names <- c("sampleA", "sampleB") sample_names <- c("sampleA", "sampleB")
se <- make_test_tx_se(sample_names = sample_names) base_se <- make_test_tx_se(sample_names = sample_names)
row_ranges <- make_test_bambu_row_ranges(out_dir)
se <- SummarizedExperiment::SummarizedExperiment(
assays = SummarizedExperiment::assays(base_se),
rowRanges = row_ranges
)
gene_se <- make_test_gene_se(sample_names = sample_names) gene_se <- make_test_gene_se(sample_names = sample_names)
sample_df <- data.frame(alias = sample_names, stringsAsFactors = FALSE) sample_df <- data.frame(alias = sample_names, stringsAsFactors = FALSE)
argv <- list(out_dir = out_dir, transcriptome_mode = "discover", ndr = 0.15) argv <- list(out_dir = out_dir, transcriptome_mode = "discover", ndr = 0.15)
@ -358,13 +370,7 @@ testthat::test_that("bambu outputs written correctly", {
gene_se, gene_se,
sample_df, sample_df,
argv, argv,
qc_stats, qc_stats
write_gtf_fn = function(row_ranges, file) {
writeLines(
'chr1\tsim\texon\t1\t50\t.\t+\t.\tgene_id "gene1"; transcript_id "tx1";',
file
)
}
) )
testthat::expect_true(file.exists(file.path(out_dir, "transcripts.gtf"))) testthat::expect_true(file.exists(file.path(out_dir, "transcripts.gtf")))

View File

@ -8,7 +8,7 @@
# Covariates CLI arg is comma-separated string with possible whitespace/empty values. # Covariates CLI arg is comma-separated string with possible whitespace/empty values.
testthat::test_that("covariates parsed and trimmed", { testthat::test_that("covariates parsed and trimmed", {
testthat::expect_equal( testthat::expect_equal(
de_parse_covariates(" batch, sex ,, site "), workflow_glue_r_parse_csv_list(" batch, sex ,, site "),
c("batch", "sex", "site") c("batch", "sex", "site")
) )
}) })
@ -26,6 +26,7 @@ testthat::test_that("sample sheet structure validated", {
covariates = "batch", covariates = "batch",
reference_level = NULL reference_level = NULL
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
missing_alias <- data.frame(condition = rep(c("control", "treated"), each = 3)) missing_alias <- data.frame(condition = rep(c("control", "treated"), each = 3))
testthat::expect_error( testthat::expect_error(
@ -78,6 +79,7 @@ testthat::test_that("formula-unsafe column names rejected", {
covariates = "batch", covariates = "batch",
reference_level = NULL reference_level = NULL
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
testthat::expect_error( testthat::expect_error(
de_validate_inputs(tx_se, gene_se, sample_df, argv), de_validate_inputs(tx_se, gene_se, sample_df, argv),
@ -150,6 +152,7 @@ testthat::test_that("non-syntactic aliases allowed, sheets reordered", {
covariates = "batch", covariates = "batch",
reference_level = "control" reference_level = "control"
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
validated <- de_validate_inputs(tx_se, gene_se, sample_df, argv) validated <- de_validate_inputs(tx_se, gene_se, sample_df, argv)
@ -173,6 +176,7 @@ testthat::test_that("reference level defaults to control", {
covariates = "batch", covariates = "batch",
reference_level = NULL reference_level = NULL
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
sample_df <- data.frame( sample_df <- data.frame(
alias = colnames(tx_se), alias = colnames(tx_se),
condition = rep(c("control", "treated"), each = 3), condition = rep(c("control", "treated"), each = 3),
@ -220,6 +224,7 @@ testthat::test_that("explicit reference level required without control", {
covariates = "batch", covariates = "batch",
reference_level = NULL reference_level = NULL
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
testthat::expect_error( testthat::expect_error(
de_validate_inputs(tx_se, gene_se, sample_df, argv), de_validate_inputs(tx_se, gene_se, sample_df, argv),
@ -246,6 +251,7 @@ testthat::test_that("unusable count data rejected", {
covariates = "batch", covariates = "batch",
reference_level = "control" reference_level = "control"
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
sample_df <- data.frame( sample_df <- data.frame(
alias = colnames(tx_se), alias = colnames(tx_se),
condition = rep(c("control", "treated"), each = 3), condition = rep(c("control", "treated"), each = 3),
@ -326,6 +332,7 @@ testthat::test_that("transcript SE without GENEID rejected", {
reference_level = "control", reference_level = "control",
out_dir = file.path(fixture_dir, "out") out_dir = file.path(fixture_dir, "out")
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
testthat::expect_error( testthat::expect_error(
main_run_de_analysis(argv), main_run_de_analysis(argv),
@ -369,6 +376,7 @@ testthat::test_that("underspecified designs rejected", {
reference_level = "control", reference_level = "control",
out_dir = file.path(fixture_dir, "out") out_dir = file.path(fixture_dir, "out")
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
testthat::expect_error( testthat::expect_error(
suppressWarnings(main_run_de_analysis(argv)), suppressWarnings(main_run_de_analysis(argv)),
@ -621,6 +629,7 @@ testthat::test_that("contrasts expanded and samples subsetted", {
out_dir = file.path(fixture_dir, "out") out_dir = file.path(fixture_dir, "out")
) )
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
suppressWarnings(suppressMessages(main_run_de_analysis(argv))) suppressWarnings(suppressMessages(main_run_de_analysis(argv)))
@ -680,6 +689,7 @@ testthat::test_that("pipe characters in transcript IDs preserved", {
out_dir = file.path(fixture_dir, "out") out_dir = file.path(fixture_dir, "out")
) )
) )
argv <- workflow_glue_r_normalise_args(argv, de_analysis_arg_spec())
suppressWarnings(suppressMessages(main_run_de_analysis(argv))) suppressWarnings(suppressMessages(main_run_de_analysis(argv)))

View File

@ -61,7 +61,7 @@ process runJointBambu {
--transcriptome_mode "${params.transcriptome_mode}" \ --transcriptome_mode "${params.transcriptome_mode}" \
--threads ${task.cpus} \ --threads ${task.cpus} \
${ndr_arg} \ ${ndr_arg} \
--out_dir cohort --out_dir cohort \
""" """
} }