Skip to content

Commit 1eacf5a

Browse files
data(aroma): seven new heads from unique chemistry, and a count sweep (#262) (#274)
Chasing `blackberry` had produced nothing across three rebuilds, so this asks the better question instead: which descriptors DESERVE a head that do not have one? Ten sat below the ten-positive floor. Seven had genuinely distinctive character chemistry and were given it. aroma heads 166 -> 172 total heads 189 -> 195 angelica AUROC 0.956 precision 0.67 CONFIDENT — macrocyclic lactones quince AUROC 0.955 precision 0.02 indicative — marmelo lactone / oxathiane galbanum AUROC 0.922 precision 0.14 indicative — galbanum pyrazine, undecatriene wormwood AUROC 0.862 precision 0.23 indicative — thujones, artemisia ketone lychee AUROC 0.831 precision 0.09 indicative — rose oxide immortelle AUROC 0.781 precision 0.14 indicative — italidiones Deliberately NOT attempted: cucumber, watermelon and boronia. Their chemistry is the same C9 nonadienal/nonadienol set `melon` already owns and the ionones `violet` owns — entangled classes, which is the trap that cost blackberry 0.10 AUROC. Adding them would likely damage heads that work. `quince` is worth staring at: AUROC 0.955 with 0.02 precision. Near-perfect ranking, right one time in fifty when it fires. It is now the sharpest single example in ACCURACY.md of why AUROC alone is not a quality measure, and it replaces a caveat there that had gone stale. THE COUNT SWEEP. While verifying the README I found we had been publishing 190 heads when the app served 189 — the figure predated blackberry dropping and had propagated into the README badge, the CHANGELOG, ACCURACY.md, the v0.2.0 release notes and code comments. That is now 195 everywhere and checked against the manifests rather than carried forward. The ACCURACY.md tables were stale too: the precision distribution read 25/9/60/58 against a live 19/15/74/58, and the named "never wrong" heads were the wrong sixteen. Both regenerated. ON THE ROSTER NUMBER, now documented in ACCURACY.md. A cluster of heads sits at AUROC 0.69-0.71 on 11-15 positives, where 5-fold CV cannot resolve better than about +/-0.02. `blackberry` measured 0.696, 0.707, 0.687 and 0.720 across four consecutive rebuilds WITH ITS POSITIVE COUNT UNCHANGED AT 13 — the folds moved, not the head. A head crossing the bar only means something if n_pos moved with it, which is exactly what separates that from `chamomile`: 11 -> 15 positives, 0.691 -> 0.741, and it now makes confident calls at 1.00 precision. MIN_AUROC stays at 0.70. Lowering it to 0.69 would hand back the marginal heads instantly and make the roster whatever we wanted it to be. Rebuilt: master_enrichment 8,869 (all named), profile_index 8,850 x 183, flavor_map. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent ce7a480 commit 1eacf5a

9 files changed

Lines changed: 128 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The honesty release. Every trained head now publishes how good it actually is, a
1515
exists to install it somewhere other than the machine it was built on.
1616

1717
### Added
18-
- **Per-head calibrated thresholds with a 50% precision floor.** Each of the 190 heads carries a
18+
- **Per-head calibrated thresholds with a 50% precision floor.** Each of the 195 heads carries a
1919
decision threshold fitted on out-of-fold predictions, plus its measured precision and recall,
2020
published in `/api/heads` and shown on every bar. Heads that cannot be right more than half the
2121
time are marked `indicative` rather than confident — kept in full, never dressed up.

README.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Flavormancer
22

3+
<p align="center">
4+
<a href="https://github.com/echelonts/flavormancer/releases/latest"><img src="https://img.shields.io/github/v/release/echelonts/flavormancer?style=flat-square&color=8A6BE0&label=release" alt="latest release"></a>
5+
<img src="https://img.shields.io/badge/heads-195%20trained-2BC4C4?style=flat-square" alt="195 trained heads">
6+
<img src="https://img.shields.io/badge/runs-100%25%20on--prem-0A0A0A?style=flat-square" alt="runs entirely on-prem">
7+
<a href="docs/ACCURACY.md"><img src="https://img.shields.io/badge/accuracy-published%20per%20head-E0913C?style=flat-square" alt="per-head accuracy published"></a>
8+
</p>
9+
310
<p align="center">
411
<img src="docs/assets/hero.png" alt="Flavormancer — taste &amp; aroma prediction from chemical structure" width="860">
512
</p>
@@ -9,6 +16,15 @@ behavior, formulation notes, and safety flags** for any molecule, plus substitut
916
Enter a **common or IUPAC name** (or a SMILES) and get a single, honest flavor read,
1017
running entirely on hardware you own.
1118

19+
**Every head tells you how good it is.** Each of the 195 trained heads publishes the decision
20+
threshold it fires at and its measured out-of-fold precision — because AUROC alone will flatter a
21+
model badly on rare classes. The `ginger` head scores **AUROC 0.979** and is right **1 time in 10**
22+
when it fires; both are true, and only one of them was ever being reported. Heads that cannot be
23+
right more than half the time ship marked **indicative** rather than confident: they keep their
24+
score, their chips and every molecule they find, but they are never dressed up as answers.
25+
[**How accurate is it, really?**](docs/ACCURACY.md) explains all of it without assuming an ML
26+
background.
27+
1228
<p align="center">
1329
<img src="docs/assets/molecule-read.png" alt="A full, confidence-tagged molecule flavor read" width="640">
1430
</p>
@@ -17,9 +33,9 @@ running entirely on hardware you own.
1733
<p align="center">
1834
<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">
1935
</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>
36+
<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 172 aroma + 6 taste classes labelled. <b>195 trained heads</b> in all: 6 taste + 172 aroma + 5 mouthfeel + 12 safety.</sub></p>
2137

22-
> **8,847 unique molecules** across the open datasets · **taste + aroma + mouthfeel** prediction from
38+
> **8,869 unique molecules** across the open datasets · **taste + aroma + mouthfeel** prediction from
2339
> structure · a **flavor library** (start from a flavor → its character-impact molecule) and
2440
> **flavor designer** (pick your notes → best food-safe molecules + drop-in swaps) · an
2541
> interactive 2D/3D **flavor-space map** · **2D & 3D** structure views. All on
@@ -28,10 +44,10 @@ running entirely on hardware you own.
2844
> Per set (unique molecules): taste training **3,845** · aroma training **2,394** · odor
2945
> corpus **2,255** · documented taste **676** · mouthfeel training **2,534** · Tox21 (safety)
3046
> **7,823** · GRAS reference **2,781** · sweetness intensity **316** · character-impact aroma
31-
> supplement **602 associations** (open-gov-sourced). Every one of the 8,847 is enriched with
47+
> supplement **602 associations** (open-gov-sourced). Every one of the 8,861 is enriched with
3248
> names + measured properties from public-domain PubChem.
3349
>
34-
> *How the universe grows:* the flavor-space map and enrichment table show **8,847 unique
50+
> *How the universe grows:* the flavor-space map and enrichment table show **8,861 unique
3551
> structures** (deduped by connectivity skeleton), expanded by ingesting the **full EU/GB
3652
> flavourings Union List (~2,200 authorised, Open Government Licence v3)** so the browse-able
3753
> universe is food-forward. Meaningfully-distinct stereoisomers (e.g. R- vs S-limonene) that carry
@@ -59,7 +75,7 @@ is tagged by how it was derived**, so nothing reads as more certain than its sou
5975
**tasteless** (RandomForests on fingerprint + physicochemical features), plus a
6076
sweetness-**intensity** regressor. Sour and salty *also* keep a transparent chemistry
6177
rule (acid group / alkali-salt) as a deterministic cross-check alongside the model.
62-
- **Aroma****167 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
78+
- **Aroma****172 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
6379
petroleum, earthy, medicinal, sulfurous, camphor, fruity, fishy, garlic, ethereal,
6480
ammoniacal, pungent, pine, rose, rancid, alcoholic, woody, green, grassy, putrid)
6581
trained on **public-domain** HSDB odor text + curated character-impact facts, surfaced
@@ -162,7 +178,7 @@ Flavormancer ships as two editions of one method:
162178
| Edition | Commercial | Academic / open-source *(coming soon)* |
163179
| License | Apache-2.0 | open-source, **research / NonCommercial** |
164180
| Data | commercial-clean open data only | adds research odor datasets with **NonCommercial** terms |
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) |
181+
| Aroma | **172 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) |
166182
| Use | free to use, sell, run on-prem | research, teaching, advancing the method |
167183

