Extractables & Leachables

Quick Start

Extractables and Leachables (E&L) in medical devices must be estimated via validated methodologies, at the core of which are robust estimates of diffusion coefficents. Here you see how these depend on MWt, T and polymer type, and can view the upper and lower limits of how much might be extracted over time.

Credits

The app is based on the core paper from the FDA by Elder and Saylor1 with additions from my HSP experience.

Diffusion - E&L

MWt
T °C
L mm
tmax d
V=Liquid/Solid
K = Liquid/Solid
Option
Integrated
Solvents
General
ISO 10993
Polymer
Solvent
Values
//One universal basic required here to get things going once loaded
"use strict";
const ISOArray = ["Acetone : 15.5 : 10.4 : 7 : 74 : 560",
    "Acetonitrile : 15.3 : 18 : 6.1 : 53 : 230",
    "Cyclohexane : 16.8 : 0 : 0.2 : 109 : 560",
    "Dimethyl Sulfoxide (DMSO) : 18.4 : 16.4 : 10.2 : 71 : 3",
    "Ethanol : 15.8 : 8.8 : 19.4 : 59 : 150",
    "Heptane : 15.3 : 0 : 0 : 147 : 390",
    "Hexane : 14.9 : 0 : 0 : 131 : 830",
    "Methanol : 14.7 : 12.3 : 22.3 : 41 : 300",
    "Methylene Chloride : 17 : 7.3 : 7.1 : 64 : 140",
    "1-Propanol : 16 : 6.8 : 17.4 : 75 : 86",
    "2-Propanol : 15.8 : 6.1 : 16.4 : 77 : 150",
    "Tetrahydrofuran (THF) : 16.8 : 5.7 : 8 : 82 : 472",
    "Toluene : 18 : 1.4 : 2 : 107 : 190",
    "Water : 15.5 : 16 : 42.3 : 18 : 80",
]
const SolventArray = ["Acetone : 15.5 : 10.4 : 7 : 74 : 560",
    "Acetonitrile : 15.3 : 18 : 6.1 : 53 : 230",
    "Benzyl Alcohol : 18.4 : 6.3 : 13.7 : 104 : 0.6",
    "1-Butanol : 16 : 5.7 : 15.8 : 92 : 43",
    "2-Butanol : 15.8 : 5.7 : 14.5 : 92 : 81",
    "n-Butyl Acetate : 15.8 : 3.7 : 6.3 : 133 : 100",
    "Cyclohexane : 16.8 : 0 : 0.2 : 109 : 560",
    "Cyclohexanol : 17.4 : 4.1 : 13.5 : 106 : 1",
    "Cyclohexanone : 17.8 : 8.4 : 5.1 : 104 : 29",
    "Diacetone Alcohol : 15.8 : 8.2 : 10.8 : 124 : 12",
    "N,N-Dimethyl Acetamide : 16.8 : 11.5 : 10.2 : 93 : 13.8",
    "N,N-Dimethyl Formamide (DMF) : 17.4 : 13.7 : 11.3 : 77 : 10",
    "Dimethyl Sulfoxide (DMSO) : 18.4 : 16.4 : 10.2 : 71 : 3",
    "1~4-Dioxane : 17.5 : 1.8 : 9 : 86 : 24",
    "Ethanol : 15.8 : 8.8 : 19.4 : 59 : 150",
    "Ethyl Acetate : 15.8 : 5.3 : 7.2 : 99 : 390",
    "Ethyl Benzene : 17.8 : 0.6 : 1.4 : 123 : 89",
    "Ethyl Lactate : 16 : 7.6 : 12.5 : 115 : 22",
    "Ethylene Carbonate : 18 : 21.7 : 5.1 : 66 : 0.04",
    "Ethylene Glycol : 17 : 11 : 26 : 56 : 1",
    "Ethylene Glycol Monobutyl Ether : 16 : 5.1 : 12.3 : 132 : 3",
    "Ethylene Glycol Monomethyl Ether : 16 : 8.2 : 15 : 79 : 33",
    "gamma-Butyrolactone (GBL) : 18 : 16.6 : 7.4 : 77 : 3",
    "Heptane : 15.3 : 0 : 0 : 147 : 390",
    "Hexane : 14.9 : 0 : 0 : 131 : 830",
    "Iso-Butanol : 15.1 : 5.7 : 15.9 : 93 : 62",
    "Iso-Propyl Acetate : 14.9 : 4.5 : 8.2 : 117 : 350",
    "Isophorone : 17 : 8 : 5 : 150 : 2",
    "d-Limonene : 17.2 : 1.8 : 4.3 : 163 : 25",
    "Methanol : 14.7 : 12.3 : 22.3 : 41 : 300",
    "Methyl Acetate : 15.5 : 7.2 : 7.6 : 80 : 1180",
    "Methyl Ethyl Ketone (MEK) : 16 : 9 : 5.1 : 90 : 380",
    "N-Methyl-2-Pyrrolidone (NMP) : 18 : 12.3 : 7.2 : 97 : 3",
    "Methylene Chloride : 17 : 7.3 : 7.1 : 64 : 140",
    "1-Nitropropane : 16.6 : 12.3 : 5.5 : 90 : 100",
    "1-Propanol : 16 : 6.8 : 17.4 : 75 : 86",
    "2-Propanol : 15.8 : 6.1 : 16.4 : 77 : 150",
    "n-Propyl Acetate : 15.3 : 4.3 : 7.6",
    "Propylene Carbonate : 20 : 18 : 4.1 : 85 : 0.5",
    "PG Monobutyl Ether : 15.3 : 4.5 : 9.2 : 132 : 7",
    "PG Monoethyl Ether Acetate : 15.6 : 6.3 : 7.7 : 155 : 34",
    "PG Monomethyl Ether : 15.6 : 6.3 : 11.6 : 98 : 70",
    "PG Monomethyl Ether Acetate : 15.6 : 5.6 : 9.8 : 137 : 33",
    "Tetrahydrofuran (THF) : 16.8 : 5.7 : 8 : 82 : 472",
    "Toluene : 18 : 1.4 : 2 : 107 : 190",
    "Water : 15.5 : 16 : 42.3 : 18 : 80",
    "Xylene : 17.6 : 1 : 3.1 : 124 : 80",
]
let SolvChoice = "XXX", PolyChoice = "XXX"
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults
    if (document.getElementById('ISO').checked) { populateComboBox(SolventArray); SolvChoice = "Solvents" } else { populateComboBox(ISOArray); SolvChoice = "ISO" }
    ;
    Main();
};

