Tsimech2000/Protein_Synthesizer_Simulator
0
1import streamlit as st2from Bio.Seq import Seq3from Bio import Entrez, SeqIO4from Bio.SeqUtils.ProtParam import ProteinAnalysis5import re6import requests7from textwrap import wrap8 9# ----------------------------10# App Config11# ----------------------------12st.set_page_config(page_title="Protein Synthesizer Simulator v2", page_icon="π§¬", layout="wide")13st.title("𧬠Protein Synthesizer Simulator β v2")14 15# ----------------------------16# Settings (sidebar)17# ----------------------------18st.sidebar.header("βοΈ Settings")19Entrez.email = st.sidebar.text_input("NCBI Email (required for E-utilities)", value="your.email@example.com")20ENTREZ_API_KEY = st.sidebar.text_input("NCBI API key (optional)", type="password")21if ENTREZ_API_KEY:22 Entrez.api_key = ENTREZ_API_KEY23 24min_orf_len = st.sidebar.number_input("Minimum ORF length (aa)", min_value=10, max_value=3000, value=30, step=10)25show_one_letter = st.sidebar.checkbox("Show protein in 1-letter code", value=True)26wrap_width = st.sidebar.slider("Wrap width for sequence display", 30, 120, 60)27 28MAX_INPUT_LEN = 200_000 # guard against pasting whole chromosomes into the ORF scanner29 30# ----------------------------31# Genetic Code (Standard RNA)32# ----------------------------33CODON_TO_AA = {34 'AUG': 'M', 'UUU': 'F', 'UUC': 'F', 'UUA': 'L', 'UUG': 'L',35 'UCU': 'S', 'UCC': 'S', 'UCA': 'S', 'UCG': 'S',36 'UAU': 'Y', 'UAC': 'Y', 'UAA': '*', 'UAG': '*', 'UGA': '*',37 'UGU': 'C', 'UGC': 'C', 'UGG': 'W',38 'CUU': 'L', 'CUC': 'L', 'CUA': 'L', 'CUG': 'L',39 'CCU': 'P', 'CCC': 'P', 'CCA': 'P', 'CCG': 'P',40 'CAU': 'H', 'CAC': 'H', 'CAA': 'Q', 'CAG': 'Q',41 'CGU': 'R', 'CGC': 'R', 'CGA': 'R', 'CGG': 'R',42 'AUU': 'I', 'AUC': 'I', 'AUA': 'I', 'ACU': 'T', 'ACC': 'T', 'ACA': 'T', 'ACG': 'T',43 'AAU': 'N', 'AAC': 'N', 'AAA': 'K', 'AAG': 'K',44 'AGU': 'S', 'AGC': 'S', 'AGA': 'R', 'AGG': 'R',45 'GUU': 'V', 'GUC': 'V', 'GUA': 'V', 'GUG': 'V',46 'GCU': 'A', 'GCC': 'A', 'GCA': 'A', 'GCG': 'A',47 'GAU': 'D', 'GAC': 'D', 'GAA': 'E', 'GAG': 'E',48 'GGU': 'G', 'GGC': 'G', 'GGA': 'G', 'GGG': 'G'49}50 51AA1_TO_AA3 = {52 'A': 'Ala', 'R': 'Arg', 'N': 'Asn', 'D': 'Asp', 'C': 'Cys', 'Q': 'Gln', 'E': 'Glu', 'G': 'Gly', 'H': 'His', 'I': 'Ile',53 'L': 'Leu', 'K': 'Lys', 'M': 'Met', 'F': 'Phe', 'P': 'Pro', 'S': 'Ser', 'T': 'Thr', 'W': 'Trp', 'Y': 'Tyr', 'V': 'Val', '*': 'Stop'54}55AA3_TO_AA1 = {v: k for k, v in AA1_TO_AA3.items()}56 57AA_TO_CODONS = {}58for c, a in CODON_TO_AA.items():59 AA_TO_CODONS.setdefault(a, []).append(c)60 61# Simple codon usage (most frequent codon per AA for H. sapiens; compact, illustrative)62HUMAN_TOP_CODON = {63 'A': 'GCC', 'R': 'CGC', 'N': 'AAC', 'D': 'GAC', 'C': 'UGC', 'Q': 'CAG', 'E': 'GAG', 'G': 'GGC', 'H': 'CAC', 'I': 'AUC',64 'L': 'CUG', 'K': 'AAG', 'M': 'AUG', 'F': 'UUC', 'P': 'CCC', 'S': 'AGC', 'T': 'ACC', 'W': 'UGG', 'Y': 'UAC', 'V': 'GUG', '*': 'UAA'65}66 67# Kyte-Doolittle hydropathy index68KD = {'I': 4.5, 'V': 4.2, 'L': 3.8, 'F': 2.8, 'C': 2.5, 'M': 1.9, 'A': 1.8, 'G': -0.4, 'T': -0.7, 'S': -0.8, 'W': -0.9, 'Y': -1.3,69 'P': -1.6, 'H': -3.2, 'E': -3.5, 'Q': -3.5, 'D': -3.5, 'N': -3.5, 'K': -3.9, 'R': -4.5, '*': 0, 'X': 0}70 71# ----------------------------72# Helpers73# ----------------------------74@st.cache_data(show_spinner=False)75def esearch_nuccore(term: str, retmax: int = 5):76 with Entrez.esearch(db="nuccore", term=term, retmax=retmax) as h:77 return Entrez.read(h)78 79 80@st.cache_data(show_spinner=False)81def efetch_fasta(nuccore_id: str):82 with Entrez.efetch(db="nuccore", id=nuccore_id, rettype="fasta", retmode="text") as h:83 rec = SeqIO.read(h, "fasta")84 return str(rec.seq), rec.description85 86 87@st.cache_data(show_spinner=False)88def search_rcsb_by_name(term: str, max_results: int = 8):89 """90 Free-text search against RCSB's Search API (search.rcsb.org), returning a91 list of {id, title} dicts for the top matches. Used when the user types a92 common/organism name (e.g. "sars-cov-2") instead of a literal PDB ID.93 """94 query = {95 "query": {96 "type": "terminal",97 "service": "full_text",98 "parameters": {"value": term},99 },100 "return_type": "entry",101 "request_options": {102 "results_content_type": ["experimental"],103 "paginate": {"start": 0, "rows": max_results},104 "sort": [{"sort_by": "score", "direction": "desc"}],105 },106 }107 resp = requests.post("https://search.rcsb.org/rcsbsearch/v2/query", json=query, timeout=15)108 resp.raise_for_status()109 data = resp.json()110 ids = [r["identifier"] for r in data.get("result_set", [])]111 if not ids:112 return []113 114 # Fetch a short title for each hit via the Data API's core entry endpoint115 # so results are recognizable rather than a bare list of codes.116 results = []117 for pdb_id in ids:118 title = pdb_id119 try:120 entry_resp = requests.get(f"https://data.rcsb.org/rest/v1/core/entry/{pdb_id}", timeout=10)121 if entry_resp.status_code == 200:122 struct = entry_resp.json().get("struct", {})123 title = struct.get("title", pdb_id)124 except requests.exceptions.RequestException:125 pass126 results.append({"id": pdb_id, "title": title})127 return results128 129 130def clean_dna(s: str) -> str:131 s = ''.join(s.upper().split())132 return re.sub(r"[^ATCG]", "", s)133 134 135def transcribe_dna(dna: str) -> str:136 return str(Seq(dna).transcribe())137 138 139def translate_from_first_aug(mrna: str, stop_symbol: str = '*') -> str:140 start = mrna.find('AUG')141 if start == -1:142 return ''143 prot = []144 for i in range(start, len(mrna), 3):145 codon = mrna[i:i + 3]146 if len(codon) < 3:147 break148 aa = CODON_TO_AA.get(codon, 'X')149 if aa == '*':150 break151 prot.append(aa)152 return ''.join(prot)153 154 155def translate_all(mrna: str) -> str:156 prot = []157 for i in range(0, len(mrna), 3):158 codon = mrna[i:i + 3]159 if len(codon) < 3:160 break161 prot.append(CODON_TO_AA.get(codon, 'X'))162 return ''.join(prot)163 164 165def format_seq(seq: str, width: int = 60) -> str:166 return "\n".join(wrap(seq, width))167 168 169def find_orfs(dna: str, min_len_aa: int = 30):170 """171 Scans all 6 reading frames (3 forward, 3 reverse-complement) for AUG->Stop172 ORFs of at least min_len_aa amino acids.173 174 Coordinate note: nt_start/nt_end are always reported relative to the175 ORIGINAL input sequence (5'->3', position 0 = first base of `dna` as176 passed in), regardless of which strand the ORF was found on. This is177 independent from any strand/frame selection made elsewhere in the app178 (e.g. Tab 1's manual strand toggle), so ORF coordinates here should not179 be assumed to line up with Tab 1's separately-chosen frame/strand view.180 """181 results = []182 seq = dna183 rc = str(Seq(dna).reverse_complement())184 n = len(dna)185 186 def scan(one_dna: str, strand: str):187 for frame in range(3):188 mrna = transcribe_dna(one_dna[frame:])189 i = 0190 while True:191 start = mrna.find('AUG', i)192 if start == -1:193 break194 j = start195 pep = []196 while j + 3 <= len(mrna):197 codon = mrna[j:j + 3]198 aa = CODON_TO_AA.get(codon, 'X')199 if aa == '*':200 break201 pep.append(aa)202 j += 3203 if pep and len(pep) >= min_len_aa:204 nt_start = frame + start205 nt_end = frame + j - 1206 if strand == '+':207 results.append({208 'strand': strand,209 'frame': frame,210 'nt_start': nt_start,211 'nt_end': nt_end,212 'length_aa': len(pep),213 'protein': ''.join(pep)214 })215 else:216 # Map coordinates on the reverse-complement string217 # back to positions on the original (+) strand.218 results.append({219 'strand': strand,220 'frame': frame,221 'nt_start': n - nt_end - 1,222 'nt_end': n - nt_start - 1,223 'length_aa': len(pep),224 'protein': ''.join(pep)225 })226 i = start + 3227 228 scan(seq, '+')229 scan(rc, '-')230 231 results.sort(key=lambda x: x['length_aa'], reverse=True)232 return results233 234 235def simple_features(protein: str):236 # Filter out non-standard letters for analyses237 clean = re.sub(r"[^ACDEFGHIKLMNPQRSTVWY]", "", protein)238 stats = {}239 if clean:240 pa = ProteinAnalysis(clean)241 stats['length'] = len(protein)242 stats['mw'] = round(pa.molecular_weight(), 2)243 stats['pi'] = round(pa.isoelectric_point(), 2)244 stats['aromaticity'] = round(pa.aromaticity(), 3)245 stats['instability_index'] = round(pa.instability_index(), 2)246 stats['gravy'] = round(pa.gravy(), 3)247 else:248 stats = {'length': len(protein), 'mw': 0, 'pi': None, 'aromaticity': None, 'instability_index': None, 'gravy': None}249 250 # N-glycosylation motifs: N-X-[ST] (X != P)251 glyco_sites = [m.start() + 1 for m in re.finditer(r"N[^P][ST]", protein)]252 253 # Very simple TM prediction by KD>1.6 over window 19254 win = 19255 kd_vals = []256 for i in range(len(protein)):257 window = protein[i:i + win]258 if len(window) < win:259 kd_vals.append(0)260 continue261 kd_vals.append(sum(KD.get(a, 0) for a in window) / win)262 tms = []263 thr = 1.6264 i = 0265 while i < len(kd_vals):266 if kd_vals[i] >= thr:267 start = i268 while i < len(kd_vals) and kd_vals[i] >= thr:269 i += 1270 end = i + win - 1271 tms.append((start + 1, min(end, len(protein))))272 else:273 i += 1274 275 return stats, glyco_sites, tms276 277 278def back_translate(protein: str, strategy: str = 'human_top') -> str:279 mrna = []280 for aa in protein:281 if aa == '*':282 codon = 'UAA'283 elif strategy == 'human_top' and aa in HUMAN_TOP_CODON:284 codon = HUMAN_TOP_CODON[aa]285 else:286 # default to the first codon listed for that AA287 codon = AA_TO_CODONS.get(aa, ['NNN'])[0]288 mrna.append(codon)289 return '-'.join(mrna)290 291 292# ----------------------------293# Tabs294# ----------------------------295tab1, tab2, tab3, tab4, tab5 = st.tabs(["π¬ Synthesis", "π ORF Finder", "π Reverse Translation", "π 3D Structure", "βΉοΈ About"])296 297# ----------------------------298# Tab 1 β Synthesis299# ----------------------------300with tab1:301 st.subheader("π Fetch Gene from NCBI")302 colq1, colq2 = st.columns([2, 1])303 with colq1:304 gene_query = st.text_input("Enter gene name and organism (e.g., BRCA1 Homo sapiens)")305 with colq2:306 st.write("")307 st.write("")308 309 if st.button("π Fetch Sequence") and gene_query:310 try:311 # Uses the cached helpers so repeated searches/fetches don't312 # re-hit NCBI on every click or rerun.313 search_result = esearch_nuccore(gene_query, retmax=1)314 id_list = search_result.get("IdList", [])315 if id_list:316 seq_id = id_list[0]317 dna_seq, description = efetch_fasta(seq_id)318 st.session_state['fetched_dna'] = dna_seq319 st.success(f"Fetched sequence: {description}")320 st.code(format_seq(st.session_state['fetched_dna'], wrap_width), language='text')321 else:322 st.warning("No sequences found for this query.")323 except Exception as e:324 st.error(f"Error: {e}")325 326 st.divider()327 st.subheader("1οΈβ£ Enter or Upload DNA (coding strand)")328 up = st.file_uploader("Upload FASTA (optional)", type=["fa", "fasta", "txt"])329 if up is not None:330 try:331 record = SeqIO.read(up, "fasta")332 dna_input = str(record.seq)333 st.info(f"Loaded FASTA: {record.description}")334 except Exception:335 up.seek(0)336 dna_input = up.read().decode("utf-8")337 else:338 dna_input = st.text_area("Paste DNA (A/T/C/G)", height=160, value=st.session_state.get('fetched_dna', ''))339 340 frame = st.selectbox("Reading frame (0, 1, 2)", [0, 1, 2], index=0)341 strand = st.radio("Strand", ["+ (as given)", "- (reverse complement)"])342 343 colA, colB, colC = st.columns([1, 1, 1])344 with colA:345 do_transcribe = st.button("𧬠Transcribe")346 with colB:347 do_translate_from_aug = st.button("π§« Translate (AUG β Stop)")348 with colC:349 do_translate_all = st.button("π§ͺ Translate (full, no start)")350 351 if dna_input:352 dna_clean = clean_dna(dna_input)353 if not dna_clean:354 st.error("No valid nucleotides found. Please provide A/T/C/G.")355 elif len(dna_clean) > MAX_INPUT_LEN:356 st.error(357 f"Input is {len(dna_clean):,} bp, which exceeds the {MAX_INPUT_LEN:,} bp limit for this tool. "358 "Please provide a shorter sequence (e.g. a single gene/transcript rather than a whole chromosome)."359 )360 else:361 if strand.startswith('-'):362 dna_clean = str(Seq(dna_clean).reverse_complement())363 if frame in (0, 1, 2):364 dna_frame = dna_clean[frame:]365 else:366 dna_frame = dna_clean367 368 mrna = transcribe_dna(dna_frame)369 370 if do_transcribe:371 st.subheader("2οΈβ£ mRNA")372 st.code(format_seq(mrna, wrap_width))373 374 if do_translate_from_aug:375 prot = translate_from_first_aug(mrna)376 if not prot:377 st.warning("No AUG found in-frame; cannot start translation.")378 else:379 st.subheader("3οΈβ£ Protein (AUG β Stop)")380 seq_show = prot if show_one_letter else '-'.join(AA1_TO_AA3.get(a, 'Xxx') for a in prot)381 st.code(format_seq(seq_show, wrap_width))382 383 stats, glyco, tms = simple_features(prot)384 c1, c2, c3 = st.columns(3)385 with c1:386 st.metric("Length (aa)", stats['length'])387 st.metric("MW (Da)", stats['mw'])388 with c2:389 st.metric("pI (theoretical)", stats['pi'])390 st.metric("Aromaticity", stats['aromaticity'])391 with c3:392 st.metric("Instability Index", stats['instability_index'])393 st.metric("GRAVY", stats['gravy'])394 395 st.markdown("**Predicted features**")396 st.write(f"β’ N-glycosylation N-X-[ST] sites at positions: {glyco if glyco else 'None'}")397 st.write(f"β’ Putative transmembrane helices (startβend): {tms if tms else 'None'}")398 399 fasta_header = ">translated_protein|frame={}|strand={}".format(frame, '+' if strand.startswith('+') else '-')400 aa_for_fasta = prot401 st.download_button(402 "π₯ Download protein (FASTA)",403 data=f"{fasta_header}\n{format_seq(aa_for_fasta, 60)}\n",404 file_name="protein.fasta",405 mime="text/plain",406 )407 408 if do_translate_all:409 prot_full = translate_all(mrna)410 st.subheader("3οΈβ£ Protein (full translation, includes * for stops)")411 st.code(format_seq(prot_full, wrap_width))412 413 fasta_header = ">translated_full|frame={}|strand={}".format(frame, '+' if strand.startswith('+') else '-')414 st.download_button(415 "π₯ Download full translation (FASTA)",416 data=f"{fasta_header}\n{format_seq(prot_full, 60)}\n",417 file_name="protein_full.fasta",418 mime="text/plain",419 )420 421# ----------------------------422# Tab 2 β ORF Finder (6 frames)423# ----------------------------424with tab2:425 st.subheader("π ORF Finder (6 frames, AUGβStop)")426 st.caption("Coordinates are reported relative to the sequence exactly as pasted below (5'β3', position 0 = first base), independent of any strand/frame choice made in the Synthesis tab.")427 orf_input = st.text_area("DNA sequence to scan", height=160)428 if st.button("π Find ORFs") and orf_input:429 dna = clean_dna(orf_input)430 if not dna:431 st.error("No valid nucleotides found. Please provide A/T/C/G.")432 elif len(dna) > MAX_INPUT_LEN:433 st.error(434 f"Input is {len(dna):,} bp, which exceeds the {MAX_INPUT_LEN:,} bp limit for this tool. "435 "Please provide a shorter sequence."436 )437 else:438 orfs = find_orfs(dna, min_len_aa=min_orf_len)439 if not orfs:440 st.info("No ORFs β₯ minimum length found.")441 else:442 # Summary table443 rows = []444 for i, o in enumerate(orfs, start=1):445 rows.append({446 'Rank': i,447 'Strand': o['strand'],448 'Frame': o['frame'],449 'NT start': o['nt_start'],450 'NT end': o['nt_end'],451 'Length (aa)': o['length_aa']452 })453 st.dataframe(rows, use_container_width=True, hide_index=True)454 455 sel = st.number_input("Select ORF rank to inspect", min_value=1, max_value=len(orfs), value=1, step=1)456 chosen = orfs[int(sel) - 1]457 st.markdown(f"**Chosen ORF:** strand {chosen['strand']} β’ frame {chosen['frame']} β’ {chosen['length_aa']} aa")458 prot = chosen['protein']459 seq_show = prot if show_one_letter else '-'.join(AA1_TO_AA3.get(a, 'Xxx') for a in prot)460 st.code(format_seq(seq_show, wrap_width))461 462 stats, glyco, tms = simple_features(prot)463 c1, c2, c3 = st.columns(3)464 with c1:465 st.metric("Length (aa)", stats['length'])466 st.metric("MW (Da)", stats['mw'])467 with c2:468 st.metric("pI (theoretical)", stats['pi'])469 st.metric("Aromaticity", stats['aromaticity'])470 with c3:471 st.metric("Instability Index", stats['instability_index'])472 st.metric("GRAVY", stats['gravy'])473 474 fasta_header = ">orf_rank{}|strand={}|frame={}|len={}".format(int(sel), chosen['strand'], chosen['frame'], chosen['length_aa'])475 st.download_button(476 "π₯ Download ORF protein (FASTA)",477 data=f"{fasta_header}\n{format_seq(prot, 60)}\n",478 file_name=f"orf_{int(sel)}.fasta",479 mime="text/plain",480 )481 482# ----------------------------483# Tab 3 β Reverse Translation484# ----------------------------485with tab3:486 st.subheader("π Reverse Translation (Protein β mRNA)")487 st.caption("Uses human-preferred codons by default; provides one codon per amino acid.")488 prot_in = st.text_input("Protein sequence (1-letter, e.g., MVLTI...) ")489 if prot_in:490 prot_in = prot_in.strip().upper()491 if re.fullmatch(r"[ACDEFGHIKLMNPQRSTVWY\*]+", prot_in):492 mrna_bt = back_translate(prot_in, strategy='human_top')493 st.code(mrna_bt, language='text')494 st.download_button(495 "π₯ Download back-translated mRNA (codon list)",496 data=mrna_bt,497 file_name="back_translation.txt",498 mime="text/plain",499 )500 501 # Show degeneracy helper for the first few residues502 st.markdown("**Codon options (first 10 aa)**")503 first10 = prot_in[:10]504 for i, aa in enumerate(first10, start=1):505 opts = AA_TO_CODONS.get(aa, [])506 st.write(f"{i:>2}. {aa} β {', '.join(opts) if opts else 'β'}")507 else:508 st.error("Invalid characters. Use 20 canonical amino acids and '*' for stop.")509 510# ----------------------------511# Tab 4 β 3D Structure512# ----------------------------513def _is_pdb_id(text: str) -> bool:514 # PDB IDs are always exactly 4 characters: 1 digit + 3 alphanumerics.515 return bool(re.fullmatch(r"[0-9][A-Za-z0-9]{3}", text)) and len(text) == 4516 517 518def _is_uniprot_id(text: str) -> bool:519 # UniProt accessions: 6 or 10 characters, start with a letter, alphanumeric.520 # Not a full validation of the UniProt spec, just enough to distinguish521 # from a free-text name/search term.522 return bool(re.fullmatch(r"[A-Za-z][A-Za-z0-9]{5}([A-Za-z0-9]{4})?", text)) and len(text) in (6, 10)523 524 525def _render_structure(pid: str):526 pid = pid.strip()527 if _is_pdb_id(pid):528 source_label = "PDB"529 fetch_url = f"https://files.rcsb.org/download/{pid.upper()}.pdb"530 entry_url = f"https://www.rcsb.org/structure/{pid.upper()}"531 else:532 source_label = "AlphaFold"533 up = pid.upper()534 fetch_url = f"https://alphafold.ebi.ac.uk/files/AF-{up}-F1-model_v4.pdb"535 entry_url = f"https://alphafold.ebi.ac.uk/entry/{up}"536 537 try:538 import py3Dmol539 540 # Fetch the PDB file server-side (in Python) rather than letting541 # py3Dmol's JS fetch it client-side via addModelFromUri. The542 # client-side fetch is frequently blocked by CORS depending on543 # the browser/host, which causes a silently blank viewer with no544 # Python-visible error. Fetching here and passing raw text into545 # addModel() avoids that failure mode entirely.546 resp = requests.get(fetch_url, timeout=15)547 if resp.status_code == 200 and resp.text.strip():548 view = py3Dmol.view(width=600, height=450)549 view.addModel(resp.text, 'pdb')550 view.setStyle({'cartoon': {'color': 'spectrum'}})551 view.zoomTo()552 view.render()553 st.components.v1.html(view._make_html(), height=470, width=650, scrolling=True)554 st.caption(f"Source: [{source_label} entry]({entry_url})")555 else:556 st.error(f"Could not fetch structure from {source_label} (HTTP {resp.status_code}). Check the ID and try again.")557 st.info("Direct link to try in your browser:")558 st.code(fetch_url)559 except requests.exceptions.RequestException as e:560 st.error(f"Network error fetching structure: {e}")561 st.info("If this environment blocks outbound requests, try opening the model link directly:")562 st.code(fetch_url)563 except Exception as e:564 st.error(f"3D viewer error: {e}")565 st.info("If this environment blocks external URLs, try opening the model links directly:")566 st.code(fetch_url)567 568 569with tab4:570 st.subheader("π AlphaFold/PDB 3D Structure Viewer")571 st.caption(572 "Enter a 4-character PDB ID (e.g., 1A3N), a UniProt ID (e.g., P69905), "573 "or a common name (e.g., \"sars-cov-2 spike\", \"hemoglobin\") to search PDB entries by name."574 )575 pid = st.text_input("PDB ID, UniProt ID, or structure name")576 577 if pid:578 pid_stripped = pid.strip()579 580 if _is_pdb_id(pid_stripped) or _is_uniprot_id(pid_stripped):581 # Looks like a literal accession β fetch and render directly.582 _render_structure(pid_stripped)583 else:584 # Treat as a free-text search term against RCSB PDB.585 with st.spinner(f"Searching PDB for \"{pid_stripped}\"..."):586 try:587 matches = search_rcsb_by_name(pid_stripped)588 except requests.exceptions.RequestException as e:589 matches = None590 st.error(f"Search error: {e}")591 592 if matches is not None:593 if not matches:594 st.warning(595 f"No PDB entries found for \"{pid_stripped}\". "596 "Try a different term, or enter a literal PDB/UniProt ID."597 )598 else:599 st.markdown(f"**{len(matches)} match(es) found β pick one to view:**")600 labels = [f"{m['id']} β {m['title']}" for m in matches]601 chosen_label = st.radio("Search results", labels, label_visibility="collapsed")602 chosen_id = matches[labels.index(chosen_label)]["id"]603 _render_structure(chosen_id)604 605# ----------------------------606# Tab 5 β About607# ----------------------------608with tab5:609 st.markdown(610 """611 ### βΉοΈ About v2612 **What's new/improved:**613 - Robust **NCBI search & fetch** with selectable results and caching614 - Clean DNA handling, **strand & frame controls**615 - Clear **AUGβStop** translation and full translation (with `*` stops)616 - **Protein stats** (MW, pI, aromaticity, instability index, GRAVY)617 - Simple **feature prediction**: NXS/T glycosites and KD-based TM helices618 - Compact **back-translation** using human-preferred codons619 - **6-frame ORF finder** with sortable table and downloads620 - **AlphaFold/PDB** 3D viewer with graceful fallback621 - Better downloads (FASTA formatting), wider layout & wrapped sequence display622 - Input length guard to prevent pasting whole chromosomes into the scanner623 - NCBI fetch/search now goes through cached helpers (fewer redundant API calls)624 625 **Tips**626 - Provide a valid NCBI email (and optional API key) to be a good E-utilities citizen.627 - Use the ORF Finder to verify intronless sequences before translating.628 - Back-translation is heuristic; for cloning, tailor codon usage to your host.629 - ORF Finder coordinates are relative to the sequence as pasted in that tab, not the Synthesis tab's strand/frame selection.630 """631 )632 