syntheogenesis / tests /test_exon.py
Tengo Gzirishvili
CRISPR M0: validation test suite + fix KO cut-position copy-paste bug
36858f4
Raw
History Blame Contribute Delete
2.9 kB
"""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]