diff --git a/index.html b/index.html index 478d802..37f3295 100644 --- a/index.html +++ b/index.html @@ -5,46 +5,64 @@ Montecarlo simulation of bushfire risk - - -
-
-
+ +
+
+
- -
- - - - -
- -
- This simulation + +
+ + + + +
0
+
+ +
+ + + + + + + +
+ +
+ +
+ +
-
-
-
- + + - - - - + + + + + - - - - + + @@ -52,22 +70,36 @@ class Simulation { constructor(canvas,data) { this.canvas=canvas - this.W = canvas.width = data.fuel.shape[0] - this.H = canvas.height = data.fuel.shape[1] + this.H = canvas.height = data.fuel.shape[0] + this.W = canvas.width = data.fuel.shape[1] this.clock = 0; - // this.data = nj.zeros([this.W,this.H,4]) // fire,fuel, ash, alpha - data.fuel = data.fuel.reshape([data.fuel.shape[0],data.fuel.shape[1],1]) data.fire = nj.zeros(data.fuel.shape) data.ash = nj.zeros(data.fuel.shape) - data.alpha = nj.ones(data.fuel.shape) - this.data = nj.concatenate([data.fire,data.fuel,data.ash,data.alpha]) + data.elev = data.elev.reshape([data.elev.shape[0],data.elev.shape[1],1]) + this.data = nj.concatenate([data.fire,data.fuel,data.ash,data.elev]) + + var coords = this.ignite() + this.display(); + } + + /* set an initial fire */ + ignite(e){ + // TODO make click to ignore work with zooming + var canvas = this.canvas // initial point away from walls - // we will make a nice bright cross - var x = _.round(d3.randomUniform(20, this.W-20)()) - var y = _.round(d3.randomUniform(20, this.H-20)()) + // we will make a nice bright cross so it's visible + var x,y + if (e){ + // get click location FIXME might not work on all browsers + y=_.round(e.layerX/this.canvas.offsetWidth*this.canvas.width) + x=_.round(e.layerY/this.canvas.offsetHeight*this.canvas.height) + } else { + x = _.round(d3.randomUniform(20, this.W-20)()) + y = _.round(d3.randomUniform(20, this.H-20)()) + } console.log('fire',x,y) this.data.set(x,y,0,1) this.data.set(x-1,y-1,0,0.3) @@ -75,130 +107,199 @@ class Simulation { this.data.set(x-1,y+1,0,0.3) this.data.set(x+1,y+1,0,0.3) this.data.set(x,y,1,0.3) - - this.display(); + this.display() + return [x,y] } + + /* advance model by one tick */ tick(){ // PARAMS TODO move them var fuelMultipler = 5 // how many turns it burns for var fireMultiplier = 1 + var fireGrowth = 1.5 + var transmissionChance = 0.3 + + // distance to diagonal tiles as a ratio to tile size np.sqrt(1**2+1**2) + var diagDist = 1.42 // tick the environment this.clock++; - if (this.clock>100) return 0; + var t0 = new Date().getTime() + document.getElementById('tick').innerText=this.clock + if (this.clock>1000) return 0; console.log('tick',this.clock) + // + // we are modifying the data in place, so freeze a copy of the old data + var oldData = this.data.clone() - // I could do nearby fires as a convoluton, but lets wait to include elevation diff - var all_fires = this.data.slice(null,null,[0,1]).clone() - // var filter = nj.array([0.5,1,0.5,1,0,1,0.5,1,0.5]).reshape(3,3,1) - // var nearby_fires = nj.convolve(all_fires,filter) - // console.log('max',nearby_fires.max()) - - var new_fires = [] - + /** + * Equations: transmission_probability from a nearby tile: + * $ t = I * Rt$ where I is fire intensity and Rt is the slope term + * $ Rt = exp(0.069 theta) $ theta is the slope angle from -90 to 90 degrees + * this reflects that its hard for fire to spread downhill + * $ theta = atan(dh/w) $ where dh is the difference in height and w is width + * $ theta ~= dh/w $ using the small tan approximation + * + * giving the total equation + * $ t = I * exp(0.069 *dh/w) $ + * + * (from Noble et al 1980, DOI: 10.1111/j.1442-9993.1980.tb01243.x) + */ var fires=0 for (var x = 1; x < this.W-1; x++) { for (var y = 1; y < this.H-1; y++) { // can we do this as a convolution? - var fire = this.data.get(x,y,0)*fireMultiplier - var fuel = this.data.get(x,y,1)*fuelMultipler - var ash = this.data.get(x,y,2) + var fire = oldData.get(x,y,0)*fireMultiplier + var fuel = oldData.get(x,y,1)*fuelMultipler + var ash = oldData.get(x,y,2) + var transmissionProbability = 0 if (fuel==0) continue ash += fire // fire from last turn causes ash to build up fuel = _.clamp(fuel-fire,0,fuelMultipler) // and fuel to decrease if (fire>0){ - fire*=2 // exponentially grow within pixel + fire*=fireGrowth // exponentially grow within pixel } else { // each neighbouring tile might light it - // - // TODO it spreads right because new updates effects the next cell, fix this - // TODO include slope or elevation differences - var fires_adajacent = [ - this.data.get(x-1,y,0), - this.data.get(x,y+1,0), - this.data.get(x+1,y,0), - this.data.get(x,y-1,0), - ] - var fires_diagonal = [ - this.data.get(x-1,y-1,0), - this.data.get(x+1,y+1,0), - this.data.get(x+1,y-1,0), - this.data.get(x-1,y+1,0), - ] - var nearby_fires = (_.sum(fires_adajacent)+_.sum(fires_diagonal)/2)/8 - if (nearby_fires==0) continue + // intensity of fires in nearby cells + var fires_nearby = oldData.slice([x-1,x+2],[y-1,y+2],[0,1]).reshape(3,3) + // account for diagonal and zero the middle + if (fires_nearby.sum()==0) continue + var width_inv = nj.array([ + [1/diagDist,1, 1/diagDist ], + [1, 1e-7, 1 ], + [diagDist, 1, 1/diagDist ] + ]) + var intensity = nj.multiply(fires_nearby,width_inv) + + + // Slope spreading term // + // get difference in height ( height is in pixel width units) + var height = oldData.slice([x-1,x+2],[y-1,y+2],[3,4]).reshape(3,3) + var h0 = oldData.get(x,y,3) + var dHeight = nj.subtract(height,h0) + var Rt = nj.exp(nj.multiply(dHeight,width_inv).multiply(0.069)) + + transmissionProbability = nj.multiply(intensity,Rt) + transmissionProbability = transmissionProbability.mean() // neighbouring tiles have a chance of lighting our tile - if (nearby_fires0){ - fires+=fire - // console.debug('fire',x,y,fire,fuel,ash) - } } } - // set all the new fires - for (var x = 1; x < this.W-1; x++) { - for (var y = 1; y < this.H-1; y++) { - this.data.set(x,y,0,all_fires.get(x,y,0)) - } - } - - console.log('tick',this.clock,'change',fires) - return fires + var t = new Date().getTime()-t0 + console.log('tick',this.clock,'time',t) + return t } + + /** push data onto canvases */ display(){ - var H = this.canvas.height; - var W = this.canvas.width; - var ctx = this.canvas.getContext('2d'); + var H = this.W; + var W = this.H; + + // first show fire + + // Fill in the fire layer + var canvas = document.getElementById('fire') + var ctx = canvas.getContext('2d'); var imageData = ctx.getImageData(0,0,H,W) - console.assert(this.data.selection.data.length==imageData.data.length) - - // for (var x = 1; x < this.W-1; x++) { - // for (var y = 1; y < this.H-1; y++) { - // // mix the data into colors - // var cell = this.data.slice([0,1],[0,1],null).reshape(1,4) - // var filter = nj.array([ - // [1,0,0,0], - // [0,1,0,0], - // [0,0,1,0], - // [0,0,0,1] // r,g,b,a - // ]).reshape(4,4) - // var colors = nj.dot(cell,filter) - // - // // now set imageData - // for (var i = 0; i < 4; i++) { - // imageData.data[i + this.W *(x + 4*z)]=colors.selection.data[i]*255.0 - // } - // } - // } - - // set the data with fire,fuel,ash as red,green,blue - for (var i = 0; i < imageData.data.length; i++) { - imageData.data[i]=this.data.selection.data[i]*255.0 + var color_scale = d3.interpolateLab('yellow','red') + var fire = this.data.slice(null,null,[0,1]) + for (var x = 0; x < imageData.width; x++) { + for (var y = 0; y < imageData.height; y++) { + var d = fire.get(x,y,0)*2 + var c = new d3.color(color_scale(d)) + var i = (x*H+y)*4 + imageData.data[i+0]=c.r + imageData.data[i+1]=c.g + imageData.data[i+2]=c.b + imageData.data[i+3]=_.clamp(d*255*2,0,255) + } } ctx.putImageData(imageData, 0, 0); + console.assert(this.data.selection.data.length==imageData.data.length) + + // Fill in the fire layer + var canvas = document.getElementById('fuel') + var ctx = canvas.getContext('2d'); + var imageData = ctx.getImageData(0,0,H,W) + var color_scale = d3.interpolateLab('white','green') + var fuel = this.data.slice(null,null,[1,2]) + for (var x = 0; x < imageData.width; x++) { + for (var y = 0; y < imageData.height; y++) { + var d = fuel.get(x,y,0)*2 + var c = new d3.color(color_scale(d)) + var i = (x*H+y)*4 + imageData.data[i+0]=c.r + imageData.data[i+1]=c.g + imageData.data[i+2]=c.b + imageData.data[i+3]=_.clamp(d*255*2,0,255) + } + } + ctx.putImageData(imageData, 0, 0); + console.assert(this.data.selection.data.length==imageData.data.length) + + + // Fill in the ash layer + var canvas = document.getElementById('ash') + var ctx = canvas.getContext('2d'); + var imageData = ctx.getImageData(0,0,H,W) + var color_scale = d3.interpolateLab('white','grey') + var ash = this.data.slice(null,null,[2,3]) + for (var x = 0; x < imageData.width; x++) { + for (var y = 0; y < imageData.height; y++) { + var d = ash.get(x,y,0)*2 + var c = new d3.color(color_scale(d)) + var i = (x*H+y)*4 + imageData.data[i+0]=c.r + imageData.data[i+1]=c.g + imageData.data[i+2]=c.b + imageData.data[i+3]=_.clamp(d*255*2,0,255) + } + } + ctx.putImageData(imageData, 0, 0); + console.assert(this.data.selection.data.length==imageData.data.length) } - start(n=10000){ + /** this returns stats **/ + stats(){ + var fire = this.data.slice(null,null,[0,1]) + var burning_cells = 0 + for (var x = 1; x < this.W-1; x++) { + for (var y = 1; y < this.H-1; y++) { + if (fire.get(x,y,0)!=0) burning_cells++ + } + } + return { + fire: _.round(fire.sum(),2), + fuel: _.round(this.data.slice(null,null,[1,2]).sum(),2), + ash: _.round(this.data.slice(null,null,[2,3]).sum(),2), + burning_cells:burning_cells + } + } + start(n=5000){ this.stop() this.loop = setInterval(()=>{ this.tick() @@ -212,11 +313,18 @@ class Simulation { + +