Heat Flow

Quick Start

You have a hot surface put into contact with a stack of one or more materials - plastics (packaging), bricks (insulation), rocks (large-scale heat flows). How do their temperatures change over time? It depends on a bunch of parameters, which are carefully explained. Don't panic at the opening graphic - it all makes sense.

Credits

I've written so many heat flow apps for specific set-ups that it's nice to make a general-purpose app that covers most things I've encountered.

Heat Flow

h1
D1 10-7 m2/s
h2
D2 10-7 m2/s
h3
D3 10-7 m2/s
h4
D4 10-7 m2/s
t
TStart
TTop
TBelow 0=Float
hStep
tStep
K W/m.K
Cp J/kg
ρ kg/m³
DCalc
//One universal basic required here to get things going once loaded
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults
    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 = {
        h1: sliders.Slideh1.value, //All calculations assume layers in microns
        D1: sliders.SlideD1.value * 1e-7, //to correct units
        h2: sliders.Slideh2.value, //All calculations assume layers in microns
        D2: sliders.SlideD2.value * 1e-7, //to correct units
        h3: sliders.Slideh3.value, //All calculations assume layers in microns
        D3: sliders.SlideD3.value * 1e-7, //to correct units
        h4: sliders.Slideh4.value, //All calculations assume layers in microns
        D4: sliders.SlideD4.value * 1e-7, //to correct units
        tMax: sliders.SlidetMax.value, //Assume seconds
        TStart: sliders.SlideTStart.value, //Assumed to be degC
        TTop: sliders.SlideTTop.value, //Assumed to be degC
        TBelow: sliders.SlideTBelow.value, //Assumed to be degC
        K: sliders.SlideK.value, 
        Cp: sliders.SlideCp.value, 
        rho: sliders.Sliderho.value, 
        hStep: sliders.SlidehStep.value,
        tStep: sliders.SlidetStep.value,
    };

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

    const result = CalcIt(inputs);

    //Set all the text box outputs
    document.getElementById('DCalc').value = result.DCalc;
    // document.getElementById('Solid').value = result.sInfo;
    //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("time")
    //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({ h1, D1, h2, D2, h3, D3, h4, D4, hStep, tStep, tMax, TStart, TTop, TBelow, K, Cp, rho }) {
    //Do the D calculation first
    const DCalc=K/(Cp*rho)

    const isFloating = (TBelow <1)
    let Thick = [] //Thickness
    let D = [] //Diffusivity
    if (h1 > 0) {
        Thick.push(h1)
        D.push(D1)
    }
    if (h2 > 0) {
        Thick.push(h2)
        D.push(D2)
    }
    if (h3 > 0) {
        Thick.push(h3)
        D.push(D3)
    }
    if (h4 > 0) {
        Thick.push(h4)
        D.push(D4)
    }

    let LThick = [], NLayers = 0, TotThick = 0, SumThick = [], LTC = [], LHC = []
    let plotData = [], lineLabels = [], myColors = [], borderWidth = [], dottedLine = []
    for (var i = 0; i < Thick.length; i++) {
        LThick[NLayers] = Thick[i]
        TotThick += Thick[i]
        SumThick[NLayers] = TotThick
        NLayers += 1
    };

    var GSteps = Math.floor(TotThick / hStep)
    var GD = [], GT = [], TmpT = [], ThisLayer = 0
    for (var i = 0; i < GSteps; i++) {
        if (i * hStep >= SumThick[ThisLayer]) { ThisLayer += 1 }
        GD[i] = D[ThisLayer]*1e18 //Convert from cubic microns
        GT[i] = TStart 
        TmpT[i] = GT[i]
    };

    GT[0] = TTop;
    GT[GSteps - 1] = isFloating?TStart:TBelow
    GT[GSteps] = isFloating?TStart:TBelow

    //Now do things in small steps and report them every 0.01s
    let tNow = 0, TShow = 0.01, TStep = 0.000001*tStep, TNext = TShow
    let JFact = 1e-6 / (hStep * hStep) //*TStep//Each slice is in microns but units are in m and convert from Watts to J

    while (tNow <= tMax) {
        for (let i = 1; i < GSteps - 2; i++) {
            TmpT[i] = GT[i] + (((GT[i - 1] - GT[i]) * GD[i] - (GT[i] - GT[i + 1]) * GD[i + 1]) * JFact * TStep)
        };
        i = GSteps - 2
        if (isFloating) {
            TmpT[i] = GT[i] + (((GT[i - 1] - GT[i]) * GD[i]) * JFact * TStep)
        } else {
            TmpT[i] = GT[i] + (((GT[i - 1] - GT[i]) * GD[i] - (GT[i] - GT[i + 1]) * GD[i + 1]) * JFact * TStep)
        }

        for (let i = 1; i < GSteps - 1; i++) {
            GT[i] = TmpT[i]
        };

        if (tNow >= TNext) {
            let tPts = []
            for (var i = 0; i < GSteps - 1; i++) {
                tPts.push({ x: GT[i], y: TotThick - i * hStep })
            };
            if (isFloating) {tPts.push({ x: GT[GSteps-2], y: 0 })} else {tPts.push({ x: GT[GSteps-1], y: 0 })}
            rbow = Rainbow(tNow / tMax); Col = "rgb(" + rbow.r + "," + rbow.g + "," + rbow.b + ")"
            plotData.push(tPts)
            lineLabels.push(tNow.toFixed(2) + "s")
            myColors.push(Col)
            borderWidth.push(2)
            dottedLine.push(false)
            TNext += TShow
        }
        tNow += TStep
    }//Big While

    //Dotted lines between layers
    for (i=0;i < NLayers-1;i++){
        yPos = TotThick-SumThick[i]
        makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TTop, hStep,  "Layer "+(i+1))
    }


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

