v2 methylation control QC (WIP): simurg methyl qc|convert-manifest (MET-001..018)

Methylation CSV manifest reader (MET-002/003), raw M/U assembly with bead masking
(MET-005), pOOBAH detection p-values, noob background correction, linear/nonlinear dye
bias, beta and M-values (MET-010..012), the 21 methylation control metrics (MET-006,
bs-conv red-bg set option), per-sample ctrl/cgs tables, summary tsv/xlsx/html with the
detection histogram and a beta PCA, pcs.tsv.gz, controls_raw/controls_qc.tsv; methylation
datasets load in qc report (MET-014); qc controls --array-type methylation; minfi-style
Sentrix_ID/Sentrix_Position sheet keys. Vendor EPIC manifests carry a 5th field on some
control lines: >= 4 fields accepted. Runs on the public minfiDataEPIC set (3 x NA12878).
This commit is contained in:
kemal committed 2026-09-29 00:13:22 +01:00
1 parent d235dc5419
commit 3eb51639cc
13 files changed
+1292 -21

No files matched your search

+20 -7
View File
@@ -36,6 +36,7 @@ from simurg.qc.controls import (
ControlBead,
ControlQC,
beads_from_definitions,
canonical_id,
compare_definitions,
compute_control_metrics,
control_intensities,
@@ -123,9 +124,10 @@ def control_sample(
beads: Sequence[ControlBead],
bg_offset: float,
sheet_values: Mapping[str, str] | None = None,
array_type: str = "genotyping",
) -> ControlSample:
g, r = control_intensities(grn, red, beads)
qc = compute_control_metrics(beads, g, r, "genotyping", bg_offset)
qc = compute_control_metrics(beads, g, r, array_type, bg_offset)
return ControlSample(sample_id, idat_metadata(grn, red, sheet_values), g, r, qc)
@@ -250,8 +252,8 @@ def run_qc_controls(opts: QcControlsOptions, rep: Reporter) -> int:
rm = RunManifest(command="qc controls", argv=opts.argv)
rep.run_id = rm.run_id
if opts.array_type == "methylation":
raise SimurgError("E_QC_METHYLATION", "methylation control metrics are planned for v2 (QC-006)")
if opts.array_type == "methylation" and opts.csv is None:
raise SimurgError("E_NO_INPUT", "methylation control QC needs the methylation CSV manifest (--csv)")
if opts.bpm is None and opts.csv is None:
raise SimurgError("E_NO_INPUT", "give --bpm or --csv for the control definitions (QC-001)")
if opts.bg_offset < 0:
@@ -261,8 +263,15 @@ def run_qc_controls(opts: QcControlsOptions, rep: Reporter) -> int:
names = (COMPAT_RAW, COMPAT_QC) if opts.compat else (CONTROLS_RAW, CONTROLS_QC)
targets = [opts.out / n for n in names]
check_outputs_absent(targets, force=opts.force) # QC-014: other files in the folder are fine
manifest = read_bpm(opts.bpm) if opts.bpm else None
beads = load_control_beads(manifest, opts.csv, rep)
if opts.array_type == "methylation":
from simurg.formats.methyl_manifest import read_methyl_manifest
assert opts.csv is not None
mman = read_methyl_manifest(opts.csv)
beads = [ControlBead(c.type, c.color_channel, c.name, c.address, canonical_id(c.name)) for c in mman.controls]
else:
manifest = read_bpm(opts.bpm) if opts.bpm else None
beads = load_control_beads(manifest, opts.csv, rep)
if not beads:
raise SimurgError("E_BPM_FORMAT", "the manifest has no control definitions")
samples: list[ControlSample] = []
@@ -273,7 +282,9 @@ def run_qc_controls(opts: QcControlsOptions, rep: Reporter) -> int:
assert s.pair.grn is not None and s.pair.red is not None
grn, red = read_idat(s.pair.grn), read_idat(s.pair.red)
check_pair(grn, red)
samples.append(control_sample(s.sample_id, grn, red, beads, opts.bg_offset, sheet_values(s, sset.sheet)))
samples.append(
control_sample(s.sample_id, grn, red, beads, opts.bg_offset, sheet_values(s, sset.sheet), opts.array_type)
)
# QC-014: sample IDs of an existing summary must match
summary = opts.out / "sample_summary.tsv"
if summary.exists():
@@ -282,7 +293,9 @@ def run_qc_controls(opts: QcControlsOptions, rep: Reporter) -> int:
if ids ^ mine:
rep.warn("W_QC_UNMATCHED_SAMPLES", f"{len(ids ^ mine)} sample ID(s) differ from {summary}")
opts.out.mkdir(parents=True, exist_ok=True)
written = write_control_tables(opts.out, samples, beads, opts.bg_offset, rep, compat=opts.compat)
written = write_control_tables(
opts.out, samples, beads, opts.bg_offset, rep, array_type=opts.array_type, compat=opts.compat
)
for p in written:
rm.add_output(p, "simurg.controls/1")
rm.summary = {"samples": len(samples)}