From 9a8717635efb110be3704854f06c196a6ce3c15b Mon Sep 17 00:00:00 2001 From: jfallmann Date: Tue, 16 Jun 2026 20:33:31 +0200 Subject: [PATCH 01/80] added SPIT as DTU module --- .gitignore | 3 + configs/template.json | 12 +- configs/template_base_commented.json | 15 +- configs/template_clean.json | 12 +- configs/tutorial_exhaustive.json | 25 +- containers/apptainer/spit_DTU.def | 23 ++ docs/source/workflows.rst | 8 +- envs/spit_DTU.yaml | 19 ++ profile_nextflow/nextflow.config | 2 +- scripts/Analysis/DTU/SPIT.py | 330 +++++++++++++++++++++++++++ workflows/spit_DTU.nf | 270 ++++++++++++++++++++++ workflows/spit_DTU.smk | 168 ++++++++++++++ 12 files changed, 879 insertions(+), 8 deletions(-) create mode 100644 containers/apptainer/spit_DTU.def create mode 100644 envs/spit_DTU.yaml create mode 100644 scripts/Analysis/DTU/SPIT.py create mode 100644 workflows/spit_DTU.nf create mode 100644 workflows/spit_DTU.smk diff --git a/.gitignore b/.gitignore index d1f3fcb2..7d38c430 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ venv.bak/ # singularity *.sif + +# AI +.abacusai/ diff --git a/configs/template.json b/configs/template.json index ad52f0ed..df2c2bfc 100644 --- a/configs/template.json +++ b/configs/template.json @@ -440,7 +440,8 @@ "TOOLS" : { "dexseq" : "Analysis/DTU/DEXSEQ.R", - "drimseq" : "Analysis/DTU/DRIMSEQ.R" + "drimseq" : "Analysis/DTU/DRIMSEQ.R", + "spit" : "Analysis/DTU/SPIT.py" }, "COMPARABLE" : { @@ -468,6 +469,15 @@ } }, "drimseq": + { + "OPTIONS": + { + "INDEX": "", # salmon index options + "QUANT": "--validateMappings --seqBias --gcBias", # salmon quant options + "DTU": "" # Options for Analysis + } + }, + "spit": { "OPTIONS": { diff --git a/configs/template_base_commented.json b/configs/template_base_commented.json index 93928bb8..32b5987e 100644 --- a/configs/template_base_commented.json +++ b/configs/template_base_commented.json @@ -507,7 +507,8 @@ "DTU": { "TOOLS": { "dexseq": "Analysis/DTU/DEXSEQ.R", - "drimseq": "Analysis/DTU/DRIMSEQ.R" + "drimseq": "Analysis/DTU/DRIMSEQ.R", + "spit": "Analysis/DTU/SPIT.py" }, "ENV" : "", "BIN" : "", @@ -544,6 +545,18 @@ "QUANT": "--validateMappings --seqBias --gcBias", "DTU": "" } + }, + "spit": { + "comment": { + "INDEX": "salmon index options, e.g. path to decoy file", + "QUANT": "salmon quant options", + "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ..." + }, + "OPTIONS": { + "INDEX": "-d GENOMES/salmon_decoy", + "QUANT": "--validateMappings --seqBias --gcBias", + "DTU": "" + } } } } diff --git a/configs/template_clean.json b/configs/template_clean.json index 78ea39f1..7d5d4267 100644 --- a/configs/template_clean.json +++ b/configs/template_clean.json @@ -413,7 +413,8 @@ "DTU": { "TOOLS": { "dexseq": "Analysis/DTU/DEXSEQ.R", - "drimseq": "Analysis/DTU/DRIMSEQ.R" + "drimseq": "Analysis/DTU/DRIMSEQ.R", + "spit": "Analysis/DTU/SPIT.py" }, "COMPARABLE": { "WTvsKO": [ @@ -442,6 +443,15 @@ } }, "drimseq": + { + "OPTIONS": + { + "INDEX": "", + "QUANT": "--validateMappings --seqBias --gcBias", + "DTU": "" + } + }, + "spit": { "OPTIONS": { diff --git a/configs/tutorial_exhaustive.json b/configs/tutorial_exhaustive.json index da2cecac..8ad239ac 100644 --- a/configs/tutorial_exhaustive.json +++ b/configs/tutorial_exhaustive.json @@ -504,7 +504,8 @@ "TOOLS" : { "dexseq" : "Analysis/DTU/DEXSEQ.R", - "drimseq" : "Analysis/DTU/DRIMSEQ.R" + "drimseq" : "Analysis/DTU/DRIMSEQ.R", + "spit" : "Analysis/DTU/SPIT.py" }, "COMPARABLE" : { @@ -535,6 +536,17 @@ "QUANT": "", # Options for read counting, independent of COUNTS workflows "DTU": "min_samps_feature_expr = 1, min_feature_expr = .1, min_samps_gene_expr = 1, min_gene_expr = 1" # Options for DE Analysis } + }, + "spit": { + "REFERENCE": "GENOMES/Ecoli/Ecoli_trans.fa.gz", + "ANNOTATION": "GENOMES/Ecoli/Ecoli_trans_fix.gtf.gz", + "DECOY": "GENOMES/Ecoli/salmon_decoy", + "OPTIONS": + { + "INDEX": "", + "QUANT": "", # Options for read counting, independent of COUNTS workflows + "DTU": "" # Options for DE Analysis + } } } }, @@ -560,6 +572,17 @@ "QUANT": "", # Options for read counting, independent of COUNTS workflows "DTU": "min_samps_feature_expr = 1, min_feature_expr = .1, min_samps_gene_expr = 1, min_gene_expr = 1" # Options for DE Analysis } + }, + "spit": { + "REFERENCE": "GENOMES/Ecoli/Ecoli_trans.fa.gz", + "ANNOTATION": "GENOMES/Ecoli/Ecoli_trans_fix.gtf.gz", + "DECOY": "GENOMES/Ecoli/salmon_decoy", + "OPTIONS": + { + "INDEX": "", + "QUANT": "", # Options for read counting, independent of COUNTS workflows + "DTU": "" # Options for DE Analysis + } } } } diff --git a/containers/apptainer/spit_DTU.def b/containers/apptainer/spit_DTU.def new file mode 100644 index 00000000..f228de5a --- /dev/null +++ b/containers/apptainer/spit_DTU.def @@ -0,0 +1,23 @@ +Bootstrap: docker +From: continuumio/miniconda3 + +%files + /home/fall/MONSDA/envs/spit_DTU.yaml /opt/envs/ + ${HOME}/MONSDA/scripts /opt/MONSDA/ + +%environment + +%post + ls -alrt /opt/envs + chmod -R +x /opt/envs/spit_DTU.yaml + + ENV_NAME=spit_DTU + echo ". /opt/conda/etc/profile.d/conda.sh" >> $APPTAINER_ENVIRONMENT + echo "conda activate $ENV_NAME" >> $APPTAINER_ENVIRONMENT + + . /opt/conda/etc/profile.d/conda.sh + conda env create -f /opt/envs/spit_DTU.yaml -p /opt/conda/envs/$ENV_NAME + conda clean --all + +%runscript + exec "$@" diff --git a/docs/source/workflows.rst b/docs/source/workflows.rst index 008756bc..d9b75814 100644 --- a/docs/source/workflows.rst +++ b/docs/source/workflows.rst @@ -231,9 +231,11 @@ Includes DE, DEU, DAS and DTU +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+ | DIEGO | Differential Alternative Splicing | | | | Mann-Whitney U test | pValue | results | | Sig | Dendrogram-plot | | ✓ | +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+ - | DRIMSeq | Differential Transcript Usage | dmFilter() | | DM | | pValue, LFC | results(transcript, genes) | Proportions-table, genewise precision | Sig, SigUP, SigDOWN (transcipt, gene) | | FeatPerGene, precision, Pvalues (per comparison) | ✓ | - +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+ - + | DRIMSeq | Differential Transcript Usage | dmFilter() | | DM | | pValue, LFC | results(transcript, genes) | Proportions-table, genewise precision | Sig, SigUP, SigDOWN (transcipt, gene) | | FeatPerGene, precision, Pvalues (per comparison) | ✓ | + +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+ + | SPIT | Differential Transcript Usage | IF-dominance | | KDE | Mann-Whitney U | pValue, LFC | results(transcript, genes, ifs) | IF-table | Sig, SigUP, SigDOWN (transcipt, gene) | | PValues, Wasserstein (per comparison) | ✓ | + +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+ + TRACKS ############### diff --git a/envs/spit_DTU.yaml b/envs/spit_DTU.yaml new file mode 100644 index 00000000..e6a7c3ec --- /dev/null +++ b/envs/spit_DTU.yaml @@ -0,0 +1,19 @@ +name: spit_DTU +channels: + - bioconda + - conda-forge + - nodefaults +dependencies: + - python >=3.9 + - pandas >=1.3.4 + - numpy >=1.20.3 + - scipy >=1.11.1 + - scikit-learn >=1.1.1 + - matplotlib-base >=3.4.3 + - matplotlib-venn >=0.11.6 + - seaborn >=0.11.2 + - tqdm >=4.62.3 + - sed=4.8 + - pip + - pip: + - spit diff --git a/profile_nextflow/nextflow.config b/profile_nextflow/nextflow.config index 6d15e289..bc36eb4b 100644 --- a/profile_nextflow/nextflow.config +++ b/profile_nextflow/nextflow.config @@ -33,7 +33,7 @@ profiles { queue = 'medium' time = { 18.h * (1 << ((task.attempt ?: 1) - 1)) } } - withName: 'run_drimseq|run_dexseq' { + withName: 'run_drimseq|run_dexseq|run_spit' { memory = { 40.GB * (1 << ((task.attempt ?: 1) - 1)) } queue = 'long' time = { 120.h * (1 << ((task.attempt ?: 1) - 1)) } diff --git a/scripts/Analysis/DTU/SPIT.py b/scripts/Analysis/DTU/SPIT.py new file mode 100644 index 00000000..8a354203 --- /dev/null +++ b/scripts/Analysis/DTU/SPIT.py @@ -0,0 +1,330 @@ +#! /usr/bin/env python3 + +import sys +import os +import inspect +import gzip +import re +import shutil +import pickle +import logging +import subprocess +import traceback as tb + +import numpy as np +import pandas as pd +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +cmd_subfolder = os.path.join( + os.path.dirname( + os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe()))) + ), + "../lib", +) +if cmd_subfolder not in sys.path: + sys.path.insert(0, cmd_subfolder) + +from Logger import * + +scriptname = os.path.basename(inspect.stack()[-1].filename).replace(".py", "") + + +def read_quant(path): + logid = scriptname + ".read_quant: " + if os.path.isdir(path): + qf = os.path.join(path, "quant.sf.gz") + if not os.path.exists(qf): + qf = os.path.join(path, "quant.sf") + else: + qf = path + log.debug(logid + "reading " + str(qf)) + comp = "gzip" if str(qf).endswith(".gz") else None + df = pd.read_csv(qf, sep="\t", compression=comp) + df["Name"] = [re.sub(r"\.[0-9]+$", "", str(x)) for x in df["Name"]] + s = df.set_index("Name")["NumReads"] + s = s.groupby(level=0).sum() + return s + + +def parse_tx2gene(gtf): + logid = scriptname + ".parse_tx2gene: " + tx2gene = {} + gene2name = {} + opn = gzip.open if str(gtf).endswith(".gz") else open + with opn(gtf, "rt") as fh: + for line in fh: + if line.startswith("#"): + continue + cols = line.rstrip("\n").split("\t") + if len(cols) < 9: + continue + attr = cols[8] + tid = re.search(r'transcript_id "?([^";]+)"?', attr) + gid = re.search(r'gene_id "?([^";]+)"?', attr) + gname = re.search(r'gene_name "?([^";]+)"?', attr) + if tid and gid: + t = re.sub(r"\.[0-9]+$", "", tid.group(1)) + g = re.sub(r"\.[0-9]+$", "", gid.group(1)) + tx2gene[t] = g + if gname: + gene2name[g] = gname.group(1) + log.debug(logid + "found " + str(len(tx2gene)) + " transcripts") + return tx2gene, gene2name + + +def run_spit_cmd(subcmd, args_list): + logid = scriptname + ".run_spit_cmd: " + cmd = ["spit", subcmd] + [str(x) for x in args_list] + log.info(logid + " ".join(cmd)) + proc = subprocess.run(cmd, capture_output=True, text=True) + log.debug(logid + "STDOUT: " + proc.stdout) + if proc.returncode != 0: + log.error(logid + "STDERR: " + proc.stderr) + raise RuntimeError("spit " + subcmd + " failed: " + proc.stderr) + else: + log.debug(logid + "STDERR: " + proc.stderr) + return + + +def compute_lfc(ifs, ctrl_samples, case_samples): + eps = 1e-6 + num = ifs.select_dtypes(include=[np.number]) + a = num[case_samples].mean(axis=1) + b = num[ctrl_samples].mean(axis=1) + lfc = np.log2((a + eps) / (b + eps)) + lfc[~np.isfinite(lfc)] = 0 + return lfc + + +def main(anname, gtf, outdir, cmp, combi, cores, spitopts): + logid = scriptname + ".main: " + + pre_extra = [] + dtu_extra = [] + if spitopts and spitopts.strip(): + if "||" in spitopts: + pre_part, dtu_part = spitopts.split("||", 1) + pre_extra = pre_part.split() + dtu_extra = dtu_part.split() + else: + dtu_extra = spitopts.split() + + tx2gene, gene2name = parse_tx2gene(gtf) + + sampleData_all = pd.read_csv(anname, sep="\t", compression="gzip", dtype=str) + sampleData_all["sample_id"] = sampleData_all["sample_id"].astype(str) + + counts = {} + for _, row in sampleData_all.iterrows(): + counts[row["sample_id"]] = read_quant(row["path"]) + cts_all = pd.DataFrame(counts) + cts_all = cts_all.fillna(0) + cts_all = cts_all[cts_all.index.isin(tx2gene.keys())] + cts_all = cts_all[cts_all.sum(axis=1) > 0] + cts_all = cts_all.round().astype(int) + cts_all.index.name = "tx_id" + + if combi == "none": + combi = "" + + os.makedirs(os.path.join(outdir, "Tables"), exist_ok=True) + os.makedirs(os.path.join(outdir, "Figures"), exist_ok=True) + olddir = os.getcwd() + os.chdir(outdir) + + session = {} + + for contrast in cmp.split(","): + contrast_name = contrast.split(":")[0] + groups = contrast.split(":")[1].split("-vs-") + A = groups[0].split("+") + B = groups[1].split("+") + conds = A + B + log.info(logid + "contrast " + contrast_name + " A=" + str(A) + " B=" + str(B)) + + sub = sampleData_all[sampleData_all["condition"].isin(conds)] + case_samples = sub[sub["condition"].isin(A)]["sample_id"].tolist() + ctrl_samples = sub[sub["condition"].isin(B)]["sample_id"].tolist() + keep = case_samples + ctrl_samples + cts = cts_all[keep] + cts = cts[cts.sum(axis=1) > 0] + + tmpdir = os.path.abspath("spit_tmp_" + contrast_name) + if os.path.exists(tmpdir): + shutil.rmtree(tmpdir) + os.makedirs(tmpdir) + + cts_file = os.path.join(tmpdir, "tx_counts.txt") + tx2gene_file = os.path.join(tmpdir, "tx2gene.txt") + labels_file = os.path.join(tmpdir, "labels.txt") + + cts.to_csv(cts_file, sep="\t") + t2g = pd.DataFrame( + {"tx_id": cts.index, "gene_id": [tx2gene[t] for t in cts.index]} + ) + t2g.to_csv(tx2gene_file, sep="\t", index=False) + + labels = pd.DataFrame( + { + "id": keep, + "condition": [1] * len(case_samples) + [0] * len(ctrl_samples), + } + ) + batches = sub.set_index("sample_id").loc[keep, "batch"].tolist() + if len(set(batches)) > 1: + labels["batch"] = batches + labels.to_csv(labels_file, sep="\t", index=False) + + shared = [ + "-i", cts_file, + "-m", tx2gene_file, + "-l", labels_file, + "-O", tmpdir, + "--no_clusters", + ] + run_spit_cmd("preprocess", shared + pre_extra) + run_spit_cmd("dtu", shared + dtu_extra) + + spitout = os.path.join(tmpdir, "SPIT_analysis", "spit_out.txt") + pvals = os.path.join(tmpdir, "SPIT_analysis", "all_p_values.txt") + ifsf = os.path.join(tmpdir, "SPIT_analysis", "dominance_selected_ifs.txt") + if not os.path.exists(ifsf): + ifsf = os.path.join(tmpdir, "SPIT_analysis", "filtered_ifs.txt") + + spit_df = ( + pd.read_csv(spitout, sep="\t") + if os.path.exists(spitout) + else pd.DataFrame(columns=["gene_id", "flag", "wasserstein_distance"]) + ) + pv_df = ( + pd.read_csv(pvals, sep="\t", index_col=0) + if os.path.exists(pvals) + else pd.DataFrame(columns=["p_value"]) + ) + ifs_df = pd.read_csv(ifsf, sep="\t", index_col=0) + + lfc = compute_lfc(ifs_df, ctrl_samples, case_samples) + + tx_tab = pd.DataFrame( + { + "feature_id": ifs_df.index, + "pvalue": [ + pv_df["p_value"].get(t, np.nan) if "p_value" in pv_df else np.nan + for t in ifs_df.index + ], + "lfc": lfc.values, + "gene_id": ifs_df["gene_id"].values, + } + ) + tx_tab["Gene"] = [gene2name.get(g, g) for g in tx_tab["gene_id"]] + tx_tab = tx_tab.sort_values("pvalue", na_position="last") + + gene_p = tx_tab.groupby("gene_id")["pvalue"].min() + maxidx = tx_tab.groupby("gene_id")["lfc"].apply(lambda s: s.abs().idxmax()) + gene_lfc = tx_tab.loc[maxidx.values].set_index("gene_id")["lfc"] + gene_tab = pd.DataFrame({"gene_id": gene_p.index}) + gene_tab["pvalue"] = gene_p.values + gene_tab["lfc"] = gene_lfc.reindex(gene_tab["gene_id"]).values + wd = spit_df.set_index("gene_id")["wasserstein_distance"] if len(spit_df) else pd.Series(dtype=float) + fl = spit_df.set_index("gene_id")["flag"] if len(spit_df) else pd.Series(dtype=str) + gene_tab["wasserstein_distance"] = gene_tab["gene_id"].map(wd).fillna("NA").values + gene_tab["flag"] = gene_tab["gene_id"].map(fl).fillna("").values + gene_tab["Gene"] = [gene2name.get(g, g) for g in gene_tab["gene_id"]] + gene_tab = gene_tab.sort_values("pvalue", na_position="last") + + prefix = "_".join(["DTU", "SPIT", combi, contrast_name, "table"]) + gene_tab.to_csv( + os.path.join("Tables", prefix + "_genes.tsv.gz"), + sep="\t", index=False, compression="gzip", + ) + tx_tab.to_csv( + os.path.join("Tables", prefix + "_transcripts.tsv.gz"), + sep="\t", index=False, compression="gzip", + ) + ifs_out = ifs_df.copy() + ifs_out.insert(0, "feature_id", ifs_out.index) + ifs_out.to_csv( + os.path.join("Tables", prefix + "_ifs.tsv.gz"), + sep="\t", index=False, compression="gzip", + ) + + figpre = "_".join(["DTU", "SPIT", combi, contrast_name, "figure"]) + pv_clean = tx_tab["pvalue"].dropna() + plt.figure(figsize=(8, 6)) + if len(pv_clean) > 0: + plt.hist(pv_clean, bins=20, color="steelblue", edgecolor="black") + plt.xlabel("p-value") + plt.ylabel("Frequency") + plt.title("SPIT transcript p-values " + contrast_name) + plt.tight_layout() + plt.savefig(os.path.join("Figures", figpre + "_PValues.png"), dpi=150) + plt.close() + + plt.figure(figsize=(8, 6)) + if len(spit_df) > 0: + top = spit_df.head(20) + plt.barh( + [str(x) for x in top["gene_id"]][::-1], + pd.to_numeric(top["wasserstein_distance"], errors="coerce").fillna(0).values[::-1], + color="darkorange", + ) + plt.xlabel("Wasserstein distance") + plt.ylabel("Gene") + plt.title("SPIT top DTU genes " + contrast_name) + plt.tight_layout() + plt.savefig(os.path.join("Figures", figpre + "_Wasserstein.png"), dpi=150) + plt.close() + + session[contrast_name] = { + "genes": gene_tab, + "transcripts": tx_tab, + "spit_out": spit_df, + } + shutil.rmtree(tmpdir) + log.info(logid + "done " + contrast_name) + + with gzip.open("_".join(["DTU_SPIT", combi, "SESSION.gz"]), "wb") as fh: + pickle.dump(session, fh) + + os.chdir(olddir) + + +if __name__ == "__main__": + logid = scriptname + ".main: " + try: + args = sys.argv[1:] + anname = args[0] + gtf = args[1] + outdir = args[2] + cmp = args[3] + combi = args[4] + cores = int(args[5]) + spitopts = args[6] if len(args) > 6 else "" + + makelogdir("LOGS") + try: + log = setup_logger( + name=scriptname, + log_file="stderr", + logformat="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M", + level="INFO", + ) + except: + log = logging.getLogger(os.path.basename(inspect.stack()[-1].filename)) + + main(anname, gtf, outdir, cmp, combi, cores, spitopts) + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + tbe = tb.TracebackException(exc_type, exc_value, exc_tb) + try: + log.error(logid + "".join(tbe.format())) + except Exception: + print("".join(tbe.format()), file=sys.stderr) + sys.exit(1) + +# +# SPIT.py ends here diff --git a/workflows/spit_DTU.nf b/workflows/spit_DTU.nf new file mode 100644 index 00000000..f02db235 --- /dev/null +++ b/workflows/spit_DTU.nf @@ -0,0 +1,270 @@ +BINS = get_always('BINS') +DTUENV = get_always('DTUENV') +DTUBIN = get_always('DTUBIN') +DTUREF = get_always('DTUREF') +DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR') +DTUANNO = get_always('DTUANNO') +DTUDECOY = get_always('DTUDECOY') +DTUIDX = get_always('DTUIDX') +DTUUIDX = get_always('DTUUIDX') +DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx' +IDXPARAMS = get_always('spit_DTU_params_INDEX') ?: '' +COUNTPARAMS = get_always('spit_DTU_params_COUNT') ?: '' +DTUPARAMS = get_always('spit_DTU_params_DTU') ?: '' +DTUREPS = get_always('DTUREPS') ?: '' +DTUCOMP = get_always('DTUCOMP') ?: '' +DTUCOMPS = get_always('DTUCOMPS') ?: '' +PVAL = get_always('DTUPVAL') ?: '' +LFC = get_always('DTULFC') ?: '' +PCOMBO = get_always('COMBO') ?: 'none' + +COUNTBIN = 'salmon' +COUNTENV = 'salmon' + +//DTU PROCESSES + +process salmon_idx{ + conda "$COUNTENV"+".yaml" + container "oras://jfallmann/monsda:"+"$COUNTENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', + saveAs: {filename -> + if (filename.indexOf(".log") >0) "LOGS/${COMBO}/${CONDITION}/DTU/spit_index.log" + else if (filename == "spit.idx") "$DTUIDX" + else "$DTUUIDX" + } + + input: + path genome + + output: + path "$DTUUIDXNAME", emit: idx + path "*.log", emit: idxlog + path "*.idx", emit: tmpidx + + script: + gen = genome.getName() + if (DTUDECOY){ + decoy = "-d "+"${DTUDECOY}" + }else{ + decoy = '' + } + """ + $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME spit.idx + """ + +} + +process salmon_quant{ + conda "$COUNTENV"+".yaml" + container "oras://jfallmann/monsda:"+"$COUNTENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', + saveAs: {filename -> + if (filename.indexOf(".log") >0) "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}" + else "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}" + } + + input: + path reads + + output: + path "*.gz", emit: counts + path "*.log", emit: logs + + script: + + idx = reads[0] + if (PAIRED == 'paired'){ + if (STRANDED == 'fr' || STRANDED == 'ISF'){ + stranded = '-l ISF' + }else if (STRANDED == 'rf' || STRANDED == 'ISR'){ + stranded = '-l ISR' + }else{ + stranded = '-l IU' + } + rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] } + r1 = rs[0] + r2 = rs[1] + fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"") + lf = "salmon_"+fn+".log" + of = fn+"/quant.sf" + oz = fn+"/quant.sf.gz" + ol = fn+"_counts.gz" + """ + $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol + """ + }else{ + if (STRANDED == 'fr' || STRANDED == 'SF'){ + stranded = '-l SF' + }else if (STRANDED == 'rf' || STRANDED == 'SR'){ + stranded = '-l SR' + }else{ + stranded = '-l U' + } + read = reads[1] + fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"") + lf = "salmon_"+fn+".log" + of = fn+"/quant.sf" + oz = fn+"/quant.sf.gz" + ol = fn+"_counts.gz" + """ + $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol + """ + } +} + +process prepare_dtu_annotation{ + conda "$DTUENV"+".yaml" + container "oras://jfallmann/monsda:"+"$DTUENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + publishDir "${workflow.workDir}/../" , mode: 'link', + saveAs: {filename -> + if (filename.indexOf(".gz") > 0) "DTU/${SCOMBO}/Tables/${file(filename).getName()}" + else if (filename.indexOf(".log") > 0) "LOGS/DTU/${SCOMBO}/featurecount_spit_annotation.log" + } + + output: + path "*.gz", emit: anno + path "*.log", emit: log + + script: + ca = COMBO+"_ANNOTATION.gz" + ol = "create_DTU_table.log" + """ + mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol + """ +} + +process run_spit{ + conda "$DTUENV"+".yaml" + container "oras://jfallmann/monsda:"+"$DTUENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + publishDir "${workflow.workDir}/../" , mode: 'link', + saveAs: {filename -> + if (filename.indexOf("_table") > 0) "DTU/${SCOMBO}/Tables/${file(filename).getName()}" + else if (filename.indexOf("_figure") > 0) "DTU/${SCOMBO}/Figures/${file(filename).getName()}" + else if (filename.indexOf("SESSION") > 0) "DTU/${SCOMBO}/${file(filename).getName()}" + else if (filename.indexOf("log") > 0) "LOGS/DTU/${SCOMBO}/run_spit.log" + } + + input: + path counts + path anno + path ref + + output: + path "*_table*", emit: tbls + path "*_figure*", emit: figs, optional:true + path "*SESSION.gz", emit: session + path "log", emit: log + + script: + outdir = "DTU"+File.separatorChar+"${SCOMBO}" + bin = "${BINS}"+File.separatorChar+"${DTUBIN}" + comp = "${DTUCOMP}".split(':')[0] + dparams = "'${DTUPARAMS}'" + + """ + mkdir -p Figures Tables + python3 $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* . + """ +} + +process create_summary_snippet{ + conda "$DTUENV"+".yaml" + container "oras://jfallmann/monsda:"+"$DTUENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + publishDir "${workflow.workDir}/../" , mode: 'link', + saveAs: {filename -> + if (filename.indexOf(".Rmd") > 0) "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd" + else if (filename.indexOf("log") > 0) "LOGS/DTU/create_summary_snippet.log" + } + + input: + path de + + output: + path "*.Rmd", emit: snps + path "log", emit: log + + script: + inlist = de.toString() + // inlist = de.toList() // { $workflow.projectDir += "$it.code," } + """ + touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log + """ +} + +process collect_spit{ + conda "$DTUENV"+".yaml" + container "oras://jfallmann/monsda:"+"$DTUENV" + cpus THREADS + cache 'lenient' + //validExitStatus 0,1 + + input: + path de + + script: + """ + echo "$de DONE" + """ +} + +workflow DTU{ + take: collection + + main: + + TRIMSAMPLES = LONGSAMPLES.collect{ + element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz" + } + + trimsamples_ch = Channel.fromPath(TRIMSAMPLES.sort()) + annofile = Channel.fromPath(DTUANNO) + checkidx = file(DTUIDX) + + if (checkidx.exists()){ + idxfile = Channel.fromPath(DTUUIDX) + if (PAIRED == 'paired'){ + salmon_quant(idxfile.combine(trimsamples_ch.collate(2))) + } else{ + salmon_quant(idxfile.combine(trimsamples_ch.collate(1))) + } + } + else{ + genomefile = Channel.fromPath(DTUREF) + salmon_idx(genomefile) + if (PAIRED == 'paired'){ + salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2))) + } else{ + salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1))) + } + } + + prepare_dtu_annotation() + run_spit(salmon_quant.out.counts.collect(), prepare_dtu_annotation.out.anno, annofile) + create_summary_snippet(run_spit.out.tbls.concat(run_spit.out.figs.concat(run_spit.out.session)).collect()) + collect_spit(run_spit.out.tbls.collect().concat(create_summary_snippet.out.snps.collect())) + + emit: + tbls = run_spit.out.tbls + figs = run_spit.out.figs + snps = create_summary_snippet.out.snps +} diff --git a/workflows/spit_DTU.smk b/workflows/spit_DTU.smk new file mode 100644 index 00000000..32d9575c --- /dev/null +++ b/workflows/spit_DTU.smk @@ -0,0 +1,168 @@ +logid = 'spit_DTU.smk ' +DTUBIN, DTUENV = env_bin_from_config(config,'DTU') +log.debug(logid+"DTUENV: "+str(DTUENV)) +COUNTBIN, COUNTENV = ['salmon','salmon']#env_bin_from_config(SAMPLES, config,'COUNTING') ##PINNING subreads package to version 1.6.4 due to changes in 2.0.1 gene_id length cutoff that interfers + +comparison = comparable_as_string(config,'DTU') +compstr = [i.split(":")[0] for i in comparison.split(",")] +usededup = config.get('RUNDEDUP', False) + +keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX']) +keydict["REF"] = REFERENCE +keydict["DECOY"] = DECOY +keydict["ENV"] = DTUENV +unik = get_dict_hash(keydict) + +rule themall: + input: session = expand("DTU/{combo}/DTU_SPIT_{scombo}_SESSION.gz", combo=combo, scombo=scombo), + res_t = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + res_g = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + res_i = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_ifs.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + fig_PV = expand("DTU/{combo}/Figures/DTU_SPIT_{scombo}_{comparison}_figure_PValues.png", combo=combo, comparison=compstr, scombo=scombo), + fig_W = expand("DTU/{combo}/Figures/DTU_SPIT_{scombo}_{comparison}_figure_Wasserstein.png", combo=combo, comparison=compstr, scombo=scombo), + sig_t = expand("DTU/{combo}/Tables/Sig_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + sig_dt = expand("DTU/{combo}/Tables/SigDOWN_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + sig_ut = expand("DTU/{combo}/Tables/SigUP_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + sig_g = expand("DTU/{combo}/Tables/Sig_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + sig_dg = expand("DTU/{combo}/Tables/SigDOWN_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + sig_ug = expand("DTU/{combo}/Tables/SigUP_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo), + Rmd = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo) + +rule salmon_index: + input: fa = REFERENCE + output: idx = directory(INDEX), + uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik)) + log: expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV) + conda: ""+COUNTENV+".yaml" + container: "oras://jfallmann/monsda:"+COUNTENV+"" + threads: MAXTHREAD + params: mapp = COUNTBIN, + ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""), + decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '', + linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0])) + shell: "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}" + +if paired == 'paired': + rule simulate_trim: + input: r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz", + r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz" + output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz", + r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz" + threads: 1 + params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)), + filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2)) + shell: "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}" + +else: + rule simulate_trim: + input: r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz" + output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz" + threads: 1 + params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)) + shell: "ln -s {params.filetolink} {output.r1}" + +if paired == 'paired': + rule mapping: + input: r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo), + r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo), + index = rules.salmon_index.output.idx, + uix = rules.salmon_index.output.uidx + output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"), + ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING") + log: "LOGS/{combo}/{file}/salmonquant.log" + conda: ""+COUNTENV+".yaml" + container: "oras://jfallmann/monsda:"+COUNTENV+"" + threads: MAXTHREAD + params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""), + mapp=COUNTBIN, + stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU', + linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir)) + shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}" + +else: + rule mapping: + input: r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo), + index = rules.salmon_index.output.idx, + uix = rules.salmon_index.output.uidx + output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"), + ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING") + log: "LOGS/{combo}/{file}/salmonquant.log" + conda: ""+COUNTENV+".yaml" + container: "oras://jfallmann/monsda:"+COUNTENV+"" + threads: MAXTHREAD + params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""), + mapp=COUNTBIN, + stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U', + linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir)) + shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}" + + +rule create_annotation_table: + input: dir = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config)), + output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo) + log: expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo) + conda: ""+COUNTENV+".yaml" + container: "oras://jfallmann/monsda:"+COUNTENV+"" + threads: 1 + params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'), + bins = BINS + shell: "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}" + +rule run_DTU: + input: anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo) + output: session = rules.themall.input.session, + res_t = rules.themall.input.res_t, + res_g = rules.themall.input.res_g, + res_i = rules.themall.input.res_i, + fig_PV = rules.themall.input.fig_PV, + fig_W = rules.themall.input.fig_W + log: expand("LOGS/DTU/{combo}/run_DTU.log", combo=combo) + conda: ""+DTUENV+".yaml" + container: "oras://jfallmann/monsda:"+DTUENV+"" + threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1 + params: bins = str.join(os.sep,[BINS, DTUBIN]), + compare = comparison, + pcombo = scombo if scombo != '' else 'none', + outdir = 'DTU/'+combo, + ref = os.path.abspath(ANNOTATION), + dtuopt = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('DTU', "") + shell: "python3 {params.bins} {input.anno} {params.ref} {params.outdir} {params.compare} {params.pcombo} {threads} \'{params.dtuopt}\' 2> {log}" + +rule filter_significant_spit: + input: res_g = rules.themall.input.res_g, + res_t = rules.themall.input.res_t + output: sig_g = rules.themall.input.sig_g, + sig_dg = rules.themall.input.sig_dg, + sig_ug = rules.themall.input.sig_ug, + sig_t = rules.themall.input.sig_t, + sig_dt = rules.themall.input.sig_dt, + sig_ut = rules.themall.input.sig_ut + log: "LOGS/DTU/filter_spitDTU.log" + conda: ""+DTUENV+".yaml" + container: "oras://jfallmann/monsda:"+DTUENV+"" + threads: 1 + params: pv_cut = get_cutoff_as_string(config, 'DTU', 'pvalue'), + lfc_cut = get_cutoff_as_string(config, 'DTU', 'lfc') + shell: "set +o pipefail; arr=({input.res_g}); arrt=({input.res_t}); orr=({output.sig_g}); orrd=({output.sig_dg}); orru=({output.sig_ug}); orrt=({output.sig_t}); orrtd=({output.sig_dt}); orrtu=({output.sig_ut}); for i in \"${{!arr[@]}}\"; do a=\"${{arr[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orr[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orrd[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orru[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orr[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orru[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrd[$i]}}\"; else touch \"${{orr[$i]}}\" \"${{orrd[$i]}}\" \"${{orru[$i]}}\"; fi;done; for i in \"${{!arrt[@]}}\"; do a=\"${{arrt[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orrt[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtu[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrt[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtu[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtd[$i]}}\"; else touch \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\" \"${{orrtu[$i]}}\"; fi;done 2> {log}" + +rule create_summary_snippet: + input: rules.run_DTU.output.res_t, + rules.run_DTU.output.res_g, + rules.run_DTU.output.res_i, + rules.run_DTU.output.fig_PV, + rules.run_DTU.output.fig_W, + rules.themall.input.sig_g, + rules.themall.input.sig_dg, + rules.themall.input.sig_ug, + rules.themall.input.sig_t, + rules.themall.input.sig_dt, + rules.themall.input.sig_ut, + rules.themall.input.session + output: rules.themall.input.Rmd + log: expand("LOGS/DTU/{combo}/create_summary_snippet.log", combo=combo) + conda: ""+DTUENV+".yaml" + container: "oras://jfallmann/monsda:"+DTUENV+"" + threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1 + params: bins = BINS, + abspathfiles = lambda w, input: [os.path.abspath(x) for x in input] + shell: "python3 {params.bins}/Analysis/RmdCreator.py --files {params.abspathfiles} --output {output} --env {DTUENV} --loglevel DEBUG 2>> {log}" From c1a9dd472138ec852de50067f424818f37e92e4e Mon Sep 17 00:00:00 2001 From: jfallmann Date: Tue, 23 Jun 2026 21:50:34 +0200 Subject: [PATCH 02/80] CI/CD push with Opus 4.8 --- .github/workflows/python-app.yaml | 106 +++++++++- .pre-commit-config.yaml | 36 ++++ CICD.md | 321 ++++++++++++++++++++++++++++++ Makefile | 86 ++++++++ 4 files changed, 540 insertions(+), 9 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 CICD.md create mode 100644 Makefile diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 3ba2edd8..b2cca401 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -1,6 +1,10 @@ name: MONSDA CI on: + push: + branches: [main, develop] + tags: + - "v*" pull_request: types: [opened, synchronize, reopened, labeled, unlabeled] workflow_dispatch: @@ -11,10 +15,35 @@ on: default: false type: boolean +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: + lint: + name: Lint (flake8, isort) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - name: Install linting tools + run: | + python -m pip install --upgrade pip + python -m pip install flake8 isort + - name: Check import sorting with isort + run: isort --check-only --diff MONSDA/ tests/ + continue-on-error: true + - name: Lint with flake8 + run: flake8 MONSDA/ tests/ --count --statistics --show-source + continue-on-error: true + tests: name: Unit tests (pytest) runs-on: ubuntu-latest + needs: lint defaults: run: shell: bash -el {0} @@ -34,16 +63,54 @@ jobs: show-channel-urls: true use-only-tar-bz2: false auto-activate-base: false - - name: build and install MONSDA + - name: Install dependencies run: | cd ${{ github.workspace }} python -m pip install --upgrade pip python -m pip install -e . - python -m pip install pytest - - name: run unit tests + python -m pip install pytest pytest-cov + - name: Run unit tests with coverage run: | cd ${{ github.workspace }} - pytest -q tests/test_Utils.py + pytest tests/test_Utils.py -v --cov=MONSDA --cov-report=xml --cov-report=html + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + if: always() + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true + + build: + name: Build package + runs-on: ubuntu-latest + needs: tests + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - name: Install build tools + run: | + python -m pip install --upgrade pip + python -m pip install build wheel setuptools versioneer + - name: Build distribution + run: | + python -m build + - name: Check build artifacts + run: | + ls -lh dist/ + python -m pip install twine + twine check dist/* + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + retention-days: 7 full-pipeline: name: Full pipeline integration test @@ -71,23 +138,23 @@ jobs: show-channel-urls: true use-only-tar-bz2: false auto-activate-base: false - - name: build and install MONSDA + - name: Build and install MONSDA run: | cd ${{ github.workspace }} python -m pip install --upgrade pip python -m pip install -e . - - name: prepare integration test config + - name: Prepare integration test config run: | cd ${{ github.workspace }} VERSION=$(monsda --version 2>&1 | sed 's/MONSDA version //g') sed -i "s/\"VERSION\": \"FIXME\"/\"VERSION\": \"${VERSION}\"/g" tests/data/config_Test.json - - name: run full pipeline test + - name: Run full pipeline test run: | cd ${{ github.workspace }}/tests ln -fs data/* . mkdir -p CONDALIB monsda -j 6 -c config_Test.json --directory ${PWD} --use-conda --conda-prefix CONDALIB --save - - name: upload test logs on failure + - name: Upload test logs on failure if: failure() uses: actions/upload-artifact@v4 with: @@ -100,4 +167,25 @@ jobs: ${{ github.workspace }}/tests/QC ${{ github.workspace }}/tests/COUNTING ${{ github.workspace }}/tests/PEAKS - ${{ github.workspace }}/tests/TRACKS \ No newline at end of file + ${{ github.workspace }}/tests/TRACKS + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + environment: + name: pypi + url: https://pypi.org/project/MONSDA + permissions: + id-token: write + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://upload.pypi.org/legacy/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..03f766a0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=1000'] + - id: check-merge-conflict + - id: check-json + - id: mixed-line-ending + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort + language_version: python3.12 + + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + name: flake8 + language_version: python3.12 + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.1 + hooks: + - id: mypy + name: mypy + language_version: python3.12 + additional_dependencies: ['types-PyYAML', 'types-setuptools'] + args: ['--ignore-missing-imports', '--show-error-codes'] + stages: [manual] diff --git a/CICD.md b/CICD.md new file mode 100644 index 00000000..92c998c3 --- /dev/null +++ b/CICD.md @@ -0,0 +1,321 @@ +# CI/CD Pipeline for MONSDA + +This document describes the Continuous Integration/Continuous Deployment (CI/CD) pipeline for MONSDA. + +## Overview + +The CI/CD pipeline is implemented using GitHub Actions and includes the following stages: + +1. **Linting** - Code quality checks with flake8 and isort +2. **Testing** - Unit tests with pytest and coverage reporting +3. **Building** - Package building and artifact verification +4. **Integration Testing** - Full pipeline integration tests (optional) +5. **Publishing** - Automatic deployment to PyPI on releases + +## Pipeline Stages + +### 1. Linting (Runs on all PRs and pushes) + +**Job:** `lint` + +Automatically checks code quality using: +- **isort** - Validates import statement ordering +- **flake8** - Checks PEP 8 compliance and code style + +**Configuration:** +- Line length: 110 characters +- Ignores: E203, E266, E501, W503 +- Continues on error (warnings don't block) + +Run locally: +```bash +make lint-check # Check without fixing +make lint # Check and fix issues +make format # Format imports only +``` + +### 2. Unit Testing (Runs after linting) + +**Job:** `tests` + +Runs pytest test suite with coverage reporting: +- Tests located in `tests/test_Utils.py` +- Generates coverage reports (XML and HTML) +- Uploads coverage to Codecov + +**Dependencies:** +- Conda environment from `environment.yml` +- Python 3.12.2 +- pytest and pytest-cov + +Run locally: +```bash +make test # Run unit tests +make test-cov # Run with coverage report +``` + +### 3. Building (Runs after tests) + +**Job:** `build` + +Builds distribution packages: +- Creates wheel and source distributions +- Validates build artifacts with twine +- Stores artifacts for 7 days + +**Tools:** +- setuptools +- wheel +- versioneer (for version management) +- twine (for package validation) + +Run locally: +```bash +make build # Build distribution packages +make clean-build # Remove build artifacts +``` + +### 4. Full Pipeline Integration Test (Optional) + +**Job:** `full-pipeline` + +Runs comprehensive integration tests of the Snakemake/Nextflow pipeline functionality. + +**Trigger conditions:** +- Manual workflow dispatch with `run_full_pipeline=true` +- PR with `full-pipeline` label + +**Process:** +1. Sets up Conda environment from `environment.yml` +2. Installs MONSDA package +3. Updates test configuration with current version +4. Runs test workflow with sample data +5. Uploads debug artifacts on failure + +Run locally: +```bash +RUN_INTEGRATION_TESTS=1 bash tests/cicd_test.sh +``` + +### 5. Publishing to PyPI (Runs on version tags) + +**Job:** `publish` + +Automatically publishes releases to PyPI when tags are pushed. + +**Trigger:** Tags matching pattern `v*` (e.g., `v1.0.0`) + +**Requirements:** +- Build job must succeed +- PyPI environment configured with trusted publishing +- Token in GitHub secrets (automated via Trusted Publisher) + +**Manual deployment:** +```bash +make deploy # Build and upload to PyPI +``` + +## Workflow Triggers + +The pipeline runs automatically on: + +- **Pull requests** - All PR types (opened, synchronize, reopened, labeled, unlabeled) +- **Push to main/develop** - On code pushes to main and develop branches +- **Version tags** - On tags matching `v*` (triggers PyPI publishing) +- **Manual dispatch** - Triggered via GitHub Actions UI with options + +## Local Development Setup + +### Prerequisites + +- Python 3.12+ +- Conda/Mamba (for integration tests) +- Git + +### Quick Setup + +```bash +# Install development dependencies +make install-dev + +# Set up pre-commit hooks +make pre-commit +``` + +### Available Commands + +```bash +# Code quality +make lint # Run isort and flake8 (auto-fix) +make lint-check # Check linting (don't fix) +make format # Format imports with isort + +# Testing +make test # Run unit tests +make test-cov # Run tests with coverage +make test-integration # Run full pipeline tests + +# Building +make build # Build packages +make clean # Clean all artifacts +make clean-pyc # Remove Python cache files + +# Useful +make help # Show all commands +``` + +## Pre-commit Hooks + +Pre-commit hooks are configured to automatically check code before commits. + +### Setup + +```bash +make pre-commit +``` + +### Hooks Included + +- Trailing whitespace removal +- End-of-file fixer +- YAML validation +- Large file detection (>1MB) +- Merge conflict detection +- JSON validation +- Line ending normalization +- Import sorting (isort) +- Code linting (flake8) +- Type checking (mypy, optional) + +Run manually: +```bash +pre-commit run --all-files +``` + +## Configuration Files + +### GitHub Actions Workflow +- **File:** `.github/workflows/python-app.yaml` +- Defines all CI/CD pipeline stages +- Configures job dependencies and triggers + +### Pre-commit Config +- **File:** `.pre-commit-config.yaml` +- Defines hooks run before commits +- Includes linting and formatting tools + +### Makefile +- **File:** `Makefile` +- Provides convenient commands for local development +- Mirrors CI/CD pipeline steps + +### Package Configuration +- **File:** `pyproject.toml` - Modern Python project metadata +- **File:** `setup.py` - Package installation script +- **File:** `setup.cfg` - Tool configuration + - isort settings + - flake8 settings + - pytest configuration + - coverage settings + +### Environment +- **File:** `environment.yml` - Conda environment for testing +- Includes all runtime and development dependencies + +## Troubleshooting + +### Linting Failures + +If linting fails locally or in CI: + +```bash +# Fix imports +make format + +# Fix other issues manually (requires code changes) +make lint-check # See what needs fixing +``` + +### Test Failures + +```bash +# Run tests with verbose output +make test-cov + +# Check coverage gaps +# Open htmlcov/index.html in browser +``` + +### Build Issues + +```bash +# Clean previous builds +make clean + +# Rebuild +make build + +# Check for issues +twine check dist/* +``` + +### Integration Test Failures + +```bash +# Debug locally +cd tests +RUN_INTEGRATION_TESTS=1 bash cicd_test.sh + +# Check logs in tests/LOGS directory +``` + +## Version Management + +MONSDA uses **versioneer** for automatic version management: + +1. Version is defined in git tags +2. Versioneer automatically derives version from git state +3. Package version matches git tag (e.g., tag `v1.0.0` → package version `1.0.0`) + +**Release process:** +1. Create release tag: `git tag v1.0.0` +2. Push tag: `git push --tags` +3. GitHub Actions automatically builds and publishes to PyPI + +## Best Practices + +1. **Always run linting before committing:** + ```bash + make pre-commit # or rely on pre-commit hooks + ``` + +2. **Run tests locally before pushing:** + ```bash + make test-cov + ``` + +3. **Use meaningful commit messages** that reference issue numbers + +4. **Add tests for new features** to maintain coverage + +5. **Keep dependencies updated** by regularly running `pip install --upgrade` + +6. **Review CI logs** if builds fail to understand what needs fixing + +## Contact & Support + +For issues or questions about the CI/CD pipeline: +- Check GitHub Actions logs for specific failures +- Review this documentation +- Open an issue on GitHub +- Contact the maintainers + +## References + +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [pytest Documentation](https://docs.pytest.org/) +- [flake8 Documentation](https://flake8.pycqa.org/) +- [isort Documentation](https://pycqa.github.io/isort/) +- [Pre-commit Framework](https://pre-commit.com/) +- [Setuptools Documentation](https://setuptools.pypa.io/) +- [Versioneer](https://github.com/python-versioneer/python-versioneer) diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7f10bc85 --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ +.PHONY: help install install-dev lint lint-check format test test-cov build clean clean-build clean-pyc pre-commit deploy + +help: + @echo "MONSDA Development Commands:" + @echo "" + @echo "Setup:" + @echo " make install Install package in development mode" + @echo " make install-dev Install with development dependencies" + @echo "" + @echo "Code Quality:" + @echo " make lint Run isort and flake8 linters" + @echo " make lint-check Check linting without fixing (CI mode)" + @echo " make format Format code with isort" + @echo " make pre-commit Run pre-commit hooks" + @echo "" + @echo "Testing:" + @echo " make test Run unit tests" + @echo " make test-cov Run tests with coverage report" + @echo " make test-integration Run integration tests (requires conda)" + @echo "" + @echo "Building:" + @echo " make build Build distribution packages" + @echo "" + @echo "Cleanup:" + @echo " make clean Remove all build/test artifacts" + @echo " make clean-build Remove build artifacts" + @echo " make clean-pyc Remove Python cache files" + @echo "" + +install: + python -m pip install --upgrade pip + python -m pip install -e . + +install-dev: install + python -m pip install pytest pytest-cov flake8 isort pre-commit build wheel twine + +lint: + isort MONSDA/ tests/ + flake8 MONSDA/ tests/ + +lint-check: + isort --check-only --diff MONSDA/ tests/ + flake8 MONSDA/ tests/ --count --statistics --show-source + +format: + isort MONSDA/ tests/ + +test: + pytest tests/test_Utils.py -v + +test-cov: + pytest tests/test_Utils.py -v --cov=MONSDA --cov-report=term-missing --cov-report=html + +test-integration: + cd tests && RUN_INTEGRATION_TESTS=1 bash cicd_test.sh + +build: clean + python -m pip install --upgrade pip + python -m pip install build wheel setuptools versioneer + python -m build + python -m pip install twine + twine check dist/* + +clean: clean-build clean-pyc + +clean-build: + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info + rm -rf .eggs/ + rm -rf *.egg + rm -rf .pytest_cache/ + rm -rf .coverage + rm -rf htmlcov/ + +clean-pyc: + find . -type f -name '*.py[cod]' -delete + find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true + find . -type d -name '*.egg-info' -exec rm -rf {} + 2>/dev/null || true + +pre-commit: + pre-commit install + pre-commit run --all-files + +deploy: build + python -m twine upload dist/* From 54a007a4d80d963e3cf567b597dcef0cecc2d307 Mon Sep 17 00:00:00 2001 From: jfallmann Date: Tue, 23 Jun 2026 22:14:24 +0200 Subject: [PATCH 03/80] version config automation --- .github/workflows/python-app.yaml | 8 ++- CICD.md | 33 +++++++++++ Makefile | 14 ++++- scripts/update_config_versions.py | 95 +++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 scripts/update_config_versions.py diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index b2cca401..f1f142ea 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -89,6 +89,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.12" @@ -97,6 +98,9 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install build wheel setuptools versioneer + - name: Sync config versions + run: | + python scripts/update_config_versions.py configs/*.json tests/data/config_*.json - name: Build distribution run: | python -m build @@ -126,6 +130,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + fetch-depth: 0 - uses: conda-incubator/setup-miniconda@v3 with: miniconda-version: "latest" @@ -146,8 +151,7 @@ jobs: - name: Prepare integration test config run: | cd ${{ github.workspace }} - VERSION=$(monsda --version 2>&1 | sed 's/MONSDA version //g') - sed -i "s/\"VERSION\": \"FIXME\"/\"VERSION\": \"${VERSION}\"/g" tests/data/config_Test.json + python scripts/update_config_versions.py tests/data/config_Test.json - name: Run full pipeline test run: | cd ${{ github.workspace }}/tests diff --git a/CICD.md b/CICD.md index 92c998c3..1c3efdb7 100644 --- a/CICD.md +++ b/CICD.md @@ -160,6 +160,10 @@ make build # Build packages make clean # Clean all artifacts make clean-pyc # Remove Python cache files +# Versioning +make sync-config-versions # Stamp config VERSION fields with the git-tag version +make check-config-versions # Verify config VERSION fields are in sync + # Useful make help # Show all commands ``` @@ -282,6 +286,35 @@ MONSDA uses **versioneer** for automatic version management: 2. Push tag: `git push --tags` 3. GitHub Actions automatically builds and publishes to PyPI +### Config Version Synchronization + +The `VERSION` field in the config files under `configs/` and `tests/data/` +is kept in sync with the versioneer-derived version automatically, so that +`monsda`'s runtime version check (`monsda_check_version`) always passes. + +This is handled by `scripts/update_config_versions.py`, which mirrors the same +version source used at runtime (`MONSDA.__version__`, falling back to +`python setup.py --version`). The script is comment-safe (config files contain +`#` comments and are not strict JSON) and only rewrites empty, `FIXME`, or +version-like `VERSION` values, leaving documentation strings untouched. + +It runs automatically in CI: +- In the **build** job, before `python -m build`, so the published package ships + templates matching the release version. +- In the **full-pipeline** job, before the integration test, replacing the old + `sed`-based patching. + +Both jobs use `fetch-depth: 0` on checkout so versioneer can resolve git tags. + +Run locally: +```bash +make sync-config-versions # Stamp config VERSION fields with the git-tag version +make check-config-versions # Verify config VERSION fields are in sync (CI mode) + +# Or invoke the script directly with an explicit version +python scripts/update_config_versions.py --version 1.5.0 configs/*.json +``` + ## Best Practices 1. **Always run linting before committing:** diff --git a/Makefile b/Makefile index 7f10bc85..869d272d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install install-dev lint lint-check format test test-cov build clean clean-build clean-pyc pre-commit deploy +.PHONY: help install install-dev lint lint-check format test test-cov build clean clean-build clean-pyc pre-commit deploy sync-config-versions check-config-versions help: @echo "MONSDA Development Commands:" @@ -18,6 +18,10 @@ help: @echo " make test-cov Run tests with coverage report" @echo " make test-integration Run integration tests (requires conda)" @echo "" + @echo "Versioning:" + @echo " make sync-config-versions Stamp config VERSION fields with the git-tag version" + @echo " make check-config-versions Verify config VERSION fields match (CI mode)" + @echo "" @echo "Building:" @echo " make build Build distribution packages" @echo "" @@ -54,7 +58,13 @@ test-cov: test-integration: cd tests && RUN_INTEGRATION_TESTS=1 bash cicd_test.sh -build: clean +sync-config-versions: + python scripts/update_config_versions.py configs/*.json tests/data/config_*.json + +check-config-versions: + python scripts/update_config_versions.py --check configs/*.json tests/data/config_*.json + +build: clean sync-config-versions python -m pip install --upgrade pip python -m pip install build wheel setuptools versioneer python -m build diff --git a/scripts/update_config_versions.py b/scripts/update_config_versions.py new file mode 100644 index 00000000..d5f17ea8 --- /dev/null +++ b/scripts/update_config_versions.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +"""Synchronize the "VERSION" field in MONSDA config files with the +versioneer-derived package version (single source of truth). + +This mirrors what versioneer does for the Python package: instead of hard +coding a version, the value is derived from the current git tag/state so the +config files always fit the release they ship with. + +MONSDA config files may contain ``#`` comments (i.e. they are not strict +JSON), therefore replacement is line/regex based and preserves comments, +indentation and trailing commas. Only values that are empty, ``FIXME`` or +version-like are replaced, so documentation strings such as +``"VERSION": "Version of MONSDA to use, ..."`` are left untouched. +""" + +import argparse +import subprocess +import sys +from pathlib import Path +import re + +VERSION_LINE = re.compile( + r'^(?P
\s*"VERSION"\s*:\s*")(?P[^"]*)(?P".*)$', re.MULTILINE
+)
+VERSION_LIKE = re.compile(r"^v?\d+(\.\d+)*([._\-+].*)?$")
+
+
+def get_version():
+    """Return the versioneer-derived version (same value used at runtime)."""
+    try:
+        from MONSDA import __version__
+
+        return __version__
+    except Exception:
+        root = Path(__file__).resolve().parent.parent
+        out = subprocess.check_output(
+            [sys.executable, "setup.py", "--version"], cwd=root
+        )
+        return out.decode().strip().splitlines()[-1].strip()
+
+
+def is_replaceable(val):
+    v = val.strip()
+    return v == "" or v.upper() == "FIXME" or bool(VERSION_LIKE.match(v))
+
+
+def update_text(text, version):
+    changed = {"n": 0}
+
+    def repl(m):
+        if is_replaceable(m.group("val")) and m.group("val") != version:
+            changed["n"] += 1
+            return m.group("pre") + version + m.group("post")
+        return m.group(0)
+
+    return VERSION_LINE.sub(repl, text), changed["n"]
+
+
+def main(argv=None):
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("files", nargs="+", help="Config files to update")
+    parser.add_argument(
+        "--version",
+        default=None,
+        help="Version to write (default: versioneer-derived package version)",
+    )
+    parser.add_argument(
+        "--check",
+        action="store_true",
+        help="Do not modify files; exit non-zero if any file is out of sync",
+    )
+    args = parser.parse_args(argv)
+
+    version = args.version or get_version()
+    rc = 0
+    for f in args.files:
+        path = Path(f)
+        if not path.is_file():
+            print(f"skip (not found): {f}", file=sys.stderr)
+            continue
+        text = path.read_text()
+        new, n = update_text(text, version)
+        if n == 0:
+            continue
+        if args.check:
+            print(f"out of sync: {f} (expected VERSION={version})", file=sys.stderr)
+            rc = 1
+        else:
+            path.write_text(new)
+            print(f"updated {f}: VERSION -> {version} ({n} field(s))")
+    return rc
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

From a616bb0e23ff108f121afd71edb2c87caf27c61f Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Thu, 2 Jul 2026 17:13:40 +0200
Subject: [PATCH 04/80] added gene coordinates to all DE output to make it
 easier to catch CNV like patterns

---
 scripts/Analysis/DAS/EDGER.R       | 20 +++++++++-----
 scripts/Analysis/DE/DESEQ2.R       | 30 ++++++++++++---------
 scripts/Analysis/DE/EDGER.R        | 23 +++++++++++-----
 scripts/Analysis/DE/EDGER_no_rep.R |  9 +++++++
 scripts/Analysis/DEU/DEXSEQ.R      |  1 +
 scripts/Analysis/DEU/EDGER.R       |  3 +++
 scripts/Analysis/DTU/DEXSEQ.R      |  1 +
 scripts/Analysis/DTU/DRIMSEQ.R     |  4 +++
 scripts/Analysis/DTU/SPIT.py       | 11 ++++++--
 scripts/lib/_lib.R                 | 43 ++++++++++++++++++++++++++++++
 10 files changed, 117 insertions(+), 28 deletions(-)

diff --git a/scripts/Analysis/DAS/EDGER.R b/scripts/Analysis/DAS/EDGER.R
index f38a7c21..c8f55e4a 100755
--- a/scripts/Analysis/DAS/EDGER.R
+++ b/scripts/Analysis/DAS/EDGER.R
@@ -98,6 +98,7 @@ tmm.genes <- tmm
 tmm.genes$Gene <- lapply(tmm.genes$ID, function(x) {
     get_gene_name(x, gtf_gene)
 })
+tmm.genes <- add_gene_coordinates(tmm.genes, tmm.genes$ID, gtf_gene, after = "ID")
 tmm.genes <- as.data.frame(apply(tmm.genes, 2, as.character))
 write.table(as.data.frame(tmm.genes), gzfile(paste("Tables/DAS", "EDGER", combi, "DataSet", "table", "AllConditionsNormalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
@@ -203,6 +204,7 @@ for (compare in comparisons[[1]]) {
     tmm.genes$Gene <- lapply(tmm.genes$ID, function(x) {
         get_exon_name(x, gtf.df)
     })
+    tmm.genes <- add_gene_coordinates(tmm.genes, vapply(strsplit(as.character(tmm.genes$ID), split = ":"), `[`, character(1), 1), gtf_gene, after = "ID")
     tmm.genes <- as.data.frame(apply(tmm.genes, 2, as.character))
     write.table(as.data.frame(tmm.genes), gzfile(paste("Tables/DAS", "EDGER", combi, contrast_name, "DataSet", "table", "Normalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
@@ -252,29 +254,35 @@ for (compare in comparisons[[1]]) {
         comparison_objs <- append(comparison_objs, sp)
 
         tops <- topSpliceDGE(sp, test = "gene", n = length(fit$counts))
-        tops$Gene <- lapply(strsplit(rownames(tops), split = ":")[[1]][1], function(x) {
+        gene_ids <- vapply(strsplit(rownames(tops), split = ":"), `[`, character(1), 1)
+        tops$Gene <- vapply(gene_ids, function(x) {
             get_gene_name(x, gtf.df)
-        })
+        }, character(1))
         tops$Gene_ID <- rownames(tops)
         tops <- tops[, c(7, 6, 3, 4, 5, 2)]
+        tops <- add_gene_coordinates(tops, gene_ids, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(as.data.frame(tops), gzfile(paste("Tables/DAS", "EDGER", combi, contrast_name, "table", "resultsGeneTest.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         tops <- topSpliceDGE(sp, test = "simes", n = length(fit$counts))
-        tops$Gene <- lapply(strsplit(rownames(tops), split = ":")[[1]][1], function(x) {
+        gene_ids <- vapply(strsplit(rownames(tops), split = ":"), `[`, character(1), 1)
+        tops$Gene <- vapply(gene_ids, function(x) {
             get_gene_name(x, gtf_gene)
-        })
+        }, character(1))
         tops$Gene_ID <- rownames(tops)
         tops <- tops[, c(6, 5, 2, 3, 4)]
+        tops <- add_gene_coordinates(tops, gene_ids, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(as.data.frame(tops), gzfile(paste("Tables/DAS", "EDGER", combi, contrast_name, "table", "resultsSimesTest.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         tops <- topSpliceDGE(sp, test = "exon", n = length(fit$counts))
-        tops$Gene <- lapply(strsplit(rownames(tops), split = ":")[[1]][1], function(x) {
+        gene_ids <- vapply(strsplit(rownames(tops), split = ":"), `[`, character(1), 1)
+        tops$Gene <- vapply(gene_ids, function(x) {
             get_gene_name(x, gtf_gene)
-        })
+        }, character(1))
         tops$Gene_ID <- rownames(tops)
         tops <- tops[, c(8, 7, 4, 5, 6, 2, 3)]
+        tops <- add_gene_coordinates(tops, gene_ids, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(as.data.frame(tops), gzfile(paste("Tables/DAS", "EDGER", combi, contrast_name, "table", "resultsDiffSpliceExonTest.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
diff --git a/scripts/Analysis/DE/DESEQ2.R b/scripts/Analysis/DE/DESEQ2.R
index 8d15b48c..aa3e59db 100755
--- a/scripts/Analysis/DE/DESEQ2.R
+++ b/scripts/Analysis/DE/DESEQ2.R
@@ -76,8 +76,8 @@ cpm_matrix <- calc_cpm(countData_all)
 tpm_matrix <- calc_tpm(countData_all, gtf_gene)
 
 # Write out CPM and TPM tables
-write.table(cpm_matrix, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "cpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-write.table(tpm_matrix, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "tpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+write.table(add_gene_coordinates(cpm_matrix, rownames(cpm_matrix), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "cpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+write.table(add_gene_coordinates(tpm_matrix, rownames(tpm_matrix), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "tpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
 
 # Normalize by spike in if available
 if (spike != "") {
@@ -91,17 +91,17 @@ if (spike != "") {
     counts_norm <- as.data.frame(normCounts(counts_norm))
     countData_clean <- countData_all %>% subset(!row.names(countData_all) %in% ctrlgenes) # removing spike-ins for standard analysis
     counts_norm_clean <- counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes) # removing spike-ins for standard analysis
-     write.table(counts_norm, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(counts_norm_clean, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_norm_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(countData_clean, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+     write.table(add_gene_coordinates(counts_norm, rownames(counts_norm), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(counts_norm_clean, rownames(counts_norm_clean), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_norm_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(countData_clean, rownames(countData_clean), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "counts_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
     write.table(sampleData_norm, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "sampleData_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
     # Calculate CPM and TPM
     cpm_norm_matrix <- calc_cpm(counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes))
     tpm_norm_matrix <- calc_tpm(counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes), gtf_gene)
 
     # Write out CPM and TPM tables
-    write.table(cpm_norm_matrix, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "cpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(tpm_norm_matrix, gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "tpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(cpm_norm_matrix, rownames(cpm_norm_matrix), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "cpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(tpm_norm_matrix, rownames(tpm_norm_matrix), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "tpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
 
 }
 #### Now plot and print over-all comparisons
@@ -129,8 +129,8 @@ print(DESeq2::plotPCA(vsd, intgroup = c("condition")) + geom_text_repel(aes(labe
 dev.off()
 
 # We also write the normalized counts to file
-write.table(as.data.frame(assay(rld)), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "rld.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
-write.table(as.data.frame(assay(vsd)), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "vsd.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+write.table(add_gene_coordinates(as.data.frame(assay(rld)), rownames(assay(rld)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "rld.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+write.table(add_gene_coordinates(as.data.frame(assay(vsd)), rownames(assay(vsd)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, "DataSet", "table", "vsd.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
 
 # Here we choose blind so that the initial conditions setting does not influence the outcome, ie we want to see if the conditions cluster based purely on the individual datasets, in an unbiased way. According to the documentation, the rlogTransformation method that converts counts to log2 values is apparently better than the old varienceStabilisation method when the data size factors vary by large amounts.
 
@@ -261,8 +261,8 @@ for (contrast in comparison[[1]]) {
         dev.off()
 
         # We also write the normalized counts to file
-        write.table(as.data.frame(assay(rld_norm)), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "rld_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
-        write.table(as.data.frame(assay(vsd_norm)), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "vsd_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+        write.table(add_gene_coordinates(as.data.frame(assay(rld_norm)), rownames(assay(rld_norm)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "rld_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+        write.table(add_gene_coordinates(as.data.frame(assay(vsd_norm)), rownames(assay(vsd_norm)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "vsd_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
     }
 
     # Create DESeqDataSet
@@ -294,8 +294,8 @@ for (contrast in comparison[[1]]) {
     dev.off()
 
     # We also write the normalized counts to file
-    write.table(as.data.frame(assay(rld)), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "rld.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
-    write.table(as.data.frame(assay(vsd)), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "vsd.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+    write.table(add_gene_coordinates(as.data.frame(assay(rld)), rownames(assay(rld)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "rld.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
+    write.table(add_gene_coordinates(as.data.frame(assay(vsd)), rownames(assay(vsd)), gtf_gene), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "vsd.tsv.gz", sep = "_")), sep = "\t", col.names = NA)
 
     tryCatch({
         # initialize empty objects
@@ -317,6 +317,7 @@ for (contrast in comparison[[1]]) {
         }))
         resOrdered$Gene_ID <- rownames(resOrdered)
         resOrdered <- resOrdered[, c(7, 6, 1, 2, 3, 4, 5)]
+        resOrdered <- add_gene_coordinates(resOrdered, resOrdered$Gene_ID, gtf_gene, after = "Gene_ID")
 
         # plotVolcano
         pdf(
@@ -357,6 +358,7 @@ for (contrast in comparison[[1]]) {
         })
         res$Gene_ID <- rownames(res)
         res <- res[, c(8, 7, 1, 2, 3, 5, 6)]
+        res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
         res <- as.data.frame(apply(res, 2, as.character))
 
         write.table(as.data.frame(res), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "results_noshrink.tsv.gz", sep = "_")), sep = "\t", row.names = FALSE, quote = F)
@@ -388,6 +390,7 @@ for (contrast in comparison[[1]]) {
             }))
             resOrdered$Gene_ID <- rownames(resOrdered)
             resOrdered <- resOrdered[, c(7, 6, 1, 2, 3, 4, 5)]
+            resOrdered <- add_gene_coordinates(resOrdered, resOrdered$Gene_ID, gtf_gene, after = "Gene_ID")
 
             # plot Volcano
             pdf(
@@ -428,6 +431,7 @@ for (contrast in comparison[[1]]) {
             })
             res$Gene_ID <- rownames(res)
             res <- res[, c(7, 6, 1, 2, 3, 4, 5)]
+            res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
             res <- as.data.frame(apply(res, 2, as.character))
 
             write.table(as.data.frame(res), gzfile(paste("Tables/DE", "DESEQ2", combi, contrast_name, "table", "results_norm_noshrink.tsv.gz", sep = "_")), sep = "\t", row.names = FALSE, quote = F)
diff --git a/scripts/Analysis/DE/EDGER.R b/scripts/Analysis/DE/EDGER.R
index 6ed8bcfb..d32cc218 100755
--- a/scripts/Analysis/DE/EDGER.R
+++ b/scripts/Analysis/DE/EDGER.R
@@ -74,8 +74,8 @@ cpm_matrix <- calc_cpm(countData_all)
 tpm_matrix <- calc_tpm(countData_all, gtf_gene)
 
 ## Write out CPM and TPM tables
-write.table(cpm_matrix, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "cpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-write.table(tpm_matrix, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "tpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+write.table(add_gene_coordinates(cpm_matrix, rownames(cpm_matrix), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "cpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+write.table(add_gene_coordinates(tpm_matrix, rownames(tpm_matrix), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "tpm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
 
 ## Normalize by spike in if available
 if (spike != "") {
@@ -89,17 +89,17 @@ if (spike != "") {
     counts_norm <- as.data.frame(normCounts(counts_norm))
     countData_clean <- countData_all %>% subset(!row.names(countData_all) %in% ctrlgenes) # removing spike-ins for standard analysis
     counts_norm_clean <- counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes) # removing spike-ins for standard analysis
-    write.table(counts_norm, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(counts_norm_clean, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_norm_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(countData_clean, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(counts_norm, rownames(counts_norm), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(counts_norm_clean, rownames(counts_norm_clean), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_norm_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(countData_clean, rownames(countData_clean), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "counts_clean.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
     write.table(sampleData_norm, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "sampleData_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
     # Calculate CPM and TPM
     cpm_norm_matrix <- calc_cpm(counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes))
     tpm_norm_matrix <- calc_tpm(counts_norm %>% subset(!row.names(counts_norm) %in% ctrlgenes), gtf_gene)
 
     # Write out CPM and TPM tables
-    write.table(cpm_norm_matrix, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "cpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
-    write.table(tpm_norm_matrix, gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "tpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(cpm_norm_matrix, rownames(cpm_norm_matrix), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "cpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
+    write.table(add_gene_coordinates(tpm_norm_matrix, rownames(tpm_norm_matrix), gtf_gene), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "tpm_norm.tsv.gz", sep = "_")), sep = "\t", col.names = NA, quote = FALSE)
 }
 
 ## Create design-table considering different types (paired, unpaired) and batches
@@ -126,6 +126,7 @@ colnames(tmm) <- t(dge$samples$samples)
 tmm$ID <- dge$genes$genes
 tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
 write.table(as.data.frame(tmm), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "AllConditionsNormalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
 ## create file MDS-plot with and without summarized replicates
@@ -231,6 +232,7 @@ for (contrast in comparison[[1]]) {
         tmm_norm$ID <- dge_norm$genes$genes
         tmm_norm <- tmm_norm[c(ncol(tmm_norm), 1:ncol(tmm_norm) - 1)]
 
+        tmm_norm <- add_gene_coordinates(tmm_norm, tmm_norm$ID, gtf_gene, after = "ID")
         write.table(as.data.frame(tmm_norm), gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "Normalized_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         ## create file MDS-plot with and without summarized replicates
@@ -278,6 +280,7 @@ for (contrast in comparison[[1]]) {
     tmm$ID <- dge$genes$genes
     tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+    tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
     write.table(as.data.frame(tmm), gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "Normalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
     ## create file MDS-plot with and without summarized replicates
@@ -331,6 +334,7 @@ for (contrast in comparison[[1]]) {
         qlf$table$Gene_ID <- rownames(qlf$table)
         res <- qlf$table[, c(6, 5, 2, 1, 3, 4)]
         res$FDR <- p.adjust(res$PValue, method = "BH")
+        res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
 
         # plotVolcano
         pdf(
@@ -366,11 +370,13 @@ for (contrast in comparison[[1]]) {
         # create sorted results Tables
         tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "logFC")
         tops <- tops$table[, c(7, 6, 3, 2, 4, 5, 8)]
+        tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsLogFCsorted.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "PValue")
         tops <- tops$table[, c(7, 6, 3, 2, 4, 5, 8)]
+        tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsPValueSorted.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
@@ -412,6 +418,7 @@ for (contrast in comparison[[1]]) {
             qlf$table$Gene_ID <- rownames(qlf$table)
             res <- qlf$table[, c(6, 5, 1, 2, 3, 4)]
             res$FDR <- p.adjust(res$PValue, method = "BH")
+            res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
 
             # plotVolcano
             pdf(
@@ -447,11 +454,13 @@ for (contrast in comparison[[1]]) {
             # create sorted results Tables
             tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "logFC")
             tops <- tops$table[, c(7, 6, 4, 2, 3, 5, 8)]
+            tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
             tops <- as.data.frame(apply(tops, 2, as.character))
             write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsLogFCsorted_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
             tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "PValue")
             tops <- tops$table[, c(7, 6, 4, 2, 3, 5, 8)]
+            tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
             tops <- as.data.frame(apply(tops, 2, as.character))
             write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsPValueSorted_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
diff --git a/scripts/Analysis/DE/EDGER_no_rep.R b/scripts/Analysis/DE/EDGER_no_rep.R
index 7d63d483..ea27bbd9 100755
--- a/scripts/Analysis/DE/EDGER_no_rep.R
+++ b/scripts/Analysis/DE/EDGER_no_rep.R
@@ -101,6 +101,7 @@ colnames(tmm) <- t(dge$samples$samples)
 tmm$ID <- dge$genes$genes
 tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
 write.table(as.data.frame(tmm), gzfile(paste("Tables/DE", "EDGER", combi, "DataSet", "table", "AllConditionsNormalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
 ## create dummy file MDS-plot with and without summarized replicates
@@ -194,6 +195,7 @@ for (contrast in comparison[[1]]) {
         tmm_norm$ID <- dge_norm$genes$genes
         tmm_norm <- tmm_norm[c(ncol(tmm_norm), 1:ncol(tmm_norm) - 1)]
 
+        tmm_norm <- add_gene_coordinates(tmm_norm, tmm_norm$ID, gtf_gene, after = "ID")
         write.table(as.data.frame(tmm_norm), gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "DataSet", "table", "Normalized_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         ## create dummy file MDS-plot with and without summarized replicates
@@ -232,6 +234,7 @@ for (contrast in comparison[[1]]) {
     tmm$ID <- dge$genes$genes
     tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+    tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
     write.table(as.data.frame(tmm), gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "DataSet", "table", "Normalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
     ## create dummy file MDS-plot with and without summarized replicates
@@ -283,6 +286,7 @@ for (contrast in comparison[[1]]) {
         qlf$table$Gene_ID <- rownames(qlf$table)
         res <- qlf$table[, c(5, 4, 2, 1, 3)]
         res$FDR <- p.adjust(res$PValue, method = "BH")
+        res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
 
         # plotVolcano
         pdf(
@@ -318,11 +322,13 @@ for (contrast in comparison[[1]]) {
         # create sorted results Tables
         tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "logFC")
         tops <- tops$table[, c(6, 5, 3, 2, 4, 7)]
+        tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsLogFCsorted.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
         tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "PValue")
         tops <- tops$table[, c(6, 5, 3, 2, 4, 7)]
+        tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
         tops <- as.data.frame(apply(tops, 2, as.character))
         write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsPValueSorted.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
@@ -366,6 +372,7 @@ for (contrast in comparison[[1]]) {
             qlf$table$Gene_ID <- rownames(qlf$table)
             res <- qlf$table[, c(5, 4, 2, 1, 3)]
             res$FDR <- p.adjust(res$PValue, method = "BH")
+            res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
 
             # plotVolcano
             pdf(
@@ -401,11 +408,13 @@ for (contrast in comparison[[1]]) {
             # create sorted results Tables
             tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "logFC")
             tops <- tops$table[, c(6, 5, 3, 2, 4, 7)]
+            tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
             tops <- as.data.frame(apply(tops, 2, as.character))
             write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsLogFCsorted_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
             tops <- topTags(qlf, n = nrow(qlf$table), sort.by = "PValue")
             tops <- tops$table[, c(6, 5, 3, 2, 4, 7)]
+            tops <- add_gene_coordinates(tops, tops$Gene_ID, gtf_gene, after = "Gene_ID")
             tops <- as.data.frame(apply(tops, 2, as.character))
             write.table(tops, gzfile(paste("Tables/DE", "EDGER", combi, contrast_name, "table", "resultsPValueSorted_norm.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
diff --git a/scripts/Analysis/DEU/DEXSEQ.R b/scripts/Analysis/DEU/DEXSEQ.R
index c2fa38a6..b66c1c8d 100755
--- a/scripts/Analysis/DEU/DEXSEQ.R
+++ b/scripts/Analysis/DEU/DEXSEQ.R
@@ -179,6 +179,7 @@ for (contrast in comparisons[[1]]) {
 
         toprint <- as.data.frame(dxr1)
         toprint$transcripts <- vapply(toprint$transcripts, paste, collapse = ", ", character(1L))
+        toprint <- add_gene_coordinates(toprint, toprint$groupID, gtf.df, after = "groupID")
         write.table(toprint, gzfile(paste("Tables/DEU", "DEXSEQ", combi, contrast_name, "table", "results.tsv.gz", sep = "_")), sep = "\t", row.names = FALSE, quote = F)
 
 
diff --git a/scripts/Analysis/DEU/EDGER.R b/scripts/Analysis/DEU/EDGER.R
index 763a4afa..6511bafa 100755
--- a/scripts/Analysis/DEU/EDGER.R
+++ b/scripts/Analysis/DEU/EDGER.R
@@ -97,6 +97,7 @@ colnames(tmm) <- t(dge$samples$samples)
 tmm$ID <- dge$genes$genes
 tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
 write.table(as.data.frame(tmm), gzfile(paste("Tables/DEU", "EDGER", combi, "DataSet", "table", "AllConditionsNormalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
 ## create file MDS-plot with and without summarized replicates
@@ -198,6 +199,7 @@ for (compare in comparisons[[1]]) {
     tmm$ID <- dge$genes$genes
     tmm <- tmm[c(ncol(tmm), 1:ncol(tmm) - 1)]
 
+    tmm <- add_gene_coordinates(tmm, tmm$ID, gtf_gene, after = "ID")
     write.table(as.data.frame(tmm), gzfile(paste("Tables/DEU", "EDGER", combi, contrast_name, "DataSet", "table", "Normalized.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
     ## create file MDS-plot with and without summarized replicates
@@ -253,6 +255,7 @@ for (compare in comparisons[[1]]) {
         qlf$table$Gene_ID <- qlf$genes$genes
         qlf$table$ExonPos <- qlf$genes$exons
         res <- qlf$table[, c(6, 5, 7, 1, 2, 3, 4)]
+        res <- add_gene_coordinates(res, res$Gene_ID, gtf_gene, after = "Gene_ID")
         res <- as.data.frame(apply(res, 2, as.character))
 
         # create results table
diff --git a/scripts/Analysis/DTU/DEXSEQ.R b/scripts/Analysis/DTU/DEXSEQ.R
index 5808698f..bd3a865e 100755
--- a/scripts/Analysis/DTU/DEXSEQ.R
+++ b/scripts/Analysis/DTU/DEXSEQ.R
@@ -171,6 +171,7 @@ for (contrast in comparisons[[1]]) {
     toprint$transcripts <- vapply(toprint$featureID, paste, collapse = ", ", character(1L))
     toprint$genes <- vapply(toprint$Gene, paste, collapse = ", ", character(1L))
     toprint <- subset(toprint, select = -c(Gene, featureID))
+    toprint <- add_gene_coordinates(toprint, toprint$groupID, gtf.df, after = "groupID")
 
     write.table(toprint, gzfile(paste("Tables/DTU", "DEXSEQ", combi, contrast_name, "table", "results.tsv.gz", sep = "_")), sep = "\t", quote = F, row.names = FALSE)
 
diff --git a/scripts/Analysis/DTU/DRIMSEQ.R b/scripts/Analysis/DTU/DRIMSEQ.R
index aab0fc31..c3a41d65 100755
--- a/scripts/Analysis/DTU/DRIMSEQ.R
+++ b/scripts/Analysis/DTU/DRIMSEQ.R
@@ -318,6 +318,9 @@ for (contrast in comparisons[[1]]) {
         get_gene_name(x, gtf.df)
     })
 
+    res <- add_gene_coordinates(res, res$gene_id, gtf.df, after = "gene_id")
+    res.txp <- add_gene_coordinates(res.txp, res.txp$gene_id, gtf.df, after = "gene_id")
+
     res <- res[order(res$adj_pvalue), ]
     res.txp <- res.txp[order(res$adj_pvalue), ]
     proportions <- proportions[order(proportions$lfc), ]
@@ -326,6 +329,7 @@ for (contrast in comparisons[[1]]) {
     for (c in 3:(length(colnames(proportions.print)) - 4)) {
         colnames(proportions.print)[c] <- sampleData$original_sample_id[sampleData$sample_id == colnames(proportions.print)[c]]
     }
+    proportions.print <- add_gene_coordinates(proportions.print, proportions.print$gene_id, gtf.df, after = "gene_id")
     res.print <- as.data.frame(apply(res, 2, as.character))
     res.txp.print <- as.data.frame(apply(res.txp, 2, as.character))
 
diff --git a/scripts/Analysis/DTU/SPIT.py b/scripts/Analysis/DTU/SPIT.py
index 8a354203..4c7770f3 100644
--- a/scripts/Analysis/DTU/SPIT.py
+++ b/scripts/Analysis/DTU/SPIT.py
@@ -53,6 +53,7 @@ def parse_tx2gene(gtf):
     logid = scriptname + ".parse_tx2gene: "
     tx2gene = {}
     gene2name = {}
+    gene2coord = {}
     opn = gzip.open if str(gtf).endswith(".gz") else open
     with opn(gtf, "rt") as fh:
         for line in fh:
@@ -65,6 +66,9 @@ def parse_tx2gene(gtf):
             tid = re.search(r'transcript_id "?([^";]+)"?', attr)
             gid = re.search(r'gene_id "?([^";]+)"?', attr)
             gname = re.search(r'gene_name "?([^";]+)"?', attr)
+            if cols[2] == "gene" and gid:
+                g = re.sub(r"\.[0-9]+$", "", gid.group(1))
+                gene2coord[g] = cols[0] + ":" + cols[3] + ":" + cols[4] + ":" + cols[6]
             if tid and gid:
                 t = re.sub(r"\.[0-9]+$", "", tid.group(1))
                 g = re.sub(r"\.[0-9]+$", "", gid.group(1))
@@ -72,7 +76,7 @@ def parse_tx2gene(gtf):
                 if gname:
                     gene2name[g] = gname.group(1)
     log.debug(logid + "found " + str(len(tx2gene)) + " transcripts")
-    return tx2gene, gene2name
+    return tx2gene, gene2name, gene2coord
 
 
 def run_spit_cmd(subcmd, args_list):
@@ -112,7 +116,7 @@ def main(anname, gtf, outdir, cmp, combi, cores, spitopts):
         else:
             dtu_extra = spitopts.split()
 
-    tx2gene, gene2name = parse_tx2gene(gtf)
+    tx2gene, gene2name, gene2coord = parse_tx2gene(gtf)
 
     sampleData_all = pd.read_csv(anname, sep="\t", compression="gzip", dtype=str)
     sampleData_all["sample_id"] = sampleData_all["sample_id"].astype(str)
@@ -236,6 +240,8 @@ def main(anname, gtf, outdir, cmp, combi, cores, spitopts):
         gene_tab = gene_tab.sort_values("pvalue", na_position="last")
 
         prefix = "_".join(["DTU", "SPIT", combi, contrast_name, "table"])
+        gene_tab.insert(gene_tab.columns.get_loc("gene_id") + 1, "Coordinates", [gene2coord.get(g, "NA") for g in gene_tab["gene_id"]])
+        tx_tab.insert(tx_tab.columns.get_loc("gene_id") + 1, "Coordinates", [gene2coord.get(g, "NA") for g in tx_tab["gene_id"]])
         gene_tab.to_csv(
             os.path.join("Tables", prefix + "_genes.tsv.gz"),
             sep="\t", index=False, compression="gzip",
@@ -246,6 +252,7 @@ def main(anname, gtf, outdir, cmp, combi, cores, spitopts):
         )
         ifs_out = ifs_df.copy()
         ifs_out.insert(0, "feature_id", ifs_out.index)
+        ifs_out.insert(ifs_out.columns.get_loc("gene_id") + 1, "Coordinates", [gene2coord.get(g, "NA") for g in ifs_out["gene_id"]])
         ifs_out.to_csv(
             os.path.join("Tables", prefix + "_ifs.tsv.gz"),
             sep="\t", index=False, compression="gzip",
diff --git a/scripts/lib/_lib.R b/scripts/lib/_lib.R
index 7b551306..fbe40116 100644
--- a/scripts/lib/_lib.R
+++ b/scripts/lib/_lib.R
@@ -36,6 +36,49 @@ get_exon_name <- function(id, df) {
     }
 }
 
+get_gene_coords <- function(id, df) {
+    if (!"gene_id" %in% colnames(df)) {
+        message("WARNING: gene_id not found as colname, will be replaced by first match of colname with ID")
+        colnames(df)[grepl("id$", names(df), ignore.case = TRUE)][1] <- "gene_id"
+    }
+    coord_rows <- df[df["type"] == "gene" & df["gene_id"] == id, ]
+    if (nrow(coord_rows) == 0) {
+        return(NA_character_)
+    }
+    coord_list <- paste(coord_rows$seqnames, coord_rows$start, coord_rows$end, coord_rows$strand, sep = ":")
+    if (length(unique(coord_list)) == 1) {
+        return(coord_list[1])
+    } else {
+        message(paste("WARNING: ambigous gene id: ", id))
+        return(paste(unique(coord_list), collapse = "|"))
+    }
+}
+
+
+add_gene_coordinates <- function(df, gene_ids, gtf_df, after = NULL) {
+    if (!"gene_id" %in% colnames(gtf_df)) {
+        message("WARNING: gene_id not found as colname, will be replaced by first match of colname with ID")
+        colnames(gtf_df)[grepl("id$", names(gtf_df), ignore.case = TRUE)][1] <- "gene_id"
+    }
+    df <- as.data.frame(df)
+    g <- gtf_df[gtf_df["type"] == "gene", ]
+    coords <- paste(g$seqnames, g$start, g$end, g$strand, sep = ":")
+    names(coords) <- as.character(g$gene_id)
+    df$Coordinates <- unname(coords[as.character(gene_ids)])
+    cols <- colnames(df)
+    cols <- cols[cols != "Coordinates"]
+    if (!is.null(after) && !is.na(after) && after %in% cols) {
+        pos <- match(after, cols)
+        new_order <- append(cols, "Coordinates", after = pos)
+    } else {
+        if (!is.null(after) && !is.na(after)) {
+            message(paste("WARNING: column", after, "not found, placing Coordinates first"))
+        }
+        new_order <- c("Coordinates", cols)
+    }
+    df[, new_order, drop = FALSE]
+}
+
 fpkmToTpm <- function(fpkm){
     exp(log(fpkm) - log(sum(fpkm)) + log(1e6))
 }

From c6e3953edf9cbd8150b0737a6151dbab6950db2e Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Fri, 3 Jul 2026 13:26:10 +0200
Subject: [PATCH 05/80] major overhaul of pseudomapping and single-cell
 processing. Moved to rust implementation of salmon, added rammap as minimap
 alternative, alevin-fry/piscem and simpleaf as well as oarfish. salmon can
 nor quantify directly from spliced bam as intended by creators. optional dtu
 processing via terminus for more stable calling

---
 MONSDA/Workflows.py                  |  44 ++++++---
 configs/template.json                |   8 +-
 configs/template_base_commented.json |  96 ++++++++++++++++---
 configs/template_clean.json          |   8 +-
 configs/template_umicollapse.json    |   2 +-
 docs/source/config.rst               |   9 ++
 docs/source/workflows.rst            |  32 +++++--
 envs/alevinfry.yaml                  |   7 ++
 envs/minimap.yaml                    |   2 +-
 envs/oarfish.yaml                    |   8 ++
 envs/piscem.yaml                     |   8 ++
 envs/rammap.yaml                     |  12 +++
 envs/salmon.yaml                     |   5 +-
 envs/simpleaf.yaml                   |  10 ++
 envs/terminus.yaml                   |   7 ++
 tests/data/config_Test.json          |   2 +-
 tests/data/config_Test_local.json    |   2 +-
 workflows/alevinfry.nf               |  74 +++++++++++++++
 workflows/alevinfry.smk              |  34 +++++++
 workflows/dexseq_DTU.nf              |  36 +++++++-
 workflows/dexseq_DTU.smk             |  33 ++++++-
 workflows/drimseq_DTU.nf             |  36 +++++++-
 workflows/drimseq_DTU.smk            |  33 ++++++-
 workflows/oarfish.nf                 |  53 +++++++++++
 workflows/oarfish.smk                |  26 ++++++
 workflows/piscem.nf                  | 113 +++++++++++++++++++++++
 workflows/piscem.smk                 |  39 ++++++++
 workflows/rammap.nf                  | 118 ++++++++++++++++++++++++
 workflows/rammap.smk                 |  51 +++++++++++
 workflows/salmon.nf                  |  84 +++++++++++++----
 workflows/salmon.smk                 |  37 +++++++-
 workflows/salmonalign.nf             | 132 +++++++++++++++++++++++++++
 workflows/salmonalign.smk            |  55 +++++++++++
 workflows/simpleaf.nf                | 101 ++++++++++++++++++++
 workflows/simpleaf.smk               |  43 +++++++++
 workflows/spit_DTU.nf                |  36 +++++++-
 workflows/spit_DTU.smk               |  33 ++++++-
 37 files changed, 1355 insertions(+), 74 deletions(-)
 create mode 100644 envs/alevinfry.yaml
 create mode 100644 envs/oarfish.yaml
 create mode 100644 envs/piscem.yaml
 create mode 100644 envs/rammap.yaml
 create mode 100644 envs/simpleaf.yaml
 create mode 100644 envs/terminus.yaml
 create mode 100644 workflows/alevinfry.nf
 create mode 100644 workflows/alevinfry.smk
 create mode 100644 workflows/oarfish.nf
 create mode 100644 workflows/oarfish.smk
 create mode 100644 workflows/piscem.nf
 create mode 100644 workflows/piscem.smk
 create mode 100644 workflows/rammap.nf
 create mode 100644 workflows/rammap.smk
 create mode 100644 workflows/salmonalign.nf
 create mode 100644 workflows/salmonalign.smk
 create mode 100644 workflows/simpleaf.nf
 create mode 100644 workflows/simpleaf.smk

diff --git a/MONSDA/Workflows.py b/MONSDA/Workflows.py
index 2206e7bb..8f863430 100755
--- a/MONSDA/Workflows.py
+++ b/MONSDA/Workflows.py
@@ -958,7 +958,9 @@ def make_sub(
                                 subjobs.append(line)
                             subjobs.append("\n\n")
 
-                if "MAPPING" in works:
+                if "MAPPING" in works and envs[works.index("MAPPING")] not in [
+                    "piscem"
+                ]:
                     smkf = os.path.abspath(os.path.join(workflowpath, "mapping.smk"))
                     with open(smkf, "r") as smk:
                         for line in mu.comment_remover(smk.readlines()):
@@ -1152,16 +1154,21 @@ def make_sub(
                         subjobs.append("\n\n")
 
             if "MAPPING" in subworkflows:
-                smkf = os.path.abspath(os.path.join(workflowpath, "mapping.smk"))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(condapath, 'conda: "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line, loglevel, condapath, envpath, workflowpath, logfix
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                _map_tools, _ = create_subworkflow(config, "MAPPING", [condition])
+                _skip_postmap = _map_tools is not None and any(
+                    str(t[0]) in ["piscem"] for t in _map_tools
+                )
+                if not _skip_postmap:
+                    smkf = os.path.abspath(os.path.join(workflowpath, "mapping.smk"))
+                    with open(smkf, "r") as smk:
+                        for line in mu.comment_remover(smk.readlines()):
+                            line = re.sub(condapath, 'conda: "' + envpath, line)
+                            if "include: " in line:
+                                line = fixinclude(
+                                    line, loglevel, condapath, envpath, workflowpath, logfix
+                                )
+                            subjobs.append(line)
+                        subjobs.append("\n\n")
                 if "QC" in subworkflows:
                     # Use rustqc-specific multiqc whenever rustqc is part of this combo
                     _qc_tools, _ = create_subworkflow(config, "QC", [condition])
@@ -3106,6 +3113,7 @@ def nf_make_sub(
 
                     flowlist.append("MAPPING")
 
+                    maprad = envs[works.index("MAPPING")] in ["piscem"]
                     nfi = os.path.abspath(os.path.join(workflowpath, "mapping.nf"))
                     with open(nfi, "r") as nf:
                         for line in mu.comment_remover(nf.readlines()):
@@ -3197,9 +3205,10 @@ def nf_make_sub(
                             subjobs.append(" " * 4 + w + "(TRIMMING.out.trimmed)\n")
                         elif w == "MAPPING":
                             subjobs.append(" " * 4 + w + "(TRIMMING.out.trimmed)\n")
-                            subjobs.append(
-                                " " * 4 + "POSTMAPPING(MAPPING.out.mapped)\n"
-                            )
+                            if not maprad:
+                                subjobs.append(
+                                    " " * 4 + "POSTMAPPING(MAPPING.out.mapped)\n"
+                                )
                         elif w == "DEDUPBAM":
                             if deduptool == "fgumi":
                                 subjobs.append(
@@ -3471,6 +3480,10 @@ def nf_make_sub(
                     )
 
             if "MAPPING" in subworkflows:
+                _map_tools, _ = create_subworkflow(config, "MAPPING", [condition])
+                maprad = _map_tools is not None and any(
+                    str(t[0]) in ["piscem"] for t in _map_tools
+                )
                 if "QC" not in subworkflows:
                     log.debug(logid + "Mapping without QC!")
                 if "TRIMMING" not in subworkflows:
@@ -3580,7 +3593,8 @@ def nf_make_sub(
                         subjobs.append(" " * 4 + w + "(TRIMMING.out.trimmed)\n")
                     elif w == "MAPPING":
                         subjobs.append(" " * 4 + w + "(TRIMMING.out.trimmed)\n")
-                        subjobs.append(" " * 4 + "POSTMAPPING(MAPPING.out.mapped)\n")
+                        if not maprad:
+                            subjobs.append(" " * 4 + "POSTMAPPING(MAPPING.out.mapped)\n")
                     elif w == "DEDUPBAM":
                         if deduptool == "fgumi":
                             subjobs.append(
diff --git a/configs/template.json b/configs/template.json
index df2c2bfc..5ec96ff3 100644
--- a/configs/template.json
+++ b/configs/template.json
@@ -242,7 +242,7 @@
                                             "OPTIONS":
                                             {
                                                 "INDEX": "",  # salmon index options
-                                                "QUANT": "--validateMappings --seqBias --gcBias",  # salmon quant options
+                                                "QUANT": "--seqBias --gcBias",  # salmon quant options
                                             }
                                         }
                                     }
@@ -464,7 +464,7 @@
                         "OPTIONS":
                         {
                             "INDEX": "",  # salmon index options
-                            "QUANT": "--validateMappings --seqBias --gcBias",  # salmon quant options
+                            "QUANT": "--seqBias --gcBias",  # salmon quant options
                             "DTU": ""  # Options for Analysis
                         }
                     },
@@ -473,7 +473,7 @@
                         "OPTIONS":
                         {
                             "INDEX": "",  # salmon index options
-                            "QUANT": "--validateMappings --seqBias --gcBias",  # salmon quant options
+                            "QUANT": "--seqBias --gcBias",  # salmon quant options
                             "DTU": ""  # Options for Analysis
                         }
                     },
@@ -482,7 +482,7 @@
                         "OPTIONS":
                         {
                             "INDEX": "",  # salmon index options
-                            "QUANT": "--validateMappings --seqBias --gcBias",  # salmon quant options
+                            "QUANT": "--seqBias --gcBias",  # salmon quant options
                             "DTU": ""  # Options for Analysis
                         }
                     }
diff --git a/configs/template_base_commented.json b/configs/template_base_commented.json
index 32b5987e..b3e820ad 100644
--- a/configs/template_base_commented.json
+++ b/configs/template_base_commented.json
@@ -207,7 +207,10 @@
       "hisat2": "hisat2",
       "bwa": "bwa mem",
       "bwameth": "bwameth.py",
-      "minimap": "minimap2"
+      "minimap": "minimap2",
+      "salmonalign": "salmon",
+      "rammap": "rammap",
+      "piscem": "piscem"
     },
     "ENV" : "",
     "BIN" : "",
@@ -286,20 +289,53 @@
     "minimap": {
       "comment": {
         "INDEX" : "Indexing options",
-        "MAP": "Mapping options",
+        "MAP": "Mapping options, for long reads set the appropriate preset, e.g. -x map-ont (Nanopore) or -x map-pb (PacBio)",
         "EXTENSION" : "Prefix for index name, can be left empty."
       },
       "OPTIONS": {
           "INDEX" : "",
-          "MAP": "",
+          "MAP": "-x map-ont",
           "EXTENSION" : ""
       }
     },
+    "salmonalign": {
+      "comment": {
+        "INDEX" : "salmon index options, uses the salmon environment/container",
+        "MAP": "salmon quant options; salmon is run as a selective-alignment mapper (--writeMappings) emitting SAM for downstream postprocessing"
+      },
+      "OPTIONS": {
+          "INDEX" : "",
+          "MAP": ""
+      }
+    },
+    "rammap": {
+      "comment": {
+        "INDEX" : "minimap2 index options (rammap wraps a minimap2 index)",
+        "MAP": "rammap mapping options for long-read alignment"
+      },
+      "OPTIONS": {
+          "INDEX" : "",
+          "MAP": ""
+      }
+    },
+    "piscem": {
+      "comment": {
+        "INDEX" : "piscem build options (single-cell only)",
+        "MAP": "piscem map-sc options, requires single-cell read setup (R1 = barcode+UMI, R2 = cDNA)"
+      },
+      "OPTIONS": {
+          "INDEX" : "",
+          "MAP": ""
+      }
+    },
   },
   "COUNTING": {
     "TOOLS": {
       "countreads": "featureCounts",
-      "salmon": "salmon"
+      "salmon": "salmon",
+      "oarfish": "oarfish",
+      "alevinfry": "alevin-fry",
+      "simpleaf": "simpleaf"
     },
     "ENV" : "",
     "BIN" : "",
@@ -318,12 +354,45 @@
     "salmon": {
       "comment": {
           "INDEX": "salmon index options",
-          "QUANT": "salmon quant options"
+          "QUANT": "salmon quant options",
+          "BAM": "Optional: quantify from an aligned BAM instead of FASTQ. Leave empty for standard FASTQ mode. Set to 'transcriptome' for transcriptome-aligned BAMs or 'genome' for genome-aligned BAMs (requires GTF annotation)."
+      },
+      "OPTIONS":
+        {
+            "INDEX": "",
+            "QUANT": "--seqBias --gcBias",
+            "BAM": ""
+        }
+    },
+    "oarfish": {
+      "comment": {
+          "COUNT": "oarfish options for long-read transcript quantification from a transcriptome-aligned BAM"
+      },
+      "OPTIONS":
+        {
+            "COUNT": ""
+        }
+    },
+    "alevinfry": {
+      "comment": {
+          "PERMIT": "alevin-fry generate-permit-list options (single-cell only, consumes piscem RAD output)",
+          "COUNT": "alevin-fry quant options"
+      },
+      "OPTIONS":
+        {
+            "PERMIT": "-d fw --unfiltered-pl",
+            "COUNT": "-r cr-like --use-mtx"
+        }
+    },
+    "simpleaf": {
+      "comment": {
+          "INDEX": "simpleaf index options (single-cell, self-contained index + quant from FASTQ)",
+          "COUNT": "simpleaf quant options"
       },
       "OPTIONS":
         {
             "INDEX": "",
-            "QUANT": "--validateMappings --seqBias --gcBias"
+            "COUNT": ""
         }
     }
   },
@@ -526,11 +595,12 @@
       "comment": {
           "INDEX": "salmon index options, e.g. path to decoy file",
           "QUANT": "salmon quant options",
-          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ..."
+          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ...",
+          "TERMINUS": "Optional: enable terminus transcript grouping prior to DTU. Leave blank/absent to disable (default). Set terminus group options here (e.g. -m 0.1) to enable."
       },
       "OPTIONS": {
           "INDEX": "-d GENOMES/salmon_decoy",
-          "QUANT": "--validateMappings --seqBias --gcBias",
+          "QUANT": "--seqBias --gcBias",
           "DTU": ""
       }
     },
@@ -538,11 +608,12 @@
       "comment": {
           "INDEX": "salmon index options, e.g. path to decoy file",
           "QUANT": "salmon quant options",
-          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, , can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ..."
+          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, , can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ...",
+          "TERMINUS": "Optional: enable terminus transcript grouping prior to DTU. Leave blank/absent to disable (default). Set terminus group options here (e.g. -m 0.1) to enable."
       },
       "OPTIONS": {
           "INDEX": "-d GENOMES/salmon_decoy",
-          "QUANT": "--validateMappings --seqBias --gcBias",
+          "QUANT": "--seqBias --gcBias",
           "DTU": ""
       }
     },
@@ -550,11 +621,12 @@
       "comment": {
           "INDEX": "salmon index options, e.g. path to decoy file",
           "QUANT": "salmon quant options",
-          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ..."
+          "DTU": "Options for DTU Analysis, leave blank unless you know exactly what you are doing, can be used to set cutoffs for filtering, e.g. min_samps_feature_expr, min_gene_expr ...",
+          "TERMINUS": "Optional: enable terminus transcript grouping prior to DTU. Leave blank/absent to disable (default). Set terminus group options here (e.g. -m 0.1) to enable."
       },
       "OPTIONS": {
           "INDEX": "-d GENOMES/salmon_decoy",
-          "QUANT": "--validateMappings --seqBias --gcBias",
+          "QUANT": "--seqBias --gcBias",
           "DTU": ""
       }
     }
diff --git a/configs/template_clean.json b/configs/template_clean.json
index 7d5d4267..94e94949 100644
--- a/configs/template_clean.json
+++ b/configs/template_clean.json
@@ -233,7 +233,7 @@
           "OPTIONS":
             {
                 "INDEX": "",
-                "QUANT": "--validateMappings --seqBias --gcBias"
+                "QUANT": "--seqBias --gcBias"
             }
         }
       }
@@ -438,7 +438,7 @@
             "OPTIONS":
             {
                 "INDEX": "",
-                "QUANT": "--validateMappings --seqBias --gcBias",
+                "QUANT": "--seqBias --gcBias",
                 "DTU": ""
             }
         },
@@ -447,7 +447,7 @@
             "OPTIONS":
             {
                 "INDEX": "",
-                "QUANT": "--validateMappings --seqBias --gcBias",
+                "QUANT": "--seqBias --gcBias",
                 "DTU": ""
             }
         },
@@ -456,7 +456,7 @@
             "OPTIONS":
             {
                 "INDEX": "",
-                "QUANT": "--validateMappings --seqBias --gcBias",
+                "QUANT": "--seqBias --gcBias",
                 "DTU": ""
             }
         }
diff --git a/configs/template_umicollapse.json b/configs/template_umicollapse.json
index cff24128..e264259b 100644
--- a/configs/template_umicollapse.json
+++ b/configs/template_umicollapse.json
@@ -109,7 +109,7 @@
         "salmon": {
           "OPTIONS": {
             "INDEX": "-k 31",
-            "COUNTING": "-l U --validateMappings"
+            "COUNTING": "-l U"
           }
         }
       }
diff --git a/docs/source/config.rst b/docs/source/config.rst
index 022fe09d..bb918afe 100644
--- a/docs/source/config.rst
+++ b/docs/source/config.rst
@@ -34,6 +34,15 @@ You can always define differing ENV/BIN keys for each condition-tree leaf separa
 
 The next key-level is the *OPTIONS* key which is where you can define additional parameters for each tool. It is not needed to define anything related to *single-/paired-* end or *singlecell* sequencing, this is done automatically.  To add parameters simply add the *OPTION* key which defines a dict where you can set parameters for each defined subworkflow-step. Parameters are here defined as key/value pairs corresponding to the subworkflow-step, e.g. 'INDEX' to generate an index file for mapping and all settings similar to a command line call as values. This should become clear having a look at the different processing steps in the template json.  If there are no options just leave the 'OPTIONS' dict empty.
 
+.. note::
+
+  A few tool-specific options are worth highlighting:
+
+  * **salmon COUNTING from BAM**: in addition to the default FASTQ quantification, salmon can quantify directly from an alignment BAM by setting the ``BAM`` option to ``transcriptome`` (alignments against a transcriptome FASTA) or ``genome`` (alignments against the genome, requires the annotation GTF). Leave it empty/unset for the default FASTQ mode. Salmon indices are automatically rebuilt when the environment changes (salmon >= 2.3, ``--validateMappings`` is no longer needed).
+  * **long-read quantification**: use ``minimap`` with ``-x map-ont``/``-x map-pb`` (or ``rammap``) for mapping and ``oarfish`` for transcript quantification from the resulting transcriptome BAM.
+  * **single-cell**: use ``piscem`` as MAPPING tool (emits a RAD file, single-cell only, generic post-mapping is skipped) together with ``alevinfry`` as COUNTING tool, or use the self-contained ``simpleaf`` COUNTING tool.
+  * **DTU pre-grouping**: any DTU tool (drimseq/dexseq/spit) accepts an optional ``TERMINUS`` option to pre-group transcripts with terminus before analysis. It is disabled by default.
+
 
 .. literalinclude:: ../../configs/template.json
     :language: json
diff --git a/docs/source/workflows.rst b/docs/source/workflows.rst
index d9b75814..61b75be0 100644
--- a/docs/source/workflows.rst
+++ b/docs/source/workflows.rst
@@ -159,8 +159,14 @@ Maps sequences to reference genomes or transcriptomes
   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
   | BWA-Meth          | BWA-meth, Fast and accurante alignment of BS-Seq reads.                             | bwameth               | bwameth.py     | `bwa-meth `_                            | FASTQ/TRIMMED_FASTQ  | SAM.gz/BAM  |
   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
-  | Minimap2     | Minimap2 is a versatile sequence alignment program that aligns DNA or mRNA sequences against a large reference database.   | minimap              | minimap2    | `minimap `_                        | FASTQ/TRIMMED_FASTQ  | SAM.gz/BAM  |
-  +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
+   | Minimap2     | Minimap2 is a versatile sequence alignment program that aligns DNA or mRNA sequences against a large reference database. Use ``-x map-ont``/``-x map-pb`` for long-read (Nanopore/PacBio) mapping.   | minimap              | minimap2    | `minimap `_                        | FASTQ/TRIMMED_FASTQ  | SAM.gz/BAM  |
+   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
+   | salmonalign  | Salmon selective-alignment used as a mapper (``salmon quant --writeMappings``). Reuses the salmon environment/container.   | salmonalign          | salmon      | `salmon `_     | FASTQ/TRIMMED_FASTQ  | SAM.gz/BAM  |
+   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
+   | rammap       | Rust-accelerated long-read (Nanopore/PacBio) mapper (cargo-built, not on bioconda).                                        | rammap               | rammap      | `rammap `_                      | FASTQ/TRIMMED_FASTQ  | SAM.gz/BAM  |
+   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
+   | piscem       | Fast single-cell/bulk mapper producing a RAD file for downstream alevin-fry quantification (single-cell only).             | piscem               | piscem      | `piscem `_                   | FASTQ/TRIMMED_FASTQ  | RAD         |
+   +--------------+----------------------------------------------------------------------------------------------------------------------------+----------------------+-------------+---------------------------------------------------------------------+----------------------+-------------+
 
 
 DEDUP
@@ -200,10 +206,16 @@ Count (unique) mapped reads and how often they map to defined features
   +================+=======================================================================================================================+=============+================+==================================================================+======================+=========+
   | FeatureCounts  | A software program developed for counting reads to genomic features such as genes, exons, promoters and genomic bins  | countreads  | featureCounts  | `featurecounts `_               | BAM/FASTQ            | TEXT    |
   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
-  | Salmon         | Salmon is a tool for wicked-fast transcript quantification from RNA-seq data.                                         | salmon      | salmon         | `salmon `_  | FASTQ/TRIMMED_FASTQ  | TEXT    |
-  +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
-  | Kallisto         | Kallisto is a program for quantifying abundances of transcripts from bulk and single-cell RNA-Seq data, or more generally of target sequences using high-throughput sequencing reads. | kallisto      | kallisto         | `kallisto `_  | FASTQ/TRIMMED_FASTQ  | TEXT    |
-  +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
+   | Salmon         | Salmon is a tool for wicked-fast transcript quantification from RNA-seq data. Also supports quantification directly from an alignment BAM via the ``BAM`` option (``transcriptome`` or ``genome``).  | salmon      | salmon         | `salmon `_  | FASTQ/TRIMMED_FASTQ/BAM  | TEXT    |
+   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
+   | Kallisto         | Kallisto is a program for quantifying abundances of transcripts from bulk and single-cell RNA-Seq data, or more generally of target sequences using high-throughput sequencing reads. | kallisto      | kallisto         | `kallisto `_  | FASTQ/TRIMMED_FASTQ  | TEXT    |
+   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
+   | oarfish        | Long-read (Nanopore/PacBio) transcript quantification from a transcriptome-aligned BAM.                               | oarfish     | oarfish        | `oarfish `_             | BAM                  | TEXT    |
+   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
+   | alevin-fry     | Single-cell quantification consuming a piscem RAD file to produce a count matrix.                                     | alevinfry   | alevin-fry     | `alevin-fry `_       | RAD                  | MATRIX  |
+   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
+   | simpleaf       | Self-contained single-cell quantification wrapper (index + map + quant) around salmon/alevin-fry.                     | simpleaf    | simpleaf       | `simpleaf `_           | FASTQ/TRIMMED_FASTQ  | MATRIX  |
+   +----------------+-----------------------------------------------------------------------------------------------------------------------+-------------+----------------+------------------------------------------------------------------+----------------------+---------+
 
 
 Differential Analyses
@@ -234,8 +246,12 @@ Includes DE, DEU, DAS and DTU
    | DRIMSeq   | Differential Transcript Usage       | dmFilter()       |                 | DM             |                                 | pValue, LFC    | results(transcript, genes)                           | Proportions-table, genewise precision   | Sig, SigUP, SigDOWN (transcipt, gene)   |                   | FeatPerGene, precision, Pvalues (per comparison)                  | ✓     |
    +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+
    | SPIT      | Differential Transcript Usage       | IF-dominance     |                 | KDE            | Mann-Whitney U                  | pValue, LFC    | results(transcript, genes, ifs)                      | IF-table                                | Sig, SigUP, SigDOWN (transcipt, gene)   |                   | PValues, Wasserstein (per comparison)                             | ✓     |
-   +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+
-   
+    +-----------+-------------------------------------+------------------+-----------------+----------------+---------------------------------+----------------+------------------------------------------------------+-----------------------------------------+-----------------------------------------+-------------------+-------------------------------------------------------------------+-------+
+
+.. note::
+
+  For DTU analyses, transcripts can optionally be pre-grouped with `terminus `_ to collapse transcripts that cannot be confidently distinguished. This is disabled by default and enabled per DTU tool via the ``TERMINUS`` option.
+
 TRACKS
 ###############
 
diff --git a/envs/alevinfry.yaml b/envs/alevinfry.yaml
new file mode 100644
index 00000000..6e61d630
--- /dev/null
+++ b/envs/alevinfry.yaml
@@ -0,0 +1,7 @@
+name: alevinfry
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - alevin-fry >=0.11
diff --git a/envs/minimap.yaml b/envs/minimap.yaml
index 76c64bc4..7b536391 100644
--- a/envs/minimap.yaml
+++ b/envs/minimap.yaml
@@ -4,6 +4,6 @@ channels:
   - bioconda
   - nodefaults
 dependencies:
-  - minimap2 =2.24
+  - minimap2 >=2.28
   - pigz =2.6
   - samtools =1.21
\ No newline at end of file
diff --git a/envs/oarfish.yaml b/envs/oarfish.yaml
new file mode 100644
index 00000000..8b791a5b
--- /dev/null
+++ b/envs/oarfish.yaml
@@ -0,0 +1,8 @@
+name: oarfish
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - oarfish >=0.10
+  - samtools =1.21
diff --git a/envs/piscem.yaml b/envs/piscem.yaml
new file mode 100644
index 00000000..9b457f9a
--- /dev/null
+++ b/envs/piscem.yaml
@@ -0,0 +1,8 @@
+name: piscem
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - piscem >=0.20
+  - samtools =1.21
diff --git a/envs/rammap.yaml b/envs/rammap.yaml
new file mode 100644
index 00000000..79268aea
--- /dev/null
+++ b/envs/rammap.yaml
@@ -0,0 +1,12 @@
+name: rammap
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - rust >=1.75
+  - cargo
+  - git
+  - zlib
+  - pigz =2.6
+  - samtools =1.21
diff --git a/envs/salmon.yaml b/envs/salmon.yaml
index cfff7361..a5e45adb 100644
--- a/envs/salmon.yaml
+++ b/envs/salmon.yaml
@@ -2,5 +2,8 @@ name: salmon
 channels:
   - conda-forge
   - bioconda
+  - nodefaults
 dependencies:
-  - salmon =1.11.4
\ No newline at end of file
+  - salmon >=2.3
+  - samtools =1.21
+  - pigz =2.6
\ No newline at end of file
diff --git a/envs/simpleaf.yaml b/envs/simpleaf.yaml
new file mode 100644
index 00000000..af9d2e88
--- /dev/null
+++ b/envs/simpleaf.yaml
@@ -0,0 +1,10 @@
+name: simpleaf
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - simpleaf >=0.25
+  - piscem >=0.20
+  - alevin-fry >=0.11
+  - pyroe
diff --git a/envs/terminus.yaml b/envs/terminus.yaml
new file mode 100644
index 00000000..2398124b
--- /dev/null
+++ b/envs/terminus.yaml
@@ -0,0 +1,7 @@
+name: terminus
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - terminus
diff --git a/tests/data/config_Test.json b/tests/data/config_Test.json
index e7eededd..98eb5124 100644
--- a/tests/data/config_Test.json
+++ b/tests/data/config_Test.json
@@ -255,7 +255,7 @@
             "drimseq": {
                 "OPTIONS": {
                     "INDEX": "-d GENOMES/transcripts.fa.gz",
-                    "QUANT": "--validateMappings --seqBias --gcBias",
+                    "QUANT": "--seqBias --gcBias",
                     "DTU": ""
                 }
             }
diff --git a/tests/data/config_Test_local.json b/tests/data/config_Test_local.json
index 19bc01a5..5b6a4709 100644
--- a/tests/data/config_Test_local.json
+++ b/tests/data/config_Test_local.json
@@ -255,7 +255,7 @@
             "drimseq": {
                 "OPTIONS": {
                     "INDEX": "-d GENOMES/transcripts.fa.gz",
-                    "QUANT": "--validateMappings --seqBias --gcBias",
+                    "QUANT": "--seqBias --gcBias",
                     "DTU": ""
                 }
             }
diff --git a/workflows/alevinfry.nf b/workflows/alevinfry.nf
new file mode 100644
index 00000000..9ce314b0
--- /dev/null
+++ b/workflows/alevinfry.nf
@@ -0,0 +1,74 @@
+COUNTENV = get_always('COUNTINGENV')
+COUNTBIN = get_always('COUNTINGBIN')
+COUNTREF = get_always('COUNTINGREF')
+COUNTREFDIR = "${workflow.workDir}/../"+get_always('COUNTINGREFDIR')
+COUNTANNO = get_always('COUNTINGANNO')
+COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
+
+COUNTPARAMS = get_always('alevinfry_params_COUNT') ?: ''
+PERMITPARAMS = get_always('alevinfry_params_PERMIT') ?: '--unfiltered-pl'
+
+//COUNTING PROCESSES
+
+process generate_t2g{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus 1
+	cache 'lenient'
+
+    input:
+    path anno
+
+    output:
+    path "t2g.tsv", emit: t2g
+
+    script:
+    """
+    zcat $anno | awk 'BEGIN{FS="\\t";OFS="\\t"} \$3=="transcript"{match(\$9,/transcript_id "[^"]+"/); t=substr(\$9,RSTART+15,RLENGTH-16); match(\$9,/gene_id "[^"]+"/); g=substr(\$9,RSTART+9,RLENGTH-10); print t,g}' > t2g.tsv
+    """
+}
+
+process alevinfry_quant{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/alevinfry/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/alevinfry/${CONDITION}/${file(filename).getName()}"
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*_afquant", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    t2g = reads[0]
+    raddir = reads[1]
+    fn = file(raddir).getSimpleName().replaceAll(/\Q_map\E/,"")
+    qd = fn+"_afquant"
+    lf = "alevinfry_"+fn+".log"
+    """
+    $COUNTBIN generate-permit-list -i $raddir -d fw $PERMITPARAMS -o $qd &> $lf; $COUNTBIN collate -i $qd -r $raddir -t ${task.cpus} &>> $lf; $COUNTBIN quant -i $qd -m $t2g -t ${task.cpus} -r cr-like $COUNTPARAMS -o $qd --use-mtx &>> $lf
+    """
+}
+
+workflow COUNTING{
+    take: collection
+
+    main:
+
+    radsamples = collection.filter(~/map.rad/)
+    annofile = Channel.fromPath(COUNTANNO)
+    generate_t2g(annofile)
+    alevinfry_quant(generate_t2g.out.t2g.combine(radsamples.map{it -> file(it).getParent()}))
+
+    emit:
+    counts = alevinfry_quant.out.counts
+    logs = alevinfry_quant.out.logs
+}
diff --git a/workflows/alevinfry.smk b/workflows/alevinfry.smk
new file mode 100644
index 00000000..ee91fcd7
--- /dev/null
+++ b/workflows/alevinfry.smk
@@ -0,0 +1,34 @@
+COUNTBIN, COUNTENV = env_bin_from_config(config,'COUNTING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'COUNTING', COUNTENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = COUNTENV
+unik = get_dict_hash(keydict)
+
+rule themall:
+    input:  expand("COUNTS/{combo}/{file}/alevin/quants_mat.mtx.gz", combo=combo, file=samplecond(SAMPLES, config))
+
+rule generate_t2g:
+    input:  anno = ANNOTATION
+    output: t2g = expand("{refd}/INDICES/{cenv}_{unikey}/t2g.tsv", refd=REFDIR, cenv=COUNTENV, unikey=unik)
+    log:    expand("LOGS/{sets}/{cenv}.t2g.log", sets=SETS, cenv=COUNTENV)
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: 1
+    shell:  "mkdir -p $(dirname {output.t2g}); zcat {input.anno} | awk 'BEGIN{{FS=\"\\t\";OFS=\"\\t\"}} $3==\"transcript\"{{match($9,/transcript_id \"[^\"]+\"/); t=substr($9,RSTART+15,RLENGTH-16); match($9,/gene_id \"[^\"]+\"/); g=substr($9,RSTART+9,RLENGTH-10); print t,g}}' > {output.t2g} 2> {log}"
+
+rule counting:
+    input:  rad = "MAPPED/{combo}/{file}",
+            radfile = "MAPPED/{combo}/{file}/map.rad",
+            t2g = rules.generate_t2g.output.t2g
+    output: cnts = report("COUNTS/{combo}/{file}/alevin/quants_mat.mtx.gz", category="COUNTING"),
+            ctsdir = report(directory("COUNTS/{combo}/{file}"), category="COUNTING")
+    log:    "LOGS/{combo}/{file}/alevinfryquant.log"
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('COUNT', ""),
+            ppara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('PERMIT', "--unfiltered-pl"),
+            mapp = COUNTBIN,
+            quantdir = lambda wildcards, output: os.path.join(str(output.ctsdir), "af_quant")
+    shell: "{params.mapp} generate-permit-list -i {input.rad} -d fw {params.ppara} -o {params.quantdir} &> {log}; {params.mapp} collate -i {params.quantdir} -r {input.rad} -t {threads} &>> {log}; {params.mapp} quant -i {params.quantdir} -m {input.t2g} -t {threads} -r cr-like {params.cpara} -o {params.quantdir} --use-mtx &>> {log} && mkdir -p $(dirname {output.cnts}) && gzip -c {params.quantdir}/alevin/quants_mat.mtx > {output.cnts} 2>> {log}"
diff --git a/workflows/dexseq_DTU.nf b/workflows/dexseq_DTU.nf
index edbd2a82..aa36fc82 100644
--- a/workflows/dexseq_DTU.nf
+++ b/workflows/dexseq_DTU.nf
@@ -11,6 +11,7 @@ DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
 IDXPARAMS = get_always('dexseq_DTU_params_INDEX') ?: ''
 COUNTPARAMS = get_always('dexseq_DTU_params_COUNT') ?: ''
 DTUPARAMS = get_always('dexseq_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('dexseq_DTU_params_TERMINUS') ?: ''
 DTUREPS = get_always('DTUREPS') ?: ''
 DTUCOMP = get_always('DTUCOMP') ?: ''
 DTUCOMPS = get_always('DTUCOMPS') ?: ''
@@ -228,6 +229,32 @@ process collect_dexseq{
     """
 }
 
+process terminus_collapse{
+    conda "terminus.yaml"
+    container "oras://jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+
+    input:
+    path counts
+
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+
 workflow DTU{ 
     take: collection
 
@@ -260,7 +287,14 @@ workflow DTU{
     }
 
     prepare_dtu_annotation()
-    run_dexseq(salmon_quant.out.counts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_dexseq(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
     create_summary_snippet(run_dexseq.out.tbls.concat(run_dexseq.out.figs.concat(run_dexseq.out.session)).collect())
     collect_dexseq(run_dexseq.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
 
diff --git a/workflows/dexseq_DTU.smk b/workflows/dexseq_DTU.smk
index 5eb7b6c6..79187ab8 100644
--- a/workflows/dexseq_DTU.smk
+++ b/workflows/dexseq_DTU.smk
@@ -7,6 +7,10 @@ comparison = comparable_as_string(config,'DTU')
 compstr = [i.split(":")[0] for i in comparison.split(",")]
 usededup = config.get('RUNDEDUP', False)
 
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+
 keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
 keydict["REF"] = REFERENCE
 keydict["DECOY"] = DECOY
@@ -92,8 +96,35 @@ else:
         shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
 
 
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+
 rule create_annotation_table:
-    input:  dir  = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config)),
+    input:  dir  = ctsource,
     output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
     log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
     conda:  ""+COUNTENV+".yaml"
diff --git a/workflows/drimseq_DTU.nf b/workflows/drimseq_DTU.nf
index b5d814bb..dbc28e22 100644
--- a/workflows/drimseq_DTU.nf
+++ b/workflows/drimseq_DTU.nf
@@ -11,6 +11,7 @@ DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
 IDXPARAMS = get_always('drimseq_DTU_params_INDEX') ?: ''
 COUNTPARAMS = get_always('drimseq_DTU_params_COUNT') ?: ''
 DTUPARAMS = get_always('drimseq_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('drimseq_DTU_params_TERMINUS') ?: ''
 DTUREPS = get_always('DTUREPS') ?: ''
 DTUCOMP = get_always('DTUCOMP') ?: ''
 DTUCOMPS = get_always('DTUCOMPS') ?: ''
@@ -229,6 +230,32 @@ process collect_drimseq{
     """
 }
 
+process terminus_collapse{
+    conda "terminus.yaml"
+    container "oras://jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+
+    input:
+    path counts
+
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+
 workflow DTU{ 
     take: collection
 
@@ -261,7 +288,14 @@ workflow DTU{
     }
 
     prepare_dtu_annotation()
-    run_drimseq(salmon_quant.out.counts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_drimseq(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
     create_summary_snippet(run_drimseq.out.tbls.concat(run_drimseq.out.figs.concat(run_drimseq.out.session)).collect())
     collect_drimseq(run_drimseq.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
 
diff --git a/workflows/drimseq_DTU.smk b/workflows/drimseq_DTU.smk
index 597ccb53..db41825a 100644
--- a/workflows/drimseq_DTU.smk
+++ b/workflows/drimseq_DTU.smk
@@ -7,6 +7,10 @@ comparison = comparable_as_string(config,'DTU')
 compstr = [i.split(":")[0] for i in comparison.split(",")]
 usededup = config.get('RUNDEDUP', False)
 
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+
 keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
 keydict["REF"] = REFERENCE
 keydict["DECOY"] = DECOY
@@ -100,8 +104,35 @@ else:
         shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
 
 
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+
 rule create_annotation_table:
-    input:  dir  = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config)),
+    input:  dir  = ctsource,
     output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
     log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
     conda:  ""+COUNTENV+".yaml"
diff --git a/workflows/oarfish.nf b/workflows/oarfish.nf
new file mode 100644
index 00000000..d40851dc
--- /dev/null
+++ b/workflows/oarfish.nf
@@ -0,0 +1,53 @@
+COUNTENV = get_always('COUNTINGENV')
+COUNTBIN = get_always('COUNTINGBIN')
+COUNTREF = get_always('COUNTINGREF')
+COUNTREFDIR = "${workflow.workDir}/../"+get_always('COUNTINGREFDIR')
+COUNTANNO = get_always('COUNTINGANNO')
+COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
+
+COUNTPARAMS = get_always('oarfish_params_COUNT') ?: ''
+
+//COUNTING PROCESSES
+
+process oarfish_quant{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/oarfish/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/oarfish/${CONDITION}/${file(filename).getName()}"
+    }
+
+    input:
+    path bam
+
+    output:
+    path "*.gz", includeInputs:false, emit: counts
+    path "*.log", emit: logs
+    path "*", includeInputs:false, emit: rest
+
+    script:
+    fn = file(bam).getSimpleName().replaceAll(/\Q_mapped_sorted\E/,"")
+    lf = "oarfish_"+fn+".log"
+    oz = fn+"/quant.sf.gz"
+    ol = fn+"_counts.gz"
+    """
+    mkdir -p $fn; samtools collate -@ ${task.cpus} -o namecollated.bam $bam &>> $lf; $COUNTBIN --threads ${task.cpus} --genome-alignments namecollated.bam --annotation $COUNTANNO --reference $COUNTREF $COUNTPARAMS --output $fn/oarfish &>> $lf && rm -f namecollated.bam && gzip -c $fn/oarfish.quant > $oz && ln -fs $oz $ol
+    """
+}
+
+workflow COUNTING{
+    take: collection
+
+    main:
+
+    bamsamples = collection.filter(~/.bam/)
+    oarfish_quant(bamsamples)
+
+    emit:
+    counts = oarfish_quant.out.counts
+    logs = oarfish_quant.out.logs
+}
diff --git a/workflows/oarfish.smk b/workflows/oarfish.smk
new file mode 100644
index 00000000..dc40b078
--- /dev/null
+++ b/workflows/oarfish.smk
@@ -0,0 +1,26 @@
+COUNTBIN, COUNTENV = env_bin_from_config(config,'COUNTING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'COUNTING', COUNTENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = COUNTENV
+unik = get_dict_hash(keydict)
+
+rule themall:
+    input:  expand("COUNTS/{combo}/{file}_counts.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+
+rule mapping:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_sorted.bam",
+            anno = ANNOTATION,
+            fa = REFERENCE
+    output: cnts = report("COUNTS/{combo}/{file}_counts.sf.gz", category="COUNTING"),
+            ctsdir = report(directory("COUNTS/{combo}/{file}"), category="COUNTING")
+    log:    "LOGS/{combo}/{file}/oarfishquant.log"
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('COUNT', ""),
+            mapp = COUNTBIN,
+            nbam = lambda wildcards, output: os.path.join(str(output.ctsdir), "namecollated.bam"),
+            prefix = lambda wildcards, output: os.path.join(str(output.ctsdir), "oarfish"),
+            linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+    shell: "set +euo pipefail; mkdir -p {output.ctsdir}; samtools collate -@ {threads} -o {params.nbam} {input.bam} &>> {log}; {params.mapp} --threads {threads} --genome-alignments {params.nbam} --annotation {input.anno} --reference {input.fa} {params.cpara} --output {params.prefix} &>> {log} && rm -f {params.nbam} && gzip -c {params.prefix}.quant > {output.ctsdir}/quant.sf.gz ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
diff --git a/workflows/piscem.nf b/workflows/piscem.nf
new file mode 100644
index 00000000..830f68ba
--- /dev/null
+++ b/workflows/piscem.nf
@@ -0,0 +1,113 @@
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX') ?: 'piscem_idx'
+
+IDXPARAMS = get_always('piscem_params_INDEX') ?: ''
+MAPPARAMS = get_always('piscem_params_MAP') ?: ''
+
+
+//MAPPING PROCESSES
+
+process collect_tomap{
+    input:
+    path check
+
+    output:
+    path "collect.txt", emit: done
+
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+
+process piscem_idx{
+    conda "$MAPENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "piscem.idx")                  "$MAPIDX"
+        else if (filename.indexOf("index.log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/piscem_index.log"
+        else                                            "$MAPUIDXNAME"
+    }
+
+    input:
+    path genome
+
+    output:
+    path "$MAPUIDXNAME", emit: idx
+    path "*index.log", emit: idxlog
+
+    script:
+    gen =  genome.getName()
+    """
+    mkdir -p $MAPUIDXNAME && zcat $gen > ref.fa && $MAPBIN build -t ${task.cpus} $IDXPARAMS -s ref.fa -o $MAPUIDXNAME/$MAPPREFIX &> index.log && rm -f ref.fa && ln -fs $MAPUIDXNAME piscem.idx
+    """
+
+}
+
+process piscem_mapping{
+    conda "$MAPENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else                                       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*_map", emit: maps
+    path "*.log", emit: logs
+
+    script:
+    idxdir = reads[0]
+    r1 = reads[1]
+    r2 = reads[2]
+    fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+    of = fn+"_map"
+    lf = "piscem_"+fn+".log"
+    """
+    $MAPBIN map-sc -t ${task.cpus} $MAPPARAMS -i $idxdir/$MAPPREFIX -1 $r1 -2 $r2 -o $of &> $lf
+    """
+}
+
+workflow MAPPING{
+    take: collection
+
+    main:
+
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        piscem_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        piscem_idx(genomefile)
+        piscem_mapping(piscem_idx.out.idx.combine(collection))
+    }
+
+
+    emit:
+    mapped  = piscem_mapping.out.maps
+    logs = piscem_mapping.out.logs
+}
diff --git a/workflows/piscem.smk b/workflows/piscem.smk
new file mode 100644
index 00000000..ba001850
--- /dev/null
+++ b/workflows/piscem.smk
@@ -0,0 +1,39 @@
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=MAPPERENV, unikey=unik)),
+            idxfile = expand("{refd}/INDICES/{mape}_{unikey}/{pref}.sshash", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX),
+            tmpfa = temp(expand("TMP/{mape}/ref.fa", mape=MAPPERENV))
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda:  ""+MAPPERENV+".yaml"
+    container: "oras://jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: indexer = MAPPERBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            pref = PREFIX,
+            lnkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "if [[ -f \"{output.idxfile}\" ]]; then touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && echo \"Found index, continue with mapping\" ; else zcat {input.fa} > {output.tmpfa} && mkdir -p {output.uidx} && {params.indexer} build -t {threads} {params.ipara} -s {output.tmpfa} -o {output.uidx}/{params.pref} &> {log} && touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx};fi"
+
+rule mapping:
+    input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+            r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+            uidx = rules.generate_index.output.uidx[0],
+            dummy = rules.generate_index.output.idxfile
+    output: rad = directory("MAPPED/{combo}/{file}"),
+            radfile = "MAPPED/{combo}/{file}/map.rad"
+    log:    "LOGS/{combo}/{file}/mapping.log"
+    conda:  ""+MAPPERENV+".yaml"
+    container: "oras://jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+            mapp = MAPPERBIN,
+            pref = PREFIX,
+            idxpref = lambda wildcards, input: os.path.join(str(input.uidx), PREFIX)
+    shell: "{params.mapp} map-sc -t {threads} {params.mpara} -i {params.idxpref} -1 {input.r1} -2 {input.r2} -o {output.rad} &> {log}"
diff --git a/workflows/rammap.nf b/workflows/rammap.nf
new file mode 100644
index 00000000..c0b8d0ba
--- /dev/null
+++ b/workflows/rammap.nf
@@ -0,0 +1,118 @@
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+
+IDXPARAMS = get_always('rammap_params_INDEX') ?: ''
+MAPPARAMS = get_always('rammap_params_MAP') ?: ''
+
+RAMMAPBUILD = 'command -v rammap >/dev/null 2>&1 || cargo install --git https://github.com/jwanglab/rammap --root ${CONDA_PREFIX:-$PWD/.cargo} ; export PATH=${CONDA_PREFIX:-$PWD/.cargo}/bin:$PATH; '
+
+//MAPPING PROCESSES
+
+process rammap_idx{
+    conda "$MAPENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "rammap.idx")                  "$MAPIDX"
+        else if (filename.indexOf("index.log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/rammap_index.log"
+        else                                            "$MAPUIDX"
+    }
+
+    input:
+    path genome
+
+    output:
+    path "*.idx", emit: idx
+    path "$MAPUIDXNAME", emit: uidx
+
+    script:
+    gen =  genome.getName()
+    """
+    ${RAMMAPBUILD} $MAPBIN -t ${task.cpus} -d $MAPUIDXNAME $IDXPARAMS $gen &> index.log && ln -fs $MAPUIDXNAME rammap.idx
+    """
+
+}
+
+process rammap_mapping{
+    conda "$MAPENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else null
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*.sam.gz", emit: maps
+    path "*fastq.gz", includeInputs:false, emit: unmapped
+    path "*.log", emit: logs
+
+    script:    
+    idxfile = reads[0]
+    idx = idxfile.getName()
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf1 = fn+"_R1_unmapped.fastq.gz"
+        uf2 = fn+"_R2_unmapped.fastq.gz"
+        lf = "rammap_"+fn+".log"
+        """
+        ${RAMMAPBUILD} $MAPBIN $MAPPARAMS -t ${task.cpus} $idx $r1 $r2 2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> $lf &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        """
+    }else{
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf = fn+"_unmapped.fastq.gz"
+        lf = "rammap_"+fn+".log"
+        """
+        ${RAMMAPBUILD} $MAPBIN $MAPPARAMS -t ${task.cpus} $idx $read 2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        """
+    }
+}
+
+workflow MAPPING{
+    take: collection
+
+    main:
+ 
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+    
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)       
+        rammap_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        rammap_idx(genomefile)        
+        rammap_mapping(rammap_idx.out.idx.combine(collection))
+    }
+
+
+    emit:
+    mapped  = rammap_mapping.out.maps
+    logs = rammap_mapping.out.logs
+}
diff --git a/workflows/rammap.smk b/workflows/rammap.smk
new file mode 100644
index 00000000..67ee74c1
--- /dev/null
+++ b/workflows/rammap.smk
@@ -0,0 +1,51 @@
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+
+rammapbuild = 'command -v rammap >/dev/null 2>&1 || cargo install --git https://github.com/jwanglab/rammap --root ${{CONDA_PREFIX:-$PWD/.cargo}} >> {log} 2>&1; export PATH=${{CONDA_PREFIX:-$PWD/.cargo}}/bin:$PATH; '
+
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = INDEX,
+            uidx = expand("{refd}/INDICES/{mape}_{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda:  ""+MAPPERENV+".yaml"
+    container: "oras://jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: indexer=MAPPERBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell: rammapbuild + "{params.indexer} -t {threads} -d {output.uidx} {params.ipara} {input.fa} 2> {log} && ln -s {params.linkidx} {output.idx}"
+
+if paired == 'paired':
+    rule mapping:
+        input:  q1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                q2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0]
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda:  ""+MAPPERENV+".yaml"
+        container: "oras://jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN
+        shell: rammapbuild + "{params.mapp} -t {threads} {params.mpara} {input.uidx} {input.q1} {input.q2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} - ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+
+else:
+    rule mapping:
+        input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0]
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda:  ""+MAPPERENV+".yaml"
+        container: "oras://jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN
+        shell: rammapbuild + "{params.mapp} -t {threads} {params.mpara} {input.uidx} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
diff --git a/workflows/salmon.nf b/workflows/salmon.nf
index 13c0092c..a63f1576 100644
--- a/workflows/salmon.nf
+++ b/workflows/salmon.nf
@@ -12,6 +12,7 @@ COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
 
 IDXPARAMS = get_always('salmon_params_INDEX') ?: ''
 COUNTPARAMS = get_always('salmon_params_COUNT') ?: ''
+BAMMODE = get_always('salmon_params_BAM') ?: ''
 
 //COUNTING PROCESSES
 
@@ -152,33 +153,80 @@ process salmon_quant{
     }
 }
 
+process salmon_quant_bam{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/salmon/${CONDITION}/${file(filename).getName()}"
+    }
+
+    input:
+    path bam
+
+    output:
+    path "*.gz", includeInputs:false, emit: counts
+    path "*.log", emit: logs
+    path "*", includeInputs:false, emit: rest
+
+    script:
+    fn = file(bam).getSimpleName().replaceAll(/\Q_mapped_sorted\E/,"")
+    lf = "salmon_"+fn+".log"
+    of = fn+"/quant.sf"
+    oz = fn+"/quant.sf.gz"
+    ol = fn+"_counts.gz"
+    if (BAMMODE == 'genome'){
+        """
+        mkdir -p $fn; samtools collate -@ ${task.cpus} -o namecollated.bam $bam &>> $lf; $COUNTBIN quant -p ${task.cpus} -a namecollated.bam --annotation $COUNTANNO --genome $COUNTREF $COUNTPARAMS -o $fn &>> $lf && rm -f namecollated.bam && gzip $of && ln -fs $oz $ol
+        """
+    }else{
+        """
+        mkdir -p $fn; samtools collate -@ ${task.cpus} -o namecollated.bam $bam &>> $lf; $COUNTBIN quant -p ${task.cpus} -t $COUNTREF -a namecollated.bam --deterministic $COUNTPARAMS -o $fn &>> $lf && rm -f namecollated.bam && gzip $of && ln -fs $oz $ol
+        """
+    }
+}
+
 workflow COUNTING{
     take: collection
 
     main:
    
-    checkidx = file(COUNTUIDX)
-    collection.filter(~/.fastq.gz/)
-    
-    if (checkidx.exists()){
-        idxfile = Channel.fromPath(COUNTUIDX)
-        if (PAIRED == 'paired'){
-            salmon_quant(idxfile.combine(samples_ch.collate(2)))
-        } else{
-            salmon_quant(idxfile.combine(samples_ch.collate(1)))
-        }        
+    if (BAMMODE?.length() > 0){
+        bamsamples = collection.filter(~/.bam/)
+        salmon_quant_bam(bamsamples)
+        outcounts = salmon_quant_bam.out.counts
+        outlogs = salmon_quant_bam.out.logs
     }
     else{
-        genomefile = Channel.fromPath(COUNTREF)
-        salmon_idx(genomefile)
-        if (PAIRED == 'paired'){
-            salmon_quant(salmon_idx.out.idx.combine(samples_ch.collate(2)))
-        } else{
-            salmon_quant(salmon_idx.out.idx.combine(samples_ch.collate(1)))
+        checkidx = file(COUNTUIDX)
+        collection.filter(~/.fastq.gz/)
+
+        if (checkidx.exists()){
+            idxfile = Channel.fromPath(COUNTUIDX)
+            if (PAIRED == 'paired'){
+                salmon_quant(idxfile.combine(samples_ch.collate(2)))
+            } else{
+                salmon_quant(idxfile.combine(samples_ch.collate(1)))
+            }        
+        }
+        else{
+            genomefile = Channel.fromPath(COUNTREF)
+            salmon_idx(genomefile)
+            if (PAIRED == 'paired'){
+                salmon_quant(salmon_idx.out.idx.combine(samples_ch.collate(2)))
+            } else{
+                salmon_quant(salmon_idx.out.idx.combine(samples_ch.collate(1)))
+            }
         }
+        outcounts = salmon_quant.out.counts
+        outlogs = salmon_quant.out.logs
     }
 
     emit:
-    counts = salmon_quant.out.counts
-    logs = salmon_quant.out.logs
+    counts = outcounts
+    logs = outlogs
 }
diff --git a/workflows/salmon.smk b/workflows/salmon.smk
index 621cc99a..afd8ba11 100644
--- a/workflows/salmon.smk
+++ b/workflows/salmon.smk
@@ -5,6 +5,8 @@ keydict["DECOY"] = DECOY
 keydict["ENV"] = COUNTENV
 unik = get_dict_hash(keydict)
 
+bammode = tool_params(SAMPLES[0], None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('BAM', "").lower()
+
 rule themall:
     input:  expand("COUNTS/{combo}/{file}_counts.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
 
@@ -23,9 +25,40 @@ rule salmon_index:
     shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
 
 
-if paired == 'paired':
+if bammode == 'transcriptome':
+    rule mapping:
+        input:  bam = "MAPPED/{combo}/{file}_mapped_sorted.bam",
+                fa = REFERENCE
+        output: cnts = report("COUNTS/{combo}/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("COUNTS/{combo}/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  ""+COUNTENV+".yaml"
+        container: "oras://jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('COUNT', ""),
+                mapp=COUNTBIN,
+                nbam = lambda wildcards, output: os.path.join(str(output.ctsdir), "namecollated.bam"),
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; mkdir -p {output.ctsdir}; samtools collate -@ {threads} -o {params.nbam} {input.bam} &>> {log}; {params.mapp} quant -p {threads} -t {input.fa} -a {params.nbam} --deterministic {params.cpara} -o {output.ctsdir} &>> {log} && rm -f {params.nbam} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+
+elif bammode == 'genome':
     rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
+        input:  bam = "MAPPED/{combo}/{file}_mapped_sorted.bam",
+                anno = ANNOTATION,
+                fa = REFERENCE
+        output: cnts = report("COUNTS/{combo}/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("COUNTS/{combo}/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  ""+COUNTENV+".yaml"
+        container: "oras://jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('COUNT', ""),
+                mapp=COUNTBIN,
+                nbam = lambda wildcards, output: os.path.join(str(output.ctsdir), "namecollated.bam"),
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; mkdir -p {output.ctsdir}; samtools collate -@ {threads} -o {params.nbam} {input.bam} &>> {log}; {params.mapp} quant -p {threads} -a {params.nbam} --annotation {input.anno} --genome {input.fa} {params.cpara} -o {output.ctsdir} &>> {log} && rm -f {params.nbam} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+
+elif paired == 'paired':
                 r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
                 uidx = rules.salmon_index.output.uidx[0]
         output: cnts = report("COUNTS/{combo}/{file}_counts.sf.gz", category="COUNTING"),
diff --git a/workflows/salmonalign.nf b/workflows/salmonalign.nf
new file mode 100644
index 00000000..7ec05eb3
--- /dev/null
+++ b/workflows/salmonalign.nf
@@ -0,0 +1,132 @@
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+MAPYAML = MAPBIN
+
+IDXPARAMS = get_always('salmonalign_params_INDEX') ?: ''
+MAPPARAMS = get_always('salmonalign_params_MAP') ?: ''
+
+
+//MAPPING PROCESSES
+
+process salmonalign_idx{
+    conda "$MAPYAML"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPYAML"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "salmonalign.idx")              "$MAPIDX"
+        else if (filename.indexOf("index.log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/salmonalign_index.log"
+        else                                            "$MAPUIDX"
+    }
+
+    input:
+    path genome
+
+    output:
+    path "*.idx", emit: idx
+    path "$MAPUIDXNAME", emit: uidx
+
+    script:
+    gen =  genome.getName()
+    """
+    $MAPBIN index $IDXPARAMS -p ${task.cpus} -t $gen -i $MAPUIDXNAME &> index.log && ln -fs $MAPUIDXNAME salmonalign.idx
+    """
+
+}
+
+process salmonalign_mapping{
+    conda "$MAPYAML"+".yaml"
+    container "oras://jfallmann/monsda:"+"$MAPYAML"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else null
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*.sam.gz", emit: maps
+    path "*fastq.gz", includeInputs:false, emit: unmapped
+    path "*.log", emit: logs
+
+    script:    
+    idxfile = reads[0]
+    idx = idxfile.getName()
+    if (PAIRED == 'paired'){
+        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+            stranded = '-l ISF'
+        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+            stranded = '-l ISR'
+        }else{
+            stranded = '-l IU'
+        }
+        r1 = reads[1]
+        r2 = reads[2]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf1 = fn+"_R1_unmapped.fastq.gz"
+        uf2 = fn+"_R2_unmapped.fastq.gz"
+        lf = "salmonalign_"+fn+".log"
+        """
+        $MAPBIN quant -p ${task.cpus} -i $idx $stranded $MAPPARAMS --writeMappings -o $fn -1 $r1 -2 $r2 2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> $lf &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        """
+    }else{
+        if (STRANDED == 'fr' || STRANDED == 'SF'){
+            stranded = '-l SF'
+        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+            stranded = '-l SR'
+        }else{
+            stranded = '-l U'
+        }
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf = fn+"_unmapped.fastq.gz"
+        lf = "salmonalign_"+fn+".log"
+        """
+        $MAPBIN quant -p ${task.cpus} -i $idx $stranded $MAPPARAMS --writeMappings -o $fn -r $read 2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        """
+    }
+}
+
+workflow MAPPING{
+    take: collection
+
+    main:
+ 
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+    
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)       
+        salmonalign_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        salmonalign_idx(genomefile)        
+        salmonalign_mapping(salmonalign_idx.out.idx.combine(collection))
+    }
+
+
+    emit:
+    mapped  = salmonalign_mapping.out.maps
+    logs = salmonalign_mapping.out.logs
+}
diff --git a/workflows/salmonalign.smk b/workflows/salmonalign.smk
new file mode 100644
index 00000000..b298f74a
--- /dev/null
+++ b/workflows/salmonalign.smk
@@ -0,0 +1,55 @@
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+MAPPERYAML = MAPPERBIN
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=MAPPERENV, unikey=unik))
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda:  ""+MAPPERYAML+".yaml"
+    container: "oras://jfallmann/monsda:"+MAPPERYAML+""
+    threads: MAXTHREAD
+    params: indexer=MAPPERBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell: "set +euo pipefail; {params.indexer} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
+
+if paired == 'paired':
+    rule mapping:
+        input:  q1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                q2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0]
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda:  ""+MAPPERYAML+".yaml"
+        container: "oras://jfallmann/monsda:"+MAPPERYAML+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
+                outdir = lambda wildcards: "MAPPED/"+wildcards.combo+"/"+wildcards.file+"_salmonalign"
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.uidx} {params.stranded} {params.mpara} --writeMappings -o {params.outdir} -1 {input.q1} -2 {input.q2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} - ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+
+else:
+    rule mapping:
+        input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0]
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda:  ""+MAPPERYAML+".yaml"
+        container: "oras://jfallmann/monsda:"+MAPPERYAML+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
+                outdir = lambda wildcards: "MAPPED/"+wildcards.combo+"/"+wildcards.file+"_salmonalign"
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.uidx} {params.stranded} {params.mpara} --writeMappings -o {params.outdir} -r {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
diff --git a/workflows/simpleaf.nf b/workflows/simpleaf.nf
new file mode 100644
index 00000000..bd6ed619
--- /dev/null
+++ b/workflows/simpleaf.nf
@@ -0,0 +1,101 @@
+COUNTENV = get_always('COUNTINGENV')
+COUNTBIN = get_always('COUNTINGBIN')
+COUNTIDX = get_always('COUNTINGIDX')
+COUNTUIDX = get_always('COUNTINGUIDX')
+COUNTUIDX = COUNTUIDX.replaceAll('.idx','')
+COUNTUIDXNAME = get_always('COUNTINGUIDXNAME')
+COUNTREF = get_always('COUNTINGREF')
+COUNTREFDIR = "${workflow.workDir}/../"+get_always('COUNTINGREFDIR')
+COUNTANNO = get_always('COUNTINGANNO')
+COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
+
+IDXPARAMS = get_always('simpleaf_params_INDEX') ?: '--ref-type spliced+unspliced'
+COUNTPARAMS = get_always('simpleaf_params_COUNT') ?: '--chemistry 10xv3 --resolution cr-like --expected-ori fw --unfiltered-pl'
+
+//COUNTING PROCESSES
+
+process simpleaf_idx{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename == "simpleaf.idx")            "$COUNTIDX"
+        else if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/COUNTING/simpleaf_index.log"
+        else                                      "$COUNTUIDX"
+    }
+
+    input:
+    path genome
+    path anno
+
+    output:
+    path "$COUNTUIDXNAME", emit: idx
+    path "*.log", emit: idxlog
+
+    script:
+    gen =  genome.getName()
+    an = anno.getName()
+    """
+    export ALEVIN_FRY_HOME=\$PWD/af_home; mkdir -p \$ALEVIN_FRY_HOME; $COUNTBIN set-paths &> index.log; zcat $gen > ref.fa && zcat $an > ref.gtf && $COUNTBIN index --fasta ref.fa --gtf ref.gtf -t ${task.cpus} $IDXPARAMS -o $COUNTUIDXNAME &>> index.log && rm -f ref.fa ref.gtf && ln -fs $COUNTUIDXNAME simpleaf.idx
+    """
+
+}
+
+process simpleaf_quant{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/simpleaf/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/simpleaf/${CONDITION}/${file(filename).getName()}"
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*_afquant", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    idx = reads[0]
+    rs = reads[1..2].sort()
+    r1 = rs[0]
+    r2 = rs[1]
+    fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+    qd = fn+"_afquant"
+    lf = "simpleaf_"+fn+".log"
+    """
+    export ALEVIN_FRY_HOME=\$PWD/af_home; mkdir -p \$ALEVIN_FRY_HOME; $COUNTBIN set-paths &> $lf; $COUNTBIN quant --index $idx/index --reads1 $r1 --reads2 $r2 -t ${task.cpus} $COUNTPARAMS -o $qd &>> $lf
+    """
+}
+
+workflow COUNTING{
+    take: collection
+
+    main:
+
+    checkidx = file(COUNTUIDX)
+    collection.filter(~/.fastq.gz/)
+
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(COUNTUIDX)
+        simpleaf_quant(idxfile.combine(samples_ch.collate(2)))
+    }
+    else{
+        genomefile = Channel.fromPath(COUNTREF)
+        annofile = Channel.fromPath(COUNTANNO)
+        simpleaf_idx(genomefile, annofile)
+        simpleaf_quant(simpleaf_idx.out.idx.combine(samples_ch.collate(2)))
+    }
+
+    emit:
+    counts = simpleaf_quant.out.counts
+    logs = simpleaf_quant.out.logs
+}
diff --git a/workflows/simpleaf.smk b/workflows/simpleaf.smk
new file mode 100644
index 00000000..06a1c38a
--- /dev/null
+++ b/workflows/simpleaf.smk
@@ -0,0 +1,43 @@
+COUNTBIN, COUNTENV = env_bin_from_config(config,'COUNTING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'COUNTING', COUNTENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = COUNTENV
+unik = get_dict_hash(keydict)
+
+rule themall:
+    input:  expand("COUNTS/{combo}/{file}/af_quant/alevin/quants_mat.mtx.gz", combo=combo, file=samplecond(SAMPLES, config))
+
+rule generate_index:
+    input:  fa = REFERENCE,
+            anno = ANNOTATION
+    output: idx = directory(expand("{refd}/INDICES/{cenv}_{unikey}", refd=REFDIR, cenv=COUNTENV, unikey=unik)),
+            idxfile = expand("{refd}/INDICES/{cenv}_{unikey}/index/info.json", refd=REFDIR, cenv=COUNTENV, unikey=unik),
+            tmpfa = temp(expand("TMP/{cenv}/ref.fa", cenv=COUNTENV)),
+            tmpanno = temp(expand("TMP/{cenv}/ref.gtf", cenv=COUNTENV))
+    log:    expand("LOGS/{sets}/{cenv}.idx.log", sets=SETS, cenv=COUNTENV)
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: mapp = COUNTBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('INDEX', "--ref-type spliced+unspliced"),
+            afhome = lambda wildcards, output: os.path.join(os.path.dirname(str(output.idx[0])), "af_home")
+    shell:  "if [[ -f \"{output.idxfile}\" ]]; then touch {output.idxfile} && echo \"Found index, continue with quant\" ; else export ALEVIN_FRY_HOME={params.afhome}; mkdir -p {params.afhome}; {params.mapp} set-paths &>> {log}; zcat {input.fa} > {output.tmpfa} && zcat {input.anno} > {output.tmpanno} && {params.mapp} index --fasta {output.tmpfa} --gtf {output.tmpanno} -t {threads} {params.ipara} -o {output.idx} &> {log};fi"
+
+if paired == 'paired' or paired == 'singlecell':
+    rule counting:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                idx = rules.generate_index.output.idx,
+                dummy = rules.generate_index.output.idxfile
+        output: cnts = report("COUNTS/{combo}/{file}/af_quant/alevin/quants_mat.mtx.gz", category="COUNTING"),
+                ctsdir = report(directory("COUNTS/{combo}/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/simpleafquant.log"
+        conda:  ""+COUNTENV+".yaml"
+        container: "oras://jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'COUNTING', COUNTENV)['OPTIONS'].get('COUNT', "--chemistry 10xv3 --resolution cr-like --expected-ori fw --unfiltered-pl"),
+                mapp = COUNTBIN,
+                afhome = lambda wildcards, input: os.path.join(os.path.dirname(str(input.idx)), "af_home"),
+                quantdir = lambda wildcards, output: os.path.join(str(output.ctsdir), "af_quant")
+        shell: "export ALEVIN_FRY_HOME={params.afhome}; mkdir -p {output.ctsdir}; {params.mapp} quant --index {input.idx}/index --reads1 {input.r1} --reads2 {input.r2} -t {threads} {params.cpara} -o {params.quantdir} &> {log} && gzip -c {params.quantdir}/alevin/quants_mat.mtx > {output.cnts} 2>> {log}"
diff --git a/workflows/spit_DTU.nf b/workflows/spit_DTU.nf
index f02db235..65b8bf3f 100644
--- a/workflows/spit_DTU.nf
+++ b/workflows/spit_DTU.nf
@@ -11,6 +11,7 @@ DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
 IDXPARAMS = get_always('spit_DTU_params_INDEX') ?: ''
 COUNTPARAMS = get_always('spit_DTU_params_COUNT') ?: ''
 DTUPARAMS = get_always('spit_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('spit_DTU_params_TERMINUS') ?: ''
 DTUREPS = get_always('DTUREPS') ?: ''
 DTUCOMP = get_always('DTUCOMP') ?: ''
 DTUCOMPS = get_always('DTUCOMPS') ?: ''
@@ -227,6 +228,32 @@ process collect_spit{
     """
 }
 
+process terminus_collapse{
+    conda "terminus.yaml"
+    container "oras://jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+
+    input:
+    path counts
+
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+
 workflow DTU{ 
     take: collection
 
@@ -259,7 +286,14 @@ workflow DTU{
     }
 
     prepare_dtu_annotation()
-    run_spit(salmon_quant.out.counts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_spit(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
     create_summary_snippet(run_spit.out.tbls.concat(run_spit.out.figs.concat(run_spit.out.session)).collect())
     collect_spit(run_spit.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
 
diff --git a/workflows/spit_DTU.smk b/workflows/spit_DTU.smk
index 32d9575c..1d09d589 100644
--- a/workflows/spit_DTU.smk
+++ b/workflows/spit_DTU.smk
@@ -7,6 +7,10 @@ comparison = comparable_as_string(config,'DTU')
 compstr = [i.split(":")[0] for i in comparison.split(",")]
 usededup = config.get('RUNDEDUP', False)
 
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+
 keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
 keydict["REF"] = REFERENCE
 keydict["DECOY"] = DECOY
@@ -97,8 +101,35 @@ else:
         shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
 
 
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+
 rule create_annotation_table:
-    input:  dir  = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config)),
+    input:  dir  = ctsource,
     output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
     log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
     conda:  ""+COUNTENV+".yaml"

From 2c1246cf28b41e17d955b47ad4e31dfc66841370 Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Sat, 4 Jul 2026 16:31:26 +0200
Subject: [PATCH 06/80] test coverage overhaul driven by Claude Fable,
 refactoring duplicated code into extra modules, removed dead code

---
 .github/workflows/python-app.yaml             |   2 +-
 MONSDA/Configurator.py                        |  12 +-
 MONSDA/Deprecated.py                          | 181 ----
 MONSDA/Params.py                              |   6 +-
 MONSDA/RunMONSDA.py                           |  11 +-
 MONSDA/Utils.py                               |   6 +-
 MONSDA/Workflows.py                           | 834 +++--------------
 tests/data/config_DTU_test.json               | 128 +++
 tests/golden/config_DTU/nf/_filelist.txt      |   6 +
 ...lore-picard-star_DTU_dexseq_subconfig.json |  77 ++
 ...imgalore-picard-star_DTU_dexseq_subflow.nf | 296 ++++++
 ...ore-picard-star_DTU_drimseq_subconfig.json |  77 ++
 ...mgalore-picard-star_DTU_drimseq_subflow.nf | 297 ++++++
 ...galore-picard-star_DTU_spit_subconfig.json |  77 ++
 ...trimgalore-picard-star_DTU_spit_subflow.nf | 296 ++++++
 tests/golden/config_DTU/smk/_filelist.txt     |   6 +
 ...lore-picard-star_DTU_dexseq_subconfig.json |  77 ++
 ...galore-picard-star_DTU_dexseq_subsnake.smk | 482 ++++++++++
 ...ore-picard-star_DTU_drimseq_subconfig.json |  77 ++
 ...alore-picard-star_DTU_drimseq_subsnake.smk | 511 +++++++++++
 ...galore-picard-star_DTU_spit_subconfig.json |  77 ++
 ...imgalore-picard-star_DTU_spit_subsnake.smk | 503 +++++++++++
 ...est_fastqc-bbduk-picard-bwa_subconfig.json |  87 ++
 .../Test_fastqc-bbduk-picard-bwa_subflow.nf   | 554 ++++++++++++
 ...st_fastqc-bbduk-picard-star_subconfig.json |  87 ++
 .../Test_fastqc-bbduk-picard-star_subflow.nf  | 584 ++++++++++++
 ..._fastqc-cutadapt-picard-bwa_subconfig.json |  87 ++
 ...Test_fastqc-cutadapt-picard-bwa_subflow.nf | 554 ++++++++++++
 ...fastqc-cutadapt-picard-star_subconfig.json |  87 ++
 ...est_fastqc-cutadapt-picard-star_subflow.nf | 584 ++++++++++++
 ...astqc-trimgalore-picard-bwa_subconfig.json |  87 ++
 ...st_fastqc-trimgalore-picard-bwa_subflow.nf | 549 ++++++++++++
 ...stqc-trimgalore-picard-star_subconfig.json |  87 ++
 ...t_fastqc-trimgalore-picard-star_subflow.nf | 579 ++++++++++++
 tests/golden/config_Test/nf/_filelist.txt     |  12 +
 ...est_fastqc-bbduk-picard-bwa_subconfig.json |  95 ++
 .../Test_fastqc-bbduk-picard-bwa_subsnake.smk | 839 +++++++++++++++++
 ...st_fastqc-bbduk-picard-star_subconfig.json |  95 ++
 ...Test_fastqc-bbduk-picard-star_subsnake.smk | 754 ++++++++++++++++
 ..._fastqc-cutadapt-picard-bwa_subconfig.json |  95 ++
 ...st_fastqc-cutadapt-picard-bwa_subsnake.smk | 839 +++++++++++++++++
 ...fastqc-cutadapt-picard-star_subconfig.json |  95 ++
 ...t_fastqc-cutadapt-picard-star_subsnake.smk | 754 ++++++++++++++++
 ...astqc-trimgalore-picard-bwa_subconfig.json |  95 ++
 ..._fastqc-trimgalore-picard-bwa_subsnake.smk | 846 ++++++++++++++++++
 ...stqc-trimgalore-picard-star_subconfig.json |  95 ++
 ...fastqc-trimgalore-picard-star_subsnake.smk | 761 ++++++++++++++++
 tests/golden/config_Test/smk/_filelist.txt    |  12 +
 tests/test_Utils.py                           |  37 +-
 tests/test_Workflows.py                       | 357 ++++++++
 workflows/dexseq_DTU.nf                       | 118 +--
 workflows/dexseq_DTU.smk                      | 109 +--
 workflows/drimseq_DTU.nf                      | 118 +--
 workflows/drimseq_DTU.smk                     | 107 +--
 workflows/dtu_base.nf                         | 117 +++
 workflows/dtu_base.smk                        | 106 +++
 workflows/spit_DTU.nf                         | 118 +--
 workflows/spit_DTU.smk                        | 107 +--
 58 files changed, 13076 insertions(+), 1570 deletions(-)
 delete mode 100644 MONSDA/Deprecated.py
 create mode 100644 tests/data/config_DTU_test.json
 create mode 100644 tests/golden/config_DTU/nf/_filelist.txt
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
 create mode 100644 tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
 create mode 100644 tests/golden/config_DTU/smk/_filelist.txt
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subsnake.smk
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subsnake.smk
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
 create mode 100644 tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subsnake.smk
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
 create mode 100644 tests/golden/config_Test/nf/_filelist.txt
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subconfig.json
 create mode 100644 tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subsnake.smk
 create mode 100644 tests/golden/config_Test/smk/_filelist.txt
 create mode 100644 tests/test_Workflows.py
 create mode 100644 workflows/dtu_base.nf
 create mode 100644 workflows/dtu_base.smk

diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml
index f1f142ea..33fadca7 100644
--- a/.github/workflows/python-app.yaml
+++ b/.github/workflows/python-app.yaml
@@ -72,7 +72,7 @@ jobs:
       - name: Run unit tests with coverage
         run: |          
           cd ${{ github.workspace }}
-          pytest tests/test_Utils.py -v --cov=MONSDA --cov-report=xml --cov-report=html
+          pytest tests/ -v --cov=MONSDA --cov-report=xml --cov-report=html --cov-fail-under=25
       - name: Upload coverage reports
         uses: codecov/codecov-action@v4
         if: always()
diff --git a/MONSDA/Configurator.py b/MONSDA/Configurator.py
index 97bd7be8..8cb5ec66 100755
--- a/MONSDA/Configurator.py
+++ b/MONSDA/Configurator.py
@@ -27,7 +27,7 @@
     installpath = os.path.dirname(__file__).replace(
         os.sep.join(["lib", pythonversion, "site-packages", "MONSDA"]), "share"
     )
-except:
+except Exception:
     installpath = os.getcwd()
 
 configpath = os.path.join(installpath, "MONSDA", "configs")
@@ -179,7 +179,7 @@ def proof_input(self, whitespace, proof=None, spec=None):
                     [float(x) for x in a.split(",")]
                     self.answer = a
                     break
-                except:
+                except ValueError:
                     self.clear(2)
                     prRed("please enter integer or float")
                     continue
@@ -304,7 +304,7 @@ def setInDict(dataDict, maplist, value):
                 dataDict[first].append(value)
             else:
                 dataDict[first] = value
-        except:
+        except (KeyError, TypeError, AttributeError):
             dataDict[first] = value
 
 
@@ -579,7 +579,7 @@ def show_settings():
             var = json.dumps(var, indent=4)
         try:
             print(f"{name} = {var}")
-        except:
+        except Exception:
             print(f"{name} = NestedDefaultDict()")
     print("\n============")
 
@@ -1140,7 +1140,7 @@ def set_settings():
                 try:
                     if project.samplesDict[k]["cond"] == ":".join(maplist):
                         seq = project.samplesDict[k]["seq"]
-                except:
+                except (KeyError, TypeError):
                     continue
             setInDict(project.settingsDict, maplist + ["SEQUENCING"], seq)
             settings_to_make = ["REFERENCE", "DECOY", "GTF", "GFF"]
@@ -1257,7 +1257,7 @@ def modify(config=None):
             else:
                 print("")
             prCyan(f"   {config}")
-        except:
+        except Exception:
             er = 2
 
         condition_pathes = getPathesFromDict(modify_config, "SAMPLES")
diff --git a/MONSDA/Deprecated.py b/MONSDA/Deprecated.py
deleted file mode 100644
index cd5bd546..00000000
--- a/MONSDA/Deprecated.py
+++ /dev/null
@@ -1,181 +0,0 @@
-########################################
-############## DEPRECATED ##############
-########################################
-
-@check_run
-def sources(config):
-    logid = scriptname+'.Collection_sources: '
-    ret = list()
-    search =  [x[0] for x in keysets_from_dict(config["SOURCE"]) if x[0] != 'last']
-    if len(getFromDict(config['SAMPLES'], search)) > 0:
-        ret.extend(search)
-    log.debug(logid+str(ret))
-    return ret
-
-@check_run
-def genomepath(s, config):
-    logid=scriptname+'.Collection_genomepath: '
-    sa = os.path.basename(str(s))
-    cond= s.split(os.sep)[-2]
-    sk = find_key_for_value(sa, config["SAMPLES"])
-    log.debug(logid+'GENOMEPATH: '+str([sa, cond, sk]))
-    for skey in sk:
-        klist = value_extract(skey, config["SOURCE"])
-        for k in klist:
-            for x, y in config["GENOME"].items():
-                log.debug(logid+'GENOMEPATH: '+str([x, y, k]))
-                if str(k) == str(y) or str(k) == str(x):
-                    return os.path.join(str(x), str(y))
-
-@check_run
-def genome(s, config):
-    logid=scriptname+'.Collection_genome: '
-    sa = os.path.basename(str(s))
-    sp = source_from_sample(str(s), config)
-    cond= s.split(os.sep)[-2]
-    sk = find_key_for_value(sa, config['SAMPLES'])
-    for skey in sk:
-        klist = value_extract(skey, config['SOURCE'])
-        for k in klist:
-            for x, y in config['GENOME'].items():
-                log.debug(logid+str([k, x, y]))
-                if str(k) == str(x):
-                    return str(y)
-
-@check_run
-def fullgenomepath(sa, config):
-    ret=list()
-    for s in sa:
-        l = config["GENOME"][s]
-        ret.append(os.path.join(str(s), str(l)))
-    return ret
-
-@check_run
-def genomename(s, config):
-    s = os.path.basename(str(s))
-    for k, v in config["SAMPLES"].items():
-        for g, l in v.items():
-            if s in l:
-                for x, y in config["GENOME"].items():
-                    if g == y:
-                        return str(x)
-
-@check_run
-def transcriptomepath(s, config):
-    logid=scriptname+'.Collection_transcriptomepath: '
-    sa = os.path.basename(str(s))
-    cond= s.split(os.sep)[-2]
-    sk = find_key_for_value(sa, config["SAMPLES"])
-    log.debug(logid+'TRANSCRIPTOMEPATH: '+str([sa, cond, sk]))
-    for skey in sk:
-        klist = value_extract(skey, config["SOURCE"])
-        for k in klist:
-            for x, y in config["TRANSCRIPTOME"].items():
-                log.debug(logid+'TRANSCRIPTOMEPATH: '+str([x, y, k]))
-                if str(k) == str(y) or str(k) == str(x):
-                    return os.path.join(str(x), str(y))
-
-@check_run
-def transcriptome(s, config):
-    logid=scriptname+'.Collection_transcriptome: '
-    sa = os.path.basename(str(s))
-    sp = source_from_sample(str(s), config)
-    cond= s.split(os.sep)[-2]
-    sk = find_key_for_value(sa, config['SAMPLES'])
-    for skey in sk:
-        klist = value_extract(skey, config['SOURCE'])
-        for k in klist:
-            for x, y in config['TRANSCRIPTOME'].items():
-                log.debug(logid+str([k, x, y]))
-                if str(k) == str(x):
-                    return str(y)
-
-@check_run
-def fulltranscriptomepath(sa, config):
-    ret=list()
-    for s in sa:
-        l = config["TRANSCRIPTOME"][s]
-        ret.append(os.path.join(str(s), str(l)))
-    return ret
-
-@check_run
-def transcriptomename(s, config):
-    s = os.path.basename(str(s))
-    for k, v in config["SAMPLES"].items():
-        for g, l in v.items():
-            if s in l:
-                for x, y in config["TRANSCRIPTOME"].items():
-                    if g == y:
-                        return str(x)
-
-
-@check_run
-def namefromfile(s, config):
-    if 'NAME' not in config:
-        return ''
-    else:
-        sa = os.path.basename(str(s))
-        cond= s.split(os.sep)[-2]
-        sk = find_key_for_value(sa, config["SAMPLES"])
-        for skey in sk:
-            klist = value_extract(skey, config["NAME"])
-            for k in klist:
-                if str(skey) == str(cond):
-                    return str(k)
-
-
-@check_run
-def namefrompath(p, config):
-    p = os.path.dirname(p).split(os.sep)
-    klist = getFromDict(config["NAME"], p) if 'NAME' in config else list('')
-    for k in klist:
-        return str(k)
-
-@check_run
-def pathstogenomes(samples, config):
-    ret = list()
-    for s in samples:
-        s = os.path.basename(s)
-        for k, v in config["SAMPLES"].items():
-            for g, l in v.items():
-                if s in l:
-                    for x, y in config["GENOME"].items():
-                        if g == y:
-                            ret.append(os.path.join(str(x), str(y)))
-    return sorted(list(set(ret)))
-
-
-@check_run
-def source_from_sample(sample, config):
-    logid=scriptname+'.Collection_source_from_sample: '
-    s = os.path.dirname(str(sample))
-    cond= s.split(os.sep)
-    log.debug(logid+str([s, cond]))
-    ret = getFromDict(config["SOURCE"], cond)[0]
-    return ret
-
-
-@check_run
-def anno_from_file(sample, config, step):
-    logid = scriptname+'.Collection_anno_from_file: '
-    p = os.path.dirname(genomepath(sample, config))
-    s = source_from_sample(sample, config)
-    ret = os.path.join(config["REFERENCE"], p, subDict(config["ANNOTATE"], s)[step])
-    log.debug(logid+str(ret))
-    return ret
-
-@check_run
-def anno_from_source(source, config, step):
-    logid = scriptname+'.Collection_anno_from_source: '
-    s = source.split(os.sep)[0:-1]
-    p = s[0]
-    samp = source.split(os.sep)[-1]
-    log.debug(logid+str(s))
-    runstate = runstate_from_sample([samp], config)[0]
-    lst = list()
-    lst.extend(s)
-    lst.append(runstate)
-    log.debug(logid+str(lst))
-    ret = os.path.join(config["REFERENCE"], p, subDict(config["ANNOTATE"], lst)[step])
-    log.debug(logid+str(ret))
-    return ret
diff --git a/MONSDA/Params.py b/MONSDA/Params.py
index 6a62a918..80a71fbb 100644
--- a/MONSDA/Params.py
+++ b/MONSDA/Params.py
@@ -1390,7 +1390,7 @@ def runstate_from_sample(sample: list, config: dict) -> list:
         log.debug(logid + "SAMPLE: " + s)
         try:
             c = mu.get_from_dict(config["SETTINGS"], s.split(os.sep))[0]
-        except:
+        except (KeyError, IndexError, TypeError):
             c = None
         log.debug(logid + "SETTINGS: " + str(c))
         if mu.dict_inst(c):
@@ -1698,7 +1698,7 @@ def get_pairing(
                         log.debug(logid + "Match found: " + str(v) + " : " + str(x))
                         try:
                             matching = samplecond([x], config)[0].replace("MAPPED/", "")
-                        except:
+                        except (KeyError, IndexError):
                             matching = x
                         log.info(logid + "PAIRINGS: " + sample + ": " + str(matching))
         if not matching or matching == "":
@@ -1819,7 +1819,7 @@ def check_tool_params(
             return "std"
         else:
             return ""
-    except:
+    except (KeyError, IndexError, TypeError):
         if subconf == "MAPPING":
             return "std"
         else:
diff --git a/MONSDA/RunMONSDA.py b/MONSDA/RunMONSDA.py
index 422c82d3..fc287583 100755
--- a/MONSDA/RunMONSDA.py
+++ b/MONSDA/RunMONSDA.py
@@ -147,6 +147,14 @@ def parseargs():
         help="Container registry hostname for ORAS image pulls (default: docker.io). "
              "Use e.g. ghcr.io for GitHub Container Registry.",
     )
+    parser.add_argument(
+        "--oras-namespace",
+        type=str,
+        default="jfallmann/monsda",
+        metavar="NAMESPACE",
+        help="Container namespace/repository for ORAS image pulls "
+             "(default: jfallmann/monsda). Use e.g. myuser/myrepo for a custom registry.",
+    )
 
     if len(sys.argv) == 1:
         parser.print_help(sys.stderr)
@@ -207,7 +215,7 @@ def run_snakemake(
                     os.sep.join(["lib", pythonversion, "site-packages", "MONSDA"]),
                     "share",
                 )
-            except:
+            except Exception:
                 installpath = os.getcwd()
 
             workflowpath = os.path.join(installpath, "MONSDA", "workflows")
@@ -972,6 +980,7 @@ def main():
 
         # --- set ORAS registry for container image pulls ---
         mw.set_oras_registry(knownargs.oras_registry)
+        mw.set_oras_namespace(knownargs.oras_namespace)
 
         log.debug(
             f"{logid} ARGS: {args} {type(args)} KNOWNARGS: {knownargs} {type(knownargs)} OPTIONALARGS: {optionalargs} {type(optionalargs)}"
diff --git a/MONSDA/Utils.py b/MONSDA/Utils.py
index 8dd741f5..a9a93fe6 100644
--- a/MONSDA/Utils.py
+++ b/MONSDA/Utils.py
@@ -411,7 +411,7 @@ def merge_dicts(d:dict, u:dict) -> dict:
     # python 3.8+ compatibility
     try:
         collectionsAbc = collections.abc
-    except:
+    except AttributeError:
         collectionsAbc = collections
 
     for k, v in six.iteritems(u):
@@ -876,7 +876,9 @@ def toarray(file:str, ulim:int) -> np.array:
         usecols=(ulim),
         delimiter="\t",
         unpack=True,
-        converters={ulim: lambda s: convertcol(s.decode("utf-8"))},
+        converters={
+            ulim: lambda s: convertcol(s.decode("utf-8") if isinstance(s, bytes) else s)
+        },
     )
     return x
 
diff --git a/MONSDA/Workflows.py b/MONSDA/Workflows.py
index 8f863430..293206e0 100755
--- a/MONSDA/Workflows.py
+++ b/MONSDA/Workflows.py
@@ -45,7 +45,7 @@
     installpath = os.path.dirname(__file__).replace(
         os.sep.join(["lib", pythonversion, "site-packages", "MONSDA"]), "share"
     )
-except:
+except Exception:
     installpath = os.getcwd()
 
 workflowpath = os.path.join(installpath, "MONSDA", "workflows")
@@ -57,6 +57,8 @@
 # Docker Hub namespace like "jfallmann/" without dots before the first slash)
 oraspath = re.compile(r'oras://(?![\w.-]+\.[\w.-]+/)')
 oras_registry = "docker.io"
+oras_default_namespace = "jfallmann/monsda"
+oras_namespace = oras_default_namespace
 
 
 def set_oras_registry(registry: str):
@@ -65,16 +67,53 @@ def set_oras_registry(registry: str):
     oras_registry = registry.strip().rstrip("/")
 
 
+def set_oras_namespace(namespace: str):
+    """Set the ORAS namespace/repository used for container image URIs."""
+    global oras_namespace
+    oras_namespace = namespace.strip().strip("/")
+
+
+def _rewrite_oras(content: str) -> str:
+    """Apply configured ORAS namespace and registry to oras:// URIs."""
+    if oras_namespace != oras_default_namespace:
+        content = content.replace(
+            "oras://" + oras_default_namespace, "oras://" + oras_namespace
+        )
+    return re.sub(oraspath, "oras://" + oras_registry + "/", content)
+
+
 def _fix_oras(line: str) -> str:
     """Insert oras_registry into unqualified oras:// URIs."""
-    return re.sub(oraspath, "oras://" + oras_registry + "/", line)
+    return _rewrite_oras(line)
 
 
 def _write_workflow(filepath, content):
     """Write workflow file with ORAS registry fix applied."""
-    content = re.sub(oraspath, "oras://" + oras_registry + "/", content)
+    content = _rewrite_oras(content)
     write_if_different(filepath, content)
 
+
+def _fill_template(path, loglevel, logfix, condapath, conda_repl, nfmode=False):
+    """Read a workflow template file, strip comments, and apply the standard
+    loglevel/conda/include substitutions. Returns the processed lines as a list."""
+    marker = "include {" if nfmode else "include: "
+    out = list()
+    with open(path, "r") as fh:
+        for line in mu.comment_remover(fh.readlines()):
+            line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
+            line = re.sub(condapath, conda_repl, line)
+            if marker in line:
+                if nfmode:
+                    line = fixinclude(
+                        line, loglevel, condapath, envpath, workflowpath, logfix, "nfmode"
+                    )
+                else:
+                    line = fixinclude(
+                        line, loglevel, condapath, envpath, workflowpath, logfix
+                    )
+            out.append(line)
+    return out
+
 try:
     scriptname = (
         os.path.basename(inspect.stack()[-1].filename)
@@ -298,7 +337,7 @@ def create_subworkflow(config, subwork, conditions, envs=None, stage=None):
     for condition in list(set(conditions)):
         try:
             env = str(mu.sub_dict(config[subwork], condition)[stage + "ENV"])
-        except:
+        except (KeyError, TypeError):
             if "TOOLS" not in config[subwork]:
                 log.error(
                     "No tool environment found for "
@@ -310,7 +349,7 @@ def create_subworkflow(config, subwork, conditions, envs=None, stage=None):
             env = ""
         try:
             exe = str(mu.sub_dict(config[subwork], condition)[stage + "BIN"])
-        except:
+        except (KeyError, TypeError):
             if "TOOLS" not in config[subwork]:
                 log.error(
                     "No tool binary found for "
@@ -494,15 +533,7 @@ def make_pre(
             add = list()
 
             smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda:  "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    add.append(line)
+            add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda:  "' + envpath, nfmode=False))
 
             for i in range(len(worklist)):
                 log.debug(
@@ -579,35 +610,12 @@ def make_pre(
                             subname = toolenv + "_raw.smk"
 
                         smkf = os.path.abspath(os.path.join(workflowpath, subname))
-                        with open(smkf, "r") as smk:
-                            for line in mu.comment_remover(smk.readlines()):
-                                line = re.sub(
-                                    logfix, "loglevel='" + loglevel + "'", line
-                                )
-                                line = re.sub(condapath, 'conda: "' + envpath, line)
-                                if "include: " in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                        subjobs.append("\n\n")
 
                 smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                        line = re.sub(condapath, 'conda: "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line, loglevel, condapath, envpath, workflowpath, logfix
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                subjobs.append("\n\n")
 
                 smko = os.path.abspath(
                     os.path.join(
@@ -645,16 +653,8 @@ def make_pre(
         for condition in conditions:
             add = list()
             smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    add.append(line)
-                add.append("\n\n")
+            add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+            add.append("\n\n")
 
             # Add variable for combination string
             add.append(
@@ -726,28 +726,12 @@ def make_pre(
                 )  # RuleThemAll for snakemake depending on chosen workflows
 
                 smkf = os.path.abspath(os.path.join(workflowpath, subname))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                        line = re.sub(condapath, 'conda: "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line, loglevel, condapath, envpath, workflowpath, logfix
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                subjobs.append("\n\n")
 
                 smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                        line = re.sub(condapath, 'conda: "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line, loglevel, condapath, envpath, workflowpath, logfix
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                subjobs.append("\n\n")
 
                 smko = os.path.abspath(
                     os.path.join(
@@ -815,15 +799,7 @@ def make_sub(
             add = list()
 
             smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    add.append(line)
+            add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
 
             for i in range(len(worklist)):
                 log.debug(
@@ -943,39 +919,15 @@ def make_sub(
                             subconf["PREDEDUP"] = "enabled"
 
                         smkf = os.path.abspath(os.path.join(workflowpath, subname))
-                        with open(smkf, "r") as smk:
-                            for line in mu.comment_remover(smk.readlines()):
-                                line = re.sub(condapath, 'conda: "' + envpath, line)
-                                if "include: " in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                        subjobs.append("\n\n")
 
                 if "MAPPING" in works and envs[works.index("MAPPING")] not in [
                     "piscem"
                 ]:
                     smkf = os.path.abspath(os.path.join(workflowpath, "mapping.smk"))
-                    with open(smkf, "r") as smk:
-                        for line in mu.comment_remover(smk.readlines()):
-                            line = re.sub(condapath, 'conda: "' + envpath, line)
-                            if "include: " in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                    subjobs.append("\n\n")
                 if "QC" in subworkflows:
                     # Use rustqc-specific multiqc whenever rustqc is part of this combo
                     qc_mqc = (
@@ -984,32 +936,13 @@ def make_sub(
                         else "multiqc.smk"
                     )
                     smkf = os.path.abspath(os.path.join(workflowpath, qc_mqc))
-                    with open(smkf, "r") as smk:
-                        for line in mu.comment_remover(smk.readlines()):
-                            line = re.sub(condapath, 'conda: "' + envpath, line)
-                            if "include: " in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                    subjobs.append("\n\n")
 
                 # Append footer and write out subsnake and subconf per condition
                 smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(condapath, 'conda: "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line, loglevel, condapath, envpath, workflowpath, logfix
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                subjobs.append("\n\n")
 
                 smko = os.path.abspath(
                     os.path.join(
@@ -1037,16 +970,8 @@ def make_sub(
             add = list()
 
             smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    add.append(line)
-                add.append("\n\n")
+            add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+            add.append("\n\n")
 
             # Add variable for combination string
             add.append(
@@ -1138,20 +1063,8 @@ def make_sub(
                     )
 
                     smkf = os.path.abspath(os.path.join(workflowpath, subname))
-                    with open(smkf, "r") as smk:
-                        for line in mu.comment_remover(smk.readlines()):
-                            line = re.sub(condapath, 'conda: "' + envpath, line)
-                            if "include: " in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                    subjobs.append("\n\n")
 
             if "MAPPING" in subworkflows:
                 _map_tools, _ = create_subworkflow(config, "MAPPING", [condition])
@@ -1160,15 +1073,8 @@ def make_sub(
                 )
                 if not _skip_postmap:
                     smkf = os.path.abspath(os.path.join(workflowpath, "mapping.smk"))
-                    with open(smkf, "r") as smk:
-                        for line in mu.comment_remover(smk.readlines()):
-                            line = re.sub(condapath, 'conda: "' + envpath, line)
-                            if "include: " in line:
-                                line = fixinclude(
-                                    line, loglevel, condapath, envpath, workflowpath, logfix
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                    subjobs.append("\n\n")
                 if "QC" in subworkflows:
                     # Use rustqc-specific multiqc whenever rustqc is part of this combo
                     _qc_tools, _ = create_subworkflow(config, "QC", [condition])
@@ -1179,32 +1085,12 @@ def make_sub(
                         else "multiqc.smk"
                     )
                     smkf = os.path.abspath(os.path.join(workflowpath, qc_mqc))
-                    with open(smkf, "r") as smk:
-                        for line in mu.comment_remover(smk.readlines()):
-                            line = re.sub(condapath, 'conda: "' + envpath, line)
-                            if "include: " in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+                    subjobs.append("\n\n")
 
             smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    subjobs.append(line)
-                subjobs.append("\n\n")
+            subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+            subjobs.append("\n\n")
 
             smko = os.path.abspath(
                 os.path.join(subdir, "_".join(["_".join(condition), "subsnake.smk"]))
@@ -1260,15 +1146,7 @@ def make_post(
         add = list()
 
         smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-        with open(smkf, "r") as smk:
-            for line in mu.comment_remover(smk.readlines()):
-                line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                line = re.sub(condapath, 'conda: "' + envpath, line)
-                if "include: " in line:
-                    line = fixinclude(
-                        line, loglevel, condapath, envpath, workflowpath, logfix
-                    )
-                add.append(line)
+        add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
 
         for i in range(len(envlist)):
             envs = envlist[i].split("-")
@@ -1372,38 +1250,13 @@ def make_post(
                         os.path.abspath(os.path.join(workflowpath, toolenv))
                         + "_trim.smk"
                     )
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                        line = re.sub(condapath, 'conda:  "' + envpath, line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                            )
-                        subjobs.append(line)
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda:  "' + envpath, nfmode=False))
                 subjobs.append("\n\n")
 
                 # Append footer and write out subsnake and subconf for all conditions
                 smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-                with open(smkf, "r") as smk:
-                    for line in mu.comment_remover(smk.readlines()):
-                        line = re.sub(condapath, 'conda: "../', line)
-                        if "include: " in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "../', nfmode=False))
+                subjobs.append("\n\n")
 
                 te = (
                     toolenv.split("_")[0] if "_" in toolenv else toolenv
@@ -1449,15 +1302,7 @@ def make_post(
         add = list()
         subconf = mu.NestedDefaultDict()
         smkf = os.path.abspath(os.path.join(workflowpath, "header.smk"))
-        with open(smkf, "r") as smk:
-            for line in mu.comment_remover(smk.readlines()):
-                line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                line = re.sub(condapath, 'conda: "' + envpath, line)
-                if "include: " in line:
-                    line = fixinclude(
-                        line, loglevel, condapath, envpath, workflowpath, logfix
-                    )
-                add.append(line)
+        add.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
 
         # Postprocessing runs across all conditions - collect configs from all conditions at once
         listoftools, listofconfigs = create_subworkflow(
@@ -1524,27 +1369,13 @@ def make_post(
                 smkf = (
                     os.path.abspath(os.path.join(workflowpath, toolenv)) + "_trim.smk"
                 )
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    subjobs.append(line)
-                subjobs.append("\n\n")
+            subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+            subjobs.append("\n\n")
 
             # Append footer and write out subsnake and subconf for all conditions
             smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
-            with open(smkf, "r") as smk:
-                for line in mu.comment_remover(smk.readlines()):
-                    line = re.sub(condapath, 'conda: "' + envpath, line)
-                    if "include: " in line:
-                        line = fixinclude(
-                            line, loglevel, condapath, envpath, workflowpath, logfix
-                        )
-                    subjobs.append(line)
-                subjobs.append("\n\n")
+            subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+            subjobs.append("\n\n")
 
             te = (
                 toolenv.split("_")[0] if "_" in toolenv else toolenv
@@ -1630,16 +1461,8 @@ def make_summary(config, subdir, loglevel, combinations=None):
         subjobs.append("\n\n")
 
     smkf = os.path.abspath(os.path.join(workflowpath, "summary.smk"))
-    with open(smkf, "r") as smk:
-        for line in mu.comment_remover(smk.readlines()):
-            line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-            line = re.sub(condapath, 'conda: "' + envpath, line)
-            if "include: " in line:
-                line = fixinclude(
-                    line, loglevel, condapath, envpath, workflowpath, logfix
-                )
-            subjobs.append(line)
-        subjobs.append("\n\n")
+    subjobs.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda: "' + envpath, nfmode=False))
+    subjobs.append("\n\n")
 
     smkf = os.path.abspath(os.path.join(workflowpath, "footer.smk"))
     with open(smkf, "r") as smk:
@@ -1703,15 +1526,7 @@ def rulethemall(subworkflows, config, loglevel, condapath, logfix, combo=""):
         log.debug(logid + "Simulated read trimming only!")
         mu.makeoutdir("TRIMMED_FASTQ")
         smkf = os.path.abspath(os.path.join(workflowpath, "simulatetrim.smk"))
-        with open(smkf, "r") as smk:
-            for line in mu.comment_remover(smk.readlines()):
-                line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                line = re.sub(condapath, 'conda:  "' + envpath, line)
-                if "include: " in line:
-                    line = fixinclude(
-                        line, loglevel, condapath, envpath, workflowpath, logfix
-                    )
-                todos.append(line)
+        todos.extend(_fill_template(smkf, loglevel, logfix, condapath, 'conda:  "' + envpath, nfmode=False))
         todos.append("\n\n")
 
     if (
@@ -1753,7 +1568,7 @@ def fixinclude(
         toinclude = str.split(line)[-1].replace('"', "")
         toinclude = str.join(os.sep, [workflowpath, toinclude])
         with open(toinclude, "r") as incl:
-            for line in incl.readlines():
+            for line in mu.comment_remover(incl.readlines()):
                 line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
                 line = re.sub(condapath, condaline + envpath, line)
                 line = _fix_oras(line)
@@ -2360,94 +2175,7 @@ def nf_tool_params(
 
 @check_run
 def nf_get_processes(config):
-    logid = scriptname + ".Workflows_nf_get_processes: "
-
-    preprocess = subworkflows = postprocess = []
-
-    # Define workflow stages
-    pre = ["QC", "FETCH", "BASECALL"]
-    sub = ["TRIMMING", "MAPPING", "QC", "DEDUP"]
-    post = ["COUNTING", "TRACKS", "PEAKS", "DE", "DEU", "DAS", "DTU", "CIRCS"]
-
-    wfs = [x.replace(" ", "") for x in config["WORKFLOWS"].split(",")]
-
-    if "WORKFLOWS" in config:
-        log.debug(
-            logid
-            + "CONFIG-WORKFLOWS: "
-            + str(wfs)
-            + "\t"
-            + str(pre)
-            + "\t"
-            + str(sub)
-            + "\t"
-            + str(post)
-        )
-        subworkflows = [str(x) for x in wfs if x in sub]
-        log.debug(logid + "Sub: " + str(subworkflows))
-        if len(subworkflows) == 0 or subworkflows[0] == "":
-            subworkflows = []
-        preprocess = [x for x in wfs if x in pre]
-        if len(preprocess) == 0 or preprocess[0] == "":
-            preprocess = None
-        log.debug(
-            logid
-            + "Intermediate-WORKFLOWS: "
-            + str([preprocess, subworkflows, postprocess])
-        )
-
-        if (
-            subworkflows
-            and any(w in subworkflows for w in ["TRIMMING", "MAPPING", "DEDUP"])
-            and preprocess
-            and "QC" in preprocess
-        ):
-            preprocess.remove("QC")
-
-        if (
-            preprocess
-            and "QC" in preprocess
-            and not any(w in subworkflows for w in ["TRIMMING", "MAPPING", "DEDUP"])
-        ):
-            subworkflows.remove("QC")
-
-        postprocess = [x for x in wfs if x in post]
-        if len(postprocess) == 0 or postprocess[0] == "":
-            postprocess = []
-    else:
-        log.error("NO WORKFLOWS DEFINED, NOTHING TO DO!")
-        sys.exit()
-
-    if preprocess:
-        try:
-            all([config[x] or x == "" for x in preprocess])
-        except KeyError:
-            log.warning(
-                logid
-                + "Not all required preprocessing steps have configuration in the config file"
-            )
-
-    if subworkflows:
-        try:
-            all([config[x] or x == "TRIMMING" or x == "" for x in subworkflows])
-        except KeyError:
-            log.warning(
-                logid
-                + "Not all required subworkflows have configuration in the config file"
-            )
-
-    if postprocess:
-        try:
-            all([config[x] or x == "" for x in postprocess])
-        except KeyError:
-            log.warning(
-                logid
-                + "Not all required postprocessing steps have configuration in the config file"
-            )
-
-    log.debug(logid + "WORKFLOWS: " + str([preprocess, subworkflows, postprocess]))
-
-    return [preprocess, subworkflows, postprocess]
+    return get_processes(config)
 
 
 @check_run
@@ -2484,22 +2212,8 @@ def nf_make_pre(
             add = list()
 
             nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-            with open(nfi, "r") as nf:
-                for line in mu.comment_remover(nf.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda "' + envpath, line)
-                    if "include {" in line:
-                        line = fixinclude(
-                            line,
-                            loglevel,
-                            condapath,
-                            envpath,
-                            workflowpath,
-                            logfix,
-                            "nfmode",
-                        )
-                    add.append(line)
-                add.append("\n\n")
+            add.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+            add.append("\n\n")
 
             for i in range(len(worklist)):
                 log.debug(
@@ -2572,47 +2286,15 @@ def nf_make_pre(
                             flowlist.append("MULTIQC")
 
                         nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                        with open(nfi, "r") as nf:
-                            for line in mu.comment_remover(nf.readlines()):
-                                line = re.sub(
-                                    logfix, "loglevel='" + loglevel + "'", line
-                                )
-                                line = re.sub(condapath, 'conda "' + envpath, line)
-                                if "include {" in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                        "nfmode",
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                        subjobs.append("\n\n")
 
                         if works[j] == "QC":
                             nfi = os.path.abspath(
                                 os.path.join(workflowpath, "multiqc.nf")
                             )
-                            with open(nfi, "r") as nf:
-                                for line in mu.comment_remover(nf.readlines()):
-                                    line = re.sub(
-                                        logfix, "loglevel='" + loglevel + "'", line
-                                    )
-                                    line = re.sub(condapath, 'conda "' + envpath, line)
-                                    if "include {" in line:
-                                        line = fixinclude(
-                                            line,
-                                            loglevel,
-                                            condapath,
-                                            envpath,
-                                            workflowpath,
-                                            logfix,
-                                            "nfmode",
-                                        )
-                                    subjobs.append(line)
-                                subjobs.append("\n\n")
+                            subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                            subjobs.append("\n\n")
 
                         # workflow merger
                         subjobs.append("\n\n" + "workflow {\n")
@@ -2689,22 +2371,8 @@ def nf_make_pre(
             log.debug(logid + "PREPARING " + str(subwork) + " " + str(condition))
 
             nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-            with open(nfi, "r") as nf:
-                for line in mu.comment_remover(nf.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda "' + envpath, line)
-                    if "include {" in line:
-                        line = fixinclude(
-                            line,
-                            loglevel,
-                            condapath,
-                            envpath,
-                            workflowpath,
-                            logfix,
-                            "nfmode",
-                        )
-                    subjobs.append(line)
-                subjobs.append("\n\n")
+            subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+            subjobs.append("\n\n")
 
             listoftools, listofconfigs = create_subworkflow(
                 config, subwork, [condition]
@@ -2775,21 +2443,8 @@ def nf_make_pre(
                     flowlist.append("BASECALL")
 
                 nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                with open(nfi, "r") as nf:
-                    for line in mu.comment_remover(nf.readlines()):
-                        line = re.sub(condapath, 'conda "' + envpath, line)
-                        if "include {" in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                                "nfmode",
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                subjobs.append("\n\n")
 
                 if "DEDUP" in works:
                     flowlist.append("DEDUPBAM")
@@ -2809,21 +2464,8 @@ def nf_make_pre(
                 if subwork == "QC":
                     flowlist.append("MULTIQC")
                     nfi = os.path.abspath(os.path.join(workflowpath, "multiqc.nf"))
-                    with open(nfi, "r") as nf:
-                        for line in mu.comment_remover(nf.readlines()):
-                            line = re.sub(condapath, 'conda "' + envpath, line)
-                            if "include {" in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                    "nfmode",
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                    subjobs.append("\n\n")
 
                 # workflow merger
                 log.debug("FLOWLIST: " + str(flowlist))
@@ -2906,22 +2548,8 @@ def nf_make_sub(
             add = list()
 
             nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-            with open(nfi, "r") as nf:
-                for line in mu.comment_remover(nf.readlines()):
-                    line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                    line = re.sub(condapath, 'conda "' + envpath, line)
-                    if "include {" in line:
-                        line = fixinclude(
-                            line,
-                            loglevel,
-                            condapath,
-                            envpath,
-                            workflowpath,
-                            logfix,
-                            "nfmode",
-                        )
-                    add.append(line)
-                add.append("\n\n")
+            add.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+            add.append("\n\n")
 
             for i in range(len(worklist)):
                 log.debug(
@@ -3052,21 +2680,8 @@ def nf_make_sub(
                             subname = toolenv + "_dedup.nf"
 
                         nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                        with open(nfi, "r") as nf:
-                            for line in mu.comment_remover(nf.readlines()):
-                                line = re.sub(condapath, 'conda "' + envpath, line)
-                                if "include {" in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                        "nfmode",
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                        subjobs.append("\n\n")
 
                         tp.append(
                             nf_tool_params(
@@ -3095,41 +2710,15 @@ def nf_make_sub(
                                 "simulatetrim.nf",
                             )
                         )
-                        with open(nfi, "r") as nf:
-                            for line in mu.comment_remover(nf.readlines()):
-                                line = re.sub(condapath, 'conda "' + envpath, line)
-                                if "include {" in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                        "nfmode",
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                        subjobs.append("\n\n")
 
                     flowlist.append("MAPPING")
 
                     maprad = envs[works.index("MAPPING")] in ["piscem"]
                     nfi = os.path.abspath(os.path.join(workflowpath, "mapping.nf"))
-                    with open(nfi, "r") as nf:
-                        for line in mu.comment_remover(nf.readlines()):
-                            line = re.sub(condapath, 'conda "' + envpath, line)
-                            if "include {" in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                    "nfmode",
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                    subjobs.append("\n\n")
 
                     if "DEDUP" in works:
                         flowlist.append("DEDUPBAM")
@@ -3145,21 +2734,8 @@ def nf_make_sub(
                         else "multiqc.nf"
                     )
                     nfi = os.path.abspath(os.path.join(workflowpath, _mqc_nf))
-                    with open(nfi, "r") as nf:
-                        for line in mu.comment_remover(nf.readlines()):
-                            line = re.sub(condapath, 'conda "' + envpath, line)
-                            if "include {" in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                    "nfmode",
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                    subjobs.append("\n\n")
 
                 # workflow merger
                 log.debug("FLOWLIST: " + str(flowlist))
@@ -3432,39 +3008,13 @@ def nf_make_sub(
                             flowlist.append("QC_DEDUP")
                         subname = toolenv + ".nf"
                         nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                        with open(nfi, "r") as nf:
-                            for line in mu.comment_remover(nf.readlines()):
-                                line = re.sub(condapath, 'conda "' + envpath, line)
-                                if "include {" in line:
-                                    line = fixinclude(
-                                        line,
-                                        loglevel,
-                                        condapath,
-                                        envpath,
-                                        workflowpath,
-                                        logfix,
-                                        "nfmode",
-                                    )
-                                subjobs.append(line)
-                            subjobs.append("\n\n")
+                        subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                        subjobs.append("\n\n")
                         subname = toolenv + "_dedup.nf"
 
                     nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                    with open(nfi, "r") as nf:
-                        for line in mu.comment_remover(nf.readlines()):
-                            line = re.sub(condapath, 'conda "' + envpath, line)
-                            if "include {" in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                    "nfmode",
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                    subjobs.append("\n\n")
 
                     tp.append(
                         nf_tool_params(
@@ -3490,39 +3040,13 @@ def nf_make_sub(
                     log.debug(logid + "Simulated read trimming only!")
                     flowlist.append("TRIMMING")
                     nfi = os.path.abspath(os.path.join(workflowpath, "simulatetrim.nf"))
-                    with open(nfi, "r") as nf:
-                        for line in mu.comment_remover(nf.readlines()):
-                            line = re.sub(condapath, 'conda "' + envpath, line)
-                            if "include {" in line:
-                                line = fixinclude(
-                                    line,
-                                    loglevel,
-                                    condapath,
-                                    envpath,
-                                    workflowpath,
-                                    logfix,
-                                    "nfmode",
-                                )
-                            subjobs.append(line)
-                        subjobs.append("\n\n")
+                    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                    subjobs.append("\n\n")
                 flowlist.append("MAPPING")
 
                 nfi = os.path.abspath(os.path.join(workflowpath, "mapping.nf"))
-                with open(nfi, "r") as nf:
-                    for line in mu.comment_remover(nf.readlines()):
-                        line = re.sub(condapath, 'conda "' + envpath, line)
-                        if "include {" in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                                "nfmode",
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                subjobs.append("\n\n")
 
             if "QC" in subworkflows:
                 flowlist.append("MULTIQC")
@@ -3535,21 +3059,8 @@ def nf_make_sub(
                     else "multiqc.nf"
                 )
                 nfi = os.path.abspath(os.path.join(workflowpath, _mqc_nf))
-                with open(nfi, "r") as nf:
-                    for line in mu.comment_remover(nf.readlines()):
-                        line = re.sub(condapath, 'conda "' + envpath, line)
-                        if "include {" in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                                "nfmode",
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                subjobs.append("\n\n")
 
             # workflow merger
             log.debug("FLOWLIST: " + str(flowlist))
@@ -3740,22 +3251,8 @@ def nf_make_post(
         add = list()
 
         nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-        with open(nfi, "r") as nf:
-            for line in mu.comment_remover(nf.readlines()):
-                line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                line = re.sub(condapath, 'conda "' + envpath, line)
-                if "include {" in line:
-                    line = fixinclude(
-                        line,
-                        loglevel,
-                        condapath,
-                        envpath,
-                        workflowpath,
-                        logfix,
-                        "nfmode",
-                    )
-                add.append(line)
-            add.append("\n\n")
+        add.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+        add.append("\n\n")
 
         for i in range(len(envlist)):
             envs = envlist[i].split("-")
@@ -3833,21 +3330,8 @@ def nf_make_post(
 
                 subname = toolenv + ".nf"
                 nfi = os.path.abspath(os.path.join(workflowpath, subname))
-                with open(nfi, "r") as nf:
-                    for line in mu.comment_remover(nf.readlines()):
-                        line = re.sub(condapath, 'conda "' + envpath, line)
-                        if "include {" in line:
-                            line = fixinclude(
-                                line,
-                                loglevel,
-                                condapath,
-                                envpath,
-                                workflowpath,
-                                logfix,
-                                "nfmode",
-                            )
-                        subjobs.append(line)
-                    subjobs.append("\n\n")
+                subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+                subjobs.append("\n\n")
 
                 tp.append(
                     nf_tool_params(
@@ -3923,22 +3407,8 @@ def nf_make_post(
         subwork = postworkflow
         add = list()
         nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-        with open(nfi, "r") as nf:
-            for line in mu.comment_remover(nf.readlines()):
-                line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-                line = re.sub(condapath, 'conda "' + envpath, line)
-                if "include {" in line:
-                    line = fixinclude(
-                        line,
-                        loglevel,
-                        condapath,
-                        envpath,
-                        workflowpath,
-                        logfix,
-                        "nfmode",
-                    )
-                add.append(line)
-            add.append("\n\n")
+        add.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+        add.append("\n\n")
 
         # Postprocessing runs across all conditions - collect configs from all conditions at once
         flowlist = list()
@@ -3980,21 +3450,8 @@ def nf_make_post(
             subconf.update(sconf)
             subname = toolenv + ".nf"
             nfi = os.path.abspath(os.path.join(workflowpath, subname))
-            with open(nfi, "r") as nf:
-                for line in mu.comment_remover(nf.readlines()):
-                    line = re.sub(condapath, 'conda "' + envpath, line)
-                    if "include {" in line:
-                        line = fixinclude(
-                            line,
-                            loglevel,
-                            condapath,
-                            envpath,
-                            workflowpath,
-                            logfix,
-                            "nfmode",
-                        )
-                    subjobs.append(line)
-                subjobs.append("\n\n")
+            subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+            subjobs.append("\n\n")
 
             tp.append(
                 nf_tool_params(
@@ -4124,39 +3581,12 @@ def nf_make_summary(config, subdir, loglevel, combinations=None):
     subjobs = list()
 
     nfi = os.path.abspath(os.path.join(workflowpath, "header.nf"))
-    with open(nfi, "r") as nf:
-        for line in mu.comment_remover(nf.readlines()):
-            line = re.sub(logfix, "loglevel='" + loglevel + "'", line)
-            line = re.sub(condapath, 'conda "' + envpath, line)
-            if "include {" in line:
-                line = fixinclude(
-                    line,
-                    loglevel,
-                    condapath,
-                    envpath,
-                    workflowpath,
-                    logfix,
-                    "nfmode",
-                )
-            subjobs.append(line)
-        subjobs.append("\n\n")
+    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+    subjobs.append("\n\n")
 
     nfi = os.path.abspath(os.path.join(workflowpath, "summary.nf"))
-    with open(nfi, "r") as nf:
-        for line in mu.comment_remover(nf.readlines()):
-            line = re.sub(condapath, 'conda "' + envpath, line)
-            if "include {" in line:
-                line = fixinclude(
-                    line,
-                    loglevel,
-                    condapath,
-                    envpath,
-                    workflowpath,
-                    logfix,
-                    "nfmode",
-                )
-            subjobs.append(line)
-        subjobs.append("\n\n")
+    subjobs.extend(_fill_template(nfi, loglevel, logfix, condapath, 'conda "' + envpath, nfmode=True))
+    subjobs.append("\n\n")
 
     nfo = os.path.abspath(os.path.join(subdir, "summary_subflow.nf"))
     _write_workflow(nfo, "".join(subjobs))
diff --git a/tests/data/config_DTU_test.json b/tests/data/config_DTU_test.json
new file mode 100644
index 00000000..6852e250
--- /dev/null
+++ b/tests/data/config_DTU_test.json
@@ -0,0 +1,128 @@
+{
+    "WORKFLOWS": "QC,TRIMMING,DEDUP,MAPPING,DTU",
+    "BINS": "",
+    "MAXTHREADS": "4",
+    "VERSION": "FIXME",
+    "SETTINGS": {
+        "Test": {
+            "SAMPLES": [
+                "SRR6357070",
+                "SRR6357071",
+                "SRR6357072"
+            ],
+            "SEQUENCING": "paired",
+            "REFERENCE": "GENOME/genome.fa.gz",
+            "DECOY": "",
+            "ANNOTATION": {
+                "GTF": "GENOME/genomic.gtf.gz",
+                "GFF": "GENOME/genomic.gff.gz"
+            },
+            "IP": "",
+            "GROUPS": [
+                "T1",
+                "T2",
+                "T3"
+            ],
+            "TYPES": "",
+            "BATCHES": "",
+            "INDEX": "",
+            "PREFIX": ""
+        }
+    },
+    "QC": {
+        "TOOLS": {
+            "fastqc": "fastqc"
+        },
+        "Test": {
+            "fastqc": {
+                "OPTIONS": {
+                    "QC": "",
+                    "MULTI": ""
+                }
+            }
+        }
+    },
+    "TRIMMING": {
+        "TOOLS": {
+            "trimgalore": "trim_galore"
+        },
+        "Test": {
+            "trimgalore": {
+                "OPTIONS": {
+                    "TRIM": "--length 8"
+                }
+            }
+        }
+    },
+    "DEDUP": {
+        "TOOLS": {
+            "picard": "picard"
+        },
+        "Test": {
+            "picard": {
+                "OPTIONS": {
+                    "DEDUP": ""
+                }
+            }
+        }
+    },
+    "MAPPING": {
+        "TOOLS": {
+            "star": "STAR"
+        },
+        "Test": {
+            "star": {
+                "OPTIONS": {
+                    "INDEX": "",
+                    "MAP": "--outSAMprimaryFlag AllBestScore",
+                    "EXTENSION": ""
+                }
+            }
+        }
+    },
+    "DTU": {
+        "TOOLS": {
+            "drimseq": "Analysis/DTU/DRIMSEQ.R",
+            "dexseq": "Analysis/DTU/DEXSEQ.R",
+            "spit": "Analysis/DTU/SPIT.py"
+        },
+        "CUTOFFS": {
+            "pvalue": "0.05",
+            "lfc": "1.5"
+        },
+        "COMPARABLE": {
+            "T1-VS-T2": [
+                [
+                    "T1"
+                ],
+                [
+                    "T2"
+                ]
+            ]
+        },
+        "EXCLUDE": [],
+        "Test": {
+            "drimseq": {
+                "OPTIONS": {
+                    "INDEX": "-d GENOMES/transcripts.fa.gz",
+                    "QUANT": "--seqBias --gcBias",
+                    "DTU": ""
+                }
+            },
+            "dexseq": {
+                "OPTIONS": {
+                    "INDEX": "-d GENOMES/transcripts.fa.gz",
+                    "QUANT": "--seqBias --gcBias",
+                    "DTU": ""
+                }
+            },
+            "spit": {
+                "OPTIONS": {
+                    "INDEX": "-d GENOMES/transcripts.fa.gz",
+                    "QUANT": "--seqBias --gcBias",
+                    "DTU": ""
+                }
+            }
+        }
+    }
+}
diff --git a/tests/golden/config_DTU/nf/_filelist.txt b/tests/golden/config_DTU/nf/_filelist.txt
new file mode 100644
index 00000000..985d7e01
--- /dev/null
+++ b/tests/golden/config_DTU/nf/_filelist.txt
@@ -0,0 +1,6 @@
+allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
+allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
+allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
new file mode 100644
index 00000000..7fa2c131
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/DEXSEQ.R",
+  "DTUENV": "dexseq_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
new file mode 100644
index 00000000..bc3b92bd
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
@@ -0,0 +1,296 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+BINS = get_always('BINS')
+DTUENV = get_always('DTUENV')
+DTUBIN = get_always('DTUBIN')
+DTUREF = get_always('DTUREF')
+DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+DTUANNO = get_always('DTUANNO')
+DTUDECOY = get_always('DTUDECOY')
+DTUIDX = get_always('DTUIDX')
+DTUUIDX = get_always('DTUUIDX')
+DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+IDXPARAMS = get_always('dexseq_DTU_params_INDEX') ?: ''
+COUNTPARAMS = get_always('dexseq_DTU_params_COUNT') ?: ''
+DTUPARAMS = get_always('dexseq_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('dexseq_DTU_params_TERMINUS') ?: ''
+DTUREPS = get_always('DTUREPS') ?: ''
+DTUCOMP = get_always('DTUCOMP') ?: ''
+DTUCOMPS = get_always('DTUCOMPS') ?: ''
+PVAL = get_always('DTUPVAL') ?: ''
+LFC = get_always('DTULFC') ?: ''
+PCOMBO = get_always('COMBO') ?: 'none'
+COUNTBIN = 'salmon'
+COUNTENV = 'salmon'
+process salmon_quant{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+    }
+    input:
+    path reads
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+            stranded = '-l ISF'
+        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+            stranded = '-l ISR'
+        }else{
+            stranded = '-l IU'
+        }
+        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
+        r1 = rs[0]
+        r2 = rs[1]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }else{
+        if (STRANDED == 'fr' || STRANDED == 'SF'){
+            stranded = '-l SF'
+        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+            stranded = '-l SR'
+        }else{
+            stranded = '-l U'
+        }
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }
+}
+process create_summary_snippet{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
+    }
+    input:
+    path de
+    output:
+    path "*.Rmd", emit: snps
+    path "log", emit: log
+    script:
+    inlist = de.toString()
+    """
+    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    """
+}
+process terminus_collapse{
+    conda "/envs/terminus.yaml"
+    container "oras://docker.io/jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+    input:
+    path counts
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+process salmon_idx{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/dexseq_index.log"
+        else if (filename == "dexseq.idx")            "$DTUIDX"
+        else                                          "$DTUUIDX"
+    }
+    input:
+    path genome
+    output:
+     path "$DTUUIDXNAME", emit: idx
+    path "*.log", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:    
+    gen =  genome.getName()
+    if (DTUDECOY){
+        decoy = "-d "+"${DTUDECOY}" 
+    }else{
+        decoy = ''
+    }
+    """
+    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME dexseq.idx
+    """
+}
+process prepare_dtu_annotation{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_dexseq_annotation.log"
+    }
+    output:
+    path "*.gz", emit: anno
+    path "*.log", emit: log
+    script:     
+    ca = COMBO+"_ANNOTATION.gz"
+    ol = "create_DTU_table.log"
+    """
+    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    """
+}
+process run_dexseq{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf(".html") > 0)      "DTU/${SCOMBO}/DEXSeqReport_${COMBO}_${DTUCOMP}/${file(filename).getName()}"
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_dexseq.log"
+    }
+    input:path counts
+    path anno
+    path ref
+    output:
+    path "*_table*", emit: tbls
+    path "*_figure*", emit: figs, optional:true
+    path "*.html", emit: html, optional:true
+    path "*SESSION.gz", emit: session
+    path "log", emit: log
+    script:    
+    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
+    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
+    comp = "${DTUCOMP}".split(':')[0]
+    dparams = "'${DTUPARAMS}'"
+    """
+    mkdir -p Figures Tables
+    Rscript --no-environ --no-restore --no-save $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    """
+}
+process collect_dexseq{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    input:
+    path de
+    script:
+    """
+    echo "$de DONE"
+    """
+}
+workflow DTU{ 
+    take: collection
+    main:
+    TRIMSAMPLES = LONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+        }
+    trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
+    annofile = Channel.fromPath(DTUANNO)
+    checkidx = file(DTUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(DTUUIDX)
+        if (PAIRED == 'paired'){
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
+        }        
+    }
+    else{
+        genomefile = Channel.fromPath(DTUREF)
+        salmon_idx(genomefile)
+        if (PAIRED == 'paired'){
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
+        }
+    }
+    prepare_dtu_annotation()
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_dexseq(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    create_summary_snippet(run_dexseq.out.tbls.concat(run_dexseq.out.figs.concat(run_dexseq.out.session)).collect())
+    collect_dexseq(run_dexseq.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
+    emit:
+    tbls = run_dexseq.out.tbls
+    figs = run_dexseq.out.figs
+    snps = create_summary_snippet.out.snps
+}
+
+
+
+workflow {
+    DTU(dummy)
+}
+
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
new file mode 100644
index 00000000..86f58007
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/DRIMSEQ.R",
+  "DTUENV": "drimseq_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
new file mode 100644
index 00000000..0e6684ea
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
@@ -0,0 +1,297 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+BINS = get_always('BINS')
+DTUENV = get_always('DTUENV')
+DTUBIN = get_always('DTUBIN')
+DTUREF = get_always('DTUREF')
+DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+DTUANNO = get_always('DTUANNO')
+DTUDECOY = get_always('DTUDECOY')
+DTUIDX = get_always('DTUIDX')
+DTUUIDX = get_always('DTUUIDX')
+DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+IDXPARAMS = get_always('drimseq_DTU_params_INDEX') ?: ''
+COUNTPARAMS = get_always('drimseq_DTU_params_COUNT') ?: ''
+DTUPARAMS = get_always('drimseq_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('drimseq_DTU_params_TERMINUS') ?: ''
+DTUREPS = get_always('DTUREPS') ?: ''
+DTUCOMP = get_always('DTUCOMP') ?: ''
+DTUCOMPS = get_always('DTUCOMPS') ?: ''
+PVAL = get_always('DTUPVAL') ?: ''
+LFC = get_always('DTULFC') ?: ''
+PCOMBO = get_always('COMBO') ?: 'none'
+COUNTBIN = 'salmon'
+COUNTENV = 'salmon'
+process salmon_quant{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+    }
+    input:
+    path reads
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+            stranded = '-l ISF'
+        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+            stranded = '-l ISR'
+        }else{
+            stranded = '-l IU'
+        }
+        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
+        r1 = rs[0]
+        r2 = rs[1]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }else{
+        if (STRANDED == 'fr' || STRANDED == 'SF'){
+            stranded = '-l SF'
+        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+            stranded = '-l SR'
+        }else{
+            stranded = '-l U'
+        }
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }
+}
+process create_summary_snippet{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
+    }
+    input:
+    path de
+    output:
+    path "*.Rmd", emit: snps
+    path "log", emit: log
+    script:
+    inlist = de.toString()
+    """
+    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    """
+}
+process terminus_collapse{
+    conda "/envs/terminus.yaml"
+    container "oras://docker.io/jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+    input:
+    path counts
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+process salmon_idx{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/drimseq_index.log"
+        else if (filename == "drimseq.idx")           "$DTUIDX"
+        else                                          "$DTUUIDX"
+    }
+    input:
+    path genome
+    output:
+     path "$DTUUIDXNAME", emit: idx
+    path "*.log", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:    
+    gen =  genome.getName()
+    if (DTUDECOY){
+        decoy = "-d "+"${DTUDECOY}" 
+    }else{
+        decoy = ''
+    }
+    """
+    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME drimseq.idx
+    """
+}
+process prepare_dtu_annotation{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_drimseq_annotation.log"
+    }
+    output:
+    path "*.gz", emit: anno
+    path "*.log", emit: log
+    script:     
+    ca = COMBO+"_ANNOTATION.gz"
+    ol = "create_DTU_table.log"
+    """
+    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    """
+}
+process run_drimseq{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf(".html") > 0)      "DTU/${SCOMBO}/drimseqReport_${COMBO}_${DTUCOMP}/${file(filename).getName()}"
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_drimseq.log"
+    }
+    input:
+    path counts
+    path anno
+    path ref
+    output:
+    path "*_table*", emit: tbls
+    path "*_figure*", emit: figs, optional:true
+    path "*.html", emit: html, optional:true
+    path "*SESSION.gz", emit: session
+    path "log", emit: log
+    script:    
+    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
+    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
+    comp = "${DTUCOMP}".split(':')[0]
+    dparams = "'${DTUPARAMS}'"
+    """
+    mkdir -p Figures Tables
+    Rscript --no-environ --no-restore --no-save $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    """
+}
+process collect_drimseq{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    input:
+    path de
+    script:
+    """
+    echo "$de DONE"
+    """
+}
+workflow DTU{ 
+    take: collection
+    main:
+    TRIMSAMPLES = LONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+        }
+    trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
+    annofile = Channel.fromPath(DTUANNO)
+    checkidx = file(DTUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(DTUUIDX)
+        if (PAIRED == 'paired'){
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
+        }        
+    }
+    else{
+        genomefile = Channel.fromPath(DTUREF)
+        salmon_idx(genomefile)
+        if (PAIRED == 'paired'){
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
+        }
+    }
+    prepare_dtu_annotation()
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_drimseq(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    create_summary_snippet(run_drimseq.out.tbls.concat(run_drimseq.out.figs.concat(run_drimseq.out.session)).collect())
+    collect_drimseq(run_drimseq.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
+    emit:
+    tbls = run_drimseq.out.tbls
+    figs = run_drimseq.out.figs
+    snps = create_summary_snippet.out.snps
+}
+
+
+
+workflow {
+    DTU(dummy)
+}
+
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
new file mode 100644
index 00000000..db11a2f9
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/SPIT.py",
+  "DTUENV": "spit_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
new file mode 100644
index 00000000..a68c9776
--- /dev/null
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
@@ -0,0 +1,296 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+BINS = get_always('BINS')
+DTUENV = get_always('DTUENV')
+DTUBIN = get_always('DTUBIN')
+DTUREF = get_always('DTUREF')
+DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+DTUANNO = get_always('DTUANNO')
+DTUDECOY = get_always('DTUDECOY')
+DTUIDX = get_always('DTUIDX')
+DTUUIDX = get_always('DTUUIDX')
+DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+IDXPARAMS = get_always('spit_DTU_params_INDEX') ?: ''
+COUNTPARAMS = get_always('spit_DTU_params_COUNT') ?: ''
+DTUPARAMS = get_always('spit_DTU_params_DTU') ?: ''
+RUNTERMINUS = get_always('spit_DTU_params_TERMINUS') ?: ''
+DTUREPS = get_always('DTUREPS') ?: ''
+DTUCOMP = get_always('DTUCOMP') ?: ''
+DTUCOMPS = get_always('DTUCOMPS') ?: ''
+PVAL = get_always('DTUPVAL') ?: ''
+LFC = get_always('DTULFC') ?: ''
+PCOMBO = get_always('COMBO') ?: 'none'
+COUNTBIN = 'salmon'
+COUNTENV = 'salmon'
+process salmon_quant{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+    }
+    input:
+    path reads
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+            stranded = '-l ISF'
+        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+            stranded = '-l ISR'
+        }else{
+            stranded = '-l IU'
+        }
+        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
+        r1 = rs[0]
+        r2 = rs[1]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }else{
+        if (STRANDED == 'fr' || STRANDED == 'SF'){
+            stranded = '-l SF'
+        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+            stranded = '-l SR'
+        }else{
+            stranded = '-l U'
+        }
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }
+}
+process create_summary_snippet{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
+    }
+    input:
+    path de
+    output:
+    path "*.Rmd", emit: snps
+    path "log", emit: log
+    script:
+    inlist = de.toString()
+    """
+    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    """
+}
+process terminus_collapse{
+    conda "/envs/terminus.yaml"
+    container "oras://docker.io/jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+    input:
+    path counts
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
+process salmon_idx{
+    conda "/envs/$COUNTENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/spit_index.log"
+        else if (filename == "spit.idx")           "$DTUIDX"
+        else                                          "$DTUUIDX"
+    }
+    input:
+    path genome
+    output:
+     path "$DTUUIDXNAME", emit: idx
+    path "*.log", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:    
+    gen =  genome.getName()
+    if (DTUDECOY){
+        decoy = "-d "+"${DTUDECOY}" 
+    }else{
+        decoy = ''
+    }
+    """
+    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME spit.idx
+    """
+}
+process prepare_dtu_annotation{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_spit_annotation.log"
+    }
+    output:
+    path "*.gz", emit: anno
+    path "*.log", emit: log
+    script:     
+    ca = COMBO+"_ANNOTATION.gz"
+    ol = "create_DTU_table.log"
+    """
+    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    """
+}
+process run_spit{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_spit.log"
+    }
+    input:
+    path counts
+    path anno
+    path ref
+    output:
+    path "*_table*", emit: tbls
+    path "*_figure*", emit: figs, optional:true
+    path "*SESSION.gz", emit: session
+    path "log", emit: log
+    script:    
+    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
+    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
+    comp = "${DTUCOMP}".split(':')[0]
+    dparams = "'${DTUPARAMS}'"
+    """
+    mkdir -p Figures Tables
+    python3 $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    """
+}
+process collect_spit{
+    conda "/envs/$DTUENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    input:
+    path de
+    script:
+    """
+    echo "$de DONE"
+    """
+}
+workflow DTU{ 
+    take: collection
+    main:
+    TRIMSAMPLES = LONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+        }
+    trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
+    annofile = Channel.fromPath(DTUANNO)
+    checkidx = file(DTUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(DTUUIDX)
+        if (PAIRED == 'paired'){
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
+        }        
+    }
+    else{
+        genomefile = Channel.fromPath(DTUREF)
+        salmon_idx(genomefile)
+        if (PAIRED == 'paired'){
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
+        } else{
+            salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
+        }
+    }
+    prepare_dtu_annotation()
+    if (RUNTERMINUS?.length() > 0){
+        terminus_collapse(salmon_quant.out.counts.collect())
+        dtucounts = terminus_collapse.out.counts
+    }
+    else{
+        dtucounts = salmon_quant.out.counts
+    }
+    run_spit(dtucounts.collect(), prepare_dtu_annotation.out.anno, annofile)
+    create_summary_snippet(run_spit.out.tbls.concat(run_spit.out.figs.concat(run_spit.out.session)).collect())
+    collect_spit(run_spit.out.tbls.collect().concat(create_summary_snippet.out.snps.collect()))
+    emit:
+    tbls = run_spit.out.tbls
+    figs = run_spit.out.figs
+    snps = create_summary_snippet.out.snps
+}
+
+
+
+
+workflow {
+    DTU(dummy)
+}
+
diff --git a/tests/golden/config_DTU/smk/_filelist.txt b/tests/golden/config_DTU/smk/_filelist.txt
new file mode 100644
index 00000000..69709e2e
--- /dev/null
+++ b/tests/golden/config_DTU/smk/_filelist.txt
@@ -0,0 +1,6 @@
+allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subsnake.smk
+allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subsnake.smk
+allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_DTU_spit_subsnake.smk
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
new file mode 100644
index 00000000..7fa2c131
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/DEXSEQ.R",
+  "DTUENV": "dexseq_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subsnake.smk b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subsnake.smk
new file mode 100644
index 00000000..acaeaab8
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subsnake.smk
@@ -0,0 +1,482 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-star/dexseq_DTU'
+
+scombo = 'fastqc-trimgalore-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    scombo = scombo,
+    read = "R1|R2",
+    type = "sorted|sorted_unique" if not rundedup else "sorted|sorted_unique|sorted_dedup|sorted_unique_dedup"
+
+logid = 'dexseq_DTU.smk '
+DTUBIN, DTUENV = env_bin_from_config(config,'DTU')
+log.debug(logid+"DTUENV: "+str(DTUENV))
+COUNTBIN, COUNTENV = ['salmon','salmon'] #env_bin_from_config(config,'COUNTING') ##PINNING subreads package to version 1.6.4 due to changes in 2.0.1 gene_id length cutoff that interfers
+comparison = comparable_as_string(config,'DTU')
+compstr = [i.split(":")[0] for i in comparison.split(",")]
+usededup = config.get('RUNDEDUP', False)
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = DTUENV
+unik = get_dict_hash(keydict)
+rule themall:
+    input:  session = expand("DTU/{combo}/DTU_DEXSEQ_{scombo}_SESSION.gz", combo=combo, scombo=scombo),
+            res     = expand("DTU/{combo}/Tables/DTU_DEXSEQ_{scombo}_{comparison}_table_results.tsv.gz", combo=combo, scombo=scombo, comparison=compstr),
+            # sig     = expand("DTU/{combo}/Tables/Sig_DTU_DEXSEQ_{scombo}_{comparison}_results.tsv.gz", combo=combo, scombo=scombo, comparison=compstr),
+            # sig_d   = expand("DTU/{combo}/Tables/SigDOWN_DTU_DEXSEQ_{scombo}_{comparison}_results.tsv.gz", combo=combo, scombo=scombo, comparison=compstr),
+            # sig_u   = expand("DTU/{combo}/Tables/SigUP_DTU_DEXSEQ_{scombo}_{comparison}_results.tsv.gz", combo=combo, scombo=scombo, comparison=compstr),
+            Rmd     = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
+rule salmon_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
+    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: mapp = COUNTBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
+            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
+if paired == 'paired':
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{scombo}/{file}_R2_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
+                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
+        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
+else:
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
+        shell:  "ln -s {params.filetolink} {output.r1}"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
+                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+else:
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+rule create_annotation_table:
+    input:  dir  = ctsource,
+    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
+    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: 1
+    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
+            bins = BINS
+    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+rule run_DTU:
+    input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)
+    output: session = expand(rules.themall.input.session, combo=combo, scombo=scombo),
+            res = rules.themall.input.res
+    log:    expand("LOGS/DTU/{combo}_{scombo}_{comparison}/run_DTU.log", combo=combo, scombo=scombo, comparison=compstr)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: 1  # Due to BPPARAM errors, else int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins   = str.join(os.sep,[BINS, DTUBIN]),
+            compare = comparison,
+            pcombo = scombo if scombo != '' else 'none',
+            outdir = 'DTU/'+combo,
+            ref = os.path.abspath(ANNOTATION),
+            dtuopt = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('DTU', "")
+            # pv_cut = get_cutoff_as_string(config, 'DTU', 'pval'),
+            # lfc_cut = get_cutoff_as_string(config, 'DTU', 'lfc')
+    shell: "Rscript --no-environ --no-restore --no-save {params.bins} {input.anno} {params.ref} {params.outdir} {params.compare} {params.pcombo} {threads} \'{params.dtuopt}\' 2> {log}"
+# rule filter_significant:
+#     input:  res = rules.run_DTU.output.res
+#     output: sig   = rules.themall.input.sig,
+#             sig_d = rules.themall.input.sig_d,
+#             sig_u = rules.themall.input.sig_u
+#     log:    expand("LOGS/DTU/{combo}_{scombo}_{comparison}/filter_drimseq.log", combo=combo, scombo=scombo, comparison=compstr)
+#     conda:  "/envs/"+DTUENV+".yaml"
+#     threads: 1
+#     params: pv_cut = get_cutoff_as_string(config, 'DTU', 'pval'),
+#             lfc_cut = get_cutoff_as_string(config, 'DTU', 'lfc')
+#     shell: "set +o pipefail; for i in {input};do fn=\"${{i##*/}}\"; if [[ -s \"$i\" ]]; then zcat $i| grep -v -w 'NA'|perl -F\'\\t\' -wlane ' next if (!$F[1] || !$F[2]);if ($F[1] =~ /adj_pvalue/ || $F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut})){{print}}' |gzip > DTU/{combo}/Sig_$fn && zcat $i| grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[1] || !$F[2]);if ($F[1] =~ /adj_pvalue/ || $F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut})){{print}}' |gzip > DTU/{combo}/SigUP_$fn && zcat $i| grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[1] || !$F[2]);if ($F[1] =~ /adj_pvalue/ || $F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut})){{print}}' |gzip > DTU/{combo}/Sig_$fn; else touch DTU/{combo}/Sig_$fn DTU/{combo}/SigUP_$fn DTU/{combo}/SigDOWN_$fn; fi; done 2> {log}"
+rule create_summary_snippet:
+    input:  rules.run_DTU.output.res,
+            rules.themall.input.session
+            # rules.filter_significant.output.sig,
+            # rules.filter_significant.output.sig_d,
+            # rules.filter_significant.output.sig_u,
+    output: rules.themall.input.Rmd
+    log:    expand("LOGS/DTU/{combo}create_summary_snippet.log", combo=combo)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins = BINS,
+            abspathfiles = lambda w, input: [os.path.abspath(x) for x in input]
+    shell:  "python3 {params.bins}/Analysis/RmdCreator.py --files {params.abspathfiles} --output {output} --env {DTUENV} --loglevel DEBUG 2>> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
new file mode 100644
index 00000000..86f58007
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/DRIMSEQ.R",
+  "DTUENV": "drimseq_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subsnake.smk b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subsnake.smk
new file mode 100644
index 00000000..c13e2857
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subsnake.smk
@@ -0,0 +1,511 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-star/drimseq_DTU'
+
+scombo = 'fastqc-trimgalore-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    scombo = scombo,
+    read = "R1|R2",
+    type = "sorted|sorted_unique" if not rundedup else "sorted|sorted_unique|sorted_dedup|sorted_unique_dedup"
+
+logid = 'drimseq_DTU.smk '
+DTUBIN, DTUENV = env_bin_from_config(config,'DTU')
+log.debug(logid+"DTUENV: "+str(DTUENV))
+COUNTBIN, COUNTENV = ['salmon','salmon']#env_bin_from_config(SAMPLES, config,'COUNTING') ##PINNING subreads package to version 1.6.4 due to changes in 2.0.1 gene_id length cutoff that interfers
+comparison = comparable_as_string(config,'DTU')
+compstr = [i.split(":")[0] for i in comparison.split(",")]
+usededup = config.get('RUNDEDUP', False)
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = DTUENV
+unik = get_dict_hash(keydict)
+rule themall:
+    input:  session = expand("DTU/{combo}/DTU_DRIMSEQ_{scombo}_SESSION.gz", combo=combo, scombo=scombo),
+            res_t   = expand("DTU/{combo}/Tables/DTU_DRIMSEQ_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            res_g   = expand("DTU/{combo}/Tables/DTU_DRIMSEQ_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            res_p   = expand("DTU/{combo}/Tables/DTU_DRIMSEQ_{scombo}_{comparison}_table_proportions.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            res_gwp = expand("DTU/{combo}/Tables/DTU_DRIMSEQ_{scombo}_{comparison}_table_genewise-precision.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            fig_F   = expand("DTU/{combo}/Figures/DTU_DRIMSEQ_{scombo}_{comparison}_figure_FeatPerGene.png", combo=combo, comparison=compstr, scombo=scombo),
+            fig_P   = expand("DTU/{combo}/Figures/DTU_DRIMSEQ_{scombo}_{comparison}_figure_Precision.png", combo=combo, comparison=compstr, scombo=scombo),
+            fig_PV  = expand("DTU/{combo}/Figures/DTU_DRIMSEQ_{scombo}_{comparison}_figure_PValues.png", combo=combo, comparison=compstr, scombo=scombo),
+            fig_files = expand("DTU/{combo}/Figures/DTU_DRIMSEQ_{scombo}_{comparison}_list_sig{feature}Figures.tsv", combo=combo, comparison=compstr, scombo=scombo, feature=["Gene","Transcript"]),
+            sig_t    = expand("DTU/{combo}/Tables/Sig_DTU_DRIMSEQ_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_dt   = expand("DTU/{combo}/Tables/SigDOWN_DTU_DRIMSEQ_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_ut   = expand("DTU/{combo}/Tables/SigUP_DTU_DRIMSEQ_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_g    = expand("DTU/{combo}/Tables/Sig_DTU_DRIMSEQ_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_dg   = expand("DTU/{combo}/Tables/SigDOWN_DTU_DRIMSEQ_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_ug   = expand("DTU/{combo}/Tables/SigUP_DTU_DRIMSEQ_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            Rmd = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
+rule salmon_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
+    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: mapp = COUNTBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
+            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
+if paired == 'paired':
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{scombo}/{file}_R2_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
+                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
+        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
+else:
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
+        shell:  "ln -s {params.filetolink} {output.r1}"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
+                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+else:
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+rule create_annotation_table:
+    input:  dir  = ctsource,
+    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
+    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: 1
+    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
+            bins = BINS
+    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+rule run_DTU:
+    input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)
+    output: session = rules.themall.input.session,
+            res_t   = rules.themall.input.res_t,
+            res_g   = rules.themall.input.res_g,
+            res_p   = rules.themall.input.res_p,
+            res_gwp = rules.themall.input.res_gwp,
+            fig_F   = rules.themall.input.fig_F,
+            fig_P   = rules.themall.input.fig_P,
+            fig_PV  = rules.themall.input.fig_PV,
+            fig_files = rules.themall.input.fig_files
+    log:    expand("LOGS/DTU/{combo}/run_DTU.log", combo=combo)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins   = str.join(os.sep,[BINS, DTUBIN]),
+            compare = comparison,
+            pcombo = scombo if scombo != '' else 'none',
+            outdir = 'DTU/'+combo,
+            ref = os.path.abspath(ANNOTATION),
+            dtuopt = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('DTU', "")
+    shell: "Rscript --no-environ --no-restore --no-save {params.bins} {input.anno} {params.ref} {params.outdir} {params.compare} {params.pcombo} {threads} \'{params.dtuopt}\' 2> {log}"
+rule filter_significant_drimseq:
+    input:  res_g = rules.themall.input.res_g,
+            res_t = rules.themall.input.res_t
+    output: sig_g   = rules.themall.input.sig_g,
+            sig_dg  = rules.themall.input.sig_dg,
+            sig_ug  = rules.themall.input.sig_ug,
+            sig_t   = rules.themall.input.sig_t,
+            sig_dt  = rules.themall.input.sig_dt,
+            sig_ut  = rules.themall.input.sig_ut
+    log:    "LOGS/DTU/filter_drimseqDTU.log"
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: 1
+    params: pv_cut = get_cutoff_as_string(config, 'DTU', 'pvalue'),
+            lfc_cut = get_cutoff_as_string(config, 'DTU', 'lfc')
+    shell:  "set +o pipefail; arr=({input.res_g}); arrt=({input.res_t}); orr=({output.sig_g}); orrd=({output.sig_dg}); orru=({output.sig_ug}); orrt=({output.sig_t}); orrtd=({output.sig_dt}); orrtu=({output.sig_ut}); for i in \"${{!arr[@]}}\"; do a=\"${{arr[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orr[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orrd[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orru[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orr[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orru[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrd[$i]}}\"; else touch \"${{orr[$i]}}\" \"${{orrd[$i]}}\" \"${{orru[$i]}}\"; fi;done; for i in \"${{!arrt[@]}}\"; do a=\"${{arrt[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orrt[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtu[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrt[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtu[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtd[$i]}}\"; else touch \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\" \"${{orrtu[$i]}}\"; fi;done 2> {log}"
+rule create_summary_snippet:
+    input:  rules.run_DTU.output.res_t,
+            rules.run_DTU.output.res_g,
+            rules.run_DTU.output.res_p,
+            rules.run_DTU.output.fig_F,
+            rules.run_DTU.output.fig_P,
+            rules.run_DTU.output.fig_PV,
+            rules.run_DTU.output.fig_files,
+            rules.themall.input.sig_g,
+            rules.themall.input.sig_dg,
+            rules.themall.input.sig_ug,
+            rules.themall.input.sig_t,
+            rules.themall.input.sig_dt,
+            rules.themall.input.sig_ut,
+            rules.themall.input.session
+    output: rules.themall.input.Rmd
+    log:    expand("LOGS/DTU/{combo}/create_summary_snippet.log", combo=combo)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins = BINS,
+            abspathfiles = lambda w, input: [os.path.abspath(x) for x in input]
+    shell:  "python3 {params.bins}/Analysis/RmdCreator.py --files {params.abspathfiles} --output {output} --env {DTUENV} --loglevel DEBUG 2>> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
new file mode 100644
index 00000000..db11a2f9
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subconfig.json
@@ -0,0 +1,77 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DTU": {
+    "COMPARABLE": {
+      "T1-VS-T2": [
+        [
+          "T1"
+        ],
+        [
+          "T2"
+        ]
+      ]
+    },
+    "CUTOFFS": {
+      "lfc": "1.5",
+      "pvalue": "0.05"
+    },
+    "Test": {
+      "dexseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "drimseq": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      },
+      "spit": {
+        "OPTIONS": {
+          "DTU": "",
+          "INDEX": "-d GENOMES/transcripts.fa.gz",
+          "QUANT": "--seqBias --gcBias"
+        }
+      }
+    }
+  },
+  "DTUBIN": "Analysis/DTU/SPIT.py",
+  "DTUENV": "spit_DTU",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subsnake.smk b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subsnake.smk
new file mode 100644
index 00000000..f2ae1387
--- /dev/null
+++ b/tests/golden/config_DTU/smk/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subsnake.smk
@@ -0,0 +1,503 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-star/spit_DTU'
+
+scombo = 'fastqc-trimgalore-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    scombo = scombo,
+    read = "R1|R2",
+    type = "sorted|sorted_unique" if not rundedup else "sorted|sorted_unique|sorted_dedup|sorted_unique_dedup"
+
+logid = 'spit_DTU.smk '
+DTUBIN, DTUENV = env_bin_from_config(config,'DTU')
+log.debug(logid+"DTUENV: "+str(DTUENV))
+COUNTBIN, COUNTENV = ['salmon','salmon']#env_bin_from_config(SAMPLES, config,'COUNTING') ##PINNING subreads package to version 1.6.4 due to changes in 2.0.1 gene_id length cutoff that interfers
+comparison = comparable_as_string(config,'DTU')
+compstr = [i.split(":")[0] for i in comparison.split(",")]
+usededup = config.get('RUNDEDUP', False)
+TERMINUSENV = 'terminus'
+termpara = tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('TERMINUS', None)
+runterminus = termpara is not None
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = DTUENV
+unik = get_dict_hash(keydict)
+rule themall:
+    input:  session = expand("DTU/{combo}/DTU_SPIT_{scombo}_SESSION.gz", combo=combo, scombo=scombo),
+            res_t   = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            res_g   = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            res_i   = expand("DTU/{combo}/Tables/DTU_SPIT_{scombo}_{comparison}_table_ifs.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            fig_PV  = expand("DTU/{combo}/Figures/DTU_SPIT_{scombo}_{comparison}_figure_PValues.png", combo=combo, comparison=compstr, scombo=scombo),
+            fig_W   = expand("DTU/{combo}/Figures/DTU_SPIT_{scombo}_{comparison}_figure_Wasserstein.png", combo=combo, comparison=compstr, scombo=scombo),
+            sig_t    = expand("DTU/{combo}/Tables/Sig_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_dt   = expand("DTU/{combo}/Tables/SigDOWN_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_ut   = expand("DTU/{combo}/Tables/SigUP_DTU_SPIT_{scombo}_{comparison}_table_transcripts.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_g    = expand("DTU/{combo}/Tables/Sig_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_dg   = expand("DTU/{combo}/Tables/SigDOWN_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            sig_ug   = expand("DTU/{combo}/Tables/SigUP_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
+            Rmd = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
+rule salmon_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
+    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: mapp = COUNTBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
+            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
+if paired == 'paired':
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{scombo}/{file}_R2_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
+                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
+        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
+else:
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
+        shell:  "ln -s {params.filetolink} {output.r1}"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
+                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+else:
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  "/envs/"+COUNTENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  "/envs/"+TERMINUSENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+rule create_annotation_table:
+    input:  dir  = ctsource,
+    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
+    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
+    conda:  "/envs/"+COUNTENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+COUNTENV+""
+    threads: 1
+    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
+            bins = BINS
+    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+rule run_DTU:
+    input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)
+    output: session = rules.themall.input.session,
+            res_t   = rules.themall.input.res_t,
+            res_g   = rules.themall.input.res_g,
+            res_i   = rules.themall.input.res_i,
+            fig_PV  = rules.themall.input.fig_PV,
+            fig_W   = rules.themall.input.fig_W
+    log:    expand("LOGS/DTU/{combo}/run_DTU.log", combo=combo)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins   = str.join(os.sep,[BINS, DTUBIN]),
+            compare = comparison,
+            pcombo = scombo if scombo != '' else 'none',
+            outdir = 'DTU/'+combo,
+            ref = os.path.abspath(ANNOTATION),
+            dtuopt = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('DTU', "")
+    shell: "python3 {params.bins} {input.anno} {params.ref} {params.outdir} {params.compare} {params.pcombo} {threads} \'{params.dtuopt}\' 2> {log}"
+rule filter_significant_spit:
+    input:  res_g = rules.themall.input.res_g,
+            res_t = rules.themall.input.res_t
+    output: sig_g   = rules.themall.input.sig_g,
+            sig_dg  = rules.themall.input.sig_dg,
+            sig_ug  = rules.themall.input.sig_ug,
+            sig_t   = rules.themall.input.sig_t,
+            sig_dt  = rules.themall.input.sig_dt,
+            sig_ut  = rules.themall.input.sig_ut
+    log:    "LOGS/DTU/filter_spitDTU.log"
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: 1
+    params: pv_cut = get_cutoff_as_string(config, 'DTU', 'pvalue'),
+            lfc_cut = get_cutoff_as_string(config, 'DTU', 'lfc')
+    shell:  "set +o pipefail; arr=({input.res_g}); arrt=({input.res_t}); orr=({output.sig_g}); orrd=({output.sig_dg}); orru=({output.sig_ug}); orrt=({output.sig_t}); orrtd=({output.sig_dt}); orrtu=({output.sig_ut}); for i in \"${{!arr[@]}}\"; do a=\"${{arr[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orr[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orrd[$i]}}\"; cp \"${{orr[$i]}}\" \"${{orru[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orr[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orru[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrd[$i]}}\"; else touch \"${{orr[$i]}}\" \"${{orrd[$i]}}\" \"${{orru[$i]}}\"; fi;done; for i in \"${{!arrt[@]}}\"; do a=\"${{arrt[$i]}}\"; fn=\"${{a##*/}}\"; if [[ -s \"$a\" ]];then zcat $a| head -n1 |gzip > \"${{orrt[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\"; cp \"${{orrt[$i]}}\" \"${{orrtu[$i]}}\"; zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut} ||$F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrt[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] >= {params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtu[$i]}}\" && zcat $a| tail -n+2 |grep -v -w 'NA'|perl -F\'\\t\' -wlane 'next if (!$F[2] || !$F[1]);if ($F[1] < {params.pv_cut} && ($F[2] <= -{params.lfc_cut}) ){{print}}' |gzip >> \"${{orrtd[$i]}}\"; else touch \"${{orrt[$i]}}\" \"${{orrtd[$i]}}\" \"${{orrtu[$i]}}\"; fi;done 2> {log}"
+rule create_summary_snippet:
+    input:  rules.run_DTU.output.res_t,
+            rules.run_DTU.output.res_g,
+            rules.run_DTU.output.res_i,
+            rules.run_DTU.output.fig_PV,
+            rules.run_DTU.output.fig_W,
+            rules.themall.input.sig_g,
+            rules.themall.input.sig_dg,
+            rules.themall.input.sig_ug,
+            rules.themall.input.sig_t,
+            rules.themall.input.sig_dt,
+            rules.themall.input.sig_ut,
+            rules.themall.input.session
+    output: rules.themall.input.Rmd
+    log:    expand("LOGS/DTU/{combo}/create_summary_snippet.log", combo=combo)
+    conda:  "/envs/"+DTUENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DTUENV+""
+    threads: int(MAXTHREAD-1) if int(MAXTHREAD-1) >= 1 else 1
+    params: bins = BINS,
+            abspathfiles = lambda w, input: [os.path.abspath(x) for x in input]
+    shell:  "python3 {params.bins}/Analysis/RmdCreator.py --files {params.abspathfiles} --output {output} --env {DTUENV} --loglevel DEBUG 2>> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subconfig.json
new file mode 100644
index 00000000..49c01b6e
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": ""
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "bbduk": "bbduk.sh"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "bbduk.sh",
+  "TRIMMINGENV": "bbduk"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
new file mode 100644
index 00000000..f74d4542
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
@@ -0,0 +1,554 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN $TRIMPARAMS t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
+        """
+    }
+    else{
+        o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"        
+        r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN $TRIMPARAMS t=${task.cpus} in=$r1 out=$o &> $r 
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+MAPPARAMS = get_always('bwa_params_MAP') ?: ''
+IDXBIN = MAPBIN.split('_')[0]
+MAPBIN = MAPBIN.replace('_', ' ')
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process bwa_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "bwa.idx")                          "$MAPIDX"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
+        else                                                "$MAPUIDX"
+    }
+    input:
+    path genome
+    output:
+    path "bwa.idx", emit: idx
+    path "bwa_*", emit: bwidx
+    script:
+    gen =  genome.getName()
+    """
+    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    """
+}
+process bwa_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*.sam.gz", emit: maps
+    path "*fastq.gz", includeInputs:false, emit: unmapped
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf1 = fn+"_R1_unmapped.fastq.gz"
+        uf2 = fn+"_R2_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        """
+    }else{
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf = fn+"_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        """
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        bwa_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        bwa_idx(genomefile)
+        bwa_mapping(bwa_idx.out.bwidx.combine(collection))
+    }
+    emit:
+    mapped  = bwa_mapping.out.maps
+    logs = bwa_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subconfig.json
new file mode 100644
index 00000000..8311d49f
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": "--outSAMprimaryFlag AllBestScore"
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "bbduk": "bbduk.sh"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "bbduk.sh",
+  "TRIMMINGENV": "bbduk"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
new file mode 100644
index 00000000..6f38d6fa
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
@@ -0,0 +1,584 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN $TRIMPARAMS t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
+        """
+    }
+    else{
+        o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"        
+        r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN $TRIMPARAMS t=${task.cpus} in=$r1 out=$o &> $r 
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('star_params_INDEX') ?: ''
+MAPPARAMS = get_always('star_params_MAP') ?: ''
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process star_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
+        else                                             "$MAPUIDX"
+    }
+    input:
+    path genome
+    path anno
+    output:
+    path "$MAPUIDXNAME", emit: idx
+    path "*.out", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:
+    gen =  genome.getName()
+    an  = anno.getName()
+    """
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    """
+}
+process star_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+    }
+    input:
+    path reads
+    output:
+    path "*_mapped.sam.gz", emit: maps
+    path "*.out", emit: outs
+    path "*.log", emit: logs
+    path "*Log*.out", emit: xtralogs
+    path "*.tab", emit: sjtab
+    path "*_unmapped.fastq.gz", includeInputs:false, emit: unmapped
+    script:
+    idx = reads[0]
+    idxdir = idx.toRealPath()
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        a = "Trimming_report.txt"
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        of = fn+'.Aligned.out.sam'
+        gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+        """
+        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        """
+    }
+    else{
+        if (PAIRED != 'singlecell'){
+            read = reads[1]
+            fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
+            of = fn+'Aligned.out.sam'
+            gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            """
+        }
+        else{
+            if (STRANDED == 'fr'){
+                stranded = '--soloStrand Forward'
+            }else if (STRANDED == 'rf'){
+                stranded = '--soloStrand Reverse'
+            }else{
+                stranded = '--soloStrand Unstranded'
+            }
+            r1 = reads[1]
+            fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+                t = r2
+                r2 = r1
+                r1 = t
+            }
+            of = fn+'.Aligned.sortedByCoord.out.bam'
+            gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            """
+        }
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        star_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        annofile = Channel.fromPath(MAPANNO)
+        star_idx(genomefile, annofile)
+        star_mapping(star_idx.out.idx.combine(collection))
+    }
+    emit:
+    mapped  = star_mapping.out.maps
+    logs = star_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subconfig.json
new file mode 100644
index 00000000..7b107b69
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": ""
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "cutadapt": "cutadapt"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "cutadapt",
+  "TRIMMINGENV": "cutadapt"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
new file mode 100644
index 00000000..b4b87dfe
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
@@ -0,0 +1,554 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o -p $p $r1 $r2 &> $r
+        """
+    }
+    else{
+        o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o $reads &> $r
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+MAPPARAMS = get_always('bwa_params_MAP') ?: ''
+IDXBIN = MAPBIN.split('_')[0]
+MAPBIN = MAPBIN.replace('_', ' ')
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process bwa_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "bwa.idx")                          "$MAPIDX"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
+        else                                                "$MAPUIDX"
+    }
+    input:
+    path genome
+    output:
+    path "bwa.idx", emit: idx
+    path "bwa_*", emit: bwidx
+    script:
+    gen =  genome.getName()
+    """
+    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    """
+}
+process bwa_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*.sam.gz", emit: maps
+    path "*fastq.gz", includeInputs:false, emit: unmapped
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf1 = fn+"_R1_unmapped.fastq.gz"
+        uf2 = fn+"_R2_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        """
+    }else{
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf = fn+"_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        """
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        bwa_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        bwa_idx(genomefile)
+        bwa_mapping(bwa_idx.out.bwidx.combine(collection))
+    }
+    emit:
+    mapped  = bwa_mapping.out.maps
+    logs = bwa_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subconfig.json
new file mode 100644
index 00000000..5aac3911
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": "--outSAMprimaryFlag AllBestScore"
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "cutadapt": "cutadapt"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "cutadapt",
+  "TRIMMINGENV": "cutadapt"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
new file mode 100644
index 00000000..5f91ce8f
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
@@ -0,0 +1,584 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o -p $p $r1 $r2 &> $r
+        """
+    }
+    else{
+        o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
+        r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
+        """
+        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o $reads &> $r
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('star_params_INDEX') ?: ''
+MAPPARAMS = get_always('star_params_MAP') ?: ''
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process star_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
+        else                                             "$MAPUIDX"
+    }
+    input:
+    path genome
+    path anno
+    output:
+    path "$MAPUIDXNAME", emit: idx
+    path "*.out", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:
+    gen =  genome.getName()
+    an  = anno.getName()
+    """
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    """
+}
+process star_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+    }
+    input:
+    path reads
+    output:
+    path "*_mapped.sam.gz", emit: maps
+    path "*.out", emit: outs
+    path "*.log", emit: logs
+    path "*Log*.out", emit: xtralogs
+    path "*.tab", emit: sjtab
+    path "*_unmapped.fastq.gz", includeInputs:false, emit: unmapped
+    script:
+    idx = reads[0]
+    idxdir = idx.toRealPath()
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        a = "Trimming_report.txt"
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        of = fn+'.Aligned.out.sam'
+        gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+        """
+        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        """
+    }
+    else{
+        if (PAIRED != 'singlecell'){
+            read = reads[1]
+            fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
+            of = fn+'Aligned.out.sam'
+            gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            """
+        }
+        else{
+            if (STRANDED == 'fr'){
+                stranded = '--soloStrand Forward'
+            }else if (STRANDED == 'rf'){
+                stranded = '--soloStrand Reverse'
+            }else{
+                stranded = '--soloStrand Unstranded'
+            }
+            r1 = reads[1]
+            fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+                t = r2
+                r2 = r1
+                r1 = t
+            }
+            of = fn+'.Aligned.sortedByCoord.out.bam'
+            gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            """
+        }
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        star_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        annofile = Channel.fromPath(MAPANNO)
+        star_idx(genomefile, annofile)
+        star_mapping(star_idx.out.idx.combine(collection))
+    }
+    emit:
+    mapped  = star_mapping.out.maps
+    logs = star_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subconfig.json
new file mode 100644
index 00000000..be57a0c5
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": ""
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "trimgalore": "trim_galore"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "trim_galore",
+  "TRIMMINGENV": "trimgalore"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
new file mode 100644
index 00000000..8188d76a
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
@@ -0,0 +1,549 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        """
+        $TRIMBIN --cores ${task.cpus} --paired --gzip $TRIMPARAMS $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        """
+    }
+    else{
+        """
+        $TRIMBIN --cores ${task.cpus} --gzip $TRIMPARAMS $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+MAPPARAMS = get_always('bwa_params_MAP') ?: ''
+IDXBIN = MAPBIN.split('_')[0]
+MAPBIN = MAPBIN.replace('_', ' ')
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process bwa_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename == "bwa.idx")                          "$MAPIDX"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
+        else                                                "$MAPUIDX"
+    }
+    input:
+    path genome
+    output:
+    path "bwa.idx", emit: idx
+    path "bwa_*", emit: bwidx
+    script:
+    gen =  genome.getName()
+    """
+    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    """
+}
+process bwa_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+        saveAs: {filename ->
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*.sam.gz", emit: maps
+    path "*fastq.gz", includeInputs:false, emit: unmapped
+    path "*.log", emit: logs
+    script:
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf1 = fn+"_R1_unmapped.fastq.gz"
+        uf2 = fn+"_R2_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        """
+    }else{
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")
+        pf = fn+"_mapped.sam.gz"
+        uf = fn+"_unmapped.fastq.gz"
+        lf = "bwa_"+fn+".log"
+        """
+        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        """
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    collection.filter(~/.fastq.gz/)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        bwa_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        bwa_idx(genomefile)
+        bwa_mapping(bwa_idx.out.bwidx.combine(collection))
+    }
+    emit:
+    mapped  = bwa_mapping.out.maps
+    logs = bwa_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subconfig.json b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subconfig.json
new file mode 100644
index 00000000..3aa873e6
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subconfig.json
@@ -0,0 +1,87 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "OPTIONS": {
+        "DEDUP": ""
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "OPTIONS": {
+        "EXTENSION": "",
+        "INDEX": "",
+        "MAP": "--outSAMprimaryFlag AllBestScore"
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "OPTIONS": {
+        "MULTI": "",
+        "QC": ""
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "trimgalore": "trim_galore"
+    },
+    "Test": {
+      "OPTIONS": {
+        "TRIM": "--length 8"
+      }
+    }
+  },
+  "TRIMMINGBIN": "trim_galore",
+  "TRIMMINGENV": "trimgalore"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
new file mode 100644
index 00000000..4cfad930
--- /dev/null
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
@@ -0,0 +1,579 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    if (!params.containsKey(parameter)){
+        params.put(parameter, null)
+    }
+    return params[parameter]
+}
+REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+BINS = get_always('BINS')
+THREADS = get_always('MAXTHREAD')
+PAIRED = get_always('PAIRED') ?: null
+RUNDEDUP = get_always('RUNDEDUP') ?: null
+PREDEDUP = get_always('PREDEDUP') ?: null
+STRANDED = get_always('STRANDED') ?: null
+IP = get_always('IP') ?: null
+CONDITION = get_always('CONDITION') ?: null
+COMBO = get_always('COMBO') ?: ''
+SCOMBO = get_always('SCOMBO') ?: ''
+SAMPLES = get_always('SAMPLES').split(',') ?: null
+LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+SETS = get_always('SETS') ?: null
+dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+if (PAIRED == 'paired' || PAIRED == 'singlecell'){
+    RSAMPLES = SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    RSAMPLES=SAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+samples_ch = Channel.fromPath(RSAMPLES)
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_QC') ?: ''
+process qc_raw{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_RAW{
+    take:
+    collection
+    main:
+    qc_raw(samples_ch)
+    emit:
+    qc = qc_raw.out.fastqc_results
+}
+process qc_trimmed{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_TRIMMING{
+    take: collection
+    main:
+    qc_trimmed(collection)
+    emit:
+    qc = qc_trimmed.out.fastqc_results
+}
+process qc_mapped{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    """
+}
+workflow QC_MAPPING{
+    take: collection
+    main:
+    qc_mapped(collection)
+    emit:
+    qc = qc_mapped.out.fastqc_results
+}
+process qc_dedup{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else null
+    }
+    input:
+    path read
+    output:
+    path "*.{zip,html}", emit: fastqc_results
+    script:
+    """
+    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    """
+}
+workflow QC_DEDUP{
+    take: collection
+    main:
+    qc_dedup(collection)
+    emit:
+    qc = qc_dedup.out.fastqc_results
+}
+
+
+TRIMENV=get_always('TRIMMINGENV')
+TRIMBIN=get_always('TRIMMINGBIN')
+TRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
+process trim{
+    conda "/envs/$TRIMENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    cpus 4
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path reads
+    output:
+    path "*_trimmed.fastq.gz", emit: trim
+    path "*trimming_report.txt", emit: rep
+    script:
+    if (PAIRED == 'paired'){
+        r1 = reads[0]
+        r2 = reads[1]
+        """
+        $TRIMBIN --cores ${task.cpus} --paired --gzip $TRIMPARAMS $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        """
+    }
+    else{
+        """
+        $TRIMBIN --cores ${task.cpus} --gzip $TRIMPARAMS $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        """
+    }
+}
+workflow TRIMMING{
+    take: 
+    collection    
+    main:
+    if ( PREDEDUP == 'enabled' ){  
+        trim(collection)
+    }else {        
+        if (PAIRED == 'paired'){
+            trim(samples_ch.collate(2))
+        } else{
+            trim(samples_ch.collate(1))
+        }
+    }
+    emit:
+    trimmed = trim.out.trim
+    report  = trim.out.rep
+}
+
+
+DEDUPENV=get_always('DEDUPENV')
+DEDUPBIN=get_always('DEDUPBIN')
+DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+process dedup_bam{
+    conda "/envs/$DEDUPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        else null
+    }
+    input:
+    path todedup
+    path bami
+    output:
+    path "*_dedup.bam", emit: bam
+    path "*_dedup.bam.bai", emit: bai
+    path "*_dedup.log", emit: logs
+    path "*_dedup_metrix.txt", emit: metrics
+    script:
+    bams = todedup[0]
+    bais = todedup[1]
+    outf = bams.getSimpleName()+"_dedup.bam"
+    outl = bams.getSimpleName()+"_dedup.log"
+    outm = bams.getSimpleName()+"_dedup_metrix.txt"
+    """
+    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    """
+}
+workflow DEDUPBAM{
+    take:
+    map
+    mapi
+    mapu
+    mapui
+    main:   
+    dedup_bam(map.concat(mapu), mapi.concat(mapui))
+    emit:
+    dedup = dedup_bam.out.bam
+    dedupbai = dedup_bam.out.bai
+    deduplog = dedup_bam.out.logs
+}
+
+
+MAPENV = get_always('MAPPINGENV')
+MAPBIN = get_always('MAPPINGBIN')
+MAPIDX = get_always('MAPPINGIDX')
+MAPUIDX = get_always('MAPPINGUIDX')
+MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+MAPREF = get_always('MAPPINGREF')
+MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+MAPANNO = get_always('MAPPINGANNO')
+MAPPREFIX = get_always('MAPPINGPREFIX')
+MAPUIDX = MAPUIDX.replace('.idx','')
+IDXPARAMS = get_always('star_params_INDEX') ?: ''
+MAPPARAMS = get_always('star_params_MAP') ?: ''
+process collect_tomap{
+    input:
+    path check
+    output:
+    path "collect.txt", emit: done
+    script:
+    """
+    echo "$check Collection successful!" > collect.txt
+    """
+}
+process star_idx{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
+    saveAs: {filename ->
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
+        else                                             "$MAPUIDX"
+    }
+    input:
+    path genome
+    path anno
+    output:
+    path "$MAPUIDXNAME", emit: idx
+    path "*.out", emit: idxlog
+    path "*.idx", emit: tmpidx
+    script:
+    gen =  genome.getName()
+    an  = anno.getName()
+    """
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    """
+}
+process star_mapping{
+    conda "/envs/$MAPENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
+    cpus THREADS
+	cache 'lenient'
+    label 'big_mem'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+    }
+    input:
+    path reads
+    output:
+    path "*_mapped.sam.gz", emit: maps
+    path "*.out", emit: outs
+    path "*.log", emit: logs
+    path "*Log*.out", emit: xtralogs
+    path "*.tab", emit: sjtab
+    path "*_unmapped.fastq.gz", includeInputs:false, emit: unmapped
+    script:
+    idx = reads[0]
+    idxdir = idx.toRealPath()
+    if (PAIRED == 'paired'){
+        r1 = reads[1]
+        r2 = reads[2]
+        a = "Trimming_report.txt"
+        fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+        of = fn+'.Aligned.out.sam'
+        gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+        """
+        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        """
+    }
+    else{
+        if (PAIRED != 'singlecell'){
+            read = reads[1]
+            fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
+            of = fn+'Aligned.out.sam'
+            gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            """
+        }
+        else{
+            if (STRANDED == 'fr'){
+                stranded = '--soloStrand Forward'
+            }else if (STRANDED == 'rf'){
+                stranded = '--soloStrand Reverse'
+            }else{
+                stranded = '--soloStrand Unstranded'
+            }
+            r1 = reads[1]
+            fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
+            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+                t = r2
+                r2 = r1
+                r1 = t
+            }
+            of = fn+'.Aligned.sortedByCoord.out.bam'
+            gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
+            """
+            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            """
+        }
+    }
+}
+workflow MAPPING{
+    take: collection
+    main:
+    checkidx = file(MAPUIDX)
+    if (checkidx.exists()){
+        idxfile = Channel.fromPath(MAPUIDX)
+        star_mapping(idxfile.combine(collection))
+    }
+    else{
+        genomefile = Channel.fromPath(MAPREF)
+        annofile = Channel.fromPath(MAPANNO)
+        star_idx(genomefile, annofile)
+        star_mapping(star_idx.out.idx.combine(collection))
+    }
+    emit:
+    mapped  = star_mapping.out.maps
+    logs = star_mapping.out.logs
+}
+
+
+process sortsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else null
+    }
+    input:
+    path map
+    output:
+    path "*_mapped_sorted.sam.gz", includeInputs:false, emit: sam
+    script:
+    fn = file(map[0]).getSimpleName()
+    sorted = fn+'_sorted.sam.gz'
+    def sortmem = Math.ceil(task.memory.giga as double) as int 
+    """
+    set +o pipefail; mkdir -p TMP; samtools view -H $map|grep -P '^@HD' |pigz -p ${task.cpus} -f > tmphead; samtools view -H $map|grep -P '^@SQ'|sort -t\$'\\t' -k1,1 -k2,2V |pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@RG'|pigz -p ${task.cpus} -f >> tmphead ; samtools view -H $map|grep -P '^@PG'|pigz -p ${task.cpus} -f >> tmphead ; export LC_ALL=C;zcat $map | grep -v \"^@\"|sort --parallel=${task.cpus} -S $sortmem -T TMP -t\$'\\t' -k3,3V -k4,4n - |pigz -p ${task.cpus} -f > tmpfile; cat tmphead tmpfile > $sorted && rm -f tmphead tmpfile
+    """
+}
+process sam2bam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+".bam"
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> sam2bam.log
+    """
+}
+process uniqsam{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*_unique.sam.gz", includeInputs:false, emit: sam
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    uniq = fn+'_unique.sam.gz'
+    if (COMBO.indexOf('-bwa') > 0){
+        bwa = 'bwa'
+    } else{
+        bwa = ''
+    }
+    """
+    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    """
+}
+process sam2bamuniq{
+    conda "/envs/samtools.yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"samtools"
+    cpus THREADS
+    memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        else null
+    }
+    input:
+    path sam
+    output:
+    path "*.bam", emit: bam
+    path "*.bai", emit: bai
+    path "*.log", emit: logs
+    script:
+    fn = file(sam[0]).getSimpleName()
+    bam = fn+'.bam'
+    """
+    zcat $sam | samtools view -bS - | samtools sort -T $fn -o $bam --threads ${task.cpus} && samtools index $bam 2> uniquebam.log
+    """
+}
+workflow POSTMAPPING{
+    take: collection
+    main:
+    sortsam(collection)
+    sam2bam(sortsam.out.sam)
+    uniqsam(sortsam.out.sam)
+    sam2bamuniq(uniqsam.out.sam)
+    emit:
+    postmap  = sam2bam.out.bam
+    postbai  = sam2bam.out.bai
+    postmapuni = sam2bamuniq.out.bam
+    postunibai = sam2bamuniq.out.bai
+}
+
+
+QCENV=get_always('PREQCENV')
+QCBIN=get_always('PREQCBIN')
+QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
+process mqc{
+    conda "/envs/$QCENV"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
+    cpus THREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+    }
+    input:
+    path others, stageAs: 'mqc_input??/*'
+    output:
+    path "*.zip", emit: mqc
+    path "*.html", emit: html
+    script:
+    """
+    touch $others
+    OUT=\${PWD}
+    LIST=multiqc_inputs.txt
+    TMP_LIST=multiqc_inputs_unique.txt
+    BASE_QC_DIR="${workflow.workDir}/../QC"
+    COMBO_VAL="${COMBO}"
+    CONDITION_VAL="${CONDITION}"
+    for i in $others; do
+        dirname "\$i" >> "\$LIST"
+    done
+    # If this is a rustqc combo and the corresponding fastqc combo exists,
+    # include the fastqc output directory in the same MultiQC report.
+    if [[ "\$COMBO_VAL" == *"rustqc"* ]]; then
+        FQ_COMBO="\${COMBO_VAL/rustqc/fastqc}"
+        FQ_DIR="\${BASE_QC_DIR}/\${FQ_COMBO}/\${CONDITION_VAL}"
+        if [[ -d "\$FQ_DIR" ]]; then
+            echo "\$FQ_DIR" >> "\$LIST"
+        fi
+    fi
+    sort -u "\$LIST" > "\$TMP_LIST"
+    export LC_ALL=en_US.utf8
+    export LC_ALL=C.UTF-8
+    multiqc -f --exclude picard --exclude gatk -k json -z -s -o "\$OUT" -l "\$TMP_LIST"
+    """
+}
+workflow MULTIQC{
+    take:
+    otherqcs
+    main:
+    mqc(otherqcs.collect())
+    emit:
+    mqcres = mqc.out.mqc
+}
+
+
+
+
+workflow {
+    QC_RAW(dummy)
+    TRIMMING(dummy)
+    QC_TRIMMING(TRIMMING.out.trimmed)
+    MAPPING(TRIMMING.out.trimmed)
+    POSTMAPPING(MAPPING.out.mapped)
+    DEDUPBAM(POSTMAPPING.out.postmap, POSTMAPPING.out.postbai, POSTMAPPING.out.postmapuni, POSTMAPPING.out.postunibai)
+    QC_MAPPING(POSTMAPPING.out.postmap.concat(POSTMAPPING.out.postmapuni.concat(DEDUPBAM.out.dedup)))
+    MULTIQC(QC_RAW.out.qc.concat(QC_TRIMMING.out.qc.concat(QC_MAPPING.out.qc.concat(MAPPING.out.logs))).collect())
+}
+
diff --git a/tests/golden/config_Test/nf/_filelist.txt b/tests/golden/config_Test/nf/_filelist.txt
new file mode 100644
index 00000000..ec57aa75
--- /dev/null
+++ b/tests/golden/config_Test/nf/_filelist.txt
@@ -0,0 +1,12 @@
+Test_fastqc-bbduk-picard-bwa_subconfig.json
+Test_fastqc-bbduk-picard-bwa_subflow.nf
+Test_fastqc-bbduk-picard-star_subconfig.json
+Test_fastqc-bbduk-picard-star_subflow.nf
+Test_fastqc-cutadapt-picard-bwa_subconfig.json
+Test_fastqc-cutadapt-picard-bwa_subflow.nf
+Test_fastqc-cutadapt-picard-star_subconfig.json
+Test_fastqc-cutadapt-picard-star_subflow.nf
+Test_fastqc-trimgalore-picard-bwa_subconfig.json
+Test_fastqc-trimgalore-picard-bwa_subflow.nf
+Test_fastqc-trimgalore-picard-star_subconfig.json
+Test_fastqc-trimgalore-picard-star_subflow.nf
diff --git a/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subconfig.json
new file mode 100644
index 00000000..299a9cf6
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "bwa": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": ""
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "bbduk": "bbduk.sh"
+    },
+    "Test": {
+      "bbduk": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "bbduk.sh",
+  "TRIMMINGENV": "bbduk"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subsnake.smk
new file mode 100644
index 00000000..dfa62d8d
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-bwa_subsnake.smk
@@ -0,0 +1,839 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-bbduk-picard-bwa'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+if paired == 'paired':
+    rule bbduk_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: MAXTHREAD
+        params: odir = lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV).get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} t={threads} in1={input.r1} in2={input.r2} out1={output.o1} out2={output.o2} {params.tpara}"
+    rule bbduk_rename:
+        input:  o1 = rules.bbduk_trim.output.o1,
+                o2 = rules.bbduk_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule bbduk_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: MAXTHREAD
+        params: odir = lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV).get('TRIM',""),
+                trim = TRIMBIN,
+        shell:  "{params.trim} t={threads} in={input.r1} out={output.o1} {params.tpara}"
+    rule bbduk_rename:
+        input:  o1 = rules.bbduk_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, "MAPPING", MAPPERENV)["OPTIONS"],["INDEX"],)
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  ref = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = expand("{refd}/INDICES/{mape}_{unikey}/{pref}", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX)
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: 1
+    params: indexer = MAPPERBIN.split(' ')[0],
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            linkidx = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.uidx[0]).split(os.sep)[:-1]))) if PREFIX != '' else str(os.path.abspath(str(output.uidx[0]))),
+            tolink = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.idx).split(os.sep)[:-1])))
+    shell:  "if [[ -f \"{output.uidx}\\/*\" ]]; then ln -fs {params.linkidx} {output.idx} && touch {output.uidx} && echo \"Found bwa index, continue with mapping\" ; else {params.indexer} index -p {output.uidx} {params.ipara} {input.ref} 2> {log} && ln -fs {params.linkidx} {output.idx} && touch {output.uidx};fi"
+bwaalg = MAPPERBIN.split(' ')[1]
+if bwaalg == 'mem':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    index = rules.generate_index.output.uidx,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get("MAP", ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.index} {input.r1} {input.r2}  2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.uidx} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'aln': # not supported as stand alone as we need mappign files to continue the workflow
+    if paired == 'paired': # handled like sampe
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+#                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+#                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} > {output.sai1} 2>> {log} && {params.mapp} {params.mpara} -t {threads} {input.ref} {input.r2} > {output.sai2} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+    else: #handled like samse
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log} | tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+elif bwaalg == 'samse':
+    rule mapping:
+        input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'sampe':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+elif bwaalg == 'bwasw':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} - ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subconfig.json
new file mode 100644
index 00000000..c09d4b6a
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "star": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": "--outSAMprimaryFlag AllBestScore"
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "bbduk": "bbduk.sh"
+    },
+    "Test": {
+      "bbduk": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "bbduk.sh",
+  "TRIMMINGENV": "bbduk"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subsnake.smk
new file mode 100644
index 00000000..99981727
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-bbduk-picard-star_subsnake.smk
@@ -0,0 +1,754 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-bbduk-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+if paired == 'paired':
+    rule bbduk_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: MAXTHREAD
+        params: odir = lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV).get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} t={threads} in1={input.r1} in2={input.r2} out1={output.o1} out2={output.o2} {params.tpara}"
+    rule bbduk_rename:
+        input:  o1 = rules.bbduk_trim.output.o1,
+                o2 = rules.bbduk_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule bbduk_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: MAXTHREAD
+        params: odir = lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV).get('TRIM',""),
+                trim = TRIMBIN,
+        shell:  "{params.trim} t={threads} in={input.r1} out={output.o1} {params.tpara}"
+    rule bbduk_rename:
+        input:  o1 = rules.bbduk_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=MAPPERENV, unikey=unik)),
+            idxfile = expand("{refd}/INDICES/{mape}_{unikey}/{pref}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX),
+            tmpfa = temp(expand("TMP/{mape}/ref.fa", mape=MAPPERENV)),
+            tmpref = temp(expand("TMP/{mape}/ref.anno", mape=MAPPERENV))
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: mapp = MAPPERBIN,
+            ipara = lambda w,output: fixRunParameters(config, MAPPERENV, SAMPLES[0], None, 'MAPPING', 'INDEX', "--sjdbGTFfile", f"--sjdbGTFfile {output.tmpref}"),
+            anno = ANNOTATION,                        
+            pref = PREFIX,
+            lnkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "if [[ -f \"{output.idxfile}\" ]]; then touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && echo \"Found SAindex, continue with mapping\" ; else zcat {input.fa} > {output.tmpfa} && zcat {params.anno} > {output.tmpref} && mkdir -p {output.uidx} && {params.mapp} {params.ipara} --runThreadN {threads} --runMode genomeGenerate --outFileNamePrefix {output.uidx}/{params.pref} --outTmpDir TMP/star_generate_index --genomeDir {output.uidx} --genomeFastaFiles {output.tmpfa}  &> {log} && touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && cat {output.uidx}/*Log.out >> {log};fi && rm -rf TMP/star_generate_index"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0],
+                dummy = rules.generate_index.output.idxfile,
+                ref = REFERENCE
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                log = "LOGS/{combo}/{file}/Log.out",
+                log_final = "LOGS/{combo}/{file}/Log.final.out",
+                tmp = temp("TMP/STAROUT/{combo}/{file}")
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                anno = ANNOTATION,
+                pref = PREFIX,
+                tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+        shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} {input.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} && touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+else:
+    if paired != 'singlecell':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")                    
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),                    
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} touch {output.tmp}.Unmapped.out.mate && gzip {output.tmp}.Unmapped.out.mate* && mv {output.tmp}.Unmapped.out.mate*.gz {output.unmapped} 2>> {log} && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>>{log} && touch {output.tmp}"
+    else:
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    stranded = lambda x: '--soloStrand Forward' if stranded == 'fr' else '--soloStrand Reverse' if stranded == 'rf' else '--soloStrand Unstranded',
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    r1 = lambda wildcards, input: input.r1 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r2.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq'),
+                    r2 = lambda wildcards, input: input.r2 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r1.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq').replace({combo}, ''),
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} {params.stranded} --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {params.r1} {params.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && samtools view -h {output.tmp}.Aligned.sortedByCoord.out.bam | gzip > {output.mapped} && rm -f {output.tmp}.Aligned.sortedByCoord.out.bam; touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subconfig.json
new file mode 100644
index 00000000..f7b2c00a
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "bwa": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": ""
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "cutadapt": "cutadapt"
+    },
+    "Test": {
+      "cutadapt": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "cutadapt",
+  "TRIMMINGENV": "cutadapt"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subsnake.smk
new file mode 100644
index 00000000..e93ded03
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-bwa_subsnake.smk
@@ -0,0 +1,839 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-cutadapt-picard-bwa'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+if paired == 'paired':
+    rule cutadapt_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/8),4) if min(int(MAXTHREAD/8),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} {params.tpara} --cores {threads} -o {output.o1} -p {output.o2} {input.r1} {input.r2} &> {log}"
+    rule cutadapt_rename:
+        input:  o1 = rules.cutadapt_trim.output.o1,
+                o2 = rules.cutadapt_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule cutadapt_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/8),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards:tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN,
+        shell:  "{params.trim} {params.tpara} --cores {threads} -o {output.o1} {input.r1} > {log}"
+    rule cutadapt_rename:
+        input:  o1 = rules.cutadapt_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, "MAPPING", MAPPERENV)["OPTIONS"],["INDEX"],)
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  ref = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = expand("{refd}/INDICES/{mape}_{unikey}/{pref}", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX)
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: 1
+    params: indexer = MAPPERBIN.split(' ')[0],
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            linkidx = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.uidx[0]).split(os.sep)[:-1]))) if PREFIX != '' else str(os.path.abspath(str(output.uidx[0]))),
+            tolink = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.idx).split(os.sep)[:-1])))
+    shell:  "if [[ -f \"{output.uidx}\\/*\" ]]; then ln -fs {params.linkidx} {output.idx} && touch {output.uidx} && echo \"Found bwa index, continue with mapping\" ; else {params.indexer} index -p {output.uidx} {params.ipara} {input.ref} 2> {log} && ln -fs {params.linkidx} {output.idx} && touch {output.uidx};fi"
+bwaalg = MAPPERBIN.split(' ')[1]
+if bwaalg == 'mem':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    index = rules.generate_index.output.uidx,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get("MAP", ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.index} {input.r1} {input.r2}  2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.uidx} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'aln': # not supported as stand alone as we need mappign files to continue the workflow
+    if paired == 'paired': # handled like sampe
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+#                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+#                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} > {output.sai1} 2>> {log} && {params.mapp} {params.mpara} -t {threads} {input.ref} {input.r2} > {output.sai2} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+    else: #handled like samse
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log} | tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+elif bwaalg == 'samse':
+    rule mapping:
+        input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'sampe':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+elif bwaalg == 'bwasw':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} - ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subconfig.json
new file mode 100644
index 00000000..30fe9028
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "star": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": "--outSAMprimaryFlag AllBestScore"
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "cutadapt": "cutadapt"
+    },
+    "Test": {
+      "cutadapt": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "cutadapt",
+  "TRIMMINGENV": "cutadapt"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subsnake.smk
new file mode 100644
index 00000000..d8015aa1
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-cutadapt-picard-star_subsnake.smk
@@ -0,0 +1,754 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-cutadapt-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+if paired == 'paired':
+    rule cutadapt_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/8),4) if min(int(MAXTHREAD/8),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} {params.tpara} --cores {threads} -o {output.o1} -p {output.o2} {input.r1} {input.r2} &> {log}"
+    rule cutadapt_rename:
+        input:  o1 = rules.cutadapt_trim.output.o1,
+                o2 = rules.cutadapt_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule cutadapt_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/8),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards:tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN,
+        shell:  "{params.trim} {params.tpara} --cores {threads} -o {output.o1} {input.r1} > {log}"
+    rule cutadapt_rename:
+        input:  o1 = rules.cutadapt_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=MAPPERENV, unikey=unik)),
+            idxfile = expand("{refd}/INDICES/{mape}_{unikey}/{pref}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX),
+            tmpfa = temp(expand("TMP/{mape}/ref.fa", mape=MAPPERENV)),
+            tmpref = temp(expand("TMP/{mape}/ref.anno", mape=MAPPERENV))
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: mapp = MAPPERBIN,
+            ipara = lambda w,output: fixRunParameters(config, MAPPERENV, SAMPLES[0], None, 'MAPPING', 'INDEX', "--sjdbGTFfile", f"--sjdbGTFfile {output.tmpref}"),
+            anno = ANNOTATION,                        
+            pref = PREFIX,
+            lnkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "if [[ -f \"{output.idxfile}\" ]]; then touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && echo \"Found SAindex, continue with mapping\" ; else zcat {input.fa} > {output.tmpfa} && zcat {params.anno} > {output.tmpref} && mkdir -p {output.uidx} && {params.mapp} {params.ipara} --runThreadN {threads} --runMode genomeGenerate --outFileNamePrefix {output.uidx}/{params.pref} --outTmpDir TMP/star_generate_index --genomeDir {output.uidx} --genomeFastaFiles {output.tmpfa}  &> {log} && touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && cat {output.uidx}/*Log.out >> {log};fi && rm -rf TMP/star_generate_index"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0],
+                dummy = rules.generate_index.output.idxfile,
+                ref = REFERENCE
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                log = "LOGS/{combo}/{file}/Log.out",
+                log_final = "LOGS/{combo}/{file}/Log.final.out",
+                tmp = temp("TMP/STAROUT/{combo}/{file}")
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                anno = ANNOTATION,
+                pref = PREFIX,
+                tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+        shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} {input.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} && touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+else:
+    if paired != 'singlecell':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")                    
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),                    
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} touch {output.tmp}.Unmapped.out.mate && gzip {output.tmp}.Unmapped.out.mate* && mv {output.tmp}.Unmapped.out.mate*.gz {output.unmapped} 2>> {log} && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>>{log} && touch {output.tmp}"
+    else:
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    stranded = lambda x: '--soloStrand Forward' if stranded == 'fr' else '--soloStrand Reverse' if stranded == 'rf' else '--soloStrand Unstranded',
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    r1 = lambda wildcards, input: input.r1 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r2.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq'),
+                    r2 = lambda wildcards, input: input.r2 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r1.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq').replace({combo}, ''),
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} {params.stranded} --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {params.r1} {params.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && samtools view -h {output.tmp}.Aligned.sortedByCoord.out.bam | gzip > {output.mapped} && rm -f {output.tmp}.Aligned.sortedByCoord.out.bam; touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subconfig.json
new file mode 100644
index 00000000..f7e7cab3
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "bwa": "bwa mem"
+    },
+    "Test": {
+      "bwa": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": ""
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "bwa mem",
+  "MAPPINGENV": "bwa",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "trimgalore": "trim_galore"
+    },
+    "Test": {
+      "trimgalore": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "trim_galore",
+  "TRIMMINGENV": "trimgalore"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subsnake.smk
new file mode 100644
index 00000000..c80ab9ea
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-bwa_subsnake.smk
@@ -0,0 +1,846 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-bwa'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+#outdir = 'TRIMMED_FASTQ'
+#wildcard_constraints:
+#    file = '|'.join(list(samplecond(SAMPLES, config))),
+#    read = "R1|R2"
+#    outdir = outdir
+#rule trimthemall:
+#    input: expand("{outdir}{file}_{read}_trimmed.fastq.gz", outdir=outdir, file=samplecond(SAMPLES, config), read=["R1","R2"]) if paired == \'paired\' else expand("{outdir}{file}_trimmed.fastq.gz", outdir=outdir, file=samplecond(SAMPLES, config))
+if paired == 'paired':
+    rule trimgalore_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:   "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/2),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} --cores {threads} --paired --gzip {params.tpara} -o {params.odir} {input.r1} {input.r2} &> {log}"
+    rule trimgalore_rename:
+        input:  o1 = rules.trimgalore_trim.output.o1,
+                o2 = rules.trimgalore_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule trimgalore_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/2),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir = lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} --cores {threads} --gzip {params.tpara} -o {params.odir} {input.r1} &> {log}"
+    rule trimgalore_rename:
+        input:  o1 = rules.trimgalore_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, "MAPPING", MAPPERENV)["OPTIONS"],["INDEX"],)
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  ref = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = expand("{refd}/INDICES/{mape}_{unikey}/{pref}", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX)
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: 1
+    params: indexer = MAPPERBIN.split(' ')[0],
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('INDEX', ""),
+            linkidx = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.uidx[0]).split(os.sep)[:-1]))) if PREFIX != '' else str(os.path.abspath(str(output.uidx[0]))),
+            tolink = lambda wildcards, output: str(os.path.abspath(str.join(os.sep, str(output.idx).split(os.sep)[:-1])))
+    shell:  "if [[ -f \"{output.uidx}\\/*\" ]]; then ln -fs {params.linkidx} {output.idx} && touch {output.uidx} && echo \"Found bwa index, continue with mapping\" ; else {params.indexer} index -p {output.uidx} {params.ipara} {input.ref} 2> {log} && ln -fs {params.linkidx} {output.idx} && touch {output.uidx};fi"
+bwaalg = MAPPERBIN.split(' ')[1]
+if bwaalg == 'mem':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    index = rules.generate_index.output.uidx,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get("MAP", ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.index} {input.r1} {input.r2}  2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+                    #idx = lambda wildcards, input: str.join(os.sep,[str(input.index), PREFIX]) if PREFIX != '' else input.index
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.uidx} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'aln': # not supported as stand alone as we need mappign files to continue the workflow
+    if paired == 'paired': # handled like sampe
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+#                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+#                    sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} > {output.sai1} 2>> {log} && {params.mapp} {params.mpara} -t {threads} {input.ref} {input.r2} > {output.sai2} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+    else: #handled like samse
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                    mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN,
+                    mapp1 = MAPPERBIN.split(' ')[0]
+            shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log} | tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+### FOR LATER IF WE EVER NEED aln MODE
+#        rule mapping:
+#            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+#                    #index = lambda wildcards: expand(rules.generate_index.output.idx, ref=REFERENCE, dir=source_from_sample(wildcards.file, config), gen=genome(wildcards.file, config), name=namefromfile(wildcards.file, config), mape=MAPPERENV, extension=check_tool_params(wildcards.file, None, config, 'MAPPING', 2)),
+#                    ref = lambda wildcards: expand(rules.generate_index.input.fa, ref=REFERENCE, dir = source_from_sample(wildcards.file, config), gen =genome(wildcards.file, config), name=namefromfile(wildcards.file, config))
+#            output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+#                    mapped = "UNMAPPED/{combo}/{file}_mapped.sam",
+#                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+#            log:    "LOGS/{combo}/{file}/mapping.log"
+#            conda: "/envs/"+MAPPERENV+".yaml"
+#            threads: MAXTHREAD
+#            params: mpara = lambda wildcards: ' '.join("{!s} {!s}".format(key, val) for (key, val) in tool_params(wildcards.file, None, config, 'MAPPING')['OPTIONS'][1].items()),
+#                    mapp=MAPPERBIN
+#            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} 2>> {log} && touch {output.unmapped} && touch {output.mapped}"
+elif bwaalg == 'samse':
+    rule mapping:
+        input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai = report("MAPPED/{combo}/{file}_mapped.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp1} aln {params.mpara} -t {threads} {input.ref} {input.query} > {output.sai} && {params.mapp} {params.mpara} {input.ref} {output.sai} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+elif bwaalg == 'sampe':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                ref = REFERENCE
+        output: sai1 = report("MAPPED/{combo}/{file}_mapped.R1.sai", category="MAPPING"),
+                sai2 = report("MAPPED/{combo}/{file}_mapped.R2.sai", category="MAPPING"),
+                mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                mapp1 = MAPPERBIN.split(' ')[0]
+        shell:  "{params.mapp} {params.mpara} {input.ref} {input.sai1} {input.sai2} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+elif bwaalg == 'bwasw':
+    if paired == 'paired':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell: "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.r1} {input.r2} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 {output.unmapped1} -2 {output.unmapped2} - ) 2>> {log} &>/dev/null && touch {output.unmapped1} {output.unmapped2}"
+    else:
+        rule mapping:
+            input:  query = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz"
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    mapp = MAPPERBIN
+            shell:  "{params.mapp} {params.mpara} -t {threads} {input.ref} {input.query} 2> {log}| tee >(samtools view -h -F 4 |gzip > {output.mapped}) >(samtools view -h -f 4 |samtools fastq -n - | pigz > {output.unmapped}) 2>> {log} &>/dev/null && touch {output.unmapped}"
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subconfig.json b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subconfig.json
new file mode 100644
index 00000000..1584f1af
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subconfig.json
@@ -0,0 +1,95 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "DEDUP": {
+    "TOOLS": {
+      "picard": "picard"
+    },
+    "Test": {
+      "picard": {
+        "OPTIONS": {
+          "DEDUP": ""
+        }
+      }
+    }
+  },
+  "DEDUPBIN": "picard",
+  "DEDUPENV": "picard",
+  "MAPPING": {
+    "TOOLS": {
+      "star": "STAR"
+    },
+    "Test": {
+      "star": {
+        "OPTIONS": {
+          "EXTENSION": "",
+          "INDEX": "",
+          "MAP": "--outSAMprimaryFlag AllBestScore"
+        }
+      }
+    }
+  },
+  "MAPPINGBIN": "STAR",
+  "MAPPINGENV": "star",
+  "MAXTHREADS": "4",
+  "PREQCBIN": "fastqc",
+  "PREQCENV": "fastqc",
+  "QC": {
+    "TOOLS": {
+      "fastqc": "fastqc"
+    },
+    "Test": {
+      "fastqc": {
+        "OPTIONS": {
+          "MULTI": "",
+          "QC": ""
+        }
+      }
+    }
+  },
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "SRR6357070",
+    "SRR6357071",
+    "SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  },
+  "TRIMMING": {
+    "TOOLS": {
+      "trimgalore": "trim_galore"
+    },
+    "Test": {
+      "trimgalore": {
+        "OPTIONS": {
+          "TRIM": "--length 8"
+        }
+      }
+    }
+  },
+  "TRIMMINGBIN": "trim_galore",
+  "TRIMMINGENV": "trimgalore"
+}
\ No newline at end of file
diff --git a/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subsnake.smk b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subsnake.smk
new file mode 100644
index 00000000..71c0b7f1
--- /dev/null
+++ b/tests/golden/config_Test/smk/Test_fastqc-trimgalore-picard-star_subsnake.smk
@@ -0,0 +1,761 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    rawfile = '|'.join(list(SAMPLES)),
+    file = '|'.join(list(samplecond(SAMPLES, config))),
+    read = "R1|R2"
+
+
+rule themall:
+	input:	expand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"]),
+		expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)
+
+QCBIN, QCENV = env_bin_from_config(config, 'PREQC')
+if paired == 'paired':
+    log.info('Running paired mode QC')
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_{read}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_{read}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+else:
+    rule qc_raw:
+        input:  r1 = "FASTQ/{rawfile}.fastq.gz"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_dedup:
+        input:  r1 = "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_dedup_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_dedup.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+    rule qc_trimmed:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        output: o1 = report("QC/{combo}/{file}_trimmed_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{file}_fastqc_trimmed.log"
+        conda: "/envs/"+QCENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+        threads: MAXTHREAD
+        priority: 10
+        params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+        shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f fastq {input.r1} 2> {log}"
+rule qc_mapped:
+    input:   r1 = "MAPPED/{combo}/{file}_mapped_sorted.bam"
+    output:  o1 = report("QC/{combo}/{file}_mapped_sorted_fastqc.zip", category="QC")
+    log:     "LOGS/{combo}/{file}_fastqc_mapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquemapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquemapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_dedupmapped:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_dedupmapped.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+rule qc_uniquededup:
+    input:  r1 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam",
+            r2 = "MAPPED/{combo}/{file}_mapped_sorted_unique_dedup.bam.bai"
+    output: o1 = report("QC/{combo}/{file}_mapped_sorted_unique_dedup_fastqc.zip", category="QC")
+    log:    "LOGS/{combo}/{file}_fastqc_uniquededup.log"
+    conda: "/envs/"+QCENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+QCENV+""
+    threads: MAXTHREAD
+    params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('QC', "")
+    shell: "OUT=$(dirname {output.o1});fastqc --quiet -o $OUT -t {threads} --noextract {params.qpara} -f bam {input.r1} 2> {log}"
+
+
+TRIMBIN, TRIMENV = env_bin_from_config(config,'TRIMMING')
+#outdir = 'TRIMMED_FASTQ'
+#wildcard_constraints:
+#    file = '|'.join(list(samplecond(SAMPLES, config))),
+#    read = "R1|R2"
+#    outdir = outdir
+#rule trimthemall:
+#    input: expand("{outdir}{file}_{read}_trimmed.fastq.gz", outdir=outdir, file=samplecond(SAMPLES, config), read=["R1","R2"]) if paired == \'paired\' else expand("{outdir}{file}_trimmed.fastq.gz", outdir=outdir, file=samplecond(SAMPLES, config))
+if paired == 'paired':
+    rule trimgalore_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_R1_val_1.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R1_dedup_val_1.fq.gz",
+                o2 = "TRIMMED_FASTQ/{combo}/{file}_R2_val_2.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_R2_dedup_val_2.fq.gz"
+        log:   "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/2),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir=lambda wildcards, output:os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} --cores {threads} --paired --gzip {params.tpara} -o {params.odir} {input.r1} {input.r2} &> {log}"
+    rule trimgalore_rename:
+        input:  o1 = rules.trimgalore_trim.output.o1,
+                o2 = rules.trimgalore_trim.output.o2
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1} && mv {input.o2} {output.r2}"
+else:
+    rule trimgalore_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: o1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fq.gz" if not prededup else "TRIMMED_FASTQ/{combo}/{file}_dedup_trimmed.fq.gz"
+        log:    "LOGS/{combo}/{file}_trim.log"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: min(int(MAXTHREAD/2),4) if min(int(MAXTHREAD/2),4) >= 1 else (4 if int(MAXTHREAD) >= 4 else 1)
+        params: odir = lambda wildcards, output: os.path.dirname(output.o1),
+                tpara = lambda wildcards: tool_params(wildcards.file, None, config, "TRIMMING", TRIMENV)['OPTIONS'].get('TRIM', ""),
+                trim=TRIMBIN
+        shell:  "{params.trim} --cores {threads} --gzip {params.tpara} -o {params.odir} {input.r1} &> {log}"
+    rule trimgalore_rename:
+        input:  o1 = rules.trimgalore_trim.output.o1
+        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
+        conda: "/envs/"+TRIMENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+TRIMENV+""
+        threads: 1
+        shell:  "mv {input.o1} {output.r1}"
+
+
+DEDUPBIN, DEDUPENV = env_bin_from_config(config, 'DEDUP')
+rule dedupbam:
+    input:  bam = "MAPPED/{combo}/{file}_mapped_{type}.bam"
+    output: bam = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", category="DEDUP"),
+            bai = report("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam.bai", category="DEDUP"),
+            met = report("MAPPED/{combo}/{file}_mapped_{type}_dedup_metrics.txt", category="DEDUP"),
+            td = temp(directory("TMP/UMIDD/{combo}/{file}_{type}"))
+    log:    "LOGS/{combo}/{file}_{type}/dedupbam.log"
+    conda: "/envs/"+DEDUPENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+DEDUPENV+""
+    threads: 1
+    priority: 0               # This should be done after all mapping is done
+    params: jpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('JAVA', ""),
+            dpara = lambda wildcards: tool_params(wildcards.file, None, config, "DEDUP", DEDUPENV)['OPTIONS'].get('DEDUP', ""),
+            dedup = DEDUPBIN
+    shell: "mkdir -p {output.td} && {params.dedup} {params.jpara} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR {output.td} --INPUT {input.bam} --OUTPUT {output.bam} --METRICS_FILE {output.met} {params.dpara} 2> {log} && samtools index {output.bam} 2>> {log}"
+
+MAPPERBIN, MAPPERENV = env_bin_from_config(config,'MAPPING')
+keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+keydict["REF"] = REFERENCE
+keydict["DECOY"] = DECOY
+keydict["ENV"] = MAPPERENV
+unik = get_dict_hash(keydict)
+rule generate_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=MAPPERENV, unikey=unik)),
+            idxfile = expand("{refd}/INDICES/{mape}_{unikey}/{pref}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik, pref=PREFIX),
+            tmpfa = temp(expand("TMP/{mape}/ref.fa", mape=MAPPERENV)),
+            tmpref = temp(expand("TMP/{mape}/ref.anno", mape=MAPPERENV))
+    log:    expand("LOGS/{sets}/{mape}.idx.log", sets=SETS, mape=MAPPERENV)
+    conda: "/envs/"+MAPPERENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+    threads: MAXTHREAD
+    params: mapp = MAPPERBIN,
+            ipara = lambda w,output: fixRunParameters(config, MAPPERENV, SAMPLES[0], None, 'MAPPING', 'INDEX', "--sjdbGTFfile", f"--sjdbGTFfile {output.tmpref}"),
+            anno = ANNOTATION,                        
+            pref = PREFIX,
+            lnkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "if [[ -f \"{output.idxfile}\" ]]; then touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && echo \"Found SAindex, continue with mapping\" ; else zcat {input.fa} > {output.tmpfa} && zcat {params.anno} > {output.tmpref} && mkdir -p {output.uidx} && {params.mapp} {params.ipara} --runThreadN {threads} --runMode genomeGenerate --outFileNamePrefix {output.uidx}/{params.pref} --outTmpDir TMP/star_generate_index --genomeDir {output.uidx} --genomeFastaFiles {output.tmpfa}  &> {log} && touch {output.idxfile} && ln -fs {params.lnkidx} {output.idx} && cat {output.uidx}/*Log.out >> {log};fi && rm -rf TMP/star_generate_index"
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                uidx = rules.generate_index.output.uidx[0],
+                dummy = rules.generate_index.output.idxfile,
+                ref = REFERENCE
+        output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                log = "LOGS/{combo}/{file}/Log.out",
+                log_final = "LOGS/{combo}/{file}/Log.final.out",
+                tmp = temp("TMP/STAROUT/{combo}/{file}")
+        log:    "LOGS/{combo}/{file}/mapping.log"
+        conda: "/envs/"+MAPPERENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+        threads: MAXTHREAD
+        params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                mapp = MAPPERBIN,
+                anno = ANNOTATION,
+                pref = PREFIX,
+                tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+        shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} {input.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} && touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+else:
+    if paired != 'singlecell':
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped = "UNMAPPED/{combo}/{file}_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")                    
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),                    
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {input.r1} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && gzip -c {output.tmp}.Aligned.out.sam > {output.mapped} && rm -f {output.tmp}.Aligned.out.sam 2>> {log} touch {output.tmp}.Unmapped.out.mate && gzip {output.tmp}.Unmapped.out.mate* && mv {output.tmp}.Unmapped.out.mate*.gz {output.unmapped} 2>> {log} && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>>{log} && touch {output.tmp}"
+    else:
+        rule mapping:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    uidx = rules.generate_index.output.uidx[0],
+                    dummy = rules.generate_index.output.idxfile,
+                    ref = REFERENCE
+            output: mapped = temp(report("MAPPED/{combo}/{file}_mapped.sam.gz", category="MAPPING")),
+                    unmapped_r1 = "UNMAPPED/{combo}/{file}_R1_unmapped.fastq.gz",
+                    unmapped_r2 = "UNMAPPED/{combo}/{file}_R2_unmapped.fastq.gz",
+                    log = "LOGS/{combo}/{file}/Log.out",
+                    log_final = "LOGS/{combo}/{file}/Log.final.out",
+                    tmp = temp("TMP/STAROUT/{combo}/{file}")
+            log:    "LOGS/{combo}/{file}/mapping.log"
+            conda: "/envs/"+MAPPERENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV+""
+            threads: MAXTHREAD
+            params: mpara = lambda wildcards: tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', ""),
+                    stranded = lambda x: '--soloStrand Forward' if stranded == 'fr' else '--soloStrand Reverse' if stranded == 'rf' else '--soloStrand Unstranded',
+                    mapp = MAPPERBIN,
+                    anno = ANNOTATION,
+                    pref = PREFIX,
+                    r1 = lambda wildcards, input: input.r1 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r2.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq'),
+                    r2 = lambda wildcards, input: input.r2 if not '--soloBarcodeMate 1' in tool_params(wildcards.file, None, config, 'MAPPING', MAPPERENV)['OPTIONS'].get('MAP', "") else input.r1.replace('TRIMMED_','').replace('_trimmed.fastq', '.fastq').replace({combo}, ''),
+                    tocopy = lambda wildcards, output: os.path.dirname(output.mapped)
+            shell: "{params.mapp} {params.mpara} {params.stranded} --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN {threads} --genomeDir {input.uidx} --readFilesCommand zcat --readFilesIn {params.r1} {params.r2} --outFileNamePrefix {output.tmp}. --outReadsUnmapped Fastx &> {log} && samtools view -h {output.tmp}.Aligned.sortedByCoord.out.bam | gzip > {output.mapped} && rm -f {output.tmp}.Aligned.sortedByCoord.out.bam; touch {output.tmp}.Unmapped.out.mate1 {output.tmp}.Unmapped.out.mate2 && cat {output.tmp}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\" |gzip > {output.unmapped_r1} && cat {output.tmp}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > {output.unmapped_r2} && rm -f {output.tmp}.Unmapped.out* && mv -f {output.tmp}*Log.out {output.log} && mv -f {output.tmp}*Log.final.out {output.log_final} && rm -f {output.tmp}*.Log.progress.out && mv {output.tmp}*.out* {params.tocopy} 2>> {log} && touch {output.tmp} "
+
+
+rule sortsam:
+    input:  mapps = rules.mapping.output.mapped
+    output: sortedsam = report("MAPPED/{combo}/{file}_mapped_sorted.sam.gz", category="SORTING"),
+            tmphead = temp("MAPPED/{combo}/{file}_mapped_header.gz"),
+            tmpfile = temp("TMP/{combo}/{file}")
+    log:    "LOGS/{combo}/{file}/sortsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 100
+    params: linkto = lambda wildcards, output: os.path.basename(output.sortedsam),
+            sortmem = get_sortmem
+    shell: "set +o pipefail; export LC_ALL=C; samtools view -H {input.mapps}|grep '^@HD' |pigz -p 1 -f > {output.tmphead} 2> {log}; samtools view -H {input.mapps}|grep '^@SQ'|sort -t$'\t' -k1,1 -k2,2V |pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep '^@RG'|pigz -p 1 -f >> {output.tmphead} 2>> {log}; samtools view -H {input.mapps}|grep -P '^@PG'|pigz -p {threads} -f >> {output.tmphead} 2>> {log}; samtools view -h {input.mapps} | grep -v \"^@\"|sort --parallel={threads} -S {params.sortmem}G -T TMP -t$'\t' -k3,3V -k4,4n - |pigz -p {threads} -f > {output.tmpfile} 2>> {log}; cat {output.tmphead} {output.tmpfile} > {output.sortedsam} 2>> {log}"
+rule sam2bam:
+    input:  sortedsam = rules.sortsam.output.sortedsam
+    output: bam = report("MAPPED/{combo}/{file}_mapped_sorted.bam", category="2BAM"),
+            bamindex = "MAPPED/{combo}/{file}_mapped_sorted.bam.bai"
+    log:    "LOGS/{combo}/{file}/sam2bam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins = BINS
+    shell: "zcat {input.sortedsam} | samtools view -bS - > {output.bam} && samtools index {output.bam} 2> {log}"
+rule uniqsam:
+    input:  sortedsam = rules.sortsam.output.sortedsam,
+            bam = rules.sam2bam.output
+    output: uniqsam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.sam.gz", category="UNIQUE")
+    log: "LOGS/{combo}/{file}/uniqsam.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    params: bins=BINS
+    shell:  "{params.bins}/Shells/UniqueSam_woPicard.sh {input.sortedsam} {output.uniqsam} {threads} 2> {log}"
+rule sam2bamuniq:
+    input: uniqsam = rules.uniqsam.output,
+           bam = rules.sam2bam.output
+    output:  uniqbam = report("MAPPED/{combo}/{file}_mapped_sorted_unique.bam", category="2BAM"),
+             uniqbamindex = "MAPPED/{combo}/{file}_mapped_sorted_unique.bam.bai"
+    log:     "LOGS/{combo}/{file}/sam2bamuniq.log"
+    conda: "/envs/samtools.yaml"
+    container: "oras://docker.io/jfallmann/monsda:samtools"
+    threads: MAXTHREAD
+    priority: 50
+    params: bins = BINS
+    shell: "zcat {input.uniqsam} | samtools view -bS - > {output.uniqbam} && samtools index {output.uniqbam} 2> {log}"
+
+
+if rundedup:
+    if paired == 'paired':
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),                    
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        if prededup:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo,type=["sorted", "sorted_unique"])  
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"                    
+        else:
+            rule multiqc:
+                input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                        expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),                 
+                        expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_dedupmapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.qc_uniquededup.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo),
+                        expand(rules.dedupbam.output.bam, file=samplecond(SAMPLES, config), combo=combo, type=["sorted", "sorted_unique"])
+                output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                        tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                        lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+                log:    "LOGS/{combo}/{condition}_multiqc.log"
+                conda: "/envs/qc.yaml"
+                container: "oras://docker.io/jfallmann/monsda:qc"
+                threads: 1
+                params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+                shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+else:
+    if paired == 'paired':
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), read=['R1','R2'], combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+    else:
+        rule multiqc:
+            input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo),
+                    expand(rules.qc_trimmed.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_mapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.qc_uniquemapped.output.o1, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bam.output.bam, file=samplecond(SAMPLES, config), combo=combo),
+                    expand(rules.sam2bamuniq.output.uniqbam, file=samplecond(SAMPLES, config), combo=combo)
+            output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                    tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                    lst = "QC/Multi/{combo}/{condition}/qclist.txt"
+            log:    "LOGS/{combo}/{condition}_multiqc.log"
+            conda: "/envs/qc.yaml"
+            container: "oras://docker.io/jfallmann/monsda:qc"
+            threads: 1
+            params:  qpara = lambda wildcards: tool_params(SAMPLES[0], None, config, 'QC', QCENV)['OPTIONS'].get('MULTI', "")
+            shell:  "OUT=$(dirname {output.html}); for i in {input};do echo $(dirname \"${{i}}\") >> {output.tmp};done; cat {output.tmp} |sort -u > {output.lst};export LC_ALL=C.UTF-8; multiqc -f {params.qpara} --exclude picard --exclude gatk -k json -z -s -o $OUT -l {output.lst} 2> {log}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/golden/config_Test/smk/_filelist.txt b/tests/golden/config_Test/smk/_filelist.txt
new file mode 100644
index 00000000..2b49db00
--- /dev/null
+++ b/tests/golden/config_Test/smk/_filelist.txt
@@ -0,0 +1,12 @@
+Test_fastqc-bbduk-picard-bwa_subconfig.json
+Test_fastqc-bbduk-picard-bwa_subsnake.smk
+Test_fastqc-bbduk-picard-star_subconfig.json
+Test_fastqc-bbduk-picard-star_subsnake.smk
+Test_fastqc-cutadapt-picard-bwa_subconfig.json
+Test_fastqc-cutadapt-picard-bwa_subsnake.smk
+Test_fastqc-cutadapt-picard-star_subconfig.json
+Test_fastqc-cutadapt-picard-star_subsnake.smk
+Test_fastqc-trimgalore-picard-bwa_subconfig.json
+Test_fastqc-trimgalore-picard-bwa_subsnake.smk
+Test_fastqc-trimgalore-picard-star_subconfig.json
+Test_fastqc-trimgalore-picard-star_subsnake.smk
diff --git a/tests/test_Utils.py b/tests/test_Utils.py
index 0706af65..d2b6dcf4 100755
--- a/tests/test_Utils.py
+++ b/tests/test_Utils.py
@@ -2,6 +2,7 @@
 import io
 import os
 import shutil
+import tempfile
 import unittest
 from contextlib import redirect_stdout
 
@@ -11,6 +12,7 @@
     NestedDefaultDict,
     add_to_innermost_key_by_list,
     check_ref,
+    cluster2trna,
     comment_remover,
     convertcol,
     depth,
@@ -42,6 +44,7 @@
     rmempty,
     sub_dict,
     subset_dict,
+    toarray,
     value_extract,
     yield_from_dict,
 )
@@ -177,9 +180,16 @@ def test_gethighest_dict(self):
         self.assertEqual(gethighest_dict(a, n), {"a": 5, "e": 4, "c": 3})
 
     def test_toarray(self):
-        # Assuming toarray function converts a file to np.array
-        # This test might need to create a temporary file to fully test toarray functionality
-        pass
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".tsv", delete=False) as fh:
+            fh.write("a\t1.5\nb\t2.5\nc\tnan\n")
+            path = fh.name
+        try:
+            arr = toarray(path, 1)
+            self.assertEqual(arr[0], 1.5)
+            self.assertEqual(arr[1], 2.5)
+            self.assertTrue(np.isnan(arr[2]))
+        finally:
+            os.remove(path)
 
     def test_convertcol(self):
         self.assertEqual(convertcol("1.23"), 1.23)
@@ -215,9 +225,24 @@ def test_idfromfa(self):
         self.assertEqual(idfromfa(id), ["cluster1", "chr19", "+"])
 
     def test_cluster2trna(self):
-        # Assuming cluster2trna function converts cluster to tRNA
-        # This test might need to create a temporary file to fully test cluster2trna functionality
-        pass
+        fasta = (
+            ">cluster1:chr19.tRNA5-LysCTT(+)\nACGT\n"
+            ">cluster1:chr19.tRNA6-LysCTT(+)\nTGCA\n"
+            ">cluster2:chr1.tRNA25-ArgTCT(-)\nGGGG\n"
+        )
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".fa", delete=False) as fh:
+            fh.write(fasta)
+            path = fh.name
+        try:
+            result = cluster2trna(path)
+            self.assertEqual(
+                result["cluster"]["CLUSTER1"], ["TRNA5-LYSCTT", "TRNA6-LYSCTT"]
+            )
+            self.assertEqual(result["cluster"]["CLUSTER2"], ["TRNA25-ARGTCT"])
+            self.assertEqual(result["tRNA"]["CHR19"]["+"], ["TRNA5-LYSCTT", "TRNA6-LYSCTT"])
+            self.assertEqual(result["tRNA"]["CHR1"]["-"], ["TRNA25-ARGTCT"])
+        finally:
+            os.remove(path)
 
     def test_check_ref(self):
         reference = "test_ref"
diff --git a/tests/test_Workflows.py b/tests/test_Workflows.py
new file mode 100644
index 00000000..45f1e1b0
--- /dev/null
+++ b/tests/test_Workflows.py
@@ -0,0 +1,357 @@
+import glob
+import json
+import os
+from pathlib import Path
+
+import pytest
+
+pytest.importorskip("snakemake")
+
+from snakemake.common.configfile import load_configfile  # noqa: E402
+
+import MONSDA.Params as mp  # noqa: E402
+import MONSDA.Workflows as mw  # noqa: E402
+
+REPO = Path(__file__).resolve().parents[1]
+DATA = REPO / "tests" / "data"
+GOLDEN = REPO / "tests" / "golden"
+
+UPDATE = bool(os.environ.get("UPDATE_GOLDEN"))
+
+
+@pytest.fixture(autouse=True)
+def _repo_template_paths():
+    """Point the generators at the in-repo workflow/env templates.
+
+    In a dev checkout MONSDA is not pip-installed, so the module-level
+    ``workflowpath``/``envpath`` (which assume a site-packages layout) do not
+    resolve. Redirect them to the repository copies for the duration of a test.
+    """
+    old_wf, old_env = mw.workflowpath, mw.envpath
+    mw.workflowpath = str(REPO / "workflows")
+    mw.envpath = str(REPO / "envs") + os.sep
+    yield
+    mw.workflowpath, mw.envpath = old_wf, old_env
+
+
+def _load(name):
+    return load_configfile(str(DATA / name))
+
+
+def _normalize(text, workdir):
+    return text.replace(str(workdir), "").replace(str(REPO), "")
+
+
+def _check_golden(relpath, content):
+    path = GOLDEN / relpath
+    if UPDATE:
+        path.parent.mkdir(parents=True, exist_ok=True)
+        path.write_text(content)
+        return
+    assert path.exists(), (
+        f"missing golden snapshot {relpath!s}; regenerate with UPDATE_GOLDEN=1"
+    )
+    assert content == path.read_text(), (
+        f"generator output drifted from golden snapshot {relpath!s}; "
+        f"review the diff and, if intended, regenerate with UPDATE_GOLDEN=1"
+    )
+
+
+@pytest.fixture
+def workdir(tmp_path):
+    """A temp working directory exposing tests/data inputs via symlinks."""
+    for item in os.listdir(DATA):
+        os.symlink(DATA / item, tmp_path / item)
+    old = os.getcwd()
+    os.chdir(tmp_path)
+    try:
+        yield tmp_path
+    finally:
+        os.chdir(old)
+
+
+# --------------------------------------------------------------------------- #
+# Unit tests: pure helpers
+# --------------------------------------------------------------------------- #
+
+
+class TestCreateSubworkflow:
+    def test_returns_tool_and_config(self):
+        cfg = _load("config_fgumi_test.json")
+        conditions = mp.get_conditions(cfg)
+        toollist, configs = mw.create_subworkflow(cfg, "DEDUP", conditions)
+        assert toollist == [["fgumi", "fgumi"]]
+        assert len(configs) == 1
+        assert configs[0]["RUNDEDUP"] == "enabled"
+        assert "fgumi" in configs[0]["DEDUP"]["Test"]["umi"]
+
+    def test_tools_key_without_env_bin(self):
+        cfg = _load("config_Test.json")
+        conditions = mp.get_conditions(cfg)
+        toollist, configs = mw.create_subworkflow(cfg, "MAPPING", conditions)
+        tools = sorted(t[0] for t in toollist)
+        assert tools
+        assert all(len(pair) == 2 for pair in toollist)
+
+    def test_missing_env_bin_logs_error(self, caplog):
+        cfg = {
+            "MAXTHREADS": "1",
+            "WORKFLOWS": "MAPPING",
+            "SETTINGS": {"Test": {"umi": {"SAMPLES": ["S1"]}}},
+            "MAPPING": {"Test": {"umi": {}}},
+        }
+        with caplog.at_level("ERROR"):
+            toollist, _ = mw.create_subworkflow(cfg, "MAPPING", [("Test", "umi")])
+        assert toollist == []
+        assert any(
+            "No tool environment found for MAPPING" in r.message
+            for r in caplog.records
+        )
+        assert any(
+            "No tool binary found for MAPPING" in r.message for r in caplog.records
+        )
+
+
+class TestGetCombo:
+    def test_single_tool(self):
+        cfg = _load("config_fgumi_test.json")
+        conditions = mp.get_conditions(cfg)
+        combos = mw.get_combo(["DEDUP"], cfg, conditions)
+        materialized = {c: [list(x) for x in combos[c]] for c in combos}
+        assert materialized == {("Test", "umi"): [[{"DEDUP": "fgumi"}]]}
+
+    def test_none_when_no_workflows(self):
+        cfg = _load("config_fgumi_test.json")
+        conditions = mp.get_conditions(cfg)
+        assert mw.get_combo(None, cfg, conditions) is None
+        assert mw.get_combo([], cfg, conditions) is None
+
+    def test_multi_tool_product(self):
+        cfg = _load("config_Test.json")
+        conditions = mp.get_conditions(cfg)
+        combos = mw.get_combo(["MAPPING"], cfg, conditions)
+        counts = {c: len(list(combos[c])) for c in combos}
+        assert all(n >= 1 for n in counts.values())
+
+
+# --------------------------------------------------------------------------- #
+# Golden-file / snapshot tests: file-emitting generators
+# --------------------------------------------------------------------------- #
+
+
+def _generate(engine, workdir):
+    cfg = _load("config_Test.json")
+    conditions = mp.get_conditions(cfg)
+    _pre, sub, _post = mw.get_processes(cfg)
+    samples = mp.get_samples(cfg)
+    combos = mw.get_combo(sub, cfg, conditions)
+    if engine == "smk":
+        subdir = "SubSnakes"
+        mp.create_skeleton(subdir, None)
+        mw.make_sub(sub, cfg, samples, conditions, subdir, "INFO", combinations=combos)
+    else:
+        subdir = "SubFlows"
+        mp.create_skeleton(subdir, None)
+        mw.nf_make_sub(
+            sub, cfg, samples, conditions, subdir, "INFO", combinations=combos
+        )
+    return subdir
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_sub_golden(engine, workdir):
+    subdir = _generate(engine, workdir)
+    produced = sorted(
+        os.path.relpath(p, workdir) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    assert produced, "generator produced no files"
+
+    for rel in produced:
+        raw = Path(workdir / rel).read_text()
+        if rel.endswith(".json"):
+            raw = json.dumps(json.loads(raw), indent=2, sort_keys=True)
+        content = _normalize(raw, workdir)
+        _check_golden(f"config_Test/{engine}/{os.path.basename(rel)}", content)
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_sub_file_set_stable(engine, workdir):
+    subdir = _generate(engine, workdir)
+    names = sorted(
+        os.path.basename(p) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    _check_golden(f"config_Test/{engine}/_filelist.txt", "\n".join(names) + "\n")
+
+
+# --------------------------------------------------------------------------- #
+# Golden-file snapshot for the DTU postprocessing trio (drimseq/dexseq/spit).
+# These share a large amount of scaffolding; the snapshot pins current output
+# so the shared-base extraction can be reviewed as an explicit, minimal diff.
+# --------------------------------------------------------------------------- #
+
+
+def _generate_dtu(engine, workdir):
+    cfg = _load("config_DTU_test.json")
+    conditions = mp.get_conditions(cfg)
+    _pre, sub, _post = mw.get_processes(cfg)
+    combos = mw.get_combo(sub, cfg, conditions)
+    samples = mp.get_samples_postprocess(cfg, "DTU")
+    if engine == "smk":
+        subdir = "SubPostSnakes"
+        mp.create_skeleton(subdir, None)
+        mw.make_post(
+            "DTU", cfg, samples, conditions, subdir, "INFO", combinations=combos
+        )
+    else:
+        subdir = "SubPostFlows"
+        mp.create_skeleton(subdir, None)
+        mw.nf_make_post(
+            "DTU", cfg, samples, conditions, subdir, "INFO", combinations=combos
+        )
+    return subdir
+
+
+def _sort_string_lists(node):
+    """Sort lists whose members are all strings so set-derived orderings
+    (e.g. postprocess SAMPLES) do not make the snapshot flaky."""
+    if isinstance(node, dict):
+        return {k: _sort_string_lists(v) for k, v in node.items()}
+    if isinstance(node, list):
+        items = [_sort_string_lists(v) for v in node]
+        if all(isinstance(v, str) for v in items):
+            return sorted(items)
+        return items
+    return node
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_post_dtu_golden(engine, workdir):
+    subdir = _generate_dtu(engine, workdir)
+    produced = sorted(
+        os.path.relpath(p, workdir) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    assert produced, "generator produced no files"
+
+    for rel in produced:
+        raw = Path(workdir / rel).read_text()
+        if rel.endswith(".json"):
+            data = _sort_string_lists(json.loads(raw))
+            raw = json.dumps(data, indent=2, sort_keys=True)
+        content = _normalize(raw, workdir)
+        _check_golden(f"config_DTU/{engine}/{os.path.basename(rel)}", content)
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_post_dtu_file_set_stable(engine, workdir):
+    subdir = _generate_dtu(engine, workdir)
+    names = sorted(
+        os.path.basename(p) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    _check_golden(f"config_DTU/{engine}/_filelist.txt", "\n".join(names) + "\n")
+
+
+# --------------------------------------------------------------------------- #
+# Cross-engine consistency: the Snakemake and Nextflow generators must expose
+# the same tool matrix, env/bin selection and per-step OPTIONS for one config.
+# The on-disk nesting differs between engines (smk nests OPTIONS under the tool
+# name, nf flattens it), so comparisons are made on the semantic content.
+# --------------------------------------------------------------------------- #
+
+WORKFLOW_KEYS = ("QC", "TRIMMING", "MAPPING", "DEDUP", "COUNTING", "TRACKS")
+
+
+def _combo_name(fname):
+    for suffix in ("_subconfig.json", "_subsnake.smk", "_subflow.nf"):
+        if fname.endswith(suffix):
+            return fname[: -len(suffix)]
+    return fname
+
+
+def _subconfigs_by_combo(subdir):
+    out = {}
+    for path in glob.glob(os.path.join(subdir, "*_subconfig.json")):
+        with open(path) as fh:
+            out[_combo_name(os.path.basename(path))] = json.load(fh)
+    return out
+
+
+def _collect_options(node):
+    """Recursively merge every OPTIONS dict found under a config subtree."""
+    merged = {}
+    if isinstance(node, dict):
+        for key, val in node.items():
+            if key == "OPTIONS" and isinstance(val, dict):
+                merged.update(val)
+            else:
+                merged.update(_collect_options(val))
+    return merged
+
+
+@pytest.fixture
+def both_engines(workdir):
+    smk = _subconfigs_by_combo(_generate("smk", workdir))
+    nf = _subconfigs_by_combo(_generate("nf", workdir))
+    return smk, nf
+
+
+def test_engines_expose_same_combos(both_engines):
+    smk, nf = both_engines
+    assert set(smk) == set(nf)
+    assert smk, "no combos generated"
+
+
+def test_engines_expose_same_env_and_bin(both_engines):
+    smk, nf = both_engines
+    for combo in smk:
+        sc, nc = smk[combo], nf[combo]
+        scalars = {
+            k: v
+            for k, v in sc.items()
+            if k.endswith(("ENV", "BIN")) and isinstance(v, str)
+        }
+        for key, val in scalars.items():
+            assert nc.get(key) == val, (
+                f"{combo}: {key} drift smk={val!r} nf={nc.get(key)!r}"
+            )
+
+
+def test_engines_expose_same_options(both_engines):
+    smk, nf = both_engines
+    for combo in smk:
+        sc, nc = smk[combo], nf[combo]
+        for wf in WORKFLOW_KEYS:
+            if wf not in sc and wf not in nc:
+                continue
+            smk_opts = _collect_options(sc.get(wf, {}))
+            nf_opts = _collect_options(nc.get(wf, {}))
+            assert smk_opts == nf_opts, (
+                f"{combo}/{wf}: OPTIONS drift smk={smk_opts!r} nf={nf_opts!r}"
+            )
+
+
+def test_engines_expose_same_tools(both_engines):
+    smk, nf = both_engines
+    for combo in smk:
+        sc, nc = smk[combo], nf[combo]
+        for wf in WORKFLOW_KEYS:
+            if wf not in sc and wf not in nc:
+                continue
+            assert sc.get(wf, {}).get("TOOLS") == nc.get(wf, {}).get("TOOLS"), (
+                f"{combo}/{wf}: TOOLS drift"
+            )
+
+
+def test_oras_namespace_and_registry_rewrite():
+    line = 'container: "oras://jfallmann/monsda:"+MAPPERENV'
+    try:
+        assert mw._rewrite_oras(line) == (
+            'container: "oras://docker.io/jfallmann/monsda:"+MAPPERENV'
+        )
+        mw.set_oras_namespace("myuser/myrepo")
+        mw.set_oras_registry("ghcr.io")
+        assert mw._rewrite_oras(line) == (
+            'container: "oras://ghcr.io/myuser/myrepo:"+MAPPERENV'
+        )
+    finally:
+        mw.set_oras_namespace(mw.oras_default_namespace)
+        mw.set_oras_registry("docker.io")
diff --git a/workflows/dexseq_DTU.nf b/workflows/dexseq_DTU.nf
index aa36fc82..f4f6d2b0 100644
--- a/workflows/dexseq_DTU.nf
+++ b/workflows/dexseq_DTU.nf
@@ -24,6 +24,8 @@ COUNTENV = 'salmon'
 
 //DTU PROCESSES
 
+include { salmon_quant; create_summary_snippet; terminus_collapse } from "dtu_base.nf"
+
 process salmon_idx{
     conda "$COUNTENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$COUNTENV"
@@ -59,68 +61,6 @@ process salmon_idx{
 
 }
 
-process salmon_quant{
-    conda "$COUNTENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
-    }
-
-    input:
-    path reads
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-
-    idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
-            stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
-            stranded = '-l ISR'
-        }else{
-            stranded = '-l IU'
-        }
-        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
-        r1 = rs[0]
-        r2 = rs[1]
-        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
-            stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
-            stranded = '-l SR'
-        }else{
-            stranded = '-l U'
-        }
-        read = reads[1]
-        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }
-}
-
 process prepare_dtu_annotation{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -185,34 +125,6 @@ process run_dexseq{
     """
 }
 
-process create_summary_snippet{
-    conda "$DTUENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'link',
-    saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
-    }
-
-    input:
-    path de
-
-    output:
-    path "*.Rmd", emit: snps
-    path "log", emit: log
-
-    script:
-    inlist = de.toString()
-    // inlist = de.toList()  // { $workflow.projectDir += "$it.code,"  }
-    """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
-    """
-}
-
 process collect_dexseq{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -229,32 +141,6 @@ process collect_dexseq{
     """
 }
 
-process terminus_collapse{
-    conda "terminus.yaml"
-    container "oras://jfallmann/monsda:terminus"
-    cpus THREADS
-	cache 'lenient'
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
-    }
-
-    input:
-    path counts
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-    lf = "terminus_collapse.log"
-    """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
-    """
-}
-
 workflow DTU{ 
     take: collection
 
diff --git a/workflows/dexseq_DTU.smk b/workflows/dexseq_DTU.smk
index 79187ab8..ff8eada9 100644
--- a/workflows/dexseq_DTU.smk
+++ b/workflows/dexseq_DTU.smk
@@ -25,114 +25,7 @@ rule themall:
             # sig_u   = expand("DTU/{combo}/Tables/SigUP_DTU_DEXSEQ_{scombo}_{comparison}_results.tsv.gz", combo=combo, scombo=scombo, comparison=compstr),
             Rmd     = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
 
-rule salmon_index:
-    input:  fa = REFERENCE
-    output: idx = directory(INDEX),
-            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
-    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)    
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: MAXTHREAD
-    params: mapp = COUNTBIN,
-            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
-            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
-            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
-    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
-
-
-if paired == 'paired':
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
-                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_R1_trimmed.fastq.gz",
-                r2 = "TRIMMED_FASTQ/{scombo}/{file}_R2_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
-                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
-        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
-
-else:
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
-        shell:  "ln -s {params.filetolink} {output.r1}"
-        
-
-if paired == 'paired':
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
-                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-else:
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-
-if runterminus:
-    rule terminus_group:
-        input:  ctsdir = rules.mapping.output.ctsdir
-        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
-        log:    "LOGS/{combo}/{file}/terminus_group.log"
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: tpara = termpara,
-                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
-        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
-
-    rule terminus_collapse:
-        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
-                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
-        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
-        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
-
-    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
-else:
-    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-
-rule create_annotation_table:
-    input:  dir  = ctsource,
-    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
-    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: 1
-    params: dereps = lambda wildcards, input: get_reps(input.dir, config,'DTU'),
-            bins = BINS
-    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+include: "dtu_base.smk"
 
 rule run_DTU:
     input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)
diff --git a/workflows/drimseq_DTU.nf b/workflows/drimseq_DTU.nf
index dbc28e22..f5b86d86 100644
--- a/workflows/drimseq_DTU.nf
+++ b/workflows/drimseq_DTU.nf
@@ -24,6 +24,8 @@ COUNTENV = 'salmon'
 
 //DTU PROCESSES
 
+include { salmon_quant; create_summary_snippet; terminus_collapse } from "dtu_base.nf"
+
 process salmon_idx{
     conda "$COUNTENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$COUNTENV"
@@ -59,68 +61,6 @@ process salmon_idx{
 
 }
 
-process salmon_quant{
-    conda "$COUNTENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
-    }
-
-    input:
-    path reads
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-
-    idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
-            stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
-            stranded = '-l ISR'
-        }else{
-            stranded = '-l IU'
-        }
-        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
-        r1 = rs[0]
-        r2 = rs[1]
-        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
-            stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
-            stranded = '-l SR'
-        }else{
-            stranded = '-l U'
-        }
-        read = reads[1]
-        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }
-}
-
 process prepare_dtu_annotation{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -186,34 +126,6 @@ process run_drimseq{
     """
 }
 
-process create_summary_snippet{
-    conda "$DTUENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'link',
-    saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
-    }
-
-    input:
-    path de
-
-    output:
-    path "*.Rmd", emit: snps
-    path "log", emit: log
-
-    script:
-    inlist = de.toString()
-    // inlist = de.toList()  // { $workflow.projectDir += "$it.code,"  }
-    """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
-    """
-}
-
 process collect_drimseq{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -230,32 +142,6 @@ process collect_drimseq{
     """
 }
 
-process terminus_collapse{
-    conda "terminus.yaml"
-    container "oras://jfallmann/monsda:terminus"
-    cpus THREADS
-	cache 'lenient'
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
-    }
-
-    input:
-    path counts
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-    lf = "terminus_collapse.log"
-    """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
-    """
-}
-
 workflow DTU{ 
     take: collection
 
diff --git a/workflows/drimseq_DTU.smk b/workflows/drimseq_DTU.smk
index db41825a..dd48d51a 100644
--- a/workflows/drimseq_DTU.smk
+++ b/workflows/drimseq_DTU.smk
@@ -35,112 +35,7 @@ rule themall:
             sig_ug   = expand("DTU/{combo}/Tables/SigUP_DTU_DRIMSEQ_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
             Rmd = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
 
-rule salmon_index:
-    input:  fa = REFERENCE
-    output: idx = directory(INDEX),
-            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
-    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: MAXTHREAD
-    params: mapp = COUNTBIN,
-            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
-            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
-            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
-    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
-
-if paired == 'paired':
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
-                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
-                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
-                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
-        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
-
-else:
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
-        shell:  "ln -s {params.filetolink} {output.r1}"
-
-if paired == 'paired':
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
-                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-else:
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-
-if runterminus:
-    rule terminus_group:
-        input:  ctsdir = rules.mapping.output.ctsdir
-        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
-        log:    "LOGS/{combo}/{file}/terminus_group.log"
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: tpara = termpara,
-                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
-        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
-
-    rule terminus_collapse:
-        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
-                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
-        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
-        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
-
-    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
-else:
-    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-
-rule create_annotation_table:
-    input:  dir  = ctsource,
-    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
-    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: 1
-    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
-            bins = BINS
-    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+include: "dtu_base.smk"
 
 rule run_DTU:
     input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)
diff --git a/workflows/dtu_base.nf b/workflows/dtu_base.nf
new file mode 100644
index 00000000..d37b1e65
--- /dev/null
+++ b/workflows/dtu_base.nf
@@ -0,0 +1,117 @@
+//DTU shared processes
+
+process salmon_quant{
+    conda "$COUNTENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$COUNTENV"
+    cpus THREADS
+	cache 'lenient'
+    //validExitStatus 0,1
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+    }
+
+    input:
+    path reads
+
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+
+    script:
+
+    idx = reads[0]
+    if (PAIRED == 'paired'){
+        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+            stranded = '-l ISF'
+        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+            stranded = '-l ISR'
+        }else{
+            stranded = '-l IU'
+        }
+        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
+        r1 = rs[0]
+        r2 = rs[1]
+        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }else{
+        if (STRANDED == 'fr' || STRANDED == 'SF'){
+            stranded = '-l SF'
+        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+            stranded = '-l SR'
+        }else{
+            stranded = '-l U'
+        }
+        read = reads[1]
+        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
+        lf = "salmon_"+fn+".log"
+        of = fn+"/quant.sf"
+        oz = fn+"/quant.sf.gz"
+        ol = fn+"_counts.gz"
+        """
+        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        """
+    }
+}
+
+process create_summary_snippet{
+    conda "$DTUENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$DTUENV"
+    cpus THREADS
+	cache 'lenient'
+    //validExitStatus 0,1
+
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
+    }
+
+    input:
+    path de
+
+    output:
+    path "*.Rmd", emit: snps
+    path "log", emit: log
+
+    script:
+    inlist = de.toString()
+    // inlist = de.toList()  // { $workflow.projectDir += "$it.code,"  }
+    """
+    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    """
+}
+
+process terminus_collapse{
+    conda "terminus.yaml"
+    container "oras://jfallmann/monsda:terminus"
+    cpus THREADS
+	cache 'lenient'
+
+    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
+    saveAs: {filename ->
+        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+    }
+
+    input:
+    path counts
+
+    output:
+    path "*.gz", emit: counts
+    path "*.log", emit: logs
+
+    script:
+    lf = "terminus_collapse.log"
+    """
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    """
+}
diff --git a/workflows/dtu_base.smk b/workflows/dtu_base.smk
new file mode 100644
index 00000000..e759eceb
--- /dev/null
+++ b/workflows/dtu_base.smk
@@ -0,0 +1,106 @@
+rule salmon_index:
+    input:  fa = REFERENCE
+    output: idx = directory(INDEX),
+            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
+    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: MAXTHREAD
+    params: mapp = COUNTBIN,
+            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
+            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
+            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
+    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
+
+if paired == 'paired':
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
+                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_R1_trimmed.fastq.gz",
+                r2 = "TRIMMED_FASTQ/{scombo}/{file}_R2_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
+                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
+        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
+
+else:
+    rule simulate_trim:
+        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
+        output: r1 = "TRIMMED_FASTQ/{scombo}/{file}_trimmed.fastq.gz"
+        threads: 1
+        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
+        shell:  "ln -s {params.filetolink} {output.r1}"
+
+if paired == 'paired':
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
+                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  ""+COUNTENV+".yaml"
+        container: "oras://jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+
+else:
+    rule mapping:
+        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
+                index = rules.salmon_index.output.idx,
+                uix = rules.salmon_index.output.uidx
+        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
+                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
+        log:    "LOGS/{combo}/{file}/salmonquant.log"
+        conda:  ""+COUNTENV+".yaml"
+        container: "oras://jfallmann/monsda:"+COUNTENV+""
+        threads: MAXTHREAD
+        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
+                mapp=COUNTBIN,
+                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
+                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
+        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
+
+
+if runterminus:
+    rule terminus_group:
+        input:  ctsdir = rules.mapping.output.ctsdir
+        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
+        log:    "LOGS/{combo}/{file}/terminus_group.log"
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: tpara = termpara,
+                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
+        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
+
+    rule terminus_collapse:
+        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
+                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
+        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
+        conda:  ""+TERMINUSENV+".yaml"
+        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
+        threads: MAXTHREAD
+        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
+        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
+
+    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
+
+rule create_annotation_table:
+    input:  dir  = ctsource,
+    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
+    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
+    conda:  ""+COUNTENV+".yaml"
+    container: "oras://jfallmann/monsda:"+COUNTENV+""
+    threads: 1
+    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
+            bins = BINS
+    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
diff --git a/workflows/spit_DTU.nf b/workflows/spit_DTU.nf
index 65b8bf3f..cf598cb9 100644
--- a/workflows/spit_DTU.nf
+++ b/workflows/spit_DTU.nf
@@ -24,6 +24,8 @@ COUNTENV = 'salmon'
 
 //DTU PROCESSES
 
+include { salmon_quant; create_summary_snippet; terminus_collapse } from "dtu_base.nf"
+
 process salmon_idx{
     conda "$COUNTENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$COUNTENV"
@@ -59,68 +61,6 @@ process salmon_idx{
 
 }
 
-process salmon_quant{
-    conda "$COUNTENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
-    }
-
-    input:
-    path reads
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-
-    idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
-            stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
-            stranded = '-l ISR'
-        }else{
-            stranded = '-l IU'
-        }
-        rs = reads[1..2].sort { a,b -> a[0] <=> b[0] == 0 ? (a[1..-1] as int) <=> (b[1..-1] as int) : a[0] <=> b[0] }
-        r1 = rs[0]
-        r2 = rs[1]
-        fn = file(r1).getSimpleName().replaceAll(/\Q_R1_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
-            stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
-            stranded = '-l SR'
-        }else{
-            stranded = '-l U'
-        }
-        read = reads[1]
-        fn = file(reads[1]).getSimpleName().replaceAll(/\Q_trimmed\E/,"")
-        lf = "salmon_"+fn+".log"
-        of = fn+"/quant.sf"
-        oz = fn+"/quant.sf.gz"
-        ol = fn+"_counts.gz"
-        """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
-        """
-    }
-}
-
 process prepare_dtu_annotation{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -184,34 +124,6 @@ process run_spit{
     """
 }
 
-process create_summary_snippet{
-    conda "$DTUENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
-	cache 'lenient'
-    //validExitStatus 0,1
-
-    publishDir "${workflow.workDir}/../" , mode: 'link',
-    saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
-    }
-
-    input:
-    path de
-
-    output:
-    path "*.Rmd", emit: snps
-    path "log", emit: log
-
-    script:
-    inlist = de.toString()
-    // inlist = de.toList()  // { $workflow.projectDir += "$it.code,"  }
-    """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
-    """
-}
-
 process collect_spit{
     conda "$DTUENV"+".yaml"
     container "oras://jfallmann/monsda:"+"$DTUENV"
@@ -228,32 +140,6 @@ process collect_spit{
     """
 }
 
-process terminus_collapse{
-    conda "terminus.yaml"
-    container "oras://jfallmann/monsda:terminus"
-    cpus THREADS
-	cache 'lenient'
-
-    publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
-    saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
-    }
-
-    input:
-    path counts
-
-    output:
-    path "*.gz", emit: counts
-    path "*.log", emit: logs
-
-    script:
-    lf = "terminus_collapse.log"
-    """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
-    """
-}
-
 workflow DTU{ 
     take: collection
 
diff --git a/workflows/spit_DTU.smk b/workflows/spit_DTU.smk
index 1d09d589..4bf7bdb2 100644
--- a/workflows/spit_DTU.smk
+++ b/workflows/spit_DTU.smk
@@ -32,112 +32,7 @@ rule themall:
             sig_ug   = expand("DTU/{combo}/Tables/SigUP_DTU_SPIT_{scombo}_{comparison}_table_genes.tsv.gz", combo=combo, comparison=compstr, scombo=scombo),
             Rmd = expand("REPORTS/SUMMARY/RmdSnippets/{combo}.Rmd", combo=combo)
 
-rule salmon_index:
-    input:  fa = REFERENCE
-    output: idx = directory(INDEX),
-            uidx = directory(expand("{refd}/INDICES/{mape}_{unikey}", refd=REFDIR, mape=COUNTENV, unikey=unik))
-    log:    expand("LOGS/{sets}/{cape}.idx.log", sets=SETS, cape=COUNTENV)
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: MAXTHREAD
-    params: mapp = COUNTBIN,
-            ipara = lambda wildcards, input: tool_params(SAMPLES[0], None, config, 'DTU', DTUENV)['OPTIONS'].get('INDEX', ""),
-            decoy = f"-d {os.path.abspath(DECOY)}" if DECOY else '',
-            linkidx = lambda wildcards, output: str(os.path.abspath(output.uidx[0]))
-    shell:  "set +euo pipefail; {params.mapp} index {params.ipara} {params.decoy} -p {threads} -t {input.fa} -i {output.uidx} &>> {log} && ln -fs {params.linkidx} {output.idx}"
-
-if paired == 'paired':
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}_R1.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_R1_dedup.fastq.gz",
-                r2 = lambda wildcards: "FASTQ/{rawfile}_R2.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not prededup else "DEDUP_FASTQ/{combo}/{file}_R2_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
-                r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1)),
-                filetolink2 = lambda w, input: "{r}".format(r=os.path.abspath(input.r2))
-        shell:  "ln -s {params.filetolink} {output.r1} && ln -s {params.filetolink2} {output.r2}"
-
-else:
-    rule simulate_trim:
-        input:  r1 = lambda wildcards: "FASTQ/{rawfile}.fastq.gz".format(rawfile=[x for x in SAMPLES if x.split(os.sep)[-1] in wildcards.file][0]) if not usededup else "DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz"
-        output: r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz"
-        threads: 1
-        params: filetolink = lambda w, input: "{r}".format(r=os.path.abspath(input.r1))
-        shell:  "ln -s {params.filetolink} {output.r1}"
-
-if paired == 'paired':
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R1_trimmed.fastq.gz", scombo=scombo),
-                r2 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_R2_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l ISF' if (stranded == 'fr' or stranded == 'ISF') else '-l ISR' if (stranded == 'rf' or stranded == 'ISR') else '-l IU',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -1 {input.r1} -2 {input.r2} &>> {log} && gzip {output.ctsdir}/quant.sf && ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-else:
-    rule mapping:
-        input:  r1 = expand("TRIMMED_FASTQ/{scombo}/{{file}}_trimmed.fastq.gz", scombo=scombo),
-                index = rules.salmon_index.output.idx,
-                uix = rules.salmon_index.output.uidx
-        output: cnts = report("DTU/{combo}/salmon/{file}_counts.sf.gz", category="COUNTING"),
-                ctsdir = report(directory("DTU/{combo}/salmon/{file}"), category="COUNTING")
-        log:    "LOGS/{combo}/{file}/salmonquant.log"
-        conda:  ""+COUNTENV+".yaml"
-        container: "oras://jfallmann/monsda:"+COUNTENV+""
-        threads: MAXTHREAD
-        params: cpara = lambda wildcards: tool_params(wildcards.file, None, config, 'DTU', DTUENV)['OPTIONS'].get('QUANT', ""),
-                mapp=COUNTBIN,
-                stranded = lambda x: '-l SF' if (stranded == 'fr' or stranded == 'SF') else '-l SR' if (stranded == 'rf' or stranded == 'SR') else '-l U',
-                linksf = lambda wildcards, output: str(os.path.abspath(output.ctsdir))
-        shell: "set +euo pipefail; {params.mapp} quant -p {threads} -i {input.index} {params.stranded} {params.cpara} -o {output.ctsdir} -r {input.r1} &>> {log} && gzip {output.ctsdir}/quant.sf ; ln -fs {params.linksf}/quant.sf.gz {output.cnts} &>> {log}"
-
-
-if runterminus:
-    rule terminus_group:
-        input:  ctsdir = rules.mapping.output.ctsdir
-        output: grp = "DTU/{combo}/terminus/{file}/groups.txt"
-        log:    "LOGS/{combo}/{file}/terminus_group.log"
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: tpara = termpara,
-                outdir = lambda wildcards, output: os.path.dirname(os.path.dirname(str(output.grp)))
-        shell:  "terminus group {params.tpara} -d {input.ctsdir} -o {params.outdir} &> {log}"
-
-    rule terminus_collapse:
-        input:  grps = expand("DTU/{combo}/terminus/{file}/groups.txt", combo=combo, file=samplecond(SAMPLES, config)),
-                dirs = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-        output: cnts = expand("DTU/{combo}/terminus/{file}/quant.sf.gz", combo=combo, file=samplecond(SAMPLES, config))
-        log:    expand("LOGS/DTU/{combo}/terminus_collapse.log", combo=combo)
-        conda:  ""+TERMINUSENV+".yaml"
-        container: "oras://jfallmann/monsda:"+TERMINUSENV+""
-        threads: MAXTHREAD
-        params: outdir = lambda wildcards, input: os.path.dirname(os.path.dirname(str(input.grps[0])))
-        shell:  "terminus collapse -d {input.dirs} -o {params.outdir} &> {log}; for d in {input.dirs}; do b=$(basename $d); gzip -c {params.outdir}/$b/quant.sf > {params.outdir}/$b/quant.sf.gz; done 2>> {log}"
-
-    ctsource = expand("DTU/{combo}/terminus/{file}", combo=combo, file=samplecond(SAMPLES, config))
-else:
-    ctsource = expand(rules.mapping.output.ctsdir, combo=combo, file=samplecond(SAMPLES, config))
-
-rule create_annotation_table:
-    input:  dir  = ctsource,
-    output: anno = expand("DTU/{combo}/Tables/{scombo}_ANNOTATION.gz", combo=combo, scombo=scombo)
-    log:    expand("LOGS/DTU/{combo}/create_DTU_table.log", combo=combo)
-    conda:  ""+COUNTENV+".yaml"
-    container: "oras://jfallmann/monsda:"+COUNTENV+""
-    threads: 1
-    params: dereps = lambda wildcards, input: get_reps(input.dir, config, 'DTU'),
-            bins = BINS
-    shell:  "python3 {params.bins}/Analysis/build_DTU_table.py {params.dereps} --anno {output.anno} --loglevel DEBUG 2> {log}"
+include: "dtu_base.smk"
 
 rule run_DTU:
     input:  anno = expand(rules.create_annotation_table.output.anno, combo=combo, scombo=scombo)

From 38ac8b34f51b83314ffc72e4b7bf7ed72f13ebea Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Sat, 4 Jul 2026 20:03:14 +0200
Subject: [PATCH 07/80] claude streamline of output paths between smk and nf
 where fixes where needed

---
 MONSDA/Workflows.py      |  6 +++---
 workflows/alevinfry.nf   |  4 ++--
 workflows/fastqc_raw.smk | 24 ++++++++++++------------
 workflows/kallisto.nf    |  4 ++--
 workflows/oarfish.nf     |  4 ++--
 workflows/salmon.nf      |  8 ++++----
 workflows/simpleaf.nf    |  4 ++--
 7 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/MONSDA/Workflows.py b/MONSDA/Workflows.py
index 293206e0..9575e4f3 100755
--- a/MONSDA/Workflows.py
+++ b/MONSDA/Workflows.py
@@ -1495,10 +1495,10 @@ def rulethemall(subworkflows, config, loglevel, condapath, logfix, combo=""):
         else 'rule themall:\n\tinput:\texpand("MAPPED/{combo}/{file}_mapped_{type}_dedup.bam", combo=combo, file=samplecond(SAMPLES, config), type=["sorted", "sorted_unique"])'
     )
     allqc = 'expand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
-    allrawqc = 'rule themall:\n\tinput:\texpand("QC/Multi{combo}{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
-    alltrimqc = 'rule themall:\n\tinput:\texpand("QC/Multi{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
+    allrawqc = 'rule themall:\n\tinput:\texpand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
+    alltrimqc = 'rule themall:\n\tinput:\texpand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
     alltrim = 'rule themall:\n\tinput: expand("TRIMMED_FASTQ/{combo}/{file}_{read}_trimmed.fastq.gz", combo=combo, file=samplecond(SAMPLES, config), read=["R1","R2"]) if paired == \'paired\' else expand("TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz", combo=combo, file=samplecond(SAMPLES, config))'
-    alldedupqc = 'rule themall:\n\tinput:\texpand("QC/Multi{combo}/{condition}/multiqc_report.html", combo=combo, condition=str.join(os.sep, conditiononly(SAMPLES[0], config)))'
+    alldedupqc = 'rule themall:\n\tinput:\texpand("QC/Multi/{combo}/{condition}/multiqc_report.html", combo=combo, condition=str.join(os.sep, conditiononly(SAMPLES[0], config)))'
     alldedup = 'rule themall:\n\tinput: expand("DEDUP_FASTQ/{combo}/{file}_{read}_dedup.fastq.gz", combo=combo, file=samplecond(SAMPLES, config), read=["R1","R2"]) if paired == \'paired\' else expand("DEDUP_FASTQ/{combo}/{file}_dedup.fastq.gz", combo=combo, file=samplecond(SAMPLES, config))'
     alltrimdedupqc = 'rule themall:\n\tinput:\texpand("QC/Multi/{combo}/{condition}/multiqc_report.html", condition=str.join(os.sep, conditiononly(SAMPLES[0], config)), combo=combo)'
 
diff --git a/workflows/alevinfry.nf b/workflows/alevinfry.nf
index 9ce314b0..6bbc9efd 100644
--- a/workflows/alevinfry.nf
+++ b/workflows/alevinfry.nf
@@ -36,8 +36,8 @@ process alevinfry_quant{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/alevinfry/${CONDITION}/COUNTING/${file(filename).getName()}"
-        else                                    "COUNTS/${SCOMBO}/alevinfry/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:
diff --git a/workflows/fastqc_raw.smk b/workflows/fastqc_raw.smk
index 1fe7ab37..05efccc4 100644
--- a/workflows/fastqc_raw.smk
+++ b/workflows/fastqc_raw.smk
@@ -4,8 +4,8 @@ if paired == 'paired':
     log.info('Running paired mode QC')
     rule qc_raw:
         input:  r1 = "FASTQ/{rawfile}_{read}.fastq.gz"
-        output: o1 = report("QC{combo}{rawfile}_{read}_fastqc.zip")
-        log:    "LOGS{combo}{rawfile}_fastqc_{read}_raw.log"
+        output: o1 = report("QC/{combo}/{rawfile}_{read}_fastqc.zip")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_{read}_raw.log"
         conda:  ""+QCENV+".yaml"
         container: "oras://jfallmann/monsda:"+QCENV+""
         threads: MAXTHREAD
@@ -14,10 +14,10 @@ if paired == 'paired':
 
     rule multiqc:
         input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), read=['R1','R2'], combo=combo)
-        output: html = report("QC/Multi{combo}{condition}/multiqc_report.html", category="QC"),
-                tmp = temp("QC/Multi{combo}{condition}/tmp"),
-                lst = "QC/Multi{combo}{condition}/qclist_raw.txt"
-        log:    "LOGS{combo}{condition}_multiqc_raw.log"
+        output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                lst = "QC/Multi/{combo}/{condition}/qclist_raw.txt"
+        log:    "LOGS/{combo}/{condition}_multiqc_raw.log"
         conda:  ""+QCENV+".yaml"
         container: "oras://jfallmann/monsda:"+QCENV+""
         threads: 1
@@ -27,8 +27,8 @@ if paired == 'paired':
 else:
     rule qc_raw:
         input:  r1 = "FASTQ/{rawfile}.fastq.gz"
-        output: o1 = report("QC{combo}{rawfile}_fastqc.zip", category="QC")
-        log:    "LOGS{combo}{rawfile}_fastqc_raw.log"
+        output: o1 = report("QC/{combo}/{rawfile}_fastqc.zip", category="QC")
+        log:    "LOGS/{combo}/{rawfile}_fastqc_raw.log"
         conda:  ""+QCENV+".yaml"
         container: "oras://jfallmann/monsda:"+QCENV+""
         threads: MAXTHREAD
@@ -37,10 +37,10 @@ else:
 
     rule multiqc:
         input:  expand(rules.qc_raw.output.o1, rawfile=list(SAMPLES), combo=combo)
-        output: html = report("QC/Multi{combo}{condition}/multiqc_report.html", category="QC"),
-                tmp = temp("QC/Multi{combo}{condition}/tmp"),
-                lst = "QC/Multi{combo}{condition}/qclist_raw.txt"
-        log:    "LOGS{combo}{condition}_multiqc_raw.log"
+        output: html = report("QC/Multi/{combo}/{condition}/multiqc_report.html", category="QC"),
+                tmp = temp("QC/Multi/{combo}/{condition}/tmp"),
+                lst = "QC/Multi/{combo}/{condition}/qclist_raw.txt"
+        log:    "LOGS/{combo}/{condition}_multiqc_raw.log"
         conda:  ""+QCENV+".yaml"
         container: "oras://jfallmann/monsda:"+QCENV+""
         threads: 1
diff --git a/workflows/kallisto.nf b/workflows/kallisto.nf
index 2a7425ef..2b3ecef1 100644
--- a/workflows/kallisto.nf
+++ b/workflows/kallisto.nf
@@ -96,8 +96,8 @@ process kallisto_quant{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/kallisto/${CONDITION}/COUNTING/${file(filename).getName()}"
-        else                                    "COUNTS/${SCOMBO}/kallisto/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:
diff --git a/workflows/oarfish.nf b/workflows/oarfish.nf
index d40851dc..5678bcda 100644
--- a/workflows/oarfish.nf
+++ b/workflows/oarfish.nf
@@ -17,8 +17,8 @@ process oarfish_quant{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/oarfish/${CONDITION}/COUNTING/${file(filename).getName()}"
-        else                                    "COUNTS/${SCOMBO}/oarfish/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:
diff --git a/workflows/salmon.nf b/workflows/salmon.nf
index a63f1576..fc7eb919 100644
--- a/workflows/salmon.nf
+++ b/workflows/salmon.nf
@@ -98,9 +98,9 @@ process salmon_quant{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/COUNTING/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
 
-        else                                    "COUNTS/${SCOMBO}/salmon/${CONDITION}/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:
@@ -161,8 +161,8 @@ process salmon_quant_bam{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/COUNTING/${file(filename).getName()}"
-        else                                    "COUNTS/${SCOMBO}/salmon/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:
diff --git a/workflows/simpleaf.nf b/workflows/simpleaf.nf
index bd6ed619..2e5f92f3 100644
--- a/workflows/simpleaf.nf
+++ b/workflows/simpleaf.nf
@@ -52,8 +52,8 @@ process simpleaf_quant{
 
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/simpleaf/${CONDITION}/COUNTING/${file(filename).getName()}"
-        else                                    "COUNTS/${SCOMBO}/simpleaf/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/COUNTING/${file(filename).getName()}"
+        else                                    "COUNTS/${SCOMBO}/${CONDITION}/${file(filename).getName()}"
     }
 
     input:

From 4fe61f54765f5974898b9e8b83130d6cab305ce2 Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Sun, 5 Jul 2026 20:37:59 +0200
Subject: [PATCH 08/80] Claude upgraded to deal with breaking changes of
 nextflow v26. Updated param assignment to pass strict syntax parser. Fixed
 tests to brevent breaks after fixing bugs in kallisto/salmon nf runs

---
 MONSDA/Workflows.py                           | 197 +++++++++++-
 ...imgalore-picard-star_DTU_dexseq_subflow.nf | 226 ++++++-------
 ...mgalore-picard-star_DTU_drimseq_subflow.nf | 226 ++++++-------
 ...trimgalore-picard-star_DTU_spit_subflow.nf | 224 ++++++-------
 .../Test_fastqc-bbduk-picard-bwa_subflow.nf   | 280 ++++++++--------
 .../Test_fastqc-bbduk-picard-star_subflow.nf  | 299 +++++++++---------
 ...Test_fastqc-cutadapt-picard-bwa_subflow.nf | 280 ++++++++--------
 ...est_fastqc-cutadapt-picard-star_subflow.nf | 299 +++++++++---------
 ...st_fastqc-trimgalore-picard-bwa_subflow.nf | 280 ++++++++--------
 ...t_fastqc-trimgalore-picard-star_subflow.nf | 299 +++++++++---------
 workflows/countreads.nf                       |   5 +-
 workflows/header.nf                           |   5 +-
 workflows/kallisto.nf                         |   6 +-
 workflows/salmon.nf                           |   6 +-
 14 files changed, 1418 insertions(+), 1214 deletions(-)

diff --git a/MONSDA/Workflows.py b/MONSDA/Workflows.py
index 9575e4f3..82125342 100755
--- a/MONSDA/Workflows.py
+++ b/MONSDA/Workflows.py
@@ -87,8 +87,197 @@ def _fix_oras(line: str) -> str:
     return _rewrite_oras(line)
 
 
+_NF_KW = ("def ", "process", "workflow", "include", "params", "nextflow")
+
+_NF_PFX = "g"
+
+
+def _nf_is_channel(rhs_list):
+    return any("Channel." in r for r in rhs_list)
+
+
+def _nf_rw(text, scalars, channels, ex_s=None, ex_c=None):
+    """Rewrite bare references to global names: scalars -> params.gNAME,
+    channels -> NAME(). Interpolated forms handled as well."""
+    s_active = set(scalars) - ({ex_s} if ex_s else set())
+    c_active = set(channels) - ({ex_c} if ex_c else set())
+    alln = s_active | c_active
+    if not alln:
+        return text
+
+    def repl(name, interp):
+        if name in c_active:
+            return ("${" + name + "()}") if interp else (name + "()")
+        return (
+            ("${params." + _NF_PFX + name + "}")
+            if interp
+            else ("params." + _NF_PFX + name)
+        )
+
+    alt = "|".join(sorted(map(re.escape, alln), key=len, reverse=True))
+    text = re.sub(r"\$\{(" + alt + r")\}", lambda m: repl(m.group(1), True), text)
+    text = re.sub(r"\$(" + alt + r")(?![\w(])", lambda m: repl(m.group(1), True), text)
+    text = re.sub(
+        r"(? 0:
+                    buf.append(lines[i])
+                    depth += lines[i].count("{") - lines[i].count("}")
+                    i += 1
+                for k in range(start, i):
+                    drop.add(k)
+                nm = None
+                for bl in buf:
+                    mm = re.match(r"^\s*([A-Za-z_]\w*)\s*=(?!=)", bl)
+                    if mm:
+                        nm = mm.group(1)
+                        break
+                if nm:
+                    ifblocks.append((nm, buf))
+                    ifnames.append(nm)
+                continue
+            else:
+                drop.add(i)
+        i += 1
+
+    channels = set(nm for nm in order if _nf_is_channel(assigns[nm]))
+    scalars = (set(order) - channels) | set(ifnames)
+
+    defs = []
+    for name in order:
+        rhs_list = assigns[name]
+        if name in channels:
+            block_body = []
+            for k, rhs in enumerate(rhs_list):
+                r = _nf_rw(rhs, scalars, channels, ex_c=name)
+                pre = "    def %s = %s" if k == 0 else "    %s = %s"
+                block_body.append(pre % (name, r))
+            block_body.append("    return %s" % name)
+            defs.append("def %s(){\n%s\n}" % (name, "\n".join(block_body)))
+        elif len(rhs_list) == 1:
+            r = _nf_rw(rhs_list[0], scalars, channels, ex_s=name)
+            defs.append("params.%s%s = %s" % (_NF_PFX, name, r))
+        else:
+            block_body = []
+            for k, rhs in enumerate(rhs_list):
+                r = _nf_rw(rhs, scalars, channels, ex_s=name)
+                pre = "def %s = %s" if k == 0 else "%s = %s"
+                block_body.append(pre % (name, r))
+            block_body.append("return %s" % name)
+            defs.append(
+                "params.%s%s = { %s }()" % (_NF_PFX, name, "; ".join(block_body))
+            )
+
+    for nm, buf in ifblocks:
+        tb = []
+        for bl in buf:
+            bl2 = re.sub(r"^(\s*)" + re.escape(nm) + r"\s*=\s*", r"\1return ", bl)
+            tb.append(bl2)
+        block = "\n".join(tb)
+        block = _nf_rw(block, scalars, channels, ex_s=nm)
+        defs.append("params.%s%s = {\n%s\n}()" % (_NF_PFX, nm, block))
+
+    kept = [ln for idx, ln in enumerate(lines) if idx not in drop]
+    body = "\n".join(kept)
+
+    body = _nf_rw(body, scalars, channels - {"samples_ch"})
+    if "samples_ch" in channels:
+        body = _nf_rewrite_samples_ch(body, scalars, channels)
+
+    def_txt = "\n".join(defs)
+    kl = body.split("\n")
+    out_lines = []
+    inserted = False
+    j = 0
+    while j < len(kl):
+        out_lines.append(kl[j])
+        if not inserted and kl[j].startswith("def get_always"):
+            k = j
+            while k < len(kl) and not kl[k].startswith("}"):
+                k += 1
+            for x in range(j + 1, k + 1):
+                out_lines.append(kl[x])
+            out_lines.append(def_txt)
+            j = k + 1
+            inserted = True
+            continue
+        j += 1
+    if not inserted:
+        out_lines = [def_txt] + kl
+    return "\n".join(out_lines)
+
+
 def _write_workflow(filepath, content):
     """Write workflow file with ORAS registry fix applied."""
+    if str(filepath).endswith(".nf"):
+        content = nf_paramize(content)
     content = _rewrite_oras(content)
     write_if_different(filepath, content)
 
@@ -3304,7 +3493,8 @@ def nf_make_post(
 
                 # Append subwork suffix for all applicable postprocess workflows
                 toolenv_orig = toolenv
-                toolenv = toolenv + "_" + subwork
+                if subwork in ["DE", "DEU", "DAS", "DTU"] and toolbin not in ["deseq", "diego"]:
+                    toolenv = toolenv + "_" + subwork
                 sconf[subwork + "ENV"] = toolenv
                 sconf[subwork + "BIN"] = toolbin
                 subsamples = mp.get_samples(sconf)
@@ -3437,8 +3627,9 @@ def nf_make_post(
             subjobs = list()
 
             toolenv, toolbin = map(str, listoftools[a])
-            # Append subwork suffix for all postprocessing workflows
-            toolenv = toolenv + "_" + subwork
+            # Append subwork suffix for all applicable postprocess workflows
+            if subwork in ["DE", "DEU", "DAS", "DTU"] and toolbin not in ["deseq", "diego"]:
+                toolenv = toolenv + "_" + subwork
 
             log.debug(logid + "toolenv: " + str(toolenv))
             sconf[subwork + "ENV"] = toolenv
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
index bc3b92bd..facd7d71 100644
--- a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_dexseq_subflow.nf
@@ -1,70 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = { def BINS = get_always('BINS'); BINS = get_always('BINS'); return BINS }()
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gDTUENV = get_always('DTUENV')
+params.gDTUBIN = get_always('DTUBIN')
+params.gDTUREF = get_always('DTUREF')
+params.gDTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+params.gDTUANNO = get_always('DTUANNO')
+params.gDTUDECOY = get_always('DTUDECOY')
+params.gDTUIDX = get_always('DTUIDX')
+params.gDTUUIDX = get_always('DTUUIDX')
+params.gDTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+params.gIDXPARAMS = get_always('dexseq_DTU_params_INDEX') ?: ''
+params.gCOUNTPARAMS = get_always('dexseq_DTU_params_COUNT') ?: ''
+params.gDTUPARAMS = get_always('dexseq_DTU_params_DTU') ?: ''
+params.gRUNTERMINUS = get_always('dexseq_DTU_params_TERMINUS') ?: ''
+params.gDTUREPS = get_always('DTUREPS') ?: ''
+params.gDTUCOMP = get_always('DTUCOMP') ?: ''
+params.gDTUCOMPS = get_always('DTUCOMPS') ?: ''
+params.gPVAL = get_always('DTUPVAL') ?: ''
+params.gLFC = get_always('DTULFC') ?: ''
+params.gPCOMBO = get_always('params.gCOMBO') ?: 'none'
+params.gCOUNTBIN = 'salmon'
+params.gCOUNTENV = 'salmon'
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-BINS = get_always('BINS')
-DTUENV = get_always('DTUENV')
-DTUBIN = get_always('DTUBIN')
-DTUREF = get_always('DTUREF')
-DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
-DTUANNO = get_always('DTUANNO')
-DTUDECOY = get_always('DTUDECOY')
-DTUIDX = get_always('DTUIDX')
-DTUUIDX = get_always('DTUUIDX')
-DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
-IDXPARAMS = get_always('dexseq_DTU_params_INDEX') ?: ''
-COUNTPARAMS = get_always('dexseq_DTU_params_COUNT') ?: ''
-DTUPARAMS = get_always('dexseq_DTU_params_DTU') ?: ''
-RUNTERMINUS = get_always('dexseq_DTU_params_TERMINUS') ?: ''
-DTUREPS = get_always('DTUREPS') ?: ''
-DTUCOMP = get_always('DTUCOMP') ?: ''
-DTUCOMPS = get_always('DTUCOMPS') ?: ''
-PVAL = get_always('DTUPVAL') ?: ''
-LFC = get_always('DTULFC') ?: ''
-PCOMBO = get_always('COMBO') ?: 'none'
-COUNTBIN = 'salmon'
-COUNTENV = 'salmon'
 process salmon_quant{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/salmon/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/salmon/${params.gCONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
     }
     input:
     path reads
@@ -73,10 +77,10 @@ process salmon_quant{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+    if (params.gPAIRED == 'paired'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'ISF'){
             stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'ISR'){
             stranded = '-l ISR'
         }else{
             stranded = '-l IU'
@@ -90,12 +94,12 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'SF'){
             stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'SR'){
             stranded = '-l SR'
         }else{
             stranded = '-l U'
@@ -107,18 +111,18 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }
 }
 process create_summary_snippet{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${params.gSCOMBO}.Rmd"                               
         else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
     }
     input:
@@ -129,18 +133,18 @@ process create_summary_snippet{
     script:
     inlist = de.toString()
     """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    touch log; python3 ${params.gBINS}/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env ${params.gDTUENV} --loglevel DEBUG 2>> log
     """
 }
 process terminus_collapse{
     conda "/envs/terminus.yaml"
     container "oras://docker.io/jfallmann/monsda:terminus"
-    cpus THREADS
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/terminus/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/terminus/${file(filename).getName()}"
     }
     input:
     path counts
@@ -150,69 +154,69 @@ process terminus_collapse{
     script:
     lf = "terminus_collapse.log"
     """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group ${params.gRUNTERMINUS} -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
     """
 }
 process salmon_idx{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/dexseq_index.log"
-        else if (filename == "dexseq.idx")            "$DTUIDX"
-        else                                          "$DTUUIDX"
+        if (filename.indexOf(".log") >0)    "LOGS/${params.gCOMBO}/${params.gCONDITION}/DTU/dexseq_index.log"
+        else if (filename == "dexseq.idx")            "${params.gDTUIDX}"
+        else                                          "${params.gDTUUIDX}"
     }
     input:
     path genome
     output:
-     path "$DTUUIDXNAME", emit: idx
+     path "${params.gDTUUIDXNAME}", emit: idx
     path "*.log", emit: idxlog
     path "*.idx", emit: tmpidx
     script:    
     gen =  genome.getName()
-    if (DTUDECOY){
-        decoy = "-d "+"${DTUDECOY}" 
+    if (params.gDTUDECOY){
+        decoy = "-d "+"${params.gDTUDECOY}" 
     }else{
         decoy = ''
     }
     """
-    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME dexseq.idx
+    ${params.gCOUNTBIN} index ${params.gIDXPARAMS} $decoy -p ${task.cpus} -t $gen -i ${params.gDTUUIDXNAME} &> index.log && ln -fs ${params.gDTUUIDXNAME} dexseq.idx
     """
 }
 process prepare_dtu_annotation{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_dexseq_annotation.log"
+        if (filename.indexOf(".gz") > 0)       "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${params.gSCOMBO}/featurecount_dexseq_annotation.log"
     }
     output:
     path "*.gz", emit: anno
     path "*.log", emit: log
     script:     
-    ca = COMBO+"_ANNOTATION.gz"
+    ca = params.gCOMBO+"_ANNOTATION.gz"
     ol = "create_DTU_table.log"
     """
-    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    mkdir -p TMP; ${params.gBINS}/Analysis/build_DTU_table.py ${params.gDTUREPS} --anno $ca --loglevel DEBUG --nextflow 2>> $ol
     """
 }
 process run_dexseq{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
-        else if (filename.indexOf(".html") > 0)      "DTU/${SCOMBO}/DEXSeqReport_${COMBO}_${DTUCOMP}/${file(filename).getName()}"
-        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_dexseq.log"
+        if (filename.indexOf("_table") > 0)      "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${params.gSCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf(".html") > 0)      "DTU/${params.gSCOMBO}/DEXSeqReport_${params.gCOMBO}_${params.gDTUCOMP}/${file(filename).getName()}"
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${params.gSCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${params.gSCOMBO}/run_dexseq.log"
     }
     input:path counts
     path anno
@@ -224,19 +228,19 @@ process run_dexseq{
     path "*SESSION.gz", emit: session
     path "log", emit: log
     script:    
-    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
-    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
-    comp = "${DTUCOMP}".split(':')[0]
-    dparams = "'${DTUPARAMS}'"
+    outdir = "DTU"+File.separatorChar+"${params.gSCOMBO}"
+    bin = "${params.gBINS}"+File.separatorChar+"${params.gDTUBIN}"
+    comp = "${params.gDTUCOMP}".split(':')[0]
+    dparams = "'${params.gDTUPARAMS}'"
     """
     mkdir -p Figures Tables
-    Rscript --no-environ --no-restore --no-save $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    Rscript --no-environ --no-restore --no-save $bin $anno $ref . ${params.gDTUCOMP} ${params.gPCOMBO} ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy() && ln -f Figures/* .
     """
 }
 process collect_dexseq{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     input:
     path de
@@ -248,31 +252,31 @@ process collect_dexseq{
 workflow DTU{ 
     take: collection
     main:
-    TRIMSAMPLES = LONGSAMPLES.collect{
-            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+    TRIMSAMPLES = params.gLONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${params.gCOMBO}/"+element+"_{R2,R1}*.fastq.gz"
         }
     trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
-    annofile = Channel.fromPath(DTUANNO)
-    checkidx = file(DTUIDX)
+    annofile = Channel.fromPath(params.gDTUANNO)
+    checkidx = file(params.gDTUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(DTUUIDX)
-        if (PAIRED == 'paired'){
+        idxfile = Channel.fromPath(params.gDTUUIDX)
+        if (params.gPAIRED == 'paired'){
             salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
         }        
     }
     else{
-        genomefile = Channel.fromPath(DTUREF)
+        genomefile = Channel.fromPath(params.gDTUREF)
         salmon_idx(genomefile)
-        if (PAIRED == 'paired'){
+        if (params.gPAIRED == 'paired'){
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
         }
     }
     prepare_dtu_annotation()
-    if (RUNTERMINUS?.length() > 0){
+    if (params.gRUNTERMINUS?.length() > 0){
         terminus_collapse(salmon_quant.out.counts.collect())
         dtucounts = terminus_collapse.out.counts
     }
@@ -291,6 +295,6 @@ workflow DTU{
 
 
 workflow {
-    DTU(dummy)
+    DTU(dummy())
 }
 
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
index 0e6684ea..242c43fc 100644
--- a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_drimseq_subflow.nf
@@ -1,70 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = { def BINS = get_always('BINS'); BINS = get_always('BINS'); return BINS }()
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gDTUENV = get_always('DTUENV')
+params.gDTUBIN = get_always('DTUBIN')
+params.gDTUREF = get_always('DTUREF')
+params.gDTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+params.gDTUANNO = get_always('DTUANNO')
+params.gDTUDECOY = get_always('DTUDECOY')
+params.gDTUIDX = get_always('DTUIDX')
+params.gDTUUIDX = get_always('DTUUIDX')
+params.gDTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+params.gIDXPARAMS = get_always('drimseq_DTU_params_INDEX') ?: ''
+params.gCOUNTPARAMS = get_always('drimseq_DTU_params_COUNT') ?: ''
+params.gDTUPARAMS = get_always('drimseq_DTU_params_DTU') ?: ''
+params.gRUNTERMINUS = get_always('drimseq_DTU_params_TERMINUS') ?: ''
+params.gDTUREPS = get_always('DTUREPS') ?: ''
+params.gDTUCOMP = get_always('DTUCOMP') ?: ''
+params.gDTUCOMPS = get_always('DTUCOMPS') ?: ''
+params.gPVAL = get_always('DTUPVAL') ?: ''
+params.gLFC = get_always('DTULFC') ?: ''
+params.gPCOMBO = get_always('params.gCOMBO') ?: 'none'
+params.gCOUNTBIN = 'salmon'
+params.gCOUNTENV = 'salmon'
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-BINS = get_always('BINS')
-DTUENV = get_always('DTUENV')
-DTUBIN = get_always('DTUBIN')
-DTUREF = get_always('DTUREF')
-DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
-DTUANNO = get_always('DTUANNO')
-DTUDECOY = get_always('DTUDECOY')
-DTUIDX = get_always('DTUIDX')
-DTUUIDX = get_always('DTUUIDX')
-DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
-IDXPARAMS = get_always('drimseq_DTU_params_INDEX') ?: ''
-COUNTPARAMS = get_always('drimseq_DTU_params_COUNT') ?: ''
-DTUPARAMS = get_always('drimseq_DTU_params_DTU') ?: ''
-RUNTERMINUS = get_always('drimseq_DTU_params_TERMINUS') ?: ''
-DTUREPS = get_always('DTUREPS') ?: ''
-DTUCOMP = get_always('DTUCOMP') ?: ''
-DTUCOMPS = get_always('DTUCOMPS') ?: ''
-PVAL = get_always('DTUPVAL') ?: ''
-LFC = get_always('DTULFC') ?: ''
-PCOMBO = get_always('COMBO') ?: 'none'
-COUNTBIN = 'salmon'
-COUNTENV = 'salmon'
 process salmon_quant{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/salmon/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/salmon/${params.gCONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
     }
     input:
     path reads
@@ -73,10 +77,10 @@ process salmon_quant{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+    if (params.gPAIRED == 'paired'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'ISF'){
             stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'ISR'){
             stranded = '-l ISR'
         }else{
             stranded = '-l IU'
@@ -90,12 +94,12 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'SF'){
             stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'SR'){
             stranded = '-l SR'
         }else{
             stranded = '-l U'
@@ -107,18 +111,18 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }
 }
 process create_summary_snippet{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${params.gSCOMBO}.Rmd"                               
         else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
     }
     input:
@@ -129,18 +133,18 @@ process create_summary_snippet{
     script:
     inlist = de.toString()
     """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    touch log; python3 ${params.gBINS}/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env ${params.gDTUENV} --loglevel DEBUG 2>> log
     """
 }
 process terminus_collapse{
     conda "/envs/terminus.yaml"
     container "oras://docker.io/jfallmann/monsda:terminus"
-    cpus THREADS
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/terminus/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/terminus/${file(filename).getName()}"
     }
     input:
     path counts
@@ -150,69 +154,69 @@ process terminus_collapse{
     script:
     lf = "terminus_collapse.log"
     """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group ${params.gRUNTERMINUS} -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
     """
 }
 process salmon_idx{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/drimseq_index.log"
-        else if (filename == "drimseq.idx")           "$DTUIDX"
-        else                                          "$DTUUIDX"
+        if (filename.indexOf(".log") >0)    "LOGS/${params.gCOMBO}/${params.gCONDITION}/DTU/drimseq_index.log"
+        else if (filename == "drimseq.idx")           "${params.gDTUIDX}"
+        else                                          "${params.gDTUUIDX}"
     }
     input:
     path genome
     output:
-     path "$DTUUIDXNAME", emit: idx
+     path "${params.gDTUUIDXNAME}", emit: idx
     path "*.log", emit: idxlog
     path "*.idx", emit: tmpidx
     script:    
     gen =  genome.getName()
-    if (DTUDECOY){
-        decoy = "-d "+"${DTUDECOY}" 
+    if (params.gDTUDECOY){
+        decoy = "-d "+"${params.gDTUDECOY}" 
     }else{
         decoy = ''
     }
     """
-    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME drimseq.idx
+    ${params.gCOUNTBIN} index ${params.gIDXPARAMS} $decoy -p ${task.cpus} -t $gen -i ${params.gDTUUIDXNAME} &> index.log && ln -fs ${params.gDTUUIDXNAME} drimseq.idx
     """
 }
 process prepare_dtu_annotation{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_drimseq_annotation.log"
+        if (filename.indexOf(".gz") > 0)       "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${params.gSCOMBO}/featurecount_drimseq_annotation.log"
     }
     output:
     path "*.gz", emit: anno
     path "*.log", emit: log
     script:     
-    ca = COMBO+"_ANNOTATION.gz"
+    ca = params.gCOMBO+"_ANNOTATION.gz"
     ol = "create_DTU_table.log"
     """
-    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    mkdir -p TMP; ${params.gBINS}/Analysis/build_DTU_table.py ${params.gDTUREPS} --anno $ca --loglevel DEBUG --nextflow 2>> $ol
     """
 }
 process run_drimseq{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
-        else if (filename.indexOf(".html") > 0)      "DTU/${SCOMBO}/drimseqReport_${COMBO}_${DTUCOMP}/${file(filename).getName()}"
-        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_drimseq.log"
+        if (filename.indexOf("_table") > 0)      "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${params.gSCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf(".html") > 0)      "DTU/${params.gSCOMBO}/drimseqReport_${params.gCOMBO}_${params.gDTUCOMP}/${file(filename).getName()}"
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${params.gSCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${params.gSCOMBO}/run_drimseq.log"
     }
     input:
     path counts
@@ -225,19 +229,19 @@ process run_drimseq{
     path "*SESSION.gz", emit: session
     path "log", emit: log
     script:    
-    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
-    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
-    comp = "${DTUCOMP}".split(':')[0]
-    dparams = "'${DTUPARAMS}'"
+    outdir = "DTU"+File.separatorChar+"${params.gSCOMBO}"
+    bin = "${params.gBINS}"+File.separatorChar+"${params.gDTUBIN}"
+    comp = "${params.gDTUCOMP}".split(':')[0]
+    dparams = "'${params.gDTUPARAMS}'"
     """
     mkdir -p Figures Tables
-    Rscript --no-environ --no-restore --no-save $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    Rscript --no-environ --no-restore --no-save $bin $anno $ref . ${params.gDTUCOMP} ${params.gPCOMBO} ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy() && ln -f Figures/* .
     """
 }
 process collect_drimseq{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     input:
     path de
@@ -249,31 +253,31 @@ process collect_drimseq{
 workflow DTU{ 
     take: collection
     main:
-    TRIMSAMPLES = LONGSAMPLES.collect{
-            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+    TRIMSAMPLES = params.gLONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${params.gCOMBO}/"+element+"_{R2,R1}*.fastq.gz"
         }
     trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
-    annofile = Channel.fromPath(DTUANNO)
-    checkidx = file(DTUIDX)
+    annofile = Channel.fromPath(params.gDTUANNO)
+    checkidx = file(params.gDTUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(DTUUIDX)
-        if (PAIRED == 'paired'){
+        idxfile = Channel.fromPath(params.gDTUUIDX)
+        if (params.gPAIRED == 'paired'){
             salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
         }        
     }
     else{
-        genomefile = Channel.fromPath(DTUREF)
+        genomefile = Channel.fromPath(params.gDTUREF)
         salmon_idx(genomefile)
-        if (PAIRED == 'paired'){
+        if (params.gPAIRED == 'paired'){
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
         }
     }
     prepare_dtu_annotation()
-    if (RUNTERMINUS?.length() > 0){
+    if (params.gRUNTERMINUS?.length() > 0){
         terminus_collapse(salmon_quant.out.counts.collect())
         dtucounts = terminus_collapse.out.counts
     }
@@ -292,6 +296,6 @@ workflow DTU{
 
 
 workflow {
-    DTU(dummy)
+    DTU(dummy())
 }
 
diff --git a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
index a68c9776..36f794d3 100644
--- a/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
+++ b/tests/golden/config_DTU/nf/allconditions_fastqc-trimgalore-picard-star_DTU_spit_subflow.nf
@@ -1,70 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = { def BINS = get_always('BINS'); BINS = get_always('BINS'); return BINS }()
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gDTUENV = get_always('DTUENV')
+params.gDTUBIN = get_always('DTUBIN')
+params.gDTUREF = get_always('DTUREF')
+params.gDTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
+params.gDTUANNO = get_always('DTUANNO')
+params.gDTUDECOY = get_always('DTUDECOY')
+params.gDTUIDX = get_always('DTUIDX')
+params.gDTUUIDX = get_always('DTUUIDX')
+params.gDTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
+params.gIDXPARAMS = get_always('spit_DTU_params_INDEX') ?: ''
+params.gCOUNTPARAMS = get_always('spit_DTU_params_COUNT') ?: ''
+params.gDTUPARAMS = get_always('spit_DTU_params_DTU') ?: ''
+params.gRUNTERMINUS = get_always('spit_DTU_params_TERMINUS') ?: ''
+params.gDTUREPS = get_always('DTUREPS') ?: ''
+params.gDTUCOMP = get_always('DTUCOMP') ?: ''
+params.gDTUCOMPS = get_always('DTUCOMPS') ?: ''
+params.gPVAL = get_always('DTUPVAL') ?: ''
+params.gLFC = get_always('DTULFC') ?: ''
+params.gPCOMBO = get_always('params.gCOMBO') ?: 'none'
+params.gCOUNTBIN = 'salmon'
+params.gCOUNTENV = 'salmon'
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-BINS = get_always('BINS')
-DTUENV = get_always('DTUENV')
-DTUBIN = get_always('DTUBIN')
-DTUREF = get_always('DTUREF')
-DTUREFDIR = "${workflow.workDir}/../"+get_always('DTUREFDIR')
-DTUANNO = get_always('DTUANNO')
-DTUDECOY = get_always('DTUDECOY')
-DTUIDX = get_always('DTUIDX')
-DTUUIDX = get_always('DTUUIDX')
-DTUUIDXNAME = get_always('DTUUIDXNAME')+'.idx'
-IDXPARAMS = get_always('spit_DTU_params_INDEX') ?: ''
-COUNTPARAMS = get_always('spit_DTU_params_COUNT') ?: ''
-DTUPARAMS = get_always('spit_DTU_params_DTU') ?: ''
-RUNTERMINUS = get_always('spit_DTU_params_TERMINUS') ?: ''
-DTUREPS = get_always('DTUREPS') ?: ''
-DTUCOMP = get_always('DTUCOMP') ?: ''
-DTUCOMPS = get_always('DTUCOMPS') ?: ''
-PVAL = get_always('DTUPVAL') ?: ''
-LFC = get_always('DTULFC') ?: ''
-PCOMBO = get_always('COMBO') ?: 'none'
-COUNTBIN = 'salmon'
-COUNTENV = 'salmon'
 process salmon_quant{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/salmon/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/salmon/${CONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/salmon/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/salmon/${params.gCONDITION}/"+"${filename.replaceAll(/trimmed./,"")}"
     }
     input:
     path reads
@@ -73,10 +77,10 @@ process salmon_quant{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
-        if (STRANDED == 'fr' || STRANDED == 'ISF'){
+    if (params.gPAIRED == 'paired'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'ISF'){
             stranded = '-l ISF'
-        }else if (STRANDED == 'rf' || STRANDED == 'ISR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'ISR'){
             stranded = '-l ISR'
         }else{
             stranded = '-l IU'
@@ -90,12 +94,12 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -1 $r1 -2 $r2 &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }else{
-        if (STRANDED == 'fr' || STRANDED == 'SF'){
+        if (params.gSTRANDED == 'fr' || params.gSTRANDED == 'SF'){
             stranded = '-l SF'
-        }else if (STRANDED == 'rf' || STRANDED == 'SR'){
+        }else if (params.gSTRANDED == 'rf' || params.gSTRANDED == 'SR'){
             stranded = '-l SR'
         }else{
             stranded = '-l U'
@@ -107,18 +111,18 @@ process salmon_quant{
         oz = fn+"/quant.sf.gz"
         ol = fn+"_counts.gz"
         """
-        $COUNTBIN $COUNTPARAMS quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
+        ${params.gCOUNTBIN} ${params.gCOUNTPARAMS} quant -p ${task.cpus} -i $idx $stranded -o $fn -r $read &>> $lf && gzip $of && ln -fs $oz $ol
         """
     }
 }
 process create_summary_snippet{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${SCOMBO}.Rmd"                               
+        if (filename.indexOf(".Rmd") > 0)         "REPORTS/SUMMARY/RmdSnippets/${params.gSCOMBO}.Rmd"                               
         else if (filename.indexOf("log") > 0)        "LOGS/DTU/create_summary_snippet.log"
     }
     input:
@@ -129,18 +133,18 @@ process create_summary_snippet{
     script:
     inlist = de.toString()
     """
-    touch log; python3 $BINS/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env $DTUENV --loglevel DEBUG 2>> log
+    touch log; python3 ${params.gBINS}/Analysis/RmdCreator.py --files $inlist --output out.Rmd --env ${params.gDTUENV} --loglevel DEBUG 2>> log
     """
 }
 process terminus_collapse{
     conda "/envs/terminus.yaml"
     container "oras://docker.io/jfallmann/monsda:terminus"
-    cpus THREADS
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)        "LOGS/${SCOMBO}/terminus/${CONDITION}/DTU/${file(filename).getName()}"
-        else                                    "DTU/${SCOMBO}/terminus/${file(filename).getName()}"
+        if (filename.indexOf(".log") >0)        "LOGS/${params.gSCOMBO}/terminus/${params.gCONDITION}/DTU/${file(filename).getName()}"
+        else                                    "DTU/${params.gSCOMBO}/terminus/${file(filename).getName()}"
     }
     input:
     path counts
@@ -150,68 +154,68 @@ process terminus_collapse{
     script:
     lf = "terminus_collapse.log"
     """
-    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group $RUNTERMINUS -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
+    mkdir -p termdirs; for f in *_counts.gz; do b=\${f%_counts.gz}; mkdir -p termdirs/\$b; zcat \$f > termdirs/\$b/quant.sf; done 2> $lf; dirs=\$(ls -d termdirs/*); for d in \$dirs; do terminus group ${params.gRUNTERMINUS} -d \$d -o termout &>> $lf; done; terminus collapse -d \$dirs -o termout &>> $lf; for d in \$dirs; do b=\$(basename \$d); gzip -c termout/\$b/quant.sf > \${b}_counts.gz; done 2>> $lf
     """
 }
 process salmon_idx{
-    conda "/envs/$COUNTENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$COUNTENV"
-    cpus THREADS
+    conda "/envs/${params.gCOUNTENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gCOUNTENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow',
     saveAs: {filename ->
-        if (filename.indexOf(".log") >0)    "LOGS/${COMBO}/${CONDITION}/DTU/spit_index.log"
-        else if (filename == "spit.idx")           "$DTUIDX"
-        else                                          "$DTUUIDX"
+        if (filename.indexOf(".log") >0)    "LOGS/${params.gCOMBO}/${params.gCONDITION}/DTU/spit_index.log"
+        else if (filename == "spit.idx")           "${params.gDTUIDX}"
+        else                                          "${params.gDTUUIDX}"
     }
     input:
     path genome
     output:
-     path "$DTUUIDXNAME", emit: idx
+     path "${params.gDTUUIDXNAME}", emit: idx
     path "*.log", emit: idxlog
     path "*.idx", emit: tmpidx
     script:    
     gen =  genome.getName()
-    if (DTUDECOY){
-        decoy = "-d "+"${DTUDECOY}" 
+    if (params.gDTUDECOY){
+        decoy = "-d "+"${params.gDTUDECOY}" 
     }else{
         decoy = ''
     }
     """
-    $COUNTBIN index $IDXPARAMS $decoy -p ${task.cpus} -t $gen -i $DTUUIDXNAME &> index.log && ln -fs $DTUUIDXNAME spit.idx
+    ${params.gCOUNTBIN} index ${params.gIDXPARAMS} $decoy -p ${task.cpus} -t $gen -i ${params.gDTUUIDXNAME} &> index.log && ln -fs ${params.gDTUUIDXNAME} spit.idx
     """
 }
 process prepare_dtu_annotation{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".gz") > 0)       "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${SCOMBO}/featurecount_spit_annotation.log"
+        if (filename.indexOf(".gz") > 0)       "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf(".log") > 0)        "LOGS/DTU/${params.gSCOMBO}/featurecount_spit_annotation.log"
     }
     output:
     path "*.gz", emit: anno
     path "*.log", emit: log
     script:     
-    ca = COMBO+"_ANNOTATION.gz"
+    ca = params.gCOMBO+"_ANNOTATION.gz"
     ol = "create_DTU_table.log"
     """
-    mkdir -p TMP; $BINS/Analysis/build_DTU_table.py $DTUREPS --anno $ca --loglevel DEBUG --nextflow 2>> $ol
+    mkdir -p TMP; ${params.gBINS}/Analysis/build_DTU_table.py ${params.gDTUREPS} --anno $ca --loglevel DEBUG --nextflow 2>> $ol
     """
 }
 process run_spit{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_table") > 0)      "DTU/${SCOMBO}/Tables/${file(filename).getName()}"                
-        else if (filename.indexOf("_figure") > 0)      "DTU/${SCOMBO}/Figures/${file(filename).getName()}" 
-        else if (filename.indexOf("SESSION") > 0)      "DTU/${SCOMBO}/${file(filename).getName()}"                     
-        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${SCOMBO}/run_spit.log"
+        if (filename.indexOf("_table") > 0)      "DTU/${params.gSCOMBO}/Tables/${file(filename).getName()}"                
+        else if (filename.indexOf("_figure") > 0)      "DTU/${params.gSCOMBO}/Figures/${file(filename).getName()}" 
+        else if (filename.indexOf("SESSION") > 0)      "DTU/${params.gSCOMBO}/${file(filename).getName()}"                     
+        else if (filename.indexOf("log") > 0)        "LOGS/DTU/${params.gSCOMBO}/run_spit.log"
     }
     input:
     path counts
@@ -223,19 +227,19 @@ process run_spit{
     path "*SESSION.gz", emit: session
     path "log", emit: log
     script:    
-    outdir = "DTU"+File.separatorChar+"${SCOMBO}"
-    bin = "${BINS}"+File.separatorChar+"${DTUBIN}"
-    comp = "${DTUCOMP}".split(':')[0]
-    dparams = "'${DTUPARAMS}'"
+    outdir = "DTU"+File.separatorChar+"${params.gSCOMBO}"
+    bin = "${params.gBINS}"+File.separatorChar+"${params.gDTUBIN}"
+    comp = "${params.gDTUCOMP}".split(':')[0]
+    dparams = "'${params.gDTUPARAMS}'"
     """
     mkdir -p Figures Tables
-    python3 $bin $anno $ref . $DTUCOMP $PCOMBO ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy && ln -f Figures/* .
+    python3 $bin $anno $ref . ${params.gDTUCOMP} ${params.gPCOMBO} ${task.cpus} $dparams &> log ; ln -f Tables/* . && touch Figures/dummy() && ln -f Figures/* .
     """
 }
 process collect_spit{
-    conda "/envs/$DTUENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DTUENV"
-    cpus THREADS
+    conda "/envs/${params.gDTUENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDTUENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     input:
     path de
@@ -247,31 +251,31 @@ process collect_spit{
 workflow DTU{ 
     take: collection
     main:
-    TRIMSAMPLES = LONGSAMPLES.collect{
-            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${COMBO}/"+element+"_{R2,R1}*.fastq.gz"
+    TRIMSAMPLES = params.gLONGSAMPLES.collect{
+            element -> return "${workflow.workDir}/../TRIMMED_FASTQ/${params.gCOMBO}/"+element+"_{R2,R1}*.fastq.gz"
         }
     trimsamples_ch =  Channel.fromPath(TRIMSAMPLES.sort())
-    annofile = Channel.fromPath(DTUANNO)
-    checkidx = file(DTUIDX)
+    annofile = Channel.fromPath(params.gDTUANNO)
+    checkidx = file(params.gDTUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(DTUUIDX)
-        if (PAIRED == 'paired'){
+        idxfile = Channel.fromPath(params.gDTUUIDX)
+        if (params.gPAIRED == 'paired'){
             salmon_quant(idxfile.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(idxfile.combine(trimsamples_ch.collate(1)))
         }        
     }
     else{
-        genomefile = Channel.fromPath(DTUREF)
+        genomefile = Channel.fromPath(params.gDTUREF)
         salmon_idx(genomefile)
-        if (PAIRED == 'paired'){
+        if (params.gPAIRED == 'paired'){
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(2)))
         } else{
             salmon_quant(salmon_idx.out.idx.combine(trimsamples_ch.collate(1)))
         }
     }
     prepare_dtu_annotation()
-    if (RUNTERMINUS?.length() > 0){
+    if (params.gRUNTERMINUS?.length() > 0){
         terminus_collapse(salmon_quant.out.counts.collect())
         dtucounts = terminus_collapse.out.counts
     }
@@ -291,6 +295,6 @@ workflow DTU{
 
 
 workflow {
-    DTU(dummy)
+    DTU(dummy())
 }
 
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
index f74d4542..7c21b10f 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-bwa_subflow.nf
@@ -1,51 +1,75 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = { def MAPBIN = get_always('MAPPINGBIN'); MAPBIN = MAPBIN.replace('_', ' '); return MAPBIN }()
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('bwa_params_MAP') ?: ''
+params.gIDXBIN = params.gMAPBIN.split('_')[0]
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +78,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +106,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +117,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +133,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +144,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +160,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +172,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,21 +189,21 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN $TRIMPARAMS t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
+        ${params.gTRIMBIN} ${params.gTRIMPARAMS} t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
         """
     }
     else{
         o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"        
         r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN $TRIMPARAMS t=${task.cpus} in=$r1 out=$o &> $r 
+        ${params.gTRIMBIN} ${params.gTRIMPARAMS} t=${task.cpus} in=$r1 out=$o &> $r 
         """
     }
 }
@@ -190,13 +211,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -205,21 +226,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -237,7 +254,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -255,20 +272,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
-MAPPARAMS = get_always('bwa_params_MAP') ?: ''
-IDXBIN = MAPBIN.split('_')[0]
-MAPBIN = MAPBIN.replace('_', ' ')
 process collect_tomap{
     input:
     path check
@@ -280,16 +283,16 @@ process collect_tomap{
     """
 }
 process bwa_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename == "bwa.idx")                          "$MAPIDX"
-        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
-        else                                                "$MAPUIDX"
+        if (filename == "bwa.idx")                          "${params.gMAPIDX}"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${params.gCOMBO}/${params.gCONDITION}/bwa_index.log"
+        else                                                "${params.gMAPUIDX}"
     }
     input:
     path genome
@@ -299,19 +302,19 @@ process bwa_idx{
     script:
     gen =  genome.getName()
     """
-    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    mkdir -p ${params.gMAPUIDXNAME} && ${params.gIDXBIN} index -p ${params.gMAPUIDXNAME}/${params.gMAPPREFIX} ${params.gIDXPARAMS} $gen &> Log.out && ln -fs ${params.gMAPUIDXNAME} bwa.idx
     """
 }
 process bwa_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
         saveAs: {filename ->
-        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getName()}"
         else null
     }
     input:
@@ -322,7 +325,7 @@ process bwa_mapping{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
@@ -331,7 +334,7 @@ process bwa_mapping{
         uf2 = fn+"_R2_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
         """
     }else{
         read = reads[1]
@@ -340,21 +343,21 @@ process bwa_mapping{
         uf = fn+"_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
         """
     }
 }
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     collection.filter(~/.fastq.gz/)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         bwa_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
+        genomefile = Channel.fromPath(params.gMAPREF)
         bwa_idx(genomefile)
         bwa_mapping(bwa_idx.out.bwidx.combine(collection))
     }
@@ -367,12 +370,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -390,14 +393,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -416,13 +419,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -433,26 +436,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -483,19 +486,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -509,8 +509,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -542,8 +542,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
index 6f38d6fa..2f58f05e 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-bbduk-picard-star_subflow.nf
@@ -1,51 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = get_always('MAPPINGBIN')
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('star_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('star_params_MAP') ?: ''
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +77,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +105,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +116,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +132,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +143,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +159,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +171,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('bbduk_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,21 +188,21 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN $TRIMPARAMS t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
+        ${params.gTRIMBIN} ${params.gTRIMPARAMS} t=${task.cpus} in1=$r1 in2=$r2 out1=$o out2=$p &> $r 
         """
     }
     else{
         o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"        
         r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN $TRIMPARAMS t=${task.cpus} in=$r1 out=$o &> $r 
+        ${params.gTRIMBIN} ${params.gTRIMPARAMS} t=${task.cpus} in=$r1 out=$o &> $r 
         """
     }
 }
@@ -190,13 +210,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -205,21 +225,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -237,7 +253,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -255,18 +271,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('star_params_INDEX') ?: ''
-MAPPARAMS = get_always('star_params_MAP') ?: ''
 process collect_tomap{
     input:
     path check
@@ -278,44 +282,44 @@ process collect_tomap{
     """
 }
 process star_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
-        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
-        else                                             "$MAPUIDX"
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${params.gCOMBO}/${params.gCONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "${params.gMAPIDX}"
+        else                                             "${params.gMAPUIDX}"
     }
     input:
     path genome
     path anno
     output:
-    path "$MAPUIDXNAME", emit: idx
+    path "${params.gMAPUIDXNAME}", emit: idx
     path "*.out", emit: idxlog
     path "*.idx", emit: tmpidx
     script:
     gen =  genome.getName()
     an  = anno.getName()
     """
-    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p ${params.gMAPUIDXNAME} && ${params.gMAPBIN} ${params.gIDXPARAMS} --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir ${params.gMAPUIDXNAME} --genomeFastaFiles tmp.fa && touch ${params.gMAPUIDXNAME} && ln -s ${params.gMAPUIDXNAME} star.idx && rm -f tmp.fa tmp_anno && ln -fs ${params.gMAPUIDXNAME}/* . && rm -rf STARTMP
     """
 }
 process star_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
-        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
+        else                                         "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
     }
     input:
     path reads
@@ -329,7 +333,7 @@ process star_mapping{
     script:
     idx = reads[0]
     idxdir = idx.toRealPath()
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         a = "Trimming_report.txt"
@@ -337,31 +341,31 @@ process star_mapping{
         of = fn+'.Aligned.out.sam'
         gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
         """
-        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
         """
     }
     else{
-        if (PAIRED != 'singlecell'){
+        if (params.gPAIRED != 'singlecell'){
             read = reads[1]
             fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
             of = fn+'Aligned.out.sam'
             gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
             """
         }
         else{
-            if (STRANDED == 'fr'){
+            if (params.gSTRANDED == 'fr'){
                 stranded = '--soloStrand Forward'
-            }else if (STRANDED == 'rf'){
+            }else if (params.gSTRANDED == 'rf'){
                 stranded = '--soloStrand Reverse'
             }else{
                 stranded = '--soloStrand Unstranded'
             }
             r1 = reads[1]
             fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
-            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
-            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+            r2 = "${workflow.workDir}/../FASTQ/${params.gCONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (params.gMAPPARAMS.contains('--soloBarcodeMate 1')){
                 t = r2
                 r2 = r1
                 r1 = t
@@ -369,7 +373,7 @@ process star_mapping{
             of = fn+'.Aligned.sortedByCoord.out.bam'
             gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
             """
         }
     }
@@ -377,14 +381,14 @@ process star_mapping{
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         star_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
-        annofile = Channel.fromPath(MAPANNO)
+        genomefile = Channel.fromPath(params.gMAPREF)
+        annofile = Channel.fromPath(params.gMAPANNO)
         star_idx(genomefile, annofile)
         star_mapping(star_idx.out.idx.combine(collection))
     }
@@ -397,12 +401,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -420,14 +424,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -446,13 +450,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -463,26 +467,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -513,19 +517,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -539,8 +540,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -572,8 +573,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
index b4b87dfe..9d8bbbfb 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-bwa_subflow.nf
@@ -1,51 +1,75 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = { def MAPBIN = get_always('MAPPINGBIN'); MAPBIN = MAPBIN.replace('_', ' '); return MAPBIN }()
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('bwa_params_MAP') ?: ''
+params.gIDXBIN = params.gMAPBIN.split('_')[0]
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +78,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +106,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +117,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +133,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +144,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +160,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +172,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,21 +189,21 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o -p $p $r1 $r2 &> $r
+        ${params.gTRIMBIN} --cores ${task.cpus} ${params.gTRIMPARAMS} -o $o -p $p $r1 $r2 &> $r
         """
     }
     else{
         o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o $reads &> $r
+        ${params.gTRIMBIN} --cores ${task.cpus} ${params.gTRIMPARAMS} -o $o $reads &> $r
         """
     }
 }
@@ -190,13 +211,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -205,21 +226,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -237,7 +254,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -255,20 +272,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
-MAPPARAMS = get_always('bwa_params_MAP') ?: ''
-IDXBIN = MAPBIN.split('_')[0]
-MAPBIN = MAPBIN.replace('_', ' ')
 process collect_tomap{
     input:
     path check
@@ -280,16 +283,16 @@ process collect_tomap{
     """
 }
 process bwa_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename == "bwa.idx")                          "$MAPIDX"
-        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
-        else                                                "$MAPUIDX"
+        if (filename == "bwa.idx")                          "${params.gMAPIDX}"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${params.gCOMBO}/${params.gCONDITION}/bwa_index.log"
+        else                                                "${params.gMAPUIDX}"
     }
     input:
     path genome
@@ -299,19 +302,19 @@ process bwa_idx{
     script:
     gen =  genome.getName()
     """
-    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    mkdir -p ${params.gMAPUIDXNAME} && ${params.gIDXBIN} index -p ${params.gMAPUIDXNAME}/${params.gMAPPREFIX} ${params.gIDXPARAMS} $gen &> Log.out && ln -fs ${params.gMAPUIDXNAME} bwa.idx
     """
 }
 process bwa_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
         saveAs: {filename ->
-        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getName()}"
         else null
     }
     input:
@@ -322,7 +325,7 @@ process bwa_mapping{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
@@ -331,7 +334,7 @@ process bwa_mapping{
         uf2 = fn+"_R2_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
         """
     }else{
         read = reads[1]
@@ -340,21 +343,21 @@ process bwa_mapping{
         uf = fn+"_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
         """
     }
 }
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     collection.filter(~/.fastq.gz/)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         bwa_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
+        genomefile = Channel.fromPath(params.gMAPREF)
         bwa_idx(genomefile)
         bwa_mapping(bwa_idx.out.bwidx.combine(collection))
     }
@@ -367,12 +370,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -390,14 +393,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -416,13 +419,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -433,26 +436,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -483,19 +486,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -509,8 +509,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -542,8 +542,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
index 5f91ce8f..725a5549 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-cutadapt-picard-star_subflow.nf
@@ -1,51 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = get_always('MAPPINGBIN')
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('star_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('star_params_MAP') ?: ''
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +77,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +105,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +116,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +132,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +143,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +159,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +171,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('cutadapt_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)                "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,21 +188,21 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         o = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         p = file(r2).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(r1).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o -p $p $r1 $r2 &> $r
+        ${params.gTRIMBIN} --cores ${task.cpus} ${params.gTRIMPARAMS} -o $o -p $p $r1 $r2 &> $r
         """
     }
     else{
         o = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimmed.fastq.gz"
         r = file(reads).getSimpleName().replaceAll(/_dedup/,"").replaceAll(/.fastq.gz/,"")+"_trimming_report.txt"
         """
-        $TRIMBIN --cores ${task.cpus} $TRIMPARAMS -o $o $reads &> $r
+        ${params.gTRIMBIN} --cores ${task.cpus} ${params.gTRIMPARAMS} -o $o $reads &> $r
         """
     }
 }
@@ -190,13 +210,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -205,21 +225,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -237,7 +253,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -255,18 +271,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('star_params_INDEX') ?: ''
-MAPPARAMS = get_always('star_params_MAP') ?: ''
 process collect_tomap{
     input:
     path check
@@ -278,44 +282,44 @@ process collect_tomap{
     """
 }
 process star_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
-        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
-        else                                             "$MAPUIDX"
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${params.gCOMBO}/${params.gCONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "${params.gMAPIDX}"
+        else                                             "${params.gMAPUIDX}"
     }
     input:
     path genome
     path anno
     output:
-    path "$MAPUIDXNAME", emit: idx
+    path "${params.gMAPUIDXNAME}", emit: idx
     path "*.out", emit: idxlog
     path "*.idx", emit: tmpidx
     script:
     gen =  genome.getName()
     an  = anno.getName()
     """
-    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p ${params.gMAPUIDXNAME} && ${params.gMAPBIN} ${params.gIDXPARAMS} --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir ${params.gMAPUIDXNAME} --genomeFastaFiles tmp.fa && touch ${params.gMAPUIDXNAME} && ln -s ${params.gMAPUIDXNAME} star.idx && rm -f tmp.fa tmp_anno && ln -fs ${params.gMAPUIDXNAME}/* . && rm -rf STARTMP
     """
 }
 process star_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
-        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
+        else                                         "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
     }
     input:
     path reads
@@ -329,7 +333,7 @@ process star_mapping{
     script:
     idx = reads[0]
     idxdir = idx.toRealPath()
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         a = "Trimming_report.txt"
@@ -337,31 +341,31 @@ process star_mapping{
         of = fn+'.Aligned.out.sam'
         gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
         """
-        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
         """
     }
     else{
-        if (PAIRED != 'singlecell'){
+        if (params.gPAIRED != 'singlecell'){
             read = reads[1]
             fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
             of = fn+'Aligned.out.sam'
             gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
             """
         }
         else{
-            if (STRANDED == 'fr'){
+            if (params.gSTRANDED == 'fr'){
                 stranded = '--soloStrand Forward'
-            }else if (STRANDED == 'rf'){
+            }else if (params.gSTRANDED == 'rf'){
                 stranded = '--soloStrand Reverse'
             }else{
                 stranded = '--soloStrand Unstranded'
             }
             r1 = reads[1]
             fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
-            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
-            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+            r2 = "${workflow.workDir}/../FASTQ/${params.gCONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (params.gMAPPARAMS.contains('--soloBarcodeMate 1')){
                 t = r2
                 r2 = r1
                 r1 = t
@@ -369,7 +373,7 @@ process star_mapping{
             of = fn+'.Aligned.sortedByCoord.out.bam'
             gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
             """
         }
     }
@@ -377,14 +381,14 @@ process star_mapping{
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         star_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
-        annofile = Channel.fromPath(MAPANNO)
+        genomefile = Channel.fromPath(params.gMAPREF)
+        annofile = Channel.fromPath(params.gMAPANNO)
         star_idx(genomefile, annofile)
         star_mapping(star_idx.out.idx.combine(collection))
     }
@@ -397,12 +401,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -420,14 +424,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -446,13 +450,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -463,26 +467,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -513,19 +517,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -539,8 +540,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -572,8 +573,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
index 8188d76a..e3ce88c7 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-bwa_subflow.nf
@@ -1,51 +1,75 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = { def MAPBIN = get_always('MAPPINGBIN'); MAPBIN = MAPBIN.replace('_', ' '); return MAPBIN }()
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('bwa_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('bwa_params_MAP') ?: ''
+params.gIDXBIN = params.gMAPBIN.split('_')[0]
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +78,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +106,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +117,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +133,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +144,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +160,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +172,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,16 +189,16 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         """
-        $TRIMBIN --cores ${task.cpus} --paired --gzip $TRIMPARAMS $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        ${params.gTRIMBIN} --cores ${task.cpus} --paired --gzip ${params.gTRIMPARAMS} $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
         """
     }
     else{
         """
-        $TRIMBIN --cores ${task.cpus} --gzip $TRIMPARAMS $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        ${params.gTRIMBIN} --cores ${task.cpus} --gzip ${params.gTRIMPARAMS} $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
         """
     }
 }
@@ -185,13 +206,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -200,21 +221,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -232,7 +249,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -250,20 +267,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('bwa_params_INDEX') ?: ''
-MAPPARAMS = get_always('bwa_params_MAP') ?: ''
-IDXBIN = MAPBIN.split('_')[0]
-MAPBIN = MAPBIN.replace('_', ' ')
 process collect_tomap{
     input:
     path check
@@ -275,16 +278,16 @@ process collect_tomap{
     """
 }
 process bwa_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename == "bwa.idx")                          "$MAPIDX"
-        else if (filename.indexOf("Log.out") > 0)           "LOGS/${COMBO}/${CONDITION}/bwa_index.log"
-        else                                                "$MAPUIDX"
+        if (filename == "bwa.idx")                          "${params.gMAPIDX}"
+        else if (filename.indexOf("Log.out") > 0)           "LOGS/${params.gCOMBO}/${params.gCONDITION}/bwa_index.log"
+        else                                                "${params.gMAPUIDX}"
     }
     input:
     path genome
@@ -294,19 +297,19 @@ process bwa_idx{
     script:
     gen =  genome.getName()
     """
-    mkdir -p $MAPUIDXNAME && $IDXBIN index -p $MAPUIDXNAME/$MAPPREFIX $IDXPARAMS $gen &> Log.out && ln -fs $MAPUIDXNAME bwa.idx
+    mkdir -p ${params.gMAPUIDXNAME} && ${params.gIDXBIN} index -p ${params.gMAPUIDXNAME}/${params.gMAPPREFIX} ${params.gIDXPARAMS} $gen &> Log.out && ln -fs ${params.gMAPUIDXNAME} bwa.idx
     """
 }
 process bwa_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
         saveAs: {filename ->
-        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)          "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getName()}"
+        if (filename.indexOf("_unmapped.fastq.gz") > 0)   "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)          "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getName()}"
         else null
     }
     input:
@@ -317,7 +320,7 @@ process bwa_mapping{
     path "*.log", emit: logs
     script:
     idx = reads[0]
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
@@ -326,7 +329,7 @@ process bwa_mapping{
         uf2 = fn+"_R2_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $r1 $r2  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools collate -u -O -|samtools fastq -n -c 6 -1 $uf1 -2 $uf2 ) 2>> {log} &>/dev/null && touch $uf1 $uf2 2>> $lf &> /dev/null
         """
     }else{
         read = reads[1]
@@ -335,21 +338,21 @@ process bwa_mapping{
         uf = fn+"_unmapped.fastq.gz"
         lf = "bwa_"+fn+".log"
         """
-        $MAPBIN $MAPPARAMS -t ${task.cpus} ${idx}/${MAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
+        ${params.gMAPBIN} ${params.gMAPPARAMS} -t ${task.cpus} ${idx}/${params.gMAPPREFIX} $read  2> $lf|tee >(samtools view -h -F 4 |gzip > $pf) >(samtools view -h -f 4 |samtools fastq -n - | pigz > $uf) 2>> $lf &> /dev/null && touch $uf
         """
     }
 }
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     collection.filter(~/.fastq.gz/)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         bwa_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
+        genomefile = Channel.fromPath(params.gMAPREF)
         bwa_idx(genomefile)
         bwa_mapping(bwa_idx.out.bwidx.combine(collection))
     }
@@ -362,12 +365,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -385,14 +388,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -411,13 +414,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -428,26 +431,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -478,19 +481,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -504,8 +504,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -537,8 +537,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
index 4cfad930..dc3f14f1 100644
--- a/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
+++ b/tests/golden/config_Test/nf/Test_fastqc-trimgalore-picard-star_subflow.nf
@@ -1,51 +1,74 @@
 #!/usr/bin/env nextflow
 nextflow.enable.dsl=2
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
 }
-REFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
-REFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
-BINS = get_always('BINS')
-THREADS = get_always('MAXTHREAD')
-PAIRED = get_always('PAIRED') ?: null
-RUNDEDUP = get_always('RUNDEDUP') ?: null
-PREDEDUP = get_always('PREDEDUP') ?: null
-STRANDED = get_always('STRANDED') ?: null
-IP = get_always('IP') ?: null
-CONDITION = get_always('CONDITION') ?: null
-COMBO = get_always('COMBO') ?: ''
-SCOMBO = get_always('SCOMBO') ?: ''
-SAMPLES = get_always('SAMPLES').split(',') ?: null
-LONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
-SHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
-SETS = get_always('SETS') ?: null
-dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
-if (PAIRED == 'paired' || PAIRED == 'singlecell'){
-    RSAMPLES = SAMPLES.collect{
+params.gQCENV = { def QCENV = get_always('PREQCENV'); QCENV = get_always('PREQCENV'); return QCENV }()
+params.gQCBIN = { def QCBIN = get_always('PREQCBIN'); QCBIN = get_always('PREQCBIN'); return QCBIN }()
+params.gQCPARAMS = { def QCPARAMS = get_always('fastqc_params_QC') ?: ''; QCPARAMS = get_always('fastqc_params_MULTI') ?: ''; return QCPARAMS }()
+params.gTRIMENV = get_always('TRIMMINGENV')
+params.gTRIMBIN = get_always('TRIMMINGBIN')
+params.gTRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
+params.gDEDUPENV = get_always('DEDUPENV')
+params.gDEDUPBIN = get_always('DEDUPBIN')
+params.gDEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
+params.gJAVAPARAMS = get_always('picard_params_JAVA') ?: ''
+params.gMAPENV = get_always('MAPPINGENV')
+params.gMAPBIN = get_always('MAPPINGBIN')
+params.gMAPIDX = get_always('MAPPINGIDX')
+params.gMAPUIDX = { def MAPUIDX = get_always('MAPPINGUIDX'); MAPUIDX = MAPUIDX.replace('.idx',''); return MAPUIDX }()
+params.gMAPUIDXNAME = get_always('MAPPINGUIDXNAME')
+params.gMAPREF = get_always('MAPPINGREF')
+params.gMAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
+params.gMAPANNO = get_always('MAPPINGANNO')
+params.gMAPPREFIX = get_always('MAPPINGPREFIX')
+params.gIDXPARAMS = get_always('star_params_INDEX') ?: ''
+params.gMAPPARAMS = get_always('star_params_MAP') ?: ''
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
     }
 }else{
-    RSAMPLES=SAMPLES.collect{
+    return params.gSAMPLES.collect{
         element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
     }
 }
-samples_ch = Channel.fromPath(RSAMPLES)
+}()
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_QC') ?: ''
 process qc_raw{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -54,26 +77,26 @@ process qc_raw{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_RAW{
     take:
     collection
     main:
-    qc_raw(samples_ch)
+    qc_raw(samples_ch())
     emit:
     qc = qc_raw.out.fastqc_results
 }
 process qc_trimmed{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -82,7 +105,7 @@ process qc_trimmed{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_TRIMMING{
@@ -93,14 +116,14 @@ workflow QC_TRIMMING{
     qc = qc_trimmed.out.fastqc_results
 }
 process qc_mapped{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -109,7 +132,7 @@ process qc_mapped{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS -f bam $map
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} -f bam $map
     """
 }
 workflow QC_MAPPING{
@@ -120,14 +143,14 @@ workflow QC_MAPPING{
     qc = qc_mapped.out.fastqc_results
 }
 process qc_dedup{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
+        if (filename.indexOf("zip") > 0)          "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
         else null
     }
     input:
@@ -136,7 +159,7 @@ process qc_dedup{
     path "*.{zip,html}", emit: fastqc_results
     script:
     """
-    fastqc --quiet -t ${task.cpus} $QCPARAMS --noextract -f fastq $read
+    fastqc --quiet -t ${task.cpus} ${params.gQCPARAMS} --noextract -f fastq $read
     """
 }
 workflow QC_DEDUP{
@@ -148,18 +171,15 @@ workflow QC_DEDUP{
 }
 
 
-TRIMENV=get_always('TRIMMINGENV')
-TRIMBIN=get_always('TRIMMINGBIN')
-TRIMPARAMS = get_always('trimgalore_params_TRIM') ?: ''
 process trim{
-    conda "/envs/$TRIMENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$TRIMENV"
+    conda "/envs/${params.gTRIMENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gTRIMENV}"
     cpus 4
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
-        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${COMBO}/${CONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
-        else if (filename.indexOf(".log") >0)              "LOGS/${COMBO}/${CONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("_trimmed.fastq.gz") > 0)     "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/_val_\d{1}|_trimmed|_dedup/,"")}_trimmed.fastq.gz"
+        else if (filename.indexOf("report.txt") >0)        "TRIMMED_FASTQ/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName().replaceAll(/.fastq.gz/,"")}_trimming_report.txt"
+        else if (filename.indexOf(".log") >0)              "LOGS/${params.gCOMBO}/${params.gCONDITION}/TRIMMING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -168,16 +188,16 @@ process trim{
     path "*_trimmed.fastq.gz", emit: trim
     path "*trimming_report.txt", emit: rep
     script:
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[0]
         r2 = reads[1]
         """
-        $TRIMBIN --cores ${task.cpus} --paired --gzip $TRIMPARAMS $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        ${params.gTRIMBIN} --cores ${task.cpus} --paired --gzip ${params.gTRIMPARAMS} $r1 $r2 &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/_R([1|2])_val_([1|2]).fq.gz/_R\\1_trimmed.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
         """
     }
     else{
         """
-        $TRIMBIN --cores ${task.cpus} --gzip $TRIMPARAMS $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
+        ${params.gTRIMBIN} --cores ${task.cpus} --gzip ${params.gTRIMPARAMS} $reads &> trim.log && rename 's/_dedup//g' *.fq.gz && rename 's/.fq.gz/.fastq.gz/g' *.fq.gz && rename 's/.fastq.gz_trimming/_trimming/g' *.txt
         """
     }
 }
@@ -185,13 +205,13 @@ workflow TRIMMING{
     take: 
     collection    
     main:
-    if ( PREDEDUP == 'enabled' ){  
+    if ( params.gPREDEDUP == 'enabled' ){  
         trim(collection)
     }else {        
-        if (PAIRED == 'paired'){
-            trim(samples_ch.collate(2))
+        if (params.gPAIRED == 'paired'){
+            trim(samples_ch().collate(2))
         } else{
-            trim(samples_ch.collate(1))
+            trim(samples_ch().collate(1))
         }
     }
     emit:
@@ -200,21 +220,17 @@ workflow TRIMMING{
 }
 
 
-DEDUPENV=get_always('DEDUPENV')
-DEDUPBIN=get_always('DEDUPBIN')
-DEDUPPARAMS = get_always('picard_params_DEDUP') ?: ''
-JAVAPARAMS = get_always('picard_params_JAVA') ?: ''
 process dedup_bam{
-    conda "/envs/$DEDUPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$DEDUPENV"
-    cpus THREADS
+    conda "/envs/${params.gDEDUPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gDEDUPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith("_dedup.bam"))              "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
-        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${COMBO}/${CONDITION}/DEDUP/${file(filename).getName()}"
-        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.endsWith("_dedup.bam"))              "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("_dedup.bam.bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
+        else if (filename.indexOf("dedup.log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/DEDUP/${file(filename).getName()}"
+        else if (filename.indexOf("metrix.txt") > 0)      "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
         else null
     }
     input:
@@ -232,7 +248,7 @@ process dedup_bam{
     outl = bams.getSimpleName()+"_dedup.log"
     outm = bams.getSimpleName()+"_dedup_metrix.txt"
     """
-    mkdir -p TMP && $DEDUPBIN $JAVAPARAMS MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm $DEDUPPARAMS &> $outl && samtools index $outf &>> $outl
+    mkdir -p TMP && ${params.gDEDUPBIN} ${params.gJAVAPARAMS} MarkDuplicates --REMOVE_DUPLICATES true --ASSUME_SORT_ORDER coordinate --TMP_DIR TMP --INPUT $bams --OUTPUT $outf --METRICS_FILE $outm ${params.gDEDUPPARAMS} &> $outl && samtools index $outf &>> $outl
     """
 }
 workflow DEDUPBAM{
@@ -250,18 +266,6 @@ workflow DEDUPBAM{
 }
 
 
-MAPENV = get_always('MAPPINGENV')
-MAPBIN = get_always('MAPPINGBIN')
-MAPIDX = get_always('MAPPINGIDX')
-MAPUIDX = get_always('MAPPINGUIDX')
-MAPUIDXNAME = get_always('MAPPINGUIDXNAME')
-MAPREF = get_always('MAPPINGREF')
-MAPREFDIR = "${workflow.workDir}/../"+get_always('MAPPINGREFDIR')
-MAPANNO = get_always('MAPPINGANNO')
-MAPPREFIX = get_always('MAPPINGPREFIX')
-MAPUIDX = MAPUIDX.replace('.idx','')
-IDXPARAMS = get_always('star_params_INDEX') ?: ''
-MAPPARAMS = get_always('star_params_MAP') ?: ''
 process collect_tomap{
     input:
     path check
@@ -273,44 +277,44 @@ process collect_tomap{
     """
 }
 process star_idx{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'copyNoFollow', overwrite: true,
     saveAs: {filename ->
-        if (filename.indexOf("Log.out") > 0)             "LOGS/${COMBO}/${CONDITION}/star_index.log"
-        else if (filename.indexOf(".idx") > 0)           "$MAPIDX"
-        else                                             "$MAPUIDX"
+        if (filename.indexOf("Log.out") > 0)             "LOGS/${params.gCOMBO}/${params.gCONDITION}/star_index.log"
+        else if (filename.indexOf(".idx") > 0)           "${params.gMAPIDX}"
+        else                                             "${params.gMAPUIDX}"
     }
     input:
     path genome
     path anno
     output:
-    path "$MAPUIDXNAME", emit: idx
+    path "${params.gMAPUIDXNAME}", emit: idx
     path "*.out", emit: idxlog
     path "*.idx", emit: tmpidx
     script:
     gen =  genome.getName()
     an  = anno.getName()
     """
-    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p $MAPUIDXNAME && $MAPBIN $IDXPARAMS --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir $MAPUIDXNAME --genomeFastaFiles tmp.fa && touch $MAPUIDXNAME && ln -s $MAPUIDXNAME star.idx && rm -f tmp.fa tmp_anno && ln -fs $MAPUIDXNAME/* . && rm -rf STARTMP
+    zcat $gen > tmp.fa && zcat $an > tmp_anno && mkdir -p ${params.gMAPUIDXNAME} && ${params.gMAPBIN} ${params.gIDXPARAMS} --runThreadN ${task.cpus} --runMode genomeGenerate --outTmpDir STARTMP --genomeDir ${params.gMAPUIDXNAME} --genomeFastaFiles tmp.fa && touch ${params.gMAPUIDXNAME} && ln -s ${params.gMAPUIDXNAME} star.idx && rm -f tmp.fa tmp_anno && ln -fs ${params.gMAPUIDXNAME}/* . && rm -rf STARTMP
     """
 }
 process star_mapping{
-    conda "/envs/$MAPENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$MAPENV"
-    cpus THREADS
+    conda "/envs/${params.gMAPENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gMAPENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     label 'big_mem'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${COMBO}/${CONDITION}/"+"${file(filename).getName()}"
-        else if (filename.indexOf(".log") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".out") >0)        "LOGS/${COMBO}/${CONDITION}/MAPPING/star_"+"${file(filename).getName()}"
-        else if (filename.indexOf(".tab") >0)        "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
-        else                                         "MAPPED/${COMBO}/${CONDITION}/"+"${filename}"
+        if (filename.indexOf("_unmapped") > 0)       "UNMAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${file(filename).getName()}"
+        else if (filename.indexOf(".log") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".out") >0)        "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/star_"+"${file(filename).getName()}"
+        else if (filename.indexOf(".tab") >0)        "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
+        else                                         "MAPPED/${params.gCOMBO}/${params.gCONDITION}/"+"${filename}"
     }
     input:
     path reads
@@ -324,7 +328,7 @@ process star_mapping{
     script:
     idx = reads[0]
     idxdir = idx.toRealPath()
-    if (PAIRED == 'paired'){
+    if (params.gPAIRED == 'paired'){
         r1 = reads[1]
         r2 = reads[2]
         a = "Trimming_report.txt"
@@ -332,31 +336,31 @@ process star_mapping{
         of = fn+'.Aligned.out.sam'
         gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
         """
-        $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+        ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2 --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && cat ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
         """
     }
     else{
-        if (PAIRED != 'singlecell'){
+        if (params.gPAIRED != 'singlecell'){
             read = reads[1]
             fn = file(reads[1]).getSimpleName().replaceAll(/(_dedup)?_trimmed$/,"")+"."
             of = fn+'Aligned.out.sam'
             gf = of.replaceAll(/\Q.Aligned.out.sam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $read --outFileNamePrefix $fn --outReadsUnmapped Fastx && gzip -c $of > $gf && rm -f $of && gzip *Unmapped.out* && for f in *mate*.gz; do mv "\$f" "\$(echo "\$f" | sed -r 's/\\.Unmapped.out.mate1.gz/_unmapped.fastq.gz/')"; done && mv *Log.out ${fn}_mapping.log
             """
         }
         else{
-            if (STRANDED == 'fr'){
+            if (params.gSTRANDED == 'fr'){
                 stranded = '--soloStrand Forward'
-            }else if (STRANDED == 'rf'){
+            }else if (params.gSTRANDED == 'rf'){
                 stranded = '--soloStrand Reverse'
             }else{
                 stranded = '--soloStrand Unstranded'
             }
             r1 = reads[1]
             fn = file(r1).getSimpleName().replaceAll(/_R1(_dedup)?_trimmed$/,"")
-            r2 = "${workflow.workDir}/../FASTQ/${CONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
-            if (MAPPARAMS.contains('--soloBarcodeMate 1')){
+            r2 = "${workflow.workDir}/../FASTQ/${params.gCONDITION}/"+file(reads[2]).getSimpleName().replaceAll(/\QR2_trimmed\E/,"R2.fastq.gz")
+            if (params.gMAPPARAMS.contains('--soloBarcodeMate 1')){
                 t = r2
                 r2 = r1
                 r1 = t
@@ -364,7 +368,7 @@ process star_mapping{
             of = fn+'.Aligned.sortedByCoord.out.bam'
             gf = of.replaceAll(/\Q.Aligned.sortedByCoord.out.bam\E/,"_mapped.sam.gz")
             """
-            $MAPBIN $MAPPARAMS $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
+            ${params.gMAPBIN} ${params.gMAPPARAMS} $stranded --outSAMattributes NH HI nM AS CR UR CB UB GX GN sS sQ sM --outSAMtype BAM SortedByCoordinate --runThreadN ${task.cpus} --genomeDir $idxdir --readFilesCommand zcat --readFilesIn $r1 $r2  --outFileNamePrefix ${fn}. --outReadsUnmapped Fastx && samtools view -h ${of} | gzip > $gf && rm -f $of && touch ${fn}.Unmapped.out.mate1 ${fn}.Unmapped.out.mate2 && cat ${fn}.Unmapped.out.mate1 | paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R1_unmapped.fastq.gz && at ${fn}.Unmapped.out.mate2| paste - - - - |tr \"\\t\" \"\\n\"| gzip > ${fn}_R2_unmapped.fastq.gz && mv *Log.out ${fn}_mapping.log
             """
         }
     }
@@ -372,14 +376,14 @@ process star_mapping{
 workflow MAPPING{
     take: collection
     main:
-    checkidx = file(MAPUIDX)
+    checkidx = file(params.gMAPUIDX)
     if (checkidx.exists()){
-        idxfile = Channel.fromPath(MAPUIDX)
+        idxfile = Channel.fromPath(params.gMAPUIDX)
         star_mapping(idxfile.combine(collection))
     }
     else{
-        genomefile = Channel.fromPath(MAPREF)
-        annofile = Channel.fromPath(MAPANNO)
+        genomefile = Channel.fromPath(params.gMAPREF)
+        annofile = Channel.fromPath(params.gMAPANNO)
         star_idx(genomefile, annofile)
         star_mapping(star_idx.out.idx.combine(collection))
     }
@@ -392,12 +396,12 @@ workflow MAPPING{
 process sortsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        if (filename.indexOf(".sam.gz") > 0)     "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
         else null
     }
     input:
@@ -415,14 +419,14 @@ process sortsam{
 process sam2bam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -441,13 +445,13 @@ process sam2bam{
 process uniqsam{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.sam.gz"
-        else if (filename.indexOf(".log") > 0)       "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.indexOf("unique.sam.gz") > 0)   "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.sam.gz"
+        else if (filename.indexOf(".log") > 0)       "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -458,26 +462,26 @@ process uniqsam{
     script:
     fn = file(sam[0]).getSimpleName()
     uniq = fn+'_unique.sam.gz'
-    if (COMBO.indexOf('-bwa') > 0){
+    if (params.gCOMBO.indexOf('-bwa') > 0){
         bwa = 'bwa'
     } else{
         bwa = ''
     }
     """
-    $BINS/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
+    ${params.gBINS}/Shells/UniqueSam_woPicard.sh $sam $uniq ${task.cpus} $bwa 2> uniq.log
     """
 }
 process sam2bamuniq{
     conda "/envs/samtools.yaml"
     container "oras://docker.io/jfallmann/monsda:"+"samtools"
-    cpus THREADS
+    cpus params.gTHREADS
     memory { 16.GB * (1 << ((task.attempt ?: 1) - 1)) }
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.endsWith(".bam"))       "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam"
-        else if (filename.indexOf(".bai") > 0)  "MAPPED/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.bam.bai"
-        else if (filename.indexOf(".log") > 0)  "LOGS/${COMBO}/${CONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
+        if (filename.endsWith(".bam"))       "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam"
+        else if (filename.indexOf(".bai") > 0)  "MAPPED/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.bam.bai"
+        else if (filename.indexOf(".log") > 0)  "LOGS/${params.gCOMBO}/${params.gCONDITION}/MAPPING/${file(filename).getSimpleName()}.log"
         else null
     }
     input:
@@ -508,19 +512,16 @@ workflow POSTMAPPING{
 }
 
 
-QCENV=get_always('PREQCENV')
-QCBIN=get_always('PREQCBIN')
-QCPARAMS = get_always('fastqc_params_MULTI') ?: ''
 process mqc{
-    conda "/envs/$QCENV"+".yaml"
-    container "oras://docker.io/jfallmann/monsda:"+"$QCENV"
-    cpus THREADS
+    conda "/envs/${params.gQCENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gQCENV}"
+    cpus params.gTHREADS
 	cache 'lenient'
     publishDir "${workflow.workDir}/../" , mode: 'link',
     saveAs: {filename ->
-        if (filename.indexOf("zip") > 0)          "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.zip"
-        else if (filename.indexOf("html") > 0)    "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getSimpleName()}.html"
-        else "QC/Multi/${COMBO}/${CONDITION}/${file(filename).getName()}"
+        if (filename.indexOf("zip") > 0)          "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.zip"
+        else if (filename.indexOf("html") > 0)    "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}.html"
+        else "QC/Multi/${params.gCOMBO}/${params.gCONDITION}/${file(filename).getName()}"
     }
     input:
     path others, stageAs: 'mqc_input??/*'
@@ -534,8 +535,8 @@ process mqc{
     LIST=multiqc_inputs.txt
     TMP_LIST=multiqc_inputs_unique.txt
     BASE_QC_DIR="${workflow.workDir}/../QC"
-    COMBO_VAL="${COMBO}"
-    CONDITION_VAL="${CONDITION}"
+    COMBO_VAL="${params.gCOMBO}"
+    CONDITION_VAL="${params.gCONDITION}"
     for i in $others; do
         dirname "\$i" >> "\$LIST"
     done
@@ -567,8 +568,8 @@ workflow MULTIQC{
 
 
 workflow {
-    QC_RAW(dummy)
-    TRIMMING(dummy)
+    QC_RAW(dummy())
+    TRIMMING(dummy())
     QC_TRIMMING(TRIMMING.out.trimmed)
     MAPPING(TRIMMING.out.trimmed)
     POSTMAPPING(MAPPING.out.mapped)
diff --git a/workflows/countreads.nf b/workflows/countreads.nf
index 377410ba..735e02e7 100644
--- a/workflows/countreads.nf
+++ b/workflows/countreads.nf
@@ -12,6 +12,7 @@ COUNTUIDX?.replace('.idx','')
 COUNTPARAMS = get_always('featurecounts_params_COUNT') ?: ''
 FEAT = get_always('COUNTINGFEAT') ?: ''
 COUNTMAP = get_always('COUNTINGMAP') ?: ''
+DEREPS = get_always('DEREPS') ?: ''
 
 //COUNTING PROCESSES
 process count_fastq{
@@ -226,8 +227,8 @@ process featurecount{
 
 
 process prepare_count_table{
-    conda "$DEENV"+".yaml"
-    container "oras://jfallmann/monsda:"+"$DEENV"
+    conda "base.yaml"
+    container "oras://jfallmann/monsda:"+"base"
     cpus THREADS
 	cache 'lenient'
     //validExitStatus 0,1
diff --git a/workflows/header.nf b/workflows/header.nf
index 7aaa6158..77d1a2b5 100644
--- a/workflows/header.nf
+++ b/workflows/header.nf
@@ -12,10 +12,7 @@ nextflow.enable.dsl=2
 
 //define unset Params
 def get_always(parameter){
-    if (!params.containsKey(parameter)){
-        params.put(parameter, null)
-    }
-    return params[parameter]
+    return params.containsKey(parameter) ? params[parameter] : null
 }
 
 //Params from CL
diff --git a/workflows/kallisto.nf b/workflows/kallisto.nf
index 2b3ecef1..1e032b57 100644
--- a/workflows/kallisto.nf
+++ b/workflows/kallisto.nf
@@ -6,7 +6,7 @@ COUNTUIDXNAME = get_always('COUNTINGUIDXNAME')+'.idx'
 COUNTREF = get_always('COUNTINGREF')
 COUNTREFDIR = "${workflow.workDir}/../"+get_always('COUNTINGREFDIR')
 COUNTANNO = get_always('COUNTINGANNO')
-COUNTDECOY = get_always('COUNTINDECOY')
+COUNTDECOY = get_always('COUNTINGDECOY')
 COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
 
 IDXPARAMS = get_always('kallisto_params_INDEX') ?: ''
@@ -76,8 +76,8 @@ process kallisto_idx{
 
     script:    
     gen =  genome.getName()
-    if (${COUNTINGDECOY}){
-        decoy = "-d "+"${COUNTINGDECOY}" 
+    if (COUNTDECOY && COUNTDECOY != 'None'){
+        decoy = "-d "+"${COUNTDECOY}" 
     }else{
         decoy = ''
     }
diff --git a/workflows/salmon.nf b/workflows/salmon.nf
index fc7eb919..c75b3dd0 100644
--- a/workflows/salmon.nf
+++ b/workflows/salmon.nf
@@ -7,7 +7,7 @@ COUNTUIDXNAME = get_always('COUNTINGUIDXNAME')
 COUNTREF = get_always('COUNTINGREF')
 COUNTREFDIR = "${workflow.workDir}/../"+get_always('COUNTINGREFDIR')
 COUNTANNO = get_always('COUNTINGANNO')
-COUNTDECOY = get_always('COUNTINDECOY')
+COUNTDECOY = get_always('COUNTINGDECOY')
 COUNTPREFIX = get_always('COUNTINGPREFIX') ?: COUNTBIN.split(' ')[0]
 
 IDXPARAMS = get_always('salmon_params_INDEX') ?: ''
@@ -78,8 +78,8 @@ process salmon_idx{
 
     script:    
     gen =  genome.getName()
-    if (${COUNTINGDECOY}){
-        decoy = "-d "+"${COUNTINGDECOY}" 
+    if (COUNTDECOY && COUNTDECOY != 'None'){
+        decoy = "-d "+"${COUNTDECOY}" 
     }else{
         decoy = ''
     }

From 2c936985b9b99cca043a76fe551a4b54ec685d06 Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Sun, 5 Jul 2026 21:03:01 +0200
Subject: [PATCH 09/80] fixed params error in DE runs

---
 MONSDA/Params.py                   | 4 ++--
 scripts/Analysis/DE/DESEQ2.R       | 2 +-
 scripts/Analysis/DE/EDGER.R        | 2 +-
 scripts/Analysis/DE/EDGER_no_rep.R | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/MONSDA/Params.py b/MONSDA/Params.py
index 80a71fbb..fb6767e1 100644
--- a/MONSDA/Params.py
+++ b/MONSDA/Params.py
@@ -1173,7 +1173,7 @@ def get_reps(samples: list, config: dict, analysis: str, process: str = "smk") -
         if (
             "BATCHES" in partconf
             and len(partconf["BATCHES"]) >= idx + 1
-            and partconf.get("BATCHES") != ""
+            and str(partconf["BATCHES"][idx]) != ""
         ):
             ret["batches"].append(str(partconf["BATCHES"][idx]).replace(",", "_"))
         else:
@@ -1181,7 +1181,7 @@ def get_reps(samples: list, config: dict, analysis: str, process: str = "smk") -
         if (
             "TYPES" in partconf
             and len(partconf["TYPES"]) >= idx + 1
-            and partconf.get("TYPE") != ""
+            and str(partconf["TYPES"][idx]) != ""
         ):
             ret["types"].append(str(partconf["TYPES"][idx]).replace(",", "_"))
         else:
diff --git a/scripts/Analysis/DE/DESEQ2.R b/scripts/Analysis/DE/DESEQ2.R
index aa3e59db..f934176f 100755
--- a/scripts/Analysis/DE/DESEQ2.R
+++ b/scripts/Analysis/DE/DESEQ2.R
@@ -39,7 +39,7 @@ BPPARAM <- MulticoreParam(workers = availablecores)
 
 ### SCRIPT
 ## Annotation
-sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE))
+sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE, sep = "\t"))
 colnames(sampleData_all) <- c("condition", "type", "batch")
 sampleData_all$batch <- as.factor(sampleData_all$batch)
 sampleData_all$type <- as.factor(sampleData_all$type)
diff --git a/scripts/Analysis/DE/EDGER.R b/scripts/Analysis/DE/EDGER.R
index d32cc218..933fc16b 100755
--- a/scripts/Analysis/DE/EDGER.R
+++ b/scripts/Analysis/DE/EDGER.R
@@ -41,7 +41,7 @@ gtf.df <- as.data.frame(gtf.rtl)
 gtf_gene <- droplevels(subset(gtf.df, type == "gene"))
 
 ## Annotation
-sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE))
+sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE, sep = "\t"))
 colnames(sampleData_all) <- c("condition", "type", "batch")
 sampleData_all$condition <- as.factor(sampleData_all$condition)
 sampleData_all$batch <- as.factor(sampleData_all$batch)
diff --git a/scripts/Analysis/DE/EDGER_no_rep.R b/scripts/Analysis/DE/EDGER_no_rep.R
index ea27bbd9..dd02894e 100755
--- a/scripts/Analysis/DE/EDGER_no_rep.R
+++ b/scripts/Analysis/DE/EDGER_no_rep.R
@@ -46,7 +46,7 @@ gtf.df <- as.data.frame(gtf.rtl)
 gtf_gene <- droplevels(subset(gtf.df, type == "gene"))
 
 ## Annotation
-sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE))
+sampleData_all <- as.data.frame(read.table(gzfile(anname), row.names = 1, check.names = FALSE, sep = "\t"))
 colnames(sampleData_all) <- c("condition", "type", "batch")
 sampleData_all$condition <- as.factor(sampleData_all$condition)
 sampleData_all$batch <- as.factor(sampleData_all$batch)

From a1d0e44a42d21e157f8f7508025e25ed800a87ca Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Sun, 5 Jul 2026 21:08:24 +0200
Subject: [PATCH 10/80] disbled legacy parser

---
 MONSDA/RunMONSDA.py | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

diff --git a/MONSDA/RunMONSDA.py b/MONSDA/RunMONSDA.py
index fc287583..bbbf838d 100755
--- a/MONSDA/RunMONSDA.py
+++ b/MONSDA/RunMONSDA.py
@@ -2,7 +2,6 @@
 
 import argparse
 import os
-import re
 import shlex
 import shutil
 import subprocess
@@ -857,19 +856,9 @@ def run_nextflow(
 def runjob(jobtorun):
     try:
         logid = scriptname + ".runjob: "
-        run_cmd = jobtorun
-        if (
-            re.search(r"(^|\s)nextflow(\s|$)", jobtorun)
-            and "NXF_SYNTAX_PARSER" not in jobtorun
-        ):
-            run_cmd = "NXF_SYNTAX_PARSER=v1 " + jobtorun
-            log.warning(
-                logid
-                + "Nextflow 26.04+ strict parser detected; forcing legacy parser (NXF_SYNTAX_PARSER=v1) for compatibility with generated workflows."
-            )
         # return subprocess.run(jobtorun, shell=True, universal_newlines=True, capture_output=True)  # python >= 3.7
         job = subprocess.Popen(
-            run_cmd,
+            jobtorun,
             shell=True,
             universal_newlines=True,
             stdout=subprocess.PIPE,

From da04552199b95822e1ffd63606cfc8fc2a5683dc Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Mon, 6 Jul 2026 22:58:19 +0200
Subject: [PATCH 11/80] Added Star-fusion in star or fastq mode, fable built
 tests and workflows based on CIRI2 template

---
 .vscode/settings.json                         |   3 +-
 MONSDA/Workflows.py                           |  39 +-
 configs/template.json                         |  24 +-
 configs/template_base_commented.json          |   2 +-
 configs/template_clean.json                   |  22 +
 containers/apptainer/starfusion.def           |  23 ++
 docs/source/workflows.rst                     |  31 ++
 envs/starfusion.yaml                          |   7 +
 profile_snakemake/cluster_config.yaml         |   8 +
 tests/data/config_DTU_test.json               |  16 +-
 tests/golden/config_FUSIONS/nf/_filelist.txt  |   2 +
 ...ard-star_FUSIONS_starfusion_subconfig.json |  49 +++
 ...-picard-star_FUSIONS_starfusion_subflow.nf |  97 +++++
 tests/golden/config_FUSIONS/smk/_filelist.txt |   2 +
 ...ard-star_FUSIONS_starfusion_subconfig.json |  49 +++
 ...icard-star_FUSIONS_starfusion_subsnake.smk | 387 ++++++++++++++++++
 tests/test_Workflows.py                       |  53 +++
 workflows/starfusion.nf                       |  64 +++
 workflows/starfusion.smk                      |  71 ++++
 19 files changed, 944 insertions(+), 5 deletions(-)
 create mode 100644 containers/apptainer/starfusion.def
 create mode 100644 envs/starfusion.yaml
 create mode 100644 tests/golden/config_FUSIONS/nf/_filelist.txt
 create mode 100644 tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
 create mode 100644 tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subflow.nf
 create mode 100644 tests/golden/config_FUSIONS/smk/_filelist.txt
 create mode 100644 tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
 create mode 100644 tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subsnake.smk
 create mode 100644 workflows/starfusion.nf
 create mode 100644 workflows/starfusion.smk

diff --git a/.vscode/settings.json b/.vscode/settings.json
index b3c483e6..b04e345e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -28,5 +28,6 @@
     "[python]": {
         "editor.defaultFormatter": "ms-python.black-formatter"
     },
-    "python.formatting.provider": "none"
+    "python.formatting.provider": "none",
+    "makefile.configureOnOpen": false
 }
diff --git a/MONSDA/Workflows.py b/MONSDA/Workflows.py
index 82125342..424d1549 100755
--- a/MONSDA/Workflows.py
+++ b/MONSDA/Workflows.py
@@ -431,7 +431,7 @@ def get_processes(config):
     # Define workflow stages
     pre = ["QC", "FETCH", "BASECALL"]
     sub = ["TRIMMING", "MAPPING", "DEDUP", "QC"]
-    post = ["COUNTING", "TRACKS", "PEAKS", "DE", "DEU", "DAS", "DTU", "CIRCS"]
+    post = ["COUNTING", "TRACKS", "PEAKS", "DE", "DEU", "DAS", "DTU", "CIRCS", "FUSIONS"]
 
     wfs = [x.replace(" ", "") for x in config["WORKFLOWS"].split(",")]
 
@@ -643,6 +643,7 @@ def create_subworkflow(config, subwork, conditions, envs=None, stage=None):
                                 "COUNTING",
                                 "TRACKS",
                                 "CIRCS",
+                                "FUSIONS",
                             ]
                         ]
                     ):
@@ -1373,6 +1374,12 @@ def make_post(
                             "CIRI2 needs BWA mapped files, will skip input produced otherwise"
                         )
                         continue
+                if postworkflow == "FUSIONS":
+                    if toolenv == "starfusion" and "star" not in envs:
+                        log.warning(
+                            "STAR-Fusion needs STAR chimeric output, will skip input produced otherwise"
+                        )
+                        continue
 
                 # Deep-merge postworkflow config from all conditions (preserves all condition paths)
                 for cond in all_conditions:
@@ -2231,6 +2238,30 @@ def nf_fetch_params(
         retconf["CIRCSREFDIR"] = REFDIR
         retconf["CIRCSANNO"] = ANNOTATION
 
+    # FUSIONS Variables
+    if "FUSIONS" in config:
+        FUSCONF = mu.sub_dict(config["FUSIONS"], SETUP)
+        FUSBIN, FUSENV = mp.env_bin_from_config(config, "FUSIONS")
+        log.debug(logid + "FUSIONSCONFIG: " + str(SETUP) + "\t" + str(FUSCONF))
+        REF = FUSCONF.get("REFERENCE")
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        FANNO = FUSCONF.get("ANNOTATION")
+        if FANNO:
+            ANNOTATION = FANNO
+        else:
+            ANNOTATION = (
+                ANNO.get("GTF") if "GTF" in ANNO else ANNO.get("GFF")
+            )  # by default GTF format will be used
+
+        retconf["FUSIONSREF"] = REFERENCE
+        retconf["FUSIONSREFDIR"] = REFDIR
+        retconf["FUSIONSANNO"] = ANNOTATION
+        retconf["FUSIONSLIB"] = (
+            FUSCONF.get(FUSENV, {}).get("OPTIONS", {}).get("INDEX", "")
+        )
+
     retconf["REFERENCE"] = REFERENCE
     retconf["REFDIR"] = REFDIR
     retconf["IP"] = IP
@@ -3477,6 +3508,12 @@ def nf_make_post(
                             "CIRI2 needs BWA mapped files, will skip input produced otherwise"
                         )
                         continue
+                if subwork == "FUSIONS":
+                    if toolenv == "starfusion" and "star" not in envs:
+                        log.warning(
+                            "STAR-Fusion needs STAR chimeric output, will skip input produced otherwise"
+                        )
+                        continue
 
                 # Merge subwork config from all conditions
                 for cond in all_conditions:
diff --git a/configs/template.json b/configs/template.json
index 5ec96ff3..a23f4e4d 100644
--- a/configs/template.json
+++ b/configs/template.json
@@ -1,5 +1,5 @@
 {
-    "WORKFLOWS": "", #which workflows do you want to run "MAPPING, QC, DEDUP, TRIMMING, COUNTING, TRACKS, PEAKS, DE, DEU, DAS, DTU, CIRCS"
+    "WORKFLOWS": "", #which workflows do you want to run "MAPPING, QC, DEDUP, TRIMMING, COUNTING, TRACKS, PEAKS, DE, DEU, DAS, DTU, CIRCS, FUSIONS"
     "BINS": "", #where to find customscripts used in the workflow !!!ADVANCED USAGE ONLY!!!
     "MAXTHREADS": "20", #maximum number of cores to use, make sure this fits your needs
     "VERSION": "1.4.0", #needs to be identical to the installed version for reproducibility reasons
@@ -509,5 +509,27 @@
                 }
             }
         }
+    },
+    "FUSIONS" : {
+        "TOOLS" :
+        {
+            "starfusion" : "STAR-Fusion"
+        },
+        "id": {
+            "condition": {
+                "setting": {
+                    "ENV" : "starfusion",
+                    "BIN" : "STAR-Fusion",
+                    "starfusion":{
+                        "OPTIONS":
+                        {
+                            "INDEX": "PATH_TO_CTAT_genome_lib_dir",  # Path to an existing CTAT genome resource library, will be built from REFERENCE/ANNOTATION if not present
+                            "BUILD": "",  # Additional options for prep_genome_lib.pl when building the CTAT library
+                            "FUSION": ""  # Additional options for STAR-Fusion; set "FASTQ": "true" to run in FASTQ mode instead of consuming STAR chimeric junctions
+                        }
+                    }
+                }
+            }
+        }
     }
 }
diff --git a/configs/template_base_commented.json b/configs/template_base_commented.json
index b3e820ad..4fb0cfec 100644
--- a/configs/template_base_commented.json
+++ b/configs/template_base_commented.json
@@ -1,6 +1,6 @@
 {
   "comment":{
-    "WORKFLOWS": "Select workflows you want to run, possible values: FETCH, MAPPING, QC, DEDUP, TRIMMING, COUNTING, TRACKS, PEAKS, DE, DEU, DAS, DTU, CIRCS",
+    "WORKFLOWS": "Select workflows you want to run, possible values: FETCH, MAPPING, QC, DEDUP, TRIMMING, COUNTING, TRACKS, PEAKS, DE, DEU, DAS, DTU, CIRCS, FUSIONS",
     "BINS": "Where to find the scripts used in the workflow, per default the scripts directory that comes with MONSDA. ONLY CHANGE IF YOU KNOW WHAT YOU ARE DOING!",
     "MAXTHREADS": "Maximum number of cores to use, make sure this fits your needs, acts as upper cap.",
     "VERSION": "Version of MONSDA to use, needs to match installed version"
diff --git a/configs/template_clean.json b/configs/template_clean.json
index 94e94949..903d7abe 100644
--- a/configs/template_clean.json
+++ b/configs/template_clean.json
@@ -482,4 +482,26 @@
             }
         }
     }
+  },
+  "FUSIONS" : {
+    "TOOLS" :
+    {
+      "starfusion" : "STAR-Fusion"
+    },
+    "id": {
+        "condition": {
+            "setting": {
+                "ENV" : "starfusion",
+                "BIN" : "STAR-Fusion",
+                "starfusion":{
+                    "OPTIONS":
+                    {  
+                        "INDEX": "PATH_TO_CTAT_genome_lib_dir",
+                        "BUILD": "",
+                        "FUSION": ""
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/containers/apptainer/starfusion.def b/containers/apptainer/starfusion.def
new file mode 100644
index 00000000..64818735
--- /dev/null
+++ b/containers/apptainer/starfusion.def
@@ -0,0 +1,23 @@
+Bootstrap: docker
+From: continuumio/miniconda3
+   
+%files
+    /home/fall/MONSDA/envs/starfusion.yaml /opt/envs/
+    ${HOME}/MONSDA/scripts /opt/MONSDA/
+
+%environment
+   
+%post
+    ls -alrt /opt/envs
+    chmod -R +x /opt/envs/starfusion.yaml
+    
+    ENV_NAME=starfusion
+    echo ". /opt/conda/etc/profile.d/conda.sh" >> $APPTAINER_ENVIRONMENT
+    echo "conda activate $ENV_NAME" >> $APPTAINER_ENVIRONMENT
+   
+    . /opt/conda/etc/profile.d/conda.sh
+    conda env create -f /opt/envs/starfusion.yaml -p /opt/conda/envs/$ENV_NAME
+    conda clean --all
+   
+%runscript
+    exec "$@"
\ No newline at end of file
diff --git a/docs/source/workflows.rst b/docs/source/workflows.rst
index 61b75be0..07f1fb85 100644
--- a/docs/source/workflows.rst
+++ b/docs/source/workflows.rst
@@ -32,6 +32,8 @@ DTU -> Differential Transcript Usage Analysis
 
 CIRCS -> Circular RNA identification
 
+FUSIONS -> Fusion gene detection
+
 
 PREPROCESSING
 =============
@@ -304,3 +306,32 @@ Find circular RNAs in mapping data, CIRI2 needs to be installed locally.
   +=======+=========================================================================================================================================================================================================+========+====================+=======================================================================+========+==================+
   | CIRI2 | CIRI (circRNA identifier) is a novel chiastic clipping signal based algorithm,which can unbiasedly and accurately detect circRNAs from transcriptome data by employing multiple filtration strategies.  | ciri2  | $Path_to_CIRI2.pl  | `ciri2 `_  | BAM    | BED/BEDG/BIGWIG  |
   +-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+--------------------+-----------------------------------------------------------------------+--------+------------------+
+
+
+FUSIONS
+###############
+
+Detect fusion genes with STAR-Fusion. FUSIONS requires STAR in the MAPPING step and, by
+default, consumes the chimeric junctions produced by STAR. STAR does not emit chimeric output
+unless configured to do so, therefore add the chimeric options to the STAR ``MAP`` options in
+the MAPPING config, e.g. ``--chimSegmentMin 12 --chimOutType Junctions``. MONSDA then finds the
+resulting ``MAPPED//.Chimeric.out.junction`` files and only runs STAR-Fusion when
+they exist. Set ``"FASTQ": "true"`` in the FUSIONS ``OPTIONS`` to run STAR-Fusion directly on the
+trimmed FASTQ files instead of the chimeric junctions.
+
+STAR-Fusion needs a CTAT genome resource library. Point ``INDEX`` to an existing
+``genome_lib_dir``; if it does not exist, it is built from ``REFERENCE`` and ``ANNOTATION`` with
+``prep_genome_lib.pl`` (extra build options can be passed via ``BUILD``). Building a CTAT library
+is resource intensive, so the bundled cluster profile sets a long runtime for the
+``generate_ctat_lib`` rule.
+
+.. table:: 
+  :widths: 10, 40, 10, 10, 10, 10, 10
+  :class: tight-table
+
+  +-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------+-------------+----------------------------------------------------------------+-------------------------------+---------+
+  | TOOL        | DESCRIPTION                                                                                                                                                                             | ENV        | BIN         | LINK                                                           | INPUT                         | OUTPUT  |
+  +=============+=========================================================================================================================================================================================+============+=============+================================================================+===============================+=========+
+  | STAR-Fusion | STAR-Fusion identifies candidate fusion transcripts from the chimeric alignments produced by the STAR aligner using a CTAT genome resource library.                                      | starfusion | STAR-Fusion | `starfusion `_ | Chimeric.out.junction / FASTQ | TSV     |
+  +-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------+-------------+----------------------------------------------------------------+-------------------------------+---------+
+
diff --git a/envs/starfusion.yaml b/envs/starfusion.yaml
new file mode 100644
index 00000000..4f431f93
--- /dev/null
+++ b/envs/starfusion.yaml
@@ -0,0 +1,7 @@
+name: starfusion
+channels:
+  - conda-forge
+  - bioconda
+  - nodefaults
+dependencies:
+  - star-fusion =1.15.1
diff --git a/profile_snakemake/cluster_config.yaml b/profile_snakemake/cluster_config.yaml
index f9f7b122..f2f619f8 100644
--- a/profile_snakemake/cluster_config.yaml
+++ b/profile_snakemake/cluster_config.yaml
@@ -14,5 +14,13 @@ generate_index:
 mapping:
     mem: 200GB
 
+generate_ctat_lib:
+    mem: 200GB
+    time: 2880
+
+starfusion:
+    mem: 100GB
+    time: 1500
+
 multiqc:
    mem: 30GB
diff --git a/tests/data/config_DTU_test.json b/tests/data/config_DTU_test.json
index 6852e250..c990a099 100644
--- a/tests/data/config_DTU_test.json
+++ b/tests/data/config_DTU_test.json
@@ -1,5 +1,5 @@
 {
-    "WORKFLOWS": "QC,TRIMMING,DEDUP,MAPPING,DTU",
+    "WORKFLOWS": "QC,TRIMMING,DEDUP,MAPPING,DTU,FUSIONS",
     "BINS": "",
     "MAXTHREADS": "4",
     "VERSION": "FIXME",
@@ -124,5 +124,19 @@
                 }
             }
         }
+    },
+    "FUSIONS": {
+        "TOOLS": {
+            "starfusion": "STAR-Fusion"
+        },
+        "Test": {
+            "starfusion": {
+                "OPTIONS": {
+                    "INDEX": "GENOMES/ctat_genome_lib",
+                    "BUILD": "",
+                    "FUSION": ""
+                }
+            }
+        }
     }
 }
diff --git a/tests/golden/config_FUSIONS/nf/_filelist.txt b/tests/golden/config_FUSIONS/nf/_filelist.txt
new file mode 100644
index 00000000..a1df64ea
--- /dev/null
+++ b/tests/golden/config_FUSIONS/nf/_filelist.txt
@@ -0,0 +1,2 @@
+allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subflow.nf
diff --git a/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json b/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
new file mode 100644
index 00000000..ba769040
--- /dev/null
+++ b/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
@@ -0,0 +1,49 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "FUSIONS": {
+    "Test": {
+      "starfusion": {
+        "OPTIONS": {
+          "BUILD": "",
+          "FUSION": "",
+          "INDEX": "GENOMES/ctat_genome_lib"
+        }
+      }
+    }
+  },
+  "FUSIONSBIN": "STAR-Fusion",
+  "FUSIONSENV": "starfusion",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subflow.nf b/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subflow.nf
new file mode 100644
index 00000000..acd2e5fe
--- /dev/null
+++ b/tests/golden/config_FUSIONS/nf/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subflow.nf
@@ -0,0 +1,97 @@
+#!/usr/bin/env nextflow
+nextflow.enable.dsl=2
+def get_always(parameter){
+    return params.containsKey(parameter) ? params[parameter] : null
+}
+params.gREFERENCE = "${workflow.workDir}/../"+get_always('REFERENCE')
+params.gREFDIR = "${workflow.workDir}/../"+get_always('REFDIR')
+params.gBINS = get_always('BINS')
+params.gTHREADS = get_always('MAXTHREAD')
+params.gPAIRED = get_always('PAIRED') ?: null
+params.gRUNDEDUP = get_always('RUNDEDUP') ?: null
+params.gPREDEDUP = get_always('PREDEDUP') ?: null
+params.gSTRANDED = get_always('STRANDED') ?: null
+params.gIP = get_always('IP') ?: null
+params.gCONDITION = get_always('CONDITION') ?: null
+params.gCOMBO = get_always('COMBO') ?: ''
+params.gSCOMBO = get_always('SCOMBO') ?: ''
+params.gSAMPLES = get_always('SAMPLES').split(',') ?: null
+params.gLONGSAMPLES = get_always('LONGSAMPLES').split(',') ?: null
+params.gSHORTSAMPLES = get_always('SHORTSAMPLES').split(',') ?: null
+params.gSETS = get_always('SETS') ?: null
+def dummy(){
+    def dummy = Channel.fromPath("${workflow.workDir}/../LOGS/MONSDA.log")
+    return dummy
+}
+def samples_ch(){
+    def samples_ch = Channel.fromPath(params.gRSAMPLES)
+    return samples_ch
+}
+params.gFUSENV = get_always('FUSIONSENV')
+params.gFUSBIN = get_always('FUSIONSBIN')
+params.gFUSREF = get_always('FUSIONSREF')
+params.gFUSREFDIR = "${workflow.workDir}/../"+get_always('FUSIONSREFDIR')
+params.gFUSANNO = get_always('FUSIONSANNO')
+params.gFUSLIB = get_always('FUSIONSLIB')
+params.gFUSPARAMS = get_always('starfusion_params_FUSION') ?: ''
+params.gFUSBUILD = get_always('starfusion_params_BUILD') ?: ''
+params.gRSAMPLES = {
+if (params.gPAIRED == 'paired' || params.gPAIRED == 'singlecell'){
+    return params.gSAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+"_{R2,R1}.*fastq.gz"
+    }
+}else{
+    return params.gSAMPLES.collect{
+        element -> return "${workflow.workDir}/../FASTQ/"+element+".*fastq.gz"
+    }
+}
+}()
+
+process starfusion{
+    conda "/envs/${params.gFUSENV}"+".yaml"
+    container "oras://docker.io/jfallmann/monsda:"+"${params.gFUSENV}"
+    cpus params.gTHREADS
+	cache 'lenient'
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_fusions") > 0)      "FUSIONS/${params.gSCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}"        
+        else if (filename.indexOf(".log") > 0)        "LOGS/${params.gSCOMBO}/${params.gCONDITION}/${file(filename).getSimpleName()}"
+    }
+    input:
+    path fls
+    output:
+    path "*_fusions", emit: fusions
+    path "log", emit: log
+    script:
+    ref = fls[0]
+    anno = fls[1]
+    junction = fls[2]        
+    fn = file(junction).getSimpleName()
+    of = fn+"_fusions"
+    ol = fn+".log"
+    """
+    if [[ -f \"${params.gFUSLIB}/ref_annot.gtf\" ]]; then CTAT=\"${params.gFUSLIB}\"; else CTAT=CTAT; mkdir -p \$CTAT && zcat ${ref} > ctat_ref.fa && zcat ${anno} > ctat_ref.gtf && prep_genome_lib.pl --genome_fa ctat_ref.fa --gtf ctat_ref.gtf --output_dir \$CTAT --CPU ${task.cpus} ${params.gFUSBUILD} &> log; fi; if [[ -s \"${junction}\" ]]; then ${params.gFUSBIN} --genome_lib_dir \$CTAT -J ${junction} --output_dir TMP --CPU ${task.cpus} ${params.gFUSPARAMS} &>> log && cp TMP/star-fusion.fusion_predictions.tsv ${of} 2>> log; else echo \"File ${junction} empty, no chimeric STAR output found\" >> log; fi; touch ${of}
+    """
+}
+workflow FUSIONS{ 
+    take: collection
+    main:
+    MAPPEDSAMPLES = params.gLONGSAMPLES.collect{
+        element -> return "${workflow.workDir}/../MAPPED/${params.gCOMBO}/"+element+"*.Chimeric.out.junction"
+    }
+    mapsamples_ch = Channel.fromPath(MAPPEDSAMPLES.sort())  
+    annofile = Channel.fromPath(params.gFUSANNO)
+    genomefile = Channel.fromPath(params.gFUSREF)
+    starfusion(genomefile.combine(annofile.combine(mapsamples_ch.collate(1))))
+    emit:
+    fusions = starfusion.out.fusions
+    logs = starfusion.out.log
+}
+
+
+
+
+workflow {
+    FUSIONS(dummy())
+}
+
diff --git a/tests/golden/config_FUSIONS/smk/_filelist.txt b/tests/golden/config_FUSIONS/smk/_filelist.txt
new file mode 100644
index 00000000..64f75e1f
--- /dev/null
+++ b/tests/golden/config_FUSIONS/smk/_filelist.txt
@@ -0,0 +1,2 @@
+allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
+allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subsnake.smk
diff --git a/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json b/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
new file mode 100644
index 00000000..ba769040
--- /dev/null
+++ b/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subconfig.json
@@ -0,0 +1,49 @@
+{
+  "BINS": "/MONSDA/MONSDA/scripts",
+  "FUSIONS": {
+    "Test": {
+      "starfusion": {
+        "OPTIONS": {
+          "BUILD": "",
+          "FUSION": "",
+          "INDEX": "GENOMES/ctat_genome_lib"
+        }
+      }
+    }
+  },
+  "FUSIONSBIN": "STAR-Fusion",
+  "FUSIONSENV": "starfusion",
+  "MAXTHREADS": "4",
+  "RUNDEDUP": "enabled",
+  "SAMPLES": [
+    "Test/SRR6357070",
+    "Test/SRR6357071",
+    "Test/SRR6357072"
+  ],
+  "SETTINGS": {
+    "Test": {
+      "ANNOTATION": {
+        "GFF": "GENOME/genomic.gff.gz",
+        "GTF": "GENOME/genomic.gtf.gz"
+      },
+      "BATCHES": "",
+      "DECOY": "",
+      "GROUPS": [
+        "T1",
+        "T2",
+        "T3"
+      ],
+      "INDEX": "",
+      "IP": "",
+      "PREFIX": "",
+      "REFERENCE": "GENOME/genome.fa.gz",
+      "SAMPLES": [
+        "SRR6357070",
+        "SRR6357071",
+        "SRR6357072"
+      ],
+      "SEQUENCING": "paired",
+      "TYPES": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subsnake.smk b/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subsnake.smk
new file mode 100644
index 00000000..04411316
--- /dev/null
+++ b/tests/golden/config_FUSIONS/smk/allconditions_fastqc-trimgalore-picard-star_FUSIONS_starfusion_subsnake.smk
@@ -0,0 +1,387 @@
+import glob
+import os
+import sys
+import inspect
+import snakemake
+import json
+import shutil
+import logging
+import tempfile
+import traceback as tb
+from collections import defaultdict
+from itertools import combinations
+import re
+cmd_subfolder = [os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA/MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"../MONSDA"), os.path.join(os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile( inspect.currentframe() )) )),"MONSDA")]
+for x in cmd_subfolder:
+    if x not in sys.path:
+        sys.path.insert(0, x)
+from MONSDA.Logger import makelogdir, setup_logger
+from MONSDA.Params import checkpaired, checkstranded, check_IP, sampleslong, basecall_samples, download_samples, env_bin_from_config, tool_params, samplecond, conditiononly, comparable_as_string, get_cutoff_as_string, get_reps, get_diego_samples, get_diego_groups, get_pairing, set_pairing, get_samples_postprocess, fixRunParameters
+from MONSDA.Utils import get_dict_hash, sub_dict, makeoutdir, keysets_from_dict, dict_inst
+loglevel='INFO'
+try:
+    scriptname = os.path.basename(inspect.stack()[-1].filename).replace('.py','')
+    if any(x in scriptname for x in ['MONSDA','Configurator']):
+        log = logging.getLogger(scriptname)
+    else:
+        log = logging.getLogger('snakemake')
+        for handler in log.handlers[:]:
+            handler.close()
+            log.removeHandler(handler)
+    handler = logging.FileHandler('LOGS/MONSDA.log', mode='a')
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M'))
+    log.addHandler(handler)
+    lvl = loglevel
+    log.setLevel(lvl)
+except Exception as err:
+    log = setup_logger(name='MONSDA.header', log_file='LOGS/MONSDA.log', logformat='%(asctime)s %(levelname)-8s %(name)-12s %(message)s', datefmt='%m-%d %H:%M', level=loglevel, filemode='a')
+    #log = setup_logger(name=scriptname, log_file='stderr', logformat='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', level=loglevel)
+    exc_type, exc_value, exc_tb = sys.exc_info()
+    tbe = tb.TracebackException(
+        exc_type, exc_value, exc_tb,
+    )
+    log.error(''.join(tbe.format()))
+logid = 'header.smk: '
+#Make sure we have a TMP directory
+makeoutdir("TMP")
+# Parse SUBCONFIG
+try:
+    installpath = os.path.dirname(__file__).replace(
+        os.sep.join(["lib", "python3.9", "site-packages", "MONSDA"]), "share"
+    )
+except:
+    installpath = os.path.cwd()
+BINS = config.get("BINS")
+MAXTHREAD = int(config["MAXTHREADS"])
+# set maximum available memory for sorting
+def get_sortmem(w, resources):
+    # Otherwise, try mem of form 8G / 8GB, again skipping ``
+    if hasattr(resources, "mem"):
+        mem_val = str(resources.mem)
+        if mem_val != "":
+            mem_str = re.sub(r"GB?$", "", mem_val, flags=re.IGNORECASE).strip()
+            return max(int(mem_str) -2, 4)  # reserve 2GB for other processes, min 4GB
+    # Convert mem_mb if present and not ``
+    if hasattr(resources, "mem_mb"):
+        mem_val = str(resources.mem_mb)
+        if mem_val != "":
+            mem_mb = int(mem_val)
+            # round up to full GB
+            return max(int((mem_mb + 1023) / 1024) - 2, 4)  # reserve 2GB for other processes, min 4GB
+    # Fallback if everything is `` or missing
+    # pick something conservative but nonzero
+    return 4  # 4 GB
+if not config.get('FETCH', False) and not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in sampleslong(config)]  
+elif not config.get("BASECALL", False):
+    SAMPLES = [os.path.join(x) for x in download_samples(config)] 
+else:
+    SAMPLES = [os.path.join(x) for x in basecall_samples(config)]
+if len(SAMPLES) < 1:
+    log.error(logid+'No samples found, please check config file')
+    sys.exit(logid+'ERROR: No samples found, please check config file')
+SETUP = keysets_from_dict(config['SETTINGS'], 'SAMPLES')[0]
+SETS = os.sep.join(SETUP)
+SETTINGS = sub_dict(config['SETTINGS'], SETUP)
+# Parse SETTINGS
+SEQUENCING = SETTINGS.get('SEQUENCING')
+REFERENCE = SETTINGS.get('REFERENCE')
+REFDIR = str(os.path.dirname(REFERENCE))
+INDEX = SETTINGS.get('INDEX')
+INDEX2 = SETTINGS.get('INDEX2')
+PREFIX = SETTINGS.get('PREFIX')
+ANNO = SETTINGS.get('ANNOTATION')
+DECOY = SETTINGS.get("DECOY")
+IP = SETTINGS.get('IP')
+rundedup = True if (config.get('RUNDEDUP')) == 'enabled' else False
+prededup = True if (config.get('PREDEDUP')) == 'enabled' else False
+ANNOTATION = False  # by default no annotation is set for every workflow step
+if rundedup:
+    if prededup:
+        log.info(logid+'(PRE)DEDUPLICATION ENABLED')
+    else:
+        log.info(logid+'DEDUPLICATION ENABLED')
+log.info(logid+'Working on SAMPLES: '+str(SAMPLES))
+paired = checkpaired([SAMPLES[0]], config)
+if paired == 'paired':
+    log.info('RUNNING SNAKEMAKE IN PAIRED READ MODE')
+elif paired == 'singlecell':
+    log.info('RUNNING SNAKEMAKE IN Singlecell MODE')
+stranded = checkstranded([SAMPLES[0]], config)
+if stranded != '':
+    log.info('RUNNING SNAKEMAKE WITH STRANDEDNESS '+str(stranded))
+# MAPPING Variables
+if 'MAPPING' in config:
+    MAPPERBIN, MAPPERENV = env_bin_from_config(config, 'MAPPING')
+    MAPCONF = sub_dict(config['MAPPING'], SETUP)
+    MAPPERENV = MAPPERENV.split('_')[0]
+    log.debug(logid+'MAPPINGCONFIG: '+str(SETUP)+'\t'+str(MAPCONF))
+    REF = MAPCONF[MAPPERENV].get("REFERENCE", MAPCONF.get("REFERENCE", config['MAPPING'].get("REFERENCE")))
+    MANNO = MAPCONF[MAPPERENV].get("ANNOTATION", MAPCONF.get("ANNOTATION", config['MAPPING'].get("ANNOTATION")))
+    MDECOY = MAPCONF[MAPPERENV].get("DECOY", MAPCONF.get("DECOY", config['MAPPING'].get("DECOY")))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if MANNO and MANNO != '':
+        ANNOTATION = MANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if MDECOY and not dict_inst(MDECOY):
+            DECOY = os.path.abspath(MDECOY)
+    elif dict_inst(DECOY) and DECOY.get(MAPPERENV):
+            DECOY = os.path.abspath(DECOY.get(MAPPERENV))
+    else:
+        DECOY = None
+    IDX = MAPCONF.get('INDEX', MAPCONF[MAPPERENV].get('INDEX'))
+    if IDX:
+        INDEX = IDX
+    if not INDEX:
+        INDEX = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx'
+    keydict = sub_dict(tool_params(SAMPLES[0], None, config, 'MAPPING', MAPPERENV)['OPTIONS'], ['INDEX'])
+    keydict["REF"] = REFERENCE
+    keydict["DECOY"] = DECOY
+    keydict["ENV"] = MAPPERENV
+    unik = get_dict_hash(keydict)
+    UIDX = expand("{refd}/INDICES/{mape}/{unikey}.idx", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+    INDICES = INDEX.split(',') if INDEX else list(UIDX)
+    INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+    IDX2 = MAPCONF.get('INDEX2', MAPCONF[MAPPERENV].get('INDEX2'))
+    if IDX2:
+        INDEX2 = IDX2
+    if not INDEX2 and ('segemehl' in MAPPERENV and 'bisulfite' in MAPPERENV):
+        if len(INDICES) > 1:
+            UIDX2 = expand("{refd}/INDICES/{mape}/{unikey}.idx2", refd=REFDIR, mape=MAPPERENV, unikey=unik)
+            INDEX2 = str(os.path.abspath(INDICES[1])) if str(os.path.abspath(INDICES[1])) not in UIDX2 else str(os.path.abspath(INDICES[1]))+'_idx2'
+        else:
+            INDEX2 = str.join(os.sep, [REFDIR, 'INDICES', MAPPERENV])+'.idx2'
+    else:
+        INDEX2 = None
+        UIDX2 = None
+    MAPOPT = MAPCONF.get(MAPPERENV).get('OPTIONS')
+    PRE = MAPCONF.get('PREFIX', MAPCONF.get('EXTENSION', MAPOPT.get('PREFIX', MAPOPT.get('EXTENSION'))))
+    if PRE and PRE is not None:
+        PREFIX = PRE
+    if not PREFIX or PREFIX is None:
+        PREFIX = MAPPERENV
+# Peak Calling Variables
+if 'PEAKS' in config:
+    PEAKCONF = sub_dict(config['PEAKS'], SETUP)
+    PEAKBIN, PEAKENV = env_bin_from_config(config, 'PEAKS')
+    REF = PEAKCONF.get('REFERENCE', PEAKCONF[PEAKENV].get('REFERENCE', config['PEAKS'].get('REFERENCE')))
+    ANNOPEAK = PEAKCONF.get('ANNOTATION', PEAKCONF[PEAKENV].get('ANNOTATION', config['PEAKS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    if ANNOPEAK and ANNOPEAK != '':
+        ANNOTATION = ANNOPEAK
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+    if not IP:
+        IP = check_IP(SAMPLES, config)
+    log.info(logid+'Running Peak finding for '+IP+' protocol')
+# TRACKS/COUNTING Variables
+for x in ['TRACKS', 'COUNTING']:
+    if x in config:
+        XBIN, XENV = env_bin_from_config(config, x)
+        XCONF = sub_dict(config[x], SETUP)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get('REFERENCE', XCONF.get('REFERENCE', config[x].get('REFERENCE')))
+        XANNO = XCONF[XENV].get('ANNOTATION', XCONF.get('ANNOTATION', config[x].get('ANNOTATION')))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if XENV in ["salmon", "kallisto"]:
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe=XENV, unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# DE/DEU/DAS/DTU Variables
+for x in ['DE', 'DEU', 'DAS', 'DTU']:
+    if x in config:
+        XCONF = sub_dict(config[x], SETUP)
+        XBIN, XENV = env_bin_from_config(config, x)
+        XENV = XENV.split('_')[0]
+        log.debug(logid+'XCONFIG: '+str(SETUP)+'\t'+str(XCONF))
+        REF = XCONF[XENV].get("REFERENCE", XCONF.get("REFERENCE", config[x].get("REFERENCE")))
+        XANNO = XCONF[XENV].get("ANNOTATION", XCONF.get("ANNOTATION", config[x].get("ANNOTATION")))
+        XDECOY = XCONF[XENV].get("DECOY", XCONF.get("DECOY", config[x].get("DECOY")))
+        if XANNO and XANNO != '':
+            ANNOTATION = XANNO            
+        else:
+            ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+        if XDECOY and not dict_inst(XDECOY):
+            DECOY = XDECOY
+        elif dict_inst(DECOY) and DECOY.get(XENV):
+            DECOY = DECOY.get(XENV)
+        else:
+            DECOY = None
+        if REF:
+            REFERENCE = REF
+            REFDIR = str(os.path.dirname(REFERENCE))
+        if x == 'DTU':
+            IDX = XCONF.get('INDEX')
+            if IDX:
+                INDEX = IDX
+            if not INDEX:
+                INDEX = str.join(os.sep, [REFDIR, 'INDICES', XENV])+'.idx'
+                keydict = sub_dict(tool_params(SAMPLES[0], None, config, x, XENV)['OPTIONS'], ['INDEX'])
+                keydict["REF"] = REFERENCE
+                keydict["DECOY"] = DECOY
+                keydict["ENV"] = XENV
+                unik = get_dict_hash(keydict)
+                UIDX = expand("{refd}/INDICES/{xe}/{unikey}.idx", refd=REFDIR, xe="salmon", unikey=unik)
+            INDICES = INDEX.split(',') if INDEX else list(UIDX)
+            INDEX = str(os.path.abspath(INDICES[0])) if str(os.path.abspath(INDICES[0])) not in UIDX else str(os.path.abspath(INDICES[0]))+'_idx'
+# CIRCS Variables
+if 'CIRCS' in config:
+    CIRCCONF = sub_dict(config['CIRCS'], SETUP)
+    XBIN, XENV = env_bin_from_config(config, 'CIRCS')
+    log.debug(logid+'CIRCCONFIG: '+str(SETUP)+'\t'+str(CIRCCONF))
+    REF = CIRCCONF.get('REFERENCE', CIRCCONF[XENV].get('REFERENCE', config['CIRCS'].get('REFERENCE')))
+    XANNO = CIRCCONF.get('ANNOTATION', CIRCCONF[XENV].get('ANNOTATION', config['CIRCS'].get('ANNOTATION')))
+    if REF:
+        REFERENCE = REF
+        REFDIR = str(os.path.dirname(REFERENCE))
+    CANNO = CIRCCONF.get('ANNOTATION')
+    if CANNO and CANNO != '':
+        ANNOTATION = CANNO
+    else:
+        ANNOTATION = ANNO.get('GTF') if 'GTF' in ANNO and ANNO.get('GTF') != '' else ANNO.get('GFF')  # by default GTF format will be used
+combo = ''
+if ANNOTATION:
+    log.info(logid+f'Using COMBO: {combo}, REF: {REFERENCE}, ANNO: {ANNOTATION}, DECOY: {DECOY}, INDEX: {INDEX}, INDEX2: {INDEX2}, PREFIX: {PREFIX}, IP: {IP}, SETUP: {SETUP}, SEQUENCING: {SEQUENCING}, STRANDED: {stranded}, PAIRED: {paired}, RUNDEDUP: {rundedup}, PREDEDUP: {prededup}, SAMPLES: {SAMPLES}, BINS: {BINS}, MAXTHREAD: {MAXTHREAD}')
+####HEADER ENDS HERE####
+
+combo = 'fastqc-trimgalore-picard-star/starfusion'
+
+scombo = 'fastqc-trimgalore-picard-star'
+
+wildcard_constraints:
+    combo = combo,
+    scombo = scombo,
+    read = "R1|R2",
+    type = "sorted|sorted_unique" if not rundedup else "sorted|sorted_unique|sorted_dedup|sorted_unique_dedup"
+
+FBIN, FENV = env_bin_from_config(config, 'FUSIONS')
+if not 'star' in combo or not 'star' in scombo:
+        log.warning('STAR-Fusion needs STAR chimeric output, can only be used with STAR in mapping step')
+fusopts = tool_params(SAMPLES[0], None, config, "FUSIONS", FENV)['OPTIONS']
+fastqmode = str(fusopts.get('FASTQ', "")).lower() in ("1", "true", "yes")
+CTATLIB = fusopts.get('INDEX', "") or os.path.join(REFDIR, "CTAT", FENV)
+if not rundedup:
+    rule themall:
+        input:  expand("FUSIONS/{combo}/{file}_fusions", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    rule themall:
+        input:  expand("FUSIONS/{combo}/{file}_{type}", combo=combo, file=samplecond(SAMPLES, config), type=['sorted', 'sorted_dedup'])
+rule generate_ctat_lib:
+    input:  fa = REFERENCE,
+            anno = ANNOTATION
+    output: lib = directory(CTATLIB),
+            tmpfa = temp(expand("TMP/{fenv}/ctat_ref.fa", fenv=FENV)),
+            tmpanno = temp(expand("TMP/{fenv}/ctat_ref.gtf", fenv=FENV))
+    log:    expand("LOGS/{sets}/{fenv}.ctat.log", sets=SETS, fenv=FENV)
+    conda:  "/envs/"+FENV+".yaml"
+    container: "oras://docker.io/jfallmann/monsda:"+FENV+""
+    threads: MAXTHREAD
+    params: sf = FBIN,
+            bpara = lambda wildcards: tool_params(SAMPLES[0], None, config, "FUSIONS", FENV)['OPTIONS'].get('BUILD', "")
+    shell:  "if [[ -f \"{output.lib}/ref_annot.gtf\" ]]; then echo \"Found CTAT genome lib, continue with STAR-Fusion\" &> {log} && touch {output.tmpfa} {output.tmpanno}; else zcat {input.fa} > {output.tmpfa} && zcat {input.anno} > {output.tmpanno} && mkdir -p {output.lib} && prep_genome_lib.pl --genome_fa {output.tmpfa} --gtf {output.tmpanno} --output_dir {output.lib} --CPU {threads} {params.bpara} &> {log}; fi"
+if not fastqmode:
+    rule starfusion:
+        input:  junction = expand("MAPPED/{scombo}/{{file}}.Chimeric.out.junction", scombo=scombo),
+                lib = rules.generate_ctat_lib.output.lib
+        output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+        log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+        conda:  "/envs/"+FENV+".yaml"
+        container: "oras://docker.io/jfallmann/monsda:"+FENV+""
+        threads: MAXTHREAD
+        params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                sf = FBIN
+        shell:  "set +o pipefail; if [[ -s \"{input.junction}\" ]]; then {params.sf} --genome_lib_dir {input.lib} -J {input.junction} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; else mkdir -p {output.outdir}; echo \"File {input.junction} empty, no chimeric STAR output found\" >> {log}; fi; touch {output.fusions}"
+else:
+    if paired == 'paired':
+        rule starfusion:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    lib = rules.generate_ctat_lib.output.lib
+            output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                    outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+            log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+            conda:  "/envs/"+FENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+FENV+""
+            threads: MAXTHREAD
+            params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                    sf = FBIN
+            shell:  "{params.sf} --genome_lib_dir {input.lib} --left_fq {input.r1} --right_fq {input.r2} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; touch {output.fusions}"
+    else:
+        rule starfusion:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    lib = rules.generate_ctat_lib.output.lib
+            output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                    outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+            log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+            conda:  "/envs/"+FENV+".yaml"
+            container: "oras://docker.io/jfallmann/monsda:"+FENV+""
+            threads: MAXTHREAD
+            params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                    sf = FBIN
+            shell:  "{params.sf} --genome_lib_dir {input.lib} --left_fq {input.r1} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; touch {output.fusions}"
+
+
+## Queue-agnostic fallback resources for all rules.
+## These defaults apply when a rule has no explicit resource set.
+## They keep local/non-Slurm runs from lacking baseline resource values.
+_fallback_rule_resources = {
+	"mem_mb": 20000,
+	"runtime": 480,
+	"nodes": 1,
+}
+_user_rule_defaults = config.get("DEFAULT_RULE_RESOURCES", {})
+if isinstance(_user_rule_defaults, dict):
+	_fallback_rule_resources.update(_user_rule_defaults)
+def _has_resource(rule_obj, resource_name):
+	try:
+		return resource_name in rule_obj.resources.keys()
+	except Exception:
+		return hasattr(rule_obj.resources, resource_name)
+def _set_resource(rule_obj, resource_name, value):
+	try:
+		rule_obj.resources[resource_name] = value
+	except Exception:
+		setattr(rule_obj.resources, resource_name, value)
+for _rule in workflow.rules:
+	for _res_name, _res_value in _fallback_rule_resources.items():
+		if not _has_resource(_rule, _res_name):
+			_set_resource(_rule, _res_name, _res_value)
+onsuccess:
+    print("Workflow finished, no error")
+onerror:
+	print("ERROR: "+str({log}))
+
+
diff --git a/tests/test_Workflows.py b/tests/test_Workflows.py
index 45f1e1b0..e3020b90 100644
--- a/tests/test_Workflows.py
+++ b/tests/test_Workflows.py
@@ -250,6 +250,59 @@ def test_make_post_dtu_file_set_stable(engine, workdir):
     _check_golden(f"config_DTU/{engine}/_filelist.txt", "\n".join(names) + "\n")
 
 
+# --------------------------------------------------------------------------- #
+# Golden-file snapshot for the FUSIONS postprocessing step (STAR-Fusion).
+# --------------------------------------------------------------------------- #
+
+
+def _generate_fusions(engine, workdir):
+    cfg = _load("config_DTU_test.json")
+    conditions = mp.get_conditions(cfg)
+    _pre, sub, _post = mw.get_processes(cfg)
+    combos = mw.get_combo(sub, cfg, conditions)
+    samples = mp.get_samples_postprocess(cfg, "FUSIONS")
+    if engine == "smk":
+        subdir = "SubFusionSnakes"
+        mp.create_skeleton(subdir, None)
+        mw.make_post(
+            "FUSIONS", cfg, samples, conditions, subdir, "INFO", combinations=combos
+        )
+    else:
+        subdir = "SubFusionFlows"
+        mp.create_skeleton(subdir, None)
+        mw.nf_make_post(
+            "FUSIONS", cfg, samples, conditions, subdir, "INFO", combinations=combos
+        )
+    return subdir
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_post_fusions_golden(engine, workdir):
+    subdir = _generate_fusions(engine, workdir)
+    produced = sorted(
+        os.path.relpath(p, workdir) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    assert produced, "generator produced no files"
+
+    for rel in produced:
+        raw = Path(workdir / rel).read_text()
+        if rel.endswith(".json"):
+            data = _sort_string_lists(json.loads(raw))
+            raw = json.dumps(data, indent=2, sort_keys=True)
+        content = _normalize(raw, workdir)
+        _check_golden(f"config_FUSIONS/{engine}/{os.path.basename(rel)}", content)
+
+
+@pytest.mark.parametrize("engine", ["smk", "nf"])
+def test_make_post_fusions_file_set_stable(engine, workdir):
+    subdir = _generate_fusions(engine, workdir)
+    names = sorted(
+        os.path.basename(p) for p in glob.glob(os.path.join(subdir, "*"))
+    )
+    _check_golden(f"config_FUSIONS/{engine}/_filelist.txt", "\n".join(names) + "\n")
+
+
+
 # --------------------------------------------------------------------------- #
 # Cross-engine consistency: the Snakemake and Nextflow generators must expose
 # the same tool matrix, env/bin selection and per-step OPTIONS for one config.
diff --git a/workflows/starfusion.nf b/workflows/starfusion.nf
new file mode 100644
index 00000000..9b9e93e1
--- /dev/null
+++ b/workflows/starfusion.nf
@@ -0,0 +1,64 @@
+FUSENV = get_always('FUSIONSENV')
+FUSBIN = get_always('FUSIONSBIN')
+FUSREF = get_always('FUSIONSREF')
+FUSREFDIR = "${workflow.workDir}/../"+get_always('FUSIONSREFDIR')
+FUSANNO = get_always('FUSIONSANNO')
+FUSLIB = get_always('FUSIONSLIB')
+
+FUSPARAMS = get_always('starfusion_params_FUSION') ?: ''
+FUSBUILD = get_always('starfusion_params_BUILD') ?: ''
+
+//FUSIONS PROCESSES
+
+process starfusion{
+    conda "$FUSENV"+".yaml"
+    container "oras://jfallmann/monsda:"+"$FUSENV"
+    cpus THREADS
+	cache 'lenient'
+    //validExitStatus 0,1
+
+    publishDir "${workflow.workDir}/../" , mode: 'link',
+    saveAs: {filename ->
+        if (filename.indexOf("_fusions") > 0)      "FUSIONS/${SCOMBO}/${CONDITION}/${file(filename).getSimpleName()}"        
+        else if (filename.indexOf(".log") > 0)        "LOGS/${SCOMBO}/${CONDITION}/${file(filename).getSimpleName()}"
+    }
+
+    input:
+    path fls
+
+    output:
+    path "*_fusions", emit: fusions
+    path "log", emit: log
+
+    script:
+    ref = fls[0]
+    anno = fls[1]
+    junction = fls[2]        
+    fn = file(junction).getSimpleName()
+    of = fn+"_fusions"
+    ol = fn+".log"
+    
+    """
+    if [[ -f \"$FUSLIB/ref_annot.gtf\" ]]; then CTAT=\"$FUSLIB\"; else CTAT=CTAT; mkdir -p \$CTAT && zcat ${ref} > ctat_ref.fa && zcat ${anno} > ctat_ref.gtf && prep_genome_lib.pl --genome_fa ctat_ref.fa --gtf ctat_ref.gtf --output_dir \$CTAT --CPU ${task.cpus} $FUSBUILD &> log; fi; if [[ -s \"${junction}\" ]]; then $FUSBIN --genome_lib_dir \$CTAT -J ${junction} --output_dir TMP --CPU ${task.cpus} $FUSPARAMS &>> log && cp TMP/star-fusion.fusion_predictions.tsv ${of} 2>> log; else echo \"File ${junction} empty, no chimeric STAR output found\" >> log; fi; touch ${of}
+    """
+}
+
+workflow FUSIONS{ 
+    take: collection
+
+    main:
+
+    MAPPEDSAMPLES = LONGSAMPLES.collect{
+        element -> return "${workflow.workDir}/../MAPPED/${COMBO}/"+element+"*.Chimeric.out.junction"
+    }
+
+    mapsamples_ch = Channel.fromPath(MAPPEDSAMPLES.sort())  
+    annofile = Channel.fromPath(FUSANNO)
+    genomefile = Channel.fromPath(FUSREF)
+
+    starfusion(genomefile.combine(annofile.combine(mapsamples_ch.collate(1))))
+
+    emit:
+    fusions = starfusion.out.fusions
+    logs = starfusion.out.log
+}
diff --git a/workflows/starfusion.smk b/workflows/starfusion.smk
new file mode 100644
index 00000000..9f3e776d
--- /dev/null
+++ b/workflows/starfusion.smk
@@ -0,0 +1,71 @@
+FBIN, FENV = env_bin_from_config(config, 'FUSIONS')
+
+if not 'star' in combo or not 'star' in scombo:
+        log.warning('STAR-Fusion needs STAR chimeric output, can only be used with STAR in mapping step')
+
+fusopts = tool_params(SAMPLES[0], None, config, "FUSIONS", FENV)['OPTIONS']
+fastqmode = str(fusopts.get('FASTQ', "")).lower() in ("1", "true", "yes")
+CTATLIB = fusopts.get('INDEX', "") or os.path.join(REFDIR, "CTAT", FENV)
+
+if not rundedup:
+    rule themall:
+        input:  expand("FUSIONS/{combo}/{file}_fusions", combo=combo, file=samplecond(SAMPLES, config))
+else:
+    rule themall:
+        input:  expand("FUSIONS/{combo}/{file}_{type}", combo=combo, file=samplecond(SAMPLES, config), type=['sorted', 'sorted_dedup'])
+
+rule generate_ctat_lib:
+    input:  fa = REFERENCE,
+            anno = ANNOTATION
+    output: lib = directory(CTATLIB),
+            tmpfa = temp(expand("TMP/{fenv}/ctat_ref.fa", fenv=FENV)),
+            tmpanno = temp(expand("TMP/{fenv}/ctat_ref.gtf", fenv=FENV))
+    log:    expand("LOGS/{sets}/{fenv}.ctat.log", sets=SETS, fenv=FENV)
+    conda:  ""+FENV+".yaml"
+    container: "oras://jfallmann/monsda:"+FENV+""
+    threads: MAXTHREAD
+    params: sf = FBIN,
+            bpara = lambda wildcards: tool_params(SAMPLES[0], None, config, "FUSIONS", FENV)['OPTIONS'].get('BUILD', "")
+    shell:  "if [[ -f \"{output.lib}/ref_annot.gtf\" ]]; then echo \"Found CTAT genome lib, continue with STAR-Fusion\" &> {log} && touch {output.tmpfa} {output.tmpanno}; else zcat {input.fa} > {output.tmpfa} && zcat {input.anno} > {output.tmpanno} && mkdir -p {output.lib} && prep_genome_lib.pl --genome_fa {output.tmpfa} --gtf {output.tmpanno} --output_dir {output.lib} --CPU {threads} {params.bpara} &> {log}; fi"
+
+if not fastqmode:
+    rule starfusion:
+        input:  junction = expand("MAPPED/{scombo}/{{file}}.Chimeric.out.junction", scombo=scombo),
+                lib = rules.generate_ctat_lib.output.lib
+        output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+        log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+        conda:  ""+FENV+".yaml"
+        container: "oras://jfallmann/monsda:"+FENV+""
+        threads: MAXTHREAD
+        params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                sf = FBIN
+        shell:  "set +o pipefail; if [[ -s \"{input.junction}\" ]]; then {params.sf} --genome_lib_dir {input.lib} -J {input.junction} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; else mkdir -p {output.outdir}; echo \"File {input.junction} empty, no chimeric STAR output found\" >> {log}; fi; touch {output.fusions}"
+else:
+    if paired == 'paired':
+        rule starfusion:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_R1_trimmed.fastq.gz",
+                    r2 = "TRIMMED_FASTQ/{combo}/{file}_R2_trimmed.fastq.gz",
+                    lib = rules.generate_ctat_lib.output.lib
+            output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                    outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+            log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+            conda:  ""+FENV+".yaml"
+            container: "oras://jfallmann/monsda:"+FENV+""
+            threads: MAXTHREAD
+            params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                    sf = FBIN
+            shell:  "{params.sf} --genome_lib_dir {input.lib} --left_fq {input.r1} --right_fq {input.r2} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; touch {output.fusions}"
+    else:
+        rule starfusion:
+            input:  r1 = "TRIMMED_FASTQ/{combo}/{file}_trimmed.fastq.gz",
+                    lib = rules.generate_ctat_lib.output.lib
+            output: fusions = "FUSIONS/{combo}/{file}_fusions",
+                    outdir = temp(directory("FUSIONS/{combo}/{file}_tmp"))
+            log:    "LOGS/FUSIONS/{combo}/{file}_starfusion.log"
+            conda:  ""+FENV+".yaml"
+            container: "oras://jfallmann/monsda:"+FENV+""
+            threads: MAXTHREAD
+            params: fpara = lambda wildcards: tool_params(wildcards.file, None, config, "FUSIONS", FENV)['OPTIONS'].get('FUSION', ""),
+                    sf = FBIN
+            shell:  "{params.sf} --genome_lib_dir {input.lib} --left_fq {input.r1} --output_dir {output.outdir} --CPU {threads} {params.fpara} &> {log} && cp {output.outdir}/star-fusion.fusion_predictions.tsv {output.fusions} 2>> {log}; touch {output.fusions}"

From 7cd3db7ab043d59adde7ceea0cd4b0fbb2b6ef40 Mon Sep 17 00:00:00 2001
From: jfallmann 
Date: Tue, 7 Jul 2026 15:34:45 +0200
Subject: [PATCH 12/80] profile fixes and cleanup

---
 profile_nextflow/nextflow.config           |   4 +-
 profile_snakemake/cluster_config.yaml      |  26 --
 profile_snakemake/config.v8+.yaml          |  10 +-
 profile_snakemake/config.yaml              |  12 -
 profile_snakemake/slurm-jobscript.sh       |   3 -
 profile_snakemake/slurm-status.py          |  73 -----
 profile_snakemake/slurm-submit-advanced.py | 292 -------------------
 profile_snakemake/slurm-submit.py          |  61 ----
 profile_snakemake/slurm_utils.py           | 317 ---------------------
 9 files changed, 11 insertions(+), 787 deletions(-)
 delete mode 100644 profile_snakemake/cluster_config.yaml
 delete mode 100644 profile_snakemake/config.yaml
 delete mode 100755 profile_snakemake/slurm-jobscript.sh
 delete mode 100755 profile_snakemake/slurm-status.py
 delete mode 100755 profile_snakemake/slurm-submit-advanced.py
 delete mode 100755 profile_snakemake/slurm-submit.py
 delete mode 100644 profile_snakemake/slurm_utils.py

diff --git a/profile_nextflow/nextflow.config b/profile_nextflow/nextflow.config
index bc36eb4b..61d9ba57 100644
--- a/profile_nextflow/nextflow.config
+++ b/profile_nextflow/nextflow.config
@@ -16,10 +16,10 @@ profiles {
 
         // Non-default overrides only – rules that need more than the global defaults.
         process {
-            withName: 'bwa_idx|bwa2_idx|bwameth_idx|hisat2_idx|minimap_idx|segemehl_idx|segemehl3_idx|star_idx' {
+            withName: 'bwa_idx|bwa2_idx|bwameth_idx|hisat2_idx|minimap_idx|segemehl_idx|segemehl3_idx|star_idx|generate_ctat_lib' {
                 memory = { 80.GB * (1 << ((task.attempt ?: 1) - 1)) }
             }
-            withName: 'bwa_mapping|bwa2_mapping|bwameth_mapping|hisat2_mapping|minimap_mapping|segemehl_mapping|segemehl3_mapping|star_mapping' {
+            withName: 'bwa_mapping|bwa2_mapping|bwameth_mapping|hisat2_mapping|minimap_mapping|segemehl_mapping|segemehl3_mapping|star_mapping|starfusion' {
                 memory = { 80.GB * (1 << ((task.attempt ?: 1) - 1)) }
             }
             withName: 'salmon_idx|kallisto_idx' {
diff --git a/profile_snakemake/cluster_config.yaml b/profile_snakemake/cluster_config.yaml
deleted file mode 100644
index f2f619f8..00000000
--- a/profile_snakemake/cluster_config.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-__default__:
-    account: ${USER} # your account name
-    partition: c # the partition to use
-    time: 1500 # default time (minutes)
-    nodes: 1
-    output: "/SLURMLOG/{rule}.{wildcards}.out"
-    error: "/SLURMLOG/{rule}.{wildcards}.err"
-    #ntasks: 1
-    #mem: 14GB # default memory
-
-generate_index:
-    mem: 200GB
-
-mapping:
-    mem: 200GB
-
-generate_ctat_lib:
-    mem: 200GB
-    time: 2880
-
-starfusion:
-    mem: 100GB
-    time: 1500
-
-multiqc:
-   mem: 30GB
diff --git a/profile_snakemake/config.v8+.yaml b/profile_snakemake/config.v8+.yaml
index a984a4d2..d9af2971 100755
--- a/profile_snakemake/config.v8+.yaml
+++ b/profile_snakemake/config.v8+.yaml
@@ -5,7 +5,9 @@ default-resources:
   slurm_partition: "${SLURM_PARTITION}" # the partition to use
   nodes: 1
   mem_mb: "20000 * (2 ** (attempt - 1))"  
-  runtime: "480 * (2 ** (attempt - 1))"   
+  runtime: "480 * (2 ** (attempt - 1))"  
+  partition: "('c_long' if min(480 * (2 ** (attempt - 1)), 960) > 2880 else 'c_medium' if min(480 * (2 ** (attempt - 1)), 960) > 1440 else 'c_short')"
+  qos: "('long' if min(480 * (2 ** (attempt - 1)), 960) > 2880 else 'medium' if min(480 * (2 ** (attempt - 1)), 960) > 1440 else 'short')" 
 set-resources:
   generate_index:
     mem_mb: "80000 * (2 ** (attempt - 1))"
@@ -41,6 +43,12 @@ set-resources:
   gridss_setupreference:
     mem_mb: "40000 * (2 ** (attempt - 1))"
     threads: 8
+  generate_ctat_lib:
+    mem_mb: "80000 * (2 ** (attempt - 1))"  
+    runtime: "24h"
+  starfusion:
+    mem_mb: "100000 * (2 ** (attempt - 1))"    
+    runtime: "24h"
 jobs: 1000
 keep-going: True
 rerun-incomplete: True
diff --git a/profile_snakemake/config.yaml b/profile_snakemake/config.yaml
deleted file mode 100644
index a4e4edb5..00000000
--- a/profile_snakemake/config.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-restart-times: 3
-jobscript: "slurm-jobscript.sh"
-cluster-generic-submit-cmd: "slurm-submit.py"
-#cluster-status: "slurm-status.py"
-max-jobs-per-second: 1
-max-status-checks-per-second: 3
-local-cores: 1
-latency-wait: 600
-#use-conda: True
-keep-going: True
-rerun-incomplete: True
-#printshellcmds: True
diff --git a/profile_snakemake/slurm-jobscript.sh b/profile_snakemake/slurm-jobscript.sh
deleted file mode 100755
index 391741ef..00000000
--- a/profile_snakemake/slurm-jobscript.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-# properties = {properties}
-{exec_job}
diff --git a/profile_snakemake/slurm-status.py b/profile_snakemake/slurm-status.py
deleted file mode 100755
index 688c2276..00000000
--- a/profile_snakemake/slurm-status.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python3
-import re
-import subprocess as sp
-import shlex
-import sys
-import time
-import logging
-
-logger = logging.getLogger("__name__")
-
-STATUS_ATTEMPTS = 20
-
-jobid = sys.argv[1]
-
-  # noqa: E999,E225
-cluster = "--cluster=cluster"
-  # noqa: E225
-
-for i in range(STATUS_ATTEMPTS):
-    try:
-        sacct_res = sp.check_output(shlex.split(f"sacct {cluster} -P -b -j {jobid} -n"))
-        res = {
-            x.split("|")[0]: x.split("|")[1]
-            for x in sacct_res.decode().strip().split("\n")
-        }
-        break
-    except sp.CalledProcessError as e:
-        logger.error("sacct process error")
-        logger.error(e)
-    except IndexError as e:
-        pass
-    # Try getting job with scontrol instead in case sacct is misconfigured
-    try:
-        sctrl_res = sp.check_output(
-            shlex.split(f"scontrol {cluster} -o show job {jobid}")
-        )
-        m = re.search(r"JobState=(\w+)", sctrl_res.decode())
-        res = {jobid: m.group(1)}
-        break
-    except sp.CalledProcessError as e:
-        logger.error("scontrol process error")
-        logger.error(e)
-        if i >= STATUS_ATTEMPTS - 1:
-            print("failed")
-            exit(0)
-        else:
-            time.sleep(1)
-
-status = res[jobid]
-
-if status == "BOOT_FAIL":
-    print("failed")
-elif status == "OUT_OF_MEMORY":
-    print("failed")
-elif status.startswith("CANCELLED"):
-    print("failed")
-elif status == "COMPLETED":
-    print("success")
-elif status == "DEADLINE":
-    print("failed")
-elif status == "FAILED":
-    print("failed")
-elif status == "NODE_FAIL":
-    print("failed")
-elif status == "PREEMPTED":
-    print("failed")
-elif status == "TIMEOUT":
-    print("failed")
-# Unclear whether SUSPENDED should be treated as running or failed
-elif status == "SUSPENDED":
-    print("failed")
-else:
-    print("running")
diff --git a/profile_snakemake/slurm-submit-advanced.py b/profile_snakemake/slurm-submit-advanced.py
deleted file mode 100755
index bfe9509f..00000000
--- a/profile_snakemake/slurm-submit-advanced.py
+++ /dev/null
@@ -1,292 +0,0 @@
-#!/usr/bin/env python3
-import sys
-import os
-import re
-import math
-import argparse
-import subprocess
-from snakemake.utils import read_job_properties
-
-
-##############################
-# Helper functions
-##############################
-def _get_default_partition():
-    """Retrieve default partition for cluster"""
-    if "":
-        return ""
-    cmd = "sinfo -O \"partition\""
-    res = subprocess.run(cmd, check=True, shell=True,
-                         stdout=subprocess.PIPE)
-    m = re.search("(?P\S+)\*", res.stdout.decode(), re.M)
-    partition = m.group("partition")
-    return partition
-
-
-def _get_cluster_configuration(partition):
-    """Retrieve cluster configuration for a partition."""
-    # Retrieve partition info; we tacitly assume we only get one response
-    cmd = " ".join(
-        ["sinfo -e -O \"partition,cpus,memory,time,size,maxcpuspernode\"",
-         "-h -p {}".format(partition)])
-    res = subprocess.run(cmd, check=True, shell=True, stdout=subprocess.PIPE)
-    m = re.search("(?P\S+)\s+(?P\d+)\s+(?P\S+)\s+((?P\d+)-)?(?P\d+):(?P\d+):(?P\d+)\s+(?P\S+)\s+(?P\S+)",
-                  res.stdout.decode())
-    if m is None:
-        m = re.search("(?P\S+)\s+(?P\d+)\s+(?P\S+)\s+(?P