mekosotto Claude Opus 4.7 (1M context) commited on
Commit
0e8a63d
·
1 Parent(s): 62d4000

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>

Files changed (1) hide show
  1. 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="CCO",
311
  key="bbb_smiles",
312
- help="Examples: CCO (ethanol, BBB+), CC(=O)Nc1ccc(O)cc1 (paracetamol)",
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}): "