Baking

Quick Start

If you're not a scientist, just interested in cake and bread baking, Welcome! The app is designed for you to discover what's happening to temperatures and water content while you bake. For best results (and because it's interesting!), use the Water Content app to find out how much water is in the recipe and how light (or dense) your mix is. If you are a scientist then all the science is described in detail below.

Credits

The app was inspired one day by an especially delicious fruit cake slow-baked by my wife. The calculations use the method of top baking scientist Prof Emmanuel Purlis in Argentina.

Baking

Diam. or Width cm
Height cm
% Water
Density @ Start
tBake min
tCool min
TStart °C
TOven °C
TBaked °C
Expansion %
Fan Oven
Filled map
Tin
Breadmaker
To Bake : Wt Loss
//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 = {
        Tstart: sliders.SlideTstart.value,
        Toven: sliders.SlideToven.value,
        Tbaked: sliders.SlideTbaked.value,
        Diam: sliders.SlideD.value / 100, //cm to m
        rhoStart: sliders.SliderhoStart.value * 1000, //g/cc to kg/m3
        Expansion: sliders.SlideExpansion.value / 100, //% to fraction
        H: sliders.SlideH.value / 100, //cm to m
        tHot: sliders.SlidetHot.value * 60, //min to s
        tCool: sliders.SlidetCool.value * 60, //min to s
        water: sliders.Slidewater.value / 100, //% to fraction
        isFan: document.getElementById('isFan').checked,
        isFilledMap: document.getElementById('isFilledMap').checked,
        isTin: document.getElementById('isTin').checked || document.getElementById('isBreadmaker').checked,
        isBreadmaker: document.getElementById('isBreadmaker').checked,
    };

    //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('toBake').value = result.toBake;
    // 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({ Tstart, Toven, Tbaked, Diam, rhoStart, Expansion, H, tHot, tCool, isFan, isFilledMap, isTin, isBreadmaker, water }) {

    const R = Diam / 2
    const NySteps=NxSteps=tHot>1800?40:50
    const yStep = H/NySteps, xStep = R/NxSteps
    const tCook = tHot + tCool, Tovenstart = Toven
    const HTCFFan = 30, HTCFair = 20
    let HTCF = isFan ? HTCFFan : HTCFair
    let MTCF = isFan ? 8e-9 : 5e-9
    if (isBreadmaker) HTCF = 120
    let T = Create2DArray(NySteps), tmp = Create2DArray(NySteps), W = Create2DArray(NySteps), tmpW = Create2DArray(NySteps)
    let i, j
    let Wtot = 0
    for (i = 0; i < NySteps; i++) {
        for (j = 0; j < NxSteps; j++) {
            T[i][j] = Tstart
            tmp[i][j] = T[i][j]
            W[i][j] = water
            if (i > 0) Wtot += water
            tmpW[i][j] = W[i][j]
        }
    }
    const Nxy = Wtot / water
    let K = [], rho = [], D = [], Cps = [], Cpw = [], Pa = []
    const enthEvap = 2.3e6 //J/kg
    for (i = 1; i <= Toven; i++) {
        if (i <= 100) {
            K[i] = 0.2 + 0.9 / (1 + Math.exp(-0.1 * (i - 80)))
            rho[i] = rhoStart
            D[i] = 1e-10 //* 1000 / rhoStart
            Pa[i] = 133.322 * Math.pow(10, 8.0713 - 1730.63 / (i + 233.426)) //Antoine from mmHg to Pa
        } else {
            K[i] = 0.2
            rho[i] = rhoStart
            D[i] = 6e-8 //* 1000 / rhoStart
            Pa[i] = 1e6
        }
        iK = i + 273
        Cps[i] = 5 * iK + 25
        Cpw[i] = 1000 * (5.207 - 73.17e-4 * iK + 1.35e-5 * iK * iK)
        if (rhoStart>590) K[i] /= 2.0 //This is a kludge to allow bread to bake faster
    }

    //Water activity constant
    const aPower = -1 / 0.38
    const iM = Math.floor(NySteps / 2), jM = NxSteps - 1, iLast = NySteps - 1, iLast1 = iLast - 1
    const TRoom = 22
    let tNow = 0, tBig = 10, tNext = tBig, tStep = 0.5, CP = 0, TA = 0, dT = 0
    if (yStep<0.00051) tStep/=2 
    let CData = [{ x: 0, y: Tstart }], WData = [{ x: 0, y: 100 * water }], TMax = 0
    let amCooking = true, hasBaked = false, toBake = "Unbaked"
    let yS2 = yStep * yStep, xS2 = xStep * xStep
    let Tmid = Tstart, expNow = 1
    if (isBreadmaker) Toven = 35
    while (tNow < tCook) {
        if (isBreadmaker && tNow < 600) {
            Toven = 35 + (Tovenstart - 35) * tNow / 600
        }
        if (tNow > tHot && amCooking) {
            amCooking = false
            //HTCF = HTCFair
            Toven = TRoom
            for (j = 0; j < NxSteps; j++) {
                T[0][j] = TRoom
                T[iLast][j] = TRoom  //Maybe exclude this line if we put it down so the bottom doesn't cool so fast
            }
            for (i = 0; i < NySteps; i++) {
                T[i][0] = TRoom
            }
        }
        if (Tmid < Tbaked) { expNow = 1 + Expansion * (Tmid - Tstart) / (Tbaked - Tstart) } else { expNow = 1 + Expansion }
        yS2 = yStep * yStep * expNow
        if (!isTin) xS2 = xStep * xStep * expNow
        //Water at the external surfaces
        for (j = 0; j <= NxSteps - 1; j++) {
            TA = Math.floor(T[0][j])
            a = 1 / (1 + Math.pow(100 * W[0][j] / Math.exp(-0.0056 * TA + 3.971), aPower))
            tmpW[0][j] = Math.max(0, W[0][j] - a * Pa[TA] * MTCF / yS2 + tStep * D[TA] * expNow * (W[0][j] - W[1][j]) / yS2)
        }
        //Heat at the surface   
        for (i = 0; i <= NySteps - 1; i++) {
            TA = Math.floor(T[i][0])
            CP = Cps[TA] + W[i][0] * Cpw[TA]
            if (TA >= 99 && TA <= 101) CP += W[i][0] * enthEvap
            tmp[i][0] = T[i][0] + tStep * ((Toven - T[i][0]) * HTCF / (xStep * rho[TA] * CP) - (T[i][0] - T[i][1]) * K[TA] / (rho[TA] * CP) / xS2)
            if (!isTin) {
                a = 1 / (1 + Math.pow(100 * W[i][0] / Math.exp(-0.0056 * TA + 3.971), aPower))
                tmpW[i][0] = Math.max(0, W[i][0] - a * Pa[TA] * MTCF / yS2 + tStep * D[TA] * expNow * (W[i][0] - W[i][1]) / yS2)
            } else {
                if (i > 0 && i < NySteps - 1) tmpW[i][0] = W[i][0] + tStep * D[TA] * expNow * ((W[i - 1][0] - 2 * W[i][0] + W[i + 1][0]) / yS2 - (W[i][0] - W[i][1]) / (xS2))
            }
        }
        for (j = 0; j <= NxSteps - 1; j++) {
            //Top layer
            TA = Math.floor(T[0][j])
            CP = Cps[TA] + W[0][j] * Cpw[TA]
            if (TA >= 99 && TA <= 101) CP += W[0][j] * enthEvap
            tmp[0][j] = T[0][j] + tStep * ((Toven - T[0][j]) * HTCF / (yStep * rho[TA] * CP) - (T[0][j] - T[1][j]) * K[TA] / (rho[TA] * CP) / yS2)
            //Now the bottom 
            TA = Math.floor(T[iLast][j])
            CP = Cps[TA] + W[iLast][j] * Cpw[TA]
            if (TA >= 99 && TA <= 101) CP += W[iLast][j] * enthEvap
            tmp[iLast][j] = T[iLast][j] + tStep * ((Toven - T[iLast][j]) * HTCF / (yStep * rho[TA] * CP) - (T[iLast][j] - T[iLast1][j]) * K[TA] / (rho[TA] * CP) / yS2)
        }
        for (i = 1; i < NySteps - 1; i++) { //The bulk
            for (j = 1; j < NxSteps - 1; j++) {
                TA = Math.floor(T[i - 1][j])
                CP = Cps[TA] + W[i][j] * Cpw[TA]
                if (TA >= 99 && TA <= 101) CP += W[i][j] * enthEvap
                tmp[i][j] = T[i][j] + tStep * K[TA] / (rho[TA] * CP) * ((T[i - 1][j] - 2 * T[i][j] + T[i + 1][j]) / yS2 + (T[i][j - 1] - 2 * T[i][j] + T[i][j + 1]) / (xS2))

                tmpW[i][j] = W[i][j] + tStep * D[TA] * expNow * ((W[i - 1][j] - 2 * W[i][j] + W[i + 1][j]) / yS2 + (W[i][j - 1] - 2 * W[i][j] + W[i][j + 1]) / (xS2))
            }
        }
        for (i = 1; i < NySteps - 1; i++) { //The middle vertical
            TA = Math.floor(T[i - 1][jM])
            CP = Cps[TA] + W[i][jM] * Cpw[TA]
            if (TA >= 99 && TA <= 101) CP += W[i][jM] * enthEvap
            dT = (T[i - 1][jM] - 2 * T[i][jM] + T[i + 1][jM]) * K[TA] / (rho[TA] * CP) / yS2
            dT += (T[i][jM - 1] - T[i][jM]) * K[TA] / (rho[TA] * CP) / (xS2)
            tmp[i][jM] = T[i][jM] + dT * tStep

            tmpW[i][jM] = W[i][jM] + tStep * D[TA] * expNow * ((W[i - 1][jM] - 2 * W[i][jM] + W[i + 1][jM]) / yS2 + (W[i][jM - 1] - W[i][jM]) / (xS2))
        }
        TA = Math.floor(T[iLast][jM]) //The central point
        CP = Cps[TA] + W[iLast][jM] * Cpw[TA]
        if (TA >= 99 && TA <= 101) CP += W[iLast][jM] * enthEvap
        dT = (T[iLast1][jM] - T[iLast][jM]) * K[TA] / (rho[TA] * CP) / yS2
        dT += (T[iLast][jM - 1] - T[iLast][jM]) * K[TA] / (rho[TA] * CP) / (xS2)
        tmp[iLast][jM] = T[iLast][jM] + dT * tStep

        tmpW[iLast][jM] = W[iLast][jM] + tStep * D[TA] * expNow * ((W[iLast][jM] - W[iLast1][jM]) / yS2 + (W[iLast][jM - 1] - W[iLast][jM]) / (xS2))

        for (i = 0; i < NySteps; i++) {
            for (j = 0; j < NxSteps; j++) {
                T[i][j] = tmp[i][j]
                W[i][j] = tmpW[i][j]
            }
        }

        if (tNow >= tNext) {
            if (!hasBaked && T[iM][jM] >= Tbaked) {
                toBake = (tNow / 60).toFixed(1) + "min"
                hasBaked = true
            }
            Tmid = T[iM][jM]
            iMs = Math.max(1, iM - 3); iMe = Math.min(iLast - 1, iM + 3)
            for (i = iMs; i < iMe; i++) {
                if (T[i][jM] < Tmid) Tmid = T[i][jM]
            }
            CData.push({ x: tNow / 60, y: Tmid })
            Wtot = 0
            for (i = 1; i < NySteps; i++) {
                for (j = 0; j < NxSteps; j++) {
                    Wtot += W[i][j]
                }
            }
            WData.push({ x: tNow / 60, y: 100 * Wtot / Nxy })
            TMax = Math.max(TMax, Tmid)
            tNext += tBig
        }
        tNow += tStep
    }

    //The plotting has some kludges for the cooling part
    //Hopefully things can be improved as the rest of the app also gets better
    let plotData = [], lineLabels = [], myColors = [], isStraight = [], isFilled = [], TStep = 10, lastJ = 0, plot = []
    for (let TC = 50; TC <= Tovenstart; TC += TStep) {
        // tst=80
        // for (let TC = tst; TC <= tst; TC += TStep) {
        plot = []
        hullpts = []
        for (i = 0; i < NySteps; i++) {
            for (j = 0; j < NxSteps; j++) {
                if (amCooking){
                if ( T[i][j] < TC + TStep) hullpts.push([100 * (NxSteps - 1 - j) * xStep, 100 * (NySteps - i) * yStep * expNow])} else {if ( T[i][j] > TC - TStep) hullpts.push([100 * (NxSteps - 1 - j) * xStep, 100 * (NySteps - i) * yStep * expNow])}
                    // if ( T[i][j] < TC + TStep) hullpts.push([100 * (NxSteps - 1 - j) * xStep, 100 * (NySteps - i) * yStep * expNow])
            }
        }
        if (hullpts.length > 1) {
            //The convex hull code from https://github.com/AndriiHeonia/hull
            pts = hull(hullpts, 100)
            //Indirect Hull to plot
            minx = 9999, mini = 0
            for (i = 0; i < pts.length; i++) {
                if (pts[i][1] < minx) { minx = pts[i][1]; mini = i }
            }
            newpts = []
            for (i = 0; i < pts.length; i++) {
                j = i + mini
                if (j > pts.length - 1) j -= pts.length
                newpts[i] = pts[j]

            }
            for (j = 0; j < newpts.length; j++) {
                if (j == 0 || newpts[j][0] > 0 || !(newpts[j - 1][0] == 0 && newpts[j][0] == 0)) plot.push({ x: newpts[j][0], y: newpts[j][1] })
            }

            // //Straight from Hull to plot
            // for (j = 0; j < pts.length; j++) {
            //     plot.push({ x: pts[j][0], y: pts[j][1] })
            // }
        }
        //console.log( plot)

        if (plot.length > 1) {
            //Now replicate the other half
            whole = [];
            for (i = 0; i < plot.length; i++) {
                x = plot[i].x; y = plot[i].y
                whole.push({ x: x, y: y })
            }
            for (i = whole.length - 1; i >= 0; i--) {
                x = whole[i].x; y = whole[i].y
                plot.push({ x: -x, y: y })
            }
            plotData.push(plot)
            lineLabels.push(TC)
            isStraight.push(true)
            isFilled.push(isFilledMap)
            rbow = Rainbow((TC - 50) / (Tovenstart - 50))
            myColors.push("rgb(" + rbow.r + "," + rbow.g + "," + rbow.b + ")")
        }
    }
    if (!amCooking) {
        plotData.reverse()
        lineLabels.reverse()
        myColors.reverse()
    }

    //Finally the colour-coding scale
    const CPlt = [CData, WData], CPltLabels = ["Centre T", "Centre W"], CPltColors = ["gold", "skyblue"], yAxisL1R2 = [1, 2]
    for (i = 50; i <= Tovenstart; i += 4) {
        tmp = []
        tmp.push({ x: 0, y: i }); tmp.push({ x: tCook / 600, y: i })
        CPlt.push(tmp)
        CPltLabels.push(i)
        rbow = Rainbow((i - 50) / (Tovenstart - 50))
        CPltColors.push("rgb(" + rbow.r + "," + rbow.g + "," + rbow.b + ")")
        yAxisL1R2.push(1)
    }

    //Now we return everything - text boxes, plot and the name of the canvas, which is 'canvas' for a single plot
    const prmap = {
        plotData: plotData, //An array of 1 or more datasets
        lineLabels: lineLabels, //An array of labels for each dataset
        isStraight: isStraight,
        isFilled: isFilled,
        hideLegend: true,
        colors: myColors, //An array of colors for each dataset
        xLabel: 'x&cm', //Label for the x axis, with an & to separate the units
        yLabel: 'y&cm', //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: 'P3', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: 'P3', //F for Fixed, P for Precision, E for exponential
    }
    const prmap1 = {
        plotData: CPlt, //An array of 1 or more datasets
        lineLabels: CPltLabels, //An array of labels for each dataset
        colors: CPltColors, //An array of colors for each dataset
        hideLegend: true,
        xLabel: 't&min', //Label for the x axis, with an & to separate the units
        yLabel: 'T centre&°C', //Label for the y axis, with an & to separate the units
        y2Label: "Water&%", //Label for the y2 axis, null if not needed
        yAxisL1R2: 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: [,Math.round(tCook/60)], //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: [0,], //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
    }
    return {
        toBake: toBake + " : " + (100 * (water - Wtot / Nxy)).toFixed(0) + "%",
        plots: [prmap, prmap1],
        canvas: ['canvas', 'canvas1'],
    };
}
function mySort(a, b) {
    if (a[0] == b[0]) return (a[1] - b[1])
    return a[0] - b[0]
}
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 }
}

            

