Heat Seal
Quick Start
Heat sealing is much used in the packaging industry. Here we can simulate a jaw heat seal system with (up to) 3 layers on each side. It takes into account thermal contact resistance and allows you to cool the seal afterwards.
Credits
This is an update to a version in Practical Webhandling written a long time ago using a less elegant computational infrastructure.
Heat Sealing
//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 = {
TJaws: sliders.SlideTJaws.value,
TSeal: sliders.SlideTSeal.value,
tMax: sliders.SlidetMax.value,
hSubstrate: sliders.SlidehSubstrate.value,
hPolymer: sliders.SlidehPolymer.value,
hSeal: sliders.SlidehSeal.value,
Pressure: sliders.SlidePressure.value,
hStep: sliders.SlidehStep.value,
tStepFact: sliders.SlidetStep.value,
SealK: sliders.SlideSealK.value,
SubstrateK: sliders.SlideSubstrateK.value,
PolymerK: sliders.SlidePolymerK.value,
showLayers: document.getElementById('showLayers').checked,
doCooling: document.getElementById('doCooling').checked,
tCool: sliders.SlidetCool.value,
vAir: sliders.SlidevAir.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('Info').value = result.Info;
document.getElementById('Tend').value = result.Tend;
//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({ TJaws, TSeal, tMax, hSubstrate, hPolymer, hSeal, hStep, Pressure, tStepFact, SealK, SubstrateK, PolymerK, showLayers,doCooling,tCool,vAir }) {
//Key values
const ThermCond = { Substrate: SubstrateK, Polymer: PolymerK, Seal:SealK } //W/m.K
const HeatCapacity = { Substrate: 2000, Polymer: 1800, Seal: 1700 } //J/kg.K
const MeltPt = { Substrate: 999, Polymer: 999, Seal: TSeal }
const MPtHt = { Substrate: 999, Polymer: 999, Seal: 4000 }
const MPtWidth = { Substrate: 999, Polymer: 999, Seal: 15 }
const Density = { Substrate: 1000, Polymer: 920, Seal: 1000 }
const xThick = (20 - 20 * Math.min(1,Pressure)) * ThermCond.Substrate / 0.05 //Centred around 0.5MPa and maxes at 1MPa
let Thick = [hSubstrate + xThick, hPolymer, hSeal, hSeal, hPolymer, hSubstrate + xThick]
let TC = [ThermCond.Substrate, ThermCond.Polymer, ThermCond.Seal, ThermCond.Seal, ThermCond.Polymer, ThermCond.Substrate]
let HC = [HeatCapacity.Substrate, HeatCapacity.Polymer, HeatCapacity.Seal, HeatCapacity.Seal, HeatCapacity.Polymer, HeatCapacity.Substrate]
let Dens = [Density.Substrate, Density.Polymer, Density.Seal, Density.Seal, Density.Polymer, Density.Substrate]
let MPt = [MeltPt.Substrate, MeltPt.Polymer, MeltPt.Seal, MeltPt.Seal, MeltPt.Polymer, MeltPt.Substrate]
let MHt = [MPtHt.Substrate, MPtHt.Polymer, MPtHt.Seal, MPtHt.Seal, MPtHt.Polymer, MPtHt.Substrate]
let MWidth = [MPtWidth.Substrate, MPtWidth.Polymer, MPtWidth.Seal, MPtWidth.Seal, MPtWidth.Polymer, MPtWidth.Substrate]
const Step = hStep, SealLayer = 2
let HCT = Create2DArray(Thick.length)
let HCTC = Create2DArray(Thick.length)
let LThick = [], NLayers = 0, TotThick = 0, SumThick = [], LTC = [], LHC = []
let plotData = [], lineLabels = [], myColors = [], borderWidth = [], dottedLine = []
for (var i = 0; i < Thick.length; i++) {
if (Thick[i] > 0.99) {
LThick[NLayers] = Thick[i]
TotThick += Thick[i]
SumThick[NLayers] = TotThick
LTC[NLayers] = TC[i]
LHC[NLayers] = HC[i] * Dens[i] * 1e-18 //Convert from cubic microns
for (var j = 25; j <= 300; j++) {
HCT[NLayers][j] = (HC[i] * (1 + (j - 25) / 300) + MHt[i] * Math.exp(-Math.pow((j - MPt[i]) / MWidth[i], 2))) * Dens[i] * 1e-18
//Heat capacity is - T offset by 30deg
HCTC[NLayers][j] = (HC[i] * (1 + (j - 25) / 300) + MHt[i] * Math.exp(-Math.pow((j - (MPt[i] - 30)) / MWidth[i], 2))) * Dens[i] * 1e-18
}
NLayers += 1
};
};
//Let's make 1um steps and see what happens
var GSteps = Math.floor(TotThick / Step)
var GTC = [], GHC = [], GT = [], TmpT = [], ThisLayer = 0, Seal = 0
for (var i = 0; i < GSteps; i++) {
if (i * Step >= SumThick[ThisLayer]) { ThisLayer += 1; if (ThisLayer == SealLayer) Seal = i }
GTC[i] = LTC[ThisLayer]
GHC[i] = ThisLayer
GT[i] = 25;
TmpT[i] = GT[i]
};
GT[0] = TJaws;
GT[GSteps - 1] = TJaws
GT[GSteps] = TJaws
//Now do things in small steps and report them every 0.01s
let tNow = 0, TShow = 0.01, TStep = 0.000001 * tStepFact, TNext = TShow
let PPts = [{ x: 0, y: 25 }]
let JFact = 1e-6 / (Step * Step) //*TStep//Each slice is in microns but units are in m and convert from Watts to J
let tSeal=0
while (tNow <= tMax) {
for (let i = 1; i < GSteps - 1; i++) {
//This is the logic
//Flow = ((GT[i - 1] - GT[i]) * GTC[i] - (GT[i] - GT[i + 1]) * GTC[i + 1]) * JFact * TStep
// TRise = Flow / HCT[GHC[i]][Math.floor(GT[i])]
// TmpT[i] = GT[i] + TRise
//This is faster but less obvious
TmpT[i]= GT[i] + (((GT[i - 1] - GT[i]) * GTC[i] - (GT[i] - GT[i + 1]) * GTC[i + 1]) * JFact * TStep) / HCT[GHC[i]][Math.floor(GT[i])]
};
for (let i = 1; i < GSteps - 1; i++) {
GT[i] = TmpT[i]
};
if (GT[Seal]>=TSeal && tSeal==0) tSeal=tNow
if (tNow >= TNext) {
let tPts = []
tPts.push({ x: GT[Math.floor(xThick/Step)], y: 0 })
for (var i = 1; i < GSteps - 1; i++) {
iStep = i * Step
if (iStep > xThick && iStep < TotThick - xThick) {
tPts.push({ x: GT[i], y: iStep - xThick })
}
};
PPts.push({ x: tNow, y: GT[Seal] })
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
if (tSeal==0) {tSeal="n/a"} else {tSeal=tSeal.toFixed(3)+"s"}
// //Just in case didn't reach TMax step
PPts.push({ x: tMax, y: GT[Seal] })
if (showLayers) {
//Dotted lines
const theTop = 0
yPos = Thick[0] - xThick + Thick[1] + Thick[2]
makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, "Seal")
yPos = Thick[0] - xThick
makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, "SubstratePoly")
yPos = Thick[0] - xThick + 2 * Thick[1] + 2 * Thick[2]
makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, "SubstratePoly")
yPos = Thick[0] - xThick + Thick[1]
makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, "PolySeal")
yPos = Thick[0] - xThick + Thick[1] + 2 * Thick[2]
makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, "PolySeal")
}
const sealMax=GT[Seal]
let tCross=0
tCool=doCooling?tCool:0
if (doCooling) {
var TC0 = 1.103110087, TC1 = 0.14739637, TC2 = -0.000958993, TC3 = 2.34e-06
var BC0 = TC0, BC1 = TC1, BC2 = TC2, BC3 = TC3
var AirFact = vAir/3*0.000025 / Step
TStep = 0.00004
while (tNow <= tMax+tCool) {
for (var i = 1 ; i < GSteps - 1; i++) {
//The condensed version for speed
TmpT[i] = GT[i] + ((GT[i - 1] - GT[i]) * GTC[i] - (GT[i] - GT[i + 1]) * GTC[i + 1]) * JFact * TStep / HCTC[GHC[i]][Math.floor(GT[i])]
};
//Now the top and bottom layers
Flow = (-(GT[0] - GT[ 1]) * GTC[1] - AirFact * (TC0 + TC1 * GT[0] + TC2 * GT[[0]] * GT[[0]] + TC3 * GT[[0]] * GT[[0]] * GT[[0]]) * (GT[[0]] - 25)) * JFact * TStep
TRise = Flow / HCTC[GHC[[0]]][Math.floor(GT[[0]])]
TmpT[[0]] = GT[[0]] + TRise
Flow = ((GT[GSteps-2] - GT[GSteps-1]) * GTC[GSteps-1] - AirFact * (BC0 + BC1 * GT[GSteps-1] + BC2 * GT[GSteps-1] * GT[GSteps-1] + BC3 * GT[GSteps-1] * GT[GSteps-1] * GT[GSteps-1]) * (GT[GSteps-1] - 25)) * JFact * TStep
TRise = Flow / HCTC[GHC[GSteps-1]][Math.floor(GT[GSteps-1])]
TmpT[GSteps-1] = GT[GSteps-1] + TRise
for (var i = 0; i < GSteps; i++) {
GT[i] = TmpT[i]
};
if (GT[Seal]<=TSeal && tSeal!="n/a" && tCross==0) tCross=tNow-tMax //Relative to start of cooling
if (tNow >= TNext) {
PPts.push({ x: tNow, y: GT[Seal] })
TNext += TShow
}
tNow += TStep
} //Big While
//Just in case didn't reach TMax step
PPts.push({ x: tMax+tCool, y: GT[Seal] })
if (tCross==0) {tCross="n/a"} else {tCross=tCross.toFixed(2)+"s"}
}//End of cooling
const PPtsSeal=[{x:0,y:TSeal},{x:tMax+tCool,y:TSeal}]
//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: [,Math.ceil(TotThick-2*xThick)], //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
};
const prmap1 = {
plotData: [PPts,PPtsSeal], //An array of 1 or more datasets
lineLabels: ["T @ Seal","Seal T"], //An array of labels for each dataset
dottedLine: [false,true],
hideLegend: true,
xLabel: 't&s', //Label for the x axis, with an & to separate the units
yLabel: 'T&°C', //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, tMax+tCool], //Set min and max, e.g. [-10,100], leave one or both blank for auto
yMinMax: [20,TJaws], //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: 'F3', //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 {
Info: tSeal+ " : "+ sealMax.toFixed(0) + "°",
Tend: doCooling ? GT[Seal].toFixed(0) + "° " + tCross:"-",
plots: [prmap, prmap1],
canvas: ['canvas', 'canvas1'],
};
}
function makeDotted(plotData, lineLabels, myColors, borderWidth, dottedLine, yPos, TJaws, hStep, theTop, theLabel) {
let tPts = []
yPos-=hStep
tPts.push({ x: 20, y: yPos }, { x: TJaws, y: yPos })
plotData.push(tPts)
lineLabels.push(theLabel)
myColors.push("rgba(2,100,100,0.5)")
borderWidth.push(1)
dottedLine.push(true)
}
function Create2DArray(rows) {
var arr = [];
for (var i = 0; i < rows; i++) {
arr[i] = [];
}
return arr;
}
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 }
}
The basic calculation takes your chosen jaws temperature (assumed equal top and bottom) T Jaws, Seal temperature T Seal, and the thicknesses μm of substrate (which might be a paper or heat-resistant polymer), intermediate polymer (set to 0 if you have a 2-layer system) and the seal polymer itself, then calculates the temperature distribution over timesteps up to your chosen Seal Time. The thermal conductivities W/m.K are also required. Heat capacities are assumed to be "normal".
These models need to cut the stack into individual layers of thickness hStep and divide time into small slices of size tStep. Bigger h slices are faster to calculate but potentially less accurate. Smaller t slices are slower. The combination of big h and big t can lead the calculations to "blow up" (large temperature gradients can also blow up) so find a pair of settings that are fast enough and accurate enough for your specific setup.
The main graph uses rainbow colours (blue-->red) to show the different timesteps, the x axis is temperature and y is your stack. Moving the mouse gives you time, position and temperature information. You also get a graph of the temperature at the point where the two seal layers meet. Your chosen seal temperature is shown as a dotted line and the time taken to reach it is included in the information box, along with maximum temperature.
If you select the Layers option, dotted lines show the intersections between substrate/polymer, polymer/seal and seal/seal.
The jaws make imperfect contact with the substrate surface, resulting in a "thermal contact resistance". The resistance depends on the applied pressure, currently simulated via the Pressure slider with 1 being low pressure, poor contact, to 10 being high pressure, good contact. The internal calculations translate the resistance into an extra thickness of substrate which is then assumed to make perfect contact with the jaws. This is an elegant trick found in the literature.
Cooling
Air cooling is very inefficient and the seal can remain above the seal temperature for a long time, risking damage from stresses after exiting the sealing zone. Turning on the Cooling option lets you see the cooling in the graph and providing the final temperature at the seal and also, if relevant, the time (from release of the jaws) taken to cross back over the seal temperature. Given the many uncertainties of air cooling, a "standard" rate is assumed at an air "velocity" set to 3, with cooling 3x slower at 1 and 3x faster at 9.
Numerics, accuracy, convenience
As mentioned above with all numerical simulations there is a trade-off of speed and accuracy along with the possibility of the numerics blowing up, the result being either no graph or obvious graph artefacts. The hStep slider controls the thickness of individual computed layers. Thinner is slower, more accurate and more likely to blow up. The tStep slider alters the time steps (μs), with smaller being slower, more accurate and less likely to blow up.
The best approach is to start with hStep as 2 or 3μm and tStep as 5 to 10 μs while you set up your system and explore the effects of large changes. When you are more interested in details, reduce hStep and, if neccesary tStep. If you see any changes then decrease either/both till there are no more obvious changes.