feat(training): mouthfeel modality — 5 chemesthesis heads surfaced across the stack - #243
Merged
Merged
Conversation
…tes/neighbors The read modal was slow (and tripping proxy 504s) because /api/substitutes and include_aroma re-ran all 170 taste+aroma forests on the query molecule — ~40 s cold on a novel structure. Two root causes fixed: - Shared aroma cache: extract the 164-forest scoring into _aroma_scores, memoized on the CANONICAL SMILES (not threshold/top_k), so predict_aroma, _query_profile and /api/aroma collapse to one computation per molecule instead of each rerunning. - Precomputed index lookup: the profile index already holds the exact 170-head profile for every corpus molecule (built at startup). In-corpus molecules — all demo chips and most real queries — now read their row directly (same numbers, zero forests). methyl anthranilate /api/aroma 39.8 s -> 0.01 s; /api/substitutes 2.4 s -> 0.05 s; /api/predict 60 s -> 1.1 s. - Cache the L2-normalized reference matrix once (paid at prewarm, not per request) and warm the profile path for the demo molecules at startup. UX: substitutes and structural neighbors now return every match above a similarity floor (profile >= 0.45 / Tanimoto >= 0.30), capped at 50, rendered in a scrollable list with a match count — instead of a fixed 8. Propagated the higher-N behavior to the MCP tools and the CLI skill. Added a threshold + index-lookup test. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…red modal actions Startup / 502: model heads now load on a background thread, so uvicorn binds its port within a second and serves a self-refreshing "warming the cauldron" page (live loaded/total head count + ETA) instead of the old ~90 s startup 502. /healthz (liveness+readiness) and /api/status (load progress) back it; a middleware returns the page for HTML navs and a clean 503 for API calls until MODELS_READY. Loading stays SERIAL — joblib unpickling is GIL-bound, so a thread pool only added contention (measured ~34 s serial vs ~86 s across 14 threads). The startup prewarm/precompute threads now wait on MODELS_READY. Tests force a synchronous load via FLAVORMANCER_BLOCKING_LOAD=1. Novel-molecule reads: the only path that still runs all 164 forests (genuinely out-of-corpus molecules — in-corpus reads hit the precomputed index) now fans the heads across a shared, bounded thread pool sized to ~3/4 of the box (FLAVORMANCER_INFER_WORKERS), cutting a cold read ~40 s -> ~22 s (GIL-limited). A new /api/precomputed lets the UI tell instant reads from fresh ones. Modal UX: the Export/Copy-link buttons are grouped and centered at the top (red close X stays top-right), with more breathing room below the top row, and they stay hidden until the read finishes rendering. A rotating flavor-wizardry "cantrip" line runs under the title while any read brews (5 s cadence), and out-of-corpus molecules get a "conjuring a fresh reading" note between the flask and the cantrips so the longer wait reads as intentional. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…full-height swap sections Reworked the read modal per the layout review: - Order now reads structure -> flavor identity (name + "reads as" tags) -> heads. The molecule's own bars live in ONE combined "Heads" card grouped by category (Taste, Aroma; Mouthfeel + Safety groups join as the head-roster/tox-AUROC work lands), so aroma finally sits next to taste instead of a separate card far down the page. - Top row is a 2-col dashboard (Flavor read | Heads); the aroma bar list scrolls inside its group so the card stays compact. - Substitutes and Structural neighbors are two DISTINCT sections in a swap-grid, each filling its box (bounded height + internal scroll) so there's no wasted whitespace, and the difference between profile-match vs structural look-alike stays obvious. - Behavior & safety stays as its own card. Structural-neighbors subtext now says known tastes AND smells. Tighter, more dashboard-like spacing throughout; responsive to a single column under 820px. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…, modal left-column Index build now fans the profile heads across the box's cores as separate PROCESSES (each worker loads only its shard of head models and scores them against the memmapped feature matrix), instead of one serial pass. joblib unpickling and predict_proba each run without fighting a single GIL, so the whole-universe build drops ~3 min -> ~35 s (now floored by the parent's RDKit featurization). Writes atomically (temp + os.replace) so the live index is never half-written. Worker counts are derived from os.cpu_count(), never hardcoded, so this scales from a 4-core box up: the batch build uses min(#heads, cores) processes; the novel-read inference pool uses ~3/4 of the cores (leaving headroom for the web server). CPU-bound work scales with cores; I/O-bound name resolution keeps its fixed network-concurrency cap (core count is the wrong basis there). _profile_heads() now sorts BOTH taste and aroma so the builder derives the exact column order from filenames without loading a model; adds FLAVORMANCER_NO_MODELS=1 (the builder's parent only needs featurization). Verified identical results: vanillin -> ethyl vanillin 0.95, carvone -> dill/fennel 1.0. Modal: Behavior & safety moves under the molecule card in a left column, and the tall Heads card fills the full height of the right column alongside both (the aroma list flexes + scrolls to match), closing the gap under the short molecule card. Consistent 16px spacing between all cards. Neighbor/substitute lists reset to the top on render so the best matches show first. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
train_tox.py now writes tox_models/manifest.json with each Tox21 head's 5-fold CV-AUROC + support (all 12 land 0.72-0.90, above the 0.70 bar). predict.py loads it into _TOX_META and predict_tox now reports each assay's auroc alongside its probability, so the safety heads get a bar + AUROC like taste and aroma — they were trained but never showcased. Adds head_catalog() + GET /api/heads: every head grouped by category (taste / aroma / mouthfeel / safety) with AUROC. Categories are TAGS, not buckets — cooling and pungent appear under BOTH aroma and mouthfeel (like sweet is taste + aroma). Backs the modal Heads card's Mouthfeel/Safety groups and the library category pickers. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
Mouthfeel is a distinct modality (trigeminal, not olfactory), so it gets its own independent pipeline mirroring taste: build_mouthfeel_supplement.py (curated public-domain agents) -> build_mouthfeel_dataset.py (positives + broad random background) -> train_mouthfeel.py -> mouthfeel_models/. predict.py loads it, tags it 'mouthfeel', and folds it into the profile vector. - New heads: warming (capsaicinoids), astringent (tannins/polyphenols), tingling (sanshools / spilanthol / ZP-amides) — all clear >=10 verified public positives and the 0.70 CV-AUROC bar (0.99/1.00/1.00; high because the classes are structurally distinct — a narrow-class caveat). - cooling & pungent stay AROMA heads but are TAGGED mouthfeel (multi-category, like sweet). The Piper pungent amides moved to the aroma pungent head (no molecule wasted). - The flavor-profile vector is now taste + aroma + mouthfeel; _profile_heads() returns all three, the parallel index builder derives the same order from filenames, and head_catalog / /api/heads surface a mouthfeel group. tingling stayed strict (real sanshool/ZP-amide agents, not padded). Also: build_enrichment folds the curated supplement CSVs so every trained molecule (17 that were missing — sanshools, capsiate, ellagic acid, ...) lands in the universe; deterministic head order sweep (import-only lint fixes across the build scripts); tighter modal card spacing (14px, not 0). Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…und-loader race
train_aroma now trains each head in its OWN process at n_jobs=1, so the ~177 heads train
concurrently across all cores (same process-per-unit pattern as the index build) instead of one
at a time with nested joblib — cutting the retrain from ~30 min to a few. Feature matrix is
memmapped so workers share it; each worker dumps only its own model (disjoint files, no locks).
Fix: since model heads now load on a background thread, any build script that imports predict and
runs inference could race the loader mid-populate ("dictionary changed size during iteration").
build_enrichment and build_flavor_map now wait on P.MODELS_READY before iterating/inferring.
Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
Following the sweet-is-taste-AND-aroma precedent: the SENSATION cooling/pungent is distinct from
the ODOUR descriptor of the same name (WS-23 cools but has ~no smell; menthol does both). So cooling
& pungent now train as first-class mouthfeel_models heads on TRPM8/TRPV1 agents (menthol family +
food-authorised WS-agents; capsaicinoids + isothiocyanates + alliaceous sulfur), separate from the
aroma odour heads that keep the same names. All five mouthfeel heads clear the bar (cooling 0.94,
pungent 0.98, warming 0.99, astringent 1.00, tingling 1.00).
The profile vector is taste + aroma + mouthfeel = 175 dims. Since cooling/pungent now exist as both
an aroma and a mouthfeel head, the index builder keys scores by DIM-qualified name ("mouthfeel:cooling"
vs "aroma:cooling") so they can't collide. _MOUTHFEEL_HEADS tagging is retired — the catalog's
mouthfeel group is the 5 dedicated heads. Universe folded the new coolant molecules (8830 unique).
Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
… page build_enrichment now predicts the 5 mouthfeel + 12 tox heads over the whole flavor universe and stores them per molecule (mf_*, tox_*, tox_flags), so the browsable universe table is complete: taste + aroma + mouthfeel + safety. tox uses the bare 2048-bit Morgan (_fp), not the _feat block the other heads use. Note: the Tox21 TRAINING molecules are NOT imported into the flavor universe (they're industrial/pharma compounds — a predictor we apply, not new members); and tox/mouthfeel here are display-only, still OUTSIDE the substitute-match profile vector. 1,817 molecules carry >=1 in-vitro tox flag (caution-only). Warming-up page: reuses the modal's animated flask (ring + fizzing bubbles + aroma wisps), the Flavormancer wordmark + logo, and the app's /static fonts (Grenze Gotisch / Cinzel Decorative) and palette — a branded cold-start page instead of a bare spinner. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…id (first-open feedback)
On first page open the flavor map (~2 MB payload over the tunnel) showed only tiny grey corner
text, and the molecule-universe grid showed nothing until rows arrived. Both now get a proper
branded overlay: a brand-gradient spinner + a Cinzel label ("Charting the flavor map…" /
"Loading the molecule universe…"), so a visitor always sees that data is loading. The map overlay
fades out once the canvas is drawn; the grid shows a centered spinner row while each fetch runs
(also covers search/sort reloads). Served fresh per request — no restart, no tunnel disturbance.
Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…keep-in-sync) predict() now returns a trained-mouthfeel read (predict_mouthfeel: the 5 trigeminal heads — cooling/pungent/warming/astringent/tingling — each with score + CV-AUROC), and tox already rode in safety.tox_screen. Validated: capsaicin -> pungent/warming 1.0, menthol -> cooling 1.0. Modal Heads card: adds a Mouthfeel group and a Safety group beneath aroma, each rendering ALL its heads as ranked % bars with AUROC (same treatment as taste/aroma — nothing filtered). The Heads card now scrolls as a whole; the 164-bar aroma list is capped to its own 300px scroll so mouthfeel and safety stay reachable. Tox bars go amber-red when an assay fires (>=0.5), caution-only framing. MCP read_flavor + skill read now include mouthfeel_scores/confident_mouthfeel and tox_flags; new MCP list_heads tool exposes the /api/heads catalog; fixed the stale "24 aroma scores" docstring. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…#219) Points were jamming the frame in every layout. Similarity (UMAP) view: bumped the canvas margin 18px -> 40px so the cloud breathes. Properties 2D (MW × logP) and 3D (MW × logP × TPSA): the percentile domains were tight (96-98th) and clamped outliers flat onto the edge/faces, piling them into a wall. Now each axis extends to ~99.5th percentile + ~8% headroom, so points sit at their true position with space around them instead of squashed on the border. Frontend-only, served fresh. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
Adds the Mouthfeel section (5 trained chemesthesis heads) and the Tox21 safety heads (12, with CV-AUROC) to the capabilities list, splits stability/chemesthesis now that mouthfeel is its own modality, and corrects the totals: 187 trained heads (6 taste + 164 aroma + 5 mouthfeel + 12 safety) over 8,847 unique molecules. Adds the mouthfeel + Tox21 training-set sizes. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…ync) New docs/MOUTHFEEL.md — the canonical write-up of the third modality (parallel to AROMA.md): the 5 trained chemesthesis heads + AUROCs, why cooling/pungent exist as BOTH aroma and mouthfeel heads (like sweet=taste+aroma), the build pipeline, the food-safety-is-separate rule, and the tingling near-miss honesty note. CAPABILITIES.md: mouthfeel is now a trained modality (was rule/qualitative), tox heads show their CV-AUROC (0.72-0.90), and the stale "24 aroma heads" edition note is corrected to 164 aroma + 5 mouthfeel + 12 safety. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
ruff 0.16's isort rule flagged first-party imports (predict, chemfeatures, build_properties) grouped with third-party ones across the build/train scripts. Import-order only — no behavior change — but it was failing `ruff check .` in CI. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
…its assets The warming-up page uses the app's own wordmark font, header font and logo from /static, but the warming gate returned 503 for every path except /api/status and /healthz — so a cold-start visitor saw the page with fallback fonts and no logo. /static/* is now exempt from the gate (it needs no models to serve). Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
rvnminers-A-and-N
added a commit
that referenced
this pull request
Jul 28, 2026
Mouthfeel shipped as a modality in #243 but wasn't selectable anywhere. _precompute_design now tags the design pool with the mouthfeel heads (batched, like aroma and taste), /api/studio_terms returns a third `mouthfeel` list, and both pickers gained a Mouthfeel group — the Studio chip rail and the Formulation Studio target profile — styled with a dotted amber edge. Two bugs caught by verifying against the live box rather than assuming: 1. tingling matched 0 molecules and astringent 1, because the design pool is built from the HSDB *odor* corpus which barely contains trigeminal agents (0 of 11 tingling, 1 of 11 astringent). Curated mouthfeel agents now fold into the pool via a dedicated _MOUTHFEEL_CARRIERS loader. 2. `pungent` under Mouthfeel returned acetic acid, acetone and ammonia — sharp-SMELLING molecules — because the aroma head and the sensation head shared a bare tag and aroma had far more corpus molecules. Mouthfeel tags are now namespaced (mouthfeel:pungent), matching how the profile index keys aroma:cooling vs mouthfeel:cooling. Verified after deploy: mouthfeel:pungent -> allyl isothiocyanate, diallyl disulfide, capsaicin; note:pungent -> acetic acid, acetaldehyde, acetone, ammonia. All five sensations return real agents. Part of #240, which stays open for the broader chip-picker usability pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #232. Closes #235. Closes #237. Closes #239. Closes #242.
Flavor = taste + aroma + mouthfeel. Mouthfeel is trigeminal, not olfactory, so it gets its own modality end-to-end —
build_mouthfeel_supplement.py→build_mouthfeel_dataset.py→train_mouthfeel.py→mouthfeel_models/— mirroring the independent taste subsystem rather than being bolted onto aroma. Five heads ship, all clearing the honest bar (≥10 public positives and CV-AUROC ≥ 0.70): cooling 0.94, pungent 0.98, warming 0.99, astringent 1.00, tingling 1.00. Sanity holds on the bench: capsaicin → pungent/warming 1.0, menthol → cooling 1.0.coolingandpungentdeliberately exist as both aroma and mouthfeel heads — the same waysweetis both a taste and an aroma head. The aroma head predicts what a molecule smells like; the mouthfeel head predicts the sensation (WS-23 cools with almost no odour). The profile index keys them by dimension (aroma:coolingvsmouthfeel:cooling) so they can't collide.tinglingstayed strict — real sanshool/spilanthol/ZP-amide paresthesia agents only; the Piper amides that read pungent were routed to the aromapungenthead rather than used to pad the count.The flavor-profile vector is now 175 dims (6 taste + 164 aroma + 5 mouthfeel); tox stays out of it deliberately — safety isn't a flavor-match dimension. Total trained heads: 187. Surfaced everywhere per the keep-in-sync rule: the read modal's Heads card gained Mouthfeel and Safety groups (every head as a ranked %-bar with its CV-AUROC, nothing filtered), MCP
read_flavorand the CLI skill returnmouthfeel_scores+tox_flags, and a new MCPlist_headstool exposes the head catalog viaGET /api/heads.Also in this branch: the 12 Tox21 heads now carry a CV-AUROC manifest (0.72–0.90) and are stored per-molecule in
master_enrichmentalongside mouthfeel, so the universe grid is complete (8,847 molecules); every curated supplement molecule now lands in that universe (17 were missing); training and the profile-index build were parallelized process-per-unit (index ~3 min → ~35 s, aroma retrain ~30 min → minutes), all worker counts derived fromos.cpu_count()so it scales from a 4-core box up; model heads load on a background thread so uvicorn binds instantly and serves a branded warming-up page instead of a ~90 s startup 502; flavor-map axes no longer clip points at the frame; and the map + universe grid got loading overlays.Data provenance unchanged: curated public-domain structure→sensation facts (menthol/WS coolants, capsaicinoids, tannins, sanshools), resolved to authoritative SMILES via PubChem. No restricted or NonCommercial source introduced. A head predicts a sensation, not a food-use clearance — the per-molecule food-safe flag and the IP/food review gate still govern.
Docs updated: new
docs/MOUTHFEEL.md, plusCAPABILITIES.mdandREADME.mdcorrected to 187 heads / 8,847 molecules.ruff check .clean and 25 unit tests pass.