SMILES2PEPTIDE / app.py
yzhang@u.duke.nus.edu
add sequence 2 smiles feature
f652da3
import os
import gradio_client.utils as client_utils
# Monkey path gradio_client issue
_original = client_utils._json_schema_to_python_type
def _safe_json_schema_to_python_type(schema, defs=None):
if isinstance(schema, bool):
return "Any"
return _original(schema, defs)
client_utils._json_schema_to_python_type = _safe_json_schema_to_python_type
client_utils.json_schema_to_python_type = _safe_json_schema_to_python_type
import gradio as gr
import gradio.blocks
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 tempfile
from rdkit import Chem
from swisssidechain import all_aminos
from aminoacid_selective import specific_aminos
def _internal_from_cterm(cterm: str) -> str:
s = cterm.strip()
s = re.sub(r'C\(=O\)\[\*:\s*2\]\s*$', '', s) # drop trailing carbonyl anchor
s = re.sub(r'^\[\*:\s*1\]', '', s) # drop leading anchor
s = re.sub(r'^\(?N\)?', '', s) # drop leading N
return s
def _internal_from_nterm(nterm: str) -> str:
s = nterm.strip()
s = re.sub(r'^\[\*:\s*1\]', '', s) # drop leading anchor
s = re.sub(r'^\(?N\)?', '', s) # drop leading N
s = re.sub(r'C\(=O\)O\s*$', '', s) # drop trailing COOH
return s
def _chirality_agnostic_regex(literal_smiles: str) -> re.Pattern:
"""
Make a regex that matches the literal SMILES but ignores stereo/ring digit specifics.
- Escapes all chars
- Makes '@' optional (so [C@@H] / [C@H] / [CH] all match)
- Allows any ring digit where a digit appears
"""
esc = re.escape(literal_smiles)
# make any '@' optional (two steps to handle @@)
esc = esc.replace(r'\@\@', r'\@?\@?')
esc = esc.replace(r'\@', r'\@?')
# allow any ring digit(s) where digits appear
esc = re.sub(r'\\\d+', r'\\d+', esc)
return re.compile(esc)
class PeptideAnalyzer:
def __init__(self):
self.bond_patterns = [
#(r'OC\(=O\)', 'ester'), # Ester bond
(r'N\(C\)C\(=O\)', 'n_methyl'), # N-methylated peptide bond
(r'N[0-9]C\(=O\)', 'proline'), # Proline peptide bond
(r'NC\(=O\)', 'peptide'), # Standard peptide bond
(r'C\(=O\)N\(C\)', 'n_methyl_reverse'), # Reverse N-methylated
(r'C\(=O\)N[12]?', 'peptide_reverse') # Reverse peptide bond
]
self.complex_residue_patterns = [
(r'\[C[@]H\]\(CCCNC\(=O\)CCC\[C@@H\]\(NC\(=O\)CCCCCCCCCCCCCCCC\)C\(=O\)OC\(C\)\(C\)C\)', 'Kpg'),
(r'CCCCCCCCCCCCCCCCC\(=O\)N\[C@H\]\(CCCC\(=O\)NCCC\[C@@H\]', 'Kpg'),
(r'\[C@*H\]\(CSC\(c\d+ccccc\d+\)\(c\d+ccccc\d+\)c\d+ccc\(OC\)cc\d+\)', 'Cmt'),
(r'CSC\(c.*?c.*?OC\)', 'Cmt'),
(r'COc.*?ccc\(C\(SC', 'Cmt'),
(r'c2ccccc2\)c2ccccc2\)cc', 'Cmt'),
# Glu(OAll)
(r'C=CCOC\(=O\)CC\[C@@H\]', 'Eal'),
(r'\(C\)OP\(=O\)\(O\)OCc\d+ccccc\d+', 'Tpb'),
#(r'COc\d+ccc\(C\(SC\[C@@H\]\d+.*?\)\(c\d+ccccc\d+\)c\d+ccccc\d+\)cc\d+', 'Cmt-cyclic'),
# Dtg - Asp(OtBu)-(Dmb)Gly
(r'CN\(Cc\d+ccc\(OC\)cc\d+OC\)C\(=O\)\[C@H\]\(CC\(=O\)OC\(C\)\(C\)C\)', 'Dtg'),
(r'C\(=O\)N\(CC\d+=C\(C=C\(C=C\d+\)OC\)OC\)CC\(=O\)', 'Dtg'),
(r'N\[C@@H\]\(CC\(=O\)OC\(C\)\(C\)C\)C\(=O\)N\(CC\d+=C\(C=C\(C=C\d+\)OC\)OC\)CC\(=O\)', 'Dtg'),
]
# Three to one letter code mapping
self.three_to_one = {
'Ala': 'A', 'Cys': 'C', 'Asp': 'D', 'Glu': 'E',
'Phe': 'F', 'Gly': 'G', 'His': 'H', 'Ile': 'I',
'Lys': 'K', 'Leu': 'L', 'Met': 'M', 'Asn': 'N',
'Pro': 'P', 'Gln': 'Q', 'Arg': 'R', 'Ser': 'S',
'Thr': 'T', 'Val': 'V', 'Trp': 'W', 'Tyr': 'Y',
'ala': 'a', 'cys': 'c', 'asp': 'd', 'glu': 'e',
'phe': 'f', 'gly': 'g', 'his': 'h', 'ile': 'i',
'lys': 'k', 'leu': 'l', 'met': 'm', 'asn': 'n',
'pro': 'p', 'gln': 'q', 'arg': 'r', 'ser': 's',
'thr': 't', 'val': 'v', 'trp': 'w', 'tyr': 'y', 'Cmt-cyclic': 'Ĉ',
'Aib': 'Ŷ', 'Dtg': 'Ĝ', 'Cmt': 'Ĉ', 'Eal': 'Ė', 'Nml': "Ŀ", 'Nma': 'Ṃ',
'Kpg': 'Ƙ', 'Tpb': 'Ṯ', 'Cyl': 'Ċ', 'Nle': 'Ł', 'Hph': 'Ĥ', 'Cys-Cys': 'CC', 'cys-cys': 'cc',
}
self._build_swisssidechain_lookups()
def _build_swisssidechain_lookups(self):
self.exact_smiles_lookup = {}
self.clean_smiles_lookup = {}
self.uaa_internal_exact = {}
self.uaa_internal_patterns = []
for uaa_name, uaa_data in specific_aminos.items():
code = uaa_data["Code"]
smiles = uaa_data.get("SMILES", "")
nterm = uaa_data.get("nterm", "")
cterm = uaa_data.get("cterm", "")
letter = uaa_data.get("Letter")
# keep existing full-aa lookups
if smiles:
self.exact_smiles_lookup[smiles] = code
clean = self._remove_stereochemistry(smiles)
self.clean_smiles_lookup.setdefault(clean, []).append(code)
internal = ""
if cterm:
internal = _internal_from_cterm(cterm)
elif nterm:
internal = _internal_from_nterm(nterm)
if internal:
self.exact_smiles_lookup[internal] = code
clean_int = self._remove_stereochemistry(internal)
self.clean_smiles_lookup.setdefault(clean_int, []).append(code)
self.uaa_internal_exact[code] = internal
self.uaa_internal_patterns.append((_chirality_agnostic_regex(internal), code))
if letter:
self.three_to_one[code] = letter
for uaa_name, uaa_data in all_aminos.items():
code = uaa_data["Code"]
smiles = uaa_data.get("SMILES", "")
nterm = uaa_data.get("nterm", "")
cterm = uaa_data.get("cterm", "")
letter = uaa_data.get("Letter")
# keep existing full-aa lookups
if smiles:
self.exact_smiles_lookup[smiles] = code
clean = self._remove_stereochemistry(smiles)
self.clean_smiles_lookup.setdefault(clean, []).append(code)
internal = ""
if cterm:
internal = _internal_from_cterm(cterm)
elif nterm:
internal = _internal_from_nterm(nterm)
if internal:
self.exact_smiles_lookup[internal] = code
clean_int = self._remove_stereochemistry(internal)
self.clean_smiles_lookup.setdefault(clean_int, []).append(code)
self.uaa_internal_exact[code] = internal
self.uaa_internal_patterns.append((_chirality_agnostic_regex(internal), code))
if letter:
self.three_to_one[code] = letter
def _remove_stereochemistry(self, smiles):
"""Remove stereochemistry from SMILES"""
cleaned = smiles
stereochemistry_patterns = [
'[C@@H]', '[C@H]', '[C@@]', '[C@]',
'[S@@]', '[S@]', '[N@@]', '[N@]',
'@@', '@'
]
for pattern in stereochemistry_patterns:
cleaned = cleaned.replace(pattern, pattern.replace('@@', '').replace('@', '').replace('[', '').replace(']', ''))
return cleaned
def preprocess_complex_residues(self, smiles):
complex_positions = []
for pattern, residue_type in self.complex_residue_patterns:
for match in re.finditer(pattern, smiles):
if not any(pos['start'] <= match.start() < pos['end'] or
pos['start'] < match.end() <= pos['end'] for pos in complex_positions):
complex_positions.append({
'start': match.start(),
'end': match.end(),
'type': residue_type,
'pattern': match.group()
})
for rgx, code in getattr(self, 'uaa_internal_patterns', []):
for match in rgx.finditer(smiles):
if not any(pos['start'] <= match.start() < pos['end'] or
pos['start'] < match.end() <= pos['end'] for pos in complex_positions):
complex_positions.append({
'start': match.start(),
'end': match.end(),
'type': code, # e.g., 'Dtg'
'pattern': match.group()
})
complex_positions.sort(key=lambda x: x['start'])
if not complex_positions:
return smiles, []
preprocessed_smiles = smiles
offset = 0
protected_residues = []
for pos in complex_positions:
start = pos['start'] + offset
end = pos['end'] + offset
complex_part = preprocessed_smiles[start:end]
# keep your stereo sanity check (OK to keep)
if not ('[C@H]' in complex_part or '[C@@H]' in complex_part):
# Dtg internal often *does* have [C@@H], so it will pass.
# If you find UAAs without explicit stereo, you may relax this guard.
pass
placeholder = f"COMPLEX_RESIDUE_{len(protected_residues)}"
preprocessed_smiles = preprocessed_smiles[:start] + placeholder + preprocessed_smiles[end:]
offset += len(placeholder) - (end - start)
protected_residues.append({
'placeholder': placeholder,
'type': pos['type'],
'content': complex_part
})
return preprocessed_smiles, protected_residues
def split_on_bonds(self, smiles, protected_residues=None):
"""Split SMILES into segments based on peptide bonds, with improved handling of protected residues"""
positions = []
used = set()
# Handle protected complex residues if any
if protected_residues:
for residue in protected_residues:
match = re.search(residue['placeholder'], smiles)
if match:
positions.append({
'start': match.start(),
'end': match.end(),
'type': 'complex',
'pattern': residue['placeholder'],
'residue_type': residue['type'],
'content': residue['content']
})
used.update(range(match.start(), match.end()))
# Find all peptide bonds
bond_positions = []
# 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):
bond_positions.append({
'start': match.start(),
'end': match.end(),
'type': 'gly',
'pattern': match.group()
})
used.update(range(match.start(), match.end()))
for pattern, bond_type in self.bond_patterns:
for match in re.finditer(pattern, smiles):
if not any(p in range(match.start(), match.end()) for p in used):
bond_positions.append({
'start': match.start(),
'end': match.end(),
'type': bond_type,
'pattern': match.group()
})
used.update(range(match.start(), match.end()))
bond_positions.sort(key=lambda x: x['start'])
all_positions = positions + bond_positions
all_positions.sort(key=lambda x: x['start'])
segments = []
if all_positions and all_positions[0]['start'] > 0:
segments.append({
'content': smiles[0:all_positions[0]['start']],
'bond_after': all_positions[0]['pattern'] if all_positions[0]['type'] != 'complex' else None,
'complex_after': all_positions[0]['pattern'] if all_positions[0]['type'] == 'complex' else None
})
for i in range(len(all_positions)-1):
current = all_positions[i]
next_pos = all_positions[i+1]
if current['type'] == 'complex':
segments.append({
'content': current['content'],
'bond_before': all_positions[i-1]['pattern'] if i > 0 and all_positions[i-1]['type'] != 'complex' else None,
'bond_after': next_pos['pattern'] if next_pos['type'] != 'complex' else None,
'complex_type': current['residue_type']
})
elif current['type'] == 'gly':
segments.append({
'content': 'NCC(=O)',
'bond_before': all_positions[i-1]['pattern'] if i > 0 and all_positions[i-1]['type'] != 'complex' else None,
'bond_after': next_pos['pattern'] if next_pos['type'] != 'complex' else None
})
else:
content = smiles[current['end']:next_pos['start']]
if content and next_pos['type'] != 'complex':
segments.append({
'content': content,
'bond_before': current['pattern'],
'bond_after': next_pos['pattern'] if next_pos['type'] != 'complex' else None
})
# Last segment
if all_positions and all_positions[-1]['end'] < len(smiles):
if all_positions[-1]['type'] == 'complex':
segments.append({
'content': all_positions[-1]['content'],
'bond_before': all_positions[-2]['pattern'] if len(all_positions) > 1 and all_positions[-2]['type'] != 'complex' else None,
'complex_type': all_positions[-1]['residue_type']
})
else:
segments.append({
'content': smiles[all_positions[-1]['end']:],
'bond_before': all_positions[-1]['pattern']
})
return segments
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
return False
def is_cyclic(self, smiles):
# Check for C-terminal carboxyl
if smiles.endswith('C(=O)O'):
return False, [], []
# Find all numbers used in ring closures
ring_numbers = re.findall(r'(?:^|[^c])[0-9](?=[A-Z@\(\)])', smiles)
# Aromatic ring numbers
aromatic_matches = re.findall(r'c[0-9](?:ccccc|c\[nH\]c)[0-9]', smiles)
aromatic_cycles = []
for match in aromatic_matches:
numbers = re.findall(r'[0-9]', match)
aromatic_cycles.extend(numbers)
peptide_cycles = [n for n in ring_numbers if n not in aromatic_cycles]
is_cyclic = len(peptide_cycles) > 0 and not smiles.endswith('C(=O)O')
return is_cyclic, peptide_cycles, aromatic_cycles
def clean_terminal_carboxyl(self, segment):
"""Remove C-terminal carboxyl only if it's the true terminus"""
content = segment['content']
# Only clean if:
# 1. Contains C(=O)O
# 2. No bond_after exists (meaning it's the last segment)
if 'C(=O)O' in content and not segment.get('bond_after'):
# Remove C(=O)O pattern regardless of position
cleaned = re.sub(r'\(C\(=O\)O\)', '', content)
# Remove any leftover empty parentheses
cleaned = re.sub(r'\(\)', '', cleaned)
return cleaned
return content
def identify_residue(self, segment):
if 'complex_type' in segment:
return segment['complex_type'], []
# If this was protected by dynamic UAA shielding
if segment.get('complex_type') in self.uaa_internal_exact:
return segment['complex_type'], []
content = self.clean_terminal_carboxyl(segment)
mods = self.get_modifications(segment)
if content.startswith('COc1ccc(C(SC[C@@H]'):
print("DIRECT MATCH: Found Cmt at beginning")
return 'Cmt', mods
if '[C@@H]3CCCN3C2=O)(c2ccccc2)c2ccccc2)cc' in content:
print("DIRECT MATCH: Found Pro at end")
return 'Pro', mods
# Eal - Glu(OAll)
if 'CCC(=O)OCC=C' in content or 'CC(=O)OCC=C' in content or 'C=CCOC(=O)CC' in content:
return 'Eal', mods
# Proline (P)
if any([
(segment.get('bond_after', '').startswith(f'N{n}C(=O)') and 'CCC' in content and
any(f'[C@@H]{n}' in content or f'[C@H]{n}' in content for n in '123456789'))
for n in '123456789'
]) or any([(segment.get('bond_before', '').startswith(f'C(=O)N{n}') and 'CCC' in content and
any(f'CCC{n}' for n in '123456789'))
for n in '123456789'
]) or any([
(f'CCCN{n}' in content and content.endswith('=O') and
any(f'[C@@H]{n}' in content or f'[C@H]{n}' in content for n in '123456789'))
for n in '123456789'
]) or any([
# CCC[C@H]n
(content == f'CCC[C@H]{n}' and segment.get('bond_before', '').startswith(f'C(=O)N{n}')) or
(content == f'CCC[C@@H]{n}' and segment.get('bond_before', '').startswith(f'C(=O)N{n}')) or
# N-terminal Pro with any ring number
(f'N{n}CCC[C@H]{n}' in content) or
(f'N{n}CCC[C@@H]{n}' in content)
for n in '123456789'
]):
return 'Pro', mods
# D-Proline (p)
if ('N1[C@H](CCC1)' in content):
return 'pro', mods
# Tryptophan (W)
if re.search(r'c[0-9]c\[nH\]c[0-9]ccccc[0-9][0-9]', content) and \
'c[nH]c' in content.replace(' ', ''):
if '[C@H](CC' in content: # D-form
return 'trp', mods
return 'Trp', mods
# Lysine (K)
if '[C@@H](CCCCN)' in content or '[C@H](CCCCN)' in content:
if '[C@H](CCCCN)' in content: # D-form
return 'lys', mods
return 'Lys', mods
# Arginine (R)
if '[C@@H](CCCNC(=N)N)' in content or '[C@H](CCCNC(=N)N)' in content:
if '[C@H](CCCNC(=N)N)' in content: # D-form
return 'arg', mods
return 'Arg', mods
if content == 'C' and segment.get('bond_before') and segment.get('bond_after'):
if ('C(=O)N' in segment['bond_before'] or 'NC(=O)' in segment['bond_before'] or 'N(C)C(=O)' in segment['bond_before']) and \
('NC(=O)' in segment['bond_after'] or 'C(=O)N' in segment['bond_after'] or 'N(C)C(=O)' in segment['bond_after']):
return 'Gly', mods
if 'CNC' in content and any(f'C{i}=' in content for i in range(1, 10)):
return 'Gly', mods #'CNC1=O'
if not segment.get('bond_before') and segment.get('bond_after'):
if content == 'C' or content == 'NC':
if ('NC(=O)' in segment['bond_after'] or 'C(=O)N' in segment['bond_after'] or 'N(C)C(=O)' in segment['bond_after']):
return 'Gly', mods
# Leucine patterns (L/l)
if 'CC(C)C[C@H]' in content or 'CC(C)C[C@@H]' in content or '[C@@H](CC(C)C)' in content or '[C@H](CC(C)C)' in content or (('N[C@H](CCC(C)C)' in content or 'N[C@@H](CCC(C)C)' in content) and segment.get('bond_before') is None):
if '[C@H](CC(C)C)' in content or 'CC(C)C[C@H]' in content: # D-form
return 'leu', mods
return 'Leu', mods
# Threonine patterns (T/t)
if '[C@@H]([C@@H](C)O)' in content or '[C@H]([C@H](C)O)' in content or '[C@@H]([C@H](C)O)' in content or '[C@H]([C@@H](C)O)' in content:
if '[C@H]([C@@H](C)O)' in content: # D-form
return 'thr', mods
return 'Thr', mods
if re.search(r'\[C@H\]\(CCc\d+ccccc\d+\)', content) or re.search(r'\[C@@H\]\(CCc\d+ccccc\d+\)', content):
return 'Hph', mods
# Phenylalanine patterns (F/f)
if re.search(r'\[C@H\]\(Cc\d+ccccc\d+\)', content) or re.search(r'\[C@@H\]\(Cc\d+ccccc\d+\)', content):
if re.search(r'\[C@H\]\(Cc\d+ccccc\d+\)', content): # D-form
return 'phe', mods
return 'Phe', mods
if ('CC(C)[C@@H]' in content or 'CC(C)[C@H]' in content or
'[C@H](C(C)C)' in content or '[C@@H](C(C)C)' in content or
'C(C)C[C@H]' in content or 'C(C)C[C@@H]' in content):
if not any(p in content for p in ['CC(C)C[C@H]', 'CC(C)C[C@@H]', 'CCC(=O)']):
if '[C@H]' in content and not '[C@@H]' in content: # D-form
return 'val', mods
return 'Val', mods
# Isoleucine patterns (I/i)
if (any(['CC[C@@H](C)' in content, '[C@@H](C)CC' in content, '[C@@H](CC)C' in content,
'C(C)C[C@@H]' in content, '[C@@H]([C@H](C)CC)' in content, '[C@H]([C@@H](C)CC)' in content,
'[C@@H]([C@@H](C)CC)' in content, '[C@H]([C@H](C)CC)' in content,
'C[C@H](CC)[C@@H]' in content, 'C[C@@H](CC)[C@H]' in content,
'C[C@H](CC)[C@H]' in content, 'C[C@@H](CC)[C@@H]' in content,
'CC[C@H](C)[C@@H]' in content, 'CC[C@@H](C)[C@H]' in content,
'CC[C@H](C)[C@H]' in content, 'CC[C@@H](C)[C@@H]' in content])
and 'CC(C)C' not in content): # Exclude valine pattern
if any(['[C@H]([C@@H](CC)C)' in content, '[C@H](CC)C' in content,
'[C@H]([C@@H](C)CC)' in content, '[C@H]([C@H](C)CC)' in content,
'C[C@@H](CC)[C@H]' in content, 'C[C@H](CC)[C@H]' in content,
'CC[C@@H](C)[C@H]' in content, 'CC[C@H](C)[C@H]' in content]):
# D-form
return 'ile', mods
return 'Ile', mods
# Tpb - Thr(PO(OBzl)OH)
if re.search(r'\(C\)OP\(=O\)\(O\)OCc[0-9]ccccc[0-9]', content) or 'OP(=O)(O)OCC' in content:
return 'Tpb', mods
# Alanine patterns (A/a)
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', 'CC[C@H]', 'CC[C@@H]']):
if '[C@H](C)' in content: # D-form
return 'ala', mods
return 'Ala', mods
# Tyrosine patterns (Y/y)
if re.search(r'Cc[0-9]ccc\(O\)cc[0-9]', content):
if '[C@H](Cc1ccc(O)cc1)' in content: # D-form
return 'tyr', mods
return 'Tyr', mods
# Serine patterns (S/s)
if '[C@H](CO)' in content or '[C@@H](CO)' in content:
if not ('C(C)O' in content or 'COC' in content):
if '[C@H](CO)' in content: # D-form
return 'ser', mods
return 'Ser', mods
if 'CSSC' in content:
# cysteine-cysteine bridge
if re.search(r'\[C@@H\].*CSSC.*\[C@@H\]', content) or re.search(r'\[C@H\].*CSSC.*\[C@H\]', content):
if '[C@H]' in content and not '[C@@H]' in content: # D-form
return 'cys-cys', mods
return 'Cys-Cys', mods
# N-terminal amine group
if '[C@@H](N)CSSC' in content or '[C@H](N)CSSC' in content:
if '[C@H](N)CSSC' in content: # D-form
return 'cys-cys', mods
return 'Cys-Cys', mods
# C-terminal carboxyl
if 'CSSC[C@@H](C(=O)O)' in content or 'CSSC[C@H](C(=O)O)' in content:
if 'CSSC[C@H](C(=O)O)' in content: # D-form
return 'cys-cys', mods
return 'Cys-Cys', mods
# Cysteine patterns (C/c)
if '[C@H](CS)' in content or '[C@@H](CS)' in content:
if '[C@H](CS)' in content: # D-form
return 'cys', mods
return 'Cys', mods
# Methionine patterns (M/m)
if ('CCSC' in content) or ("CSCC" in content):
if '[C@H](CCSC)' in content: # D-form
return 'met', mods
elif '[C@H]' in content:
return 'met', mods
return 'Met', mods
# Glutamine patterns (Q/q)
if (content == '[C@@H](CC' or content == '[C@H](CC' and segment.get('bond_before')=='C(=O)N' and segment.get('bond_after')=='C(=O)N') or ('CCC(=O)N' in content) or ('CCC(N)=O' in content):
if '[C@H](CCC(=O)N)' in content: # D-form
return 'gln', mods
return 'Gln', mods
# Asparagine patterns (N/n)
if (content == '[C@@H](C' or content == '[C@H](C' and segment.get('bond_before')=='C(=O)N' and segment.get('bond_after')=='C(=O)N') or ('CC(=O)N' in content) or ('CCN(=O)' in content) or ('CC(N)=O' in content):
if '[C@H](CC(=O)N)' in content: # D-form
return 'asn', mods
return 'Asn', mods
# Glutamic acid patterns (E/e)
if ('CCC(=O)O' in content):
if '[C@H](CCC(=O)O)' in content: # D-form
return 'glu', mods
return 'Glu', mods
# Aspartic acid patterns (D/d)
if ('CC(=O)O' in content):
if '[C@H](CC(=O)O)' in content: # D-form
return 'asp', mods
return 'Asp', mods
if re.search(r'Cc\d+c\[nH\]cn\d+', content) or re.search(r'Cc\d+cnc\[nH\]\d+', content):
if '[C@H]' in content: # D-form
return 'his', mods
return 'His', mods
if 'C2(CCCC2)' in content or 'C1(CCCC1)' in content or re.search(r'C\d+\(CCCC\d+\)', content):
return 'Cyl', mods
if ('N[C@@H](CCCC)' in content or '[C@@H](CCCC)' in content or 'CCCC[C@@H]' in content or
'N[C@H](CCCC)' in content or '[C@H](CCCC)' in content) and 'CC(C)' not in content:
return 'Nle', mods
if 'C(C)(C)(N)' in content:
return 'Aib', mods
if 'C(C)(C)' in content and 'OC(C)(C)C' not in content:
if (segment.get('bond_before') and segment.get('bond_after') and
any(bond in segment['bond_before'] for bond in ['C(=O)N', 'NC(=O)', 'N(C)C(=O)']) and
any(bond in segment['bond_after'] for bond in ['NC(=O)', 'C(=O)N', 'N(C)C(=O)'])):
return 'Aib', mods
# Dtg - Asp(OtBu)-(Dmb)Gly
if 'CC(=O)OC(C)(C)C' in content and 'CC1=C(C=C(C=C1)OC)OC' in content:
return 'Dtg', mods
# Kpg - Lys(palmitoyl-Glu-OtBu)
if 'CCCNC(=O)' in content and 'CCCCCCCCCCCC' in content:
return 'Kpg', mods
#======================Other UAAs from the SwissSidechain==========================================
# ADD SWISSSIDECHAIN MATCHING AT THE VERY END - only if nothing else matched
if content in self.exact_smiles_lookup:
return self.exact_smiles_lookup[content], mods
# Look up without stereochemistry differences)
content_clean = self._remove_stereochemistry(content)
if content_clean in self.clean_smiles_lookup:
matches = self.clean_smiles_lookup[content_clean]
if len(matches) == 1:
return matches[0], mods
else:
# Prefer L-forms (non-D prefixed codes) over D-forms
l_forms = [m for m in matches if not m.startswith('D')]
if l_forms:
return l_forms[0], mods
return matches[0], mods
return None, mods
def get_modifications(self, segment):
"""Get modifications based on bond types and segment content - fixed to avoid duplicates"""
mods = []
# Check for N-methylation in any form, but only add it once
# Check both bonds and segment content for N-methylation patterns
if ((segment.get('bond_after') and
('N(C)' in segment['bond_after'] or segment['bond_after'].startswith('C(=O)N(C)'))) or
('N(C)C(=O)' in segment['content'] or 'N(C)C1=O' in segment['content']) or
(segment['content'].endswith('N(C)C(=O)') or segment['content'].endswith('N(C)C1=O'))):
mods.append('N-Me')
# Check for O-linked modifications
#if segment.get('bond_after') and 'OC(=O)' in segment['bond_after']:
#mods.append('O-linked')
return mods
def analyze_structure(self, smiles, verbose=False):
logs = []
preprocessed_smiles, protected_residues = self.preprocess_complex_residues(smiles)
is_cyclic, peptide_cycles, aromatic_cycles = self.is_cyclic(smiles)
segments = self.split_on_bonds(preprocessed_smiles, protected_residues)
sequence = []
for i, segment in enumerate(segments):
if verbose:
logs.append(f"\nSegment {i}:")
logs.append(f" Content: {segment.get('content','None')}")
logs.append(f" Bond before: {segment.get('bond_before','None')}")
logs.append(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)
else:
logs.append(f"Warning: Could not identify residue in segment: {segment.get('content', 'None')}")
three_letter = '-'.join(sequence)
one_letter = ''.join(self.three_to_one.get(aa.split('(')[0], 'X') for aa in sequence)
if is_cyclic:
three_letter = f"cyclo({three_letter})"
one_letter = f"cyclo({one_letter})"
return {
'three_letter': three_letter,
'one_letter': one_letter,
'is_cyclic': is_cyclic,
'residues': sequence,
'details': "\n".join(logs)
}
def get_uaa_information(self):
uaa_info = """
## Supported Non-Standard Amino Acids (UAAs) (Common)
- **Kpg** - Lys(palmitoyl-Glu-OtBu)
- **Cmt** - Cys(Mmt)
- **Eal** - Glu(OAll)
- **Tpb** - Thr(PO(OBzl)OH)
- **Dtg** - Asp(OtBu)-(Dmb)Gly
- **Aib** - α-Aminoisobutyric acid
- **Nle** - Norleucine
- **Hph** - Homophenylalanine
- **Cyl** - Cycloleucine
- **Nml** - N-methylleucine
- **Nma** - N-methylalanine
### Special Cases:
- **Cys-Cys** - Disulfide-bridged cysteine dimer
---
## Three-to-One Letter Code Mapping
### Standard Amino Acids:
**L-amino acids:** A, C, D, E, F, G, H, I, K, L, M, N, P, Q, R, S, T, V, W, Y
**D-amino acids:** a, c, d, e, f, g, h, i, k, l, m, n, p, q, r, s, t, v, w, y
### UAA Single Letter Codes:
| UAA | Code | UAA | Code | UAA | Code |
|-----|------|-----|------|-----|------|
| Aib | Ŷ | Dtg | Ĝ | Cmt | Ĉ |
| Eal | Ė | Nml | Ŀ | Nma | Ṃ |
| Kpg | Ƙ | Tpb | Ṯ | Cyl | Ċ |
| Nle | Ł | Hph | Ĥ | | |
### Special Cases:
- **Cys-Cys:** CC (L-form) or cc (D-form)
## For other mappings, please refer to the [SwissSideChain webside](https://www.swisssidechain.ch/browse/family/table.php?family=all)
"""
return uaa_info
def annotate_cyclic_structure(mol, sequence):
"""Create structure visualization"""
AllChem.Compute2DCoords(mol)
drawer = Draw.rdMolDraw2D.MolDraw2DCairo(2000, 2000)
drawer.drawOptions().addAtomIndices = False
drawer.DrawMolecule(mol)
drawer.FinishDrawing()
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()
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):
""""Linear visualization"""
analyzer = PeptideAnalyzer()
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])
if sequence.startswith('cyclo('):
residues = sequence[6:-1].split('-')
else:
residues = sequence.split('-')
segments = analyzer.split_on_bonds(smiles)
print(f"Number of residues: {len(residues)}")
print(f"Number of segments: {len(segments)}")
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
y_pos = 1.5
for i in range(num_residues):
x_pos = 0.5 + i * spacing
rect = patches.Rectangle((x_pos-0.3, y_pos-0.2), 0.6, 0.4,
facecolor='lightblue', edgecolor='black')
ax_struct.add_patch(rect)
if i < num_residues - 1:
segment = segments[i] if i < len(segments) else None
if segment:
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 '-'
ax_struct.plot([x_pos+0.3, x_pos+spacing-0.3], [y_pos, y_pos],
color=bond_color, linestyle=linestyle, linewidth=2)
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)
ax_struct.text(x_pos, y_pos-0.5, residues[i],
ha='center', va='top', fontsize=14)
ax_detail.set_ylim(0, len(segments)+1)
ax_detail.set_xlim(0, 1)
segment_y = len(segments)
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:
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'
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)
ax_struct.set_title("Peptide Structure Overview", pad=20)
ax_detail.set_title("Segment Analysis Breakdown", pad=20)
for ax in [ax_struct, ax_detail]:
ax.set_xticks([])
ax.set_yticks([])
ax.axis('off')
plt.tight_layout()
return fig
class PeptideStructureGenerator:
"""Generate 3D structures of peptides using different embedding methods"""
@staticmethod
def prepare_molecule(smiles):
"""Prepare molecule with proper hydrogen handling"""
mol = Chem.MolFromSmiles(smiles, sanitize=False)
if mol is None:
raise ValueError("Failed to create molecule from SMILES")
for atom in mol.GetAtoms():
atom.UpdatePropertyCache(strict=False)
# Sanitize with reduced requirements
Chem.SanitizeMol(mol,
sanitizeOps=Chem.SANITIZE_FINDRADICALS|
Chem.SANITIZE_KEKULIZE|
Chem.SANITIZE_SETAROMATICITY|
Chem.SANITIZE_SETCONJUGATION|
Chem.SANITIZE_SETHYBRIDIZATION|
Chem.SANITIZE_CLEANUPCHIRALITY)
mol = Chem.AddHs(mol)
return mol
@staticmethod
def get_etkdg_params(attempt=0):
"""Get ETKDG parameters"""
params = AllChem.ETKDGv3()
params.randomSeed = -1
params.maxIterations = 200
params.numThreads = 4 # Reduced for web interface
params.useBasicKnowledge = True
params.enforceChirality = True
params.useExpTorsionAnglePrefs = True
params.useSmallRingTorsions = True
params.useMacrocycleTorsions = True
params.ETversion = 2
params.pruneRmsThresh = -1
params.embedRmsThresh = 0.5
if attempt > 10:
params.bondLength = 1.5 + (attempt - 10) * 0.02
params.useExpTorsionAnglePrefs = False
return params
def generate_structure_etkdg(self, smiles, max_attempts=20):
"""Generate 3D structure using ETKDG without UFF optimization"""
success = False
mol = None
for attempt in range(max_attempts):
try:
mol = self.prepare_molecule(smiles)
params = self.get_etkdg_params(attempt)
if AllChem.EmbedMolecule(mol, params) == 0:
success = True
break
except Exception as e:
continue
if not success:
raise ValueError("Failed to generate structure with ETKDG")
return mol
def generate_structure_uff(self, smiles, max_attempts=20):
"""Generate 3D structure using ETKDG followed by UFF optimization"""
best_mol = None
lowest_energy = float('inf')
for attempt in range(max_attempts):
try:
test_mol = self.prepare_molecule(smiles)
params = self.get_etkdg_params(attempt)
if AllChem.EmbedMolecule(test_mol, params) == 0:
res = AllChem.UFFOptimizeMolecule(test_mol, maxIters=2000,
vdwThresh=10.0, confId=0,
ignoreInterfragInteractions=True)
if res == 0:
ff = AllChem.UFFGetMoleculeForceField(test_mol)
if ff:
current_energy = ff.CalcEnergy()
if current_energy < lowest_energy:
lowest_energy = current_energy
best_mol = Chem.Mol(test_mol)
except Exception:
continue
if best_mol is None:
raise ValueError("Failed to generate optimized structure")
return best_mol
@staticmethod
def mol_to_sdf_bytes(mol):
"""Convert RDKit molecule to SDF file bytes"""
sio = StringIO()
writer = Chem.SDWriter(sio)
writer.write(mol)
writer.close()
return sio.getvalue().encode('utf-8')
class PeptideEncoder:
# map one-letter <-> three-letter
one_to_three = {
'A':'Ala','C':'Cys','D':'Asp','E':'Glu','F':'Phe','G':'Gly','H':'His','I':'Ile',
'K':'Lys','L':'Leu','M':'Met','N':'Asn','P':'Pro','Q':'Gln','R':'Arg','S':'Ser',
'T':'Thr','V':'Val','W':'Trp','Y':'Tyr',
'a':'ala','c':'cys','d':'asp','e':'glu','f':'phe','g':'gly','h':'his','i':'ile',
'k':'lys','l':'leu','m':'met','n':'asn','p':'pro','q':'gln','r':'arg','s':'ser',
't':'thr','v':'val','w':'trp','y':'tyr'
}
# L-form uses [C@@H], D-form uses [C@H].
SEG_L = {
'Ala': '[C@@H](C)',
'Gly': 'C', # your analyzer treats bare 'C' (or 'NC') as Gly in context
'Val': '[C@@H](C(C)C)',
'Leu': '[C@@H](CC(C)C)',
'Ile': '[C@@H]([C@H](C)CC)',
'Ser': '[C@@H](CO)',
'Thr': '[C@@H]([C@@H](C)O)',
'Cys': '[C@@H](CS)',
'Met': '[C@@H](CCSC)',
'Phe': '[C@@H](Cc1ccccc1)',
'Tyr': '[C@@H](Cc1ccc(O)cc1)',
'Trp': '[C@@H](Cc1c[nH]c2ccccc12)',
'His': '[C@@H](Cc1c[nH]cn1)',
'Asp': '[C@@H](CC(=O)O)',
'Glu': '[C@@H](CCC(=O)O)',
'Asn': '[C@@H](CC(=O)N)',
'Gln': '[C@@H](CCC(=O)N)',
'Lys': '[C@@H](CCCCN)',
'Arg': '[C@@H](CCCNC(=N)N)',
'Pro': 'CC[C@H]2CN2' # only used if not doing ring-number closure
}
# D-forms: flip chirality tag to [C@H]
SEG_D = {k.lower(): v.replace('[C@@H]', '[C@H]').replace('[C@H]2','[C@@H]2') for k, v in SEG_L.items()}
UAA_SEG = {
'Aib': 'C(C)(C)', # alpha,alpha-dimethyl gly (detected as Aib when bracketed by peptide bonds)
'Nle': '[C@@H](CCCC)', # norleucine ~ Lys w/o terminal amine
'Hph': '[C@@H](CCc1ccccc1)', # homophenylalanine
'Cyl': 'C1(CCCC1)', # cycloleucine
}
def __init__(self):
self.ssc_code_to_internal = {}
for name, data in specific_aminos.items():
code = data["Code"]
cterm = data.get("cterm", "")
nterm = data.get("nterm", "")
internal = ""
if cterm:
internal = _internal_from_cterm(cterm)
elif nterm:
internal = _internal_from_nterm(nterm)
if internal:
self.ssc_code_to_internal[code] = internal
for name, data in all_aminos.items():
code = data["Code"]
cterm = data.get("cterm", "")
nterm = data.get("nterm", "")
internal = ""
if cterm:
internal = _internal_from_cterm(cterm)
elif nterm:
internal = _internal_from_nterm(nterm)
if internal:
self.ssc_code_to_internal[code] = internal
def _segment_for(self, code):
if code in self.SEG_L: return self.SEG_L[code]
if code in self.SEG_D: return self.SEG_D[code]
if code in self.UAA_SEG: return self.UAA_SEG[code]
if code in self.ssc_code_to_internal:
return self.ssc_code_to_internal[code]
cap = code[:1].upper() + code[1:].lower()
if cap in self.SEG_L: return self.SEG_L[cap]
raise ValueError(f"Unknown residue code: {code}")
def _is_one_letter_seq(self, seq: str) -> bool:
"""Check if the input string looks like a one-letter code sequence."""
if "-" not in seq:
return True
def _norm_token(self, tok):
"""Normalize tokens like 'A', 'a', 'Ala', 'ala', 'Ala(N-Me)' -> (code, n_me_flag)"""
n_me = False
tok = tok.strip()
if tok in self.one_to_three:
base = self.one_to_three[tok]
else:
m = re.match(r'^([A-Za-z\-]+)(\((.*?)\))?$', tok)
if not m:
return tok, n_me
base = m.group(1)
mods = m.group(3) or ""
if 'N-Me' in mods or 'Nme' in mods or 'NME' in mods:
n_me = True
return base, n_me
def _bond_for(self, n_me=False, pro_ring=False, ring_idx=1):
"""Return the INTER-RESIDUE bond token your parser recognizes."""
if pro_ring:
return f'C(=O)N{ring_idx}'
return 'N(C)C(=O)' if n_me else 'NC(=O)'
def _split_tokens(self, seq):
if isinstance(seq, (list, tuple)):
return list(seq)
seq = seq.strip()
if self._is_one_letter_seq(seq):
return list(seq)
import re
return [t for t in re.split(r'-(?![^()]*\))', seq) if t]
def encode(self, seq, cyclic=False, use_proline_ring=True):
"""
Encode a peptide to a SMILES string using the same grammar your analyzer expects.
Args:
seq: list of tokens or a string like:
'Ala-Gly-Phe', 'A-G-F', 'Ala(N-Me)-Leu-Ser', 'Aib-Nle-Arg'
D-forms: 'ala-gly', or 'a-g'
cyclic: if True, connect C-terminus back to N-terminus (macrocycle)
use_proline_ring: if True, do ring-number closure for Pro (N{digit} ... [C@H]{digit})
"""
toks = self._split_tokens(seq)
res, mods = [], []
for t in toks:
base, n_me = self._norm_token(t) # your existing parser for "(N-Me)"
res.append(base)
mods.append(n_me)
# Build segments
segs = [self._segment_for(r) for r in res]
# Proline ring bookkeeping
# We only do the special N{digit}...{digit} closure when a bond *into* Pro occurs.
bonds = []
for i in range(len(segs)-1):
next_is_pro = res[i+1] in ('Pro','pro')
if use_proline_ring and next_is_pro:
bonds.append(self._bond_for(n_me=mods[i], pro_ring=True, ring_idx=1))
# Make the Pro segment end with the matching ring digit
segs[i+1] = 'CCC[C@H]1' if res[i+1]=='Pro' else 'CCC[C@@H]1'
else:
bonds.append(self._bond_for(n_me=mods[i], pro_ring=False))
# Assemble linear chain
# [segment0] + bond0 + [segment1] + bond1 + ... + [segmentN-1] + C(=O)O
out = []
for i, s in enumerate(segs):
out.append(s)
if i < len(bonds):
out.append(bonds[i])
if cyclic:
# TODO
pass
else:
out.append('C(=O)O')
return ''.join(out)
def process_input(
smiles_input=None,
file_obj=None,
#show_linear=False,
show_segment_details=False,
generate_3d=False,
use_uff=False
):
"""Actual Execution Command."""
analyzer = PeptideAnalyzer()
temp_dir = tempfile.mkdtemp() if generate_3d else None
structure_files = []
# Retrieve UAA information
uaa_info = analyzer.get_uaa_information()
# Handle direct SMILES input
if smiles_input:
smiles = smiles_input.strip()
if not analyzer.is_peptide(smiles):
return "Error: Input SMILES does not appear to be a peptide structure.", None, None, []
try:
# Preprocess to protect complex residues
pre_smiles, protected_residues = analyzer.preprocess_complex_residues(smiles)
# Report protected residues in summary if any
protected_info = None
if protected_residues:
protected_info = [res['type'] for res in protected_residues]
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return "Error: Invalid SMILES notation.", None, None, []
if generate_3d:
generator = PeptideStructureGenerator()
try:
# Generate ETKDG structure
mol_etkdg = generator.generate_structure_etkdg(smiles)
etkdg_path = os.path.join(temp_dir, "structure_etkdg.sdf")
writer = Chem.SDWriter(etkdg_path)
writer.write(mol_etkdg)
writer.close()
structure_files.append(etkdg_path)
# Generate UFF structure if requested
if use_uff:
mol_uff = generator.generate_structure_uff(smiles)
uff_path = os.path.join(temp_dir, "structure_uff.sdf")
writer = Chem.SDWriter(uff_path)
writer.write(mol_uff)
writer.close()
structure_files.append(uff_path)
except Exception as e:
return f"Error generating 3D structures: {str(e)}", None, None, []
analysis = analyzer.analyze_structure(smiles, verbose=show_segment_details)
three_letter = analysis['three_letter']
one_letter = analysis['one_letter']
is_cyclic = analysis['is_cyclic']
details = analysis.get('details', "")
img_cyclic = annotate_cyclic_structure(mol, three_letter)
summary = "Summary:\n"
summary += f"Sequence: {three_letter}\n"
summary += f"One-letter code: {one_letter}\n"
summary += f"Is Cyclic: {'Yes' if is_cyclic else 'No'}\n"
# Add segment details if requested
if show_segment_details and details:
summary += "\n" + "="*50 + "\n"
summary += "SEGMENT ANALYSIS:\n"
summary += "="*50 + "\n"
summary += details + "\n"
detected_uaas = [aa for aa in analysis['residues'] if aa not in [
'Ala', 'Cys', 'Asp', 'Glu', 'Phe', 'Gly', 'His', 'Ile', 'Lys', 'Leu',
'Met', 'Asn', 'Pro', 'Gln', 'Arg', 'Ser', 'Thr', 'Val', 'Trp', 'Tyr',
'ala', 'cys', 'asp', 'glu', 'phe', 'gly', 'his', 'ile', 'lys', 'leu',
'met', 'asn', 'pro', 'gln', 'arg', 'ser', 'thr', 'val', 'trp', 'tyr'
]]
if detected_uaas:
summary += f"\nDetected UAAs: {', '.join(set(detected_uaas))}\n"
if structure_files:
summary += "\n3D Structures Generated:\n"
for filepath in structure_files:
summary += f"- {os.path.basename(filepath)}\n"
#return summary, img_cyclic, img_linear, structure_files if structure_files else None
return summary, img_cyclic, uaa_info
except Exception as e:
#return f"Error processing SMILES: {str(e)}", None, None, []
return f"Error processing SMILES: {str(e)}", None, uaa_info
# Handle file input
if file_obj is not None:
try:
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 not smiles:
continue
if not analyzer.is_peptide(smiles):
output_text += f"Skipping non-peptide SMILES: {smiles}\n"
continue
try:
result = analyzer.analyze_structure(smiles)
output_text += f"\nSummary for SMILES: {smiles}\n"
output_text += f"Sequence: {result['three_letter']}\n"
output_text += f"One-letter code: {result['one_letter']}\n"
output_text += f"Is Cyclic: {'Yes' if result['is_cyclic'] else 'No'}\n"
output_text += "-" * 50 + "\n"
except Exception as e:
output_text += f"Error processing SMILES: {smiles} - {str(e)}\n"
output_text += "-" * 50 + "\n"
return output_text, None, uaa_info
except Exception as e:
#return f"Error processing file: {str(e)}", None, None, []
return f"Error processing file: {str(e)}", None, uaa_info
return (
output_text or "No analysis done.",
img_cyclic if 'img_cyclic' in locals() else None, uaa_info
#img_linear if 'img_linear' in locals() else None,
#structure_files if structure_files else []
)
def process_sequence_to_smiles(
seq_input: str,
show_segment_details: bool = False,
use_proline_ring: bool = True,
cyclic: bool = False
):
"""
Encode a peptide sequence to SMILES, then analyze back with PeptideAnalyzer for round-trip.
"""
if not seq_input or not seq_input.strip():
return "Please enter a peptide sequence.", None, None
try:
enc = PeptideEncoder() # make sure this class is defined in your file
smiles = enc.encode(seq_input.strip(), cyclic=cyclic, use_proline_ring=use_proline_ring)
analyzer = PeptideAnalyzer()
# pre-check it's a peptide
if not analyzer.is_peptide(smiles):
return "Internal error: generated SMILES did not look like a peptide.", None, None
# analyze round-trip
analysis = analyzer.analyze_structure(smiles, verbose=show_segment_details)
three_letter = analysis['three_letter']
one_letter = analysis['one_letter']
is_cyclic = analysis['is_cyclic']
details = analysis.get('details', "")
img = annotate_cyclic_structure(Chem.MolFromSmiles(smiles), three_letter)
summary = []
summary.append("Peptide → SMILES")
summary.append("-" * 50)
summary.append(f"Input sequence: {seq_input}")
summary.append(f"Generated SMILES:\n{smiles}")
summary.append("")
summary.append("Round-trip check (SMILES → sequence):")
summary.append(f"Sequence: {three_letter}")
summary.append(f"One-letter code: {one_letter}")
summary.append(f"Is Cyclic: {'Yes' if is_cyclic else 'No'}")
if show_segment_details and details:
summary.append("\n" + "="*50)
summary.append("SEGMENT ANALYSIS")
summary.append("="*50)
summary.append(details)
# UAA report
detected_uaas = [aa for aa in analysis['residues'] if aa not in [
'Ala', 'Cys', 'Asp', 'Glu', 'Phe', 'Gly', 'His', 'Ile', 'Lys', 'Leu',
'Met', 'Asn', 'Pro', 'Gln', 'Arg', 'Ser', 'Thr', 'Val', 'Trp', 'Tyr',
'ala', 'cys', 'asp', 'glu', 'phe', 'gly', 'his', 'ile', 'lys', 'leu',
'met', 'asn', 'pro', 'gln', 'arg', 'ser', 'thr', 'val', 'trp', 'tyr'
]]
if detected_uaas:
summary.append(f"\nDetected UAAs (round-trip): {', '.join(sorted(set(detected_uaas)))}")
return "\n".join(summary), img, smiles
except Exception as e:
return f"Error: {str(e)}", None, None
with gr.Blocks(title="Peptide Structure Analyzer and Visualizer") as demo:
gr.Markdown("# Peptide Structure Analyzer and Visualizer")
# 👇 place your original multi-line description right here
gr.Markdown("""
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
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
```
Example Peptide strings (copy and paste):
```
AGFS
```
```
Ala-Gly-Phe-Ser
```
```
Aib-Dtg-Ser
```
""")
with gr.Tab("SMILES → Sequence"):
gr.Markdown("Analyze peptide SMILES, detect cyclicity, parse sequence, and annotate.")
smiles_in = gr.Textbox(label="Enter SMILES string", lines=2, placeholder="Enter SMILES notation of peptide...")
file_in = gr.File(label="Or upload a text file with SMILES", file_types=[".txt"])
show_seg = gr.Checkbox(label="Show segmentation details", value=False)
run_btn_1 = gr.Button("Analyze")
out_text_1 = gr.Textbox(label="Analysis Results", lines=12)
out_img_1 = gr.Image(label="2D Structure with Annotations", type="pil")
out_md_1 = gr.Markdown(label="Side Notes for Non-Standard Amino Acids")
def _run_smiles(s_in, f_in, sh):
return process_input(
smiles_input=s_in,
file_obj=f_in,
show_segment_details=sh,
generate_3d=False,
use_uff=False
)
run_btn_1.click(
_run_smiles,
inputs=[smiles_in, file_in, show_seg],
outputs=[out_text_1, out_img_1, out_md_1]
)
with gr.Tab("Peptide → SMILES"):
gr.Markdown("Encode a peptide sequence to SMILES (one-letter or three-letter) and verify round-trip.")
seq_in = gr.Textbox(
label="Enter peptide sequence",
lines=2,
placeholder="Examples: AGFS | Ala-Gly-Phe-Ser | Ala(N-Me)-Pro-Phe | Aib-Dtg-Ser"
)
with gr.Row():
use_pro = gr.Checkbox(label="Use Proline ring join", value=True)
cyc = gr.Checkbox(label="Cyclic (macrocycle)", value=False)
show_seg2 = gr.Checkbox(label="Show segmentation details", value=False)
run_btn_2 = gr.Button("Encode")
out_text_2 = gr.Textbox(label="Results & Round-trip", lines=14)
out_img_2 = gr.Image(label="2D Structure with Annotations", type="pil")
out_smiles = gr.Textbox(label="Generated SMILES (copyable)", lines=2)
run_btn_2.click(
process_sequence_to_smiles,
inputs=[seq_in, show_seg2, use_pro, cyc],
outputs=[out_text_2, out_img_2, out_smiles]
)
if __name__ == "__main__":
demo.launch(share=True)
"""
iface = gr.Interface(
fn=process_input,
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 show segmentation details",
value=False
),],
outputs=[
gr.Textbox(
label="Analysis Results",
lines=10
),
gr.Image(
label="2D Structure with Annotations",
type="pil"
),
#gr.File(
#label="3D Structure Files",
#file_count="multiple"
#),
gr.Markdown(
label="Side Notes for Non-Standard Amino Acids",
)
],
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
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"
)
if __name__ == "__main__":
iface.launch(share=True)
"""
"""
5. Optional linear representation
6. Optional 3D structure generation (ETKDG and UFF methods)
gr.Checkbox(
label="Generate 3D structure (sdf file format)",
value=False
),
gr.Checkbox(
label="Use UFF optimization (may take long)",
value=False
)
],
"""