Skip to content

Commit 24d938a

Browse files
docs: sync every head count to the 190-head roster
The retrain in #258 took the aroma roster from 164 to 167, which moved three numbers that were quoted in nineteen places across the README, five docs, the workbench UI, the MCP server, the e2e conftest and the pipeline comments in predict.py and app.py. Several were already stale before this — the profile vector had been described as 170-dimensional since before mouthfeel shipped, and one workbench card claimed 167 heads in its title while still saying 164 survive in its body. The three canonical numbers, so future drift is easy to spot: 190 trained heads in all — 6 taste + 167 aroma + 5 mouthfeel + 12 safety 178 dimensions in the flavor-profile vector — taste + aroma + mouthfeel, tox deliberately excluded because safety is not a flavor-match dimension 167 aroma descriptor heads clearing CV-AUROC >= 0.70 (The loader reports 191 artifacts: the 190 heads plus the sweetness-intensity regressor, which is a regressor rather than a head and so is not counted as one.) Also corrects the aroma training corpus size in AROMA.md, which still read 1994 molecules against an actual 2403 after the supplement work. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent 6f5f7ea commit 24d938a

11 files changed

Lines changed: 35 additions & 35 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ running entirely on hardware you own.
1717
<p align="center">
1818
<img src="docs/assets/flavor-map.png" alt="Flavor-space map in 3D on MW × logP × TPSA axes, colored by taste and aroma" width="900">
1919
</p>
20-
<p align="center"><sub>The interactive flavor-space map in 3D on real <b>MW × logP × TPSA</b> axes, colored by <b>taste &amp; aroma</b> — every one of the 164 aroma + 6 taste classes labelled. <b>187 trained heads</b> in all: 6 taste + 164 aroma + 5 mouthfeel + 12 safety.</sub></p>
20+
<p align="center"><sub>The interactive flavor-space map in 3D on real <b>MW × logP × TPSA</b> axes, colored by <b>taste &amp; aroma</b> — every one of the 167 aroma + 6 taste classes labelled. <b>190 trained heads</b> in all: 6 taste + 167 aroma + 5 mouthfeel + 12 safety.</sub></p>
2121

2222
> **8,847 unique molecules** across the open datasets · **taste + aroma + mouthfeel** prediction from
2323
> structure · a **flavor library** (start from a flavor → its character-impact molecule) and
@@ -59,7 +59,7 @@ is tagged by how it was derived**, so nothing reads as more certain than its sou
5959
**tasteless** (RandomForests on fingerprint + physicochemical features), plus a
6060
sweetness-**intensity** regressor. Sour and salty *also* keep a transparent chemistry
6161
rule (acid group / alkali-salt) as a deterministic cross-check alongside the model.
62-
- **Aroma****164 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
62+
- **Aroma****167 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
6363
petroleum, earthy, medicinal, sulfurous, camphor, fruity, fishy, garlic, ethereal,
6464
ammoniacal, pungent, pine, rose, rancid, alcoholic, woody, green, grassy, putrid)
6565
trained on **public-domain** HSDB odor text + curated character-impact facts, surfaced
@@ -162,7 +162,7 @@ Flavormancer ships as two editions of one method:
162162
| Edition | Commercial | Academic / open-source *(coming soon)* |
163163
| License | Apache-2.0 | open-source, **research / NonCommercial** |
164164
| Data | commercial-clean open data only | adds research odor datasets with **NonCommercial** terms |
165-
| Aroma | **164 presence/absence descriptor heads ship** (public-domain HSDB); scored **intensity** is trained on your data or a licensed set (PMP 2001) | full open model incl. **intensity** (research odor data) |
165+
| Aroma | **167 presence/absence descriptor heads ship** (public-domain HSDB); scored **intensity** is trained on your data or a licensed set (PMP 2001) | full open model incl. **intensity** (research odor data) |
166166
| Use | free to use, sell, run on-prem | research, teaching, advancing the method |
167167

