Skip to content

Commit bff45d6

Browse files
fix+feat(demo): correct sweetness-intensity; add names; clickable substitutions (#66)
QA fix (the capsaicin "×2.32" bug): sweet_intensity is log10(relative sweetness), so the old chip both showed it for NON-sweet molecules and mislabeled the log as a "×" multiplier. Now it shows only when sweet is predicted (>=0.5) and converts the log to a real multiplier — aspartame "~138x as sweet as sucrose", capsaicin/caffeine: no chip. Features: - Common (PubChem Title) + IUPAC names on the main result, alongside structure + SMILES (/api/names; _name -> _names returns both; cached/best-effort). - Click any substitution candidate to load and analyze it (data-smi + delegated handler). Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent f1a19a6 commit bff45d6

2 files changed

Lines changed: 42 additions & 15 deletions

File tree

training/app.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,19 @@ def _svg(smi, w=320, h=220):
6262

6363

6464
@lru_cache(maxsize=8192)
65-
def _name(smi):
66-
"""PubChem common name (Title) for a SMILES — cached, best-effort, short timeout."""
65+
def _names(smi):
66+
"""(common, IUPAC) names from PubChem for a SMILES — cached, best-effort, short timeout."""
6767
import json
6868
import urllib.parse
6969
import urllib.request
7070
url = ("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/"
71-
f"{urllib.parse.quote(smi)}/property/Title/JSON")
71+
f"{urllib.parse.quote(smi)}/property/Title,IUPACName/JSON")
7272
try:
7373
with urllib.request.urlopen(url, timeout=4) as r:
74-
d = json.load(r)
75-
return d["PropertyTable"]["Properties"][0].get("Title")
74+
p = json.load(r)["PropertyTable"]["Properties"][0]
75+
return p.get("Title"), p.get("IUPACName")
7676
except Exception: # noqa: BLE001 — not found / timeout / throttled
77-
return None
77+
return None, None
7878

7979

8080
class Query(BaseModel):
@@ -99,10 +99,18 @@ def api_neighbors(q: Query):
9999
res = P.substitute(smi, k=q.k)
100100
for n in res.get("neighbors", []): # enrich each candidate with a structure + a name
101101
n["svg"] = _svg(n["smiles"], 132, 96)
102-
n["name"] = _name(n["smiles"])
102+
n["name"] = _names(n["smiles"])[0]
103103
return res
104104

105105

106+
@app.post("/api/names")
107+
def api_names(q: Query):
108+
"""Common (PubChem Title) + IUPAC names for the queried molecule."""
109+
smi = _resolve(q.smiles)
110+
common, iupac = _names(smi) if smi else (None, None)
111+
return {"common": common, "iupac": iupac, "smiles": smi}
112+
113+
106114
@app.post("/api/structure")
107115
def api_structure(q: Query):
108116
"""2D structure depiction (SVG); {svg: None} if drawing is unavailable."""