A baking cakeYou've just put your cake or bread into the oven. If it's round it has a Diameter and a Height, it it's a rectangle then you set the Diameter value to its Width. The oven has a temperature TOven. You plan to bake it for a time tBake and you know that it will rise by a known Expansion %. We can imagine (or we can place) a thermocouple probe in the middle to know what's going on

Obviously the outside gets hot rather quickly and it's easy to tell if it's under- or over-cooked. But the real problem is the middle. If you don't cook for long enough, the middle is under-cooked. And if you have the oven too hot, the outside will be burned and the middle might still be under-cooked.

If we define a target temperature, TBaked which might be something like 95-100°C then we just need to look at the right-hand graph which tells us the temperature in the middle (plus the water content). [Use your mouse to read out values.]. For a short time, nothing happens! It takes time for heat to flow through the mix. Then it rises steadily. If tBake is too small, then the centre never reaches your baking point. If it's too large then the although the centre temperature might remain constant near 100°C, the overall result will be either burned on the outside or over-dried. You can read the time to baking from the graph, but the app also provides the time, along with the loss of water, in the output box.

Water content

The water content is imporant. Undercooked, with too much water, we have a soggy disaster. Overcooked, with too little water, the result tastes and feels too dry. We need to know the % of water at the start and this can be estimated using the Water Content app. The blue line in the graph shows how the % water changes during baking. The text box tells you the % loss in weight, which leads to an interesting point.

