import gradio as gr import re import pandas as pd from io import StringIO import rdkit from rdkit import Chem from rdkit.Chem import AllChem, Draw import numpy as np from PIL import Image, ImageDraw, ImageFont import matplotlib.pyplot as plt import matplotlib.patches as patches from io import BytesIO import re from rdkit import Chem class PeptideAnalyzer: def __init__(self): self.bond_patterns = [ r'OC\(=O\)', # ester bond r'N\(C\)C\(=O\)', # N-methylated peptide bond r'N[12]?C\(=O\)', # peptide bond (including Pro N1/N2) r'C\(=O\)N\(C\)', # N-methylated peptide bond reverse r'C\(=O\)N' # peptide bond reverse ] def is_peptide(self, smiles): """Check if the SMILES represents a peptide structure""" mol = Chem.MolFromSmiles(smiles) if mol is None: return False # Look for peptide bonds: NC(=O) pattern peptide_bond_pattern = Chem.MolFromSmarts('[NH][C](=O)') if mol.HasSubstructMatch(peptide_bond_pattern): return True # Look for N-methylated peptide bonds: N(C)C(=O) pattern n_methyl_pattern = Chem.MolFromSmarts('[N;H0;$(NC)](C)[C](=O)') if mol.HasSubstructMatch(n_methyl_pattern): return True # Look for ester bonds in cyclic depsipeptides: OC(=O) pattern ester_bond_pattern = Chem.MolFromSmarts('O[C](=O)') if mol.HasSubstructMatch(ester_bond_pattern): return True return False def is_cyclic(self, smiles): """ Determine if SMILES represents a cyclic peptide Returns: (is_cyclic, peptide_cycles, aromatic_cycles) """ cycle_info = {} # Find all cycle numbers and their contexts for match in re.finditer(r'(\d)', smiles): number = match.group(1) position = match.start(1) if number not in cycle_info: cycle_info[number] = [] cycle_info[number].append({ 'position': position, 'full_context': smiles[max(0, position-3):min(len(smiles), position+4)] }) # Check each cycle peptide_cycles = [] aromatic_cycles = [] for number, occurrences in cycle_info.items(): if len(occurrences) != 2: continue start, end = occurrences[0]['position'], occurrences[1]['position'] segment = smiles[start:end+1] # Check for aromatic rings full_context = smiles[max(0,start-10):min(len(smiles),end+10)] is_aromatic = ('c2ccccc2' in full_context and len(segment) < 20) or \ ('c1ccccc1' in full_context and len(segment) < 20) # Check for peptide bonds peptide_patterns = [ 'C(=O)N', # Regular peptide bond 'C(=O)N(C)', # N-methylated peptide bond 'C(=O)N1', # Cyclic peptide bond 'C(=O)N2' # Cyclic peptide bond ] has_peptide_bond = any(pattern in segment for pattern in peptide_patterns) and \ len(segment) > 20 if is_aromatic and len(segment) < 20: aromatic_cycles.append(number) elif has_peptide_bond: peptide_cycles.append(number) return len(peptide_cycles) > 0, peptide_cycles, aromatic_cycles def split_on_bonds(self, smiles): """Split SMILES into segments with simplified Pro handling""" positions = [] used = set() # Find Gly pattern first gly_pattern = r'NCC\(=O\)' for match in re.finditer(gly_pattern, smiles): if not any(p in range(match.start(), match.end()) for p in used): positions.append({ 'start': match.start(), 'end': match.end(), 'type': 'gly', 'pattern': match.group() }) used.update(range(match.start(), match.end())) # Then find all bonds, including N2C(=O) bond_patterns = [ (r'OC\(=O\)', 'ester'), (r'N\(C\)C\(=O\)', 'n_methyl'), (r'N[12]C\(=O\)', 'peptide'), # Pro peptide bonds (r'NC\(=O\)', 'peptide'), # Regular peptide bonds (r'C\(=O\)N\(C\)', 'n_methyl'), (r'C\(=O\)N[12]?', 'peptide') ] for pattern, bond_type in bond_patterns: for match in re.finditer(pattern, smiles): if not any(p in range(match.start(), match.end()) for p in used): positions.append({ 'start': match.start(), 'end': match.end(), 'type': bond_type, 'pattern': match.group() }) used.update(range(match.start(), match.end())) # Sort by position positions.sort(key=lambda x: x['start']) # Create segments segments = [] if positions: # First segment if positions[0]['start'] > 0: segments.append({ 'content': smiles[0:positions[0]['start']], 'bond_after': positions[0]['pattern'] }) # Process segments for i in range(len(positions)-1): current = positions[i] next_pos = positions[i+1] if current['type'] == 'gly': segments.append({ 'content': 'NCC(=O)', 'bond_before': positions[i-1]['pattern'] if i > 0 else None, 'bond_after': next_pos['pattern'] }) else: content = smiles[current['end']:next_pos['start']] if content: segments.append({ 'content': content, 'bond_before': current['pattern'], 'bond_after': next_pos['pattern'] }) # Last segment if positions[-1]['end'] < len(smiles): segments.append({ 'content': smiles[positions[-1]['end']:], 'bond_before': positions[-1]['pattern'] }) return segments def identify_residue(self, segment): """Identify residue with Pro reconstruction""" content = segment['content'] mods = self.get_modifications(segment) # Special handling for Pro: reconstruct the complete pattern if (segment.get('bond_after') == 'N2C(=O)' and 'CCC' in content) or \ ('CCCN2' in content and content.endswith('=O')): # End case # Reconstruct the complete Pro pattern if '[C@@H]2' in content or '[C@H]2' in content: return 'Pro', mods if ('C[C@H](CCCC)' in content or 'C[C@@H](CCCC)' in content) and 'CC(C)' not in content: return 'Nle', mods # Ornithine (Orn) - 3-carbon chain with NH2 if ('C[C@H](CCCN)' in content or 'C[C@@H](CCCN)' in content) and 'CC(C)' not in content: return 'Orn', mods # 2-Naphthylalanine (2Nal) - distinct from Phe pattern if ('Cc3cc2ccccc2c3' in content) and ('C[C@H]' in content or 'C[C@@H]' in content): return '2Nal', mods # Cyclohexylalanine (Cha) - already in your code but moved here for clarity if 'N2CCCCC2' in content or 'CCCCC2' in content: return 'Cha', mods # Aminobutyric acid (Abu) - 2-carbon chain if ('C[C@H](CC)' in content or 'C[C@@H](CC)' in content) and not any(p in content for p in ['CC(C)', 'CCCC', 'CCC(C)']): return 'Abu', mods # Pipecolic acid (Pip) - 6-membered ring like Pro if ('N3CCCCC3' in content or 'CCCCC3' in content) and ('C[C@H]' in content or 'C[C@@H]' in content): return 'Pip', mods # Cyclohexylglycine (Chg) - direct cyclohexyl without CH2 if ('C[C@H](C1CCCCC1)' in content or 'C[C@@H](C1CCCCC1)' in content): return 'Chg', mods # 4-Fluorophenylalanine (4F-Phe) if ('Cc2ccc(F)cc2' in content) and ('C[C@H]' in content or 'C[C@@H]' in content): return '4F-Phe', mods # Regular residue identification if 'NCC(=O)' in content: return 'Gly', mods if 'CC(C)C[C@H]' in content or 'CC(C)C[C@@H]' in content: return 'Leu', mods if '[C@@H](CC(C)C)' in content or '[C@H](CC(C)C)' in content: return 'Leu', mods if ('C(C)C[C@H]' in content or 'C(C)C[C@@H]' in content) and 'CC(C)C' not in content: return 'Ile', mods if '[C@@H]([C@@H](C)O)' in content or '[C@H]([C@H](C)O)' in content: return 'Thr', mods if '[C@H](Cc2ccccc2)' in content or '[C@@H](Cc2ccccc2)' in content: return 'Phe', mods if '[C@H](C(C)C)' in content or '[C@@H](C(C)C)' in content: if not any(p in content for p in ['CC(C)C[C@H]', 'CC(C)C[C@@H]']): return 'Val', mods if '[C@H](COC(C)(C)C)' in content or '[C@@H](COC(C)(C)C)' in content: return 'O-tBu', mods if ('[C@H](C)' in content or '[C@@H](C)' in content): if not any(p in content for p in ['C(C)C', 'COC', 'CN(', 'C(C)O']): return 'Ala', mods return None, mods def get_modifications(self, segment): """Get modifications based on bond types""" mods = [] if segment.get('bond_after'): if 'N(C)' in segment['bond_after'] or segment['bond_after'].startswith('C(=O)N(C)'): mods.append('N-Me') if 'OC(=O)' in segment['bond_after']: mods.append('O-linked') return mods def analyze_structure(self, smiles): """Main analysis function""" print("\nAnalyzing structure:", smiles) # Split into segments segments = self.split_on_bonds(smiles) print("\nSegment Analysis:") sequence = [] for i, segment in enumerate(segments): print(f"\nSegment {i}:") print(f"Content: {segment['content']}") print(f"Bond before: {segment.get('bond_before', 'None')}") print(f"Bond after: {segment.get('bond_after', 'None')}") residue, mods = self.identify_residue(segment) if residue: if mods: sequence.append(f"{residue}({','.join(mods)})") else: sequence.append(residue) print(f"Identified as: {residue}") print(f"Modifications: {mods}") else: print(f"Warning: Could not identify residue in segment: {segment['content']}") # Check if cyclic is_cyclic = 'N1' in smiles or 'N2' in smiles final_sequence = f"cyclo({'-'.join(sequence)})" if is_cyclic else '-'.join(sequence) print(f"\nFinal sequence: {final_sequence}") return final_sequence """ def annotate_cyclic_structure(mol, sequence): '''Create annotated 2D structure with clear, non-overlapping residue labels''' # Generate 2D coordinates # Generate 2D coordinates AllChem.Compute2DCoords(mol) # Create drawer with larger size for annotations drawer = Draw.rdMolDraw2D.MolDraw2DCairo(2000, 2000) # Even larger size # Get residue list and reverse it to match structural representation if sequence.startswith('cyclo('): residues = sequence[6:-1].split('-') else: residues = sequence.split('-') residues = list(reversed(residues)) # Reverse the sequence # Draw molecule first to get its bounds drawer.drawOptions().addAtomIndices = False drawer.DrawMolecule(mol) drawer.FinishDrawing() # Convert to PIL Image img = Image.open(BytesIO(drawer.GetDrawingText())) draw = ImageDraw.Draw(img) try: # Try to use DejaVuSans as it's commonly available on Linux systems font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 60) small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 60) except OSError: try: # Fallback to Arial if available (common on Windows) font = ImageFont.truetype("arial.ttf", 60) small_font = ImageFont.truetype("arial.ttf", 60) except OSError: # If no TrueType fonts are available, fall back to default print("Warning: TrueType fonts not available, using default font") font = ImageFont.load_default() small_font = ImageFont.load_default() # Get molecule bounds conf = mol.GetConformer() positions = [] for i in range(mol.GetNumAtoms()): pos = conf.GetAtomPosition(i) positions.append((pos.x, pos.y)) x_coords = [p[0] for p in positions] y_coords = [p[1] for p in positions] min_x, max_x = min(x_coords), max(x_coords) min_y, max_y = min(y_coords), max(y_coords) # Calculate scaling factors scale = 150 # Increased scale factor center_x = 1000 # Image center center_y = 1000 # Add residue labels in a circular arrangement around the structure n_residues = len(residues) radius = 700 # Distance of labels from center # Start from the rightmost point (3 o'clock position) and go counterclockwise # Offset by -3 positions to align with structure offset = 0 # Adjust this value to match the structure alignment for i, residue in enumerate(residues): # Calculate position in a circle around the structure # Start from 0 (3 o'clock) and go counterclockwise angle = -(2 * np.pi * ((i + offset) % n_residues) / n_residues) # Calculate label position label_x = center_x + radius * np.cos(angle) label_y = center_y + radius * np.sin(angle) # Draw residue label text = f"{i+1}. {residue}" bbox = draw.textbbox((label_x, label_y), text, font=font) padding = 10 draw.rectangle([bbox[0]-padding, bbox[1]-padding, bbox[2]+padding, bbox[3]+padding], fill='white', outline='white') draw.text((label_x, label_y), text, font=font, fill='black', anchor="mm") # Add sequence at the top with white background seq_text = f"Sequence: {sequence}" bbox = draw.textbbox((center_x, 100), seq_text, font=small_font) padding = 10 draw.rectangle([bbox[0]-padding, bbox[1]-padding, bbox[2]+padding, bbox[3]+padding], fill='white', outline='white') draw.text((center_x, 100), seq_text, font=small_font, fill='black', anchor="mm") return img """ def annotate_cyclic_structure(mol, sequence): """Create structure visualization with just the sequence header""" # Generate 2D coordinates AllChem.Compute2DCoords(mol) # Create drawer with larger size for annotations drawer = Draw.rdMolDraw2D.MolDraw2DCairo(2000, 2000) # Draw molecule first drawer.drawOptions().addAtomIndices = False drawer.DrawMolecule(mol) drawer.FinishDrawing() # Convert to PIL Image img = Image.open(BytesIO(drawer.GetDrawingText())) draw = ImageDraw.Draw(img) try: small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 60) except OSError: try: small_font = ImageFont.truetype("arial.ttf", 60) except OSError: print("Warning: TrueType fonts not available, using default font") small_font = ImageFont.load_default() # Add just the sequence header at the top seq_text = f"Sequence: {sequence}" bbox = draw.textbbox((1000, 100), seq_text, font=small_font) padding = 10 draw.rectangle([bbox[0]-padding, bbox[1]-padding, bbox[2]+padding, bbox[3]+padding], fill='white', outline='white') draw.text((1000, 100), seq_text, font=small_font, fill='black', anchor="mm") return img def create_enhanced_linear_viz(sequence, smiles): """Create an enhanced linear representation using PeptideAnalyzer""" analyzer = PeptideAnalyzer() # Create analyzer instance # Create figure with two subplots fig = plt.figure(figsize=(15, 10)) gs = fig.add_gridspec(2, 1, height_ratios=[1, 2]) ax_struct = fig.add_subplot(gs[0]) ax_detail = fig.add_subplot(gs[1]) # Parse sequence and get residues if sequence.startswith('cyclo('): residues = sequence[6:-1].split('-') else: residues = sequence.split('-') # Get segments using analyzer segments = analyzer.split_on_bonds(smiles) # Debug print print(f"Number of residues: {len(residues)}") print(f"Number of segments: {len(segments)}") # Top subplot - Basic structure ax_struct.set_xlim(0, 10) ax_struct.set_ylim(0, 2) num_residues = len(residues) spacing = 9.0 / (num_residues - 1) if num_residues > 1 else 9.0 # Draw basic structure y_pos = 1.5 for i in range(num_residues): x_pos = 0.5 + i * spacing # Draw amino acid box rect = patches.Rectangle((x_pos-0.3, y_pos-0.2), 0.6, 0.4, facecolor='lightblue', edgecolor='black') ax_struct.add_patch(rect) # Draw connecting bonds if not the last residue if i < num_residues - 1: segment = segments[i] if i < len(segments) else None if segment: # Determine bond type from segment info bond_type = 'ester' if 'O-linked' in segment.get('bond_after', '') else 'peptide' is_n_methylated = 'N-Me' in segment.get('bond_after', '') bond_color = 'red' if bond_type == 'ester' else 'black' linestyle = '--' if bond_type == 'ester' else '-' # Draw bond line ax_struct.plot([x_pos+0.3, x_pos+spacing-0.3], [y_pos, y_pos], color=bond_color, linestyle=linestyle, linewidth=2) # Add bond type label mid_x = x_pos + spacing/2 bond_label = f"{bond_type}" if is_n_methylated: bond_label += "\n(N-Me)" ax_struct.text(mid_x, y_pos+0.1, bond_label, ha='center', va='bottom', fontsize=10, color=bond_color) # Add residue label ax_struct.text(x_pos, y_pos-0.5, residues[i], ha='center', va='top', fontsize=14) # Bottom subplot - Detailed breakdown ax_detail.set_ylim(0, len(segments)+1) ax_detail.set_xlim(0, 1) # Create detailed breakdown segment_y = len(segments) # Start from top for i, segment in enumerate(segments): y = segment_y - i # Check if this is a bond or residue residue, mods = analyzer.identify_residue(segment) if residue: text = f"Residue {i+1}: {residue}" if mods: text += f" ({', '.join(mods)})" color = 'blue' else: # Must be a bond text = f"Bond {i}: " if 'O-linked' in segment.get('bond_after', ''): text += "ester" elif 'N-Me' in segment.get('bond_after', ''): text += "peptide (N-methylated)" else: text += "peptide" color = 'red' # Add segment analysis ax_detail.text(0.05, y, text, fontsize=12, color=color) ax_detail.text(0.5, y, f"SMILES: {segment.get('content', '')}", fontsize=10, color='gray') # If cyclic, add connection indicator if sequence.startswith('cyclo('): ax_struct.annotate('', xy=(9.5, y_pos), xytext=(0.5, y_pos), arrowprops=dict(arrowstyle='<->', color='red', lw=2)) ax_struct.text(5, y_pos+0.3, 'Cyclic Connection', ha='center', color='red', fontsize=14) # Add titles and adjust layout ax_struct.set_title("Peptide Structure Overview", pad=20) ax_detail.set_title("Segment Analysis Breakdown", pad=20) # Remove axes for ax in [ax_struct, ax_detail]: ax.set_xticks([]) ax.set_yticks([]) ax.axis('off') plt.tight_layout() return fig def process_input(smiles_input=None, file_obj=None, show_linear=False, show_segment_details=False): """Process input and create visualizations using PeptideAnalyzer""" analyzer = PeptideAnalyzer() # Handle direct SMILES input if smiles_input: smiles = smiles_input.strip() # First check if it's a peptide using analyzer's method if not analyzer.is_peptide(smiles): return "Error: Input SMILES does not appear to be a peptide structure.", None, None try: # Create molecule mol = Chem.MolFromSmiles(smiles) if mol is None: return "Error: Invalid SMILES notation.", None, None # Use analyzer to get sequence segments = analyzer.split_on_bonds(smiles) # Process segments and build sequence sequence_parts = [] output_text = "" # Only include segment analysis in output if requested if show_segment_details: output_text += "Segment Analysis:\n" for i, segment in enumerate(segments): output_text += f"\nSegment {i}:\n" output_text += f"Content: {segment['content']}\n" output_text += f"Bond before: {segment.get('bond_before', 'None')}\n" output_text += f"Bond after: {segment.get('bond_after', 'None')}\n" residue, mods = analyzer.identify_residue(segment) if residue: if mods: sequence_parts.append(f"{residue}({','.join(mods)})") else: sequence_parts.append(residue) output_text += f"Identified as: {residue}\n" output_text += f"Modifications: {mods}\n" else: output_text += f"Warning: Could not identify residue in segment: {segment['content']}\n" output_text += "\n" else: # Just build sequence without detailed analysis in output for segment in segments: residue, mods = analyzer.identify_residue(segment) if residue: if mods: sequence_parts.append(f"{residue}({','.join(mods)})") else: sequence_parts.append(residue) # Check if cyclic using analyzer's method is_cyclic, peptide_cycles, aromatic_cycles = analyzer.is_cyclic(smiles) sequence = f"cyclo({'-'.join(sequence_parts)})" if is_cyclic else '-'.join(sequence_parts) # Create cyclic structure visualization img_cyclic = annotate_cyclic_structure(mol, sequence) # Create linear representation if requested img_linear = None if show_linear: fig_linear = create_enhanced_linear_viz(sequence, smiles) buf = BytesIO() fig_linear.savefig(buf, format='png', bbox_inches='tight', dpi=300) buf.seek(0) img_linear = Image.open(buf) plt.close(fig_linear) # Add summary to output summary = "Summary:\n" summary += f"Sequence: {sequence}\n" summary += f"Is Cyclic: {'Yes' if is_cyclic else 'No'}\n" if is_cyclic: summary += f"Peptide Cycles: {', '.join(peptide_cycles)}\n" summary += f"Aromatic Cycles: {', '.join(aromatic_cycles)}\n" return summary + output_text, img_cyclic, img_linear except Exception as e: return f"Error processing SMILES: {str(e)}", None, None # Handle file input if file_obj is not None: try: # Handle file content if hasattr(file_obj, 'name'): with open(file_obj.name, 'r') as f: content = f.read() else: content = file_obj.decode('utf-8') if isinstance(file_obj, bytes) else str(file_obj) output_text = "" for line in content.splitlines(): smiles = line.strip() if smiles: # Check if it's a peptide if not analyzer.is_peptide(smiles): output_text += f"Skipping non-peptide SMILES: {smiles}\n" continue # Process this SMILES segments = analyzer.split_on_bonds(smiles) sequence_parts = [] # Add segment details if requested if show_segment_details: output_text += f"\nSegment Analysis for SMILES: {smiles}\n" for i, segment in enumerate(segments): output_text += f"\nSegment {i}:\n" output_text += f"Content: {segment['content']}\n" output_text += f"Bond before: {segment.get('bond_before', 'None')}\n" output_text += f"Bond after: {segment.get('bond_after', 'None')}\n" residue, mods = analyzer.identify_residue(segment) if residue: if mods: sequence_parts.append(f"{residue}({','.join(mods)})") else: sequence_parts.append(residue) output_text += f"Identified as: {residue}\n" output_text += f"Modifications: {mods}\n" else: for segment in segments: residue, mods = analyzer.identify_residue(segment) if residue: if mods: sequence_parts.append(f"{residue}({','.join(mods)})") else: sequence_parts.append(residue) # Get cyclicity and create sequence is_cyclic, peptide_cycles, aromatic_cycles = analyzer.is_cyclic(smiles) sequence = f"cyclo({'-'.join(sequence_parts)})" if is_cyclic else '-'.join(sequence_parts) output_text += f"\nSummary for SMILES: {smiles}\n" output_text += f"Sequence: {sequence}\n" output_text += f"Is Cyclic: {'Yes' if is_cyclic else 'No'}\n" if is_cyclic: output_text += f"Peptide Cycles: {', '.join(peptide_cycles)}\n" output_text += f"Aromatic Cycles: {', '.join(aromatic_cycles)}\n" output_text += "-" * 50 + "\n" return output_text, None, None except Exception as e: return f"Error processing file: {str(e)}", None, None return "No input provided.", None, None iface = gr.Interface( fn=process_input, # Your processing function inputs=[ gr.Textbox( label="Enter SMILES string", placeholder="Enter SMILES notation of peptide...", lines=2 ), gr.File( label="Or upload a text file with SMILES", file_types=[".txt"] ), gr.Checkbox( label="Show linear representation", value=False ), gr.Checkbox( label="Show segment details", value=False ) ], outputs=[ gr.Textbox( label="Analysis Results", lines=10 ), gr.Image( label="2D Structure with Annotations", type="pil" ), gr.Image( label="Linear Representation", type="pil" ) ], title="Peptide Structure Analyzer and Visualizer", description=""" Analyze and visualize peptide structures from SMILES notation: 1. Validates if the input is a peptide structure 2. Determines if the peptide is cyclic 3. Parses the amino acid sequence 4. Creates 2D structure visualization with residue annotations 5. Optional linear representation Input: Either enter a SMILES string directly or upload a text file containing SMILES strings Example SMILES strings (copy and paste): ``` CC(C)C[C@@H]1NC(=O)[C@@H](CC(C)C)N(C)C(=O)[C@@H](C)N(C)C(=O)[C@H](Cc2ccccc2)NC(=O)[C@H](CC(C)C)N(C)C(=O)[C@H]2CCCN2C1=O ``` ``` C(C)C[C@@H]1NC(=O)[C@@H]2CCCN2C(=O)[C@@H](CC(C)C)NC(=O)[C@@H](CC(C)C)N(C)C(=O)[C@H](C)NC(=O)[C@H](Cc2ccccc2)NC1=O ``` ``` CC(C)C[C@H]1C(=O)N(C)[C@@H](Cc2ccccc2)C(=O)NCC(=O)N[C@H](C(=O)N2CCCCC2)CC(=O)N(C)CC(=O)N[C@@H]([C@@H](C)O)C(=O)N(C)[C@@H](C)C(=O)N[C@@H](COC(C)(C)C)C(=O)N(C)[C@@H](Cc2ccccc2)C(=O)N1C ``` """, flagging_mode="never" ) # Launch the app if __name__ == "__main__": iface.launch()