| |
|
|
| import argparse |
| from Bio import SeqIO |
| import numpy as np |
| import pandas as pd |
| import scipy.stats as ss |
| import torch |
| from tqdm import tqdm |
|
|
| from model.utils.other_utils import read_msa, greedy_select, msa_splicer, AAs |
|
|
| def zero_shot_esm_dms(wt_seq, |
| model_locations = ['esm1v_t33_650M_UR90S_1', |
| 'esm1v_t33_650M_UR90S_2', |
| 'esm1v_t33_650M_UR90S_3', |
| 'esm1v_t33_650M_UR90S_4', |
| 'esm1v_t33_650M_UR90S_5', |
| 'esm2_t36_3B_UR50D'], |
| scoring_strategy='wt-marginals', |
| num_msa_seqs=400, |
| **kwargs): |
| """ |
| Perform deep mutational scanning using ESM model. |
| |
| Args: |
| wt_seq (str): Wild-type protein sequence |
| scoring_strategy (str): 'wt-marginals' or 'masked-marginals' |
| **kwargs: Additional arguments |
| |
| Returns: |
| pandas.DataFrame: DataFrame containing mutation scores and statistics |
| """ |
| from esm import pretrained, MSATransformer |
| |
| |
| amino_acids = AAs[:-1] |
| mutations = [] |
| for i, residue in enumerate(wt_seq): |
| for aa in amino_acids: |
| if wt_seq[i] == aa: |
| continue |
| mutations.append(wt_seq[i] + str(i + 1) + aa) |
|
|
| |
| model_probs = [] |
|
|
| if torch.backends.mps.is_available(): |
| device = "mps" |
| elif torch.cuda.is_available(): |
| device = "cuda:0" |
| else: |
| device = "cpu" |
|
|
| for model_location in model_locations: |
| model, alphabet = pretrained.load_model_and_alphabet(model_location) |
| model.eval() |
| model = model.to(device) |
|
|
| batch_converter = alphabet.get_batch_converter() |
|
|
| if isinstance(model, MSATransformer): |
|
|
| assert kwargs['msa_file'] is not None, 'No MSA file provided.' |
| msa = read_msa(kwargs['msa_file']) |
|
|
| |
| inputs = greedy_select(msa, num_seqs=num_msa_seqs) |
| |
| |
| data = [msa_splicer(inputs)] |
|
|
| |
| _, __, batch_tokens = batch_converter(data) |
| all_token_probs = [] |
| for i in tqdm(range(batch_tokens.size(2))): |
| batch_tokens_masked = batch_tokens.clone() |
| batch_tokens_masked[0, 0, i] = alphabet.mask_idx |
| with torch.no_grad(): |
| token_probs = torch.log_softmax( |
| model(batch_tokens_masked.to(device))["logits"], dim=-1 |
| ) |
| all_token_probs.append(token_probs[:, 0, i]) |
| token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0) |
|
|
| else: |
| data = [ |
| ('protein1', wt_seq), |
| ] |
| batch_labels, batch_strs, batch_tokens = batch_converter(data) |
|
|
| if scoring_strategy == 'wt-marginals': |
| with torch.no_grad(): |
| token_probs = torch.log_softmax(model(batch_tokens.to(device))['logits'], dim=-1) |
|
|
| elif scoring_strategy == 'masked-marginals': |
| all_token_probs = [] |
| for i in tqdm(range(batch_tokens.size(1))): |
| batch_tokens_masked = batch_tokens.clone() |
| batch_tokens_masked[0, i] = alphabet.mask_idx |
| with torch.no_grad(): |
| token_probs = torch.log_softmax( |
| model(batch_tokens_masked.to(device))['logits'], dim=-1 |
| ) |
| all_token_probs.append(token_probs[:, i]) |
| token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0) |
|
|
| else: |
| raise ValueError(f'Invalid scoring strategy {scoring_strategy}') |
| |
| model_probs.append(token_probs.cpu().numpy()[0]) |
| |
| X = [] |
| for model_prob in model_probs: |
|
|
| X_sub = [] |
| for mutation in mutations: |
| wt, idx, mt = mutation[0], int(mutation[1:-1])-1, mutation[-1] |
| assert wt_seq[idx] == wt, 'Wild-type residue does not match input sequence.' |
| |
| wt_encoded, mt_encoded = alphabet.tok_to_idx[wt], alphabet.tok_to_idx[mt] |
|
|
| score = model_prob[idx + 1, mt_encoded] - model_prob[idx + 1, wt_encoded] |
| if not np.isfinite(score): |
| score = 0. |
| |
| X_sub.append(score) |
|
|
| X.append(X_sub) |
| |
| |
| data = {'mutations': mutations} |
| for i in range(len(X)): |
| data[f'model_{i+1}_logratio'] = X[i] |
| df = pd.DataFrame(data) |
|
|
| |
| logratio_cols = [f'model_{i+1}_logratio' for i in range(len(X))] |
| df['average_model_logratio'] = df[logratio_cols].mean(axis=1) |
|
|
| |
| for i in range(len(X)): |
| df[f'model_{i+1}_pass'] = df[f'model_{i+1}_logratio'].apply(lambda x: 1 if x > 0 else 0) |
|
|
| |
| pass_cols = [f'model_{i+1}_pass' for i in range(len(X))] |
| df['total_model_pass'] = df[pass_cols].sum(axis=1) |
| df.sort_values(by='average_model_logratio', ascending=False, inplace=True) |
| df.sort_values(by='total_model_pass', ascending=False, inplace=True) |
|
|
| df_ls = [] |
|
|
| |
|
|
| total_model_pass_list = list(set(df['total_model_pass'].values)) |
| total_model_pass_list = total_model_pass_list[::-1] |
|
|
| for model_pass_value in total_model_pass_list: |
| subset = df[df['total_model_pass'] == model_pass_value].copy() |
| subset.sort_values(by='average_model_logratio', ascending=False, inplace=True) |
| df_ls.append(subset) |
|
|
| df_sorted = pd.concat(df_ls) |
|
|
| return df_sorted |
|
|
| def zero_shot_esm_if_dms(wt_seq, pdb_file, chain_id = 'A', scoring_strategy='wt-marginals', **kwargs): |
| """ |
| Perform deep mutational scanning using ESM-IF (Inverse Folding) model. |
| |
| Args: |
| wt_seq (str): Wild-type protein sequence |
| pdb_file (str): Path to PDB file |
| chain_id (str): Chain ID in the PDB file |
| scoring_strategy (str): Currently not used, kept for consistency |
| **kwargs: Additional arguments |
| |
| Returns: |
| pandas.DataFrame: DataFrame containing mutation scores |
| """ |
| import torch_geometric |
| import torch_sparse |
| from torch_geometric.nn import MessagePassing |
| import esm |
| from esm import pretrained |
| from esm.inverse_folding.util import CoordBatchConverter |
|
|
| amino_acids = AAs[:-1] |
| mutations = [] |
| for i, residue in enumerate(wt_seq): |
| for aa in amino_acids: |
| if wt_seq[i] == aa: |
| continue |
| mutations.append(wt_seq[i] + str(i + 1) + aa) |
|
|
| model_locations = ['esm_if1_gvp4_t16_142M_UR50'] |
|
|
| model, alphabet = pretrained.load_model_and_alphabet(model_locations[0]) |
| model = model.eval() |
|
|
| structure = esm.inverse_folding.util.load_structure(pdb_file, chain_id) |
| coords, native_seq = esm.inverse_folding.util.extract_coords_from_structure(structure) |
| |
| if native_seq == wt_seq: |
| print(f"Native sequence from structure matches input sequence ({len(native_seq)} residues)") |
| else: |
| print(f"Warning: Native sequence from structure ({len(native_seq)} residues) does not match input sequence ({len(wt_seq)} residues)") |
|
|
| device = next(model.parameters()).device |
| batch_converter = CoordBatchConverter(alphabet) |
| batch = [(coords, None, wt_seq)] |
| coords, confidence, strs, tokens, padding_mask = batch_converter( |
| batch, device=device) |
|
|
| prev_output_tokens = tokens[:, :-1].to(device) |
| target = tokens[:, 1:] |
| logits, _ = model.forward(coords, padding_mask, confidence, prev_output_tokens) |
|
|
| |
|
|
| scores = logits.detach().numpy()[0] |
| mutation_score = {} |
| for pos in range(len(wt_seq)): |
| wt = wt_seq[pos] |
| for mt in alphabet.all_toks: |
| mutation = f'{wt}{pos + 1}{mt}' |
| mutation_score[mutation] = scores[alphabet.tok_to_idx[mt], pos] |
|
|
| X = [] |
| for mutation in mutations: |
| wt = mutation[0]+mutation[1:-1]+mutation[0] |
| score = mutation_score[mutation] - mutation_score[wt] |
| if not np.isfinite(score): |
| score = 0. |
| |
| X.append(score) |
|
|
| df = pd.DataFrame({'mutations': mutations, 'logratio': X}) |
|
|
| return df |
|
|
| def zero_shot_esm( |
| mutations, |
| model_locations, |
| sequence, |
| scoring_strategy='wt-marginals', |
| **kwargs |
| ): |
| """ |
| Perform zero-shot prediction using ESM (Evolutionary Scale Modeling) model. |
| |
| Args: |
| mutations (list): List of mutation sets |
| model_locations (list): List of ESM model file paths |
| sequence (str): Original protein sequence |
| scoring_strategy (str): 'wt-marginals' or 'masked-marginals' |
| **kwargs: Additional arguments (e.g., device) |
| |
| Returns: |
| numpy.ndarray: Array of mutation scores |
| """ |
| from esm import pretrained |
|
|
| |
|
|
| model_probs = [] |
| |
| for model_location in model_locations: |
| model, alphabet = pretrained.load_model_and_alphabet(model_location) |
| model.eval() |
| model = model.to(kwargs['device']) |
|
|
| batch_converter = alphabet.get_batch_converter() |
|
|
| data = [ |
| ('protein1', sequence), |
| ] |
| batch_labels, batch_strs, batch_tokens = batch_converter(data) |
|
|
| if scoring_strategy == 'wt-marginals': |
| with torch.no_grad(): |
| token_probs = torch.log_softmax(model(batch_tokens.to(kwargs['device']))['logits'], dim=-1) |
|
|
| elif scoring_strategy == 'masked-marginals': |
| all_token_probs = [] |
| for i in tqdm(range(batch_tokens.size(1))): |
| batch_tokens_masked = batch_tokens.clone() |
| batch_tokens_masked[0, i] = alphabet.mask_idx |
| with torch.no_grad(): |
| token_probs = torch.log_softmax( |
| model(batch_tokens_masked.to(kwargs['device']))['logits'], dim=-1 |
| ) |
| all_token_probs.append(token_probs[:, i]) |
| token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0) |
|
|
| else: |
| raise ValueError(f'Invalid scoring strategy {scoring_strategy}') |
| |
| model_probs.append(token_probs.cpu().numpy()[0]) |
|
|
| |
| |
| scores = np.sum(model_probs, axis=0) |
| mutation_score = {} |
| for pos in range(len(sequence)): |
| wt = sequence[pos] |
| for mt in alphabet.all_toks: |
| mutation = f'{wt}{pos + 1}{mt}' |
| mutation_score[mutation] = scores[pos + 1, alphabet.tok_to_idx[mt]] |
|
|
| X = [] |
| for mutation_set in mutations: |
| score = np.mean([ |
| mutation_score[mutation] for mutation in mutation_set |
| ]) |
| if not np.isfinite(score): |
| score = 0. |
| X.append(score) |
|
|
| return np.array(X) |
|
|
| def zero_shot_msa( |
| mutations, |
| sequence, |
| **kwargs, |
| ): |
| """ |
| Perform zero-shot prediction using MSA Transformer model. |
| |
| Args: |
| mutations (list): List of mutation sets |
| sequence (str): Original protein sequence |
| **kwargs: Additional arguments (must include 'msa_file') |
| |
| Returns: |
| numpy.ndarray: Array of mutation scores |
| """ |
| import esm |
| import torch |
| torch.set_grad_enabled(False) |
| |
| assert kwargs['msa_file'] is not None, 'No MSA file provided.' |
| msa = read_msa(kwargs['msa_file']) |
|
|
| |
| msa_transformer, msa_transformer_alphabet = esm.pretrained.esm_msa1b_t12_100M_UR50S() |
| msa_transformer = msa_transformer.eval() |
| msa_transformer_batch_converter = msa_transformer_alphabet.get_batch_converter() |
|
|
| |
| inputs = greedy_select(msa, num_seqs=128) |
| |
| |
| inputs = [msa_splicer(inputs)] |
|
|
| |
| _, __, msa_transformer_batch_tokens = msa_transformer_batch_converter(inputs) |
| msa_transformer_batch_tokens = msa_transformer_batch_tokens.to(next(msa_transformer.parameters()).device) |
| predictions = msa_transformer.forward(msa_transformer_batch_tokens, repr_layers=[12]) |
| logits = predictions['logits'][0][0] |
| token_probs = torch.softmax(logits, dim=-1) |
| print(token_probs.shape) |
|
|
| print('pulling out mutations') |
| |
| mutation_score = {} |
| for pos in range(len(sequence)): |
| wt = sequence[pos] |
| for mt in msa_transformer_alphabet.all_toks: |
| mutation = f'{wt}{pos + 1}{mt}' |
| mutation_score[mutation] = token_probs[pos + 1, msa_transformer_alphabet.tok_to_idx[mt]] |
|
|
| X = [] |
| for mutation_set in mutations: |
| score = np.mean([ |
| mutation_score[mutation] for mutation in mutation_set |
| ]) |
| if not np.isfinite(score): |
| score = 0. |
| X.append(score) |
| |
| return np.array(X) |
|
|
| def zero_shot_esm_if( |
| mutations, |
| model_locations, |
| sequence, |
| pdb_file, |
| chain_id, |
| **kwargs |
| ): |
| """ |
| Perform zero-shot prediction using ESM-IF (Inverse Folding) model. |
| |
| Args: |
| mutations (list): List of mutation sets |
| model_locations (list): List of ESM-IF model file paths |
| sequence (str): Original protein sequence |
| pdb_file (str): Path to PDB file |
| chain_id (str): Chain ID in the PDB file |
| **kwargs: Additional arguments |
| |
| Returns: |
| numpy.ndarray: Array of mutation scores |
| """ |
| |
| import torch_geometric |
| import torch_sparse |
| from torch_geometric.nn import MessagePassing |
| import esm |
| from esm import pretrained |
| from esm.inverse_folding.util import CoordBatchConverter |
|
|
| |
| |
|
|
| |
| |
|
|
| |
| |
|
|
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
|
|
| |
|
|
| model, alphabet = pretrained.load_model_and_alphabet(model_locations[0]) |
| model = model.eval() |
|
|
| structure = esm.inverse_folding.util.load_structure(pdb_file, chain_id) |
| coords, native_seq = esm.inverse_folding.util.extract_coords_from_structure(structure) |
|
|
| device = next(model.parameters()).device |
| batch_converter = CoordBatchConverter(alphabet) |
| batch = [(coords, None, sequence)] |
| coords, confidence, strs, tokens, padding_mask = batch_converter( |
| batch, device=device) |
|
|
| prev_output_tokens = tokens[:, :-1].to(device) |
| target = tokens[:, 1:] |
| logits, _ = model.forward(coords, padding_mask, confidence, prev_output_tokens) |
|
|
| |
|
|
| scores = logits.detach().numpy()[0] |
| mutation_score = {} |
| for pos in range(len(sequence)): |
| wt = sequence[pos] |
| for mt in alphabet.all_toks: |
| mutation = f'{wt}{pos + 1}{mt}' |
| mutation_score[mutation] = scores[alphabet.tok_to_idx[mt], pos] |
|
|
| X = [] |
| for mutation_set in mutations: |
| score = np.mean([ |
| mutation_score[mutation] for mutation in mutation_set |
| ]) |
| if not np.isfinite(score): |
| score = 0. |
| X.append(score) |
|
|
| return np.array(X) |
|
|