training/workbench.html

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:20px 22px}
3434
.card h2{margin:0 0 4px;font-size:12px;font-weight:650;text-transform:uppercase;
3535
letter-spacing:.08em;color:var(--muted)}
36-
.smiles{font-family:var(--mono);font-size:13px;color:var(--muted);margin:0 0 16px;
36+
.smiles{font-family:var(--mono);font-size:13px;color:var(--muted);margin:0 0 4px;
3737
word-break:break-all}
38+
.cname{font-size:16px;font-weight:650;color:var(--ink);margin:0 0 4px}
39+
.iupac{font-family:var(--mono);font-size:11px;color:var(--muted);margin:0 0 14px;word-break:break-all}
3840
.meter{margin:13px 0}
3941
.meter .row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px}
4042
.meter .name{font-weight:600;font-size:14px;text-transform:capitalize}
@@ -52,6 +54,8 @@
5254
.neighbor{display:flex;align-items:center;gap:12px;
5355
padding:11px 0;border-bottom:1px solid var(--line)}
5456
.neighbor:last-child{border-bottom:none}
57+
.neighbor{cursor:pointer}
58+
.neighbor:hover{background:var(--accent-soft)}
5559
.neighbor .nb-struct{flex:0 0 auto;width:64px;height:48px;background:#fff;border:1px solid var(--line);border-radius:6px}
5660
.neighbor .nb-struct svg{width:64px;height:48px}
5761
.neighbor .nb-info{flex:1;min-width:0}
@@ -101,7 +105,9 @@ <h1>Flavor Workbench</h1>
101105
<div class="card">
102106
<h2>Flavor read</h2>
103107
<div class="structure" id="structure"></div>
108+
<div class="cname" id="cname"></div>
104109
<p class="smiles" id="smiles"></p>
110+
<p class="iupac" id="iupac"></p>
105111
<div id="domainBanner" class="domain-banner" style="display:none"></div>
106112
<div id="meters"></div>
107113
<div class="chips" id="chips"></div>
@@ -171,13 +177,14 @@ <h2>Aroma</h2>
171177
go.disabled = true; go.textContent = 'Reading…';
172178
$('err').style.display='none';
173179
try{
174-
const [p, n, s] = await Promise.all([
180+
const [p, n, s, nm] = await Promise.all([
175181
post('/api/predict',{smiles:text}),
176182
post('/api/neighbors',{smiles:text, k:8}),
177-
post('/api/structure',{smiles:text})
183+
post('/api/structure',{smiles:text}),
184+
post('/api/names',{smiles:text})
178185
]);
179186
if(p.error){ throw new Error(p.error); }
180-
render(p, n.neighbors||[], (s&&s.svg)||null);
187+
render(p, n.neighbors||[], (s&&s.svg)||null, nm||{});
181188
}catch(e){
182189
$('results').style.display='none'; $('behaviorCard').style.display='none'; $('aromaCard').style.display='none'; $('footnote').style.display='none';
183190
$('err').textContent = e.message; $('err').style.display='block';
@@ -186,9 +193,14 @@ <h2>Aroma</h2>
186193
}
187194
}
188195

189-
function render(p, neighbors, svg){
196+
function render(p, neighbors, svg, names){
197+
names = names || {};
190198
$('structure').innerHTML = svg || '';
199+
$('cname').textContent = names.common || '';
200+
$('cname').style.display = names.common ? 'block' : 'none';
191201
$('smiles').textContent = p.smiles;
202+
$('iupac').textContent = names.iupac ? ('IUPAC: ' + names.iupac) : '';
203+
$('iupac').style.display = names.iupac ? 'block' : 'none';
192204
const inDomain = !(p.applicability && p.applicability.in_domain === false);
193205
const db = $('domainBanner');
194206
db.style.display = inDomain ? 'none' : 'block';
@@ -211,8 +223,10 @@ <h2>Aroma</h2>
211223
p.sour && p.sour_reason && p.sour_reason.length ? `sour · ${p.sour_reason[0]}` : 'not sour'));
212224
c.push(chip('salty', p.salty===true, p.salty===true?'var(--salty)':null,
213225
p.salty===true ? 'salty (known)' : 'salty — not in data'));
214-
if(inDomain && typeof p.sweet_intensity === 'number')
215-
c.push(chip('intensity', true, 'var(--sweet)', `sweetness ×${p.sweet_intensity} vs sucrose`));
226+
if(inDomain && p.sweet >= 0.5 && typeof p.sweet_intensity === 'number'){
227+
const x = Math.pow(10, p.sweet_intensity);
228+
c.push(chip('intensity', true, 'var(--sweet)', `~${x>=10?Math.round(x):x.toFixed(1)}× as sweet as sucrose`));
229+
}
216230
if(inDomain && p.multitaste) c.push(chip('multi', true, 'var(--aroma)', 'multi-taste'));
217231
$('chips').innerHTML = c.join('');
218232

@@ -221,7 +235,7 @@ <h2>Aroma</h2>
221235
$('neighbors').innerHTML = '<div class="empty">Build taste_master.parquet to enable substitution search.</div>';
222236
}else{
223237
$('neighbors').innerHTML = neighbors.map(x=>`
224-
<div class="neighbor">
238+
<div class="neighbor" data-smi="${x.smiles}" title="Click to analyze this molecule">
225239
<div class="nb-struct">${x.svg||''}</div>
226240
<div class="nb-info">
227241
<div class="nb-name">${x.name||'—'}</div>
@@ -270,6 +284,11 @@ <h2>Aroma</h2>
270284

271285
go.addEventListener('click', run);
272286
q.addEventListener('keydown', e=>{ if(e.key==='Enter') run(); });
287+
// click a substitution candidate -> analyze that molecule
288+
$('neighbors').addEventListener('click', e=>{
289+
const nb = e.target.closest('.neighbor');
290+
if(nb && nb.dataset.smi){ q.value = nb.dataset.smi; window.scrollTo({top:0,behavior:'smooth'}); run(); }
291+
});
273292
</script>
274293
</body>
275294
</html>

0 commit comments

Comments
 (0)