| """ |
| Drug Repositioning Prediction Script for REDDA Model |
| |
| This script performs drug repurposing prediction using the trained REDDA model. |
| It takes an input CSV with drug SMILES and calculates similarity to existing drugs |
| in the knowledge graph, then predicts drug-disease associations. |
| |
| Usage: |
| python prediction.py --input_csv <path_to_input_csv> --target_disease <disease_id> --output_csv <output_path> |
| """ |
|
|
| import os |
| import sys |
| import argparse |
| import numpy as np |
| import pandas as pd |
| import torch as th |
| from rdkit import Chem |
| from rdkit.Chem import DataStructs, rdMolDescriptors |
| from rdkit import RDLogger |
| import dgl |
| from tqdm import tqdm |
| import warnings |
| warnings.filterwarnings('ignore') |
|
|
| |
| RDLogger.DisableLog('rdApp.*') |
|
|
| |
| from model import Model |
| from load_data import load |
| from utils import set_seed |
|
|
|
|
| def calculate_tanimoto_similarity(smiles1, smiles2): |
| """ |
| Calculate Tanimoto similarity between two SMILES strings using Morgan fingerprints. |
| |
| Args: |
| smiles1 (str): First SMILES string |
| smiles2 (str): Second SMILES string |
| |
| Returns: |
| float: Tanimoto similarity score (0-1) |
| """ |
| try: |
| mol1 = Chem.MolFromSmiles(smiles1) |
| mol2 = Chem.MolFromSmiles(smiles2) |
| |
| if mol1 is None or mol2 is None: |
| return 0.0 |
| |
| |
| fp1 = rdMolDescriptors.GetMorganFingerprintAsBitVect(mol1, radius=2, nBits=2048) |
| fp2 = rdMolDescriptors.GetMorganFingerprintAsBitVect(mol2, radius=2, nBits=2048) |
| |
| |
| similarity = DataStructs.TanimotoSimilarity(fp1, fp2) |
| return similarity |
| except: |
| return 0.0 |
|
|
|
|
| from concurrent.futures import ThreadPoolExecutor |
| from functools import partial |
|
|
| def calculate_similarity_for_drug(query_smiles, row): |
| """ |
| Calculate similarity for a single drug. |
| |
| Args: |
| query_smiles (str): Query drug SMILES |
| row (pd.Series): Drug row from DataFrame |
| |
| Returns: |
| tuple: (drug_id, similarity_score) or None if invalid SMILES |
| """ |
| kg_smiles = row['SMILES'] |
| drug_id = row['Inter_ID'] |
| |
| |
| if pd.isna(kg_smiles) or kg_smiles == '': |
| return None |
| |
| similarity = calculate_tanimoto_similarity(query_smiles, kg_smiles) |
| if similarity > 0: |
| return (drug_id, similarity) |
| return None |
|
|
| def find_similar_drugs(query_smiles, drug_df, top_k=20, n_jobs=8): |
| """ |
| Find top-k most similar drugs to the query SMILES from the knowledge graph. |
| Uses parallel processing for faster computation. |
| |
| Args: |
| query_smiles (str): Query drug SMILES |
| drug_df (pd.DataFrame): DataFrame containing drug information with SMILES |
| top_k (int): Number of top similar drugs to return |
| n_jobs (int): Number of parallel threads to use |
| |
| Returns: |
| list: List of tuples (drug_inter_id, similarity_score) |
| """ |
| |
| calc_func = partial(calculate_similarity_for_drug, query_smiles) |
| |
| |
| similarities = [] |
| with ThreadPoolExecutor(max_workers=n_jobs) as executor: |
| results = list(executor.map(calc_func, [row for _, row in drug_df.iterrows()])) |
| |
| |
| similarities = [x for x in results if x is not None] |
| similarities.sort(key=lambda x: x[1], reverse=True) |
| return similarities[:top_k] |
|
|
|
|
|
|
|
|
| def create_drug_id_mapping(drug_df): |
| """ |
| Create mapping from Inter_ID to index for drugs. |
| |
| Args: |
| drug_df (pd.DataFrame): Drug dataframe |
| |
| Returns: |
| dict: Mapping from Inter_ID to index |
| """ |
| |
| id_to_idx = {drug_id: idx for idx, drug_id in enumerate(drug_df['Inter_ID'])} |
| return id_to_idx |
|
|
|
|
| def create_disease_id_mapping(disease_df): |
| """ |
| Create mapping from Inter_ID to index for diseases. |
| |
| Args: |
| disease_df (pd.DataFrame): Disease dataframe |
| |
| Returns: |
| dict: Mapping from Inter_ID to index |
| """ |
| return {disease_id: idx for idx, disease_id in enumerate(disease_df['Inter_ID'])} |
|
|
|
|
| def augment_graph_with_new_drugs(input_drugs_df, drug_df, drug_id_mapping, kg_dir, output_folder): |
| """ |
| Augment the graph by creating temporary node and edge files with new drugs. |
| |
| Args: |
| input_drugs_df (pd.DataFrame): Input drugs DataFrame |
| drug_df (pd.DataFrame): Knowledge graph drugs DataFrame |
| drug_id_mapping (dict): Mapping from drug ID to index |
| kg_dir (str): Base directory of knowledge graph data |
| output_folder (str): Path to output folder |
| |
| Returns: |
| tuple: (new_drug_indices, node_path, edge_path) |
| """ |
| global temp_dir |
| print("Augmenting graph with new drugs...") |
| |
| |
| last_drug_id = int(drug_df['Inter_ID'].str.extract(r'DRUG_(\d+)').astype(int).max()) |
| |
| |
| new_drugs = [] |
| new_drug_indices = {} |
| base_idx = len(drug_df) |
| |
| for idx, row in input_drugs_df.iterrows(): |
| new_drug_id = f"DRUG_{last_drug_id + idx + 1}" |
| new_drug_indices[idx] = base_idx + idx |
| |
| new_drug = { |
| 'id': f"NEW_{new_drug_id}", |
| 'name': f"New_Drug_{idx}", |
| 'source': 'Prediction', |
| 'type': 'drug', |
| 'SMILES': row['SMILES'], |
| 'Inter_ID': new_drug_id |
| } |
| new_drugs.append(new_drug) |
|
|
| |
| augmented_drug_df = pd.concat([ |
| drug_df, |
| pd.DataFrame(new_drugs) |
| ], ignore_index=True) |
| |
| |
| new_edges = [] |
| |
| |
| for idx, row in tqdm(input_drugs_df.iterrows(), total=len(input_drugs_df), desc="Finding similar drugs"): |
| query_smiles = row['SMILES'] |
| new_drug_id = new_drug_indices[idx] |
| |
| |
| similar_drugs = find_similar_drugs(query_smiles, drug_df, top_k=20) |
| |
| for similar_drug_id, similarity in similar_drugs: |
| |
| similar_drug_row = drug_df[drug_df['Inter_ID'] == similar_drug_id].iloc[0] |
| similar_drug_name = similar_drug_row['name'] |
| similar_drug_idx = drug_id_mapping[similar_drug_id] |
| |
| |
| new_edges.append({ |
| 'relation': 'drug_drug', |
| 'display_relation': 'structural_similarity', |
| 'x_index': base_idx + idx, |
| 'x_id': f'DRUG_{new_drug_id}', |
| 'x_type': 'drug', |
| 'x_name': f"New_Drug_{idx}", |
| 'x_source': 'Prediction', |
| 'y_index': similar_drug_idx, |
| 'y_id': similar_drug_id, |
| 'y_type': 'drug', |
| 'y_name': similar_drug_name, |
| 'y_source': 'DrugBank', |
| 'similarity_score': similarity |
| }) |
| new_edges.append({ |
| 'relation': 'drug_drug', |
| 'display_relation': 'structural_similarity', |
| 'x_index': similar_drug_idx, |
| 'x_id': similar_drug_id, |
| 'x_type': 'drug', |
| 'x_name': similar_drug_name, |
| 'x_source': 'DrugBank', |
| 'y_index': base_idx + idx, |
| 'y_id': f'DRUG_{new_drug_id}', |
| 'y_type': 'drug', |
| 'y_name': f"New_Drug_{idx}", |
| 'y_source': 'Prediction', |
| 'similarity_score': similarity |
| }) |
| |
| |
| new_edge_df = pd.DataFrame(new_edges) |
|
|
| |
| temp_dir = os.path.join(output_folder, 'temp') |
| os.makedirs(temp_dir, exist_ok=True) |
| |
| |
| temp_node_path = os.path.join(temp_dir, 'drug.csv') |
| temp_edge_path = os.path.join(temp_dir, 'drug_drug.csv') |
| |
| augmented_drug_df.to_csv(temp_node_path, index=False) |
| |
| |
| orig_edges = pd.read_csv(os.path.join(kg_dir, 'edge/drug_drug.csv')) |
| |
| |
| for col in orig_edges.columns: |
| if col not in new_edge_df.columns: |
| new_edge_df[col] = '' |
| |
| |
| combined_edges = pd.concat([orig_edges, new_edge_df], ignore_index=True) |
| |
| |
| combined_edges.to_csv(temp_edge_path, index=False) |
| |
| |
| for file_name in os.listdir(os.path.join(kg_dir, 'node')): |
| if file_name != 'drug.csv': |
| src = os.path.join(kg_dir, 'node', file_name) |
| dst = os.path.join(temp_dir, file_name) |
| if not os.path.exists(dst): |
| os.symlink(src, dst) |
| |
| for file_name in os.listdir(os.path.join(kg_dir, 'edge')): |
| if file_name != 'drug_drug.csv': |
| src = os.path.join(kg_dir, 'edge', file_name) |
| dst = os.path.join(temp_dir, file_name) |
| if not os.path.exists(dst): |
| os.symlink(src, dst) |
| |
| return new_drug_indices, temp_dir, temp_dir |
|
|
|
|
| def load_models(model_dir, g, feature_dim, device, hidden_feats=128, num_heads=5, dropout=0.0): |
| """ |
| Load all 5 trained model checkpoints. |
| |
| Args: |
| model_dir (str): Directory containing model checkpoints |
| g (dgl.DGLHeteroGraph): Graph for model initialization |
| feature_dim (int): Feature dimension |
| device (torch.device): Device to load models on |
| hidden_feats (int): Hidden layer dimension (default: 64) |
| num_heads (int): Number of attention heads (default: 5) |
| dropout (float): Dropout rate (default: 0.0) |
| |
| Returns: |
| list: List of loaded models |
| """ |
| models = [] |
| |
| for fold in range(1, 6): |
| model_path = os.path.join(model_dir, f"model_{fold}.pth") |
| |
| if not os.path.exists(model_path): |
| print(f"Warning: Model checkpoint {model_path} not found") |
| continue |
| |
| |
| model = Model( |
| etypes=g.etypes, |
| ntypes=g.ntypes, |
| in_feats=feature_dim, |
| hidden_feats=hidden_feats, |
| num_heads=num_heads, |
| dropout=dropout, |
| ).to(device) |
| |
| |
| checkpoint = th.load(model_path, map_location=device) |
| model.load_state_dict(checkpoint) |
| model.eval() |
| |
| models.append(model) |
| print(f"Loaded model from {model_path}") |
| |
| return models |
|
|
|
|
| def ensemble_predict(models, g, feature, device): |
| """ |
| Perform ensemble prediction using multiple model checkpoints. |
| |
| Args: |
| models (list): List of trained models |
| g (dgl.DGLHeteroGraph): Input graph |
| feature (dict): Node features |
| device (torch.device): Device for computation |
| |
| Returns: |
| tuple: (mean_predictions, std_predictions) |
| """ |
| predictions = [] |
| |
| print("Performing ensemble prediction...") |
| with th.no_grad(): |
| for i, model in enumerate(models): |
| print(f"Predicting with model {i+1}/{len(models)}") |
| pred = th.sigmoid(model(g, feature)).cpu().numpy() |
| predictions.append(pred) |
| |
| |
| predictions = np.array(predictions) |
| mean_pred = np.mean(predictions, axis=0) |
| std_pred = np.std(predictions, axis=0) |
| |
| return mean_pred, std_pred |
|
|
|
|
| |
| temp_dir = None |
|
|
| def main(): |
| global temp_dir |
| parser = argparse.ArgumentParser(description='REDDA Drug Repositioning Prediction') |
| parser.add_argument('--input_csv', type=str, required=True, |
| help='Path to input CSV file with drug SMILES') |
| parser.add_argument('--target_disease', type=str, required=True, |
| help='Target disease Inter_ID (e.g., DISEASE_123)') |
| parser.add_argument('--output_folder', type=str, required=True, |
| help='Path to output folder for results and temporary files') |
| parser.add_argument('--device_id', type=str, default=None, |
| help='GPU device ID (default: CPU)') |
| parser.add_argument('--dataset', type=str, default='KGdataset', |
| choices=['KGdataset', 'KGdataset_tiny'], |
| help='Dataset to use') |
| parser.add_argument('--model_dir', type=str, default='result_kg_42', |
| help='Directory containing model checkpoints') |
| |
| pred_args = parser.parse_args() |
| |
| |
| if pred_args.device_id: |
| device = th.device(f"cuda:{pred_args.device_id}") |
| print(f"Using GPU: {pred_args.device_id}") |
| else: |
| device = th.device("cpu") |
| print("Using CPU") |
| |
| |
| set_seed(42) |
| |
| print("Loading input data...") |
| |
| input_df = pd.read_csv(pred_args.input_csv) |
| |
| |
| required_columns = ['SMILES'] |
| if not all(col in input_df.columns for col in required_columns): |
| raise ValueError(f"Input CSV must contain columns: {required_columns}") |
| |
| print(f"Input contains {len(input_df)} drugs") |
| |
| |
| print("Loading knowledge graph data...") |
| drug_df = pd.read_csv('/vast/yg3191/AIVS/kg/node/drug.csv') |
| disease_df = pd.read_csv('/vast/yg3191/AIVS/kg/node/disease.csv') |
| |
| |
| drug_id_mapping = create_drug_id_mapping(drug_df) |
| disease_id_mapping = create_disease_id_mapping(disease_df) |
| |
| |
| if pred_args.target_disease not in disease_id_mapping: |
| available_diseases = list(disease_id_mapping.keys())[:10] |
| raise ValueError(f"Target disease '{pred_args.target_disease}' not found in knowledge graph. " |
| f"Available diseases (first 10): {available_diseases}") |
| |
| target_disease_idx = disease_id_mapping[pred_args.target_disease] |
| print(f"Target disease: {pred_args.target_disease} (index: {target_disease_idx})") |
| |
| |
| print("Loading original graph...") |
| g = load(pred_args.dataset).to(device) |
| |
| print(f"Original graph: {g.num_nodes('drug')} drugs, {g.num_nodes('disease')} diseases") |
| |
| |
| os.makedirs(pred_args.output_folder, exist_ok=True) |
| |
| |
| kg_dir = '/vast/yg3191/AIVS/kg' |
| new_drug_indices, node_path, edge_path = augment_graph_with_new_drugs( |
| input_df, drug_df, drug_id_mapping, kg_dir, pred_args.output_folder |
| ) |
| |
| |
| from load_data import _build_heterograph |
| |
| |
| node_files = { |
| 'drug': 'drug.csv', |
| 'disease': 'disease.csv', |
| 'protein': 'protein.csv', |
| |
| |
| |
| |
| |
| |
| |
| } |
| |
| edge_files = { |
| 'drug_drug': 'drug_drug.csv', |
| |
| 'drug_protein': 'drug_protein.csv', |
| 'drug_disease': 'drug_disease_indication.csv', |
| 'protein_protein': 'protein_protein.csv', |
| |
| |
| |
| |
| 'protein_disease': 'protein_disease.csv', |
| 'disease_disease': 'disease_disease.csv', |
| |
| |
| |
| |
| |
| |
| |
| } |
| |
| augmented_g = _build_heterograph( |
| node_path=node_path, |
| edge_path=edge_path, |
| node_files=node_files, |
| edge_files=edge_files, |
| feature_dim=128 |
| ).to(device) |
| |
| |
| feature = {ntype: augmented_g.nodes[ntype].data['h'] for ntype in augmented_g.ntypes} |
| feature_dim = feature['drug'].shape[1] |
| |
| |
| print("Loading trained models...") |
| |
| models = load_models(pred_args.model_dir, augmented_g, feature_dim, device, |
| hidden_feats=128, num_heads=8, dropout=0.4) |
| |
| if not models: |
| raise ValueError("No model checkpoints found!") |
| |
| print(f"Loaded {len(models)} model checkpoints") |
| |
| |
| mean_pred, std_pred = ensemble_predict(models, augmented_g, feature, device) |
| |
| |
| print("Extracting predictions...") |
| results = [] |
| print(mean_pred.shape) |
| print(std_pred.shape) |
| print(new_drug_indices) |
| for idx, row in input_df.iterrows(): |
| |
| drug_idx = new_drug_indices[idx] |
| |
| |
| Score_mean = float(mean_pred[drug_idx, target_disease_idx]) |
| Score_std = float(std_pred[drug_idx, target_disease_idx]) |
| |
| |
| result_row = row.copy() |
| result_row['Target_Disease'] = pred_args.target_disease |
| result_row['Score_Mean'] = Score_mean |
| result_row['Score_Std'] = Score_std |
| |
| results.append(result_row) |
| |
| |
| output_df = pd.DataFrame(results) |
| |
| |
| os.makedirs(pred_args.output_folder, exist_ok=True) |
| |
| |
| output_path = os.path.join(pred_args.output_folder, 'result.csv') |
| output_df.to_csv(output_path, index=False) |
| print(f"Predictions saved to: {output_path}") |
| |
| |
| print("\nPrediction Summary:") |
| print(f"Mean Score: {output_df['Score_Mean'].mean():.4f}") |
| print(f"Std Score: {output_df['Score_Mean'].std():.4f}") |
| print(f"Max Score: {output_df['Score_Mean'].max():.4f}") |
| print(f"Min Score: {output_df['Score_Mean'].min():.4f}") |
| |
| |
| print(f"\nTop 5 predictions:") |
| top_predictions = output_df.nlargest(5, 'Score_Mean') |
| for idx, row in top_predictions.iterrows(): |
| print(f" SMILES: {row['SMILES'][:50]}... | Score: {row['Score_Mean']:.4f} ± {row['Score_Std']:.4f}") |
| |
| |
| try: |
| |
| for file_name in os.listdir(temp_dir): |
| file_path = os.path.join(temp_dir, file_name) |
| if os.path.islink(file_path): |
| os.unlink(file_path) |
| elif os.path.isfile(file_path): |
| os.remove(file_path) |
| |
| os.rmdir(temp_dir) |
| print("\nTemporary files cleaned up") |
| except Exception as e: |
| print(f"\nWarning: Could not clean up temporary files: {e}") |
| print(f"Temporary files remain in: {temp_dir}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|