Fragrance Evaporation

Quick Start

Fragrances are complex mixtures of many components that contribute to High, Medium and Low notes as the molecules evaporate at different rates depending on their vapour pressures. The rates also depend on airflow. Choose your (simplified) fragrance mix and see how it changes over time

Credits

The way fragrances change over time is fascinating. This is a simplified version of some large-scale models I've created.

Fragrance Evaporation

Airflow m/s
T °C
Amount g/m²
Time min
Total
F1
F2
F3
F4
F5
F6
F7
F8
% 1
% 2
% 3
% 4
% 5
% 6
% 7
% 8
//Here are the aroma molecules, chosen mostly from the Wikipedia list of typical molecules
//Ethanol has been added as it is a common, fast evaporating, ingredient
//Water isn't included because its evaporation rate is strongly dependent on %RH
//The values are: MWt, MVol, AA, AB, AC
const mols = [['Anethole', 272.4, 263.4, 7.882, 2205.8, 171.8],
['Anisole', 108.1, 110.4, 7.07, 1519.9, 207.5],
['Benzaldehyde', 106.1, 101.4, 7.101, 1627.8, 200.8],
['Benzyl Acetate', 150.2, 142.9, 7.194, 1739, 188.1],
['Camphor', 152.2, 156.7, 6.944, 1641, 205.4],
['Carvone', 150.2, 158.9, 7.19, 1754.4, 194.6],
['Cinnamaldehyde', 132.2, 127.9, 7.19, 1786.4, 192.4],
['Citral', 152.2, 172.7, 7.219, 1750.1, 185.1],
['Citronellal', 154.2, 179.1, 7.169, 1727, 184.3],
['Citronellol', 156.3, 182.3, 7.324, 1793.8, 168.1],
['Decalactone', 154.2, 160.3, 7.234, 1776.7, 176.5],
['Ethanol', 46, 58.6, 7.526, 1285.5, 198.4],
['Ethyl Acetate', 88.1, 97.6, 7.095, 1280.2, 218.2],
['Ethyl Butyrate', 116.2, 132.1, 7.111, 1413.9, 207.1],
['Ethyl Maltol', 140.1, 114.7, 7.625, 1942.7, 162.1],
['Eucalyptol', 154.2, 166.3, 6.928, 1647, 210],
['Eugenol', 164.2, 156.1, 7.391, 1942.7, 160.6],
['Galaxolide', 258.4, 258.9, 7.138, 2285.4, 166.9],
['Geraniol', 154.2, 175.2, 7.414, 1822, 168.5],
['Geranyl Acetate', 196.3, 214.5, 7.291, 1884.1, 172],
['Hexyl Acetate', 144.2, 164.7, 7.164, 1577.3, 192.9],
['α-Ionone', 192.3, 209.4, 7.177, 1828.2, 187.6],
['Isoamyl Acetate', 130.2, 148.8, 7.108, 1467.2, 202.5],
['Jasmone', 164.2, 172.1, 7.18, 1799.9, 183.4],
['Limonene', 136.2, 161.5, 7.03, 1541.8, 208],
['Linalool', 154.2, 177.3, 7.274, 1646.9, 172.3],
['Menthol', 156.3, 186.2, 7.318, 1627.4, 184.7],
['Methyl Acetate', 74.1, 80.5, 7.091, 1204.1, 224.5],
['Methyl Anthranilate', 151.2, 131.1, 7.263, 1965.9, 174.3],
['Methyl Butyrate', 102.1, 114.9, 7.098, 1348, 212.4],
['Methyl Propionate', 88.1, 97.6, 7.095, 1280.2, 218.2],
['Muscone', 238.4, 270.9, 7.222, 2142.5, 169.1],
['Myrcene', 136.2, 173.4, 7.016, 1543.8, 201.1],
['Nerol', 154.2, 175.2, 7.414, 1822, 168.5],
['Nerolidol', 222.4, 252.7, 7.478, 2005.8, 147.1],
['Octyl Acetate', 172.3, 198.2, 7.162, 1701.3, 184.9],
['Pentyl Butyrate', 158.2, 182.7, 7.126, 1585, 193.4],
['Terpineol', 154.2, 168.7, 7.398, 1694.5, 175.6],
['Thujone', 152.2, 161, 6.99, 1596.5, 205.3],
['Vanillin', 152.1, 124.6, 7.447, 1942, 137.8],
['Whiskey Lactone', 156.2, 169, 7.227, 1877.4, 184]]



