Sync from GitHub (preserve manual model files)
Browse files- StreamlitApp/StreamlitApp.py +60 -74
StreamlitApp/StreamlitApp.py
CHANGED
|
@@ -86,14 +86,10 @@ if "predict_input_widget" not in st.session_state:
|
|
| 86 |
st.session_state.predict_input_widget = ""
|
| 87 |
if "analyze_input" not in st.session_state:
|
| 88 |
st.session_state.analyze_input = "" # last analyze input
|
| 89 |
-
if "analyze_input_widget" not in st.session_state:
|
| 90 |
-
st.session_state.analyze_input_widget = st.session_state.get("analyze_input", "")
|
| 91 |
if "analyze_output" not in st.session_state:
|
| 92 |
st.session_state.analyze_output = None # (label, conf_display, comp, props, analysis)
|
| 93 |
if "optimize_input" not in st.session_state:
|
| 94 |
st.session_state.optimize_input = "" # last optimize input
|
| 95 |
-
if "optimize_input_widget" not in st.session_state:
|
| 96 |
-
st.session_state.optimize_input_widget = st.session_state.get("optimize_input", "")
|
| 97 |
if "optimize_output" not in st.session_state:
|
| 98 |
st.session_state.optimize_output = None # (orig_seq, orig_conf, improved_seq, improved_conf, history)
|
| 99 |
if "visualize_sequences" not in st.session_state:
|
|
@@ -110,8 +106,8 @@ if st.sidebar.button("Clear All Fields"):
|
|
| 110 |
# clear only our known keys
|
| 111 |
keys = ["predictions", "predict_ran",
|
| 112 |
"predict_input_widget",
|
| 113 |
-
"analyze_input", "
|
| 114 |
-
"optimize_input", "
|
| 115 |
"visualize_sequences", "visualize_df"]
|
| 116 |
for k in keys:
|
| 117 |
if k in st.session_state:
|
|
@@ -135,7 +131,7 @@ if page == "Predict":
|
|
| 135 |
preset_cols = st.columns(2)
|
| 136 |
with preset_cols[0]:
|
| 137 |
if st.button("Use strong AMP example"):
|
| 138 |
-
st.session_state.predict_input_widget = "
|
| 139 |
st.rerun()
|
| 140 |
with preset_cols[1]:
|
| 141 |
if st.button("Use weak sequence example"):
|
|
@@ -197,7 +193,7 @@ if page == "Predict":
|
|
| 197 |
|
| 198 |
# If user hasn't just run predictions, show the last saved results (if any)
|
| 199 |
if st.session_state.predictions and not (run and st.session_state.predict_ran is False):
|
| 200 |
-
st.
|
| 201 |
|
| 202 |
top_candidate = choose_top_candidate(st.session_state.predictions)
|
| 203 |
if top_candidate:
|
|
@@ -205,34 +201,23 @@ if page == "Predict":
|
|
| 205 |
st.markdown(
|
| 206 |
"<div style='border:1px solid #e6e6e6; border-radius:0.6rem; padding:0.8rem; background:#fafafa;'>"
|
| 207 |
, unsafe_allow_html=True)
|
| 208 |
-
st.
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
st.write(f"Confidence: **{format_conf_percent(top_candidate['predicted_confidence'], digits=1)}**")
|
| 211 |
st.write(f"Reason: {top_candidate['Reason']}")
|
| 212 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 213 |
|
| 214 |
-
# Copy-to-clipboard + compact row view (buttons placed next to each sequence).
|
| 215 |
-
st.markdown("### Results with Copy")
|
| 216 |
-
for idx, row in enumerate(st.session_state.predictions):
|
| 217 |
-
seq = row.get("Sequence", "")
|
| 218 |
-
label = row.get("Prediction", "")
|
| 219 |
-
conf = row.get("Confidence", 0.0)
|
| 220 |
-
conf_display = round(float(conf) * 100, 1) if label == "AMP" else round((1 - float(conf)) * 100, 1)
|
| 221 |
-
cols = st.columns([7, 2, 1])
|
| 222 |
-
with cols[0]:
|
| 223 |
-
st.code(seq, language="text")
|
| 224 |
-
with cols[1]:
|
| 225 |
-
st.write(f"**{label}**")
|
| 226 |
-
st.caption(f"{conf_display}% confidence")
|
| 227 |
-
with cols[2]:
|
| 228 |
-
if st.button("Copy", key=f"copy_pred_{idx}"):
|
| 229 |
-
_try_copy_to_clipboard(seq)
|
| 230 |
-
toast_fn = getattr(st, "toast", None)
|
| 231 |
-
if toast_fn is not None:
|
| 232 |
-
toast_fn("Copied to clipboard")
|
| 233 |
-
else:
|
| 234 |
-
st.success("Copied to clipboard")
|
| 235 |
-
|
| 236 |
# Keep the original dataframe for full overview/download compatibility.
|
| 237 |
st.dataframe(pd.DataFrame(st.session_state.predictions), use_container_width=True)
|
| 238 |
csv = pd.DataFrame(st.session_state.predictions).to_csv(index=False)
|
|
@@ -242,22 +227,11 @@ if page == "Predict":
|
|
| 242 |
elif page == "Analyze":
|
| 243 |
st.header("Sequence Analysis")
|
| 244 |
|
| 245 |
-
preset_cols = st.columns(2)
|
| 246 |
-
with preset_cols[0]:
|
| 247 |
-
if st.button("Use strong AMP example"):
|
| 248 |
-
st.session_state.analyze_input_widget = "KWKLFKKIGAVLKVL"
|
| 249 |
-
st.rerun()
|
| 250 |
-
with preset_cols[1]:
|
| 251 |
-
if st.button("Use weak sequence example"):
|
| 252 |
-
st.session_state.analyze_input_widget = "GAGAGAGAGAGA"
|
| 253 |
-
st.rerun()
|
| 254 |
-
|
| 255 |
# show the last saved analyze output if user navigated back
|
| 256 |
last_seq = st.session_state.analyze_input
|
| 257 |
seq = st.text_input(
|
| 258 |
"Enter a peptide sequence to analyze:",
|
| 259 |
-
value=
|
| 260 |
-
key="analyze_input_widget",
|
| 261 |
)
|
| 262 |
|
| 263 |
warn = sequence_length_warning(seq)
|
|
@@ -349,21 +323,40 @@ elif page == "Analyze":
|
|
| 349 |
"Net Charge": "Favorable" if charge > 0 else "Neutral" if charge == 0 else "Unfavorable",
|
| 350 |
"Molecular Weight": "Acceptable" if 500 <= mw <= 5000 else "Extreme"
|
| 351 |
}
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
st.subheader("Property Radar Chart")
|
| 363 |
-
_tooltip_label(
|
| 364 |
-
"Radar",
|
| 365 |
-
"Radar chart compares length, hydrophobicity, net charge, and molecular weight against ideal AMP ranges.",
|
| 366 |
-
)
|
| 367 |
categories = ["Length", "Hydrophobic Fraction", "Net Charge", "Molecular Weight"]
|
| 368 |
values = [min(length / 50, 1), min(hydro, 1), 1 if charge > 0 else 0, min(mw / 5000, 1)]
|
| 369 |
values += values[:1]
|
|
@@ -390,7 +383,6 @@ elif page == "Analyze":
|
|
| 390 |
st.write(f"- {line}")
|
| 391 |
|
| 392 |
# Export analysis report
|
| 393 |
-
st.markdown("---")
|
| 394 |
st.subheader("Export Analysis Report")
|
| 395 |
export_format = st.radio("Format", ["CSV", "TXT"], horizontal=True)
|
| 396 |
|
|
@@ -437,23 +429,10 @@ elif page == "Analyze":
|
|
| 437 |
elif page == "Optimize":
|
| 438 |
st.header("AMP Sequence Optimizer")
|
| 439 |
|
| 440 |
-
|
| 441 |
-
with preset_cols[0]:
|
| 442 |
-
if st.button("Use strong AMP example"):
|
| 443 |
-
st.session_state.optimize_input_widget = "KWKLFKKIGAVLKVL"
|
| 444 |
-
st.session_state.optimize_input = "KWKLFKKIGAVLKVL"
|
| 445 |
-
st.rerun()
|
| 446 |
-
with preset_cols[1]:
|
| 447 |
-
if st.button("Use weak sequence example"):
|
| 448 |
-
st.session_state.optimize_input_widget = "GAGAGAGAGAGA"
|
| 449 |
-
st.session_state.optimize_input = "GAGAGAGAGAGA"
|
| 450 |
-
st.rerun()
|
| 451 |
-
|
| 452 |
-
# Single entry point: text input retained across navigation (widget uses a dedicated key)
|
| 453 |
seq = st.text_input(
|
| 454 |
"Enter a peptide sequence to optimize:",
|
| 455 |
-
value=st.session_state.get("
|
| 456 |
-
key="optimize_input_widget",
|
| 457 |
)
|
| 458 |
|
| 459 |
# Run optimization when user changes input and clicks button
|
|
@@ -626,6 +605,13 @@ elif page == "Visualize":
|
|
| 626 |
elif page == "About":
|
| 627 |
st.header("About the Project")
|
| 628 |
st.markdown("""
|
| 629 |
-
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
""")
|
|
|
|
| 86 |
st.session_state.predict_input_widget = ""
|
| 87 |
if "analyze_input" not in st.session_state:
|
| 88 |
st.session_state.analyze_input = "" # last analyze input
|
|
|
|
|
|
|
| 89 |
if "analyze_output" not in st.session_state:
|
| 90 |
st.session_state.analyze_output = None # (label, conf_display, comp, props, analysis)
|
| 91 |
if "optimize_input" not in st.session_state:
|
| 92 |
st.session_state.optimize_input = "" # last optimize input
|
|
|
|
|
|
|
| 93 |
if "optimize_output" not in st.session_state:
|
| 94 |
st.session_state.optimize_output = None # (orig_seq, orig_conf, improved_seq, improved_conf, history)
|
| 95 |
if "visualize_sequences" not in st.session_state:
|
|
|
|
| 106 |
# clear only our known keys
|
| 107 |
keys = ["predictions", "predict_ran",
|
| 108 |
"predict_input_widget",
|
| 109 |
+
"analyze_input", "analyze_output",
|
| 110 |
+
"optimize_input", "optimize_output",
|
| 111 |
"visualize_sequences", "visualize_df"]
|
| 112 |
for k in keys:
|
| 113 |
if k in st.session_state:
|
|
|
|
| 131 |
preset_cols = st.columns(2)
|
| 132 |
with preset_cols[0]:
|
| 133 |
if st.button("Use strong AMP example"):
|
| 134 |
+
st.session_state.predict_input_widget = "ILPWKWPWWPWRR"
|
| 135 |
st.rerun()
|
| 136 |
with preset_cols[1]:
|
| 137 |
if st.button("Use weak sequence example"):
|
|
|
|
| 193 |
|
| 194 |
# If user hasn't just run predictions, show the last saved results (if any)
|
| 195 |
if st.session_state.predictions and not (run and st.session_state.predict_ran is False):
|
| 196 |
+
st.write("**Predictions (last run)**")
|
| 197 |
|
| 198 |
top_candidate = choose_top_candidate(st.session_state.predictions)
|
| 199 |
if top_candidate:
|
|
|
|
| 201 |
st.markdown(
|
| 202 |
"<div style='border:1px solid #e6e6e6; border-radius:0.6rem; padding:0.8rem; background:#fafafa;'>"
|
| 203 |
, unsafe_allow_html=True)
|
| 204 |
+
st.write("**Top Candidate**")
|
| 205 |
+
seq = top_candidate.get("Sequence", "")
|
| 206 |
+
cc = st.columns([9, 1])
|
| 207 |
+
with cc[0]:
|
| 208 |
+
st.code(seq, language="text")
|
| 209 |
+
with cc[1]:
|
| 210 |
+
if st.button("Copy", key="copy_top_candidate"):
|
| 211 |
+
_try_copy_to_clipboard(seq)
|
| 212 |
+
toast_fn = getattr(st, "toast", None)
|
| 213 |
+
if toast_fn is not None:
|
| 214 |
+
toast_fn("Copied to clipboard")
|
| 215 |
+
else:
|
| 216 |
+
st.success("Copied to clipboard")
|
| 217 |
st.write(f"Confidence: **{format_conf_percent(top_candidate['predicted_confidence'], digits=1)}**")
|
| 218 |
st.write(f"Reason: {top_candidate['Reason']}")
|
| 219 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
# Keep the original dataframe for full overview/download compatibility.
|
| 222 |
st.dataframe(pd.DataFrame(st.session_state.predictions), use_container_width=True)
|
| 223 |
csv = pd.DataFrame(st.session_state.predictions).to_csv(index=False)
|
|
|
|
| 227 |
elif page == "Analyze":
|
| 228 |
st.header("Sequence Analysis")
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
# show the last saved analyze output if user navigated back
|
| 231 |
last_seq = st.session_state.analyze_input
|
| 232 |
seq = st.text_input(
|
| 233 |
"Enter a peptide sequence to analyze:",
|
| 234 |
+
value=last_seq,
|
|
|
|
| 235 |
)
|
| 236 |
|
| 237 |
warn = sequence_length_warning(seq)
|
|
|
|
| 323 |
"Net Charge": "Favorable" if charge > 0 else "Neutral" if charge == 0 else "Unfavorable",
|
| 324 |
"Molecular Weight": "Acceptable" if 500 <= mw <= 5000 else "Extreme"
|
| 325 |
}
|
| 326 |
+
def _info_icon(tooltip_text: str) -> str:
|
| 327 |
+
safe = _html.escape(tooltip_text, quote=True)
|
| 328 |
+
return (
|
| 329 |
+
"<span "
|
| 330 |
+
f"title='{safe}' "
|
| 331 |
+
"style=\"display:inline-flex; align-items:center; justify-content:center; "
|
| 332 |
+
"margin-left:6px; width:16px; height:16px; border-radius:50%; "
|
| 333 |
+
"background:#f2f2f2; border:1px solid #d9d9d9; color:#333; "
|
| 334 |
+
"font-size:12px; font-weight:700; cursor:help;\">(i)</span>"
|
| 335 |
+
)
|
| 336 |
|
| 337 |
+
# Render the favorability table with working inline tooltips.
|
| 338 |
+
hydro_label = f"Hydrophobic Fraction{_info_icon('Fraction of residues that prefer non-aqueous environments')}"
|
| 339 |
+
charge_label = f"Net Charge{_info_icon('Positive charge helps peptides bind bacterial membranes')}"
|
| 340 |
+
table_html = (
|
| 341 |
+
"<table style='width:100%; border-collapse:collapse;'>"
|
| 342 |
+
"<thead>"
|
| 343 |
+
"<tr>"
|
| 344 |
+
"<th style='text-align:left; padding:8px; border-bottom:1px solid #e6e6e6;'>Property</th>"
|
| 345 |
+
"<th style='text-align:right; padding:8px; border-bottom:1px solid #e6e6e6;'>Value</th>"
|
| 346 |
+
"<th style='text-align:left; padding:8px; border-bottom:1px solid #e6e6e6;'>Favorability</th>"
|
| 347 |
+
"</tr>"
|
| 348 |
+
"</thead>"
|
| 349 |
+
"<tbody>"
|
| 350 |
+
f"<tr><td style='padding:8px;'>{_html.escape('Length')}</td><td style='padding:8px; text-align:right;'>{_html.escape(str(length))}</td><td style='padding:8px;'>{_html.escape(favorability['Length'])}</td></tr>"
|
| 351 |
+
f"<tr><td style='padding:8px;'>{hydro_label}</td><td style='padding:8px; text-align:right;'>{_html.escape(str(hydro))}</td><td style='padding:8px;'>{_html.escape(favorability['Hydrophobic Fraction'])}</td></tr>"
|
| 352 |
+
f"<tr><td style='padding:8px;'>{charge_label}</td><td style='padding:8px; text-align:right;'>{_html.escape(str(charge))}</td><td style='padding:8px;'>{_html.escape(favorability['Net Charge'])}</td></tr>"
|
| 353 |
+
f"<tr><td style='padding:8px;'>{_html.escape('Molecular Weight')}</td><td style='padding:8px; text-align:right;'>{_html.escape(str(mw))}</td><td style='padding:8px;'>{_html.escape(favorability['Molecular Weight'])}</td></tr>"
|
| 354 |
+
"</tbody>"
|
| 355 |
+
"</table>"
|
| 356 |
+
)
|
| 357 |
+
st.markdown(table_html, unsafe_allow_html=True)
|
| 358 |
|
| 359 |
st.subheader("Property Radar Chart")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
categories = ["Length", "Hydrophobic Fraction", "Net Charge", "Molecular Weight"]
|
| 361 |
values = [min(length / 50, 1), min(hydro, 1), 1 if charge > 0 else 0, min(mw / 5000, 1)]
|
| 362 |
values += values[:1]
|
|
|
|
| 383 |
st.write(f"- {line}")
|
| 384 |
|
| 385 |
# Export analysis report
|
|
|
|
| 386 |
st.subheader("Export Analysis Report")
|
| 387 |
export_format = st.radio("Format", ["CSV", "TXT"], horizontal=True)
|
| 388 |
|
|
|
|
| 429 |
elif page == "Optimize":
|
| 430 |
st.header("AMP Sequence Optimizer")
|
| 431 |
|
| 432 |
+
# Single entry point: text input retained across navigation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
seq = st.text_input(
|
| 434 |
"Enter a peptide sequence to optimize:",
|
| 435 |
+
value=st.session_state.get("optimize_input", ""),
|
|
|
|
| 436 |
)
|
| 437 |
|
| 438 |
# Run optimization when user changes input and clicks button
|
|
|
|
| 605 |
elif page == "About":
|
| 606 |
st.header("About the Project")
|
| 607 |
st.markdown("""
|
| 608 |
+
PeptideAI is a lightweight Streamlit app for exploring antimicrobial peptide (AMP) sequences.
|
| 609 |
+
|
| 610 |
+
It uses a trained neural network to estimate whether a peptide is likely to be antimicrobial, then helps you interpret and improve candidates:
|
| 611 |
+
- **Predict**: run batch predictions and highlights the best candidate using simple profile heuristics.
|
| 612 |
+
- **Analyze**: show amino-acid composition, physicochemical properties, and an AMP-range radar chart.
|
| 613 |
+
- **Optimize**: iteratively propose residue mutations and summarize how confidence/charge/hydrophobic balance changes.
|
| 614 |
+
- **Visualize**: embed sequences and inspect clusters with t-SNE.
|
| 615 |
+
|
| 616 |
+
Note: predictions are model-based heuristics and are not a substitute for wet-lab validation.
|
| 617 |
""")
|