P6 complete: optional VCF fields, SNV+indel merging, auxiliary MNV loci, PLINK 2 .pgen
- gtc-to-vcf --info-fields ASSAY_TYPE,NORM_BIN,GENTRAIN,CLUSTER_SEP,ILMN_STRAND,PROBE_IDS (--egt) and --extra-format X,Y,NORMX,NORMY,THETA,R, in per-sample VCFs and the --merge BCF (OUT-012, plan §8.3); equal to gtc2vcf's values (THETA/R missing where both normalized intensities are 0, K-023) - --merge-snv-indel (OUT-021, EDGE-013): alleles extended to the longest REF; SNV probes read the base at POS - --auxiliary-loci (OUT-035, EDGE-015): rules (a)-(e), E_AUX_INVALID; rule (d) uses the last plus-strand base for minus-strand designs (spec amended, as GTCtoVCF) - export plink --format bed|pgen|both: PLINK 2 fixed-width .pgen/.pvar/.psam; plink2 reads it back to a .bed byte-identical to ours - cohort store keeps raw/normalized intensities for the BCF path; tests (unit + gtc2vcf oracle); docs
This commit is contained in:
1 parent
d3add008f7
commit
1001bf951e
13 files changed
+611
-50
No files matched your search
@@ -308,27 +308,45 @@ class SampleInfo:
|
||||
class CohortStore:
|
||||
"""Disk-backed (samples × loci) matrices: genotypes (uint8), BAF and GenCall scores (float32, optional)."""
|
||||
|
||||
def __init__(self, root: Path, n_samples: int, n_loci: int, *, baf: bool, scores: bool, lrr: bool = False) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
n_samples: int,
|
||||
n_loci: int,
|
||||
*,
|
||||
baf: bool,
|
||||
scores: bool,
|
||||
lrr: bool = False,
|
||||
intensities: bool = False,
|
||||
) -> None:
|
||||
self.root, self.shape = root, (n_samples, n_loci)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
self.gt = np.lib.format.open_memmap(root / "gt.npy", "w+", np.uint8, self.shape)
|
||||
self.baf = np.lib.format.open_memmap(root / "baf.npy", "w+", np.float32, self.shape) if baf else None
|
||||
self.score = np.lib.format.open_memmap(root / "score.npy", "w+", np.float32, self.shape) if scores else None
|
||||
self.lrr = np.lib.format.open_memmap(root / "lrr.npy", "w+", np.float32, self.shape) if lrr else None
|
||||
# raw and normalized intensities (needs root/lookup.npy: normalization-transform index per locus)
|
||||
self.rx = self.ry = self.nx = self.ny = None
|
||||
if intensities:
|
||||
self.rx = np.lib.format.open_memmap(root / "rx.npy", "w+", np.uint16, self.shape)
|
||||
self.ry = np.lib.format.open_memmap(root / "ry.npy", "w+", np.uint16, self.shape)
|
||||
self.nx = np.lib.format.open_memmap(root / "nx.npy", "w+", np.float32, self.shape)
|
||||
self.ny = np.lib.format.open_memmap(root / "ny.npy", "w+", np.float32, self.shape)
|
||||
|
||||
def close(self) -> None:
|
||||
for name in ("gt", "baf", "score", "lrr"):
|
||||
for name in ("gt", "baf", "score", "lrr", "rx", "ry", "nx", "ny"):
|
||||
a = getattr(self, name)
|
||||
if a is not None:
|
||||
a.flush()
|
||||
self.gt = self.baf = self.score = self.lrr = None # type: ignore[assignment]
|
||||
self.rx = self.ry = self.nx = self.ny = None
|
||||
shutil.rmtree(self.root, ignore_errors=True)
|
||||
|
||||
|
||||
def _load_one(task: tuple[int, str, str, str, bool, bool, bool]) -> SampleInfo:
|
||||
def _load_one(task: tuple[int, str, str, str, bool, bool, bool, bool]) -> SampleInfo:
|
||||
from simurg.formats.gtc import read_gtc
|
||||
|
||||
i, sid, path, root, want_baf, want_scores, want_lrr = task
|
||||
i, sid, path, root, want_baf, want_scores, want_lrr, want_int = task
|
||||
info = SampleInfo(sid, path)
|
||||
try:
|
||||
g = read_gtc(path)
|
||||
@@ -356,6 +374,15 @@ def _load_one(task: tuple[int, str, str, str, bool, bool, bool]) -> SampleInfo:
|
||||
sc = np.load(Path(root) / "score.npy", mmap_mode="r+")
|
||||
sc[i] = g.genotype_scores
|
||||
sc.flush()
|
||||
if want_int:
|
||||
from simurg.signal.normalization.apply import normalize
|
||||
|
||||
lookup = np.load(Path(root) / "lookup.npy")
|
||||
nx, ny = normalize(g.raw_x, g.raw_y, lookup, g.normalization_transforms)
|
||||
for name, arr in (("rx", g.raw_x), ("ry", g.raw_y), ("nx", nx), ("ny", ny)):
|
||||
mm = np.load(Path(root) / f"{name}.npy", mmap_mode="r+")
|
||||
mm[i] = arr
|
||||
mm.flush()
|
||||
except SimurgError as e:
|
||||
info.error = f"{e.code}: {e.message}"
|
||||
except Exception as e:
|
||||
@@ -372,11 +399,15 @@ def load_cohort(
|
||||
scores: bool,
|
||||
workers: int,
|
||||
lrr: bool = False,
|
||||
lookup: npt.NDArray[np.int32] | None = None,
|
||||
) -> tuple[CohortStore, list[SampleInfo]]:
|
||||
"""Read GTCs (sample_id, path) into a :class:`CohortStore` with a process pool."""
|
||||
store = CohortStore(tmp, len(samples), n_loci, baf=baf, scores=scores, lrr=lrr)
|
||||
"""Read GTCs (sample_id, path) into a :class:`CohortStore` with a process pool. With ``lookup`` (the manifest's
|
||||
normalization-transform index per locus) raw and normalized intensities are stored too."""
|
||||
store = CohortStore(tmp, len(samples), n_loci, baf=baf, scores=scores, lrr=lrr, intensities=lookup is not None)
|
||||
if lookup is not None:
|
||||
np.save(tmp / "lookup.npy", lookup)
|
||||
store.gt.flush()
|
||||
tasks = [(i, sid, p, str(tmp), baf, scores, lrr) for i, (sid, p) in enumerate(samples)]
|
||||
tasks = [(i, sid, p, str(tmp), baf, scores, lrr, lookup is not None) for i, (sid, p) in enumerate(samples)]
|
||||
if workers <= 1 or len(tasks) <= 1:
|
||||
infos = [_load_one(t) for t in tasks]
|
||||
else:
|
||||
|
||||
Reference in new issue
Block a user