File size: 6,954 Bytes
b7731cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
218
219
220
221
222
223
224
# Copyright 2004 by James Casbon.  All rights reserved.
#
# This file is part of the Biopython distribution and governed by your
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
# Please see the LICENSE file that should have been included as part of this
# package.

"""Code to deal with COMPASS output, a program for profile/profile comparison.

Compass is described in:

Sadreyev R, Grishin N. COMPASS: a tool for comparison of multiple protein
alignments with assessment of statistical significance. J Mol Biol. 2003 Feb
7;326(1):317-36.

Tested with COMPASS 1.24.
"""

import re


def read(handle):
    """Read a COMPASS file containing one COMPASS record."""
    record = None
    try:
        line = next(handle)
        record = Record()
        __read_names(record, line)
        line = next(handle)
        __read_threshold(record, line)
        line = next(handle)
        __read_lengths(record, line)
        line = next(handle)
        __read_profilewidth(record, line)
        line = next(handle)
        __read_scores(record, line)
    except StopIteration:
        if not record:
            raise ValueError("No record found in handle") from None
        else:
            raise ValueError("Unexpected end of stream.") from None
    for line in handle:
        if not line.strip():  # skip empty lines
            continue
        __read_query_alignment(record, line)
        try:
            line = next(handle)
            __read_positive_alignment(record, line)
            line = next(handle)
            __read_hit_alignment(record, line)
        except StopIteration:
            raise ValueError("Unexpected end of stream.") from None
    return record


def parse(handle):
    """Iterate over records in a COMPASS file."""
    record = None
    try:
        line = next(handle)
    except StopIteration:
        return
    while True:
        try:
            record = Record()
            __read_names(record, line)
            line = next(handle)
            __read_threshold(record, line)
            line = next(handle)
            __read_lengths(record, line)
            line = next(handle)
            __read_profilewidth(record, line)
            line = next(handle)
            __read_scores(record, line)
        except StopIteration:
            raise ValueError("Unexpected end of stream.") from None
        for line in handle:
            if not line.strip():
                continue
            if "Ali1:" in line:
                yield record
                break
            __read_query_alignment(record, line)
            try:
                line = next(handle)
                __read_positive_alignment(record, line)
                line = next(handle)
                __read_hit_alignment(record, line)
            except StopIteration:
                raise ValueError("Unexpected end of stream.") from None
        else:
            yield record
            break


class Record:
    """Hold information from one compass hit.

    Ali1 is the query, Ali2 the hit.
    """

    def __init__(self):
        """Initialize the class."""
        self.query = ""
        self.hit = ""
        self.gap_threshold = 0
        self.query_length = 0
        self.query_filtered_length = 0
        self.query_nseqs = 0
        self.query_neffseqs = 0
        self.hit_length = 0
        self.hit_filtered_length = 0
        self.hit_nseqs = 0
        self.hit_neffseqs = 0
        self.sw_score = 0
        self.evalue = -1
        self.query_start = -1
        self.hit_start = -1
        self.query_aln = ""
        self.hit_aln = ""
        self.positives = ""

    def query_coverage(self):
        """Return the length of the query covered in the alignment."""
        s = self.query_aln.replace("=", "")
        return len(s)

    def hit_coverage(self):
        """Return the length of the hit covered in the alignment."""
        s = self.hit_aln.replace("=", "")
        return len(s)


# Everything below is private

__regex = {
    "names": re.compile(r"Ali1:\s+(\S+)\s+Ali2:\s+(\S+)\s+"),
    "threshold": re.compile(r"Threshold of effective gap content in columns: (\S+)"),
    "lengths": re.compile(
        r"length1=(\S+)\s+filtered_length1=(\S+)"
        r"\s+length2=(\S+)\s+filtered_length2=(\S+)"
    ),
    "profilewidth": re.compile(
        r"Nseqs1=(\S+)\s+Neff1=(\S+)\s+Nseqs2=(\S+)\s+Neff2=(\S+)"
    ),
    "scores": re.compile(r"Smith-Waterman score = (\S+)\s+Evalue = (\S+)"),
    "start": re.compile(r"(\d+)"),
    "align": re.compile(r"^.{15}(\S+)"),
    "positive_alignment": re.compile(r"^.{15}(.+)"),
}


def __read_names(record, line):
    # Ali1: 60456.blo.gz.aln  Ali2: allscop//14984.blo.gz.aln
    #       ------query-----        -------hit-------------
    if "Ali1:" not in line:
        raise ValueError(f"Line does not contain 'Ali1:':\n{line}")
    m = __regex["names"].search(line)
    record.query = m.group(1)
    record.hit = m.group(2)


def __read_threshold(record, line):
    if not line.startswith("Threshold"):
        raise ValueError(f"Line does not start with 'Threshold':\n{line}")
    m = __regex["threshold"].search(line)
    record.gap_threshold = float(m.group(1))


def __read_lengths(record, line):
    if not line.startswith("length1="):
        raise ValueError(f"Line does not start with 'length1=':\n{line}")
    m = __regex["lengths"].search(line)
    record.query_length = int(m.group(1))
    record.query_filtered_length = float(m.group(2))
    record.hit_length = int(m.group(3))
    record.hit_filtered_length = float(m.group(4))


def __read_profilewidth(record, line):
    if "Nseqs1" not in line:
        raise ValueError(f"Line does not contain 'Nseqs1':\n{line}")
    m = __regex["profilewidth"].search(line)
    record.query_nseqs = int(m.group(1))
    record.query_neffseqs = float(m.group(2))
    record.hit_nseqs = int(m.group(3))
    record.hit_neffseqs = float(m.group(4))


def __read_scores(record, line):
    if not line.startswith("Smith-Waterman"):
        raise ValueError(f"Line does not start with 'Smith-Waterman':\n{line}")
    m = __regex["scores"].search(line)
    if m:
        record.sw_score = int(m.group(1))
        record.evalue = float(m.group(2))
    else:
        record.sw_score = 0
        record.evalue = -1.0


def __read_query_alignment(record, line):
    m = __regex["start"].search(line)
    if m:
        record.query_start = int(m.group(1))
    m = __regex["align"].match(line)
    assert m is not None, "invalid match"
    record.query_aln += m.group(1)


def __read_positive_alignment(record, line):
    m = __regex["positive_alignment"].match(line)
    assert m is not None, "invalid match"
    record.positives += m.group(1)


def __read_hit_alignment(record, line):
    m = __regex["start"].search(line)
    if m:
        record.hit_start = int(m.group(1))
    m = __regex["align"].match(line)
    assert m is not None, "invalid match"
    record.hit_aln += m.group(1)