// Function to populate the combobox
function populateComboBox(array) {
    // Get the combobox element by its ID
    let comboBox = document.getElementById('Solvent');

    // Clear existing options
    comboBox.innerHTML = '';

    // Iterate over the array and create an option element for each value
    array.forEach(value => {
        let option = document.createElement('option');
        option.value = value;
        option.textContent = value;
        comboBox.appendChild(option);
    });
}

//Any global variables go here
let amUpdating = false, lastFit = "", newFit = false, lastQ = 0, lastUnits = "", lastProbe = "", lastT = 0, divBy = 1, ModelLabel = "Model Parameters", G22conv = 1
let fitData = [], naFitData = [], anFitData = [], scaleData = [], Loading = false, xmin = 0, xmax = 1

//Main is hard wired as THE place to start calculating when input changes
//It does no calculations itself, it merely sets them up, sends off variables, gets results and, if necessary, plots them.

function Main() {
    saveSettings();
    if (Loading) return
    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!
    const inputs = {
        MWt: document.getElementById('SlideMWt').value,
        T: sliders.SlideT.value + 273, //to K
        L: sliders.SlideL.value / 10, //mm to cm
        tmax: sliders.Slidetmax.value,
        V: sliders.SlideV.value,
        K: sliders.SlideK.value,
        Integrated: document.getElementById('Integrated').checked,
        ISO: document.getElementById('ISO').checked,
        Polymer: document.getElementById('Polymer').value,
        Solvent: document.getElementById('Solvent').value,
    }

    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    console.time("Calc")

    const result = CalcIt(inputs)


    //Set all the text box outputs
    document.getElementById('Values').value = result.Values

    //Do all relevant plots by calling plotIt - if there's no plot, nothing happens
    //plotIt is part of the app infrastructure in app.new.js
    if (result.plots) {
        for (let i = 0; i < result.plots.length; i++) {
            plotIt(result.plots[i], result.canvas[i]);
        }
    }
    console.timeEnd("Calc")
    //You might have some other stuff to do here, but for most apps that's it for CalcIt!
}

