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
@@ -148,3 +148,74 @@ def test_bedgraph_equals_gtc(tmp_path: Path) -> None: # OUT-050, OUT-052, EDGE-
|
||||
assert v == f"{float(g.logr_ratios[single[key]]):.6g}"
|
||||
checked += 1
|
||||
assert checked > 500_000
|
||||
|
||||
|
||||
def test_info_and_extra_format_match_gtc2vcf(tmp_path: Path) -> None: # OUT-012, plan §8.3
|
||||
"""INFO ASSAY_TYPE/GENTRAIN/CLUSTER_SEP and FORMAT X/Y/NORMX/NORMY equal gtc2vcf's; THETA/R too, except where the
|
||||
normalized intensities are both 0 (SimurgArray: missing, plan §7.4; gtc2vcf: 0)."""
|
||||
from tests.conftest import GSA_EGT
|
||||
|
||||
plugin = BCFTOOLS.parent / "plugins" / "gtc2vcf.so"
|
||||
need(GSA_BPM, GSA_CSV, GSA_EGT, FASTA, ORACLE_GTCS)
|
||||
if not plugin.exists():
|
||||
pytest.skip("gtc2vcf plugin not built")
|
||||
import os
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from simurg.cli.main import app
|
||||
|
||||
gtcs = tmp_path / "g"
|
||||
gtcs.mkdir()
|
||||
src = sorted(ORACLE_GTCS.glob("*.gtc"))[0]
|
||||
(gtcs / src.name).symlink_to(src)
|
||||
out = tmp_path / "v"
|
||||
res = CliRunner().invoke(
|
||||
app,
|
||||
["gtc-to-vcf", "--bpm", str(GSA_BPM), "--csv", str(GSA_CSV), "--fasta", str(FASTA), "--gtcs", str(gtcs),
|
||||
"--out", str(out), "--threads", "1", "--unsquash-duplicates", "--quiet", "--egt", str(GSA_EGT),
|
||||
"--info-fields", "ASSAY_TYPE,GENTRAIN,CLUSTER_SEP", "--extra-format", "X,Y,NORMX,NORMY,THETA,R"],
|
||||
) # fmt: skip
|
||||
assert res.exit_code == 0, res.output
|
||||
env = dict(os.environ, BCFTOOLS_PLUGINS=str(plugin.parent))
|
||||
g2v = subprocess.run(
|
||||
[str(BCFTOOLS), "+gtc2vcf", "--no-version", "-t", "GT,X,Y,NORMX,NORMY,THETA,R", "-b", str(GSA_BPM), "-c",
|
||||
str(GSA_CSV), "-e", str(GSA_EGT), "-f", str(FASTA), "-g", str(gtcs), "-O", "v"],
|
||||
capture_output=True, text=True, env=env, check=True,
|
||||
).stdout # fmt: skip
|
||||
ref = {}
|
||||
for line in g2v.splitlines():
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
c = line.split("\t")
|
||||
info = dict(kv.split("=", 1) for kv in c[7].split(";") if "=" in kv)
|
||||
ref[c[2]] = (info, dict(zip(c[8].split(":"), c[9].split(":"), strict=True)))
|
||||
|
||||
def close(a: str, b: str) -> bool:
|
||||
na, nb = a in (".", "") or "nan" in a.lower(), b in (".", "") or "nan" in b.lower()
|
||||
if na or nb:
|
||||
return na and nb
|
||||
return abs(float(a) - float(b)) <= 1e-5 * max(1.0, abs(float(b)))
|
||||
|
||||
checked = zero_norm = 0
|
||||
with gzip.open(out / f"{src.stem}.snv.vcf.gz", "rt") as fh:
|
||||
for line in fh:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
c = line.rstrip("\n").split("\t")
|
||||
if "," in c[2] or ";" in c[2] or c[2] not in ref:
|
||||
continue
|
||||
info = dict(kv.split("=", 1) for kv in c[7].split(";"))
|
||||
fmt = dict(zip(c[8].split(":"), c[9].split(":"), strict=True))
|
||||
ri, rf = ref[c[2]]
|
||||
assert info["ASSAY_TYPE"] == ri["ASSAY_TYPE"]
|
||||
assert close(info["GENTRAIN"], ri["GenTrain_Score"]) and close(info["CLUSTER_SEP"], ri["Cluster_Sep"])
|
||||
for k in ("X", "Y", "NORMX", "NORMY"):
|
||||
assert close(fmt[k], rf[k]), (c[2], k, fmt[k], rf[k])
|
||||
if fmt["NORMX"] == "0" and fmt["NORMY"] == "0":
|
||||
zero_norm += 1
|
||||
assert fmt["THETA"] == "." and float(rf["THETA"]) == 0.0
|
||||
else:
|
||||
assert close(fmt["THETA"], rf["THETA"]) and close(fmt["R"], rf["R"]), (c[2], fmt, rf)
|
||||
checked += 1
|
||||
assert checked > 600_000 and zero_norm < 1000
|
||||
Reference in new issue
Block a user