From 35801c2b003fdf908681f9ac32de04d52e4bfd10 Mon Sep 17 00:00:00 2001 From: "Austin L." <86896075+rvnminers-A-and-N@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:46:38 +0000 Subject: [PATCH] feat(demo): structures + names on substitution; dosing/applicability/tox notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Substitution candidates now show a 2D structure thumbnail and the molecule's name (PubChem Title, cached/best-effort) alongside SMILES + known tastes + % match — app.py enriches each neighbor via reusable _svg() + _name() helpers; the workbench renders thumbnail | name/SMILES/tastes | % match. - Footnote notes (web interface): the trained heads are for ORGANIC molecules (carbon-free flagged out-of-domain); tox-assay flags are indicative caution-only; quantitative dosing/odor-activity needs threshold tables (licensed/customer), so it stays qualitative here. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com> --- training/app.py | 54 +++++++++++++++++++++++++++++------------ training/workbench.html | 27 +++++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/training/app.py b/training/app.py index 8d23614..bcd1f9d 100644 --- a/training/app.py +++ b/training/app.py @@ -18,6 +18,7 @@ prediction core doesn't change. """ +from functools import lru_cache from pathlib import Path from fastapi import FastAPI @@ -45,6 +46,37 @@ def _resolve(text: str): return None +def _svg(smi, w=320, h=220): + """2D structure SVG; None if drawing is unavailable (headless box w/o libXrender).""" + mol = Chem.MolFromSmiles(smi) if smi else None + if mol is None: + return None + try: + from rdkit.Chem.Draw import rdMolDraw2D + d = rdMolDraw2D.MolDraw2DSVG(w, h) + d.DrawMolecule(mol) + d.FinishDrawing() + return d.GetDrawingText() + except Exception: # noqa: BLE001 — missing X11 libs etc.; degrade gracefully + return None + + +@lru_cache(maxsize=8192) +def _name(smi): + """PubChem common name (Title) for a SMILES — cached, best-effort, short timeout.""" + import json + import urllib.parse + import urllib.request + url = ("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/" + f"{urllib.parse.quote(smi)}/property/Title/JSON") + try: + with urllib.request.urlopen(url, timeout=4) as r: + d = json.load(r) + return d["PropertyTable"]["Properties"][0].get("Title") + except Exception: # noqa: BLE001 — not found / timeout / throttled + return None + + class Query(BaseModel): smiles: str k: int = 8 @@ -64,25 +96,17 @@ def api_neighbors(q: Query): smi = _resolve(q.smiles) if not smi: return {"neighbors": []} - return P.substitute(smi, k=q.k) + res = P.substitute(smi, k=q.k) + for n in res.get("neighbors", []): # enrich each candidate with a structure + a name + n["svg"] = _svg(n["smiles"], 132, 96) + n["name"] = _name(n["smiles"]) + return res @app.post("/api/structure") def api_structure(q: Query): - """2D structure depiction (SVG). Degrades to {svg: None} if RDKit's drawing module - can't load (e.g. a headless box missing libXrender) — never 500s the demo.""" - smi = _resolve(q.smiles) - mol = Chem.MolFromSmiles(smi) if smi else None - if mol is None: - return {"svg": None} - try: - from rdkit.Chem.Draw import rdMolDraw2D - d = rdMolDraw2D.MolDraw2DSVG(320, 220) - d.DrawMolecule(mol) - d.FinishDrawing() - return {"svg": d.GetDrawingText()} - except Exception: # noqa: BLE001 — missing X11 libs etc.; degrade gracefully - return {"svg": None} + """2D structure depiction (SVG); {svg: None} if drawing is unavailable.""" + return {"svg": _svg(_resolve(q.smiles))} @app.get("/", response_class=HTMLResponse) diff --git a/training/workbench.html b/training/workbench.html index abd27dc..f5f1a41 100644 --- a/training/workbench.html +++ b/training/workbench.html @@ -49,10 +49,14 @@ .chip{font-family:var(--mono);font-size:12px;padding:5px 11px;border-radius:20px; border:1px solid var(--line);background:var(--surface)} .chip.on{color:#fff;border-color:transparent} - .neighbor{display:flex;justify-content:space-between;align-items:center;gap:12px; + .neighbor{display:flex;align-items:center;gap:12px; padding:11px 0;border-bottom:1px solid var(--line)} .neighbor:last-child{border-bottom:none} - .neighbor .s{font-family:var(--mono);font-size:12.5px;word-break:break-all} + .neighbor .nb-struct{flex:0 0 auto;width:64px;height:48px;background:#fff;border:1px solid var(--line);border-radius:6px} + .neighbor .nb-struct svg{width:64px;height:48px} + .neighbor .nb-info{flex:1;min-width:0} + .neighbor .nb-name{font-weight:600;font-size:13px;text-transform:capitalize;color:var(--ink)} + .neighbor .s{font-family:var(--mono);font-size:12px;color:var(--muted);word-break:break-all} .neighbor .sim{font-family:var(--mono);font-size:12px;color:var(--accent);white-space:nowrap} .neighbor .kt{font-family:var(--mono);font-size:11px;color:var(--muted)} .empty{color:var(--muted);font-size:14px;padding:8px 0} @@ -127,10 +131,13 @@