Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions training/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,17 @@ def _ensure_sub_index():
_build_sub_index()


def _predicted_tastes_at(profiles, i):
"""The PREDICTED taste heads (score >= 0.5) for reference-set row i, from the profile matrix.
Lets neighbor / substitute cards show a taste read even when nothing is *documented* — the
taste columns are the first len(_CLASSIFIERS) of the profile vector. Marked predicted in the UI."""
if profiles is None:
return []
taste_heads = sorted(_CLASSIFIERS)
row = profiles[i]
return [t for j, t in enumerate(taste_heads) if float(row[j]) >= 0.5]


def structural_neighbors(smiles: str, k: int = 8, min_similarity: float = 0.0) -> dict:
"""STRUCTURAL neighbors: the k labeled molecules most structurally similar to the query
(Tanimoto over Morgan fingerprints), each with its known tastes. Structural look-alikes —
Expand All @@ -1158,7 +1169,7 @@ def structural_neighbors(smiles: str, k: int = 8, min_similarity: float = 0.0) -
if mol is None:
return {"error": f"unparseable SMILES: {smiles}"}
_ensure_sub_index()
fps, smis, tastes, _aromas, _profiles, _dims = _SUB_INDEX
fps, smis, tastes, _aromas, profiles, _dims = _SUB_INDEX
if not fps:
return {"neighbors": [], "note": "no reference set loaded (taste_master.parquet absent)"}
q = _MORGAN.GetFingerprint(mol)
Expand All @@ -1179,6 +1190,7 @@ def structural_neighbors(smiles: str, k: int = 8, min_similarity: float = 0.0) -
continue
neighbors.append({"smiles": smis[i], "similarity": round(float(sims[i]), 3),
"known_tastes": tastes[i],
"predicted_tastes": _predicted_tastes_at(profiles, i),
# confident aromas precomputed once in the index — reused so the
# endpoint never re-runs the 24 aroma heads per neighbor (8x ~1.3s saved)
"aromas": _aromas[i] if i < len(_aromas) else []})
Expand Down Expand Up @@ -1220,7 +1232,8 @@ def substitutes(smiles: str, k: int = 8) -> dict:
if ni is None or Chem.MolToInchiKey(ni).split("-")[0] == self_skel:
continue
subs.append({"smiles": smis[i], "profile_match": round(float(sims[i]), 3),
"known_tastes": tastes[i], "aromas": aromas[i] if i < len(aromas) else []})
"known_tastes": tastes[i], "predicted_tastes": _predicted_tastes_at(profiles, i),
"aromas": aromas[i] if i < len(aromas) else []})
if len(subs) >= k:
break
return {"query": Chem.MolToSmiles(mol), "substitutes": subs,
Expand Down
3 changes: 2 additions & 1 deletion training/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
.kt{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}
.rx-from{font-size:10px;color:var(--muted);line-height:1.3;margin-top:3px}
.kt .tchip{font-size:10px;font-weight:600;padding:1px 8px;border-radius:20px;color:#10140F;text-transform:capitalize}
.kt .tchip.pred{background:transparent!important;border:1px solid;font-weight:500}
.kt .muted{color:var(--muted);font-size:11px}
.browse{margin:22px 0 4px;border:1px solid #2C6E74;border-radius:12px;padding:15px 18px;background:linear-gradient(180deg,#15211F,#181D24)}
.browse-head{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.04em;color:var(--brand-2);margin:0 0 12px;display:flex;align-items:center;justify-content:space-between;gap:8px}
Expand Down Expand Up @@ -1416,7 +1417,7 @@ <h4>Software &amp; type</h4>
<div class="nb-name">${x.name||'—'}${x.gras?'<span class="nb-gras" title="Listed in a food-use reference (FDA Substances-Added-to-Food / EU flavourings) — a listing flag, not a GRAS or safety clearance">Food-listed</span>':''}</div>
${x.iupac?`<div class="nb-iupac">${x.iupac}</div>`:''}
<div class="s">${x.smiles}</div>
<div class="kt">${x.known_tastes.length?x.known_tastes.map(t=>`<span class="tchip" style="background:${TASTE_COLORS[t]||'var(--muted)'}">${t}</span>`).join(''):'<span class="muted">no known taste</span>'}</div>
<div class="kt">${(()=>{const doc=(x.known_tastes||[]).map(t=>`<span class="tchip" style="background:${TASTE_COLORS[t]||'var(--muted)'}">${t}</span>`);const pred=(x.predicted_tastes||[]).filter(t=>!(x.known_tastes||[]).includes(t)).map(t=>`<span class="tchip pred" style="color:${TASTE_COLORS[t]||'var(--muted)'};border-color:${TASTE_COLORS[t]||'var(--muted)'}" title="predicted taste (model)">${t}</span>`);return [...doc,...pred].join('')||'<span class="muted">no taste read</span>';})()}</div>
${(x.aroma&&x.aroma.length)?`<div class="nb-aroma" title="aroma (found = documented, else predicted •)">${x.aroma.map(a=>`<span class="atag" style="color:${aromaColor(a.odor)};border-color:${aromaColor(a.odor)}44">${a.odor}${a.source==='predicted'?' •':''}</span>`).join('')}</div>`:''}
</div>
<div class="sim">${Math.round((x[scoreKey]||0)*100)}% match</div>
Expand Down
Loading