| import torch |
| import os |
| import joblib |
| import warnings |
| import pandas as pd |
| import torch.nn.functional as F |
| from tqdm import tqdm |
| from torch.utils.data import DataLoader |
| from torch_geometric.data import Batch |
| from torch_scatter import scatter_mean, scatter_sum, scatter_max |
| from .encoder.gvp import AutoGraphEncoder |
| from .utils.data_utils import convert_graph, BatchSampler, extract_seq_from_pdb |
| from .build_graph import generate_graph |
| from .build_subgraph import generate_pos_subgraph |
| from pathos.multiprocessing import Pool |
| from pathos.threading import ThreadPool |
| from pathlib import Path |
|
|
| def iter_parallel_map(func, data, workers: int = 2): |
| pool = Pool(workers) |
| return pool.imap(func, data) |
|
|
| def iter_threading_map(func, data, workers: int = 2): |
| pool = ThreadPool(workers) |
| return pool.imap(func, data) |
|
|
| def threading_map(func, data, workers: int = 2): |
| pool = ThreadPool(workers) |
| return pool.map(func, data) |
|
|
| warnings.filterwarnings("ignore") |
|
|
|
|
| def predict_sturcture(model, cluster_models, dataloader, device): |
| epoch_iterator = tqdm(dataloader) |
| struc_label_dict = {} |
| cluster_model_dict = {} |
|
|
| for cluster_model_path in cluster_models: |
| cluster_model_name = cluster_model_path.split("/")[-1].split(".")[0] |
| struc_label_dict[cluster_model_name] = [] |
| cluster_model_dict[cluster_model_name] = joblib.load(cluster_model_path) |
|
|
| with torch.no_grad(): |
| for batch in epoch_iterator: |
| batch.to(device) |
| h_V = (batch.node_s, batch.node_v) |
| h_E = (batch.edge_s, batch.edge_v) |
|
|
| node_emebddings = model.get_embedding(h_V, batch.edge_index, h_E) |
| graph_emebddings = scatter_mean(node_emebddings, batch.batch, dim=0).cpu() |
| norm_graph_emebddings = F.normalize(graph_emebddings, p=2, dim=1) |
| for name, cluster_model in cluster_model_dict.items(): |
| batch_structure_labels = cluster_model.predict( |
| norm_graph_emebddings |
| ).tolist() |
| struc_label_dict[name].extend(batch_structure_labels) |
|
|
| return struc_label_dict |
|
|
|
|
| def get_embeds(model, dataloader, device, pooling="mean"): |
| epoch_iterator = tqdm(dataloader) |
| embeds = [] |
| with torch.no_grad(): |
| for batch in epoch_iterator: |
| batch.to(device) |
| h_V = (batch.node_s, batch.node_v) |
| h_E = (batch.edge_s, batch.edge_v) |
| node_embeds = model.get_embedding(h_V, batch.edge_index, h_E).cpu() |
| if pooling == "mean": |
| graph_embeds = scatter_mean(node_embeds, batch.batch.cpu(), dim=0) |
| elif pooling == "sum": |
| graph_embeds = scatter_sum(node_embeds, batch.batch.cpu(), dim=0) |
| elif pooling == "max": |
| graph_embeds, _ = scatter_max(node_embeds, batch.batch.cpu(), dim=0) |
| else: |
| raise ValueError("pooling should be mean, sum or max") |
| embeds.append(graph_embeds) |
|
|
| embeds = torch.cat(embeds, dim=0) |
| norm_embeds = F.normalize(embeds, p=2, dim=1) |
| return norm_embeds |
|
|
|
|
| def subgraph_conventer(subgraph_dir, pdb_dir, max_batch_nodes, num_processes=12): |
| print("---------- Load Subgraphs ----------") |
| results, node_counts = [], [] |
| assert pdb_dir is not None, "pdb_dir is required" |
| subgraph_files = sorted( |
| [os.path.join(subgraph_dir, p) for p in os.listdir(subgraph_dir)] |
| ) |
|
|
| def process_subgraph_file(subgraph_file): |
| result_dict = {} |
| name = subgraph_file.split("/")[-1].split(".")[0] |
| result_dict["name"] = name + ".pdb" |
| aa_seq = extract_seq_from_pdb(os.path.join(pdb_dir, f"{name}.pdb")) |
| result_dict["aa_seq"] = aa_seq |
| return result_dict, len(aa_seq) |
|
|
| for result in tqdm(iter_threading_map(process_subgraph_file, subgraph_files, num_processes), total = len(subgraph_files)): |
| result_dict, node_count = result |
| results.append(result_dict) |
| node_counts.append(node_count) |
| |
|
|
| def collate_fn(batch): |
| |
| batch_graphs = [] |
| for d in batch: |
| subgraph_dict = torch.load(d) |
| batch_graphs.extend(list(subgraph_dict.values())) |
|
|
| |
| prue_batch_graphs = [] |
| for d in batch_graphs: |
| prue_batch_graphs.append(convert_graph(d)) |
|
|
| batch_graphs = Batch.from_data_list(prue_batch_graphs) |
| batch_graphs.node_s = torch.zeros_like(batch_graphs.node_s) |
| return batch_graphs |
|
|
| data_loader = DataLoader( |
| subgraph_files, |
| num_workers=num_processes, |
| batch_sampler=BatchSampler(node_counts, max_batch_nodes, shuffle=False), |
| collate_fn=collate_fn, |
| ) |
|
|
| return data_loader, results |
|
|
|
|
| def graph_conventer( |
| graph_dir, |
| subgraph_depth, |
| max_distance, |
| max_batch_nodes, |
| num_processes=12, |
| num_threads=12, |
| cache_subgraph_dir=None, |
| ): |
| print("---------- Load Graphs ----------") |
| graph_files = sorted([os.path.join(graph_dir, p) for p in os.listdir(graph_dir)]) |
| dataset, results, node_counts = [], [], [] |
|
|
| def process_graph_file( |
| graph_file, subgraph_depth, max_distance |
| ): |
| result_dict, subgraph_dict = {}, {} |
| result_dict["name"] = graph_file.split("/")[-1].split(".")[0] + ".pdb" |
| graph = torch.load(graph_file) |
| result_dict["aa_seq"] = graph.aa_seq |
| anchor_nodes = list(range(0, len(graph.aa_seq), 1)) |
|
|
| def process_subgraph(anchor_node): |
| subgraph = generate_pos_subgraph( |
| graph, |
| subgraph_depth, |
| max_distance, |
| anchor_node, |
| verbose=False, |
| pure_subgraph=True, |
| )[anchor_node] |
| subgraph = convert_graph(subgraph) |
| return anchor_node, subgraph |
|
|
| |
| for result in tqdm(iter_threading_map(process_subgraph, anchor_nodes, num_threads), total=len(anchor_nodes)): |
| anchor, subgraph = result |
| subgraph_dict[anchor] = subgraph |
|
|
| subgraph_dict = dict(sorted(subgraph_dict.items(), key=lambda x: x[0])) |
| if cache_subgraph_dir: |
| torch.save( |
| subgraph_dict, |
| os.path.join(cache_subgraph_dir, f"{result_dict['name']}.pt"), |
| ) |
| return [], result_dict, len(graph.node_s) |
| subgraphs = list(subgraph_dict.values()) |
| return subgraphs, result_dict, len(graph.node_s) |
|
|
| |
| def handle_grpaph_file(graph_file): |
| return process_graph_file( |
| graph_file, subgraph_depth, max_distance |
| ) |
| |
| for result in tqdm(iter_parallel_map(handle_grpaph_file, graph_files, num_processes), total=len(graph_files)): |
| pdb_subgraphs, result_dict, node_count = result |
| dataset.append(pdb_subgraphs) |
| results.append(result_dict) |
| node_counts.append(node_count) |
| |
| def collate_fn(batch): |
| batch_graphs = [] |
| if cache_subgraph_dir: |
| for d in batch: |
| name = d.split("/")[-1].split(".")[0] |
| graph = torch.load(os.path.join(cache_subgraph_dir, f"{name}.pt")) |
| batch_graphs.extend(graph.values()) |
| else: |
| for d in batch: |
| batch_graphs.extend(d) |
|
|
| batch_graphs = Batch.from_data_list(batch_graphs) |
| batch_graphs.node_s = torch.zeros_like(batch_graphs.node_s) |
| return batch_graphs |
|
|
| data_loader = DataLoader( |
| dataset, |
| num_workers=num_processes, |
| batch_sampler=BatchSampler(node_counts, max_batch_nodes, shuffle=False), |
| collate_fn=collate_fn, |
| ) |
|
|
| return data_loader, results |
|
|
|
|
| def process_pdb_file( |
| pdb_file, |
| subgraph_depth, |
| max_distance, |
| num_threads, |
| cache_subgraph_dir, |
| ): |
| result_dict, subgraph_dict = {}, {} |
| result_dict["name"] = pdb_file.split("/")[-1] |
| |
| try: |
| graph = generate_graph(pdb_file, max_distance) |
| except Exception as e: |
| result_dict["error"] = str(e) |
| return None, result_dict, 0 |
|
|
| |
| result_dict["aa_seq"] = graph.aa_seq |
| anchor_nodes = list(range(0, len(graph.node_s), 1)) |
|
|
| def process_subgraph(anchor_node): |
| subgraph = generate_pos_subgraph( |
| graph, |
| subgraph_depth, |
| max_distance, |
| anchor_node, |
| verbose=False, |
| pure_subgraph=True, |
| )[anchor_node] |
| subgraph = convert_graph(subgraph) |
| return anchor_node, subgraph |
|
|
| for anchor_node in threading_map(process_subgraph, anchor_nodes, num_threads): |
| anchor, subgraph = anchor_node |
| subgraph_dict[anchor] = subgraph |
| subgraph_dict = dict(sorted(subgraph_dict.items(), key=lambda x: x[0])) |
|
|
| |
| if cache_subgraph_dir is not None: |
| subgraph_file = os.path.join( |
| cache_subgraph_dir, f"{result_dict['name'].split('.')[0]}.pt" |
| ) |
| torch.save(subgraph_dict, subgraph_file) |
| return subgraph_file, result_dict, len(anchor_nodes) |
| subgraphs = list(subgraph_dict.values()) |
| return subgraphs, result_dict, len(anchor_nodes) |
|
|
|
|
| def pdb_conventer( |
| pdb_files, |
| subgraph_depth, |
| max_distance, |
| max_batch_nodes, |
| error_file, |
| num_processes=12, |
| num_threads=12, |
| cache_subgraph_dir=None, |
| ): |
| print("---------- Building Subgraphs ----------") |
| error_proteins, error_messages = [], [] |
|
|
| dataset, results, node_counts = [], [], [] |
| |
|
|
| def handle_pdf_file(pdb_file): |
| return process_pdb_file( |
| pdb_file, |
| subgraph_depth, |
| max_distance, |
| num_threads, |
| cache_subgraph_dir, |
| ) |
| |
| for result in tqdm(iter_parallel_map(handle_pdf_file, pdb_files, num_processes), total=len(pdb_files)): |
| pdb_subgraphs, result_dict, node_count = result |
| if pdb_subgraphs is None: |
| error_proteins.append(result_dict["name"]) |
| error_messages.append(result_dict["error"]) |
| continue |
| dataset.append(pdb_subgraphs) |
| results.append(result_dict) |
| node_counts.append(node_count) |
| |
| |
| if error_proteins: |
| print(f"---------- Save Error File ----------") |
| if error_file is None: |
| error_file = os.path.join(os.path.dirname(pdb_files[0]), f"{os.path.basename(pdb_files[0]).split('.')[0]}_error.csv") |
| os.makedirs(os.path.dirname(error_file), exist_ok=True) |
| pd.DataFrame({"name": error_proteins, "error": error_messages}).to_csv( |
| error_file, index=False |
| ) |
|
|
| def collate_fn(batch): |
| batch_graphs = [] |
| if cache_subgraph_dir is not None: |
| for d in batch: |
| name = d.split("/")[-1].split(".")[0] |
| graph = torch.load(os.path.join(cache_subgraph_dir, f"{name}.pt")) |
| batch_graphs.extend(graph.values()) |
| else: |
| for d in batch: |
| batch_graphs.extend(d) |
|
|
| batch_graphs = Batch.from_data_list(batch_graphs) |
| batch_graphs.node_s = torch.zeros_like(batch_graphs.node_s) |
| return batch_graphs |
|
|
| data_loader = DataLoader( |
| dataset, |
| num_workers=num_processes, |
| batch_sampler=BatchSampler( |
| node_counts, max_batch_nodes=max_batch_nodes, shuffle=False |
| ), |
| collate_fn=collate_fn, |
| ) |
|
|
| return data_loader, results |
|
|
|
|
| class SSTPredictor: |
| def __init__( |
| self, |
| model_path=None, |
| cluster_dir=None, |
| cluster_model=None, |
| max_distance=10, |
| subgraph_depth=None, |
| max_batch_nodes=10000, |
| num_processes=12, |
| num_threads=16, |
| device=None, |
| structure_vocab_size=2048, |
| ) -> None: |
| """Initialize the SST predictor. |
| |
| Args: |
| model_path: Path to the model checkpoint, defaults to static/AE.pt |
| cluster_dir: Directory containing cluster models, defaults to static/ |
| cluster_model: List of cluster model names, defaults to ["{structure_vocab_size}.joblib"] |
| max_distance: Maximum distance for edges |
| subgraph_depth: Depth of subgraphs |
| max_batch_nodes: Maximum number of nodes in a batch |
| num_processes: Number of processes for data loading |
| num_threads: Number of threads for data loading |
| device: Device to run on (cuda or cpu) |
| structure_vocab_size: Size of structure vocabulary (20, 64, 128, 512, 1024, 2048, 4096) |
| """ |
| assert structure_vocab_size in [20, 64, 128, 512, 1024, 2048, 4096] |
| |
| if model_path is None: |
| self.model_path = str(Path(__file__).parent / "static" / "AE.pt") |
| else: |
| self.model_path = model_path |
| |
| if cluster_dir is None: |
| self.cluster_dir = str(Path(__file__).parent / "static") |
| self.cluster_model = [f"{structure_vocab_size}.joblib"] |
| else: |
| self.cluster_dir = cluster_dir |
| self.cluster_model = cluster_model if cluster_model is not None else [f"{structure_vocab_size}.joblib"] |
| |
| self.max_distance = max_distance |
| self.subgraph_depth = subgraph_depth |
| self.max_batch_nodes = max_batch_nodes |
| self.num_processes = num_processes |
| self.num_threads = num_threads |
| self.structure_vocab_size = structure_vocab_size |
| |
| if device is None: |
| self.device = "cuda" if torch.cuda.is_available() else "cpu" |
| else: |
| self.device = device |
|
|
| print(f"---------- Load Model on {self.device} ----------") |
| |
| node_dim = (256, 32) |
| edge_dim = (64, 2) |
| model = AutoGraphEncoder( |
| node_in_dim=(20, 3), |
| node_h_dim=node_dim, |
| edge_in_dim=(32, 1), |
| edge_h_dim=edge_dim, |
| num_layers=6, |
| ) |
| if self.device == "cpu": |
| model.load_state_dict(torch.load(self.model_path, map_location=torch.device('cpu'))) |
| else: |
| model.load_state_dict(torch.load(self.model_path)) |
| model.to(self.device) |
| model.eval() |
| self.model = model |
| params = sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6 |
| print(f"MODEL: {params:.2f}M parameters") |
| |
| self.cluster_models = [os.path.join(self.cluster_dir, m) for m in self.cluster_model] |
|
|
| def predict_from_pdb(self, pdb_files, error_file=None, cache_subgraph_dir=None): |
| """Predict structure from PDB files. |
| |
| Args: |
| pdb_files: Single PDB file path or list of PDB file paths |
| error_file: Path to save error log |
| cache_subgraph_dir: Directory to cache subgraphs |
| |
| Returns: |
| List of dictionaries containing predictions for each PDB |
| """ |
| if isinstance(pdb_files, str): |
| pdb_files = [pdb_files] |
| |
| data_loader, results = pdb_conventer( |
| pdb_files, |
| self.subgraph_depth, |
| self.max_distance, |
| self.max_batch_nodes, |
| error_file, |
| self.num_processes, |
| self.num_threads, |
| cache_subgraph_dir |
| ) |
| |
| structures = predict_sturcture(self.model, self.cluster_models, data_loader, self.device) |
| |
| start, end = 0, 0 |
| for result in results: |
| end += len(result["aa_seq"]) |
| for cluster_name, structure_labels in structures.items(): |
| result[f"{cluster_name}_sst_seq"] = structure_labels[start:end] |
| start = end |
| |
| return results |
|
|
| def predict_from_graph(self, graph_dir, cache_subgraph_dir=None): |
| """Predict structure from pre-built graph files. |
| |
| Args: |
| graph_dir: Directory containing graph files |
| cache_subgraph_dir: Directory to cache subgraphs |
| |
| Returns: |
| List of dictionaries containing predictions for each graph |
| """ |
| data_loader, results = graph_conventer( |
| graph_dir, |
| self.subgraph_depth, |
| self.max_distance, |
| self.max_batch_nodes, |
| self.num_processes, |
| self.num_threads, |
| cache_subgraph_dir |
| ) |
| |
| structures = predict_sturcture(self.model, self.cluster_models, data_loader, self.device) |
| |
| start, end = 0, 0 |
| for result in results: |
| end += len(result["aa_seq"]) |
| for cluster_name, structure_labels in structures.items(): |
| result[f"{cluster_name}_s_seq"] = structure_labels[start:end] |
| start = end |
| |
| return results |
|
|
| def predict_from_subgraph(self, subgraph_dir, pdb_dir): |
| """Predict structure from pre-built subgraph files. |
| |
| Args: |
| subgraph_dir: Directory containing subgraph files |
| pdb_dir: Directory containing corresponding PDB files |
| |
| Returns: |
| List of dictionaries containing predictions for each subgraph |
| """ |
| data_loader, results = subgraph_conventer( |
| subgraph_dir, |
| pdb_dir, |
| self.max_batch_nodes, |
| self.num_processes |
| ) |
| |
| structures = predict_sturcture(self.model, self.cluster_models, data_loader, self.device) |
| |
| start, end = 0, 0 |
| for result in results: |
| end += len(result["aa_seq"]) |
| for cluster_name, structure_labels in structures.items(): |
| result[f"{cluster_name}_s_seq"] = structure_labels[start:end] |
| start = end |
| |
| return results |
|
|
|
|