168168
The split is deliberate. The richest aroma data is licensed for research only, so

docs/AROMA-AUDIT.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **Update — supplement applied, then made food-only.** `build_aroma_supplement.py` adds a curated
44
> **public-domain** character-impact set (`aroma_supplement.csv`, resolved via PubChem) for the sparse
5-
> descriptors below. Result: **42 → 164 heads** (food-safe core + non-food aroma-only tier; see the
5+
> descriptors below. Result: **42 → 167 heads** (food-safe core + non-food aroma-only tier; see the
66
> multi-industry note below), after open-government sourcing (see below) took the
77
> curated supplement to **~1,994 labelled molecules**. New heads include `coconut`, `nutty`, `caramel`,
88
> `winey`, `onion`, `honey`, `herbal`, `vanilla`, `buttery`, `balsamic`, `smoky`, `cinnamon`, `spicy`,
@@ -28,10 +28,10 @@
2828
> 2-phenylethanol) instead of the E/Z terpene twins (nerol/geraniol) that dedupe to one row under the
2929
> connectivity-skeleton key. That was the **food-safe** ceiling at ~86.
3030
>
31-
> **Then we walked the road past it — the non-food aroma-only descriptor space — to 164 heads.** The
31+
> **Then we walked the road past it — the non-food aroma-only descriptor space — to 167 heads.** The
3232
> corpus is now deliberately **multi-industry**, not food-only: odor is physical, so to read
3333
> structure→smell well the corpus should include the fragrance space, and a per-molecule **safety flag**
34-
> (open-gov food registers) — not corpus membership — gates edibility per mode. The same 164 heads serve
34+
> (open-gov food registers) — not corpus membership — gates edibility per mode. The same 167 heads serve
3535
> every "Mancer" mode: **Flavormancer** (food chemistry), **Beveragemancer** (beverage), **Aromamancer**
3636
> (fragrance), **Vapemancer** (inhalation), **Oilmancer** (essential oils). Molecule names are resolved
3737
> to structures through PubChem and folded in flagged non-food. New heads cleared across several
@@ -60,7 +60,7 @@
6060
> its white-floral neighbours (`jasmine`/`gardenia`/`ylang`/`champaca`), nearly doubling their example
6161
> counts and de-inflating their small-n scores.
6262
>
63-
> `sweet`-odor rides the bar (0.72, documented-only until a GNN). Net: **164 aroma heads / 170 total
63+
> `sweet`-odor rides the bar (0.72, documented-only until a GNN). Net: **167 aroma heads / 190 total
6464
> (with the 6 taste heads).** The count is inherently ±2–3 at the margin — ~15 heads sit right at the
6565
> 10-positive / 0.70-AUROC boundary, so any data change reshuffles which marginal heads ship.
6666
>

docs/AROMA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enumerable via the annotations API. Pipeline:
1818
physicochemical descriptor block** (`chemfeatures.py`: MW, logP, TPSA, H-bond donors/
1919
acceptors, rotatable bonds, ring counts, fraction sp3, heteroatoms — the global properties
2020
bare bits miss). After broadening the vocabulary, folding the curated `flavors.csv` molecules
21-
in (825 → **1994** molecules, incl. a curated public-domain supplement), and adding those features, **164 heads clear CV-AUROC ≥ 0.70**
21+
in (825 → **2403** molecules, incl. a curated public-domain supplement), and adding those features, **167 heads clear CV-AUROC ≥ 0.70**
2222
(each score is that descriptor's own held-out AUROC): medicinal 0.97, ammoniacal 0.96,
2323
petroleum 0.96, citrus 0.94, camphor 0.93, almond 0.92, fatty 0.89, minty 0.88, ethereal 0.87,
2424
fruity 0.87, fishy 0.86, sulfurous 0.85, garlic 0.84, floral 0.83, pungent 0.81, earthy 0.73.

docs/CAPABILITIES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ shape of the tool. The organizing discipline throughout: **every output is tagge
66
by how it's derived, and nothing claims more certainty than its source supports.**
77

88
> **Edition note.** This catalogues the **commercial** edition (Apache-2.0,
9-
> commercial-clean data). **Aroma ships here** as 164 presence/absence odor-descriptor
9+
> commercial-clean data). **Aroma ships here** as 167 presence/absence odor-descriptor
1010
> heads trained on public-domain HSDB text (plus 5 mouthfeel/chemesthesis heads and 12
1111
> Tox21 safety heads) — what's still gated is scored **intensity** (*how strong* a note
1212
> is), which needs research/customer panel data and lives in the **academic edition**
@@ -33,7 +33,7 @@ by how it's derived, and nothing claims more certainty than its source supports.
3333
- Multitaste — fires when 2+ taste heads are high (**trained**-derived).
3434
- Known-taste ground truth — verified labels override predictions (**lookup**).
3535

