CoolFace
Apppublic

ahmedmiloudi/BioTechLabAI

sourceHugging Faceapache-2.0updated 8mo agoView on Hugging Face
1likes
structural_analysis.py698 linesDownload Raw Back to src
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