168184
The split is deliberate. The richest aroma data is licensed for research only, so
@@ -208,10 +224,30 @@ tests/ pytest suite for the prediction core
208224

209225
## Getting started
210226

211-
See [training/SETUP.md](training/SETUP.md) for the clean-machine setup. Datasets and
212-
trained models are **not** committed — the training scripts pull their sources and
227+
**With Docker** — the app plus a pgvector-backed Postgres, one command:
228+
229+
```bash
230+
cp .env.example .env # every value has a working default
231+
docker compose up -d
232+
curl localhost:8000/healthz
233+
```
234+
235+
Trained models are **not** in the image (they are ~1 GB and change on every retrain) — point
236+
`MODELS_DIR` at them and they mount read-only at run time. Set `FLAVORMANCER_HOME` if you run
237+
without Compose.
238+
239+
**From source**[`training/SETUP.md`](training/SETUP.md) covers the install;
240+
[`docs/DATA-PIPELINE.md`](docs/DATA-PIPELINE.md) is the clean-machine walkthrough with every
241+
build step in dependency order, timings, and an end-to-end check that verifies a *prediction*
242+
rather than just that the server started.
243+
244+
Datasets and trained models are **not** committed — the training scripts pull their sources and
213245
`.gitignore` keeps artifacts out of the repo.
214246