36-
**Aroma****164 odor-descriptor heads ship** (**trained**)
36+
**Aroma****167 odor-descriptor heads ship** (**trained**)
3737
- Presence/absence per descriptor (citrus, floral, minty, almond, fatty, petroleum, earthy,
3838
medicinal, sulfurous, camphor, fruity, fishy, garlic, ethereal, ammoniacal, pungent) —
3939
RandomForests on fingerprint + physicochemical features, over public-domain HSDB odor text +

docs/HOW-IT-WORKS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ features (trained on SweetenersDB).
6262

6363
---
6464

65-
## 3. Aroma — 164 descriptor heads from public odor text
65+
## 3. Aroma — 167 descriptor heads from public odor text
6666

6767
Odor is the hard, licensed part of this field — most rich odor datasets are NonCommercial. Our
6868
clean route:
@@ -75,7 +75,7 @@ clean route:
7575
`minty`), producing a presence/absence label per descriptor. We also fold in the curated
7676
character-impact facts from `flavors.csv`.
7777
3. **`train_aroma.py`** trains one RandomForest per descriptor (on fingerprint + descriptors) and
78-
**keeps only the heads that clear CV-AUROC ≥ 0.70** — an honest bar. **164 heads** survive
78+
**keeps only the heads that clear CV-AUROC ≥ 0.70** — an honest bar. **167 heads** survive
7979
(citrus, floral, minty, almond, fatty, petroleum, earthy, medicinal, sulfurous, camphor,
8080
fruity, fishy, garlic, ethereal, ammoniacal, pungent, pine, rose, rancid, alcoholic,
8181
woody, green, grassy, putrid), 0.71–0.98. The lower-population heads (≈10–20 documented

mcp-server/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def _flavor_map(label: str, limit: int, full: bool) -> dict:
256256
def read_flavor(molecule: str) -> dict:
257257
"""Predict the flavor of a single molecule (name or SMILES).
258258
259-
Returns the six taste-head probabilities, confident aromas plus all 164 aroma scores, the
259+
Returns the six taste-head probabilities, confident aromas plus all 167 aroma scores, the
260260
trained mouthfeel/chemesthesis scores (cooling/pungent/warming/astringent/tingling), GRAS
261261
status, structural alerts, caution-only Tox21 flags, and the applicability-domain flag.
262262
Prediction only — tox flags are indicative in-vitro activity, never a determination.

tests/e2e/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def models_present(base_url):
5757
"""
5858
import json
5959
import time
60-
deadline = time.monotonic() + 180 # poll for up to 3 min — covers a cold 160-head load under load
60+
deadline = time.monotonic() + 180 # poll for up to 3 min — covers a cold 190-head load under load
6161
while time.monotonic() < deadline:
6262
with contextlib.suppress(Exception): # server busy/starting -> wait and retry
6363
req = urllib.request.Request(base_url + "/api/predict", method="POST",

training/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def api_substitutes(q: Query):
445445
@app.post("/api/precomputed")
446446
def api_precomputed(q: Query):
447447
"""Fast check: is this molecule's profile already in the index (instant read) or does it need a
448-
fresh 170-head compute? Lets the UI show a 'conjuring a fresh reading' note for novel molecules."""
448+
fresh 178-head compute? Lets the UI show a 'conjuring a fresh reading' note for novel molecules."""
449449
smi = _resolve(q.smiles)
450450
return {"precomputed": bool(smi and P.is_precomputed(smi))}
451451

@@ -583,7 +583,7 @@ def font(path, size):
583583
"tasteless": out.get("tasteless")}
584584
taste_cells = sorted(((t, float(v) if isinstance(v, (int, float)) else 0.0)
585585
for t, v in taste_src.items()), key=lambda kv: -kv[1])
586-
# The card is a shareable SNAPSHOT — a PNG can't scroll, and there are 187 heads. So: the 6
586+
# The card is a shareable SNAPSHOT — a PNG can't scroll, and there are 190 heads. So: the 6
587587
# tastes ALWAYS render (a complete, fixed row you can compare across cards), while aroma,
588588
# mouthfeel and safety show only what actually FIRES, capped. The labels say "N of M" so a
589589
# reader knows they're seeing the firing subset, not the whole model.
@@ -1315,15 +1315,15 @@ def _prewarm_formulation():
13151315
with contextlib.suppress(Exception): # best-effort
13161316
P.substitute("CCO")
13171317
# Warm the PROFILE path too: builds the normalized reference matrix (_profiles_unit, the
1318-
# 8k×170 renormalization) once here instead of on the first /api/substitutes.
1318+
# 8k×178 renormalization) once here instead of on the first /api/substitutes.
13191319
P.substitutes("CCO")
13201320
for n in _FORMULATION_WARM:
13211321
with contextlib.suppress(Exception): # best-effort warmup; a miss just means a cold first hit
13221322
smi = _resolve(n)
13231323
m = Chem.MolFromSmiles(smi) if smi else None
13241324
if m is not None:
13251325
canon = Chem.MolToSmiles(m)
1326-
P.predict_aroma(canon) # fills the shared 164-head aroma cache
1326+
P.predict_aroma(canon) # fills the shared 167-head aroma cache
13271327
P.substitutes(canon) # taste heads + profile cosine, so the demo chips are instant
13281328

13291329

training/predict.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def _load_rf(path):
223223

224224
def _infer_pool():
225225
"""A process-wide thread pool for parallel head inference on novel molecules. Sized to ~3/4 of
226-
the box (env FLAVORMANCER_INFER_WORKERS overrides) so a fresh 170-head read rips across cores
226+
the box (env FLAVORMANCER_INFER_WORKERS overrides) so a fresh 190-head read rips across cores
227227
(~40 s -> a couple of seconds). Shared, so many concurrent novel reads share one bounded pool
228228
instead of each spawning its own — in-corpus reads never touch it (they hit the index)."""
229229
global _INFER_POOL
@@ -1066,12 +1066,12 @@ def analyze_balance(ingredients):
10661066

10671067
@lru_cache(maxsize=8192)
10681068
def _aroma_scores_canon(canon):
1069-
"""Run all 164 descriptor forests for a CANONICAL SMILES and return {head: score}."""
1069+
"""Run all 167 descriptor forests for a CANONICAL SMILES and return {head: score}."""
10701070
m = Chem.MolFromSmiles(canon)
10711071
if m is None or not _AROMA_MODELS:
10721072
return None
10731073
fp = _feat(m)
1074-
# Fan the 164 forests across cores — each predict_proba releases the GIL, so this turns the
1074+
# Fan the 167 forests across cores — each predict_proba releases the GIL, so this turns the
10751075
# ~40 s serial read (the only remaining cost, for genuinely novel/out-of-corpus molecules) into
10761076
# a couple of seconds. In-corpus molecules never reach here (they read the precomputed index row).
10771077
def _score(it):
@@ -1082,13 +1082,13 @@ def _score(it):
10821082

10831083

10841084
def _aroma_scores(smiles):
1085-
"""The expensive part of the aroma read: all 164 descriptor forests → {head: score}. Keyed on
1085+
"""The expensive part of the aroma read: all 167 descriptor forests → {head: score}. Keyed on
10861086
the CANONICAL SMILES (not threshold/top_k, not the raw string) so every caller shares one
10871087
computation per molecule regardless of how they spelled it — predict_aroma, _query_profile
10881088
(substitutes) and the /api/aroma endpoint all collapse to the same cache entry instead of each
1089-
re-running 164 forests (that double/mismatched inference was the ~6 s /api/substitutes).
1089+
re-running 167 forests (that double/mismatched inference was the ~6 s /api/substitutes).
10901090
1091-
In-corpus molecules skip the forests entirely: their 164 scores are read straight off the
1091+
In-corpus molecules skip the forests entirely: their 167 scores are read straight off the
10921092
precomputed profile index (built at startup) — the same numbers, ~40 s cheaper on a cold hit."""
10931093
m = Chem.MolFromSmiles(smiles)
10941094
if m is None or not _AROMA_MODELS:
@@ -1265,7 +1265,7 @@ def _aroma(a):
12651265
def _build_sub_index():
12661266
global _SUB_INDEX
12671267
import numpy as np
1268-
# Fast path: load the precomputed profile index (build_profile_index.py). The 170-head
1268+
# Fast path: load the precomputed profile index (build_profile_index.py). The 178-dim
12691269
# inference over ~8.8k molecules is slow (~3 min); the cache makes startup instant. We only
12701270
# rebuild the cheap Morgan fingerprints from SMILES on load.
12711271
cache = Path("profile_index.npz")
@@ -1340,7 +1340,7 @@ def _ensure_sub_index():
13401340
def _index_row(mol):
13411341
"""Row of `mol` in the profile index (matched by connectivity skeleton), or None if the
13421342
molecule isn't in the reference corpus. In-corpus molecules can reuse their PRECOMPUTED
1343-
170-head profile (built once at index build / startup) instead of re-running 164 forests at
1343+
178-dim profile (built once at index build / startup) instead of re-running 167 forests at
13441344
query time — that inference is ~40 s cold on a novel molecule and was the real /api/substitutes
13451345
and include_aroma cost. The precomputed row is the SAME model output, just paid up front."""
13461346
_ensure_sub_index()
@@ -1362,7 +1362,7 @@ def _index_row(mol):
13621362

13631363
def is_precomputed(smiles):
13641364
"""True if this molecule's full taste+aroma profile is already in the index (an instant read),
1365-
False if it's out-of-corpus and the 170 heads have to run fresh (the slower path). Used by the
1365+
False if it's out-of-corpus and the 178 profile heads have to run fresh (the slower path). Used by the
13661366
UI to decide whether to show the 'conjuring a fresh reading' note while a read brews."""
13671367
mol = Chem.MolFromSmiles(smiles or "")
13681368
return mol is not None and _index_row(mol) is not None

training/train_aroma.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
descriptor that has enough positives, reports HONEST 5-fold CV AUROC, and keeps only the
77
descriptors that clear a minimum AUROC. Saves the kept heads to aroma_models/ + a manifest.
88
9-
PARALLELISM: each head trains in its OWN process with n_jobs=1, so the ~164 heads train
9+
PARALLELISM: each head trains in its OWN process with n_jobs=1, so the ~167 heads train
1010
concurrently across the box's cores instead of one-at-a-time (the same process-per-unit pattern
1111
that made build_profile_index fast). Feature matrix is memmapped so workers share it. A head
1212
either clears the bar and dumps its model, or is dropped — workers write disjoint files, no locks.

0 commit comments

Comments
 (0)