//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({ MWt, T, L, V, K, Polymer, Solvent, tmax, Integrated, ISO }) {
    let DoSort = false
    if (SolvChoice != "Solvents" && !ISO) {
        SolvChoice = "Solvents"
        DoSort = true
    } else {
        if (SolvChoice != "ISO" && ISO) {
            SolvChoice = "ISO"
            DoSort = true
        }
    }
    const Pvals = Polymer.split(" : "), PType = Pvals[1], EAT = 10454 / T
    const dD = parseFloat(Pvals[3]), dP = parseFloat(Pvals[4]), dH = parseFloat(Pvals[5])
    if (Pvals[0] != PolyChoice) {
        PolyChoice = Pvals[0]
        DoSort = true
    }
    if (DoSort) {
        let SortArray = SolvChoice == "ISO" ? [...ISOArray] : [...SolventArray], DistArray = [], i = 0, j = 0, Sval = [], dDs, dPs, dHs, Dist, tmp
        for (i = 0; i < SortArray.length; i++) {
            Sval = SortArray[i].split(" : ")
            dDs = parseFloat(Sval[1]), dPs = parseFloat(Sval[2]), dHs = parseFloat(Sval[3])
            Dist = Math.sqrt(4 * Math.pow(dD - dDs, 2) + Math.pow(dP - dPs, 2) + Math.pow(dH - dHs, 2))
            DistArray.push(Dist)
        }
        //Crude sort, it works well-enough, fast-enough
        for (i = 0; i < DistArray.length; i++) {
            for (j = 0; j < DistArray.length - 1; j++) {
                if (DistArray[j] > DistArray[j + 1]) {
                    tmp = SortArray[j]; SortArray[j] = SortArray[j + 1]; SortArray[j + 1] = tmp
                    tmp = DistArray[j]; DistArray[j] = DistArray[j + 1]; DistArray[j + 1] = tmp
                }
            }
        }
        for (i = 0; i < SortArray.length; i++) {
            SortArray[i] += " : Ra = " + DistArray[i].toFixed(1)
        }
        populateComboBox(SortArray);
        Solvent = SortArray[0]
    }
    let Values = "-"
    let Dplb = [], Dpub = [], MWtp = [], Upper = [], Lower = [], UpperN = [], LowerN = [], m = 2, Dub = 1, Dlb = 1, minD = 1, maxD = 1e-99, Dvlb = 1, Dvub = 1
    const Sval = Solvent.split(" : ")
    const dDs = parseFloat(Sval[1]), dPs = parseFloat(Sval[2]), dHs = parseFloat(Sval[3])
    const Dist = Math.sqrt(4 * Math.pow(dD - dDs, 2) + Math.pow(dP - dPs, 2) + Math.pow(dH - dHs, 2))
    //These are dummy values for development purposes only
    let Sw = "Lo", MedVal = 8, HiVal = 5
    if (PType.startsWith("P")) { MedVal = 6; HiVal = 4 }
    if (PType.startsWith("G")) { MedVal = 5; HiVal = 3 }
    if (Dist < MedVal) Sw = "Med"
    if (Dist < HiVal) Sw = "Hi"
    let ConcDiff = 1
    if (Sw == "Med") ConcDiff = 10
    if (Sw == "Hi") ConcDiff = 100
    const Gcorr = Math.exp(-EAT) / Math.exp(-10475 / (310))
    let blo = -2 / 3, bhi = -5.2, aublo = 8.5e-6, aubhi = 1.5e-2, alblo = 1.2e-9, albhi = 1.5e-6
    if (PType.startsWith("G")) {
        if (PType == "G1") {
            bhi = -3.2; aublo = 1.8e-6; aubhi = 1.1e-4; alblo = 2.4e-9; albhi = 1.3e-8
        }
    } else {
        aublo = 1; aubhi = 17.4; alblo = 14; albhi = 10.3
        if (PType == "R2") { aublo = 14.5; aubhi = 14.2; alblo = 10.5; albhi = 10 }
        if (PType == "P1") { aublo = 12.9; aubhi = 12.9; alblo = 7; albhi = 6.8 }
        if (PType == "P2") { aublo = 12.2; aubhi = 9.9; alblo = 6.3; albhi = 3.1 }
        if (PType == "P3") { aublo = 13.5; aubhi = 8.2; alblo = 6; albhi = 0.4 }
        if (PType == "P4") { aublo = 12.1; aubhi = 7.2; alblo = 5; albhi = -6.2 }
    }
    for (m = 1; m <= 1500; m++) {
        if (PType.startsWith("G")) {
            if (m <= 50) {
                Dub = aublo * Math.pow(m, blo)
                Dlb = alblo * Math.pow(m, blo)
            } else {
                Dub = aubhi * Math.pow(m, bhi)
                Dlb = albhi * Math.pow(m, bhi)
            }
            Dub *= Gcorr; Dlb *= Gcorr //Unofficial T correction
        } else {
            if (m <= 50) {
                Dub = Math.exp(aublo - 0.1351 * Math.pow(m, 0.6667) + 0.003 * m - EAT)
                Dlb = Math.exp(alblo - 0.1351 * Math.pow(m, 0.6667) + 0.003 * m - EAT)
            } else {
                Dub = Math.exp(aubhi - 0.1351 * Math.pow(m, 0.6667) + 0.003 * m - EAT)
                Dlb = Math.exp(albhi - 0.1351 * Math.pow(m, 0.6667) + 0.003 * m - EAT)
            }
        }
        if (PType == "R1" && m < 200) Dub = 1.5e-9 * Math.pow(200 / m, 0.2)
        Dub *= 1e4 * ConcDiff; Dlb *= 1e4 * ConcDiff //to cm2/s
        //D in liquids are never lower than 1e-5 so let's not allow very low values
        Dub = Math.min(Dub, 1e-5); Dlb = Math.min(Dlb, 1e-5)
        Dplb.push({ x: m, y: Dlb })
        Dpub.push({ x: m, y: Dub })
        minD = Math.min(minD, Dlb)
        maxD = Math.max(maxD, Dub)
        if (m < MWt) { Dvlb = Dlb; Dvub = Dub }
    }

    MWtp.push({ x: MWt, y: minD })
    MWtp.push({ x: MWt, y: maxD })
    Values = "Dub = " + Dvub.toExponential(1) + " Dlb = " + Dvlb.toExponential(1) + " cm²/s" + " Ra = " + Dist.toFixed(1) + " Swellability = " + Sw
    //Now the Extraction plot
    let t = 1
    let Uniform = []
    if (Integrated) {
        Uniform.push({ x: 1, y: 100 / tmax }); Uniform.push({ x: tmax, y: 100 })
    } else {
        Uniform.push({ x: 1, y: 1 / tmax }); Uniform.push({ x: tmax, y: 1 / tmax })
    }
    const dtos = 24 * 3600, tauu = Dvub / (L * L) * dtos, taul = Dvlb / (L * L) * dtos, PI = Math.PI, PI2 = PI * PI
    const alpha=V*K //Piringer inverts K so his alpha is V/K
    const amult =(V >4.9 && K > 4.9)?1:alpha/(1+alpha)
    const a2=alpha*alpha
    //
    //get the qn terms
    let qn=[],pren=[]
    qn.push(0);pren.push(0) //We don't use [0]
    const terms=50
    let guess=1
    for (let n=1; n<=terms;n++){
        if (alpha<0.1){guess=0.1+n*Math.PI/(1+alpha)} else {guess=0.1+Math.PI*(n-alpha/(2*(1+alpha)))}
        let sign=Math.sign(Math.tan(guess)+alpha*guess), count=0
        while ((Math.sign(Math.tan(guess)+alpha*guess) == sign) && (count < 10000)){
            guess-=0.001
            count++
        }
        qn.push(guess*guess) //We use qn^2
        pren.push(2*alpha*(1+alpha)/(1+alpha+a2*guess*guess))
    }

    let tau = 1, mt = 0, mtlast = 0, mtl = 0, mtllast = 0, sum=0,f=0
    let tmin = 1; if (tmax <= 10) tmin = 0.1
    //if (alpha > 10) alphal=10
    for (t = tmin; t <= tmax; t += tmin) {
        tau = taul * t
        if (tau < 0.001){
            let z=Math.sqrt(tau)/alpha
            mtl=(1+alpha)*(1-Math.exp(z*z)*(1-erf(z)))
        } else {
            sum=0
            for (let n=1;n<=terms; n++){
                f=pren[n]*Math.exp(-qn[n]*tau)
                if (f<1e-30) break
                sum+=f
            }
             mtl=(1-sum)
        }
        mtl*=amult
        if (Integrated) { Lower.push({ x: t, y: (100 * mtl) }) } else { Lower.push({ x: t, y: Math.max((mtl - mtllast),1e-10) }) }
        //Upper
        tau = tauu * t
        if (tau < 0.01){
            let z=Math.sqrt(tau)/alpha
            mt=(1+alpha)*(1-Math.exp(z*z)*(1-erf(z)))
        } else {
            sum=0
            for (let n=1;n<=terms; n++){
                f=pren[n]*Math.exp(-qn[n]*tau)
                if (f<1e-20) break
                sum+=f
            }
             mt=(1-sum)
        }
        mt*=amult

        if (Integrated) { Upper.push({ x: t, y: (100 * mt) }) } else { if (mt - mtlast > mtl - mtllast) { Upper.push({ x: t, y: (mt - mtlast) }) } else { Upper.push({ x: t, y: Math.max( (mtl - mtllast),1e-10) }) } }
        mtllast = mtl
        mtlast = mt
    }
    //Get rid of glitches at transition from tau 0.2
    for (let i = 2; i < Upper.length - 1; i++) {
        if (Upper[i].y > Upper[i - 1].y) Upper[i-1].y = 0.5 * (Upper[i - 2].y + Upper[i ].y)
        if (Lower[i].y > Lower[i - 1].y) Lower[i-1].y = 0.5 * (Lower[i - 2].y + Lower[i ].y)
    }
    //Now set up all the graphing data detail by detail.
    const prmap = {
        plotData: [Dplb, Dpub, MWtp], //An array of 1 or more datasets
        lineLabels: ["LB", "UB", "MWt"], //An array of labels for each dataset
        dottedLine: [false, false, true],
        borderWidth: [3, 3, 1],
        colors: ['#edc240', '#afd8ff', "gray"],
        xLabel: "MWt& ", //Label for the x axis, with an & to separate the units
        yLabel: "D&cm²/s", //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: true, //Is the x-axis in log form?
        // xTicks: function (value, index, ticks) {console.log(value, index); if (value == 1 || value == "50" || value == "100" || value == 500 || value == 1000) { return value } else { return "" }; }, //We can define a tick function if we're being fancy
        xTicks: function (value, index, ticks) { if (index % 9 === 0) { return value } else { return "" }; }, //We can define a tick function if we're being fancy
        logY: true, //Is the y-axis in log form?
        yTicks: function (value, index, ticks) { if (value.toExponential(0).startsWith("1") || value.toExponential(0).startsWith("5")) { return value.toExponential(0) } }, //We can define a tick function if we're being fancy
        legendPosition: 'top', //Where we want the legend - top, bottom, left, right
        xMinMax: [, 9000], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        yMinMax: [,], //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: 'F0', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: 'E1', //F for Fixed, P for Precision, E for exponential
    };
    const Release = Integrated ? "Release&%" : "Release Rate (Relative)&/day"
    const ySigFigs = Integrated ? 'P3' : 'E1'
    const plotData=[Lower, Upper, Uniform]
    const prmap1 = {
        plotData: plotData, //An array of 1 or more datasets
        lineLabels: ["LB", "UB", "Uni.", "LB-Num.", "UB-Num."], //An array of labels for each dataset
        colors: ['#edc240', '#afd8ff', "gray", "magenta", "green"],
        dottedLine: [false, false, true, false, false],
        borderWidth: [3, 3, 1, 4, 4],
        xLabel: "t&days", //Label for the x axis, with an & to separate the units
        yLabel: Release, //Label for the y axis, with an & to separate the units
        y2Label: undefined, //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: true, //Is the y-axis in log form?
        // yTicks: function (value, index, ticks) { return value.toExponential(0) }, //We can define a tick function if we're being fancy
        yTicks: function (value, index, ticks) { if (value.toExponential(0).startsWith("1")) { return value.toExponential(0) } }, //We can define a tick function if we're being fancy
        legendPosition: 'top', //Where we want the legend - top, bottom, left, right
        xMinMax: [,], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        yMinMax: [,], //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: 'F0', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: ySigFigs, //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 {
        Values: Values,
        plots: [prmap, prmap1],
        canvas: ['canvas', 'canvas1'],
    };

}
function erf(x) {
    // constants
    var a1 =  0.254829592;
    var a2 = -0.284496736;
    var a3 =  1.421413741;
    var a4 = -1.453152027;
    var a5 =  1.061405429;
    var p  =  0.3275911;

    // Save the sign of x
    var sign = 1;
    if (x < 0) {
        sign = -1;
    }
    x = Math.abs(x);

    // A&S formula 7.1.26
    var t = 1.0/(1.0 + p*x);
    var y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*Math.exp(-x*x);

    return sign*y;
}            

