Polymer RI Calculator

Quick Start

It is surprisingly easy to measure the refractive index of a polymer film - you just have to make sure its surface is rough enough! In your UV spectrometer at a convenient wavelength such as 589nm, measure the %T as the sample sits in liquids of 4-6 different RIs. A fit to a Lorentz curve gives you the RI of the polymer.


This app uses the technique devised by the Caseri group at ETH in Switzerland, reference below1, including a link to download their excellent paper.

Polymer RI

//One universal basic required here to get things going once loaded
let DoIt = true
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults

//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

    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!
    const inputs = {
        RI1: sliders.SlideRI1.value,
        T1: sliders.SlideT1.value,
        RI2: sliders.SlideRI2.value,
        T2: sliders.SlideT2.value,
        RI3: sliders.SlideRI3.value,
        T3: sliders.SlideT3.value,
        RI4: sliders.SlideRI4.value,
        T4: sliders.SlideT4.value,
        RI5: sliders.SlideRI5.value,
        T5: sliders.SlideT5.value,
        RI6: sliders.SlideRI6.value,
        T6: sliders.SlideT6.value,

    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    const result = CalcIt(inputs);

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

    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!

//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({ RI1, T1, RI2, T2, RI3, T3, RI4, T4, RI5, T5, RI6, T6 }) {
    let Texp = [], Tcalc = [], RI="Not calculated"
    let x = [], y = [], i = 0
    if (T1>0) {x.push(parseFloat(RI1));y.push(parseFloat(T1))}
    if (T2>0) {x.push(parseFloat(RI2));y.push(parseFloat(T2))}
    if (T3>0) {x.push(parseFloat(RI3));y.push(parseFloat(T3))}
    if (T4>0) {x.push(parseFloat(RI4));y.push(parseFloat(T4))}
    if (T5>0) {x.push(parseFloat(RI5));y.push(parseFloat(T5))}
    if (T6>0) {x.push(parseFloat(RI6));y.push(parseFloat(T6))}
    for (i = 0; i < x.length; i++) {
         Texp.push({ x: x[i], y: y[i] })
    if (x.length>3) {
    PFunc = function (x, P) {
        return x.map(function (xi) {
            return P[0] * P[1] / (4 * Math.pow(xi - P[2], 2) + P[1] * P[1])
    var Parms = fminsearch(PFunc, [10, 0.2, 1.4], x, y, { maxIter: 1000 })
    for (i = 1.3; i <= 1.7; i += 0.01) {
        Tcalc.push({ x: i, y: Parms[0] * Parms[1] / (4 * Math.pow(i - Parms[2], 2) + Parms[1] * Parms[1]) })
    RI = Parms[2].toFixed(3)
    //Now set up all the graphing data.
    //We use the amazing Open Source Chart.js, https://www.chartjs.org/
    //A lot of the sophistication is addressed directly here
    //But if you need something more, read the Chart.js documentation or search Stack Overflow
    const plotData = [Texp, Tcalc]
    const lineLabels = ["%Texp", "%Tcalc"]
    const myColors = ["blue", "orange"]
    const showLines = [false, true]; showPoints = [2, 0]
    //Now set up all the graphing data detail by detail.
    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
        hideLegend: false,
        showLines: showLines,
        showPoints: showPoints,
        borderWidth: [2],
        xLabel: 'RI&', //Label for the x axis, with an & to separate the units
        yLabel: '%T&', //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,
        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: [,], //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: 'P3', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: 'P3', //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'],
        RI: RI,
function clearOldName() { //This is needed because otherwise you can't re-load the same file name!
    document.getElementById('Load').value = ""
function handleFileSelect(evt) {
    var f = evt.target.files[0];

    if (f) {
        var r = new FileReader();
        r.onload = function (e) {
            var data = e.target.result;
    } else {

//Load data from a chosen file
function LoadData(S) {
    Papa.parse(S, {
        download: false,
        header: true,
        skipEmptyLines: true,
        complete: papaCompleteFn,
        error: papaErrorFn
function papaErrorFn(error, file) {
    console.log("Error:", error, file)
function papaCompleteFn() {
    var pData = arguments[0]
    fitData = []; scaleData = []; logData = []; lastFit = "", newFit = true
    if (pData.data.length < 3) return //Not a valid data file
    let maxY = 0, lastx = -1, xval = 0
    theData = []
    for (i = 0; i < pData.data.length; i++) {
        theRow = pData.data[i]
        if (theRow.t && theRow.Temp) {
            xval = parseFloat(theRow.t)
            yval = parseFloat(theRow.Temp)
            theData.push([xval, yval])
    if (theData.length < 5) {
        alert("There was something wrong with the data file")
        return //Invalid data
//The fitting routine
fminsearch = function (fun, Parm0, x, y, Opt) {
    if (!Opt) {
        Opt = {}
    if (!Opt.maxIter) {
        Opt.maxIter = 1000
    if (!Opt.step) { // initial step is 1/100 of initial value (remember not to use zero in Parm0)
        Opt.step = Parm0.map(function (p) {
            return p / 100
        Opt.step = Opt.step.map(function (si) {
            if (si == 0) {
                return 1
            } else {
                return si
        }); // convert null steps into 1's
    if (!Opt.objFun) {
        Opt.objFun = function (y, yp) {
            return y.map(function (yi, i) {
                return Math.pow((yi - yp[i]), 2)
            }).reduce(function (a, b) {
                return a + b
    } //SSD

    var cloneVector = function (V) {
        return V.map(function (v) {
            return v
    var P0 = cloneVector(Parm0),
        P1 = cloneVector(Parm0);
    var n = P0.length;
    var step = Opt.step;
    var funParm = function (P) {
        return Opt.objFun(y, fun(x, P))
    } //function (of Parameters) to minimize multi-univariate screening
    let lastfP = 1e-19, newfP = 0
    for (var i = 0; i < Opt.maxIter; i++) {
        for (var j = 0; j < n; j++) { // take a step for each parameter
            P1 = cloneVector(P0);
            P1[j] += step[j];
            if (funParm(P1) < funParm(P0)) { // if parm value going in the righ direction
                step[j] = 1.2 * step[j]; // then go a little faster
                P0 = cloneVector(P1);
            } else {
                step[j] = -(0.5 * step[j]); // otherwiese reverse and go slower
        //Stop trying too hard
        newfP = funParm(P0)
        if (i > 1000 && newfP < lastfP && Math.abs((newfP - lastfP) / lastfP) < 1e-10) break
        lastfP = newfP
    return P0

This ingenious technique is both simple and effective. We know that if you have a perfectly matched RI between any transparent object and a liquid then it will "disappear" when placed in the liquid. This is because we see transparent objects via the light reflected from their surface, and when RIs are matched, there is zero reflection.

While this is true, it's not helpful for what we want. If you have a set of liquids of known RIs and place the polymer film into them, the % transmisiion, %T, might vary by only a few %, making it hard to see the difference between a good and a very good match.

All this changes if you roughen the surface. Now you have reflection and scattering and even a modest RI mismatch leads to a lot of scattering and, therefore, a low %T

So, roughen your film sample, place it in a UV cell containing a liquid of known RI and record the %T value at a convenient wavelength, conventionally 589nm, the sodium D line used in old refractometers. Use some care to ensure the sample is properly oriented in the cell. Do this with a minimum of 4 solvents with RIs that span the value of the films. Using the sliders, enter the RI values (6 common solvents are already provided, see the list below) and the %T, using %T = 0 to indicate that you are not using that solvent.

If the RI of your polymer is nP then we can use a Lorenz curve (a Guassian or a polynomial work as well) containing two extra parameters A (area under the curve) and w (width of the curve) to fit the data to find our RI:


As we don't care about the A and w values, the app returns the fitted nP.

If you don't provide a minimum of 4 values, you see your experimental points and get a "Not calculated" value.

How rough should the sample be? Happily it doesn't much matter, but the more highly scattering the surface, the better. As shown in the Gloss app scattering needs a good mix of amplitude and high frequency, so you might find you need a bit of experimenting to get a good reduction in %T in a badly-matched liquid.

The only limitations to this technique are that:

  1. The film can't be too absorbent at the measuring wavelength (you then get complex RI values!)
  2. The film isn't soluble in your chosen solvents
  3. You chosen liquids span the RI of your polymer

Here is the list of solvents provided by the authors, plus two benzoates that I have found useful in the past:

  • Water, 1.333
  • Ethanol, 1.363
  • Isooctane, 1.391
  • Methylcyclohexane, 1.423
  • Hexadecane, 1.435
  • Cyclohexanone, 1.45
  • Trans-Decalin, 1.469
  • Toluene, 1.497
  • Butyl benzoate, 1.498
  • 1,2-dichlorobenzene, 1.552
  • Dibenzylether, 1.562
  • Benzyl benzoate, 1.569
  • 1,2,4-trichlorobenzene, 1.571
  • 1-methylnaphthalene, 1.614

The paper, link below, provides useful tips and tricks as well as a deeper discussion about the accuracy of this technique. Reading it is highly recommended!

1RJ Nussbaumer, M Halter, T Tervoort, WR Caseri, P Smith, A simple method for the determination of refractive indices of (rough) transparent solids, J. Mat Sci 40 (2005) 575-582. Available from the ETH website