feat(frontend): edge-case dropdown for BBB robustness demo
Browse files- Curated 5 robustness probes: ethanol baseline, invalid SMILES, empty
string, cyclosporine-like macrocycle (OOD), decafluorobiphenyl (OOD).
- HTTP 400 branch shows st.warning (recoverable) instead of st.error to
visualize the robustness story to the jury.
- No backend / schema / test changes — UI curation only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/frontend/app.py +61 -2
src/frontend/app.py
CHANGED
|
@@ -305,11 +305,62 @@ def _render_bbb_tab() -> None:
|
|
| 305 |
"explaining the decision.",
|
| 306 |
)
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
smiles = st.text_input(
|
| 309 |
"SMILES string",
|
| 310 |
-
value="
|
| 311 |
key="bbb_smiles",
|
| 312 |
-
help="Examples: CCO (ethanol
|
| 313 |
)
|
| 314 |
top_k = st.slider(
|
| 315 |
"SHAP features to display", min_value=3, max_value=10, value=5, key="bbb_topk",
|
|
@@ -328,6 +379,14 @@ def _render_bbb_tab() -> None:
|
|
| 328 |
"`python -m src.models.bbb_model` to train it, "
|
| 329 |
"then retry."
|
| 330 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
else:
|
| 332 |
st.error(
|
| 333 |
f"Prediction failed (HTTP {e.response.status_code}): "
|
|
|
|
| 305 |
"explaining the decision.",
|
| 306 |
)
|
| 307 |
|
| 308 |
+
EDGE_CASES = {
|
| 309 |
+
"Custom input (default)": {
|
| 310 |
+
"smiles": "CCO",
|
| 311 |
+
"label": "Ethanol — small, drug-like, BBB-permeable",
|
| 312 |
+
"expectation": "High confidence, label = permeable",
|
| 313 |
+
},
|
| 314 |
+
"Invalid SMILES (parse-error path)": {
|
| 315 |
+
"smiles": "this_is_not_a_valid_molecule_at_all_!!",
|
| 316 |
+
"label": "Garbage string — should not parse",
|
| 317 |
+
"expectation": "API returns HTTP 400 with parse error; UI shows recoverable warning",
|
| 318 |
+
},
|
| 319 |
+
"Empty string (boundary)": {
|
| 320 |
+
"smiles": "",
|
| 321 |
+
"label": "Empty input — boundary condition",
|
| 322 |
+
"expectation": "Pydantic accepts empty; API returns 400 (RDKit cannot parse)",
|
| 323 |
+
},
|
| 324 |
+
"Massive OOD: cyclosporine-like macrocycle": {
|
| 325 |
+
"smiles": (
|
| 326 |
+
"CC[C@H](C)[C@@H]1NC(=O)[C@H](CC(C)C)N(C)C(=O)[C@H](CC(C)C)N(C)C(=O)"
|
| 327 |
+
"[C@@H]2CCCN2C(=O)[C@H](C(C)C)NC(=O)[C@H]([C@@H](C)CC)N(C)C(=O)"
|
| 328 |
+
"[C@H](C)NC(=O)[C@H](C)NC(=O)[C@H](CC(C)C)N(C)C(=O)[C@@H](NC(=O)"
|
| 329 |
+
"[C@H](CC(C)C)N(C)C(=O)CN(C)C1=O)C(C)C"
|
| 330 |
+
),
|
| 331 |
+
"label": "Cyclosporine — 11-residue macrocycle (~1.2 kDa)",
|
| 332 |
+
"expectation": (
|
| 333 |
+
"Far outside training distribution; model should hedge "
|
| 334 |
+
"with low confidence (well-calibrated systems don't "
|
| 335 |
+
"pretend to know)."
|
| 336 |
+
),
|
| 337 |
+
},
|
| 338 |
+
"OOD: heavy halogenated aromatic": {
|
| 339 |
+
"smiles": "Fc1c(F)c(F)c(c(F)c1F)c2c(F)c(F)c(F)c(F)c2F",
|
| 340 |
+
"label": "Decafluorobiphenyl — extreme halogen density",
|
| 341 |
+
"expectation": "Rare scaffold; expect lowered confidence vs ethanol",
|
| 342 |
+
},
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
case_name = st.selectbox(
|
| 346 |
+
"Test Edge Cases",
|
| 347 |
+
options=list(EDGE_CASES.keys()),
|
| 348 |
+
index=0,
|
| 349 |
+
key="bbb_case",
|
| 350 |
+
help=(
|
| 351 |
+
"Pick a robustness probe. Each case demonstrates how the "
|
| 352 |
+
"system handles a real-world failure mode — invalid input, "
|
| 353 |
+
"out-of-distribution molecules, or boundary conditions."
|
| 354 |
+
),
|
| 355 |
+
)
|
| 356 |
+
case = EDGE_CASES[case_name]
|
| 357 |
+
st.caption(f"**Probe:** {case['label']} · **Expected:** {case['expectation']}")
|
| 358 |
+
|
| 359 |
smiles = st.text_input(
|
| 360 |
"SMILES string",
|
| 361 |
+
value=case["smiles"],
|
| 362 |
key="bbb_smiles",
|
| 363 |
+
help="Examples: CCO (ethanol), CC(=O)Nc1ccc(O)cc1 (paracetamol)",
|
| 364 |
)
|
| 365 |
top_k = st.slider(
|
| 366 |
"SHAP features to display", min_value=3, max_value=10, value=5, key="bbb_topk",
|
|
|
|
| 379 |
"`python -m src.models.bbb_model` to train it, "
|
| 380 |
"then retry."
|
| 381 |
)
|
| 382 |
+
elif e.response.status_code == 400:
|
| 383 |
+
# Robustness story: show the WARNING instead of an ERROR
|
| 384 |
+
# — invalid input is a recoverable path, not a crash.
|
| 385 |
+
st.warning(
|
| 386 |
+
f"Robustness check passed: API rejected the input "
|
| 387 |
+
f"with HTTP 400 (no crash). Detail: "
|
| 388 |
+
f"{e.response.json().get('detail', e.response.text)}"
|
| 389 |
+
)
|
| 390 |
else:
|
| 391 |
st.error(
|
| 392 |
f"Prediction failed (HTTP {e.response.status_code}): "
|