It would be a very good habit to measure the weight of the mix at the start then weigh the final baked cake or bread. This can easily be done in the tin, as long as you remember to measure and record the empty tin weight. The loss of water during baking is a significant factor and after a while you will be able to tell how well baked something is by briefly taking it out of the oven to weigh it. It's a good idea to put something like a plate onto the scales first so the measuring pan never get heated during your brief weighing.

Better baking

For a given density/expansion, weight and moisture content of the mix, baking depends on:

  • Diameter or Width
  • Oven temperature
  • Baking time
  • Tin or no tin (for bread)
  • Fan or fanless

The app lets you explore all the options. If you increase the size of the baking tin then the original height decreases so baking is significantly faster. A 40% increase in diameter (or for a rectangular tin both width & length) means a halving of the height. A higher temperature will more easily over-cook the outside even if the inside cooks faster. A longer baking time allows you to use a lower temperature. Without the walls of the tin, water escapes faster. If the oven is fanless then the air next to the baking tin is cooled by the contents, so the heating is slower. Turning on the fan allows fresh hot air to push away "stuck" cold air, so baking is faster. If you've seen recipes saying "180°C [160°C fan] you can test this for yourself in the app. Find the baking time at 180°C with the fan off, then turn on the fan and reduce the oven temperature till you get the same baking time. Hopefully it agrees OK with what your recipes say.