247+
> The Docker path has not yet been run end to end on a machine with Docker installed — the
248+
> Compose file parses and the path handling is tested, but treat the first `docker compose up`
249+
> as the test rather than a guarantee.
250+
215251
## Team
216252

217253
Built by **[Echelon Technology Solutions](https://echelonts.net)** — a small team where

docs/ACCURACY.md

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ Companion to [`HOW-IT-WORKS.md`](HOW-IT-WORKS.md) (how it's built) and
1111

1212
## The setup
1313

14-
Flavormancer is **190 separate yes/no experts**, called *heads*. One asks *does this smell like
14+
Flavormancer is **195 separate yes/no experts**, called *heads*. One asks *does this smell like
1515
vanilla?* Another asks *does this taste bitter?* A third asks *does this feel cooling in the
1616
mouth?* Each looks at a molecule's structure and returns a number between 0 and 1 — how strongly
1717
it believes the answer is yes.
1818

1919
| modality | heads | what they answer |
2020
|---|---|---|
2121
| Taste | 6 | the five basics — sweet, bitter, umami, sour, salty — plus `tasteless` |
22-
| Aroma | 167 | vanilla, citrus, smoky, pine, jasmine… |
22+
| Aroma | 172 | vanilla, citrus, smoky, pine, jasmine… |
2323
| Mouthfeel | 5 | cooling, warming, pungent, tingling, astringent |
2424
| Safety | 12 | Tox21 assay screens — caution flags, never a clearance |
2525

@@ -124,7 +124,7 @@ because of it, and the reasoning is recorded in `train_aroma._calibrate` so nobo
124124

125125
## "Indicative" heads — marked, never removed
126126

127-
**73 of 167 aroma heads cannot reach 50% precision at any threshold.** They are right less than
127+
**65 of 172 aroma heads cannot reach 50% precision at any threshold.** They are right less than
128128
half the time when they fire.
129129

130130
They are **not deleted, disabled, or hidden.** They keep their score, their column in the
@@ -168,18 +168,46 @@ goal is to build every one of them up to confident, not to retire them.
168168
| when the head fires, it is right… | how many heads |
169169
|---|---|
170170
| 90–100% of the time | **25** |
171-
| 75–90% | 9 |
172-
| 50–75% | 60 |
173-
| under 50% *(shipped as `indicative`)* | 72 |
171+
| 75–90% | 11 |
172+
| 50–75% | 71 |
173+
| under 50% *(shipped as `indicative`)* | 65 |
174174

175-
**23 heads have never been wrong on unseen molecules** — clove, jasmine, minty, buttery, cooling,
176-
maple, grape, camphor, hay, cognac, geranium, aldehydic, chamomile, champaca and more.
175+
**23 heads have never been wrong on unseen molecules** — aldehydic, balsamic, buttery, caramel,
176+
chamomile, champaca, cinnamon, cooling, grape, jasmine, lavender, malty, maple, melon, minty,
177+
mushroom, peach, pine, plum, tarry, tropical, wintergreen, ylang. When one of those fires, every out-of-fold call it made was correct.
178+
179+
Read that alongside the discovery test below, though, because precision alone is not enough
180+
either. `quince` scores **AUROC 0.955** and **0.02 precision** — near-perfect ranking, right one
181+
time in fifty when it actually fires. And `turmeric` is the one head still memorising: it makes
182+
only correct calls, on molecules it was trained on, and finds nothing outside them. Three numbers,
183+
three different failure modes, which is why all three are published.
177184

178185
**Safety — 12 Tox21 screens, caution-only.** These flag a molecule for review. They are never a
179186
clearance, and food-use status comes from open-government registers, not from a model.
180187

181188
---
182189

190+
## A note on the roster number
191+
192+
The head count is not perfectly stable, and it is worth understanding why before quoting it.
193+
194+
A cluster of heads sits at **AUROC 0.69–0.71**, right against the 0.70 shipping bar, on 11–15
195+
positives. At that sample size 5-fold cross-validation cannot resolve a head more finely than
196+
about ±0.02, so ordinary corpus churn — three molecules added anywhere — reshuffles the folds and
197+
tips them across the line. `blackberry` measured 0.696, then 0.707, then 0.687 across three
198+
consecutive rebuilds **with its positive count unchanged at 13 the entire time**. Nothing about
199+
the head changed; the folds did.
200+
201+
The distinction that matters is whether an intervention actually lands. In the same rebuilds,
202+
`chamomile` went from 0.691 to **0.741** because its positives genuinely rose 11 → 15 (Roman
203+
chamomile angelate esters, Matricaria bisabolol oxides) — and it now makes confident calls at
204+
**1.00** precision. Adding molecules that are *already* positives, as happened with blackberry,
205+
changes nothing but the noise.
206+
207+
So: a head crossing the bar is only meaningful if `n_pos` moved with it. And the bar stays at
208+
0.70 — lowering it to 0.69 to recover a head would manufacture a number, which is the exact
209+
self-flattery the rest of this document exists to remove.
210+
183211
## Does a head *learn*, or just *memorise*?
184212

185213
A separate honesty check, because none of the above catches it. A head trained on twelve molecules
@@ -196,23 +224,23 @@ python training/audit_generalization.py # aroma heads
196224
python training/audit_generalization.py --mouthfeel
197225
```
198226

199-
Current state of the 167 aroma heads:
227+
Current state of the 172 aroma heads:
200228

201229
| verdict | count | meaning |
202230
|---|---|---|
203-
| generalises | **146** | finds molecules nobody labelled (median 9) |
204-
| precision-limited | 19 | strict on purpose, to hold the precision floor |
205-
| memorising | **2** | `turmeric`, `celery` — fire only on their own training set |
231+
| generalises | **154** | finds molecules nobody labelled (median 9) |
232+
| precision-limited | 17 | strict on purpose, to hold the precision floor |
233+
| memorising | **1** | `turmeric` — fires only on its own training set |
206234

207-
`turmeric` and `celery` are narrow single-scaffold classes. More molecules will not fix them; that
208-
is a model-architecture problem, tracked with the GNN work in
235+
`turmeric` is a narrow single-scaffold class (bisabolane sesquiterpenes). More molecules will not
236+
fix it; that is a model-architecture problem, tracked with the GNN work in
209237
[#199](https://github.com/echelonts/flavormancer/issues/199).
210238

211239
---
212240

213241
## The short version
214242

215-
> It's 190 separate models. Every one is graded on molecules it has never seen, and every one
243+
> It's 195 separate models. Every one is graded on molecules it has never seen, and every one
216244
> publishes both how confident it is *and* how often it is actually right at that confidence.
217245
> About a hundred are trustworthy enough to act on — two dozen have never been wrong. The rest are
218246
> marked as hints rather than answers, because they're right less than half the time, and saying so

docs/AROMA-AUDIT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: **167 aroma heads / 190 total
63+
> `sweet`-odor rides the bar (0.72, documented-only until a GNN). Net: **172 aroma heads / 195 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/DATA-PIPELINE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ local compute.
4545
**Verify it worked** (this is the honest end-to-end check, not just "the server started"):
4646

4747
```bash
48-
curl -s localhost:8000/api/status # {"ready":true,"total":190,...}
48+
curl -s localhost:8000/api/status # {"ready":true,"total":190,...} (190 artifacts = 189 heads + the intensity regressor)
4949
curl -s localhost:8000/api/heads | jq '.aroma | length' # 166
5050
# vanillin — the vanilla head should fire at 1.0, confident, with its calibrated threshold
5151
curl -s -X POST localhost:8000/api/predict \
@@ -58,7 +58,7 @@ curl -s -X POST localhost:8000/api/predict \
5858
`.aroma.top` is the *confident* subset ordered by score, so `top[0]` is whichever head scored
5959
highest — not necessarily the one you are looking for. Query `descriptors[]` by name, as above.
6060

61-
If `/api/status` reports fewer than 190 heads, a training step was skipped — the app degrades
61+
If `/api/status` reports fewer than 195 heads, a training step was skipped — the app degrades
6262
gracefully rather than failing, so it will start regardless.
6363

6464
---
@@ -95,7 +95,7 @@ override that, which is why it is recorded even though it is unused.
9595
|---|---|---|
9696
| `odor_notes.parquet` | 2,258 | `inchikey`, `smiles`, `name`, `odor`, `odor_source`, `odor_threshold_ppm`, `odor_threshold_note` |
9797
| `aroma_supplement.csv` | 2,159 | `flavor`, `molecule`, `smiles`, `category` |
98-
| `aroma_train.parquet` | 2,417 | `inchikey`, `smiles`, + one 0/1 column per descriptor |
98+
| `aroma_train.parquet` | 2,431 | `inchikey`, `smiles`, + one 0/1 column per descriptor |
9999

100100
`odor` is **free text** as recorded ("sweet, floral, slightly minty"), normalised into the
101101
controlled vocabulary by `build_aroma_dataset.tag()`. `odor_threshold_ppm` is populated only where
@@ -134,7 +134,7 @@ a safety clearance, and the UI must never render it as one. See
134134

135135
### Master table
136136

137-
`master_enrichment.parquet` — 8,861 rows, the single table the app reads for everything that is
137+
`master_enrichment.parquet` — 8,869 rows, the single table the app reads for everything that is
138138
not a live prediction: `inchikey_skel`, `smiles`, `name`, `mw`, `logp`, `tpsa`, `hbd`, `hba`,
139139
`rot_bonds`, `rings`, `melting_point_c`, `boiling_point_c`, `gras`, `taste_documented`, plus a
140140
`tox_*` column per Tox21 assay.

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 190-head load under load
60+
deadline = time.monotonic() + 180 # poll for up to 3 min — covers a cold 195-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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 190 heads. So: the 6
586+
# The card is a shareable SNAPSHOT — a PNG can't scroll, and there are 195 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.

0 commit comments

Comments
 (0)