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
//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:
- Relative volatility
- 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.