//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
    dottedLine: dottedLine,
    borderWidth: borderWidth,
    hideLegend: true,
    xLabel: 'T&°C', //Label for the x axis, with an & to separate the units
    yLabel: 'z&μm', //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: [,], //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: 'F0', //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 {
    DCalc:DCalc.toExponential(3),
    plots: [prmap],
    canvas: ['canvas'],
};

function makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TTop, hStep, theLabel) {
    let tPts = []
    yPos+=hStep
    tPts.push({ x: 20, y: yPos }, { x: TTop, y: yPos })
    plotData.push(tPts)
    lineLabels.push(theLabel)
    myColors.push("rgba(2,100,100,0.5)")
    borderWidth.push(1)
    dottedLine.push(true)
}

}
var RB = [[0, 48, 245], [0, 52, 242], [0, 55, 238], [0, 59, 235], [3, 62, 231], [9, 66, 228], [14, 69, 225], [18, 72, 221], [20, 74, 218], [22, 77, 214], [23, 80, 211], [24, 82, 207], [25, 85, 204], [25, 87, 200], [25, 90, 197], [25, 92, 193], [25, 94, 190], [25, 96, 187], [24, 99, 183], [24, 101, 180], [24, 103, 177], [23, 105, 173], [23, 106, 170], [24, 108, 167], [24, 110, 164], [25, 112, 160], [27, 113, 157], [28, 115, 154], [30, 117, 151], [32, 118, 148], [34, 120, 145], [36, 121, 142], [39, 122, 139], [41, 124, 136], [43, 125, 133], [45, 126, 130], [47, 128, 127], [49, 129, 124], [51, 130, 121], [53, 132, 118], [54, 133, 115], [56, 134, 112], [57, 136, 109], [58, 137, 106], [59, 138, 103], [60, 139, 99], [61, 141, 96], [62, 142, 93], [62, 143, 90], [63, 145, 87], [63, 146, 83], [64, 147, 80], [64, 149, 77], [64, 150, 74], [65, 151, 70], [65, 153, 67], [65, 154, 63], [65, 155, 60], [66, 156, 56], [66, 158, 53], [67, 159, 50], [68, 160, 46], [69, 161, 43], [70, 162, 40], [71, 163, 37], [73, 164, 34], [75, 165, 31], [77, 166, 28], [79, 167, 26], [82, 168, 24], [84, 169, 22], [87, 170, 20], [90, 171, 19], [93, 172, 18], [96, 173, 17], [99, 173, 17], [102, 174, 16], [105, 175, 16], [108, 176, 16], [111, 176, 16], [114, 177, 17], [117, 178, 17], [121, 179, 17], [124, 179, 18], [127, 180, 18], [130, 181, 19], [132, 182, 19], [135, 182, 20], [138, 183, 20], [141, 184, 20], [144, 184, 21], [147, 185, 21], [150, 186, 22], [153, 186, 22], [155, 187, 23], [158, 188, 23], [161, 188, 24], [164, 189, 24], [166, 190, 25], [169, 190, 25], [172, 191, 25], [175, 192, 26], [177, 192, 26], [180, 193, 27], [183, 194, 27], [186, 194, 28], [188, 195, 28], [191, 195, 29], [194, 196, 29], [196, 197, 30], [199, 197, 30], [202, 198, 30], [204, 199, 31], [207, 199, 31], [210, 200, 32], [212, 200, 32], [215, 201, 33], [217, 201, 33], [220, 202, 34], [223, 202, 34], [225, 202, 34], [227, 203, 35], [230, 203, 35], [232, 203, 35], [234, 203, 36], [236, 203, 36], [238, 203, 36], [240, 203, 36], [241, 202, 36], [243, 202, 36], [244, 201, 36], [245, 200, 36], [246, 200, 36], [247, 199, 36], [248, 197, 36], [248, 196, 36], [249, 195, 36], [249, 194, 35], [249, 192, 35], [250, 191, 35], [250, 190, 35], [250, 188, 34], [250, 187, 34], [250, 185, 34], [250, 184, 33], [250, 182, 33], [250, 180, 33], [250, 179, 32], [249, 177, 32], [249, 176, 32], [249, 174, 31], [249, 173, 31], [249, 171, 31], [249, 169, 30], [249, 168, 30], [249, 166, 30], [248, 165, 29], [248, 163, 29], [248, 161, 29], [248, 160, 29], [248, 158, 28], [248, 157, 28], [248, 155, 28], [247, 153, 27], [247, 152, 27], [247, 150, 27], [247, 148, 26], [247, 147, 26], [246, 145, 26], [246, 143, 26], [246, 142, 25], [246, 140, 25], [246, 138, 25], [245, 137, 24], [245, 135, 24], [245, 133, 24], [245, 132, 24], [244, 130, 23], [244, 128, 23], [244, 127, 23], [244, 125, 23], [244, 123, 22], [243, 121, 22], [243, 119, 22], [243, 118, 22], [243, 116, 21], [242, 114, 21], [242, 112, 21], [242, 110, 21], [241, 109, 21], [241, 107, 21], [241, 105, 21], [241, 103, 21], [240, 101, 21], [240, 100, 22], [240, 98, 22], [240, 96, 23], [240, 95, 24], [240, 93, 26], [240, 92, 27], [240, 90, 29], [240, 89, 31], [240, 88, 33], [240, 87, 36], [240, 87, 38], [241, 86, 41], [241, 86, 44], [242, 86, 47], [242, 86, 51], [243, 86, 54], [243, 87, 58], [244, 88, 62], [245, 88, 65], [245, 89, 69], [246, 90, 73], [247, 91, 77], [247, 92, 82], [248, 94, 86], [249, 95, 90], [249, 96, 94], [250, 97, 98], [251, 99, 102], [251, 100, 106], [252, 101, 111], [252, 103, 115], [253, 104, 119], [253, 105, 123], [254, 107, 128], [254, 108, 132], [255, 109, 136], [255, 111, 140], [255, 112, 145], [255, 114, 149], [255, 115, 153], [255, 116, 157], [255, 118, 162], [255, 119, 166], [255, 120, 170], [255, 122, 175], [255, 123, 179], [255, 125, 183], [255, 126, 188], [255, 127, 192], [255, 129, 196], [255, 130, 201], [255, 132, 205], [255, 133, 210], [255, 134, 214], [255, 136, 219], [255, 137, 223], [255, 139, 227], [255, 140, 232], [255, 141, 236], [254, 143, 241], [254, 144, 245], [253, 146, 250]]
function Rainbow(v) {
    var i = Math.floor((Math.min(v, 1), Math.max(v, 0)) * 255)
    r = RB[i][0]
    g = RB[i][1]
    b = RB[i][2]
    return { r: r, g: g, b: b }
}

            

