File size: 8,946 Bytes
b7b760f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261


"""Functions for parsing various file formats."""

from collections.abc import Iterable, Sequence
from typing import IO, TypeAlias

from flax_model.alphafold3.cpp import fasta_iterator
from flax_model.alphafold3.cpp import msa_conversion


DeletionMatrix: TypeAlias = Sequence[Sequence[int]]


def lazy_parse_fasta_string(fasta_string: str) -> Iterable[tuple[str, str]]:
  """Lazily parses a FASTA/A3M string and yields (sequence, description) tuples.

  This implementation is more memory friendly than `fasta_sequence` while
  offering comparable performance. The underlying implementation is in C++ and
  is therefore faster than a pure Python implementation.

  Use this method when parsing FASTA files where you already have the FASTA
  string, but need to control how far you iterate through its sequences.

  Arguments:
    fasta_string: A string with the contents of FASTA/A3M file.

  Returns:
    Iterator of (sequence, description). In the description, the leading ">" is
    stripped.

  Raises:
    ValueError if the FASTA/A3M file is invalid, e.g. empty.
  """

  # The lifetime of the FastaStringIterator is tied to the lifetime of
  # fasta_string - fasta_string must be kept while the iterator is in use.
  return fasta_iterator.FastaStringIterator(fasta_string)


def parse_fasta(fasta_string: str) -> tuple[Sequence[str], Sequence[str]]:
  """Parses FASTA string and returns list of strings with amino-acid sequences.

  Arguments:
    fasta_string: The string contents of a FASTA file.

  Returns:
    A tuple of two lists:
    * A list of sequences.
    * A list of sequence descriptions taken from the comment lines. In the
      same order as the sequences.
  """
  return fasta_iterator.parse_fasta_include_descriptions(fasta_string)


def convert_a3m_to_stockholm(a3m: str, max_seqs: int | None = None) -> str:
  """Converts MSA in the A3M format to the Stockholm format."""
  sequences, descriptions = parse_fasta(a3m)
  if max_seqs is not None:
    sequences = sequences[:max_seqs]
    descriptions = descriptions[:max_seqs]

  stockholm = ['# STOCKHOLM 1.0', '']

  # Add the Stockholm header with the sequence metadata.
  names = []
  for i, description in enumerate(descriptions):
    name, _, rest = description.replace('\t', ' ').partition(' ')
    # Ensure that the names are unique - stockholm format requires that
    # the sequence names are unique.
    name = f'{name}_{i}'
    names.append(name)
    # Avoid zero-length description due to historic hmmbuild parsing bug.
    desc = rest.strip() or '<EMPTY>'
    stockholm.append(f'#=GS {name.strip()} DE {desc}')
  stockholm.append('')

  # Convert insertions in a sequence into gaps in all other sequences that don't
  # have an insertion in that column as well.
  sequences = msa_conversion.convert_a3m_to_stockholm(sequences)

  # Add the MSA data.
  max_name_width = max(len(name) for name in names)
  for name, sequence in zip(names, sequences, strict=True):
    # Align the names to the left and pad with spaces to the maximum length.
    stockholm.append(f'{name:<{max_name_width}s} {sequence}')

  # Add the reference annotation for the query (the first sequence).
  ref_annotation = ''.join('.' if c == '-' else 'x' for c in sequences[0])
  stockholm.append(f'{"#=GC RF":<{max_name_width}s} {ref_annotation}')
  stockholm.append('//')

  return '\n'.join(stockholm)