So far we've just looked at the temperature in the centre. What about the temperatures throughout the mix?

Temperature in the whole mix

The temperature "map" plots lines of constant temperature reached at your baking time (forgive the glitches - it's very hard to produce a good map). So there might be a 60°C zone near the middle with a 170°C zone near the top and around the side. The zones are rainbow colour coded from blue to red, from 50°C to your chosen oven temperature. Move your mouse to get a readout of any area.

Some users like the line map (contours) some prefer the Filled map view where it's easier to see the gradations from cold to hot. The colour code is shown in the graph on the right.

Density and Expansion

If you have a very dense cake batter (e.g. a fruit cake), it takes a lot more heat to raise the temperature compared to the same volume of a bread dough that has risen before baking. This means, unfortunately, that you have to specify a starting density. A bread dough might have a density of 0.4 g/cc, a classic pre-aerated sponge, 0.6, a sponge with chemical raising agent might be over 1 and a fruit cake might be 1.3. You can get a good estimate of your density via the Water Content app.

Expansion slows down the cooking because it is harder for heat to flow through a foam structure than through liquid or solid. So we need to know your starting density and the (approximate) expansion, where 100% is twice the size. For simplicity, even for the no-tin case, expansion takes place only in the height direction.

Cooling

If you've baked for, say, 25min, slide TCool to give 5-10min of cooling. If you look at the graph of temperature in the middle you might be surprised. It just carries on rising for a while! This makes sense. Although the outside is starting to cool, hotter parts of the interior can still pass on their heat to cooler parts deeper inside. So although everything might be baked perfectly when you take it out, it might be over-cooked by the time you get to try it. Or if it was under-cooked in the centre when you take it from the oven might be perfect when you try a slice later.

Apologies for the colour map on cooling. It's even harder to get right.

The science bit

There are plenty of simplifying assumptions. The batter/dough is perfectly flat at the start, heat flows equally through the top, the base and through the sides, and your oven is exactly at the specified temperature. If you are baking without a tin, you still have to pretend that it starts, and remains, as a perfect cylinder.

But there are plenty of complications. As the mixture warms, the rate at which heat can flow (the thermal conductivity) changes. Basically it rises with temperature thanks to heat flow via water evaporation, then falls rapidly when the cake loses water and goes solid. Similarly, the rate at which water diffuses and exits from the surface is highly temperature-dependent. We also have the complexity of expansion.

The app works by dividing the system into a number of "cells" and baking is assumed to take place via a number of time steps. At each step the amount of heat flowing into or out of a cell depends on the temperatures of the cells around it and the temperature changes according to the heat flow and the local "heat capacity" (very strongly-dependent on water content) and density. Similar calculations allow the water content in each cell to change. At the end of each time step, the temperature and water-content values are updated and the process repeats.

The number of cells depends on how fine each slice through the batter/dough should be. The finer the slices, the more accurate the result. Similarly, the smaller the timestep, the better the accuracy. Clearly there's a trade-off with calculation speed. The chosen parameters seem to work well-enough, i.e. if you double them, the calculated results don't change too much, though the calculation time gets 4x longer.

The only way a model can be made to work is by having lots of experimentally derived values from the literature then via some further tweaking the parameters to match experiments. The starting point was an excellent set of data on Génoise sponge cakes (they are pre-foamed and don't rise too much) in the paper: Edward A. Olszewski, From baking a cake to solving the diffusion equation, Am. J. Phys. 74, 2006, 502-509. It quickly becomes apparent that the specific model is too simple (it doesn't model water content) and contains some contradictions, but the experimental data are very useful. Other references had other examples and recommended parameters. Here are some of them.

Emmanuel Purlis, Viviana O. Salvadori, Bread baking as a moving boundary problem. Part 2: Model validation and numerical simulation, Journal of Food Engineering, 91 (2009) 434–442

B. Zanoni, S. Pierucci & C. Peri, Study of the Bread Baking Process - II. Mathematical Modelling, Journal of Food Engineering, 23 (1994) 321-336

Melike Sakin, Figen Kaymak-Ertekin, Coskan Ilicali, Simultaneous heat and mass transfer simulation applied to convective oven cup cake baking, Journal of Food Engineering 83 (2007) 463–474

My thanks to family and neighbours who kindly provided their own experimental data so that results generally seemed to agree across a range of baking styles, equipment and recipes. There were a few "aha" moments from their data which made all the difference. The addition of the Density calculation came from one of those moment. I also especially appreciated the early beta feedback testing from scientist/baker Kaja Harton.