Extractables and Leachables

Assuming, for the moment, that any extractable molecule in a polymer can fully dissolve in the extraction medium, the amount we can extract per unit time depends on the size of the object and the diffusion coefficient, D, of the molecule. Given that it is impossible for us to measure every D value for every molecule in every polymer, we need reliable estimates from two givens: the MWt of the molecule and the type of polymer it's trapped in.

The paper gives a list of common polymers sorted into types: Rubbers R1, R2; Plastics P1, P2, P3, P4 and Glasses G1, G2. For each of these 8 categories, a set of parameters is listed from which D can be calculated for any molecule/polymer pair. For everything other than the glasses, a Piringer-style equation is used, where `E_A` is an activation energy, R is the gas constant and T is the temperature. It is assumed that `E_A/R` is a constant = 10454 K:

`D=exp[A-0.1351"MW"_t^(2/3)+0.003MW_t-E_A/(RT)]`

For the glassy polymers a power law is used. From abundance of caution the authors provided no T dependence, but the app has used the same Arrhenius term as in the plastics to give you a sense of the effects. To quote any regulatory value, use the 37°C value:

`D=α"MW"_t^β`

The parameters A, α and β are themselves split into 4 categories. First there is an Upper and Lower bound of diffusion coefficient. Second, there is a significant jump in D for MWt <e 50 (due to a common 0.4nm channel size in all polymers).

