wuxing0105's picture
Add files using upload-large-folder tool
b7b760f verified
Raw
History Blame Contribute Delete
8.95 kB
"""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'