|
|
|
|
| """Functions for producing "paired" and "unpaired" MSA features for each chain. |
| |
| The paired MSA: |
| - Is made from the result of the all_seqs MSA query. |
| - Is ordered such that you can concatenate features across chains and related |
| sequences will end up on the same row. Related here means "from the same |
| species". Gaps are added to facilitate this whenever a sequence has no |
| suitable pair. |
| |
| The unpaired MSA: |
| - Is made from the results of the remaining MSA queries. |
| - Has no special ordering properties. |
| - Is deduplicated such that it doesn't contain any sequences in the paired MSA. |
| """ |
|
|
| from typing import Mapping, MutableMapping, Sequence |
| from flax_model.alphafold3.model import data_constants |
| import numpy as np |
|
|
|
|
| def _align_species( |
| all_species: Sequence[bytes], |
| chains_species_to_rows: Sequence[Mapping[bytes, np.ndarray]], |
| min_hits_per_species: Mapping[bytes, int], |
| ) -> np.ndarray: |
| """Aligns MSA row indices based on species. |
| |
| Within a species, MSAs are aligned based on their original order (the first |
| sequence for a species in the first chain's MSA is aligned to the first |
| sequence for the same species in the second chain's MSA). |
| |
| Args: |
| all_species: A list of all unique species identifiers. |
| chains_species_to_rows: A dictionary for each chain, that maps species to |
| the set of MSA row indices from that species in that chain. |
| min_hits_per_species: A mapping from species id, to the minimum MSA size |
| across chains for that species (ignoring chains with zero hits). |
| |
| Returns: |
| A matrix of size [num_msa_rows, num_chains], where the i,j element is an |
| index into the jth chains MSA. Each row consists of sequences from each |
| chain for the same species (or -1 if that chain has no sequences for that |
| species). |
| """ |
| |
| |
| |
| species_blocks = [] |
| for species in all_species: |
| chain_row_indices = [] |
| for species_to_rows in chains_species_to_rows: |
| min_msa_size = min_hits_per_species[species] |
| if species not in species_to_rows: |
| |
| |
| |
| row_indices = np.full(min_msa_size, fill_value=-1, dtype=np.int32) |
| else: |
| |
| row_indices = species_to_rows[species][:min_msa_size] |
| chain_row_indices.append(row_indices) |
| species_block = np.stack(chain_row_indices, axis=1) |
| species_blocks.append(species_block) |
| aligned_matrix = np.concatenate(species_blocks, axis=0) |
| return aligned_matrix |
|
|
|
|
| def create_paired_features( |
| chains: Sequence[MutableMapping[str, np.ndarray]], |
| max_paired_sequences: int, |
| nonempty_chain_ids: set[str], |
| max_hits_per_species: int, |
| ) -> Sequence[MutableMapping[str, np.ndarray]]: |
| """Creates per-chain MSA features where the MSAs have been aligned. |
| |
| Args: |
| chains: A list of feature dicts, one for each chain. |
| max_paired_sequences: No more than this many paired sequences will be |
| returned from this function. |
| nonempty_chain_ids: A set of chain ids (str) that are included in the crop |
| there is no reason to process chains not in this list. |
| max_hits_per_species: No more than this number of sequences will be returned |
| for a given species. |
| |
| Returns: |
| An updated feature dictionary for each chain, where the {}_all_seq features |
| have been aligned so that the nth row in chain 1 is aligned to the nth row |
| in chain 2's features. |
| """ |
| |
| |
| species_num_chains = {} |
|
|
| |
| |
| chains_species_to_rows = [] |
|
|
| |
| min_hits_per_species = {} |
|
|
| for chain in chains: |
| species_ids = chain['msa_species_identifiers_all_seq'] |
|
|
| |
| if ( |
| species_ids.size == 0 |
| or (species_ids.size == 1 and not species_ids[0]) |
| or chain['chain_id'] not in nonempty_chain_ids |
| ): |
| chains_species_to_rows.append({}) |
| continue |
|
|
| |
| |
| row_indices = np.arange(len(species_ids)) |
| |
| |
| sort_idxs = species_ids.argsort() |
| species_ids = species_ids[sort_idxs] |
| row_indices = row_indices[sort_idxs] |
|
|
| species, unique_row_indices = np.unique(species_ids, return_index=True) |
| grouped_row_indices = np.split(row_indices, unique_row_indices[1:]) |
| species_to_rows = dict(zip(species, grouped_row_indices, strict=True)) |
| chains_species_to_rows.append(species_to_rows) |
|
|
| for s in species: |
| species_num_chains[s] = species_num_chains.get(s, 0) + 1 |
|
|
| for species, row_indices in species_to_rows.items(): |
| min_hits_per_species[species] = min( |
| min_hits_per_species.get(species, max_hits_per_species), |
| len(row_indices), |
| ) |
|
|
| |
| |
| num_chains_to_species = {} |
| for species, num_chains in species_num_chains.items(): |
| if not species or num_chains <= 1: |
| continue |
| if num_chains not in num_chains_to_species: |
| num_chains_to_species[num_chains] = [] |
| num_chains_to_species[num_chains].append(species) |
|
|
| num_rows_seen = 0 |
| |
| all_rows = [np.array([[0] * len(chains)], dtype=np.int32)] |
|
|
| |
| for num_chains in sorted(num_chains_to_species, reverse=True): |
| all_species = num_chains_to_species[num_chains] |
|
|
| |
| |
| rows = _align_species( |
| all_species, chains_species_to_rows, min_hits_per_species |
| ) |
| |
| |
| rank_metric = np.abs(np.prod(rows.astype(np.float32), axis=1)) |
| sorted_rows = rows[np.argsort(rank_metric), :] |
| all_rows.append(sorted_rows) |
| num_rows_seen += rows.shape[0] |
| if num_rows_seen >= max_paired_sequences: |
| break |
|
|
| all_rows = np.concatenate(all_rows, axis=0) |
| all_rows = all_rows[:max_paired_sequences, :] |
|
|
| |
| |
| paired_chains = [] |
| for chain_idx, chain in enumerate(chains): |
| out_chain = {k: v for k, v in chain.items() if 'all_seq' not in k} |
| selected_row_indices = all_rows[:, chain_idx] |
| for feat_name in {'msa', 'deletion_matrix'}: |
| all_seq_name = f'{feat_name}_all_seq' |
| feat_value = chain[all_seq_name] |
|
|
| |
| |
| |
| |
| pad_value = data_constants.MSA_PAD_VALUES[feat_name] |
| feat_value = np.concatenate([ |
| feat_value, |
| np.full((1, feat_value.shape[1]), pad_value, feat_value.dtype), |
| ]) |
|
|
| feat_value = feat_value[selected_row_indices, :] |
| out_chain[all_seq_name] = feat_value |
| out_chain['num_alignments_all_seq'] = np.array( |
| out_chain['msa_all_seq'].shape[0] |
| ) |
| paired_chains.append(out_chain) |
| return paired_chains |
|
|
|
|
| def deduplicate_unpaired_sequences( |
| np_chains: Sequence[MutableMapping[str, np.ndarray]], |
| ) -> Sequence[MutableMapping[str, np.ndarray]]: |
| """Deduplicates unpaired sequences based on paired sequences.""" |
|
|
| feature_names = np_chains[0].keys() |
| msa_features = ( |
| data_constants.NUM_SEQ_MSA_FEATURES |
| + data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES |
| ) |
|
|
| for chain in np_chains: |
| sequence_set = set( |
| hash(s.data.tobytes()) for s in chain['msa_all_seq'].astype(np.int8) |
| ) |
| keep_rows = [] |
| |
| |
| for row_num, seq in enumerate(chain['msa'].astype(np.int8)): |
| if hash(seq.data.tobytes()) not in sequence_set: |
| keep_rows.append(row_num) |
| for feature_name in feature_names: |
| if feature_name in msa_features: |
| chain[feature_name] = chain[feature_name][keep_rows] |
| chain['num_alignments'] = np.array(chain['msa'].shape[0], dtype=np.int32) |
| return np_chains |
|
|
|
|
| def choose_paired_unpaired_msa_crop_sizes( |
| unpaired_msa: np.ndarray, |
| paired_msa: np.ndarray | None, |
| total_msa_crop_size: int, |
| max_paired_sequences: int, |
| ) -> tuple[int, int | None]: |
| """Returns the sizes of the MSA crop and MSA_all_seq crop. |
| |
| NOTE: Unpaired + paired MSA sizes can exceed total_msa_size when |
| there are lots of gapped rows. Through the pairing logic another chain(s) |
| will have fewer than total_msa_size. |
| |
| Args: |
| unpaired_msa: The unpaired MSA array (not all_seq). |
| paired_msa: The paired MSA array (all_seq). |
| total_msa_crop_size: The maximum total number of sequences to crop to. |
| max_paired_sequences: The maximum number of sequences that can come from |
| MSA pairing. |
| |
| Returns: |
| A tuple of: |
| The size of the reduced MSA crop (not all_seq features). |
| The size of the unreduced MSA crop (for all_seq features) or None, if |
| paired_msa is None. |
| """ |
| if paired_msa is not None: |
| paired_crop_size = np.minimum(paired_msa.shape[0], max_paired_sequences) |
|
|
| |
| |
| |
| cropped_all_seq_msa = paired_msa[:max_paired_sequences] |
| num_non_gapped_pairs = cropped_all_seq_msa.shape[0] |
|
|
| assert num_non_gapped_pairs <= max_paired_sequences |
| unpaired_crop_size = np.minimum( |
| unpaired_msa.shape[0], total_msa_crop_size - num_non_gapped_pairs |
| ) |
| assert unpaired_crop_size >= 0 |
| else: |
| unpaired_crop_size = np.minimum(unpaired_msa.shape[0], total_msa_crop_size) |
| paired_crop_size = None |
| return unpaired_crop_size, paired_crop_size |
|
|
|
|
| def remove_all_gapped_rows_from_all_seqs( |
| chains_list: Sequence[dict[str, np.ndarray]], asym_ids: Sequence[float] |
| ) -> Sequence[dict[str, np.ndarray]]: |
| """Removes all gapped rows from all_seq feat based on selected asym_ids.""" |
|
|
| merged_msa_all_seq = np.concatenate( |
| [ |
| chain['msa_all_seq'] |
| for chain in chains_list |
| if chain['asym_id'][0] in asym_ids |
| ], |
| axis=1, |
| ) |
|
|
| non_gapped_keep_rows = np.any( |
| merged_msa_all_seq != data_constants.MSA_GAP_IDX, axis=1 |
| ) |
| for chain in chains_list: |
| for feat_name in list(chains_list)[0]: |
| if '_all_seq' in feat_name: |
| feat_name_split = feat_name.split('_all_seq')[0] |
| if feat_name_split in ( |
| data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES |
| + data_constants.NUM_SEQ_MSA_FEATURES |
| ): |
| |
| |
| chain[feat_name] = chain[feat_name][non_gapped_keep_rows] |
| chain['num_alignments_all_seq'] = np.sum(non_gapped_keep_rows) |
| return chains_list |
|
|