Spaces:
Running
Running
| """Exon-aware annotation — alignment + exon mapping + NMD logic. | |
| NO NETWORK. We never call Ensembl: a GeneStructure is built by hand and | |
| the pure mapping functions are exercised directly. | |
| """ | |
| import random | |
| import pytest | |
| from dee.core.exon import ( | |
| Exon, | |
| GeneStructure, | |
| annotate_guides_with_exons, | |
| _align_input_to_cds, | |
| _exon_at_cds_position, | |
| ) | |
| # A deterministic, NON-repeating 300-nt CDS so 40-mer seeds align | |
| # unambiguously (a repeating sequence would make find() return the | |
| # wrong offset). Seed the RNG ONCE — instantiating Random(42) per | |
| # iteration would re-seed every draw and yield a constant base. | |
| _RNG = random.Random(42) | |
| CDS = "".join(_RNG.choice("ACGT") for _ in range(300)) | |
| GENE = GeneStructure( | |
| organism="human", | |
| gene_symbol="TESTG", | |
| transcript_id="ENST00000TEST", | |
| strand=1, | |
| cds_sequence=CDS, | |
| exons=[ | |
| Exon(number=1, chrom_start=1000, chrom_end=1099, cds_start=0, cds_end=100), | |
| Exon(number=2, chrom_start=2000, chrom_end=2099, cds_start=100, cds_end=200), | |
| Exon(number=3, chrom_start=3000, chrom_end=3099, cds_start=200, cds_end=300), | |
| ], | |
| last_junction_cds_pos=200, | |
| ) | |
| def test_align_exact_and_offset(): | |
| assert _align_input_to_cds(CDS, CDS) == 0 | |
| assert _align_input_to_cds(CDS[30:], CDS) == 30 | |
| def test_align_returns_none_for_short_or_unmatched(): | |
| assert _align_input_to_cds("ACGT", CDS) is None # < 30 nt | |
| assert _align_input_to_cds("", CDS) is None | |
| assert _align_input_to_cds("Z" * 50, CDS) is None # nothing valid | |
| def test_exon_at_cds_position(): | |
| assert _exon_at_cds_position(GENE, 50).number == 1 | |
| assert _exon_at_cds_position(GENE, 150).number == 2 | |
| assert _exon_at_cds_position(GENE, 250).number == 3 | |
| assert _exon_at_cds_position(GENE, 350) is None # past CDS end | |
| def _annotate_single(cut): | |
| return annotate_guides_with_exons(CDS, [(cut, "+", cut)], GENE)[0] | |
| def test_annotate_maps_cut_to_exon(): | |
| ctx = _annotate_single(50) | |
| assert ctx.in_cds is True | |
| assert ctx.exon_number == 1 | |
| assert ctx.distance_to_splice_5p == 50 # 50 - cds_start(0) | |
| assert ctx.distance_to_splice_3p == 49 # cds_end(100) - 50 - 1 | |
| assert "exon 1 of 3" in ctx.summary | |
| def test_nmd_zone_rule(): | |
| # PTC > 50 nt upstream of the last junction (cds pos 200) → NMD likely. | |
| assert _annotate_single(50).in_nmd_zone is True # 50 < 150 | |
| assert _annotate_single(250).in_nmd_zone is False # last exon, escapes NMD | |
| def test_cut_outside_cds_marked_not_in_cds(): | |
| ctx = _annotate_single(500) # beyond CDS length | |
| assert ctx.in_cds is False | |
| assert ctx.exon_number == 0 | |
| def test_annotation_order_matches_input(): | |
| cuts = [(50, "+", 50), (150, "+", 150), (250, "+", 250)] | |
| out = annotate_guides_with_exons(CDS, cuts, GENE) | |
| assert [c.exon_number for c in out] == [1, 2, 3] | |