window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults
    loadOptions()
    //Load the stored settings of the newly created combo boxes
    const store = window.location.pathname;
    let storedSettings = window.localStorage.getItem(store);
    if (storedSettings) {
        for (const inputData of storedSettings.split('\n')) {
            if (inputData) {
                const tmp = inputData.split(':');
                const name = tmp[0]
                const input = document.getElementById(name);
                let value = tmp[1]
                if (tmp.length > 2) {
                    for (let i = 2; i < tmp.length; i++) {
                        value += ":" + tmp[i]
                    }
                }
                try { input.value = value; } catch { }; //Some controls can be odd
            }
        }
    }
    const startFs = ['Ethanol', 'Ethyl Butyrate', 'Isoamyl Acetate', 'Limonene', 'Eucalyptol', 'Carvone', 'Linalool', 'Citronellal']
    let acount=0
    for (let j = 1; j < 9; j++) {
        let select = document.getElementById("F" + j)
       if (select.value =="Anethole") acount++
    }
    for (let j = 1; j < 9; j++) {
        let select = document.getElementById("F" + j)
        if (acount>4 || !select.value) select.value = startFs[j - 1]
    }

    Main();
};

//Main() is hard wired as THE place to start calculating when inputs change
//It does no calculations itself, it merely sets them up, sends off variables, gets results and, if necessary, plots them.
function Main() {
    //Save settings every time you calculate, so they're always ready on a reload
    saveSettings();

    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!
    const inputs = {
        Masst: sliders.Slideh.value, //in gsm so no unit change required
        Air: sliders.SlideAir.value,
        T: sliders.SlideT.value,
        tsec: sliders.Slidet.value * 60, //min to s
    };


    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    const result = CalcIt(inputs);
    document.getElementById('Total').value = result.Total;
    //document.getElementById('Data').value = result.Data;
    if (result.plots) {
        for (let i = 0; i < result.plots.length; i++) {
            plotIt(result.plots[i], result.canvas[i]);
        }
    }

    //You might have some other stuff to do here, but for most apps that's it for Main!
}


function loadOptions() {
    for (let j = 1; j < 9; j++) {
        let select = document.getElementById("F" + j)
        select.innerHTML = "";
        for (let i = 0; i < mols.length; i++) {
            let opt = mols[i][0];
            select.innerHTML += "";
        }
    }
}


