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' |