You have a heat source at temperature TTop at the top of a stack of materials. At the other side of the stack you either have the temperature "floating" or it's connected to a heat source/sink at temperature TBelow. At time t=0 the stack material is all at Tstart. What we want to know is what the temperature is at any given point in the stack after any given time. And the graph (plus the mouse readout) tells you everything you need to know.

The Rainbow Graph

To see what's happening to temperature and time we have to plot 3 variables in a 2D graph. It turns out to be clearest to plot temperature on the X-axis, the stack itself on the Y-axis (with dotted lines showing the different layers) and to show the time evolution of temperature as a rainbow-coloured set of lines from blue at short times to red at long times. It takes a while to get used to it, but you will. And if you move your mouse you get a readout of everything.

Thermal Diffusivity

For each layer you have to enter the thickness (obviously) and the thermal diffusivity, D. Most of us have heard of thermal conductivity, K (J/m.K), so why use diffusivity? We are interested in how the temperature changes throughout the system. That depends, of course, on the heat flowing in via conductivity but it also depends on the head capacity Cp (J/kg.K) (heat needed to raise the temperature of 1kg by a degree) and, therefore, on the density ρ (kg/m³), the mass per unit volume. Fortunately, the thermal diffusivity, in m²/s combines all three parameters.

`D=K/(Cp.ρ)`

