Fragrance Diffusion
Quick Start
The different molecules in fragrances and flavours can diffuse through packaging at different rates, changing their profile over time. The different rates depend both on molecular size and partition coefficients. It's complicated!
Credits
I've made a number of these models over the years. It's nice to have a simple one in the public domain.
Fragrance Diffusion
//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
//The values are: MWt, MVol, δD, δP, δH
const mols = [['Anethole', 272.4, 263.4, 18.4, 4.2, 2.2],
['Anisole', 108.1, 110.4, 18.3, 4.9, 5.6],
['Benzaldehyde', 106.1, 101.4, 18.9, 8, 6.2],
['Benzyl Acetate', 150.2, 142.9, 17.9, 4.3, 6],
['Camphor', 152.2, 156.7, 17.4, 5.2, 2.1],
['Carvone', 150.2, 158.9, 17.5, 5.8, 3.7],
['Cinnamaldehyde', 132.2, 127.9, 18.7, 6.7, 5.4],
['Citral', 152.2, 172.7, 16.9, 4.7, 4.1],
['Citronellal', 154.2, 179.1, 16.5, 4.5, 3.9],
['Citronellol', 156.3, 182.3, 16.5, 4, 7.9],
['Decalactone', 154.2, 160.3, 17.4, 3.9, 8.6],
['Ethanol', 46, 58.6, 15.8, 8.8, 19.4],
['Ethyl Acetate', 88.1, 97.6, 15.7, 6.3, 7.5],
['Ethyl Butyrate', 116.2, 132.1, 15.7, 4.7, 5.8],
['Ethyl Maltol', 140.1, 114.7, 18.6, 11, 12.6],
['Eucalyptol', 154.2, 166.3, 16.7, 2.8, 2.5],
['Eugenol', 164.2, 156.1, 18.5, 5.6, 9.5],
['Galaxolide', 258.4, 258.9, 17, 0.6, 1.3],
['Geraniol', 154.2, 175.2, 16.9, 4.2, 7.6],
['Geranyl Acetate', 196.3, 214.5, 16.7, 3.1, 4.4],
['Hexyl Acetate', 144.2, 164.7, 16, 4.1, 5.4],
['α-Ionone', 192.3, 209.4, 17, 3.5, 3.2],
['Isoamyl Acetate', 130.2, 148.8, 15.7, 4.1, 5.3],
['Jasmone', 164.2, 172.1, 17.8, 4.7, 4.7],
['Limonene', 136.2, 161.5, 16.7, 1.9, 3.2],
['Linalool', 154.2, 177.3, 16.8, 2.9, 6.9],
['Menthol', 156.3, 186.2, 16.5, 3.5, 6.9],
['Methyl Acetate', 74.1, 80.5, 15.9, 7.4, 8.6],
['Methyl Anthranilate', 151.2, 131.1, 19.1, 8.5, 9.2],
['Methyl Butyrate', 102.1, 114.9, 15.8, 5.2, 6.5],
['Methyl Propionate', 88.1, 97.6, 15.7, 6.3, 7.5],
['Muscone', 238.4, 270.9, 17.3, 2.9, 2.4],
['Myrcene', 136.2, 173.4, 16.1, 1.9, 2.8],
['Nerol', 154.2, 175.2, 16.9, 4.2, 7.6],
['Nerolidol', 222.4, 252.7, 16.9, 2.2, 5.4],
['Octyl Acetate', 172.3, 198.2, 16, 3.4, 4.4],
['Pentyl Butyrate', 158.2, 182.7, 15.8, 3.1, 4.2],
['Terpineol', 154.2, 168.7, 17, 3.6, 7.7],
['Thujone', 152.2, 161, 17.2, 5.7, 1.8],
['Vanillin', 152.1, 124.6, 19.6, 10.7, 12.5],
['Whiskey Lactone', 156.2, 169, 16.2, 10.4, 4.1]]
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.SlideWt.value*1e-4, //gsm to g/cm2
h: sliders.Slideh.value*1e-4, //μm to cm units
D: sliders.SlideD.value * 1e-10, //in cm units
dDP: sliders.SlidedD.value,
dPP: sliders.SlidedP.value,
dHP: sliders.SlidedH.value,
R: sliders.SlidedR.value,
tsec: sliders.Slidet.value * 3600, //hr 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, h, D, dDP, dPP, dHP, R, tsec }) {
let FName = [], Perc = [], MWt = [], dD = [], dP = [], dH = [], Dist = [], DC = [], Wt = [], i = 0, j = 0, mol = "", tot = 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])
dD.push(mols[j][3])
dP.push(mols[j][4])
dH.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
Wt[i] = Masst*Perc[i]
Dist[i] = Math.exp(-(4*Math.pow(dDP-dD[i],2)+Math.pow(dPP-dP[i],2)+Math.pow(dHP-dH[i],2))/Math.pow(R,2))
DC[i] = D*100/MWt[i]
}
let PercT = 0, NextP=0
let PPts = []
for (i = 0; i <= 8; i++) PPts[i] = new Array(0)
for (i = 0; i < 8; i++) {
PPts[i][NextP] = { x: 0, y: 100 * Perc[i] }
}
PPts[8][NextP] = { x: 0, y: 100 }
NextP++
const deltat = 60 //1 min timestep seems OK
PercT = tot
for (let t = deltat; t <= tsec; t += deltat) {
PercT = 0
for (i = 0; i < 8; i++) {
Flux = deltat* 0.01*Dist[i]*Perc[i] * DC[i]/h
Wt[i] = Math.max(0, Wt[i] - Flux)
Perc[i] = Wt[i]
PercT += Wt[i]
}
MWtAv = 0
for (i = 0; i < 8; i++) {
Perc[i] /= PercT
PPts[i][NextP] = { x: t / 3600, y: 100 * Perc[i] }
}
PPts[8][NextP] = { x: t / 3600, y: 100 * PercT / Masst }
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&hr', //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,
};
}
D and Partition
As Diffusion Basics explains, the flux of a molecule across a barrier of thickness x with a concentration difference of ΔC is given by `Flux =(DΔC)/x` where D is the diffusion coefficient of that molecule in that barrier material (we'll call it "polymer" from now on).
D, for a representative MWt = 100, may vary, for the purposes of this app from 10-8 to 10-10 cm²/s depending on the polymer. Let's call it D100. But smaller molecules diffuse faster (and larger ones slower) and although we can debate the effect of size, it's good enough to say that `D=D_(100)(100)/(MWt)`
You can get some idea of typical values of D from the Diffusion Coefficients app which also shows the effect of MWt via a more sophisticated method.
A much bigger effect is due to ΔC. If we assume that everything that reaches the other side disappears, then ΔC is just the concentration at the surface in contact with the fragrance. This concentration depends on partition between the fragrance phase and the polymer. This is complicated. For simplicity we just use the HSP values of each molecule and the polymer to calculate the HSP Distance (see HSP Basics). If the Distance is small, we say, for simplicity, that the concentration is 1%. At any given Distance we say that the concentration falls of exponentially by (Distance/R)², where R is the typical HSP radius of a polymer - the distance beyond which the polymer is insoluble. Although the calculation is, of necessity, simplistic, the effect is very real and you get a good sense of the trends.
A thin layer of fragrance
We could have a fancy app specifying the volume of product, the concentration of fragrance, the surface area in contact etc. But because we are focussing on principles, you have to make the assumption that your fragrance is sitting as a layer of Amount g/m² (1 gsm is equivalent to a thickness of 1μm). This forces you to do some rough calculations of your own system, but it's better than the alternative.
What a difference a day makes
The app is set up with a choice of 8 ingredients (chosen from 40+ typical fragrance molecules) and at your chosen % values. The default ingredients are the same as the Fragrance Evaporation app. You have the thickness of the polymer, the timescale in hours and the value of D for a 100 MWt molecule (you slide from 1 to 100, changing from 10-10 to 10-8
You define your polymer in terms of its HSP values and the radius, R. HSP values for typical polymers are shown below. You have to decide if it's a "soft" polymer with a large R (say 8) or a "hard" polymer with a small R (say 5).
So now, over a day or two you can see how the fragrance changes. It's good to get a feel for the effects of the sliders, Thickness, D, Amount and Time. Then you can start changing the HSP and R values. R has a big effect. If you change the δH slider you will find big changes in how those ingredients with -OH groups behave. You will also see clusters of molecules changing together - because many fragrance ingredients, e.g. the terpenes, are in similar parts of HSP space.
Poor barriers, great barriers
It turns out that any single polymer is going to be a poor barrier for some molecules - there will be a good HSP match so the partition will be large and diffusion will be fast. The answer, familiar to anyone who has used a "simple" food packaging, is to have multiple layers with very different HSP values. The 2-3μm of EVOH coextruded inside two 12μm layers of PE is a very thin barrier. The PE provides resistance to water and polar fragarance molecules. The EVOH is an excellent barrier to oxygen and to hydrophobic molecules such as terpene flavours. The emphasis on partition is clear. EVOH isn't a great polymer in any way, and 2μm is very thin. But because partition into such a polar polymer is low for hydrophobic molecules, it provides a great barrier.
A real world example
On the Hansen Solubility site there is an example, Flavor Scalping, of a real-world case of some of the flavor molecules in the package of an aqueous flavor concentrate having a smaller HSP Distance than others and preferentially disappearing over a period of some days. At the time, the use of HSP Distance to analyse such a problem was unknown to the industry.
Typical polymer HSP values
- Polyethylene (PE) [16.9, 0.8, 2.8]
- Polypropylene (PP) [18, 0, 1]
- Polystyrene (PS) [18.5, 4.5, 2.9]
- Polyvinylchloride (PVC) [19.2, 7.9, 3.4]
- Polyacrylonitrile (PAN) [22.4, 14.1, 9.1]
- Polymethylmethacrylate (PMMA) [18.6, 10.5, 5.1]
- Polyethylmethacrylate (PEMA) [17.6, 9.7, 4]
- Polycarbonate (PC) [18.2, 5.9, 6.9]
- Polycaprolactone [17.7, 5, 8.4]
- Polyvinylacetate (PVA) [17.6, 2.2, 4]
- Nylon 66 [17.4, 9.9, 14.6]
- PET [18.2, 6.4, 6.6]
- Epoxy [17.4, 10.5, 9]
- Polyvinylbutyral [18.6, 4.4, 13]
- Polyvinylidenefluoride (PVdF) [17, 12.1, 10.2]
- Polyphenyleneoxide (PPO) [17.9, 3.1, 8.5]
- Polyurethane (PU) [18.1, 9.3, 4.5]
- Polysulphone [16, 6, 6.6]
- Polydimethylsiloxane (PDMS) [17.2, 3, 3]
- Polyethersulfone [19, 11, 8]
- Polyoxymethylene (PON) [17.2, 9.2, 9.8]
- Polyvinylpyrrolidone (PVP) [17.5, 8, 15]
- CyclicOlefinCopolymer (COC) [18, 3, 2]
- Polyethylene oxide (PEO, PEG) [17, 10, 5]
- Polypropylene oxide (PPO, PPG) [16.5, 9, 7]
- Polyvinylalcohol (PVOH) [15, 17.2, 17.8]
- Ethylene-vinylalcohol (EVOH) [15.5, 13, 13]
- Polylactic acid (PLA) [18.6, 9.9, 6]