Rev 1221 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
671 | werner | 1 | /******************************************************************************************** |
2 | ** iLand - an individual based forest landscape and disturbance model |
||
3 | ** http://iland.boku.ac.at |
||
4 | ** Copyright (C) 2009- Werner Rammer, Rupert Seidl |
||
5 | ** |
||
6 | ** This program is free software: you can redistribute it and/or modify |
||
7 | ** it under the terms of the GNU General Public License as published by |
||
8 | ** the Free Software Foundation, either version 3 of the License, or |
||
9 | ** (at your option) any later version. |
||
10 | ** |
||
11 | ** This program is distributed in the hope that it will be useful, |
||
12 | ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | ** GNU General Public License for more details. |
||
15 | ** |
||
16 | ** You should have received a copy of the GNU General Public License |
||
17 | ** along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
18 | ********************************************************************************************/ |
||
19 | |||
646 | werner | 20 | #include "firemodule.h" |
21 | |||
650 | werner | 22 | #include "grid.h" |
646 | werner | 23 | #include "model.h" |
24 | #include "modelcontroller.h" |
||
25 | #include "globalsettings.h" |
||
26 | #include "resourceunit.h" |
||
27 | #include "watercycle.h" |
||
28 | #include "climate.h" |
||
662 | werner | 29 | #include "soil.h" |
654 | werner | 30 | #include "dem.h" |
1172 | werner | 31 | #include "seeddispersal.h" |
665 | werner | 32 | #include "outputmanager.h" |
690 | werner | 33 | #include "species.h" |
656 | werner | 34 | #include "3rdparty/SimpleRNG.h" |
646 | werner | 35 | |
697 | werner | 36 | /** @defgroup firemodule iLand firemodule |
37 | The fire module is a disturbance module within the iLand framework. |
||
38 | |||
39 | See http://iland.boku.ac.at/wildfire for the science behind the module, |
||
40 | and http://iland.boku.ac.at/fire+module for the implementation/ using side. |
||
41 | */ |
||
42 | |||
650 | werner | 43 | //********************************************************************************* |
44 | //******************************************** FireData *************************** |
||
45 | //********************************************************************************* |
||
46 | |||
47 | void FireRUData::setup() |
||
48 | { |
||
654 | werner | 49 | // data items loaded here are provided per resource unit |
650 | werner | 50 | XmlHelper xml(GlobalSettings::instance()->settings().node("modules.fire")); |
664 | werner | 51 | mKBDIref = xml.valueDouble(".KBDIref", 0.3); |
650 | werner | 52 | mRefMgmt = xml.valueDouble(".rFireSuppression", 1.); |
654 | werner | 53 | mRefLand = xml.valueDouble(".rLand", 1.); |
650 | werner | 54 | mRefAnnualPrecipitation = xml.valueDouble(".meanAnnualPrecipitation", -1); |
680 | werner | 55 | mAverageFireSize = xml.valueDouble(".averageFireSize", 10000.); |
56 | mMinFireSize = xml.valueDouble(".minFireSize", 0.); |
||
57 | mMaxFireSize = xml.valueDouble(".maxFireSize", 1000000.); |
||
654 | werner | 58 | mFireReturnInterval = xml.valueDouble(".fireReturnInterval", 100); // every x year |
59 | if (mAverageFireSize * mFireReturnInterval == 0.) |
||
656 | werner | 60 | throw IException("Fire-setup: invalid values for 'averageFireSize' or 'fireReturnInterval' (values must not be 0)."); |
654 | werner | 61 | double p_base = 1. / mFireReturnInterval; |
62 | // calculate the base ignition probabiility for a cell (eg 20x20m) |
||
63 | mBaseIgnitionProb = p_base * FireModule::cellsize()*FireModule::cellsize() / mAverageFireSize; |
||
64 | mFireExtinctionProb = xml.valueDouble(".fireExtinctionProbability", 0.); |
||
65 | |||
667 | werner | 66 | |
650 | werner | 67 | } |
68 | |||
69 | //********************************************************************************* |
||
70 | //****************************************** FireLayers *************************** |
||
71 | //********************************************************************************* |
||
72 | |||
73 | |||
74 | double FireLayers::value(const FireRUData &data, const int param_index) const |
||
75 | { |
||
76 | switch(param_index){ |
||
757 | werner | 77 | case 0: return data.mBaseIgnitionProb; // base ignition value |
755 | werner | 78 | case 1: return data.mKBDI; // KBDI values |
79 | case 2: return data.mKBDIref; // reference KBDI value |
||
80 | case 3: return data.fireRUStats.fire_id; // the ID of the last recorded fire |
||
81 | case 4: return data.fireRUStats.crown_kill; // crown kill fraction (average on resource unit) |
||
82 | case 5: return data.fireRUStats.died_basal_area; // basal area died in the last fire |
||
83 | case 6: return data.fireRUStats.n_trees > 0 ? data.fireRUStats.n_trees_died / double(data.fireRUStats.n_trees) : 0.; |
||
84 | case 7: return data.fireRUStats.fuel_dwd + data.fireRUStats.fuel_ff; // fuel load (forest floor + dwd) kg/ha |
||
650 | werner | 85 | default: throw IException(QString("invalid variable index for FireData: %1").arg(param_index)); |
86 | } |
||
87 | } |
||
88 | |||
1014 | werner | 89 | const QVector<LayeredGridBase::LayerElement> &FireLayers::names() |
650 | werner | 90 | { |
1014 | werner | 91 | if (mNames.isEmpty()) |
92 | mNames= QVector<LayeredGridBase::LayerElement>() |
||
93 | << LayeredGridBase::LayerElement(QLatin1Literal("baseIgnition"), QLatin1Literal("base ignition rate"), GridViewRainbow) |
||
94 | << LayeredGridBase::LayerElement(QLatin1Literal("KBDI"), QLatin1Literal("KBDI"), GridViewRainbow) |
||
95 | << LayeredGridBase::LayerElement(QLatin1Literal("KBDIref"), QLatin1Literal("reference KBDI value"), GridViewRainbow) |
||
96 | << LayeredGridBase::LayerElement(QLatin1Literal("fireID"), QLatin1Literal("Id of the fire"), GridViewRainbow) |
||
97 | << LayeredGridBase::LayerElement(QLatin1Literal("crownKill"), QLatin1Literal("crown kill rate"), GridViewRainbow) |
||
98 | << LayeredGridBase::LayerElement(QLatin1Literal("diedBasalArea"), QLatin1Literal("m2 of died basal area"), GridViewRainbow) |
||
99 | << LayeredGridBase::LayerElement(QLatin1Literal("diedStemsFrac"), QLatin1Literal("fraction of died stems"), GridViewRainbow) |
||
100 | << LayeredGridBase::LayerElement(QLatin1Literal("fuel"), QLatin1Literal("fuel"), GridViewRainbow); |
||
101 | return mNames; |
||
650 | werner | 102 | |
103 | } |
||
104 | |||
105 | //********************************************************************************* |
||
106 | //****************************************** FireModule *************************** |
||
107 | //********************************************************************************* |
||
108 | |||
109 | |||
110 | |||
646 | werner | 111 | FireModule::FireModule() |
112 | { |
||
649 | werner | 113 | mFireLayers.setGrid(mRUGrid); |
654 | werner | 114 | mWindSpeedMin=10.;mWindSpeedMax=10.; |
115 | mWindDirection=45.; |
||
662 | werner | 116 | mFireId = 0; |
646 | werner | 117 | } |
118 | |||
119 | // access data element |
||
649 | werner | 120 | FireRUData &FireModule::data(const ResourceUnit *ru) |
646 | werner | 121 | { |
122 | QPointF p = ru->boundingBox().center(); |
||
649 | werner | 123 | return mRUGrid.valueAt(p.x(), p.y()); |
646 | werner | 124 | } |
125 | void FireModule::setup() |
||
126 | { |
||
127 | // setup the grid (using the size/resolution) |
||
649 | werner | 128 | mRUGrid.setup(GlobalSettings::instance()->model()->RUgrid().metricRect(), |
654 | werner | 129 | GlobalSettings::instance()->model()->RUgrid().cellsize()); |
649 | werner | 130 | // setup the fire spread grid |
131 | mGrid.setup(mRUGrid.metricRect(), cellsize()); |
||
132 | mGrid.initialize(0.f); |
||
680 | werner | 133 | mFireId = 0; |
646 | werner | 134 | |
654 | werner | 135 | // set some global settings |
646 | werner | 136 | XmlHelper xml(GlobalSettings::instance()->settings().node("modules.fire")); |
654 | werner | 137 | mWindSpeedMin = xml.valueDouble(".wind.speedMin", 5.); |
138 | mWindSpeedMax = xml.valueDouble(".wind.speedMax", 10.); |
||
139 | mWindDirection = xml.valueDouble(".wind.direction", 270.); // defaults to "west" |
||
656 | werner | 140 | mFireSizeSigma = xml.valueDouble(".fireSizeSigma", 0.25); |
654 | werner | 141 | |
680 | werner | 142 | mOnlyFireSimulation = xml.valueBool(".onlySimulation", false); |
143 | |||
667 | werner | 144 | // fuel parameters |
668 | werner | 145 | mFuelkFC1 = xml.valueDouble(".fuelKFC1", 0.8); |
146 | mFuelkFC2 = xml.valueDouble(".fuelKFC2", 0.2); |
||
147 | mFuelkFC3 = xml.valueDouble(".fuelKFC3", 0.4); |
||
667 | werner | 148 | |
149 | // parameters for crown kill |
||
150 | mCrownKillkCK1 = xml.valueDouble(".crownKill1", 0.21111); |
||
694 | werner | 151 | mCrownKillkCK2 = xml.valueDouble(".crownKill2", -0.00445); |
667 | werner | 152 | mCrownKillDbh = xml.valueDouble(".crownKillDbh", 40.); |
153 | |||
154 | QString formula = xml.value(".mortalityFormula", "1/(1 + exp(-1.466 + 1.91*bt - 0.1775*bt*bt - 5.41*ck*ck))"); |
||
155 | mFormula_bt = mMortalityFormula.addVar("bt"); |
||
156 | mFormula_ck = mMortalityFormula.addVar("ck"); |
||
157 | mMortalityFormula.setExpression(formula); |
||
158 | |||
668 | werner | 159 | mBurnSoilBiomass = xml.valueDouble(".burnSOMFraction", 0.); |
160 | mBurnStemFraction = xml.valueDouble(".burnStemFraction", 0.1); |
||
161 | mBurnBranchFraction = xml.valueDouble(".burnBranchFraction", 0.5); |
||
162 | mBurnFoliageFraction = xml.valueDouble(".burnFoliageFraction", 1.); |
||
696 | werner | 163 | |
164 | mAfterFireEvent = xml.value(".onAfterFire"); |
||
165 | |||
646 | werner | 166 | // setup of the visualization of the grid |
647 | werner | 167 | GlobalSettings::instance()->controller()->addLayers(&mFireLayers, "fire"); |
651 | werner | 168 | GlobalSettings::instance()->controller()->addGrid(&mGrid, "fire spread", GridViewRainbow,0., 50.); |
646 | werner | 169 | |
654 | werner | 170 | // check if we have a DEM in the system |
171 | if (!GlobalSettings::instance()->model()->dem()) |
||
172 | throw IException("FireModule:setup: a digital elevation model is required for the fire module!"); |
||
646 | werner | 173 | } |
174 | |||
175 | void FireModule::setup(const ResourceUnit *ru) |
||
176 | { |
||
649 | werner | 177 | if (mRUGrid.isEmpty()) |
646 | werner | 178 | throw IException("FireModule: grid not properly setup!"); |
649 | werner | 179 | FireRUData &fire_data = data(ru); |
646 | werner | 180 | fire_data.setup(); |
181 | } |
||
182 | |||
649 | werner | 183 | /** yearBegin is called at the beginnig of every year. |
184 | do some cleanup here. |
||
185 | */ |
||
186 | void FireModule::yearBegin() |
||
187 | { |
||
683 | werner | 188 | // setting KBDI=0 is not really necessary; in addition: kbdi-grids are emtpy if grid export is called during management (between yearBegin() and run()) |
189 | //for (FireRUData *fd = mRUGrid.begin(); fd!=mRUGrid.end(); ++fd) |
||
190 | // fd->reset(); // reset drought index |
||
649 | werner | 191 | } |
192 | |||
193 | /** main function of the fire module. |
||
194 | |||
195 | */ |
||
196 | void FireModule::run() |
||
197 | { |
||
701 | werner | 198 | if (GlobalSettings::instance()->settings().valueBool("modules.fire.enabled") == false) |
199 | return; |
||
200 | |||
649 | werner | 201 | // ignition() calculates ignition and calls 'spread()' if a new fire is created. |
202 | ignition(); |
||
203 | } |
||
204 | |||
205 | |||
646 | werner | 206 | /** perform the calculation of the KBDI drought index. |
207 | see http://iland.boku.ac.at/wildfire#fire_ignition |
||
208 | */ |
||
209 | void FireModule::calculateDroughtIndex(const ResourceUnit *resource_unit, const WaterCycleData *water_data) |
||
210 | { |
||
649 | werner | 211 | FireRUData &fire_data = data(resource_unit); |
646 | werner | 212 | const ClimateDay *end = resource_unit->climate()->end(); |
213 | int iday = 0; |
||
654 | werner | 214 | double kbdi = 100.; // starting value of the year |
646 | werner | 215 | const double mean_ap = fire_data.mRefAnnualPrecipitation; // reference mean annual precipitation |
216 | double dp, dq, tmax; |
||
654 | werner | 217 | |
681 | werner | 218 | // debug!!! |
219 | // QFile dump("e:/kbdidump.txt"); |
||
220 | // dump.open(QFile::WriteOnly); |
||
221 | // QTextStream ts(&dump); |
||
222 | |||
654 | werner | 223 | double kbdi_sum = 0.; |
646 | werner | 224 | for (const ClimateDay *day = resource_unit->climate()->begin(); day!=end; ++day, ++iday) { |
225 | dp = water_data->water_to_ground[iday]; // water reaching the ground for this day |
||
226 | double wetting = - dp/25.4 * 100.; |
||
227 | kbdi += wetting; |
||
228 | if (kbdi<0.) kbdi=0.; |
||
229 | |||
654 | werner | 230 | tmax = day->max_temperature; |
650 | werner | 231 | // drying is only simulated, if: |
877 | werner | 232 | // * the temperature > 10� |
646 | werner | 233 | // * there is no snow cover |
234 | if (tmax > 10. && water_data->snow_cover[iday]==0.) { |
||
235 | // calculate drying: (kbdi already includes current wetting!) |
||
236 | dq = 0.001*(800.-kbdi)*( (0.9676*exp(0.0486*(tmax*9./5.+32.))-8.299) / (1 + 10.88*exp(-0.0441*mean_ap/25.4)) ); |
||
237 | |||
238 | kbdi += dq; |
||
239 | } |
||
654 | werner | 240 | kbdi_sum += kbdi; |
681 | werner | 241 | // // debug |
242 | // ts << iday << ";" << water_data->water_to_ground[iday] << ";" << water_data->snow_cover[iday] << ";" << tmax << ";" << kbdi << endl; |
||
646 | werner | 243 | } |
654 | werner | 244 | // the effective relative KBDI is calculated |
245 | // as the year sum related to the maximum value (800*365) |
||
246 | fire_data.mKBDI = kbdi_sum / (365. * 800.); |
||
646 | werner | 247 | } |
248 | |||
249 | |||
649 | werner | 250 | /** evaluates the probability that a fire starts for each cell (20x20m) |
251 | see http://iland.boku.ac.at/wildfire#fire_ignition |
||
252 | |||
253 | */ |
||
757 | werner | 254 | double FireModule::ignition(bool only_ignite) |
649 | werner | 255 | { |
654 | werner | 256 | |
649 | werner | 257 | int cells_per_ru = (cRUSize / cellsize()) * (cRUSize / cellsize()); // number of fire cells per resource unit |
258 | |||
259 | for (FireRUData *fd = mRUGrid.begin(); fd!=mRUGrid.end(); ++fd) |
||
260 | if (fd->enabled() && fd->kbdi()>0.) { |
||
261 | // calculate the probability that a fire ignites within this resource unit |
||
262 | // the climate factor is the current drought index relative to the reference drought index |
||
654 | werner | 263 | double odds_base = fd->mBaseIgnitionProb / (1. - fd->mBaseIgnitionProb); |
649 | werner | 264 | double r_climate = fd->mKBDI / fd->mKBDIref; |
265 | double odds = odds_base * r_climate / fd->mRefMgmt; |
||
266 | // p_cell is the ignition probability for one 20x20m cell |
||
267 | double p_cell = odds / (1. + odds); |
||
758 | werner | 268 | // p_cell is the probability of ignition for a "fire"-pixel. We scale that to RU-level by multiplying with the number of pixels per RU. |
269 | // for small probabilities this yields almost the same results as the more correct 1-(1-p)^cells_per_ru [for p=0.0000001 and cells=25 the difference is 0.000000000003] |
||
270 | p_cell *= cells_per_ru; |
||
649 | werner | 271 | if (!p_cell) |
272 | continue; |
||
273 | |||
758 | werner | 274 | double p = drandom(); |
649 | werner | 275 | |
758 | werner | 276 | if (p < p_cell) { |
277 | // We have a fire event on the particular resource unit |
||
278 | // now randomly select a pixel within the resource unit as the starting point |
||
1164 | werner | 279 | int pixel_index = irandom(0, cells_per_ru); |
758 | werner | 280 | int ix = pixel_index % (int((cRUSize / cellsize()))); |
281 | int iy = pixel_index / (int((cRUSize / cellsize()))); |
||
282 | QPointF startcoord = mRUGrid.cellRect(mRUGrid.indexOf(fd)).bottomLeft(); |
||
283 | fireStats.startpoint = QPointF(startcoord.x() + (ix+0.5)*cellsize(), startcoord.y() + (iy+0.5)*cellsize() ); |
||
284 | QPoint startpoint = mGrid.indexAt(fireStats.startpoint); |
||
757 | werner | 285 | |
758 | werner | 286 | // check if we have enough fuel to start the fire: done in the spread routine |
287 | // in this case "empty" fires (with area=0) are in the output |
||
649 | werner | 288 | |
758 | werner | 289 | // now start the fire!!! |
290 | mFireId++; // this fire gets a new id |
||
291 | if (only_ignite) { |
||
292 | int idx, gen, refill; |
||
293 | RandomGenerator::debugState(idx, gen, refill); |
||
665 | werner | 294 | |
802 | werner | 295 | //qDebug() << "test: rand-sum" << test_rand_sum << "n" << test_rand_n << pixel_index << startpoint << p_cell<< "rng:" << idx << gen << refill; |
758 | werner | 296 | return p; // no real fire spread |
297 | } |
||
710 | werner | 298 | |
758 | werner | 299 | spread(startpoint); |
300 | |||
301 | // finalize statistics after fire event |
||
302 | afterFire(); |
||
303 | |||
304 | // provide outputs: This calls the FireOut::exec() function |
||
305 | GlobalSettings::instance()->outputManager()->execute("fire"); |
||
306 | |||
307 | // we allow only one fire event per year for the whole landscape |
||
308 | return fireStats.fire_size_realized_m2; |
||
649 | werner | 309 | } |
758 | werner | 310 | |
649 | werner | 311 | } |
757 | werner | 312 | return -1.; // nothing burnt |
649 | werner | 313 | |
314 | } |
||
315 | |||
316 | /** calculate the actual fire spread. |
||
317 | */ |
||
674 | werner | 318 | void FireModule::spread(const QPoint &start_point, const bool prescribed) |
649 | werner | 319 | { |
320 | qDebug() << "fire event starting at position" << start_point; |
||
650 | werner | 321 | |
322 | mGrid.initialize(0.f); |
||
649 | werner | 323 | mGrid.valueAtIndex(start_point) = 1.f; |
694 | werner | 324 | for (FireRUData *fds = mRUGrid.begin(); fds!=mRUGrid.end(); ++fds) |
325 | fds->fireRUStats.clear(); |
||
649 | werner | 326 | |
674 | werner | 327 | if (!prescribed) { |
328 | // randomly choose windspeed and wind direction |
||
329 | mCurrentWindSpeed = nrandom(mWindSpeedMin, mWindSpeedMax); |
||
330 | mCurrentWindDirection = fmod(mWindDirection + nrandom(-45., 45.)+360., 360.); |
||
331 | mPrescribedFiresize = -1; |
||
332 | } |
||
654 | werner | 333 | |
650 | werner | 334 | // choose spread algorithm |
335 | probabilisticSpread(start_point); |
||
649 | werner | 336 | |
662 | werner | 337 | |
650 | werner | 338 | } |
646 | werner | 339 | |
650 | werner | 340 | /// Estimate fire size (m2) from a fire size distribution. |
680 | werner | 341 | double FireModule::calculateFireSize(const FireRUData *data) |
646 | werner | 342 | { |
680 | werner | 343 | // calculate fire size based on a negative exponential firesize distribution |
344 | double size = -log(drandom()) * data->mAverageFireSize; |
||
345 | size = qMin(size, data->mMaxFireSize); |
||
346 | size = qMax(size, data->mMinFireSize); |
||
656 | werner | 347 | return size; |
680 | werner | 348 | |
349 | // old code: uses a log normal distribution -- currently not used: |
||
350 | // SimpleRNG rng; |
||
351 | // rng.SetState(irandom(0, std::numeric_limits<unsigned int>::max()-1), irandom(0, std::numeric_limits<unsigned int>::max()-1)); |
||
352 | // double size = rng.GetLogNormal(log(average_fire_size), mFireSizeSigma); |
||
353 | // return size; |
||
646 | werner | 354 | } |
355 | |||
654 | werner | 356 | /// calculate effect of slope on fire spread |
357 | /// for upslope following Keene and Albini 1976 |
||
358 | /// It was designed by RKeane (2/2/99) (calc.c) |
||
359 | /// the downslope function is "not based on empirical data" (Keane in calc.c) |
||
360 | /// return is the metric distance to spread (and not number of pixels) |
||
361 | double FireModule::calcSlopeFactor(const double slope) const |
||
362 | { |
||
363 | double slopespread; /* Slope spread rate in pixels / timestep */ |
||
364 | static double firebgc_cellsize = 30.; /* cellsize for which this functions were originally designed */ |
||
652 | werner | 365 | |
654 | werner | 366 | if (slope < 0.) { |
367 | // downslope effect |
||
368 | slopespread = 1.0 - ( 20.0 * slope * slope ); |
||
369 | |||
370 | } else { |
||
371 | // upslope effect |
||
372 | static double alpha = 4.0; /* Maximum number of pixels to spread */ |
||
373 | static double beta = 3.5; /* Scaling coeff for inflection point */ |
||
374 | static double gamma = 10.0;/* Scaling coeff for graph steepness */ |
||
375 | static double zeta = 0.0; /* Scaling coeff for y intercept */ |
||
376 | |||
377 | slopespread = zeta + ( alpha / ( 1.0 + ( beta * exp( -gamma * slope ) ) ) ); |
||
378 | } |
||
379 | |||
380 | |||
381 | return( slopespread ) * firebgc_cellsize; |
||
382 | |||
383 | } |
||
384 | |||
385 | /// calculate the effect of wind on the spread. |
||
386 | /// function designed by R. Keane, 2/2/99 |
||
387 | /// @param direction direction (in degrees) of spread (0=north, 90=east, ...) |
||
388 | /// @return spread (in meters) |
||
389 | double FireModule::calcWindFactor(const double direction) const |
||
646 | werner | 390 | { |
654 | werner | 391 | const double firebgc_cellsize = 30.; /* cellsize for which this functions were originally designed */ |
392 | double windspread; /* Wind spread rate in pixels / timestep */ |
||
393 | double coeff; /* Coefficient that reflects wind direction*/ |
||
394 | double lwr; /* Length to width ratio */ |
||
395 | const double alpha = 0.6; /* Wind spread power coeffieicnt */ |
||
396 | const double MPStoMPH = 1. / 0.44704; |
||
397 | |||
398 | /* .... If zero wind speed return 1.0 for the factor .... */ |
||
399 | if ( mCurrentWindSpeed <= 0.5 ) |
||
400 | return ( 1.0 ) * firebgc_cellsize; // not 0???? |
||
401 | |||
402 | /* .... Change degrees to radians .... */ |
||
403 | coeff = fabs( direction - mCurrentWindDirection ) * M_PI/180.; |
||
404 | |||
405 | /* .... If spread direction equal zero, then spread direction = wind direct */ |
||
406 | if ( direction <= 0.01 ) |
||
407 | coeff = 0.0; |
||
408 | |||
409 | /* .... Compute the length:width ratio from Andrews (1986) ..... */ |
||
410 | |||
411 | lwr = 1.0 + ( 0.125 * mCurrentWindSpeed * MPStoMPH ); |
||
412 | |||
413 | /* .... Scale the difference between direction between 0 and 1.0 ..... */ |
||
414 | coeff = ( cos( coeff ) + 1.0 ) / 2.0; |
||
415 | |||
416 | /* .... Scale the function based on windspeed between 1 and 10... */ |
||
417 | windspread = pow( coeff, pow( (mCurrentWindSpeed * MPStoMPH ), alpha ) ) * lwr; |
||
418 | |||
419 | return( windspread ) * firebgc_cellsize; |
||
420 | |||
421 | } |
||
422 | |||
423 | |||
656 | werner | 424 | /** calculates probability of spread from one pixel to one neighbor. |
425 | In this functions the effect of the terrain, the wind and others are used to estimate a probability. |
||
426 | @param fire_data reference to the variables valid for the current resource unit |
||
427 | @param height elevation (m) of the origin point |
||
428 | @param pixel_from pointer to the origin point in the fire grid |
||
429 | @param pixel_to pointer to the target pixel |
||
1170 | werner | 430 | @param direction codes the direction from the origin point (1..8, N, E, S, W, NE, SE, SW, NW) |
656 | werner | 431 | */ |
654 | werner | 432 | void FireModule::calculateSpreadProbability(const FireRUData &fire_data, const double height, const float *pixel_from, float *pixel_to, const int direction) |
433 | { |
||
1170 | werner | 434 | const double directions[8]= {0., 90., 180., 270., 45., 135., 225., 315. }; |
765 | werner | 435 | (void) pixel_from; // remove 'unused parameter' warning |
654 | werner | 436 | double spread_metric; // distance that fire supposedly spreads |
437 | |||
438 | // calculate the slope from the curent point (pixel_from) to the spreading cell (pixel_to) |
||
439 | double h_to = GlobalSettings::instance()->model()->dem()->elevation(mGrid.cellCenterPoint(mGrid.indexOf(pixel_to))); |
||
1170 | werner | 440 | if (h_to==-1) { |
764 | werner | 441 | // qDebug() << "invalid elevation for pixel during fire spread: " << mGrid.cellCenterPoint(mGrid.indexOf(pixel_to)); |
442 | // the pixel is "outside" the project area. No spread is possible. |
||
654 | werner | 443 | return; |
444 | } |
||
656 | werner | 445 | double pixel_size = cellsize(); |
654 | werner | 446 | // if we spread diagonal, the distance is longer: |
447 | if (direction>4) |
||
656 | werner | 448 | pixel_size *= 1.41421356; |
654 | werner | 449 | |
656 | werner | 450 | double slope = (h_to - height) / pixel_size; |
451 | |||
654 | werner | 452 | double r_wind, r_slope; // metric distance for spread |
453 | r_slope = calcSlopeFactor( slope ); // slope factor (upslope / downslope) |
||
454 | |||
656 | werner | 455 | r_wind = calcWindFactor(directions[direction-1]); // metric distance from wind |
654 | werner | 456 | |
457 | spread_metric = r_slope + r_wind; |
||
458 | |||
656 | werner | 459 | double spread_pixels = spread_metric / pixel_size; |
460 | if (spread_pixels<=0.) |
||
654 | werner | 461 | return; |
462 | |||
463 | double p_spread = pow(0.5, 1. / spread_pixels); |
||
464 | // apply the r_land factor that accounts for different land types |
||
465 | p_spread *= fire_data.mRefLand; |
||
652 | werner | 466 | // add probabilites |
1170 | werner | 467 | *pixel_to = 1. - (1. - *pixel_to)*(1. - p_spread); |
652 | werner | 468 | |
646 | werner | 469 | } |
470 | |||
650 | werner | 471 | /** a cellular automaton spread algorithm. |
656 | werner | 472 | @param start_point the starting point of the fire spread as index of the fire grid |
650 | werner | 473 | */ |
474 | void FireModule::probabilisticSpread(const QPoint &start_point) |
||
646 | werner | 475 | { |
1170 | werner | 476 | QRect max_spread = QRect(start_point, start_point+QPoint(1,1)); |
650 | werner | 477 | // grow the rectangle by one row/column but ensure validity |
651 | werner | 478 | max_spread.setCoords(qMax(start_point.x()-1,0),qMax(start_point.y()-1,0), |
1170 | werner | 479 | qMin(max_spread.right()+1,mGrid.sizeX()),qMin(max_spread.bottom()+1,mGrid.sizeY()) ); |
656 | werner | 480 | |
662 | werner | 481 | FireRUData *rudata = &mRUGrid.valueAt( mGrid.cellCenterPoint(start_point) ); |
680 | werner | 482 | double fire_size_m2 = calculateFireSize(rudata); |
674 | werner | 483 | |
484 | // for test cases, the size of the fire is predefined. |
||
485 | if (mPrescribedFiresize>=0) |
||
486 | fire_size_m2 = mPrescribedFiresize; |
||
487 | |||
1170 | werner | 488 | fireStats.fire_size_plan_m2 = fire_size_m2; |
665 | werner | 489 | fireStats.iterations = 0; |
490 | fireStats.fire_size_realized_m2 = 0; |
||
690 | werner | 491 | fireStats.fire_psme_died = 0.; |
492 | fireStats.fire_psme_total = 0.; |
||
656 | werner | 493 | double sum_fire_size = fire_size_m2; // cumulative fire size |
494 | // calculate a factor describing how much larger/smaller the selected fire is compared to the average |
||
495 | // fire size of the ignition cell |
||
496 | double fire_scale_factor = fire_size_m2 / rudata->mAverageFireSize; |
||
497 | |||
1170 | werner | 498 | int cells_to_burn = fire_size_m2 / (cellsize() * cellsize()); |
651 | werner | 499 | int cells_burned = 1; |
656 | werner | 500 | int last_round_burned = cells_burned; |
651 | werner | 501 | int iterations = 1; |
650 | werner | 502 | // main loop |
654 | werner | 503 | float *neighbor[8]; |
650 | werner | 504 | float *p; |
662 | werner | 505 | |
506 | rudata->fireRUStats.enter(mFireId); |
||
507 | if (!burnPixel(start_point, *rudata)) { |
||
508 | // no fuel / no trees on the starting pixel |
||
509 | return; |
||
510 | } |
||
650 | werner | 511 | while (cells_burned < cells_to_burn) { |
512 | // scan the current spread area |
||
513 | // and calcuate for each pixel the probability of spread from a burning |
||
514 | // pixel to a non-burning pixel |
||
651 | werner | 515 | GridRunner<float> runner(mGrid, max_spread); |
516 | while ((p = runner.next())) { |
||
650 | werner | 517 | if (*p == 1.f) { |
802 | werner | 518 | // p==1: pixel is burning in this iteration and might spread fire to neighbors |
654 | werner | 519 | const QPointF pt = mGrid.cellCenterPoint(mGrid.indexOf(p)); |
662 | werner | 520 | FireRUData &fire_data = mRUGrid.valueAt(pt); |
521 | fire_data.fireRUStats.enter(mFireId); // setup/clear statistics if this is the first pixel in the resource unit |
||
654 | werner | 522 | double h = GlobalSettings::instance()->model()->dem()->elevation(pt); |
1170 | werner | 523 | if (h==-1) { |
760 | werner | 524 | qDebug() << "Fire-Spread: invalid elevation at " << pt.x() << "/" << pt.y(); |
525 | qDebug() << "value is: " << GlobalSettings::instance()->model()->dem()->elevation(pt); |
||
526 | return; |
||
527 | } |
||
654 | werner | 528 | |
650 | werner | 529 | // current cell is burning. |
656 | werner | 530 | // check the neighbors: get an array with neighbors |
531 | // 1-4: north, east, west, south |
||
532 | // 5-8: NE/NW/SE/SW |
||
654 | werner | 533 | runner.neighbors8(neighbor); |
650 | werner | 534 | if (neighbor[0] && *(neighbor[0])<1.f) |
654 | werner | 535 | calculateSpreadProbability(fire_data, h, p, neighbor[0], 1); |
651 | werner | 536 | if (neighbor[1] && *(neighbor[1])<1.f) |
654 | werner | 537 | calculateSpreadProbability(fire_data, h, p, neighbor[1], 2); |
651 | werner | 538 | if (neighbor[2] && *(neighbor[2])<1.f) |
654 | werner | 539 | calculateSpreadProbability(fire_data, h, p, neighbor[2], 3); |
651 | werner | 540 | if (neighbor[3] && *(neighbor[3])<1.f) |
654 | werner | 541 | calculateSpreadProbability(fire_data, h, p, neighbor[3], 4); |
542 | if (neighbor[4] && *(neighbor[4])<1.f) |
||
543 | calculateSpreadProbability(fire_data, h, p, neighbor[4], 5); |
||
544 | if (neighbor[5] && *(neighbor[5])<1.f) |
||
545 | calculateSpreadProbability(fire_data, h, p, neighbor[5], 6); |
||
656 | werner | 546 | if (neighbor[6] && *(neighbor[6])<1.f) |
654 | werner | 547 | calculateSpreadProbability(fire_data, h, p, neighbor[6], 7); |
656 | werner | 548 | if (neighbor[7] && *(neighbor[7])<1.f) |
654 | werner | 549 | calculateSpreadProbability(fire_data, h, p, neighbor[7], 8); |
651 | werner | 550 | *p = iterations + 1; |
650 | werner | 551 | } |
552 | } |
||
553 | // now draw random numbers and calculate the real spread |
||
554 | runner.reset(); |
||
651 | werner | 555 | while ((p = runner.next())) { |
650 | werner | 556 | if (*p<1.f && *p>0.f) { |
651 | werner | 557 | if (drandom() < *p) { |
650 | werner | 558 | // the fire spreads: |
651 | werner | 559 | *p = 1.f; |
662 | werner | 560 | FireRUData &fire_data = mRUGrid.valueAt(mGrid.cellCenterPoint(mGrid.indexOf(p))); |
694 | werner | 561 | fire_data.fireRUStats.enter(mFireId); |
650 | werner | 562 | cells_burned++; |
662 | werner | 563 | // do the severity calculations: |
564 | // the function returns false if no trees are on the pixel |
||
565 | bool really_burnt = burnPixel(mGrid.indexOf(p), fire_data); |
||
656 | werner | 566 | // update the fire size |
567 | sum_fire_size += fire_data.mAverageFireSize * fire_scale_factor; |
||
764 | werner | 568 | // the fire stops |
569 | // (*) if no trees were on the pixel, or |
||
570 | // (*) if the fire extinguishes |
||
699 | werner | 571 | bool spread = really_burnt; |
572 | if (spread && fire_data.mFireExtinctionProb>0.) { |
||
755 | werner | 573 | // exinguishing of fire is only effective, when at least the minimum fire size is already reached |
699 | werner | 574 | if (cells_burned*cellsize()*cellsize() > fire_data.mMinFireSize) { |
575 | if (drandom() < fire_data.mFireExtinctionProb) |
||
576 | spread = false; |
||
654 | werner | 577 | } |
699 | werner | 578 | |
654 | werner | 579 | } |
699 | werner | 580 | if (!spread) |
581 | *p = iterations + 1; |
||
582 | |||
662 | werner | 583 | } else { |
584 | *p = 0.f; // if the fire does note spread to the cell, the value is cleared again. |
||
650 | werner | 585 | } |
586 | } |
||
587 | } |
||
656 | werner | 588 | |
1170 | werner | 589 | cells_to_burn = (sum_fire_size/(double)cells_burned) / (cellsize() * cellsize()); |
656 | werner | 590 | if (cells_to_burn <= cells_burned) |
591 | break; |
||
592 | |||
651 | werner | 593 | // now determine the maximum extent with burning pixels... |
594 | runner.reset(); |
||
595 | int left = mGrid.sizeX(), right = 0, top = mGrid.sizeY(), bottom = 0; |
||
596 | while ((p = runner.next())) { |
||
597 | if (*p == 1.f) { |
||
598 | QPoint pt = mGrid.indexOf(p); |
||
599 | left = qMin(left, pt.x()-1); |
||
1170 | werner | 600 | right = qMax(right, pt.x()+2); // coord of right is never reached |
651 | werner | 601 | top = qMin(top, pt.y()-1); |
1170 | werner | 602 | bottom = qMax(bottom, pt.y()+2); // coord bottom never reacher |
651 | werner | 603 | } |
604 | } |
||
1170 | werner | 605 | max_spread.setCoords(qMax(left,0), |
606 | qMax(top,0), |
||
607 | qMin(right, mGrid.sizeX()), |
||
608 | qMin(bottom, mGrid.sizeY()) ); |
||
651 | werner | 609 | |
1170 | werner | 610 | qDebug() << "Iter: " << iterations << "totalburned:" << cells_burned << "spread-rect:" << max_spread << "cells to burn" << cells_to_burn; |
650 | werner | 611 | iterations++; |
656 | werner | 612 | if (last_round_burned == cells_burned) { |
613 | qDebug() << "Firespread: a round without new burning cells - exiting!"; |
||
651 | werner | 614 | break; |
615 | } |
||
656 | werner | 616 | last_round_burned = cells_burned; |
617 | if (iterations > 10000) { |
||
618 | qDebug() << "Firespread: maximum number of iterations (10000) reached!"; |
||
619 | break; |
||
620 | } |
||
650 | werner | 621 | } |
656 | werner | 622 | qDebug() << "Fire:probabilstic spread: used " << iterations |
623 | << "iterations. Planned (m2/cells):" << fire_size_m2 << "/" << cells_to_burn |
||
624 | << "burned (m2/cells):" << cells_burned*cellsize()*cellsize() << "/" << cells_burned; |
||
665 | werner | 625 | |
668 | werner | 626 | fireStats.iterations = iterations; |
665 | werner | 627 | fireStats.fire_size_realized_m2 = cells_burned*cellsize()*cellsize(); |
628 | |||
646 | werner | 629 | } |
630 | |||
654 | werner | 631 | void FireModule::testSpread() |
632 | { |
||
656 | werner | 633 | // QPoint pt = mGrid.indexAt(QPointF(1000., 600.)); |
634 | // spread( pt ); |
||
635 | SimpleRNG rng; |
||
1164 | werner | 636 | rng.SetState(irandom(0, std::numeric_limits<unsigned int>::max()), irandom(0, std::numeric_limits<unsigned int>::max())); |
656 | werner | 637 | int bins[20]; |
638 | for(int i=0;i<20;i++) bins[i]=0; |
||
639 | for (int i=0;i<10000;i++) { |
||
640 | double value = rng.GetLogNormal(log(2000.),0.25); |
||
641 | if (value>=0 && value<10000.) |
||
642 | bins[(int)(value/500.)]++; |
||
643 | } |
||
644 | for(int i=0;i<20;i++) |
||
645 | qDebug() << bins[i]; |
||
646 | werner | 646 | |
656 | werner | 647 | for (int r=0;r<360;r+=90) { |
648 | mWindDirection = r; |
||
649 | for (int i=0;i<5;i++) { |
||
663 | werner | 650 | QPoint pt = mGrid.indexAt(QPointF(730., 610.)); // was: 1100/750 |
662 | werner | 651 | mFireId++; // this fire gets a new id |
652 | |||
656 | werner | 653 | spread( pt ); |
662 | werner | 654 | // stats |
655 | for (FireRUData *fds = mRUGrid.begin(); fds!=mRUGrid.end(); ++fds) |
||
656 | fds->fireRUStats.calculate(mFireId); |
||
657 | |||
656 | werner | 658 | GlobalSettings::instance()->controller()->repaint(); |
659 | GlobalSettings::instance()->controller()->saveScreenshot(GlobalSettings::instance()->path(QString("%1_%2.png").arg(r).arg(i), "temp")); |
||
660 | } |
||
661 | } |
||
654 | werner | 662 | } |
649 | werner | 663 | |
664 | |||
757 | werner | 665 | double FireModule::prescribedIgnition(const double x_m, const double y_m, const double firesize, const double windspeed, const double winddirection) |
674 | werner | 666 | { |
667 | QPoint pt = mGrid.indexAt(QPointF(x_m, y_m)); |
||
668 | if (!mGrid.isIndexValid(pt)) { |
||
669 | qDebug() << "Fire starting point is not valid!"; |
||
757 | werner | 670 | return -1.; |
674 | werner | 671 | } |
672 | mFireId++; // this fire gets a new id |
||
673 | |||
755 | werner | 674 | mPrescribedFiresize = firesize; // if -1, then a fire size is estimated |
674 | werner | 675 | |
676 | if (windspeed>=0) { |
||
677 | mCurrentWindSpeed = windspeed; |
||
678 | mCurrentWindDirection = winddirection; |
||
679 | } |
||
680 | |||
681 | spread( pt, true ); |
||
682 | |||
680 | werner | 683 | afterFire(); |
674 | werner | 684 | |
681 | werner | 685 | // provide outputs: This calls the FireOut::exec() function |
686 | GlobalSettings::instance()->outputManager()->execute("fire"); |
||
690 | werner | 687 | GlobalSettings::instance()->outputManager()->save(); |
757 | werner | 688 | |
689 | return fireStats.fire_size_realized_m2; |
||
674 | werner | 690 | } |
691 | |||
662 | werner | 692 | /** burning of a single 20x20m pixel. see http://iland.boku.ac.at/wildfire. |
693 | The function is called from the fire spread function. |
||
694 | @return boolean true, if any trees were burned on the pixel |
||
649 | werner | 695 | |
662 | werner | 696 | */ |
697 | bool FireModule::burnPixel(const QPoint &pos, FireRUData &ru_data) |
||
698 | { |
||
699 | // extract a list of trees that are within the pixel boundaries |
||
700 | QRectF pixel_rect = mGrid.cellRect(pos); |
||
701 | ResourceUnit *ru = GlobalSettings::instance()->model()->ru(pixel_rect.center()); |
||
702 | if (!ru) |
||
703 | return false; |
||
649 | werner | 704 | |
662 | werner | 705 | // retrieve a list of trees within the active pixel |
665 | werner | 706 | // NOTE: the check with isDead() is necessary because dead trees could be already in the trees list |
662 | werner | 707 | QVector<Tree*> trees; |
708 | QVector<Tree>::iterator tend = ru->trees().end(); |
||
709 | for (QVector<Tree>::iterator t = ru->trees().begin(); t!=tend; ++t) { |
||
1172 | werner | 710 | if ( pixel_rect.contains( (*t).position() ) && !(*t).isDead()) { |
662 | werner | 711 | trees.push_back(&(*t)); |
1172 | werner | 712 | } |
662 | werner | 713 | } |
649 | werner | 714 | |
662 | werner | 715 | // calculate mean values for dbh |
716 | double sum_dbh = 0.; |
||
680 | werner | 717 | double sum_ba = 0.; |
760 | werner | 718 | double avg_dbh = 0.; |
680 | werner | 719 | foreach (const Tree* t, trees) { |
662 | werner | 720 | sum_dbh += t->dbh(); |
680 | werner | 721 | sum_ba += t->basalArea(); |
722 | } |
||
654 | werner | 723 | |
760 | werner | 724 | if(trees.size()>0) |
1165 | werner | 725 | avg_dbh = sum_dbh / static_cast<double>( trees.size() ); |
760 | werner | 726 | |
662 | werner | 727 | // (1) calculate fuel |
667 | werner | 728 | const double kfc1 = mFuelkFC1; |
729 | const double kfc2 = mFuelkFC2; |
||
730 | const double kfc3 = mFuelkFC3; |
||
662 | werner | 731 | // retrieve values for fuel. |
732 | // forest_floor: sum of leaves and twigs (t/ha) = yR pool |
||
733 | // DWD: downed woody debris (t/ha) = yL pool |
||
654 | werner | 734 | |
680 | werner | 735 | // fuel per ha (kg biomass): derive available fuel using the KBDI as estimate for humidity. |
1172 | werner | 736 | double fuel_ff = (kfc1 + kfc2*ru_data.kbdi()) * (ru->soil()? ru->soil()->youngLabile().biomass() * 1000. : 1000.); |
737 | double fuel_dwd = kfc3*ru_data.kbdi() * (ru->soil() ? ru->soil()->youngRefractory().biomass() * 1000. : 1000. ); |
||
680 | werner | 738 | // calculate fuel (kg biomass / ha) |
739 | double fuel = (fuel_ff + fuel_dwd); |
||
654 | werner | 740 | |
680 | werner | 741 | ru_data.fireRUStats.n_cells++; // number of cells burned in the resource unit |
742 | ru_data.fireRUStats.fuel_ff += fuel_ff; // fuel in kg/ha Biomass |
||
743 | ru_data.fireRUStats.fuel_dwd += fuel_dwd; // fuel in kg/ha Biomass |
||
663 | werner | 744 | ru_data.fireRUStats.n_trees += trees.size(); |
680 | werner | 745 | ru_data.fireRUStats.basal_area += sum_ba; |
760 | werner | 746 | |
747 | // if fuel level is below 0.05kg BM/m2 (=500kg/ha), then no burning happens! |
||
748 | // note, that it is not necessary that trees are on the pixel, as long as there is enough fuel on the ground. |
||
662 | werner | 749 | if (fuel < 500.) |
750 | return false; |
||
751 | |||
752 | // (2) calculate the "crownkill" fraction |
||
667 | werner | 753 | const double dbh_trehshold = mCrownKillDbh; // dbh |
754 | const double kck1 = mCrownKillkCK1; |
||
755 | const double kck2 = mCrownKillkCK2; |
||
662 | werner | 756 | if (avg_dbh > dbh_trehshold) |
757 | avg_dbh = dbh_trehshold; |
||
758 | |||
759 | double crown_kill_fraction = (kck1+kck2*avg_dbh)*fuel/1000.; // fuel: to t/ha |
||
691 | werner | 760 | crown_kill_fraction = limit(crown_kill_fraction, 0., 1.); // limit to 0..1 |
662 | werner | 761 | |
762 | |||
763 | // (3) derive mortality of single trees |
||
764 | double p_mort; |
||
765 | int died = 0; |
||
766 | double died_basal_area=0.; |
||
690 | werner | 767 | bool tree_is_psme; |
662 | werner | 768 | foreach (Tree* t, trees) { |
769 | // the mortality probability depends on the thickness of the bark: |
||
667 | werner | 770 | *mFormula_bt = t->barkThickness(); // cm |
771 | *mFormula_ck = crown_kill_fraction; // fraction of crown that is killed (0..1) |
||
772 | p_mort = mMortalityFormula.execute(); |
||
662 | werner | 773 | // note: 5.41 = 0.000541*10000, (fraction*fraction) = 10000 * pct*pct |
667 | werner | 774 | //p_mort = 1. / (1. + exp(-1.466 + 1.91*bt - 0.1775*bt*bt - 5.41*crown_kill_fraction*crown_kill_fraction)); |
690 | werner | 775 | tree_is_psme = t->species()->id()=="Psme"; |
776 | if (tree_is_psme) |
||
777 | fireStats.fire_psme_total += t->basalArea(); |
||
663 | werner | 778 | if (drandom() < p_mort) { |
662 | werner | 779 | // the tree actually dies. |
780 | died_basal_area += t->basalArea(); |
||
690 | werner | 781 | if (tree_is_psme) |
782 | fireStats.fire_psme_died += t->basalArea(); |
||
1172 | werner | 783 | |
784 | if (t->species()->seedDispersal() && t->species()->isTreeSerotinous(t->age()) ) { |
||
785 | //SeedDispersal *sd = t->species()->seedDispersal(); |
||
786 | t->species()->seedDispersal()->seedProductionSerotiny(t->positionIndex()); |
||
787 | } |
||
788 | |||
680 | werner | 789 | if (!mOnlyFireSimulation) { |
790 | // before tree biomass is transferred to the snag-state, a part of the biomass is combusted: |
||
1165 | werner | 791 | t->setDeathReasonFire(); |
903 | werner | 792 | t->removeBiomassOfTree(mBurnFoliageFraction, mBurnBranchFraction, mBurnStemFraction); |
1165 | werner | 793 | // kill the tree and calculate flows to soil/snags |
794 | t->removeDisturbance(0., 1., // 100% of the remaining stem goes to snags |
||
795 | 0., 1., // 100% of the remaining branches go to snags |
||
796 | 1.); // the remaining foliage goes to soil |
||
797 | |||
680 | werner | 798 | } |
662 | werner | 799 | ++died; |
800 | // some statistics??? |
||
801 | } |
||
802 | } |
||
803 | |||
804 | // update statistics |
||
805 | ru_data.fireRUStats.n_trees_died += died; |
||
806 | ru_data.fireRUStats.died_basal_area += died_basal_area; |
||
807 | ru_data.fireRUStats.crown_kill += crown_kill_fraction; |
||
693 | werner | 808 | ru_data.fireRUStats.avg_dbh += avg_dbh; |
662 | werner | 809 | |
680 | werner | 810 | if (!mOnlyFireSimulation) { |
811 | // (4) effect of forest fire on saplings: all saplings are killed. |
||
812 | // As regeneration happens before the fire routine, any newly regenarated saplings are killed as well. |
||
1162 | werner | 813 | if (GlobalSettings::instance()->model()->saplings()) |
814 | GlobalSettings::instance()->model()->saplings()->clearSaplings(pixel_rect, true); |
||
815 | //ru->clearSaplings(pixel_rect, true); [old version] |
||
680 | werner | 816 | } |
817 | return true; |
||
818 | } |
||
662 | werner | 819 | |
680 | werner | 820 | /// do some cleanup / statistics after the fire. |
821 | /// apply the effect of fire on dead wood pools and soil pools of the resource units |
||
822 | /// biomass of living trees is consumed in the burnPixel() routine. |
||
823 | void FireModule::afterFire() |
||
824 | { |
||
1157 | werner | 825 | const double pixel_fraction = cellsize()*cellsize() / cRUArea; // fraction of one pixel, default: 0.04 (20x20 / 100x100) |
662 | werner | 826 | |
680 | werner | 827 | int ru_idx=0; |
828 | for (FireRUData *fds = mRUGrid.begin(); fds!=mRUGrid.end(); ++fds, ++ru_idx) { |
||
829 | fds->fireRUStats.calculate(mFireId); |
||
830 | if (fds->fireRUStats.n_cells>0) { |
||
831 | // on this resource unit really a fire happened. |
||
832 | // so we need to update snags/soil pools |
||
833 | if (!mOnlyFireSimulation) { |
||
834 | ResourceUnit *ru = GlobalSettings::instance()->model()->ru(ru_idx); |
||
835 | double ru_fraction = fds->fireRUStats.n_cells * pixel_fraction; // fraction of RU burned (0..1) |
||
836 | |||
1172 | werner | 837 | if (ru && ru->soil()) { |
838 | // (1) effect of forest fire on the dead wood pools. fuel_dwd and fuel_ff is the amount of fuel |
||
839 | // available on the pixels that are burnt. The assumption is: all of it was burnt. |
||
1165 | werner | 840 | ru->soil()->disturbanceBiomass(fds->fireRUStats.fuel_dwd, fds->fireRUStats.fuel_ff, 0.); |
680 | werner | 841 | |
1172 | werner | 842 | // (2) remove also a fixed fraction of the biomass that is in the soil |
843 | if (mBurnSoilBiomass>0.) |
||
844 | ru->soil()->disturbance(0,0, mBurnSoilBiomass*ru_fraction); |
||
845 | // (3) effect on the snsgs |
||
1165 | werner | 846 | ru->snag()->removeCarbon(mBurnStemFraction*ru_fraction); |
1172 | werner | 847 | } |
680 | werner | 848 | } |
849 | } |
||
850 | } |
||
696 | werner | 851 | |
852 | // execute the after fire event |
||
853 | if (!mAfterFireEvent.isEmpty()) { |
||
854 | // evaluate the javascript function... |
||
767 | werner | 855 | GlobalSettings::instance()->executeJavascript(mAfterFireEvent); |
696 | werner | 856 | } |
662 | werner | 857 | } |
858 | |||
859 | |||
860 | |||
861 | |||
862 | |||
863 | |||
864 | |||
865 | |||
866 | |||
674 | werner | 867 |