CN: MF split normalised by per-probe reference BAF noise; genotype-based complete loss (CYTO-041)

The mosaic BAF split is now estimated over heterozygous calls against each probe's expected noise (the
reference heterozygous spread scaled to the sample), inverting the median of |N(+-d, sigma)|. A mosaic loss
lacking the heterozygous calls its PFB predicts gets MF 1 and is promoted (CYTO-020).
This commit is contained in:
kemal committed 2026-09-29 10:56:21 +01:00
1 parent 4315f37f1f
commit aa3031a18a
4 files changed
+90 -31

No files matched your search

+9 -6
View File
@@ -12,7 +12,7 @@ from typer.testing import CliRunner
from simurg.cli.main import app
from simurg.cn import hmm
from simurg.cn.events import Event, Thresholds, _mf_from_baf, apply_thresholds, effective_size, extract
from simurg.cn.events import Event, Thresholds, _mf_from_baf, apply_thresholds, complete_loss, effective_size, extract
from simurg.cn.gcwave import gc_correct
from simurg.cn.model import gc_fractions, population_bfreq
from simurg.formats.gtc import Gtc, write_gtc
@@ -237,12 +237,15 @@ def test_mf_refinements() -> None: # CYTO-018 / CYTO-041
b = 0.5 + rng.normal(0, sd, 400)
mf = _mf_from_baf("MDEL", b, sd)
assert mf is not None and mf < 0.08
assert (_mf_from_baf("MDEL", b, 0.0) or 0) > 0.05 # uncorrected, the old estimate was biased upwards
assert (_mf_from_baf("MDEL", b) or 0) > 0.05 # uncorrected, the old estimate was biased upwards
# a true 40 % mosaic loss is still recovered
d = 0.4 / (2 * (2 - 0.4))
b = 0.5 + rng.choice([-d, d], 400) + rng.normal(0, sd, 400)
assert abs((_mf_from_baf("MDEL", b, sd) or 0) - 0.4) < 0.08
# no heterozygotes where ~50 are expected: a complete (constitutional) loss
b = rng.choice([0.0, 1.0], 100) + rng.normal(0, 0.01, 100)
assert _mf_from_baf("MDEL", np.clip(b, 0, 1), sd, np.full(100, 0.3)) == 1.0
assert _mf_from_baf("MDEL", np.clip(b, 0, 1), sd, np.full(100, 0.01)) is None # too few expected
# no heterozygous calls where ~12 are expected: a complete (constitutional) loss
hom = np.full(30, 1, dtype=np.uint8)
assert complete_loss(hom, np.full(30, 0.3))
assert not complete_loss(hom, np.full(30, 0.02)) # too few expected
mixed = hom.copy()
mixed[:8] = 2
assert not complete_loss(mixed, np.full(30, 0.3))