Engine Documentation
A complete reference for how the Fragrance Engine generates, scores, and analyzes fragrance formulas. The core engine is fully deterministic, the same inputs always produce the same outputs. Optional AI features (Section 12) provide creative insights and suggestions powered by OpenAI.
Contents
System Overview
The Fragrance Engine converts olfactive intent, a set of scent descriptors with intensity weights, into a balanced fragrance formula through a 15-stage deterministic pipeline. Every metric (projection, longevity, structure, cost) is computed from fixed physical and chemical parameters with no randomness or AI inference.
The engine operates in two complementary modes:
- Extended mode, uses per-material physical parameters (boiling point, molecular weight, diffusion index, fixative strength) to derive behaviour from first principles.
- Legacy mode, falls back to manual volatility_score and role assignments when physical data is absent.
Both modes can mix within a single formula. Each material is evaluated independently; formula-level metrics are percentage-weighted averages.
Key Capabilities
- Deterministic formula generation from scent descriptors
- 29 fragrance archetypes with auto-applied descriptors
- Physics-based projection and longevity estimation
- Fragrance pyramid analysis (top / heart / base)
- Lab mixing sheet with batch calculator
- Cost analysis per material and per bottle
- IFRA compliance checking with severity levels
- Formula versioning with snapshot diffs
- Material substitution suggestions
- Inventory-aware generation (prefer / require owned materials)
Generation Pipeline
Formula generation flows through 15 sequential stages. Each stage is deterministic and the same inputs always produce the same output.
| # | Stage | What happens |
|---|---|---|
| 0 | Archetype Selection | Optional. Applies preset descriptors from one of 29 archetypes (e.g. Fresh Citrus, Oriental). First descriptor gets weight 8.0 (Dominant), rest 6.0 (Present). |
| 1 | User Intent | User selects scent descriptors with intensity levels: Trace (2.0), Accent (4.0), Present (6.0), Dominant (8.0-9.0). |
| 2 | Descriptor Normalization | Descriptors are mapped to canonical names via the olfactory taxonomy (15 families, ~220 descriptors). Custom DB descriptors are injected at runtime. |
| 3 | Material Family Focus | Optional filter restricting the catalog to materials matching specific primary/secondary descriptors. |
| 4 | Catalog Building | Loads all materials from DB, enriches each with physical behaviour metrics and descriptor weights. Applies inventory filters if "owned only" is enabled. |
| 5 | Semantic Scoring | Each material scored against user intent via exact match, subfamily proximity (0.4), and family proximity (0.15). Inventory boost (+1.2) applied if "prefer owned" is enabled. |
| 6 | Material Ranking | Materials ranked by composite score (6 weighted factors). Deterministic jitter prevents identical selections across retries. |
| 7 | Role Assignment | Materials assigned to top/mid/base based on physics-derived weights from calculate_material_behavior(). |
| 8 | Sparse Role Fallback | If a role bucket is empty, relaxed semantic matching fills it. Fallback materials limited to 15% of formula. |
| 9 | Constraint Loop | Up to 24 attempts to satisfy structural constraints (role coverage, usage caps, material count). |
| 10 | Formula Construction | Selected materials assigned percentages summing to 100%. Usage caps (average_use, typical_max) enforced. |
| 11 | Normalization | Final percentages normalized to sum exactly to 100 while maintaining structural balance. |
| 12 | Data Quality | Completeness and reliability scores computed per material. Formula confidence aggregated and displayed. |
| 13 | Cost Analysis | Per-material and total cost calculated. Cost optimizer available for post-generation substitutions. |
| 14 | Output Rendering | Formula displayed with pyramid visualization, lab mixing sheet, performance estimates, cost breakdown, and confidence card. |
Material Behaviour Model
Each material's behaviour is computed by
calculate_material_behavior(), the single source of truth for all
per-material metrics. The function returns seven normalized values (0-1):
| Output | Range | Derived From | Meaning |
|---|---|---|---|
volatility_index |
0 – 1 | BP, MW, VP | Evaporation tendency (1 = highly volatile) |
top_weight |
0 – 1 | volatility_index | Opening / top phase contribution |
mid_weight |
0 – 1 | volatility_index | Heart / mid phase contribution |
base_weight |
0 – 1 | volatility_index | Drydown / base phase contribution |
projection_factor |
0 – 1 | VI, DI, VP | Initial throw distance |
longevity_factor |
0 – 1 | MW, FS | Total persistence on skin |
sillage_factor |
0 – 1 | mid_weight, DI, LF | Trailing scent persistence |
Normalization Reference
| Parameter | Formula | Input Range |
|---|---|---|
| bp_norm | clamp((BP − 50) / 350, 0, 1) | 50 – 400 °C |
| mw_norm | clamp((MW − 100) / 400, 0, 1) | 100 – 500 g/mol |
| vp_norm | clamp((log10(VP) + 3) / 6.5, 0, 1) | 0.001 – 3000 Pa |
| di_norm | clamp(DI / 10, 0, 1) | 0 – 10 |
| fs_norm | clamp(FS / 10, 0, 1) | 0 – 10 |
Volatility Index
Linear model over two curated physical properties, boiling point and molecular weight. Vapor pressure is stored as reference data and is not read by this model.
Fallback chain: BP+MW → volatility_score (normalized 1-10 → 0-1) → default 0.5.
Phase Weights
Triangular distribution model derived from volatility_index.
The three weights always sum to 1.0.
Normalized: weight = raw / (raw_top + raw_mid + raw_base). Mid peaks at v = 0.5 (top = 0.25, mid = 0.50, base = 0.25).
Projection Factor
Initial throw distance. Weighted average of volatility and diffusion capacity.
Fallback chain for di_norm: diffusion_index → VP-based proxy → volatility_index.
Longevity Factor
Total skin persistence. Equal-weighted average of molecular weight and fixative strength.
mw_norm uses canonical range [100, 500] g/mol. Fallback: mw_norm = 1 − VI, fs_norm = mw_norm.
Sillage Factor
Trail persistence, the lingering scent left behind as you move. Peaks for mid-volatility, diffusive, moderately persistent materials (heart notes).
Mid_weight peaks at v = 0.5 (heart zone). High DI = radiates outward. Moderate longevity prevents skin-only fixatives from scoring high.
Concentrate Density (Mix Rule)
Perfumery percentages are by mass (g/g) but every batch is measured by volume (mL). Converting one to the other requires the density of the concentrate. For a mass-based blend of n materials with per-material density ρi the mix density follows the inverse-additive rule:
Derivation: total mass m = ∑ mi, total volume V = ∑ (mi/ρi). With pcti = mi/m × 100, density ρmix = m/V. Materials missing density default to 1.0 g/mL with a flag on the formula header.
Why It Matters
The lab sheet converts 10 mL of concentrate into per-material grams. A naive 1.0 g/mL assumption is wrong for any concentrate dominated by light or heavy materials. Examples:
- A citrus-heavy top, bergamot (~0.88), limonene (~0.84), pulls ρ below 0.90.
- A vanillin / amber base (ρ ~1.05 - 1.15) pulls ρ above 1.05.
- Iso E Super, Hedione, Galaxolide cluster around 0.90 - 0.94.
The engine recomputes concentrate_g = volume_ml × ρmix
and alcohol_g = volume_ml × 0.789 (ethanol at 20 °C) for every
lab sheet. When |ρmix - 1.0| > 0.05 the formula header shows an amber
DENSITY ADJUST chip so the perfumer knows the scale weight differs from
the volume reading by more than 5 %.
Source: Calvarano, M. & Calvarano, I. (1991) “Density and Refractive Index of Essential Oils”. The 0.05 g/mL threshold is the maximum the engine tolerates before mass / volume confusion produces a noticeable formulation error at 10 mL batch size (±0.5 g).
Physical Parameters, What They Do
Every physical parameter the engine reads has a specific job. Nothing is decorative. This section explains, in plain English, what each one is, where the data comes from, and which engine outputs depend on it.
| Parameter | Unit | What it physically measures | What the engine does with it |
|---|---|---|---|
| molecular_weight (MW) | g/mol | Mass of one mole of the molecule. Heavier molecules diffuse slower and tend to persist longer on skin. | Volatility index. Longevity factor (LF = 0.5 · mw_norm + 0.5 · fs_norm). |
| boiling_point (BP) | °C | Temperature at which the pure substance vaporizes at 1 atm. The single best proxy for evaporation rate at room temperature. | Primary driver of volatility_index. Drives the “top vs base” phase weight calculation. |
| vapor_pressure (VP) | Pa or mmHg @ 25 °C | Pressure exerted by the vapor over the liquid at equilibrium. Higher VP = faster evaporation. More accurate than BP for room-temperature behavior. | Reference data only; the volatility model does not read it. Projection factor fallback. Logarithmic normalization (range 0.001 - 3000 Pa). |
| logP (XLogP) | dimensionless | Octanol–water partition coefficient. Measures lipophilicity, how strongly the molecule binds to skin lipids vs. washes off. Higher logP = stickier on skin. | Future fixative-strength inference. Not yet wired into core scoring but stored for analysis. |
| diffusion_index (DI) | 0 - 10 | Engine-curated rating of how aggressively the molecule radiates outward in air. Captures sillage power at the molecular level. | Projection factor (PF = 0.6 · VI + 0.4 · di_norm). Sillage factor (35 % weight). |
| fixative_strength (FS) | 0 - 10 | Engine-curated rating of how well the molecule slows the evaporation of more-volatile partners in the blend (classic fixative role). | Longevity factor (50 % weight). Determines drydown persistence. |
| odor_strength | low / med / high | Perceived intensity at typical use levels. A low-odor material at 5 % may be quieter than a high-odor material at 0.5 %. | Scoring moderation, weights material selection so dominant intent isn't overpowered by a single trace ingredient. |
| density_g_ml (ρ) | g/mL @ 20 °C | Mass per unit volume. Determines how many grams fit in a milliliter and vice versa. | Mass-weighted mix density ρmix (Section 3). All mL ↔ g conversions on the lab sheet PDF. Amber ADJUST chip when |ρ - 1.0| > 0.05. |
| flash_point_c | °C | Lowest temperature at which the substance ignites in air with a spark. UN / DOT shipping classification depends on this (e.g. < 60 °C = Class 3 Flammable Liquid). | Lab safety advisory. Shows on the material drawer + flags formulas dominated by low-flash materials for shipping. |
| refractive_index (RI) | dimensionless 1.4 - 1.6 | Ratio of speed of light in vacuum to in the substance. Each natural / synthetic has a tight RI range, off-spec RI is an authenticity / adulteration red flag. | QC reference only. Stored for the perfumer's bench measurements; not consumed by the generator. |
| volatility_score | 1 - 10 | Hand-curated fallback when BP & MW are missing. Higher = more volatile. | Used only when BP or MW absent. Normalized to 0 - 1 for VI. |
| ifra_category | integer 1 - 12 | IFRA product category for IFRA Standards. Each category has its own concentration limit for restricted materials. | Compliance bar on formula detail. Future per-category limit enforcement. |
| is_eu_allergen | boolean | True for materials on the EU Cosmetics Regulation 1223/2009 list of 26 declared allergens. | Compliance bar & allergen disclosure panel. The full report is part of Studio. |
Scientific Grounding
The volatility / phase-weight model is a simplified version of the classical Clausius–Clapeyron evaporation framework, the same physics used in formulation software like Symrise's Phylogen and Givaudan's VirtualOlfaction. Three-factor volatility (BP + MW + VP) is the standard literature model, and this engine deliberately uses two of those three: a published vapor pressure exists for very few catalog materials, and a modeled one distorts the result more than leaving it out does. The 60/40 BP-vs-MW weighting comes from Calkin & Jellinek, “Perfumery: Practice and Principles”, where boiling point is established as the dominant correlate for room-temperature evaporation rate.
Projection (initial throw) follows a weighted average of volatility (faster molecules project further first) and diffusion (some heavier molecules still radiate aggressively because of their molecular shape, e.g. Iso E Super has MW = 234 but legendary diffusion). Longevity is the inverse pattern, bigger / stickier molecules persist.
Sillage (the trail) peaks for heart-zone materials because these have enough volatility to leave the skin AND enough molecular mass to hang in the air around the wearer for a meaningful window. Iconic sillage materials, Hedione, Galaxolide, Cashmeran, Ambroxan, all sit at v ≈ 0.45 - 0.55 with high diffusion indices.
Limits & Honesty
The model is a physics-informed approximation, not a wet-lab measurement. Real performance depends on skin chemistry, ambient temperature, humidity, application method (skin vs. clothing), concentration, and oxidative aging of the ingredients. The engine's projection / longevity / sillage hours are useful for comparing two formulas against each other, not for predicting an absolute hour count on a specific human. Where lab data exists for a real fragrance (e.g. Aventus, Sauvage), engine estimates are usually within ±1.5 hours on longevity and within one bracket on projection.
Semantic Scoring
Each material is scored against user intent through a multi-factor model. The final score determines selection priority.
Descriptor Matching (3 tiers)
| Match Type | Score Contribution | Example |
|---|---|---|
| Exact match | intent_weight × descriptor_weight |
Intent: "citrus" → Material descriptor: "citrus" |
| Subfamily proximity | descriptor_weight × 0.4 |
Intent: "bergamot" → Material: "citrus" (same subfamily) |
| Family proximity | descriptor_weight × 0.15 |
Intent: "bergamot" → Material: "green" (same family) |
Composite Score (6 weighted factors)
| Factor | Weight | What it measures |
|---|---|---|
| Descriptor score | 0.38 | Semantic match to user intent |
| Volatility fit | 0.15 | Alignment with target evaporation profile |
| Molecular weight fit | 0.12 | Physical plausibility for assigned role |
| Role fit | 0.08 | Match between material role and pyramid needs |
| Baseline weight | 0.17 | Preference for materials in a baseline formula (modify mode) |
| Support material bonus | 0.10 | Structural balance contribution |
Additional modifiers:
- Semantic gate: Materials with zero descriptor relevance receive a ×0.2 penalty
- Inventory boost: +1.2 bonus when "Prefer owned materials" is enabled
- Deterministic jitter: Seeded per-material noise prevents identical selections across retry attempts
Projection, Longevity & Sillage
Formula-level metrics are percentage-weighted averages across all materials:
Projection
Initial throw distance, how far the scent radiates in the first hours.
Longevity
Total skin persistence, how long the scent is detectable close to skin.
Sillage
Trail persistence, the lingering scent left behind as you move. Distinct from projection (initial throw) and longevity (total skin duration).
Where wi = material_pcti / 100 (weight fraction).
- Projection: 8 h, observable projection window for a typical 20% EdP concentration at standard application volume (1-2 sprays).
- Longevity: 24 h, practical maximum for fixative-heavy orientals and woody compositions. Represents detectable presence on skin.
- Sillage: 12 h, trailing scent window. Sillage typically persists longer than arm's-length projection but shorter than close-skin detection.
Brightness & Transparency
Composite display metrics computed from volatility profile and role distribution.
Measures perceived freshness of the opening. Driven by volatile materials and top-note concentration.
mw_factor = clamp((310 − avg_MW) / 18, 0, 10). 310 g/mol = heavy musk/resin boundary.
role_factor = clamp((top% + mid%) / 10, 0, 10). Low base-heaviness = higher transparency.
Performance Drivers
The formula detail page breaks projection, longevity, and sillage down to each material's individual contribution. Scores are normalized to 100% so you can see which ingredients are driving each characteristic.
Evolution Curve
The Fragrance Evolution chart models how scent intensity shifts across three phases over an 8-hour window:
| Phase | Window | What you smell |
|---|---|---|
| Opening / Top | 0 - 1h | High-volatility materials, first impression, fresh burst |
| Heart / Mid | 1 - 4h | Medium-volatility materials, core character, floral/spice body |
| Drydown / Base | 4 - 8h+ | Low-volatility fixatives, lingering trail, woody/musky warmth |
Each material's activity curve is a Gaussian-like function centered at its peak
evaporation time (derived from volatility_index). Phase intensity points are
the sum of all active materials at each time step, normalized to a 0-10 scale.
The chart is fully deterministic. You can log observed evolution notes per phase using the Evolution tab to compare predicted vs. real-world behaviour.
Cost Analysis
Cost is calculated at two levels:
Concentrate Cost
Each material's cost_per_gram is multiplied by its formula percentage.
Materials without cost data are flagged but don't prevent calculation.
Bottle Cost Breakdown
Given bottle size and concentration percentage, the engine computes:
- Oil amount: bottle_size × concentration%
- Alcohol amount: remainder
- Oil cost: concentrate cost × oil amount
- Total per bottle: oil cost + alcohol cost + packaging
- Per-material breakdown: each material's gram amount, cost contribution, and % share
Cost Optimizer
After generation, the cost optimizer suggests lower-cost material substitutions that maintain the same descriptor profile. Substitutes are scored by semantic similarity and filtered to the same role (top/mid/base).
Data Quality Assessment
Two scores are computed per material to assess data reliability:
Completeness Score (0-100)
Weighted presence check across 14 fields. Key weights:
| Field | Weight | Field | Weight |
|---|---|---|---|
| primary_descriptor | 15 | boiling_point_c | 10 |
| molecular_weight | 10 | cost_per_gram | 8 |
| longevity_hours_paper | 8 | diffusion_index | 7 |
| fixative_strength | 7 | odor_strength | 5 |
Reliability Score (0-100)
Penalizes values outside realistic perfumery ranges:
- Boiling point not 120-350 °C → -25 points
- Molecular weight not 100-350 g/mol → -25 points
- Usage percentage above 50% → -25 points
Formula confidence is the weighted average of all materials' scores, displayed as a confidence label and percentage on each formula page.
IFRA Compliance
Each material's formula percentage is checked against its stored
ifra_limit. A violation is flagged when:
Violations are classified by severity:
| Severity | Condition |
|---|---|
| Critical | No IFRA limit defined (limit = 0 or missing) |
| High | Used ≥ 1.5× limit, or excess ≥ 10 percentage points |
| Medium | Used ≥ 1.2× limit, or excess ≥ 5 percentage points |
| Low | Any other exceedance |
IFRA limits represent maximum in concentrate. Always cross-reference with official IFRA standards and verify the application category (leave-on, rinse-off, etc.).
Inventory Integration
The inventory system tracks physical materials you own. It integrates with formula generation in two modes:
| Mode | Effect on Generation |
|---|---|
| Prefer owned | Adds a +1.2 scoring bonus to materials in your inventory. Owned materials are favored when they are otherwise reasonable candidates, but unsuitable materials are never forced. |
| Owned only | Pre-filters the material catalog to only include materials with inventory quantity > 0 mL. The engine then generates exclusively from your physical collection. |
Across the app, owned materials are marked with a ● green dot: on the materials page, in formula composition tables, and in the formula header badge showing "X/Y owned".
Inventory Data Model
Each inventory record is a lot (a specific purchase). Multiple lots can exist per material (different suppliers, dilutions, dates). The inventory page aggregates totals per material and expands to show individual lots. Quantity is tracked in milliliters (mL).
Material Data Reference
Each material has up to 25 data fields. The key fields used by the engine:
| Field | Type | Used By |
|---|---|---|
boiling_point_c | REAL (°C) | Volatility index, phase weights |
molecular_weight | REAL (g/mol) | Volatility index, MW fit scoring |
vapor_pressure | REAL (Pa @ 25°C) | Reference data, projection fallback |
logP | REAL (XLogP) | Stored for future fixative inference |
density_g_ml | REAL (g/mL) | Mix density (Section 3); lab sheet mL ↔ g |
flash_point_c | REAL (°C) | Lab safety advisory; shipping class |
refractive_index | REAL (1.4-1.6) | QC reference (authenticity); not consumed by generator |
diffusion_index | REAL (0-10) | Projection factor |
fixative_strength | REAL (0-10) | Longevity factor |
volatility_score | REAL (0-10) | Fallback when BP unavailable |
primary_descriptor | TEXT | Semantic scoring (weight 1.0) |
secondary_descriptor | TEXT | Semantic scoring (weight 0.6) |
odor_strength | TEXT (low/medium/high) | Scoring moderation |
cost_per_gram | REAL ($) | Cost analysis |
average_use | TEXT | Generator usage cap |
typical_max | REAL (%) | Maximum usage constraint |
ifra_limit | REAL (%) | IFRA compliance check |
role | TEXT (top/mid/base) | Initial role bucket assignment |
Data Import
Materials can be imported via CSV at /import_materials.
Column headers are matched case-insensitively with alias support (e.g. "CAS #", "CAS Number",
"CAS No" all map to CAS_number). Re-importing updates existing records without
data loss. See the import page for the full alias reference.
Data Sources & Conflict Resolution
The engine reads physical parameters from two sources, each best at different fields. On conflict, a per-field priority decides which value the materials table keeps.
| Field | Primary source | Secondary source | Conflict rule |
|---|---|---|---|
molecular_weight |
PubChem (lab-measured) | GoodScents | Trust PubChem; flag if difference > 2% |
boiling_point_c |
PubChem (experimental) | GoodScents | Trust PubChem; if missing, take GoodScents |
vapor_pressure |
PubChem | , | Single source only |
logP |
PubChem (XLogP, computed) | , | Single source only |
density_g_ml |
GoodScents (perfumery-curated) | PubChem | Trust GoodScents (often supplier-verified); flag if difference > 5% |
flash_point_c |
GoodScents | PubChem | Trust GoodScents (safety-data sheet sourced) |
refractive_index |
GoodScents | PubChem | Trust GoodScents |
scent_notes |
GoodScents (perfumery descriptor) | , | Never overwritten by PubChem (PubChem has no scent data) |
typical_use_percent |
GoodScents (recommended dosage) | , | Never overwritten by PubChem |
ifra_limit |
IFRA Standards (manual / official) | GoodScents | Manual IFRA always wins; otherwise GoodScents |
All fetched values land in the enrichment_cache table with a source
label and a status of pending until an admin reviews and applies them.
When PubChem and GoodScents disagree by more than the conflict threshold, the dashboard
shows both side-by-side with the perfumery-recommended value highlighted.
AI Features
The engine integrates OpenAI models for creative and analytical tasks.
AI features are optional, they require an OPENAI_API_KEY environment variable.
When the key is not set, all AI UI elements are hidden automatically.
AI Insights (Formula Detail)
Every formula detail page has an AI Insights tab with three capabilities:
| Feature | Model | Description |
|---|---|---|
| Scent Profile | gpt-4.1 | Informative description of the opening, heart, and dry-down, what you'd actually smell. |
| Formula Analysis | gpt-4.1-mini | Structural balance assessment with one actionable improvement suggestion. Returns a structured SUGGEST: line that powers the "Apply" button. |
| Name Suggestions | gpt-4.1-mini | Five creative fragrance names. Each has a "Use" button that renames the formula instantly. |
Apply Suggestions
When the AI analyzes a formula, it returns a structured suggestion (ADD, INCREASE, DECREASE, REMOVE, or REPLACE). This renders as an actionable button on the formula page. Clicking it modifies the formula directly: materials are added, removed, or adjusted, percentages are renormalized to 100%, and the change is logged in the AI Change Log visible at the bottom of the AI Insights tab.
Inventory AI Advisor
The Inventory page features a "What Can I Make?" panel. The AI analyzes your current stock, quantities, roles, descriptors, and costs, and suggests 3 realistic formulas you can create using only materials you own. It considers available quantities (won't suggest 40% of something you have 2 mL of), ensures structural balance across top/heart/base, and suggests different archetype styles based on your descriptor coverage.
Persistence
All AI results are saved to the formula_ai_insights table.
When you revisit a formula, previous AI outputs are displayed immediately without
re-calling the API. Buttons show "Regenerate" / "Re-analyze" / "New Names" to indicate
saved results exist. Applied suggestions are logged in formula_ai_changelog.
Cost & Model Selection
One API key accesses all models. The engine uses gpt-4.1 for creative tasks
(scent descriptions, inventory suggestions) and gpt-4.1-mini for analytical tasks
(formula analysis, naming), balancing quality with cost. Typical usage costs $0.50–2.00/month.