"""The benchmark is the project's strongest scientific claim, so its arithmetic is tested.""" import json import math import zipfile from pathlib import Path import pytest from rarelens_ml.benchmark import cases, gene_annotations, rank_of, report from rarelens_ml.hpo import information_content from rarelens_ml.hpo import phenotype_score as score def annotations_file(tmp_path: Path, genes: dict[str, list[str]]) -> str: path = tmp_path / "gp.tsv" path.write_text("".join(f"{g}\t{t}\n" for g, terms in genes.items() for t in terms)) return str(path) def phenopacket(gene: str, terms: list[str], excluded: list[str] | None = None) -> dict: return { "phenotypicFeatures": [{"type": {"id": t}} for t in terms] + [{"type": {"id": t}, "excluded": True} for t in (excluded or [])], "interpretations": [ { "diagnosis": { "genomicInterpretations": [ {"variantInterpretation": {"variationDescriptor": { "geneContext": {"symbol": gene}}}} ] } } ], } def store(tmp_path: Path, packets: dict[str, dict]) -> str: path = tmp_path / "pps.zip" with zipfile.ZipFile(path, "w") as z: for name, packet in packets.items(): z.writestr(name, json.dumps(packet)) return str(path) def test_information_content_makes_a_universal_term_worthless() -> None: genes = {"A": {"HP:1", "HP:2"}, "B": {"HP:1"}, "C": {"HP:1"}} ic = information_content(genes) assert ic["HP:1"] == pytest.approx(0.0) # every gene has it assert ic["HP:2"] == pytest.approx(math.log(3)) # one gene in three def test_score_is_recall_weighted_by_specificity() -> None: ic = {"HP:1": 0.0, "HP:2": 4.0} assert score(["HP:1", "HP:2"], {"HP:2"}, ic, 1.0) == pytest.approx(1.0) assert score(["HP:1", "HP:2"], {"HP:1"}, ic, 1.0) == pytest.approx(0.0) def test_rank_separates_optimistic_from_pessimistic_on_ties() -> None: """Every gene carrying the same term ties; the report must not hide that.""" genes = {"RIGHT": {"HP:1"}, "TIED": {"HP:1"}, "WRONG": {"HP:9"}} ic = {"HP:1": 1.0, "HP:9": 1.0} optimistic, pessimistic, target = rank_of("RIGHT", ["HP:1"], genes, ic, 1.0) assert (optimistic, pessimistic) == (1, 2) assert target == pytest.approx(1.0) def test_a_uniquely_matching_gene_ranks_first_either_way() -> None: genes = {"RIGHT": {"HP:1", "HP:2"}, "PARTIAL": {"HP:1"}, "WRONG": set()} ic = {"HP:1": 1.0, "HP:2": 1.0} assert rank_of("RIGHT", ["HP:1", "HP:2"], genes, ic, 1.0)[:2] == (1, 1) def test_cases_reads_the_causal_gene_and_drops_excluded_terms(tmp_path: Path) -> None: """An excluded feature means the authors looked and did not find it.""" path = store(tmp_path, { "a/one.json": phenopacket("TGFBR2", ["HP:1", "HP:2"], excluded=["HP:3"]), "a/notes.txt": {}, }) assert list(cases(path)) == [("TGFBR2", ["HP:1", "HP:2"])] def test_cases_skips_packets_without_exactly_one_causal_gene(tmp_path: Path) -> None: two = phenopacket("A", ["HP:1"]) two["interpretations"][0]["diagnosis"]["genomicInterpretations"].append( {"variantInterpretation": {"variationDescriptor": {"geneContext": {"symbol": "B"}}}} ) path = store(tmp_path, {"two.json": two, "none.json": phenopacket("C", [])}) assert list(cases(path)) == [] def test_gene_annotations_reads_the_export(tmp_path: Path) -> None: path = annotations_file(tmp_path, {"A": ["HP:1", "HP:2"], "B": ["HP:1"]}) assert gene_annotations(path) == {"A": {"HP:1", "HP:2"}, "B": {"HP:1"}} def test_report_states_the_contamination_and_both_bounds() -> None: text = report([(1, 2, 1.0), (1, 1, 1.0), (3, 5, 0.5)], n_genes=100) assert "optimistic" in text and "pessimistic" in text assert "HPO already carries" in text # the caveat travels with the number