def convert_stockholm_to_a3m(
    stockholm: IO[str],
    max_sequences: int | None = None,
    remove_first_row_gaps: bool = True,
    linewidth: int | None = None,
) -> str:
  """Converts MSA in Stockholm format to the A3M format."""
  descriptions = {}
  sequences = {}
  reached_max_sequences = False

  if linewidth is not None and linewidth <= 0:
    raise ValueError('linewidth must be > 0 or None')

  for line in stockholm:
    reached_max_sequences = max_sequences and len(sequences) >= max_sequences
    line = line.strip()
    # Ignore blank lines, markup and end symbols - remainder are alignment
    # sequence parts.
    if not line or line.startswith(('#', '//')):
      continue
    seqname, aligned_seq = line.split(maxsplit=1)
    if seqname not in sequences:
      if reached_max_sequences:
        continue
      sequences[seqname] = ''
    sequences[seqname] += aligned_seq

  if not sequences:
    return ''

  stockholm.seek(0)
  for line in stockholm:
    line = line.strip()
    if line[:4] == '#=GS':
      # Description row - example format is:
      # #=GS UniRef90_Q9H5Z4/4-78            DE [subseq from] cDNA: FLJ22755 ...
      columns = line.split(maxsplit=3)
      seqname, feature = columns[1:3]
      value = columns[3] if len(columns) == 4 else ''
      if feature != 'DE':
        continue
      if reached_max_sequences and seqname not in sequences:
        continue
      descriptions[seqname] = value
      if len(descriptions) == len(sequences):
        break

  assert len(descriptions) <= len(sequences)

  # Convert sto format to a3m line by line
  a3m_sequences = {}
  # query_sequence is assumed to be the first sequence
  query_sequence = next(iter(sequences.values()))
  for seqname, sto_sequence in sequences.items():
    if remove_first_row_gaps:
      a3m_sequences[seqname] = msa_conversion.align_sequence_to_gapless_query(
          sequence=sto_sequence, query_sequence=query_sequence
      ).replace('.', '')
    else:
      a3m_sequences[seqname] = sto_sequence.replace('.', '')

  fasta_chunks = []

  for seqname, seq in a3m_sequences.items():
    fasta_chunks.append(f'>{seqname} {descriptions.get(seqname, "")}')

    if linewidth:
      fasta_chunks.extend(
          seq[i : linewidth + i] for i in range(0, len(seq), linewidth)
      )
    else:
      fasta_chunks.append(seq)

  return '\n'.join(fasta_chunks) + '\n'  # Include terminating newline.


def convert_mmseqs_stockholm_to_a3m(
    stockholm: IO[str],
    max_sequences: int | None = None,
    remove_first_row_gaps: bool = True,
    linewidth: int | None = None,
) -> str:
    """Converts MSA in Stockholm format to the A3M format."""
    from collections import defaultdict

    descriptions = {}
    sequences = {}  # 存储最终序列(自动处理重复键)
    seqname_counter = defaultdict(int)  # 记录原始seqname出现次数
    original_seqnames = {}  # 记录处理后的seqname对应的原始名�?

    if linewidth is not None and linewidth <= 0:
        raise ValueError('linewidth must be > 0 or None')

    # 第一遍:读取所有序列行,处理重复键
    for line in stockholm:
        line = line.strip()
        if not line or line.startswith(('#', '//')):
            continue
        parts = line.split(maxsplit=1)
        if len(parts) < 2:
            continue
        original_seqname, aligned_seq = parts

        # 生成唯一seqname(例�?"_1", "_1_a", "_1_b"�?
        count = seqname_counter[original_seqname]
        if count == 0:
            new_seqname = original_seqname
        else:
            new_seqname = f"{original_seqname}_{chr(97 + count - 1)}"  # 97是ASCII码的'a'
        seqname_counter[original_seqname] += 1

        # 达到最大序列数时跳过后续序�?
        if max_sequences and len(sequences) >= max_sequences:
            continue

        # 累积序列(原逻辑�?
        sequences.setdefault(new_seqname, '')
        sequences[new_seqname] += aligned_seq
        original_seqnames[new_seqname] = original_seqname

    if not sequences:
        return ''

    # 第二遍:读取描述信息
    stockholm.seek(0)
    for line in stockholm:
        line = line.strip()
        if line.startswith('#=GS'):
            columns = line.split(maxsplit=3)
            if len(columns) < 4:
                continue
            seqname, feature = columns[1:3]
            if feature != 'DE':
                continue
            value = columns[3] if len(columns) >= 4 else ''
            descriptions[seqname] = value

    # 转换序列格式
    a3m_sequences = {}
    query_sequence = next(iter(sequences.values())).replace('.', '')  # 假设第一个序列是查询序列

    for seqname, sto_sequence in sequences.items():
        current_seq = sto_sequence.replace('.', '')
        if remove_first_row_gaps:
            aligned = msa_conversion.align_sequence_to_gapless_query(
                sequence=current_seq, query_sequence=query_sequence
            ).replace('.', '')
        else:
            aligned = current_seq
        a3m_sequences[seqname] = aligned

    # 生成FASTA
    fasta_chunks = []
    for seqname, seq in a3m_sequences.items():
        original_seqname = original_seqnames.get(seqname, seqname)
        desc = descriptions.get(original_seqname, "")
        fasta_chunks.append(f'>{seqname} {desc}')

        if linewidth:
            chunks = [seq[i:i+linewidth] for i in range(0, len(seq), linewidth)]
            fasta_chunks.extend(chunks)
        else:
            fasta_chunks.append(seq)

    return '\n'.join(fasta_chunks) + '\n'