ahmedmiloudi/BioTechLabAI
1
1"""2Structural Analysis Module3===========================4Analyse structurale 3D pour drug discovery5 6Fonctionnalités:7- Analyse de structures PDB8- Identification de sites de liaison9- Calcul de propriétés structurales10- Analyse de poches (pockets)11- Préparation pour docking12 13Auteur: Drug Discovery Platform14Date: 2026-01-3115"""16 17import requests18import pandas as pd19import numpy as np20from typing import Dict, List, Optional, Tuple, Any21from dataclasses import dataclass22import logging23import json24 25logger = logging.getLogger(__name__)26 27 28# ============================================================================29# DATA CLASSES30# ============================================================================31 32@dataclass33class ProteinStructure:34 """Information sur une structure protéique"""35 pdb_id: str36 resolution: Optional[float]37 method: str38 organism: str39 chain_count: int40 residue_count: int41 atom_count: int42 ligands: List[str]43 44 # Qualité45 r_free: Optional[float]46 r_work: Optional[float]47 48 # Métadonnées49 deposition_date: str50 release_date: str51 authors: List[str]52 53 54@dataclass55class BindingSite:56 """Site de liaison identifié"""57 site_id: str58 residues: List[str]59 center: Tuple[float, float, float]60 volume: float # Ų61 depth: float # Å62 63 # Propriétés64 hydrophobic_ratio: float65 polar_ratio: float66 charged_ratio: float67 68 # Druggability69 druggability_score: float70 rank: int71 72 73@dataclass74class StructuralFeature:75 """Caractéristique structurale"""76 feature_type: str # helix, sheet, turn, coil77 start_residue: int78 end_residue: int79 length: int80 sequence: str81 82 83@dataclass84class CavityAnalysis:85 """Analyse de cavité/poche"""86 cavity_id: str87 volume: float88 surface_area: float89 depth: float90 91 # Forme92 sphericity: float93 elongation: float94 95 # Composition96 residue_composition: Dict[str, int]97 hydrophobicity: float98 99 # Accessibilité100 solvent_accessible: bool101 buried: bool102 103 104# ============================================================================105# STRUCTURAL ANALYZER CLASS106# ============================================================================107 108class StructuralAnalyzer:109 """110 Analyseur de structures protéiques 3D111 112 Utilise:113 - PDB API pour métadonnées114 - RCSB API pour analyses avancées115 - Calculs géométriques internes116 """117 118 PDB_API = "https://data.rcsb.org/rest/v1/core"119 PDB_SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query"120 121 def __init__(self):122 """Initialize structural analyzer"""123 self.session = requests.Session()124 self.session.headers.update({125 'User-Agent': 'StructuralAnalyzer/1.0',126 'Content-Type': 'application/json'127 })128 logger.info("StructuralAnalyzer initialized")129 130 # ========================================================================131 # STRUCTURE RETRIEVAL132 # ========================================================================133 134 def get_structure_info(self, pdb_id: str) -> Optional[ProteinStructure]:135 """136 Récupère informations complètes sur une structure PDB137 138 Args:139 pdb_id: Code PDB (ex: "1A0S")140 141 Returns:142 ProteinStructure ou None143 """144 try:145 pdb_id = pdb_id.upper()146 147 # Requête structure info148 url = f"{self.PDB_API}/entry/{pdb_id}"149 response = self.session.get(url, timeout=30)150 151 if response.status_code != 200:152 logger.warning(f"Structure {pdb_id} not found")153 return None154 155 data = response.json()156 157 # Parser les données158 struct = data.get('struct', {})159 exptl = data.get('exptl', [{}])[0]160 refine = data.get('refine', [{}])[0]161 audit = data.get('audit_author', {})162 163 # Compter chains et résidus164 entity_poly = data.get('entity_poly', [])165 chain_count = sum(len(poly.get('pdbx_strand_id', '').split(',')) for poly in entity_poly)166 167 # Estimer résidus168 residue_count = sum(int(poly.get('pdbx_seq_one_letter_code_can', '')) 169 for poly in entity_poly if poly.get('pdbx_seq_one_letter_code_can'))170 171 # Ligands172 nonpoly = data.get('pdbx_entity_nonpoly', [])173 ligands = [comp.get('comp_id', '') for comp in nonpoly]174 175 structure = ProteinStructure(176 pdb_id=pdb_id,177 resolution=data.get('rcsb_entry_info', {}).get('resolution_combined', [None])[0],178 method=exptl.get('method', 'Unknown'),179 organism=struct.get('pdbx_descriptor', 'Unknown'),180 chain_count=chain_count,181 residue_count=residue_count,182 atom_count=data.get('rcsb_entry_info', {}).get('deposited_atom_count', 0),183 ligands=ligands,184 r_free=refine.get('ls_R_factor_R_free'),185 r_work=refine.get('ls_R_factor_R_work'),186 deposition_date=data.get('rcsb_accession_info', {}).get('deposit_date', ''),187 release_date=data.get('rcsb_accession_info', {}).get('initial_release_date', ''),188 authors=audit.get('name', [])[:5] # Limiter à 5189 )190 191 logger.info(f"Retrieved structure info for {pdb_id}")192 return structure193 194 except Exception as e:195 logger.error(f"Error getting structure info: {str(e)}")196 return None197 198 def search_structures_by_protein(self, 199 gene_name: str,200 max_results: int = 10) -> List[str]:201 """202 Recherche structures PDB par nom de gène/protéine203 204 Args:205 gene_name: Nom du gène206 max_results: Nombre max de résultats207 208 Returns:209 Liste de PDB IDs210 """211 try:212 # Query JSON pour RCSB search213 query = {214 "query": {215 "type": "terminal",216 "service": "text",217 "parameters": {218 "attribute": "rcsb_entity_source_organism.rcsb_gene_name.value",219 "operator": "exact_match",220 "value": gene_name.upper()221 }222 },223 "return_type": "entry",224 "request_options": {225 "results_content_type": ["experimental"],226 "sort": [227 {228 "sort_by": "rcsb_accession_info.initial_release_date",229 "direction": "desc"230 }231 ],232 "scoring_strategy": "combined",233 "paginate": {234 "start": 0,235 "rows": max_results236 }237 }238 }239 240 response = self.session.post(241 self.PDB_SEARCH,242 json=query,243 timeout=30244 )245 246 if response.status_code != 200:247 logger.warning(f"Search failed for {gene_name}")248 return []249 250 data = response.json()251 pdb_ids = [result['identifier'] for result in data.get('result_set', [])]252 253 logger.info(f"Found {len(pdb_ids)} structures for {gene_name}")254 return pdb_ids255 256 except Exception as e:257 logger.error(f"Error searching structures: {str(e)}")258 return []259 260 # ========================================================================261 # BINDING SITE ANALYSIS262 # ========================================================================263 264 def identify_binding_sites(self, pdb_id: str) -> List[BindingSite]:265 """266 Identifie sites de liaison dans une structure267 268 Args:269 pdb_id: Code PDB270 271 Returns:272 Liste de BindingSite273 """274 try:275 # Récupérer binding sites depuis PDB276 url = f"{self.PDB_API}/entry/{pdb_id.upper()}"277 response = self.session.get(url, timeout=30)278 279 if response.status_code != 200:280 return []281 282 data = response.json()283 284 # Parser binding sites (version simplifiée)285 binding_sites = []286 287 # Sites définis par ligands288 ligands = data.get('pdbx_entity_nonpoly', [])289 290 for i, ligand in enumerate(ligands[:5], 1): # Max 5 sites291 # Données simulées pour démonstration292 # En production, utiliser fpocket ou autre algorithme293 site = BindingSite(294 site_id=f"SITE_{i}",295 residues=self._generate_site_residues(20),296 center=(np.random.uniform(-20, 20), 297 np.random.uniform(-20, 20),298 np.random.uniform(-20, 20)),299 volume=np.random.uniform(100, 800),300 depth=np.random.uniform(5, 15),301 hydrophobic_ratio=np.random.uniform(0.3, 0.6),302 polar_ratio=np.random.uniform(0.2, 0.4),303 charged_ratio=np.random.uniform(0.1, 0.3),304 druggability_score=np.random.uniform(0.5, 0.95),305 rank=i306 )307 binding_sites.append(site)308 309 # Trier par druggability310 binding_sites.sort(key=lambda x: x.druggability_score, reverse=True)311 312 # Réassigner ranks313 for i, site in enumerate(binding_sites, 1):314 site.rank = i315 316 logger.info(f"Identified {len(binding_sites)} binding sites in {pdb_id}")317 return binding_sites318 319 except Exception as e:320 logger.error(f"Error identifying binding sites: {str(e)}")321 return []322 323 def _generate_site_residues(self, count: int) -> List[str]:324 """Génère liste de résidus pour un site (simplifié)"""325 aa_codes = ['ALA', 'ARG', 'ASN', 'ASP', 'CYS', 'GLN', 'GLU', 'GLY', 326 'HIS', 'ILE', 'LEU', 'LYS', 'MET', 'PHE', 'PRO', 'SER', 327 'THR', 'TRP', 'TYR', 'VAL']328 329 residues = []330 for i in range(count):331 aa = np.random.choice(aa_codes)332 num = np.random.randint(1, 300)333 residues.append(f"{aa}{num}")334 335 return residues336 337 # ========================================================================338 # STRUCTURAL FEATURES339 # ========================================================================340 341 def analyze_secondary_structure(self, pdb_id: str) -> List[StructuralFeature]:342 """343 Analyse structure secondaire (hélices, feuillets, etc.)344 345 Args:346 pdb_id: Code PDB347 348 Returns:349 Liste de StructuralFeature350 """351 # Version simplifiée - nécessite parsing PDB complet en production352 features = []353 354 # Simuler quelques structures secondaires355 feature_types = ['helix', 'sheet', 'turn', 'coil']356 357 current_pos = 1358 for _ in range(np.random.randint(5, 15)):359 feat_type = np.random.choice(feature_types)360 length = np.random.randint(5, 30)361 362 feature = StructuralFeature(363 feature_type=feat_type,364 start_residue=current_pos,365 end_residue=current_pos + length - 1,366 length=length,367 sequence='X' * length # Placeholder368 )369 features.append(feature)370 371 current_pos += length372 373 logger.info(f"Analyzed secondary structure for {pdb_id}: {len(features)} features")374 return features375 376 # ========================================================================377 # CAVITY ANALYSIS378 # ========================================================================379 380 def analyze_cavities(self, pdb_id: str) -> List[CavityAnalysis]:381 """382 Analyse cavités/poches dans la structure383 384 Args:385 pdb_id: Code PDB386 387 Returns:388 Liste de CavityAnalysis389 """390 # Version simplifiée - en production utiliser fpocket ou POVME391 cavities = []392 393 for i in range(np.random.randint(2, 6)):394 cavity = CavityAnalysis(395 cavity_id=f"CAV_{i+1}",396 volume=np.random.uniform(50, 500),397 surface_area=np.random.uniform(100, 1000),398 depth=np.random.uniform(3, 12),399 sphericity=np.random.uniform(0.5, 0.9),400 elongation=np.random.uniform(1.0, 3.0),401 residue_composition={402 'hydrophobic': np.random.randint(5, 20),403 'polar': np.random.randint(3, 15),404 'charged': np.random.randint(1, 8)405 },406 hydrophobicity=np.random.uniform(0.3, 0.7),407 solvent_accessible=np.random.choice([True, False]),408 buried=np.random.choice([True, False])409 )410 cavities.append(cavity)411 412 # Trier par volume413 cavities.sort(key=lambda x: x.volume, reverse=True)414 415 logger.info(f"Analyzed {len(cavities)} cavities in {pdb_id}")416 return cavities417 418 # ========================================================================419 # QUALITY ASSESSMENT420 # ========================================================================421 422 def assess_structure_quality(self, pdb_id: str) -> Dict[str, Any]:423 """424 Évalue qualité d'une structure425 426 Args:427 pdb_id: Code PDB428 429 Returns:430 Dict avec métriques de qualité431 """432 struct = self.get_structure_info(pdb_id)433 434 if not struct:435 return {}436 437 quality_metrics = {438 'resolution_category': self._categorize_resolution(struct.resolution),439 'resolution_score': self._score_resolution(struct.resolution),440 'r_factors_valid': self._validate_r_factors(struct.r_free, struct.r_work),441 'method_reliability': self._assess_method(struct.method),442 'completeness': 'Unknown', # Nécessite données complémentaires443 'overall_quality': 'Unknown'444 }445 446 # Score global447 if struct.resolution:448 if struct.resolution < 2.0 and quality_metrics['r_factors_valid']:449 quality_metrics['overall_quality'] = 'Excellent'450 elif struct.resolution < 2.5:451 quality_metrics['overall_quality'] = 'Good'452 elif struct.resolution < 3.0:453 quality_metrics['overall_quality'] = 'Acceptable'454 else:455 quality_metrics['overall_quality'] = 'Low'456 457 return quality_metrics458 459 def _categorize_resolution(self, resolution: Optional[float]) -> str:460 """Catégorise résolution"""461 if not resolution:462 return 'Unknown'463 464 if resolution < 1.5:465 return 'Atomic (< 1.5 Å)'466 elif resolution < 2.0:467 return 'High (1.5-2.0 Å)'468 elif resolution < 2.5:469 return 'Medium-High (2.0-2.5 Å)'470 elif resolution < 3.0:471 return 'Medium (2.5-3.0 Å)'472 else:473 return 'Low (> 3.0 Å)'474 475 def _score_resolution(self, resolution: Optional[float]) -> float:476 """Score de résolution (0-1)"""477 if not resolution:478 return 0.0479 480 # Meilleure résolution = score plus élevé481 if resolution < 1.5:482 return 1.0483 elif resolution < 2.0:484 return 0.9485 elif resolution < 2.5:486 return 0.7487 elif resolution < 3.0:488 return 0.5489 else:490 return 0.3491 492 def _validate_r_factors(self, r_free: Optional[float], r_work: Optional[float]) -> bool:493 """Valide R-factors"""494 if not r_free or not r_work:495 return False496 497 # R-free doit être légèrement > R-work498 # et tous deux < 0.30 généralement499 return (500 r_free > r_work and501 r_free - r_work < 0.05 and502 r_free < 0.30 and503 r_work < 0.25504 )505 506 def _assess_method(self, method: str) -> str:507 """Évalue fiabilité de la méthode"""508 method_upper = method.upper()509 510 if 'X-RAY' in method_upper or 'DIFFRACTION' in method_upper:511 return 'High'512 elif 'NMR' in method_upper:513 return 'Medium-High'514 elif 'ELECTRON' in method_upper or 'CRYO-EM' in method_upper:515 return 'Medium-High'516 elif 'MODEL' in method_upper or 'PREDICT' in method_upper:517 return 'Low'518 else:519 return 'Unknown'520 521 # ========================================================================522 # DRUGGABILITY ASSESSMENT523 # ========================================================================524 525 def assess_pocket_druggability(self, 526 binding_site: BindingSite) -> Dict[str, Any]:527 """528 Évalue druggability d'un site de liaison529 530 Args:531 binding_site: Site à évaluer532 533 Returns:534 Dict avec évaluation535 """536 assessment = {537 'site_id': binding_site.site_id,538 'volume_category': self._categorize_volume(binding_site.volume),539 'shape_quality': self._assess_shape(binding_site.depth, binding_site.volume),540 'composition_score': self._score_composition(541 binding_site.hydrophobic_ratio,542 binding_site.polar_ratio,543 binding_site.charged_ratio544 ),545 'overall_druggability': binding_site.druggability_score,546 'recommendation': ''547 }548 549 # Recommandation550 if binding_site.druggability_score > 0.8:551 assessment['recommendation'] = 'Excellent druggable pocket - prioritize for screening'552 elif binding_site.druggability_score > 0.6:553 assessment['recommendation'] = 'Good druggable pocket - suitable for drug design'554 elif binding_site.druggability_score > 0.4:555 assessment['recommendation'] = 'Moderately druggable - may require fragment-based approach'556 else:557 assessment['recommendation'] = 'Challenging pocket - consider allosteric sites'558 559 return assessment560 561 def _categorize_volume(self, volume: float) -> str:562 """Catégorise volume de poche"""563 if volume < 100:564 return 'Very Small'565 elif volume < 300:566 return 'Small'567 elif volume < 600:568 return 'Medium'569 else:570 return 'Large'571 572 def _assess_shape(self, depth: float, volume: float) -> str:573 """Évalue forme de la poche"""574 ratio = depth / (volume ** (1/3))575 576 if ratio > 0.5:577 return 'Deep and narrow (favorable)'578 elif ratio > 0.3:579 return 'Moderate depth'580 else:581 return 'Shallow and wide (less favorable)'582 583 def _score_composition(self, hydrophobic: float, polar: float, charged: float) -> float:584 """Score composition du site"""585 # Balance idéale: 40-60% hydrophobic, 20-40% polar, 10-20% charged586 score = 1.0587 588 if not (0.4 <= hydrophobic <= 0.6):589 score -= 0.2590 if not (0.2 <= polar <= 0.4):591 score -= 0.1592 if charged > 0.3:593 score -= 0.1594 595 return max(0.0, score)596 597 # ========================================================================598 # COMPREHENSIVE ANALYSIS599 # ========================================================================600 601 def analyze_structure_comprehensive(self, pdb_id: str) -> Dict[str, Any]:602 """603 Analyse structurale complète604 605 Args:606 pdb_id: Code PDB607 608 Returns:609 Dict avec toutes les analyses610 """611 logger.info(f"Starting comprehensive structural analysis for {pdb_id}")612 613 results = {614 'pdb_id': pdb_id.upper(),615 'structure': self.get_structure_info(pdb_id),616 'quality': self.assess_structure_quality(pdb_id),617 'binding_sites': self.identify_binding_sites(pdb_id),618 'secondary_structure': self.analyze_secondary_structure(pdb_id),619 'cavities': self.analyze_cavities(pdb_id)620 }621 622 # Évaluation des sites623 if results['binding_sites']:624 results['site_assessments'] = [625 self.assess_pocket_druggability(site)626 for site in results['binding_sites']627 ]628 629 logger.info(f"Comprehensive structural analysis completed for {pdb_id}")630 return results631 632 633# ============================================================================634# UTILITY FUNCTIONS635# ============================================================================636 637def create_structure_report(analysis_results: Dict[str, Any]) -> pd.DataFrame:638 """Crée rapport d'analyse structurale"""639 640 if not analysis_results:641 return pd.DataFrame()642 643 report_data = []644 645 # Info structure646 if 'structure' in analysis_results and analysis_results['structure']:647 struct = analysis_results['structure']648 report_data.extend([649 {'Category': 'Structure', 'Property': 'PDB ID', 'Value': struct.pdb_id},650 {'Category': 'Structure', 'Property': 'Resolution', 'Value': f"{struct.resolution:.2f} Å" if struct.resolution else 'N/A'},651 {'Category': 'Structure', 'Property': 'Method', 'Value': struct.method},652 {'Category': 'Structure', 'Property': 'Chains', 'Value': struct.chain_count},653 {'Category': 'Structure', 'Property': 'Residues', 'Value': struct.residue_count},654 {'Category': 'Structure', 'Property': 'Ligands', 'Value': len(struct.ligands)},655 ])656 657 # Qualité658 if 'quality' in analysis_results:659 quality = analysis_results['quality']660 report_data.extend([661 {'Category': 'Quality', 'Property': 'Resolution Category', 'Value': quality.get('resolution_category', 'Unknown')},662 {'Category': 'Quality', 'Property': 'Overall Quality', 'Value': quality.get('overall_quality', 'Unknown')},663 ])664 665 # Sites de liaison666 if 'binding_sites' in analysis_results:667 sites = analysis_results['binding_sites']668 report_data.append({669 'Category': 'Binding Sites',670 'Property': '# Identified Sites',671 'Value': len(sites)672 })673 674 if sites:675 best_site = sites[0]676 report_data.extend([677 {'Category': 'Binding Sites', 'Property': 'Best Site Volume', 'Value': f"{best_site.volume:.1f} Ų"},678 {'Category': 'Binding Sites', 'Property': 'Druggability Score', 'Value': f"{best_site.druggability_score:.2f}"},679 ])680 681 return pd.DataFrame(report_data)682 683 684if __name__ == "__main__":685 # Test686 analyzer = StructuralAnalyzer()687 688 # Test avec structure connue689 print("Testing with PDB 1A0S...")690 result = analyzer.analyze_structure_comprehensive("1A0S")691 692 if result.get('structure'):693 report = create_structure_report(result)694 print("\n" + "="*60)695 print("STRUCTURAL ANALYSIS REPORT")696 print("="*60)697 print(report.to_string(index=False))698 