Per-sample mod summary

This commit is contained in:
Sam Nicholls 2026-05-27 08:11:02 +00:00
parent 74cbe196b2
commit 4bc3cf727f
10 changed files with 556 additions and 4 deletions

View File

@ -227,8 +227,8 @@ analysis, optional [`SQANTI3`](https://github.com/conesalab/SQANTI3) QC, and opt
When aligned BAMs contain modified base tags (`MM` and `ML`), the workflow also
runs `modkit` on each sample alignment. It first checks which modified base
codes are present in the BAM, then runs `modkit pileup` to produce a per-sample
bedMethyl file and one bigWig track per requested or inferred modification
under `samples/<alias>/mods/`.
bedMethyl file, a simple per-sample summary table, and one bigWig track per
requested or inferred modification under `samples/<alias>/mods/`.
If `--mod_codes` is set, those codes are passed directly to `modkit pileup`.
If it is omitted, the workflow infers the available `primary_base:mod_code`
@ -378,6 +378,7 @@ Output files may be aggregated including information for all samples or provided
| Aligned BAM index | samples/{{ alias }}/alignment/reads.bam.bai | Index for the aligned BAM. | per-sample |
| Alignment summary | samples/{{ alias }}/alignment/bamstats.flagstat.tsv | bamstats flagstat summary for the aligned BAM. | per-sample |
| Modified base pileup | samples/{{ alias }}/mods/{{ alias }}.mods.bedmethyl.gz | Per-sample modkit bedMethyl pileup generated from the aligned BAM when MM and ML tags are present. | per-sample |
| Modified base summary | samples/{{ alias }}/mods/{{ alias }}.mods.summary.tsv | Per-sample global modification-percent summary aggregated from the modkit bedMethyl pileup, with one row per modification code. | per-sample |
| Modified base bigWig | samples/{{ alias }}/mods/{{ alias }}.mods.*.bw | Per-sample modkit bigWig tracks generated from the aligned BAM, with one file per requested or inferred modification code. | per-sample |
| Reference and annotation preparation summary | cohort/reference/annotation_reference_summary.json | Summary of reference and annotation preparation, including seqname overlap, build/provider hints, and excluded unstranded annotation counts. | aggregated |
| Excluded unstranded annotation records | cohort/reference/unstranded_annotation.gtf | Full set of annotation records excluded because their strand was not '+' or '-'. Present only when unstranded records are found. | aggregated |

View File

@ -1,5 +1,6 @@
"""Create workflow report for wf-transcriptomes."""
from html import escape
import json
import math
import os
@ -23,6 +24,8 @@ from .hierarchical_clustering import hierarchical, clustering_info # noqa: ABS
from .util import get_named_logger, wf_parser # noqa: ABS101
from .volcano import volcano # noqa: ABS101
logger = get_named_logger("Report")
# Suppress asyncio deprecation warning triggered by dominate on Python 3.10+.
# dominate calls asyncio.get_event_loop() outside a running async context.
@ -203,6 +206,168 @@ def _sqanti_table(sqanti_dir):
return sqanti_df.sort_values(["_is_cohort", "Sample"]).drop(columns="_is_cohort")
def _sample_mod_summaries(summary_dir):
"""Return a combined modified base summary table across all samples."""
if not summary_dir:
return None
summaries_path = Path(summary_dir)
if not summaries_path.exists() or not summaries_path.is_dir():
return None
summaries = []
for summary_file in sorted(summaries_path.glob("*.mods.summary.tsv")):
if not summary_file.is_file():
continue
summary = _read_table(summary_file)
if summary is None or summary.empty:
continue
if "sample" not in summary.columns:
raise ValueError(
f"Modified base summary is missing required 'sample' column: "
f"{summary_file}"
)
sample_names = summary["sample"].unique()
if len(sample_names) != 1:
raise ValueError(
f"Modified base summary must contain exactly one sample name: "
f"{summary_file}"
)
summary["mod"] = summary["mod_label"].where(
summary["mod_label"] != "",
summary["full_mod_code"],
)
summary = summary[
[
"sample",
"mod",
"full_mod_code",
"modification_percent",
"modified_calls",
"valid_coverage",
]
]
summaries.append(summary)
if not summaries:
return None
summary = pd.concat(summaries, ignore_index=True)
return summary.sort_values(["mod", "sample"]).reset_index(drop=True)
def _render_mod_summary_matrix(summary):
"""Render modified base summaries as a cross-sample comparison matrix."""
if summary.duplicated(["sample", "mod"]).any():
raise ValueError(
"Modified base summary contains duplicate sample/mod combinations."
)
samples = sorted(summary["sample"].astype(str).unique())
mods = sorted(summary["mod"].astype(str).unique())
lookup = {
(str(row.sample), str(row.mod)): row
for row in summary.itertuples(index=False)
}
header_html = "".join(
f"<th>{escape(mod)}</th>" for mod in mods
)
body_rows = []
for sample in samples:
cells = []
for mod in mods:
row = lookup.get((sample, mod))
if row is None:
cells.append(
"<td class='mod-summary-cell mod-summary-empty'>"
"<div class='mod-summary-empty-mark'>&mdash;</div>"
"</td>"
)
continue
cells.append(
"<td class='mod-summary-cell'>"
f"<div class='mod-summary-percent'>"
f"{_format_mod_summary_percent(row.modification_percent)}%"
f"</div>"
f"<div class='mod-summary-meta'>"
f"{_format_mod_summary_count(row.modified_calls)} modified"
f"</div>"
f"<div class='mod-summary-meta'>"
f"{_format_mod_summary_count(row.valid_coverage)} valid"
f"</div>"
"</td>"
)
body_rows.append(
"<tr>"
f"<th class='mod-summary-sample'>{escape(sample)}</th>"
+ "".join(cells)
+ "</tr>"
)
return (
"<div class='mod-summary-matrix-wrap'>"
"<table class='mod-summary-matrix'>"
"<thead><tr><th>Sample</th>"
f"{header_html}</tr></thead>"
f"<tbody>{''.join(body_rows)}</tbody>"
"</table>"
"</div>"
)
def _format_mod_summary_percent(value):
"""Format a modified base percentage for display."""
return f"{float(value):.2f}"
def _format_mod_summary_count(value):
"""Format a modified base count for display."""
return format(int(round(float(value))), ",")
def _mod_summary_matrix_style():
"""Return CSS for the modified base summary comparison matrix."""
return """
.mod-summary-matrix-wrap { overflow-x: auto; margin-bottom: 0.75rem; }
.mod-summary-matrix { width: 100%; border-collapse: collapse; }
.mod-summary-matrix th,
.mod-summary-matrix td {
border-bottom: 1px solid #e5e7eb;
padding: 0.75rem 0.875rem;
vertical-align: top;
text-align: left;
}
.mod-summary-matrix thead th {
font-weight: 600;
white-space: nowrap;
}
.mod-summary-sample {
white-space: nowrap;
font-weight: 600;
}
.mod-summary-percent {
font-size: 1.05rem;
font-weight: 700;
line-height: 1.2;
}
.mod-summary-meta {
margin-top: 0.15rem;
font-size: 0.8rem;
color: #6b7280;
line-height: 1.25;
white-space: nowrap;
}
.mod-summary-empty {
color: #9ca3af;
}
.mod-summary-empty-mark {
font-size: 1rem;
line-height: 1.2;
}
"""
def _contrast_results(de_dir, filename, n=None):
"""Return a dict of per-contrast result DataFrames read from filename."""
tables = {}
@ -490,6 +655,23 @@ def main(args):
.rename(columns={"index": "Field"}),
use_index=False,
)
mod_summaries = _sample_mod_summaries(args.mod_summary_dir)
if mod_summaries is not None and not mod_summaries.empty:
with report.add_section("Modified base summaries", "Modifications"):
dom_style(raw(_mod_summary_matrix_style()))
raw(_render_mod_summary_matrix(mod_summaries))
small(raw(
"<b>Valid coverage</b> Sum of the valid residues: all the "
"modified, canonical and other mod (where the modification "
"is different from the listed base) bedMethyl columns "
"counts for this combination of sample and mod. "
"<b>Modified calls</b> Number of calls passing filters "
"that were classified as a residue with a specified base "
"modification. "
"<b>Modification percent</b> (Modified calls / Valid "
"coverage) * 100"
))
annotation_reference_summary = _load_annotation_reference_summary(args.ref_summary)
if annotation_reference_summary:
@ -1233,6 +1415,11 @@ def argparser():
required=True,
help="Per-sample output directory.",
)
parser.add_argument(
"--mod_summary_dir",
default=None,
help="Modified base summary directory.",
)
parser.add_argument(
"--sqanti_dir",
required=True,

View File

@ -0,0 +1,145 @@
#!/usr/bin/env python
"""Summarise per-sample modkit bedMethyl outputs."""
from collections import defaultdict
import csv
import gzip
from .util import wf_parser # noqa: ABS101
SUMMARY_FIELDS = (
"sample",
"full_mod_code",
"mod_code",
"mod_label",
"valid_coverage",
"modified_calls",
"canonical_calls",
"other_calls",
"delete_calls",
"fail_calls",
"diff_calls",
"nocall_calls",
"modification_percent",
)
def load_mod_code_labels(label_tsv):
"""Load friendly labels keyed by full mod code."""
labels = {}
with open(label_tsv, newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle, delimiter="\t")
for row in reader:
mod_code = row.get("mod_code", "").strip()
label = row.get("label", "").strip()
if mod_code and label:
labels[mod_code] = label
return labels
def open_text_maybe_gzip(path):
"""Open plain text or gzipped text files transparently."""
if str(path).endswith(".gz"):
return gzip.open(path, "rt", encoding="utf-8")
return open(path, encoding="utf-8")
def build_full_code_lookup(mod_codes):
"""Map stripped mod codes back to their full primary_base:mod_code form."""
lookup = {}
for full_code in mod_codes.split(","):
full_code = full_code.strip()
if not full_code:
continue
if ":" in full_code:
_, mod_code = full_code.split(":", 1)
else:
mod_code = full_code
lookup[mod_code] = full_code
return lookup
def summarise_bedmethyl(bedmethyl, sample, mod_codes, labels):
"""Aggregate a bedMethyl file into one row per modification code."""
full_code_lookup = build_full_code_lookup(mod_codes)
counts = defaultdict(
lambda: {
"valid_coverage": 0,
"modified_calls": 0,
"canonical_calls": 0,
"other_calls": 0,
"delete_calls": 0,
"fail_calls": 0,
"diff_calls": 0,
"nocall_calls": 0,
}
)
# TODO would be nice to tidy this up as an ezcharts component
with open_text_maybe_gzip(bedmethyl) as handle:
for line in handle:
if not line.strip() or line.startswith("#"):
continue
fields = line.rstrip("\n").split("\t")
mod_code = fields[3]
stats = counts[mod_code]
stats["valid_coverage"] += int(fields[9])
stats["modified_calls"] += int(fields[11])
stats["canonical_calls"] += int(fields[12])
stats["other_calls"] += int(fields[13])
stats["delete_calls"] += int(fields[14])
stats["fail_calls"] += int(fields[15])
stats["diff_calls"] += int(fields[16])
stats["nocall_calls"] += int(fields[17])
rows = []
for mod_code, stats in sorted(counts.items()):
full_mod_code = full_code_lookup.get(mod_code, mod_code)
valid_coverage = stats["valid_coverage"]
modification_percent = (
(stats["modified_calls"] / valid_coverage) * 100
if valid_coverage else 0.0
)
rows.append(
{
"sample": sample,
"full_mod_code": full_mod_code,
"mod_code": mod_code,
"mod_label": labels.get(full_mod_code, mod_code),
**stats,
"modification_percent": f"{modification_percent:.2f}",
}
)
return rows
def write_summary(rows, output):
"""Write summary rows to a TSV file."""
with open(output, "w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=SUMMARY_FIELDS, delimiter="\t")
writer.writeheader()
writer.writerows(rows)
def main(args):
"""Run the entry point."""
labels = load_mod_code_labels(args.mod_code_labels)
rows = summarise_bedmethyl(
args.bedmethyl,
args.sample,
args.mod_codes,
labels,
)
write_summary(rows, args.output)
def argparser():
"""Argument parser for entrypoint."""
parser = wf_parser("summarise_modkit_bedmethyl")
parser.add_argument("bedmethyl", help="Input modkit bedMethyl file")
parser.add_argument("sample", help="Sample alias")
parser.add_argument("mod_codes", help="Comma-separated full modification codes")
parser.add_argument("mod_code_labels", help="TSV mapping full mod codes to labels")
parser.add_argument("output", help="Output TSV")
return parser

View File

@ -106,6 +106,8 @@ def _build_report_args(tmp_path, de_qc=None):
samples = tmp_path / "samples"
samples.mkdir()
mod_summaries = tmp_path / "mod_summaries"
mod_summaries.mkdir()
sqanti = tmp_path / "sqanti"
sqanti.mkdir()
alignment_stats = tmp_path / "alignment_stats"
@ -149,6 +151,8 @@ def _build_report_args(tmp_path, de_qc=None):
str(reference / "annotation_reference_summary.json"),
"--samples_dir",
str(samples),
"--mod_summary_dir",
str(mod_summaries),
"--sqanti_dir",
str(sqanti),
"--versions",
@ -213,6 +217,10 @@ def test_report_main_accepts_optional_file_sentinels(monkeypatch, tmp_path):
samples.mkdir()
(samples / "OPTIONAL_FILE").touch()
mod_summaries = tmp_path / "mod_summaries"
mod_summaries.mkdir()
(mod_summaries / "OPTIONAL_FILE").touch()
sqanti = tmp_path / "sqanti"
sqanti.mkdir()
(sqanti / "OPTIONAL_FILE").touch()
@ -235,6 +243,8 @@ def test_report_main_accepts_optional_file_sentinels(monkeypatch, tmp_path):
str(cohort / "reference" / "annotation_reference_summary.json"),
"--samples_dir",
str(samples),
"--mod_summary_dir",
str(mod_summaries),
"--sqanti_dir",
str(sqanti),
"--versions",
@ -254,6 +264,64 @@ def test_report_main_accepts_optional_file_sentinels(monkeypatch, tmp_path):
assert any("GRCh38" in table.to_string() for table in tables)
def test_report_main_renders_modified_base_summary_tables(monkeypatch, tmp_path):
"""Per-sample modified base summaries should render as report tables."""
tables = []
raw_calls = []
monkeypatch.setattr(report.labs, "LabsReport", _FakeReport)
monkeypatch.setattr(report, "Tabs", _FakeTabs)
monkeypatch.setattr(report, "p", lambda *args, **kwargs: None)
monkeypatch.setattr(report, "pre", lambda *args, **kwargs: None)
monkeypatch.setattr(report, "raw", lambda value: raw_calls.append(value) or value)
monkeypatch.setattr(report, "_create_warning_banner", lambda *args, **kwargs: None)
monkeypatch.setattr(report.fastcat, "SeqSummary", lambda *args, **kwargs: None)
monkeypatch.setattr(
report.DataTable,
"from_pandas",
staticmethod(lambda table, *args, **kwargs: tables.append(table.copy())),
)
args, out_report = _build_report_args(tmp_path)
mod_summaries = tmp_path / "mod_summaries"
_write(
mod_summaries / "not_the_sample_name.mods.summary.tsv",
(
"sample\tfull_mod_code\tmod_code\tmod_label\tvalid_coverage\t"
"modified_calls\tcanonical_calls\tother_calls\tdelete_calls\t"
"fail_calls\tdiff_calls\tnocall_calls\tmodification_percent\n"
"sampleA\tA:a\ta\tm6A\t12\t6\t6\t0\t0\t3\t0\t0\t50.00\n"
),
)
_write(
mod_summaries / "sampleB.mods.summary.tsv",
(
"sample\tfull_mod_code\tmod_code\tmod_label\tvalid_coverage\t"
"modified_calls\tcanonical_calls\tother_calls\tdelete_calls\t"
"fail_calls\tdiff_calls\tnocall_calls\tmodification_percent\n"
"sampleB\tC:m\tm\tm5C\t20\t5\t15\t0\t0\t0\t0\t0\t25.00\n"
),
)
report.main(args)
assert out_report.exists()
matrix_html = next(
value for value in raw_calls
if "<table class='mod-summary-matrix'>" in value
)
assert "sampleA" in matrix_html
assert "sampleB" in matrix_html
assert "m6A" in matrix_html
assert "m5C" in matrix_html
assert "50.00%" in matrix_html
assert "25.00%" in matrix_html
assert "6 modified" in matrix_html
assert "12 valid" in matrix_html
assert "5 modified" in matrix_html
assert "20 valid" in matrix_html
def test_report_main_handles_degenerate_bambu_qc_and_read_summary(
monkeypatch,
tmp_path,

View File

@ -0,0 +1,102 @@
"""Tests for modkit bedMethyl summarisation."""
import gzip
from workflow_glue import summarise_modkit_bedmethyl
def _write(path, text):
path.write_text(text, encoding="utf-8")
return path
def test_summarise_bedmethyl_aggregates_per_mod_code(tmp_path):
"""Rows should be summed into one row per modification code."""
label_tsv = _write(
tmp_path / "mod_code_labels.tsv",
"mod_code\tlabel\nA:a\tm6A\nT:17802\tpseU\n",
)
bedmethyl = _write(
tmp_path / "sample.mods.bedmethyl",
(
"chr1\t10\t11\ta\t1\t+\t10\t11\t255,0,0\t5\t40.00\t2\t3\t0\t0\t1\t0\t0\n"
"chr1\t11\t12\ta\t1\t+\t11\t12\t255,0,0\t7\t57.14\t4\t3\t0\t0\t2\t0\t0\n"
"chr1\t20\t21\t17802\t1\t+\t20\t21\t255,0,0\t4\t25.00\t1\t3\t0\t0\t0\t0\t"
"0\n"
),
)
rows = summarise_modkit_bedmethyl.summarise_bedmethyl(
bedmethyl,
"sample1",
"A:a,T:17802",
summarise_modkit_bedmethyl.load_mod_code_labels(label_tsv),
)
assert rows == [
{
"sample": "sample1",
"full_mod_code": "T:17802",
"mod_code": "17802",
"mod_label": "pseU",
"valid_coverage": 4,
"modified_calls": 1,
"canonical_calls": 3,
"other_calls": 0,
"delete_calls": 0,
"fail_calls": 0,
"diff_calls": 0,
"nocall_calls": 0,
"modification_percent": "25.00",
},
{
"sample": "sample1",
"full_mod_code": "A:a",
"mod_code": "a",
"mod_label": "m6A",
"valid_coverage": 12,
"modified_calls": 6,
"canonical_calls": 6,
"other_calls": 0,
"delete_calls": 0,
"fail_calls": 3,
"diff_calls": 0,
"nocall_calls": 0,
"modification_percent": "50.00",
},
]
def test_summarise_bedmethyl_reads_gzipped_input(tmp_path):
"""The helper should aggregate multiple rows from gzipped bedMethyl input."""
bedmethyl = tmp_path / "sample.mods.bedmethyl.gz"
with gzip.open(bedmethyl, "wt", encoding="utf-8") as handle:
handle.write(
"chr1\t10\t11\ta\t1\t+\t10\t11\t255,0,0\t5\t40.00\t2\t3\t0\t0\t1\t0\t0\n"
"chr1\t11\t12\ta\t1\t+\t11\t12\t255,0,0\t7\t57.14\t4\t3\t0\t0\t2\t0\t0\n"
)
rows = summarise_modkit_bedmethyl.summarise_bedmethyl(
bedmethyl,
"sample1",
"A:a",
{"A:a": "m6A"},
)
assert rows == [
{
"sample": "sample1",
"full_mod_code": "A:a",
"mod_code": "a",
"mod_label": "m6A",
"valid_coverage": 12,
"modified_calls": 6,
"canonical_calls": 6,
"other_calls": 0,
"delete_calls": 0,
"fail_calls": 3,
"diff_calls": 0,
"nocall_calls": 0,
"modification_percent": "50.00",
}
]

View File

@ -72,8 +72,8 @@ analysis, optional [`SQANTI3`](https://github.com/conesalab/SQANTI3) QC, and opt
When aligned BAMs contain modified base tags (`MM` and `ML`), the workflow also
runs `modkit` on each sample alignment. It first checks which modified base
codes are present in the BAM, then runs `modkit pileup` to produce a per-sample
bedMethyl file and one bigWig track per requested or inferred modification
under `samples/<alias>/mods/`.
bedMethyl file, a simple per-sample summary table, and one bigWig track per
requested or inferred modification under `samples/<alias>/mods/`.
If `--mod_codes` is set, those codes are passed directly to `modkit pileup`.
If it is omitted, the workflow infers the available `primary_base:mod_code`

View File

@ -7,6 +7,7 @@ Output files may be aggregated including information for all samples or provided
| Aligned BAM index | samples/{{ alias }}/alignment/reads.bam.bai | Index for the aligned BAM. | per-sample |
| Alignment summary | samples/{{ alias }}/alignment/bamstats.flagstat.tsv | bamstats flagstat summary for the aligned BAM. | per-sample |
| Modified base pileup | samples/{{ alias }}/mods/{{ alias }}.mods.bedmethyl.gz | Per-sample modkit bedMethyl pileup generated from the aligned BAM when MM and ML tags are present. | per-sample |
| Modified base summary | samples/{{ alias }}/mods/{{ alias }}.mods.summary.tsv | Per-sample global modification-percent summary aggregated from the modkit bedMethyl pileup, with one row per modification code. | per-sample |
| Modified base bigWig | samples/{{ alias }}/mods/{{ alias }}.mods.*.bw | Per-sample modkit bigWig tracks generated from the aligned BAM, with one file per requested or inferred modification code. | per-sample |
| Reference and annotation preparation summary | cohort/reference/annotation_reference_summary.json | Summary of reference and annotation preparation, including seqname overlap, build/provider hints, and excluded unstranded annotation counts. | aggregated |
| Excluded unstranded annotation records | cohort/reference/unstranded_annotation.gtf | Full set of annotation records excluded because their strand was not '+' or '-'. Present only when unstranded records are found. | aggregated |

View File

@ -43,6 +43,7 @@ process makeReport {
path "params.json"
path cohort_dir, stageAs: "cohort"
path sample_dirs, stageAs: "samples/*"
path mod_summary_files, stageAs: "mod_summaries/*"
path sqanti_dirs, stageAs: "sqanti/*"
path de_files
path "annotation_reference_summary.tsv"
@ -62,6 +63,7 @@ process makeReport {
${stats_args} \
--cohort_dir cohort \
--samples_dir samples \
--mod_summary_dir mod_summaries \
--sqanti_dir sqanti \
${de_args} \
--versions versions \
@ -132,6 +134,11 @@ workflow wf {
.map { meta, sample_dir -> sample_dir }
.collect()
mod_summaries_for_report = mod_results.summary
.map { alias, summary -> summary }
.ifEmpty(OPTIONAL_FILE)
.collect()
sqanti_dirs_for_report = transcriptome_results.joint_sqanti_dir
.concat(transcriptome_results.sample_sqanti_dirs.map { meta, sqanti_dir -> sqanti_dir })
.ifEmpty(OPTIONAL_FILE)
@ -155,6 +162,7 @@ workflow wf {
workflow_params,
transcriptome_results.joint_dir.ifEmpty(OPTIONAL_FILE),
sample_dirs_for_report,
mod_summaries_for_report,
sqanti_dirs_for_report,
de_dir,
transcriptome_results.annotation_reference_summary,

View File

@ -40,6 +40,14 @@
"optional": true,
"type": "per-sample"
},
"sample-mod-summary": {
"filepath": "samples/{{ alias }}/mods/{{ alias }}.mods.summary.tsv",
"title": "Modified base summary",
"description": "Per-sample global modification-percent summary aggregated from the modkit bedMethyl pileup, with one row per modification code.",
"mime-type": "text/tab-separated-values",
"optional": true,
"type": "per-sample"
},
"sample-mod-bigwig": {
"filepath": "samples/{{ alias }}/mods/{{ alias }}.mods.*.bw",
"title": "Modified base bigWig",

View File

@ -99,6 +99,33 @@ process modkit_tobigwig {
"""
}
process summariseModkitBedmethyl {
label "wf_common"
cpus 1
memory "2 GB"
input:
tuple val(alias),
path(bedmethyl),
val(mod_codes)
path mod_code_labels
output:
tuple val(alias),
path("${alias}.mods.summary.tsv"),
emit: summary
publishDir "${params.out_dir}/${output_key}/mods"
script:
output_key = alias == "cohort" ? "cohort" : "samples/${alias}" // nodef
"""
workflow-glue summarise_modkit_bedmethyl \
"${bedmethyl}" \
"${alias}" \
"${mod_codes}" \
"${mod_code_labels}" \
"${alias}.mods.summary.tsv"
"""
}
process inferModkitBases {
label "modkit"
cpus 1
@ -153,6 +180,10 @@ workflow mods {
mod_samples = xams_with.mods.join(sample_modcodes)
pileup = runModkitPileup(mod_samples, ref_genome)
sample_summaries = summariseModkitBedmethyl(
pileup.bedmethyl.join(sample_modcodes),
file("$projectDir/data/mod_code_labels.tsv")
)
sample_bigwigs = modkit_tobigwig(
ref_genome,
@ -162,5 +193,6 @@ workflow mods {
emit:
bedmethyl = pileup.bedmethyl
summary = sample_summaries.summary
bigwig = sample_bigwigs.bigwig
}