You can find a lot of values on Wikipedia's Thermal Diffusivity page

If you happen to know the thermal conductivity but not the diffusivity, use the calculator. If you don't know the heat capacity or density, enter 2000 and 1000 respectively. The calculated values are inconveniently small, hence the 10-7 adjustment implied when you enter the D values.

Where are the units?

For thickness and time no units are specified. In fact the underlying units in the calculation are μm and s. However, because the calculation is general, the thickness units could be in mm or m and the calculations remain correct provided you mentally adjust the time units. So if you have a structure defined in mm, you simply tell yourself that 1 thickness unit is 1mm and therefore if you calculate for 1 time unit, that's 1000s.

Why have I done it this way? Because any slider that went from 1μm to 1km or from 1ms to 1ks would be useless. The single app lets us explore heat flows through packaging films (10s of μms), heat flows through brick walls with insulation panels, and heat flows through kms of rocks.

What cannot be left unitless are the D values. A slider that covers everything from insulating aerogel to highly conductive silver is a bit impractical so there are some restrictions on what you can model.

Floating or not

If TBelow is set to 0 then it is assumed that the lowest surface makes no thermal contact, so the temperature can "float". In fact this can simulate something like heat sealing of a symmetrical structure - it's not that the middle of the heat seal is "floating", it's just that via symmetry we can calculate what's happening via this numerical trick. So you can simulate an 8-layer sealing process with a 4-layer model.

Thermal Contact Resistance

This model assumes that the top heat source is in perfect contact with the top layer and the heat can flow with infinite ease. In practice there is a thermal contact resistance from imperfections in contact. It can be modelled in the Contacts app. I could add a thermal resistance input, but that would make the interface even more complex!

Numerics

The calculations are numerical interations based on cutting the layers into virtual slices of thickness hStep and calculating what's happened after a short time interval of tStep. If you choose a larger hStep calculations are faster but you lose detail of smaller layers. If you go smaller in hStep the calculations can blow up (you see nothing in the graph or get funny lines). The fix is to reduce the tStep value, but then calculations are doubly slower. The default values of 5 are fine for many purposes but if you need very fine detail (small hStep) then you might need a small tStep. If you have a slow device then you need a larger hStep and a larger tStep, but again, too large in tStep and the calculation will blow up.