The app simply finds the appropriate parameters from the class of your chosen polymer and plots the upper and lower diffusion coefficient curves. A dotted line appears at your selected MWt for visual checking.

Release Rate

From a simple equation described in the paper you can calculate M(t)/M0, the ratio of the total amount released after time t to the original mass `M_0`. The rate of loss from a sheet with half-thickness L depends on the dimensionless parameter τ given by: `τ=(Dt)/L^2`

If you subtract the previous day's value from next, you get the daily release rate. This is plotted for both the upper and lower bound cases. Obviously the rate for different geometries will be different. For example, if extraction was from one side of this sample then you would need to enter the full thickness. For highly swollen polymers (see below) the upper curve values are limited.

Released % and effect of V & K

Selecting the "Integrated" option lets you see what % of your molecule is released over time. This might seem unhelpful if you don't know how much is present originally. But if you have an absolute measured extracted value after time t then the relative amount at a longer time can be found. Just read the % value at t then the value at your new time and use the ratio to find your true value.

The equations in the original paper assume "infinite sink" - all the molecules that leave the polymer disappear into an infinite volume. But when V, the ratio of Liquid/Solid approaches 1 then the build up of concentration can become significant and release reaches a lower equilibrium value. If the partition coefficient, K, between Liquid/Solid becomes small (e.g. a hydrophobic molecule from a hydrophobic polymer into water) then the equilibrium release is even lower.

