File size: 7,653 Bytes
5278136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c357a0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""
Tests for the deterministic half of the pipeline.

The model outputs cannot be asserted on, but the glue can -- and the glue is
where a silent error does the most damage. An off-by-one in the TIS coordinate
shifts every reading frame and still produces a plausible-looking protein, so
these cases exist mainly to pin that down.
"""

import orf
import pipeline


class TestTranslate:
    def test_basic(self):
        assert orf.translate("ATGGCCTAA") == "MA"

    def test_stops_at_stop(self):
        assert orf.translate("ATGGCCTAAATGGGG") == "MA"

    def test_can_read_through(self):
        assert orf.translate("ATGTAAATG", stop_at_stop=False) == "M*M"

    def test_rna_input(self):
        assert orf.translate("AUGGCCUAA") == "MA"

    def test_n_becomes_x(self):
        assert orf.translate("ATGNNNGCC") == "MXA"

    def test_trailing_partial_codon_ignored(self):
        assert orf.translate("ATGGCCA") == "MA"


class TestExtractORF:
    def test_finds_in_frame_stop(self):
        o = orf.extract_orf("AAATGGCCTAAGGG", 2)
        assert o.start_codon == "ATG"
        assert o.protein == "MA"
        assert o.stop_pos == 8
        assert o.has_stop

    def test_skips_out_of_frame_stop(self):
        # ATG CTA AGC TAA -- there is a TAA at index 4 which is out of frame
        # and must be ignored in favour of the in-frame one at index 9.
        o = orf.extract_orf("ATGCTAAGCTAA", 0)
        assert o.has_stop
        assert o.stop_pos == 9
        assert o.protein == "MLS"

    def test_runs_off_end(self):
        o = orf.extract_orf("ATGGCCGCC", 0)
        assert not o.has_stop
        assert o.stop_pos is None
        assert o.protein == "MAA"

    def test_microprotein_threshold(self):
        short = orf.extract_orf("ATG" + "GCC" * 50 + "TAA", 0)
        assert short.aa_len == 51
        assert short.is_microprotein
        long = orf.extract_orf("ATG" + "GCC" * 120 + "TAA", 0)
        assert not long.is_microprotein


class TestCoordinateConversion:
    """TIS Transformer reports TIS_pos 1-based; everything here is 0-based."""

    def test_one_based_to_zero_based(self):
        assert orf.tis_pos_to_zero_based(1) == 0
        assert orf.tis_pos_to_zero_based(60) == 59

    def test_round_trip_against_real_values(self):
        # Observed: INS TIS_pos 60 is the annotated CDS start at 0-based 59.
        assert orf.tis_pos_to_zero_based(60) == 59   # INS
        assert orf.tis_pos_to_zero_based(379) == 378  # TYK2
        assert orf.tis_pos_to_zero_based(85) == 84    # ACTB


class TestClassifyORF:
    # A transcript with CDS at [100, 400).
    CDS_START, CDS_END = 100, 400

    def _at(self, pos, stop=None):
        o = orf.extract_orf("ATG" + "GCC" * 10 + "TAA", 0)
        o.tis_pos = pos
        o.stop_pos = stop if stop is not None else pos + 30
        return o

    def test_canonical(self):
        assert orf.classify_orf(self._at(100), self.CDS_START, self.CDS_END) == "CDS"

    def test_uorf_entirely_upstream(self):
        assert orf.classify_orf(self._at(10, 40), self.CDS_START, self.CDS_END) == "uORF"

    def test_n_extension_is_in_frame_upstream(self):
        # 100 - 91 = 9, divisible by 3, and it overlaps the CDS.
        assert orf.classify_orf(self._at(91, 200), self.CDS_START, self.CDS_END) == "N-ext"

    def test_uoorf_is_out_of_frame_upstream_overlap(self):
        assert orf.classify_orf(self._at(92, 200), self.CDS_START, self.CDS_END) == "uoORF"

    def test_n_truncation_in_frame_inside_cds(self):
        assert orf.classify_orf(self._at(163), self.CDS_START, self.CDS_END) == "N-trunc"

    def test_internal_orf_out_of_frame(self):
        assert orf.classify_orf(self._at(164), self.CDS_START, self.CDS_END) == "intORF"

    def test_downstream_orf(self):
        assert orf.classify_orf(self._at(450), self.CDS_START, self.CDS_END) == "dORF"

    def test_unannotated_is_ncorf(self):
        assert orf.classify_orf(self._at(50), None, None) == "ncORF"


class TestTilePeptides:
    def test_lengths(self):
        peps = orf.tile_peptides("ACDEFGHIKLMNP", lengths=(9,))
        assert all(len(p) == 9 for p in peps)
        assert "ACDEFGHIK" in peps

    def test_too_short_yields_nothing(self):
        assert orf.tile_peptides("ACDEF", lengths=(9,)) == []

    def test_initiator_met_removal(self):
        # Without Met removal the 8mer starting at position 1 is unreachable.
        peps = orf.tile_peptides("MACDEFGHI", lengths=(8,), drop_initiator_met=True)
        assert "ACDEFGHI" in peps
        assert "MACDEFGH" in peps

    def test_drops_peptides_with_x(self):
        peps = orf.tile_peptides("ACDXFGHIKLMN", lengths=(9,))
        assert all("X" not in p for p in peps)


class TestHeaderAnnotation:
    def test_parses_real_header(self):
        header = (
            "INS_ENST00000381330.5 Homo sapiens insulin (INS), MANE Select, "
            "cDNA len=465nt 5UTR=59nt CDS=333nt 3UTR=73nt | Ensembl GRCh38"
        )
        start, end = orf.parse_cds_annotation(header)
        assert start == 59
        assert end == 59 + 333

    def test_missing_tags_returns_none(self):
        assert orf.parse_cds_annotation("just a name") == (None, None)


class TestFastaParsing:
    def test_name_is_first_token(self):
        recs, headers = pipeline.parse_fasta_with_headers(
            ">INS_ENST1 some description here 5UTR=10nt CDS=30nt\nATGGCC\n"
        )
        assert recs[0][0] == "INS_ENST1"
        assert "5UTR=10nt" in headers["INS_ENST1"]

    def test_bare_sequence_gets_a_header(self):
        recs, _ = pipeline.parse_fasta_with_headers("ATGGCCTAA")
        assert recs == [("query", "ATGGCCTAA")]

    def test_rna_normalised(self):
        recs, _ = pipeline.parse_fasta_with_headers(">x\nAUGGCC")
        assert recs[0][1] == "ATGGCC"

    def test_rejects_protein_sequence(self):
        try:
            pipeline.parse_fasta_with_headers(">x\nMAEEGFKL")
            assert False, "should have rejected amino acids"
        except ValueError as e:
            assert "unexpected characters" in str(e)


class TestNovelty:
    def test_self_peptide_flagged_not_novel(self):
        import pandas as pd

        canonical = {"tx": "MAEEGFKLDYVPT"}
        df = pd.DataFrame({"peptide": ["EEGFKLDY", "QQQWWWYY"]})
        out = pipeline.flag_peptide_novelty(df, canonical)
        assert out.loc[0, "novel"] is False or not out.loc[0, "novel"]
        assert out.loc[1, "novel"]

    def test_no_false_join_across_proteins(self):
        """A peptide spanning two concatenated proteins must not count as self."""
        import pandas as pd

        canonical = {"a": "MAEEG", "b": "FKLDY"}
        df = pd.DataFrame({"peptide": ["AEEGFKLD"]})
        out = pipeline.flag_peptide_novelty(df, canonical)
        assert out.loc[0, "novel"]


class TestCutoffSplit:
    """Stage 1 runs at the floor, so the cutoff is applied here instead."""

    @staticmethod
    def _table():
        import pandas as pd

        return pd.DataFrame({
            pipeline.TIS_SCORE_COL: [0.9, 0.03, 0.0037, 0.0008],
            "orf_type": ["CDS", "uORF", "N-trunc", "dORF"],
        })

    def test_cutoff_is_inclusive(self):
        above, below = pipeline.split_at_cutoff(self._table(), 0.03)
        assert list(above["orf_type"]) == ["CDS", "uORF"]

    def test_rejected_sites_come_back_ranked(self):
        _, below = pipeline.split_at_cutoff(self._table(), 0.03)
        assert list(below["orf_type"]) == ["N-trunc", "dORF"]

    def test_empty_input_gives_two_empty_frames(self):
        import pandas as pd

        above, below = pipeline.split_at_cutoff(pd.DataFrame(), 0.03)
        assert above.empty and below.empty