|
|
|
|
| """Functions for getting MSA and calculating alignment features.""" |
|
|
| from collections.abc import MutableMapping, Sequence |
| import string |
| from typing import Self |
|
|
| from absl import logging |
| from flax_model.alphafold3.constants import mmcif_names |
| from flax_model.alphafold3.data import msa_config |
| from flax_model.alphafold3.data import msa_features |
| from flax_model.alphafold3.data import parsers |
| from flax_model.alphafold3.data.tools import jackhmmer |
| from flax_model.alphafold3.data.tools import msa_tool |
| from flax_model.alphafold3.data.tools import nhmmer |
| from flax_model.alphafold3.data.tools import mmseqs |
| import numpy as np |
|
|
|
|
|
|
| class Error(Exception): |
| """Error indicatating a problem with MSA Search.""" |
|
|
|
|
| def _featurize(seq: str, chain_poly_type: str) -> str | list[int]: |
| if mmcif_names.is_standard_polymer_type(chain_poly_type): |
| featurized_seqs, _ = msa_features.extract_msa_features( |
| msa_sequences=[seq], chain_poly_type=chain_poly_type |
| ) |
| return featurized_seqs[0].tolist() |
| |
| return seq |
|
|
|
|
| def sequences_are_feature_equivalent( |
| sequence1: str, |
| sequence2: str, |
| chain_poly_type: str, |
| ) -> bool: |
| feat1 = _featurize(sequence1, chain_poly_type) |
| feat2 = _featurize(sequence2, chain_poly_type) |
| return feat1 == feat2 |
|
|
|
|
| class Msa: |
| """Multiple Sequence Alignment container with methods for manipulating it.""" |
|
|
| def __init__( |
| self, |
| query_sequence: str, |
| chain_poly_type: str, |
| sequences: Sequence[str], |
| descriptions: Sequence[str], |
| deduplicate: bool = True, |
| ): |
| """Raw constructor, prefer using the from_{a3m,multiple_msas} class methods. |
| |
| The first sequence must be equal (in featurised form) to the query sequence. |
| If sequences/descriptions are empty, they will be initialised to the query. |
| |
| Args: |
| query_sequence: The sequence that was used to search for MSA. |
| chain_poly_type: Polymer type of the query sequence, see mmcif_names. |
| sequences: The sequences returned by the MSA search tool. |
| descriptions: Metadata for the sequences returned by the MSA search tool. |
| deduplicate: If True, the MSA sequences will be deduplicated in the input |
| order. Lowercase letters (insertions) are ignored when deduplicating. |
| """ |
| if len(sequences) != len(descriptions): |
| raise ValueError('The number of sequences and descriptions must match.') |
|
|
| self.query_sequence = query_sequence |
| self.chain_poly_type = chain_poly_type |
|
|
| if not deduplicate: |
| self.sequences = sequences |
| self.descriptions = descriptions |
| else: |
| self.sequences = [] |
| self.descriptions = [] |
| |
| deletion_table = str.maketrans('', '', string.ascii_lowercase) |
| unique_sequences = set() |
| for seq, desc in zip(sequences, descriptions, strict=True): |
| |
| sequence_no_deletions = seq.translate(deletion_table) |
| if sequence_no_deletions not in unique_sequences: |
| unique_sequences.add(sequence_no_deletions) |
| self.sequences.append(seq) |
| self.descriptions.append(desc) |
|
|
| |
| self.sequences = self.sequences or [query_sequence] |
| self.descriptions = self.descriptions or ['Original query'] |
|
|
| |
| |
| if not sequences_are_feature_equivalent( |
| self.sequences[0], query_sequence, chain_poly_type |
| ): |
| raise ValueError( |
| f'First MSA sequence {self.sequences[0]} is not the {query_sequence=}' |
| ) |
|
|
| @classmethod |
| def from_multiple_msas( |
| cls, msas: Sequence[Self], deduplicate: bool = True |
| ) -> Self: |
| """Initializes the MSA from multiple MSAs. |
| |
| Args: |
| msas: A sequence of Msa objects representing individual MSAs produced by |
| different tools/dbs. |
| deduplicate: If True, the MSA sequences will be deduplicated in the input |
| order. Lowercase letters (insertions) are ignored when deduplicating. |
| |
| Returns: |
| An Msa object created by merging multiple MSAs. |
| """ |
| if not msas: |
| raise ValueError('At least one MSA must be provided.') |
|
|
| query_sequence = msas[0].query_sequence |
| chain_poly_type = msas[0].chain_poly_type |
| sequences = [] |
| descriptions = [] |
|
|
| for msa in msas: |
| if msa.query_sequence != query_sequence: |
| raise ValueError( |
| f'Query sequences must match: {[m.query_sequence for m in msas]}' |
| ) |
| if msa.chain_poly_type != chain_poly_type: |
| raise ValueError( |
| f'Chain poly types must match: {[m.chain_poly_type for m in msas]}' |
| ) |
| sequences.extend(msa.sequences) |
| descriptions.extend(msa.descriptions) |
|
|
| return cls( |
| query_sequence=query_sequence, |
| chain_poly_type=chain_poly_type, |
| sequences=sequences, |
| descriptions=descriptions, |
| deduplicate=deduplicate, |
| ) |
|
|
| @classmethod |
| def from_multiple_a3ms( |
| cls, a3ms: Sequence[str], chain_poly_type: str, deduplicate: bool = True |
| ) -> Self: |
| """Initializes the MSA from multiple A3M strings. |
| |
| Args: |
| a3ms: A sequence of A3M strings representing individual MSAs produced by |
| different tools/dbs. |
| chain_poly_type: Polymer type of the query sequence, see mmcif_names. |
| deduplicate: If True, the MSA sequences will be deduplicated in the input |
| order. Lowercase letters (insertions) are ignored when deduplicating. |
| |
| Returns: |
| An Msa object created by merging multiple A3Ms. |
| """ |
| if not a3ms: |
| raise ValueError('At least one A3M must be provided.') |
|
|
| query_sequence = None |
| all_sequences = [] |
| all_descriptions = [] |
|
|
| for a3m in a3ms: |
| sequences, descriptions = parsers.parse_fasta(a3m) |
| if query_sequence is None: |
| query_sequence = sequences[0] |
|
|
| if sequences[0] != query_sequence: |
| raise ValueError( |
| f'Query sequences must match: {sequences[0]=} != {query_sequence=}' |
| ) |
| all_sequences.extend(sequences) |
| all_descriptions.extend(descriptions) |
|
|
| return cls( |
| query_sequence=query_sequence, |
| chain_poly_type=chain_poly_type, |
| sequences=all_sequences, |
| descriptions=all_descriptions, |
| deduplicate=deduplicate, |
| ) |
|
|
| @classmethod |
| def from_a3m( |
| cls, |
| query_sequence: str, |
| chain_poly_type: str, |
| a3m: str, |
| max_depth: int | None = None, |
| deduplicate: bool = True, |
| ) -> Self: |
| """Parses the single A3M and builds the Msa object.""" |
| sequences, descriptions = parsers.parse_fasta(a3m) |
|
|
| if max_depth is not None and 0 < max_depth < len(sequences): |
| logging.info( |
| 'MSA cropped from depth of %d to %d for %s.', |
| len(sequences), |
| max_depth, |
| query_sequence, |
| ) |
| sequences = sequences[:max_depth] |
| descriptions = descriptions[:max_depth] |
|
|
| return cls( |
| query_sequence=query_sequence, |
| chain_poly_type=chain_poly_type, |
| sequences=sequences, |
| descriptions=descriptions, |
| deduplicate=deduplicate, |
| ) |
|
|
| @classmethod |
| def from_empty(cls, query_sequence: str, chain_poly_type: str) -> Self: |
| """Creates an empty Msa containing just the query sequence.""" |
| return cls( |
| query_sequence=query_sequence, |
| chain_poly_type=chain_poly_type, |
| sequences=[], |
| descriptions=[], |
| deduplicate=False, |
| ) |
|
|
| @property |
| def depth(self) -> int: |
| return len(self.sequences) |
|
|
| def __repr__(self) -> str: |
| return f'Msa({self.depth} sequences, {self.chain_poly_type})' |
|
|
| def to_a3m(self) -> str: |
| """Returns the MSA in the A3M format.""" |
| a3m_lines = [] |
| for desc, seq in zip(self.descriptions, self.sequences, strict=True): |
| a3m_lines.append(f'>{desc}') |
| a3m_lines.append(seq) |
| return '\n'.join(a3m_lines) + '\n' |
|
|
| def featurize(self) -> MutableMapping[str, np.ndarray]: |
| """Featurises the MSA and returns a map of feature names to features. |
| |
| Returns: |
| A dictionary mapping feature names to values. |
| |
| Raises: |
| msa.Error: |
| * If the sequences in the MSA don't have the same length after deletions |
| (lower case letters) are removed. |
| * If the MSA contains an unknown amino acid code. |
| * If there are no sequences after aligning. |
| """ |
| try: |
| msa, deletion_matrix = msa_features.extract_msa_features( |
| msa_sequences=self.sequences, chain_poly_type=self.chain_poly_type |
| ) |
| except ValueError as e: |
| raise Error(f'Error extracting MSA or deletion features: {e}') from e |
|
|
| if msa.shape == (0, 0): |
| raise Error(f'Empty MSA feature for {self}') |
|
|
| species_ids = msa_features.extract_species_ids(self.descriptions) |
|
|
| return { |
| 'msa_species_identifiers': np.array(species_ids, dtype=object), |
| 'num_alignments': np.array(self.depth, dtype=np.int32), |
| 'msa': msa, |
| 'deletion_matrix': deletion_matrix, |
| } |
|
|
|
|
| def get_msa_tool( |
| msa_tool_config: msa_config.JackhmmerConfig | msa_config.NhmmerConfig | msa_config.MmseqsConfig, |
| ) -> msa_tool.MsaTool: |
| """Returns the requested MSA tool.""" |
|
|
| match msa_tool_config: |
| case msa_config.JackhmmerConfig(): |
| return jackhmmer.Jackhmmer( |
| binary_path=msa_tool_config.binary_path, |
| database_path=msa_tool_config.database_config.path, |
| n_cpu=msa_tool_config.n_cpu, |
| n_iter=msa_tool_config.n_iter, |
| e_value=msa_tool_config.e_value, |
| z_value=msa_tool_config.z_value, |
| max_sequences=msa_tool_config.max_sequences, |
| max_threads=msa_tool_config.max_threads, |
| ) |
| case msa_config.NhmmerConfig(): |
| return nhmmer.Nhmmer( |
| binary_path=msa_tool_config.binary_path, |
| hmmalign_binary_path=msa_tool_config.hmmalign_binary_path, |
| hmmbuild_binary_path=msa_tool_config.hmmbuild_binary_path, |
| database_path=msa_tool_config.database_config.path, |
| n_cpu=msa_tool_config.n_cpu, |
| e_value=msa_tool_config.e_value, |
| max_sequences=msa_tool_config.max_sequences, |
| max_threads=msa_tool_config.max_threads, |
| alphabet=msa_tool_config.alphabet, |
| ) |
| case msa_config.MmseqsConfig(): |
| return mmseqs.Mmseqs( |
| binary_path=msa_tool_config.binary_path, |
| database_path=msa_tool_config.database_config.path, |
| n_cpu=msa_tool_config.n_cpu, |
| use_gpu=msa_tool_config.use_gpu, |
| mmseqs_options=msa_tool_config.mmseqs_options, |
| result2msa_options=msa_tool_config.result2msa_options, |
| ) |
| case _: |
| raise ValueError(f'Unknown MSA tool: {msa_tool_config}.') |
|
|
|
|
| def get_msa( |
| target_sequence: str, |
| run_config: msa_config.RunConfig, |
| chain_poly_type: str, |
| deduplicate: bool = False, |
| ) -> Msa: |
| """Computes the MSA for a given query sequence. |
| |
| Args: |
| target_sequence: The target amino-acid sequence. |
| run_config: MSA run configuration. |
| chain_poly_type: The type of chain for which to get an MSA. |
| deduplicate: If True, the MSA sequences will be deduplicated in the input |
| order. Lowercase letters (insertions) are ignored when deduplicating. |
| |
| Returns: |
| Aligned MSA sequences. |
| """ |
|
|
| return Msa.from_a3m( |
| query_sequence=target_sequence, |
| chain_poly_type=chain_poly_type, |
| a3m=get_msa_tool(run_config.config).query(target_sequence).a3m, |
| max_depth=run_config.crop_size, |
| deduplicate=deduplicate, |
| ) |
|
|