Following Piringer's detailed explanation2, V and K can be combined into a value α which feeds into a couple of complex equations using tricky coefficients and "error functions" (see the code if you are interested). These equations reduce, of course, to those of the original paper and for situations involving small diffusion coefficients, there is no difference between the values. For numerical reasons, V & K can only be set to a maximum of 5, which would lead to a maximum release of 96%. When both are set to 5, this value is boosted to 100%.

The impact of errors in estimates of D

The errors in estimates arise both from necessary simplifications and from the absence of extensive datasets of carefully-measured values across a wide range of polymers and molecules. However, there is one piece of good news:

The % extracted depends on `sqrt(D)` so an error of a factor of 10 in D translates to a factor ~3 error in extracted amount.

Although we would prefer lower levels of uncertainty in D, the `sqrt(D)` effect gives us that lower level of uncertainty in the % extracted.

HSP and Swelling

The Elder & Saylor D values assume no swelling of the polymer. In this app you get to choose the extraction solvent and via the well-known theory of Hansen Solubility parameters the "Distance", Ra, between the polymer and solvent is calculated. This is calculated from the differences between the values of the Dispersive interactions (van der Waals), δD, Polar parameters, δP and Hydrogen-bonding parameters, δH. Values of these parameters for the polymers (p) and solvents (s) are shown in the combo boxes. The equation for Ra is:

`Ra^2=4(δD_p-δD_s)^2+(δP_p-δP_s)^2+(δH_p-δH_s)^2`

If Ra is very small the solvent is very compatible with the polymer, so swelling is "Hi" and D increases 100x. If Ra is intermediate, swelling is "Med" and D increases 10x. The solvents are automatically sorted from small to high Ra so you can easily choose aggressive, moderate or non-aggressive solvents for your calculations.

But "very small" and "small" depend on the polymer. For G1/2 the solvent has to be very similar in order to swell; for R1/2 you can get a lot of swelling at a larger Ra.

This is a development version and the thresholds are a working estimate to be refined with more data and analysis.

You can choose between two lists of solvents - a general one and an ISO 10993 one. Note that although ISO 10993 distinguishes between "polar", "semi-polar" and "non-polar" it is more useful to use HSP's Ra because the strength of solvent interaction depends on relative values of all three of the δD, δP and δH values.

For small molecules in highly swollen polymers, D cannot be higher than a typical liquid value of 10-5cm²/s. In such cases, to avoid graphing artefacts, in the release-rate plot the upper line is artificially limited to being slightly above the lower. There is no need for adjustments in the Integrated plot.

Acknowledgement

The app was created thanks to the encouragement from E&L experts Dr Jianwei Li, Dr Alicja Sobańtka and Trent Wells. Responsibility for errors is mine.

1

Robert M. Elder, David M. Saylor, Robust estimates of solute diffusivity in polymers for predicting patient exposure to medical device leachables, J Polym Sci. 2023;1–18 https://doi.org/10.1002/pol.20230219

2

Otto Piringer and Titus Beu, Chapter 7 of Plastic Packaging edited by Otto G. Piringer and Albert L. Baner