//Here's the app calculation
//The inputs are just the names provided - their order in the curly brackets is unimportant!
//By convention the input values are provided with the correct units within Main
function CalcIt({ Masst, Air, T, tsec }) {
    const U = Air, TK = T + 273, L = 1
    let FName = [], Perc = [], MWt = [], MVol = [], rho = [], AA = [], AB = [], AC = [], VP = [], Wt = [], i = 0, j = 0, mol = "", pval = 0, tot = 0, NextP = 0
    for (i = 1; i < 9; i++) {
        mol = document.getElementById("F" + i).value
        for (j = 0; j < mols.length; j++) {
            if (mols[j][0] == mol) {
                FName.push(mol)
                MWt.push(mols[j][1])
                MVol.push(mols[j][2])
                rho.push(mols[j][1] / mols[j][2])
                AA.push(mols[j][3])
                AB.push(mols[j][4])
                AC.push(mols[j][5])
                break
            }
        }
        pval = parseFloat(document.getElementById("P" + i).value)
        if (isNaN(pval)) pval = 0
        tot += pval
        Perc.push(pval)
    }
    const Total = tot
    FName.push("Total")

    let MolFr = [], MolT = 0
    //The totals might not add exactly to 100% so redefine them as if they were
    for (i = 0; i < 8; i++) {
        Perc[i] /= tot
        MolFr[i] = Perc[i] / MWt[i]
        MolT += MolFr[i]
        VP[i] = Math.pow(10, AA[i] - AB[i] / (AC[i] + T))
    }
    for (i = 0; i < 8; i++) {
        MolFr[i] /= MolT
    }
    //The evaporation code is old and klunky, but it spells out steps in detail
    let MWtAv = 0, MVolAv = 0, VPAv = 0, Mr = 0, Va = 0, Vb = 0, D = 0, KV = 0, Re = 0, Sc = 0, Sh = 0, k = 0, Moles = 0, PercT = 0
    let PPts = []
    for (i = 0; i <= 8; i++) PPts[i] = new Array(0)
    for (i = 0; i < 8; i++) {
        MWtAv += Perc[i] * MWt[i]
        MVolAv += MolFr[i] * MVol[i]
        PPts[i][NextP] = { x: 0, y: 100 * Perc[i] }
    }
    PPts[8][NextP] = { x: 0, y: 100 }
    NextP++
    MWtAv /= 8; MVolAv /= 8

    tot = 0
    for (i = 0; i < 8; i++) {
        Wt[i] = Perc[i] * Masst
        tot += Wt[i]
        VPAv += MolFr[i] * VP[i]
    }
    const deltat = 1

    PercT = tot
    for (let t = deltat; t <= tsec; t += deltat) {
        Mr = (29 + MWtAv) / (29 * MWtAv)
        Va = Math.pow(20, 0.33333)
        Vb = Math.pow(MVolAv, 0.3333)
        D = 1e-7 * Math.pow(TK, 1.75) * Math.sqrt(Mr) / (1 * Math.pow(Va + Vb, 2))
        KV = 0.00001021 * TK * TK + 0.0031 * TK - 0.2803
        KV *= 1e-5
        Re = L * U / KV
        Sc = KV / D
        Sh = 0.664 * Math.pow(Re, 0.5) * Math.pow(Sc, 0.333)
        k = Sh * D / L
        if (PercT <= 0) break
        PercT = 0
        for (i = 0; i < 8; i++) {
            Moles = deltat * k * L * 1000 * VP[i] * MolFr[i] / 760 / (0.08205 * TK)
            Wt[i] = Math.max(0, Wt[i] - Moles * MWt[i])
            Perc[i] = Wt[i]
            PercT += Wt[i]
            MolFr[i] -= Moles * MolFr[i]
        }
        MWtAv = 0
        for (i = 0; i < 8; i++) {
            Perc[i] /= PercT
            MWtAv += Perc[i] * MWt[i]
            PPts[i][NextP] = { x: t / 60, y: 100 * Perc[i] }
        }
        MWtAv /= 8
        PPts[8][NextP] = { x: t / 60, y: 100 * PercT / tot }
        NextP++
    }

    //Now set up all the graphing data detail by detail.
    let plotData = [PPts[0], PPts[1], PPts[2], PPts[3], PPts[4], PPts[5], PPts[6], PPts[7], PPts[8]], lineLabels = FName, myColors = ['red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 'brown', 'lightblue', 'black']

    const prmap = {
        plotData: plotData, //An array of 1 or more datasets
        lineLabels: lineLabels, //An array of labels for each dataset
        colors: myColors, //An array of colors for each dataset
        borderWidth: [2, 2, 2, 2, 2, 2, 2, 2, 2], //An array of line widths for each dataset
        hideLegend: false, //Set to true if you don't want to see any labels/legnds.
        xLabel: 't&min', //Label for the x axis, with an & to separate the units
        yLabel: 'Fragrance&%', //Label for the y axis, with an & to separate the units
        y2Label: null, //Label for the y2 axis, null if not needed
        yAxisL1R2: [], //Array to say which axis each dataset goes on. Blank=Left=1
        logX: false, //Is the x-axis in log form?
        xTicks: undefined, //We can define a tick function if we're being fancy
        logY: false, //Is the y-axis in log form?
        yTicks: undefined, //We can define a tick function if we're being fancy
        legendPosition: 'top', //Where we want the legend - top, bottom, left, right
        xMinMax: [0,], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        yMinMax: [0, 100], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        y2MinMax: [,], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        xSigFigs: 'F2', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: 'F1', //F for Fixed, P for Precision, E for exponential
    };


    //Now we return everything - text boxes, plot and the name of the canvas, which is 'canvas' for a single plot
    return {
        plots: [prmap],
        canvas: ['canvas'],
        Total: Total,
    };
}

                        

High, Medium and Low notes

A "simple" commercial fragrance might have 20 components, an exotic perfume might have 100. Whatever the fragrance, it will tend to have High, Medium and Low notes - the aroma of the instant hit, the "true" fragrance that's relatively constant for a while, then the lingering low notes.

We have two issues:

  1. Relative volatility
  2. Absolute volatility

Relative volatility

This is governed by 2 factors. The first is obvious: the relative vapour pressures. The second is less obvious: the relative molecular weights. If two molecules have the same vapour pressure, they have the same molar concentration in the air. But the one with the larger molecular weight will have more molecule in the air, so the mass loss will be larger.

The vapour pressure is calculated from the 3 Antoine Constants, AA, AB, AC for each molecule along with the temperature. If you click ShowCode you will find the list of values for the illustrative fragrance molecules. The values are, by convention, in mm/Hg and °C

`log_10(VP)="AA"-(AB)/(AC+T)`

Note that in real formulations the activity coefficients of some of the ingredients will be different from unity. For these demo formulations, the deviations will be small. For real formulations with other components such as oils or water, the deviations might be large.

Absolute volatility

If you just allow molecules to evaporate by diffusion, it takes a long time. It is airflow that makes things volatilize; the airflow sweeps away the slowly-diffusing molecules. The relation between airflow and evaporation rate is complicated, involving Reynolds, Schmidt and Sherwoods numbers. See Solvent Evaporation for more details.

What air velocity should you use? The default of 0.5 m/s is OK for a generally calm environment. 1 m/s is starting to be reasonably fast. If you were in a 10 m/s wind that's quite brisk - 22 mph, 36kph.

Your "fragrance" and your results

We only have 8 ingredients here as we are merely getting a feel for what might happen in a typical fragrance. You can choose between ~ 40 "typical" fragrance molecules. Choose your ingredients and put in your values for the relative amounts. They are always converted to % of whatever total amount you provide, but it's probably a good idea to check that your numbers add up to ~ 100%.

Don't be alarmed by my choice of default fragrance - I just selected a few by approximate order of volatility and by being rather well-known ingredients.