AlphaFold × Pharma.AI 벤치마크 기반
타겟 발견 → 화합물 생성 → 검증까지 통합 워크플로우
# ═══════════════════════════════════════════════ # STEP 01: 타겟 발견 (Target Identification) # 벤치마크: DeepMind AlphaFold # ═══════════════════════════════════════════════ import torch import numpy as np import requests from Bio import SeqIO from chembl_webresource_client.new_client import new_client # ── 1-1. ChEMBL DB에서 타겟 데이터 로드 ── class TargetDB: def __init__(self, disease="lung cancer"): self.disease = disease self.client = new_client def fetch_targets(self, limit=50): """질병 관련 타겟 단백질 검색""" target = self.client.target results = target.filter( target_synonym__icontains=self.disease ).only('target_chembl_id', 'pref_name', 'target_type') return list(results)[:limit] # ── 1-2. AlphaFold 구조 예측 모듈 ── class AlphaFoldPredictor: def __init__(self): # HuggingFace ESMFold (AlphaFold-class 모델) self.api_url = "https://api.esmatlas.com/foldSequence/v1/pdb/" def predict_structure(self, amino_seq: str) -> dict: """아미노산 시퀀스 → 3D PDB 구조 예측""" response = requests.post( self.api_url, data=amino_seq, headers={"Content-Type": "text/plain"} ) pdb_data = response.text plddt = self._extract_confidence(pdb_data) return {"pdb": pdb_data, "pLDDT": plddt} def _extract_confidence(self, pdb_text: str) -> float: """pLDDT 신뢰도 점수 추출""" scores = [] for line in pdb_text.split('\n'): if line.startswith('ATOM'): try: scores.append(float(line[60:66])) except: pass return np.mean(scores) if scores else 0.0 # ── 1-3. 타겟 스코어링 & 우선순위 결정 ── class TargetScorer: def score(self, target_info: dict, plddt: float) -> float: druggability = target_info.get('druggability', 0.7) expression_lvl = target_info.get('expression', 0.8) pathway_score = target_info.get('pathway_central',0.6) # 가중 합산 공식 score = (0.4 * druggability + 0.3 * (plddt / 100) + 0.2 * expression_lvl + 0.1 * pathway_score) return round(score, 4) # ── 메인 파이프라인 실행 ── if __name__ == "__main__": db = TargetDB(disease="non-small cell lung cancer") predictor = AlphaFoldPredictor() scorer = TargetScorer() targets = db.fetch_targets(limit=20) ranked = [] for t in targets: seq = t.get('sequence', '') result = predictor.predict_structure(seq) score = scorer.score(t, result['pLDDT']) ranked.append({'target': t, 'score': score, 'pLDDT': result['pLDDT']}) top_targets = sorted(ranked, key=lambda x: x['score'], reverse=True)[:10] print(f"✅ Top-10 타겟 발견 완료: {[t['target']['pref_name'] for t in top_targets]}")
▶ 버튼을 눌러 타겟 발견 파이프라인 시뮬레이션을 실행합니다.
# ═══════════════════════════════════════════════ # STEP 02: 화합물 라이브러리 생성 # 벤치마크: Insilico Medicine Pharma.AI # ═══════════════════════════════════════════════ import torch import torch.nn as nn from rdkit import Chem from rdkit.Chem import Descriptors, QED, AllChem import numpy as np # ── 2-1. Lipinski Rule of 5 필터 ── def lipinski_filter(smiles: str) -> bool: mol = Chem.MolFromSmiles(smiles) if not mol: return False mw = Descriptors.MolWt(mol) logp = Descriptors.MolLogP(mol) hbd = Descriptors.NumHDonors(mol) hba = Descriptors.NumHAcceptors(mol) return (mw < 500 and logp < 5 and hbd <= 5 and hba <= 10) # ── 2-2. VAE 분자 생성 모델 ── class MolecularVAE(nn.Module): def __init__(self, vocab_size=60, latent_dim=256): super().__init__() # 인코더: SMILES → 잠재벡터 self.encoder = nn.Sequential( nn.Embedding(vocab_size, 128), nn.GRU(128, 512, batch_first=True) ) self.fc_mu = nn.Linear(512, latent_dim) self.fc_logvar = nn.Linear(512, latent_dim) # 디코더: 잠재벡터 → SMILES self.decoder = nn.Sequential( nn.Linear(latent_dim, 512), nn.GRU(512, 512, batch_first=True), nn.Linear(512, vocab_size) ) def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + eps * std def generate(self, n_samples=1000, latent_dim=256): """잠재공간 샘플링으로 신규 분자 생성""" z = torch.randn(n_samples, latent_dim) with torch.no_grad(): decoded = self.decoder(z.unsqueeze(1)) return decoded # ── 2-3. GAN 기반 생성 모듈 ── class MolGAN: def __init__(self): self.generator = self._build_generator() self.discriminator = self._build_discriminator() def _build_generator(self) -> nn.Module: return nn.Sequential( nn.Linear(128, 512), nn.ReLU(), nn.Linear(512, 1024), nn.ReLU(), nn.Linear(1024, 60), nn.Softmax(dim=-1) ) def _build_discriminator(self) -> nn.Module: return nn.Sequential( nn.Linear(60, 512), nn.LeakyReLU(0.2), nn.Linear(512, 256), nn.LeakyReLU(0.2), nn.Linear(256, 1), nn.Sigmoid() ) # ── 2-4. 통합 라이브러리 생성 파이프라인 ── class LibraryGenerator: def __init__(self): self.vae = MolecularVAE() self.gan = MolGAN() def generate_library(self, n_candidates=10000) -> list: generated = [] batch = n_candidates // 100 # 배치 처리 for _ in range(batch): raw = self.vae.generate(100) # SMILES 변환 후 Lipinski 필터 적용 valid = [s for s in raw if lipinski_filter(s)] generated.extend(valid) print(f"✅ {len(generated):,}개 유효 후보 생성 (목표: {n_candidates:,})") return generated if __name__ == "__main__": gen = LibraryGenerator() library = gen.generate_library(n_candidates=10000)
▶ GAN/VAE 기반 화합물 라이브러리 생성 시뮬레이션
# ═══════════════════════════════════════════════ # STEP 03: 바인딩 예측 (Binding Prediction) # 모델: Graph Convolutional Network (GCN) # ═══════════════════════════════════════════════ import torch import torch.nn as nn import torch_geometric.nn as gnn from torch_geometric.data import Data from rdkit import Chem from rdkit.Chem import AllChem import numpy as np # ── 3-1. 분자 → 그래프 변환 ── def mol_to_graph(smiles: str) -> Data: """SMILES 문자열을 PyG 그래프로 변환""" mol = Chem.MolFromSmiles(smiles) if not mol: return None # 원자 특성 (노드) atom_features = [] for atom in mol.GetAtoms(): features = [ atom.GetAtomicNum(), # 원자번호 atom.GetDegree(), # 결합 수 atom.GetFormalCharge(), # 형식 전하 int(atom.GetIsAromatic()), # 방향족 여부 atom.GetHybridization().real # 혼성화 ] atom_features.append(features) # 결합 인덱스 (엣지) edge_index = [] for bond in mol.GetBonds(): i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx() edge_index.extend([[i,j],[j,i]]) x = torch.tensor(atom_features, dtype=torch.float) ei = torch.tensor(edge_index, dtype=torch.long).t().contiguous() return Data(x=x, edge_index=ei) # ── 3-2. GCN 결합 예측 모델 ── class BindingGCN(nn.Module): def __init__(self, node_feat=5, hidden=256): super().__init__() self.conv1 = gnn.GCNConv(node_feat, hidden) self.conv2 = gnn.GCNConv(hidden, hidden) self.conv3 = gnn.GCNConv(hidden, hidden // 2) self.pool = gnn.global_mean_pool self.fc = nn.Sequential( nn.Linear(hidden // 2, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 1) # 결합 에너지 출력 ) def forward(self, data): x, ei, batch = data.x, data.edge_index, data.batch x = torch.relu(self.conv1(x, ei)) x = torch.relu(self.conv2(x, ei)) x = torch.relu(self.conv3(x, ei)) x = self.pool(x, batch) return self.fc(x) # ΔG 결합 자유에너지 예측 # ── 3-3. ADMET 예측 모듈 ── class ADMETPredictor(nn.Module): def __init__(self): super().__init__() self.model = nn.Sequential( nn.Linear(2048, 512), # Morgan 핑거프린트 입력 nn.ReLU(), nn.Dropout(0.2), nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, 5) # A, D, M, E, T 5개 출력 ) def predict(self, smiles: str) -> dict: mol = Chem.MolFromSmiles(smiles) fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, 2048) x = torch.tensor(list(fp), dtype=torch.float).unsqueeze(0) pred = self.model(x).squeeze().detach().numpy() return { 'absorption': pred[0], 'distribution': pred[1], 'metabolism': pred[2], 'excretion': pred[3], 'toxicity': pred[4] } # ── 3-4. 통합 스크리닝 파이프라인 ── def screen_library(library: list, top_n=100) -> list: binding_model = BindingGCN() admet_model = ADMETPredictor() results = [] for smiles in library: graph = mol_to_graph(smiles) binding_dg = binding_model(graph).item() admet = admet_model.predict(smiles) if admet['toxicity'] < 0.3: # 독성 필터 results.append({'smiles': smiles, 'dG': binding_dg, 'admet': admet}) return sorted(results, key=lambda x: x['dG'])[:top_n]
▶ GCN 바인딩 예측 + ADMET 스크리닝 시뮬레이션
# ═══════════════════════════════════════════════ # STEP 04: 리드 최적화 (RL 기반) # 알고리즘: Proximal Policy Optimization (PPO) # ═══════════════════════════════════════════════ import torch import torch.nn as nn from rdkit import Chem from rdkit.Chem import RWMol, QED import numpy as np # ── 4-1. 분자 편집 환경 (OpenAI Gym 스타일) ── class MolEnv: ACTIONS = [ 'add_atom_C', 'add_atom_N', 'add_atom_O', 'remove_atom', 'add_bond_single', 'add_bond_double', 'add_ring_benzene', 'add_ring_pyridine', 'change_hybridization', 'add_substituent_F' ] def __init__(self, init_smiles: str, max_steps=10): self.mol = Chem.MolFromSmiles(init_smiles) self.max_steps = max_steps self.step_n = 0 def step(self, action_idx: int): action = self.ACTIONS[action_idx] rw_mol = RWMol(self.mol) # 분자 편집 수행 self._apply_action(rw_mol, action) new_mol = rw_mol.GetMol() reward = self._compute_reward(new_mol) self.step_n += 1 done = (self.step_n >= self.max_steps) if Chem.SanitizeMol(new_mol, catchErrors=True) == 0: self.mol = new_mol return self.mol, reward, done def _compute_reward(self, mol) -> float: if not mol: return -1.0 qed_score = QED.qed(mol) # 결합 친화도는 GCN 모델로 예측 (여기선 간략화) binding_dg = np.random.uniform(-12, -6) toxicity = np.random.uniform(0, 0.5) reward = (0.4 * abs(binding_dg)/12 + 0.3 * qed_score + 0.3 * (1 - toxicity)) return reward # ── 4-2. PPO 정책 네트워크 ── class PPOAgent(nn.Module): def __init__(self, state_dim=512, n_actions=10): super().__init__() self.actor = nn.Sequential( nn.Linear(state_dim, 256), nn.Tanh(), nn.Linear(256, n_actions), nn.Softmax(dim=-1) ) self.critic = nn.Sequential( nn.Linear(state_dim, 256), nn.Tanh(), nn.Linear(256, 1) ) def forward(self, state): action_probs = self.actor(state) value = self.critic(state) return action_probs, value # ── 4-3. 최적화 루프 ── def optimize_lead(lead_smiles: str, episodes=1000) -> dict: env = MolEnv(lead_smiles) agent = PPOAgent() optimizer = torch.optim.Adam(agent.parameters(), lr=3e-4) best_mol, best_reward = lead_smiles, 0.0 for ep in range(episodes): state = torch.randn(1, 512) # 분자 인코딩 ep_reward = 0 for _ in range(env.max_steps): probs, val = agent(state) action = torch.multinomial(probs, 1).item() mol, reward, done = env.step(action) ep_reward += reward if done: break if ep_reward > best_reward: best_reward = ep_reward best_mol = Chem.MolToSmiles(env.mol) if ep % 100 == 0: print(f"Episode {ep:4d} | Reward: {ep_reward:.4f} | Best: {best_reward:.4f}") return {'optimized_smiles': best_mol, 'reward': best_reward} if __name__ == "__main__": result = optimize_lead("CC(=O)Nc1ccc(O)cc1", episodes=1000) print(f"✅ 최적화 완료: {result['optimized_smiles']}")
▶ PPO 강화학습 리드 최적화 시뮬레이션
| 후보 ID | ΔG (kcal/mol) | IC50 (nM) | ADMET | MD 안정성 | 판정 |
|---|---|---|---|---|---|
| CAND-001 | -11.42 | 2.3 | 0.84 | RMSD 1.2Å | ✅ 통과 |
| CAND-002 | -10.87 | 4.1 | 0.79 | RMSD 1.8Å | ✅ 통과 |
| CAND-003 | -10.31 | 6.7 | 0.71 | RMSD 2.4Å | ⚠️ 주의 |
| CAND-004 | -9.95 | 9.2 | 0.65 | RMSD 3.1Å | ⚠️ 주의 |
| CAND-005 | -8.73 | 18.5 | 0.42 | RMSD 5.8Å | ❌ 탈락 |
# ═══════════════════════════════════════════════ # STEP 05: 검증 및 MD 시뮬레이션 # 도구: AI 프록시 + GROMACS 연동 # ═══════════════════════════════════════════════ import torch import torch.nn as nn import numpy as np import subprocess, os # ── 5-1. AI 프록시 MD 시뮬레이션 ── class MDProxyModel(nn.Module): """GROMACS 대체 AI 프록시 (학습 기반)""" def __init__(self): super().__init__() self.backbone = nn.Sequential( nn.Linear(1024, 512), nn.GELU(), nn.Linear(512, 256), nn.GELU(), nn.Linear(256, 128), nn.GELU(), ) # 다중 출력 헤드 self.rmsd_head = nn.Linear(128, 1) # RMSD 예측 self.binding_head = nn.Linear(128, 1) # 결합 에너지 self.stability_head= nn.Linear(128, 1) # 안정성 점수 def forward(self, mol_embed, protein_embed): x = torch.cat([mol_embed, protein_embed], dim=-1) h = self.backbone(x) return { 'rmsd': self.rmsd_head(h).item(), 'binding_e':self.binding_head(h).item(), 'stability':torch.sigmoid(self.stability_head(h)).item() } # ── 5-2. GROMACS 실제 연동 (선택) ── class GromacsRunner: def __init__(self, gromacs_path="/usr/local/gromacs/bin/gmx"): self.gmx = gromacs_path def run_md(self, pdb_file: str, ns=100) -> dict: """100ns MD 시뮬레이션 실행""" steps = int(ns * 1e6 / 2) # 2fs timestep mdp_content = f""" integrator = md nsteps = {steps} dt = 0.002 tcoupl = V-rescale pcoupl = Parrinello-Rahman """ with open('md.mdp', 'w') as f: f.write(mdp_content) # GROMACS 명령 실행 subprocess.run([self.gmx, 'grompp', '-f', 'md.mdp', '-c', pdb_file, '-o', 'md.tpr']) subprocess.run([self.gmx, 'mdrun', '-v', '-deffnm', 'md']) return self._analyze() def _analyze(self) -> dict: """RMSD/RMSF 분석""" subprocess.run([self.gmx, 'rms', '-s', 'md.tpr', '-f', 'md.xtc', '-o', 'rmsd.xvg']) rmsd_data = np.loadtxt('rmsd.xvg', comments=['@','#']) return {'mean_rmsd': rmsd_data[:,1].mean(), 'max_rmsd': rmsd_data[:,1].max()} # ── 5-3. 임상 데이터 매칭 ── class ClinicalMatcher: def match(self, candidate_props: dict, chembl_db) -> float: """유사 구조 임상 성공률 예측""" similar = chembl_db.find_similar( candidate_props['smiles'], threshold=0.7 ) success_rate = sum(1 for d in similar if d['clinical_phase'] >= 3) return success_rate / max(len(similar), 1) # ── 5-4. 최종 검증 파이프라인 ── def validate_candidates(candidates: list) -> list: proxy = MDProxyModel() # clinical = ClinicalMatcher() # DB 연결 시 활성화 validated = [] for cand in candidates: mol_emb = torch.randn(1, 512) prot_emb = torch.randn(1, 512) md_results = proxy(mol_emb, prot_emb) if md_results['rmsd'] < 3.0 and md_results['stability'] > 0.7: cand['md'] = md_results validated.append(cand) print(f"✅ 최종 검증 완료: {len(validated)}개 후보 → IND 신청 준비") return sorted(validated, key=lambda x: x['md']['binding_e'])
▶ MD 시뮬레이션 + 임상 검증 파이프라인 실행