v2 CN: cn annotate (JSON report, ISCN 2020, cytobands, genes) and the annotation bundle builder

cn call now records per-chromosome and sample statistics (percent het, LRR stats, BAF
deviations, robust LogR dev, call rate) in the sidecar; cn annotate writes
<sample>.cyto.json (CYTO-033) with ISCN microarray/traditional strings (CYTO-034), bands
and RefSeq gene lists (CYTO-035), LOH summaries, weighted median CN (CYTO-026); annotation
bundle from UCSC cytoBand + ncbiRefSeq (CYTO-036, IN-054). Tests for ISCN strings, gene
overlap, sidecar-missing error and an end-to-end call+annotate.
This commit is contained in:
kemal committed 2026-09-29 01:37:56 +01:00
1 parent 615a6139bf
commit 0bf5576049
6 files changed
+428 -2

No files matched your search

+48
View File
@@ -171,3 +171,51 @@ def test_cn_build_and_call(tmp_path: Path) -> None: # CYTO-028 .. CYTO-032, CYT
summ = (out / "cn_sample_summary.tsv").read_text().splitlines()
assert summ[0].startswith("sample_id\tsample_name\tstatus") and len(summ) == 3
assert (out / "200000000001_R01C01.cn_segments.parquet").exists()
# ------------------------------------------------------------------------------------------------ annotate
def test_iscn_strings_and_genes() -> None: # CYTO-034 / CYTO-035
from simurg.cn.annotate import Bands, Genes, iscn_sample
bands = Bands.from_ucsc("chr1\t0\t2300000\tp36.33\tgneg\nchr1\t2300000\t5300000\tp36.32\tgpos25\n"
"chr9\t0\t2200000\tp24.3\tgneg\n") # fmt: skip
dele = {"chrom": "1", "start": 1_567_924, "end": 1_632_825, "call": "DEL", "call_type": "DEL", "copy_number": 1,
"mosaic": False} # fmt: skip
dup = {"chrom": "1", "start": 2_000_000, "end": 3_000_000, "call": "DUP", "call_type": "DUP", "copy_number": 3,
"mosaic": False} # fmt: skip
loh = {"chrom": "9", "start": 100, "end": 2_000_000, "call": "LOH", "call_type": "LOH", "copy_number": 2,
"mosaic": False} # fmt: skip
mos = {"chrom": "9", "start": 100, "end": 1_000, "call": "MDEL", "call_type": "DEL", "copy_number": 1,
"mosaic": True, "base_cn": 2} # fmt: skip
trad, micro = iscn_sample([loh, dup, dele], bands, "GRCh38", "F")
assert micro == ("arr[GRCh38] 1p36.33(1567924_1632825)x1,1p36.33p36.32(2000000_3000000)x3,"
"9p24.3(100_2000000)x2 hmz") # fmt: skip
assert trad == "del(1)(p36.33p36.33),dup(1)(p36.33p36.32)"
assert iscn_sample([mos], bands, "GRCh38", "F")[1] == "arr[GRCh38] 9p24.3(100_1000)x2~1"
assert iscn_sample([], bands, "GRCh38", "M")[1] == "arr[GRCh38](1-22)x2,(XY)x1"
assert iscn_sample([], bands, "GRCh37", "F")[1] == "arr[GRCh37](1-22,X)x2"
assert iscn_sample([], bands, "GRCh38", "U")[1] == "arr[GRCh38](1-22)x2"
genes = Genes.from_rows([("1", 100, 5_000, "A"), ("1", 4_000, 4_500, "B"), ("1", 200, 300, "A"),
("1", 9_000, 9_100, "C")]) # fmt: skip
assert genes.overlapping("1", 4_400, 8_999) == ["A", "B"]
def test_cn_annotate_end_to_end(tmp_path: Path) -> None: # CYTO-032 / CYTO-033
bpm, fa, gtcs = _dataset(tmp_path)
model, out = tmp_path / "m.simurg-cyto", tmp_path / "cn"
run = CliRunner().invoke
assert run(app, ["cn", "annotate", "--vcfs", str(tmp_path)]).exit_code == 2 # E_CN_SIDECAR_MISSING
assert run(app, ["cn", "build-model", "--bpm", str(bpm), "--fasta", str(fa), "--out", str(model),
"--quiet"]).exit_code == 0 # fmt: skip
r = run(app, ["cn", "call", "--bpm", str(bpm), "--model", str(model), "--gtcs", str(gtcs), "--fasta", str(fa),
"--out", str(out), "--threads", "1", "--unknown-sex-policy", "female", "--quiet"]) # fmt: skip
assert r.exit_code == 0, r.output
r = run(app, ["cn", "annotate", "--vcfs", str(out), "--min-qual", "0", "--quiet"])
assert r.exit_code == 0, r.output
doc = json.loads((out / "200000000001_R01C01.cyto.json").read_text())
assert doc["schema"] == "simurg.cyto/1" and doc["annotation_type"] == "constitutional"
assert doc["iscn_microarray"].startswith("arr[")
assert any(e["call_type"] == "DEL" and e["chrom"] == "1" for e in doc["events"])
assert {c["id"] for c in doc["chromosomes"]} == {"1", "2"}
normal = json.loads((out / "200000000001_R02C01.cyto.json").read_text())
assert normal["iscn_microarray"] in ("arr[GRCh38](1-22,X)x2", "arr[GRCh37](1-22,X)x2") or normal["events"]