P4 (WIP): GenCall-equivalent calling, BAF/LRR, sex, GTC v5, simurg genotype

- genotype/clusters.py: EGT aligned to manifest order (float32 mean R)
- genotype/gencall.py: flair, six-case likelihood, priors/posteriors,
  fuzzy sub-scores and score map, vectorised with the reference's
  float/double evaluation order and libm transcendentals
- genotype/baf_lrr.py, sex.py (rules v1/v2), sample.py (IDAT -> Gtc)
- pipeline/inputs.py shared by ingest and genotype
- pipeline/genotype.py + CLI: GTC per sample, sample_summary.tsv/json
  (OUT-060..063), run manifest
- GTCs from IDATs are byte-identical to the oracle for 36 GSA samples x
  2 normalization versions (NaN sign canonicalised to the x86 pattern)
This commit is contained in:
kemal committed 2026-09-28 00:07:37 +01:00
1 parent fca3f5f72f
commit 1f7b6d10c5
12 files changed
+1270 -53

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
"""Phase P4 acceptance on P0 data: GTCs produced from IDATs are byte-identical to oracle GTCs (gtc2vcf built without
FMA) once the four free-text metadata fields (cluster/manifest file names, AutoCall date and version) are aligned.
"""
from __future__ import annotations
import numpy as np
import pytest
from tests.conftest import GSA_BPM, GSA_EGT, GSA_IDATS, ORACLE_GTCS, ORACLE_GTCS_GT2, need
from simurg.formats.bpm import read_bpm
from simurg.formats.egt import read_egt
from simurg.formats.gtc import encode_gtc, read_gtc
from simurg.formats.idat import read_idat
from simurg.genotype.clusters import align_clusters
from simurg.genotype.sample import GenotypeParams, genotype_sample, manifest_context
pytestmark = pytest.mark.data
FREE_TEXT = ("cluster_file", "snp_manifest", "autocall_date", "autocall_version")
NEG_NAN = np.frombuffer(np.uint32(0xFFC00000).tobytes(), dtype=np.float32)[0]
def canonical_nans(g): # type: ignore[no-untyped-def]
"""NaNs computed by libm carry the platform's default sign (+ on aarch64, − on x86-64); SimurgArray always
writes 0xFFC00000 like the vendor's x86 build. Compare everything else byte for byte (known_unknowns K-014)."""
for k in ("b_allele_freqs", "logr_ratios", "genotype_scores"):
a = getattr(g, k).copy()
a[np.isnan(a)] = NEG_NAN
setattr(g, k, a)
return g
@pytest.fixture(scope="module")
def ctx(): # type: ignore[no-untyped-def]
need(GSA_BPM, GSA_EGT)
m = read_bpm(GSA_BPM)
return manifest_context(m, align_clusters(read_egt(GSA_EGT), m))
@pytest.mark.parametrize(
("folder", "norm"), [(ORACLE_GTCS, "1.2.0"), (ORACLE_GTCS_GT2, "1.1.2")], ids=["1.2.0", "1.1.2"]
)
def test_gtc_byte_identical(ctx, folder, norm) -> None: # type: ignore[no-untyped-def]
need(folder, GSA_IDATS)
gtcs = sorted(folder.glob("*.gtc"))
assert len(gtcs) >= 18
for f in gtcs:
sid = f.stem
chip = sid.split("_")[0]
ref = read_gtc(f)
g = genotype_sample(
read_idat(GSA_IDATS / chip / f"{sid}_Grn.idat"),
read_idat(GSA_IDATS / chip / f"{sid}_Red.idat"),
ctx,
GenotypeParams(norm_version=norm),
)
for k in FREE_TEXT:
setattr(g, k, getattr(ref, k))
assert encode_gtc(g) == encode_gtc(canonical_nans(ref)), sid