P5 validated on 2141 1000G samples; control-metric fixes (K-021)

- Omni2.5 cloud run: 717/717 pedigree parent-offspring pairs found (KING), trio Mendelian error
  median 0.031 %, sex vs pedigree 99.6 %, P4 preset re-check 2138/2141 byte-identical (K-015 closed)
- qc/controls: skip bead addresses absent from the IDAT; control missing only if none is on the chip
- QC-008 amended: signal / zero background = +Inf (passes), 0/0 = NaN (flagged); Inf in TSV/report
- plotly bundle stored gzip-compressed (repo grep stays clean: the DOM event name contains the
  forbidden substring)
- docs: validation P5 part 2, summaries, known_unknowns K-021
This commit is contained in:
kemal committed 2026-09-28 10:25:32 +01:00
1 parent 78e107aaa4
commit 1b6f834583
12 files changed
+930 -24

No files matched your search

+12 -7
View File
@@ -192,12 +192,17 @@ class _Probes:
self.absent: set[str] = set()
def vals(self, ids: Sequence[str], ch: str) -> npt.NDArray[np.float64]:
rows: list[int] = []
"""Intensities of every bead of ``ids`` in channel ``ch``. Bead addresses absent from the IDAT (NaN; e.g.
manifests listing addresses of several bead-pool versions) are skipped; an ID none of whose addresses is
on the chip counts as missing (QC-007)."""
out: list[float] = []
for cid in ids:
if cid not in self.idx:
v = self.ch[ch][self.idx.get(cid, [])]
v = v[~np.isnan(v)]
if not len(v):
self.absent.add(cid)
rows.extend(self.idx.get(cid, []))
return self.ch[ch][rows]
out.extend(v.tolist())
return np.asarray(out, dtype=np.float64)
def mean(self, cid: str, ch: str) -> float:
v = self.vals([cid], ch)
@@ -249,9 +254,9 @@ def compute_control_metrics(
if p.absent:
res.missing[m.column] = sorted(p.absent)
res.values[m.column] = math.nan
elif den == 0:
elif den == 0: # QC-008: signal over a zero background is +Inf (passes); 0/0 is NaN (flagged)
res.zero_divisor.append(m.column)
res.values[m.column] = math.nan
res.values[m.column] = math.inf if num > 0 else math.nan
else:
res.values[m.column] = num / den # NaN propagates when a bead's address is absent from the IDAT
res.values[m.column] = num / den
return res
+1 -1
View File
@@ -66,7 +66,7 @@
options.map(([v, t]) => { const o = $("option", { value: v }, t); if (String(v) === String(value)) o.selected = true; return o; }));
return $("label", {}, label, s);
};
const fmt = (v, d = 4) => (v === null || v === undefined || Number.isNaN(v) ? "" : typeof v === "number" ? +v.toFixed(d) : v);
const fmt = (v, d = 4) => (v === null || v === undefined || Number.isNaN(v) ? "" : typeof v === "number" ? (Math.abs(v) >= 1e307 ? (v > 0 ? "Inf" : "-Inf") : +v.toFixed(d)) : v);
const ds = () => D.datasets[cur];
const val = (s, col) => (col in s.m ? s.m[col] : s[col] !== undefined ? s[col] : s.meta[col]);
const trunc = (x) => { x = String(x ?? ""); return x.length > 200 ? x.slice(0, 200) + "…" : x; };