Subversion Repositories public iLand

Rev

Rev 562 | Rev 572 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1
 
237 werner 2
#include "global.h"
3
#include "watercycle.h"
4
#include "climate.h"
5
#include "resourceunit.h"
6
#include "species.h"
239 werner 7
#include "model.h"
237 werner 8
 
9
WaterCycle::WaterCycle()
10
{
266 werner 11
    mSoilDepth = 0;
496 werner 12
    mLastYear = -1;
237 werner 13
}
14
 
239 werner 15
void WaterCycle::setup(const ResourceUnit *ru)
237 werner 16
{
17
    mRU = ru;
18
    // get values...
266 werner 19
    mFieldCapacity = 0.; // on top
281 werner 20
    const XmlHelper &xml=GlobalSettings::instance()->settings();
21
    mSoilDepth = xml.valueDouble("model.site.soilDepth", 0.) * 10; // convert from cm to mm
338 werner 22
    double pct_sand = xml.valueDouble("model.site.pctSand");
23
    double pct_silt = xml.valueDouble("model.site.pctSilt");
24
    double pct_clay = xml.valueDouble("model.site.pctClay");
25
    if (pct_sand + pct_silt + pct_clay != 100.)
26
        throw IException(QString("Setup Watercycle: soil composition percentages do not sum up to 100. Sand: %1, Silt: %2 Clay: %3").arg(pct_sand).arg(pct_silt).arg(pct_clay));
27
 
28
    // calculate soil characteristics based on empirical functions (Schwalm & Ek, 2004)
29
    // note: the variables are percentages [0..100]
30
    mPsi_ref = -exp((1.54 - 0.0095*pct_sand + 0.0063*pct_silt) * log(10)) * 0.000098; // Eq. 83
31
    mPsi_koeff_b = -( 3.1 + 0.157*pct_clay - 0.003*pct_sand );  // Eq. 84
32
    mRho_ref = 0.01 * (50.5 - 0.142*pct_sand - 0.037*pct_clay); // Eq. 78
240 werner 33
    mCanopy.setup();
339 werner 34
 
35
    mPermanentWiltingPoint = heightFromPsi(-4000); // maximum psi is set to a constant of -4MPa
379 werner 36
    if (xml.valueBool("model.settings.waterUseSoilSaturation",false)==false) {
37
        mFieldCapacity = heightFromPsi(-15);
38
    } else {
39
        // =-EXP((1.54-0.0095* pctSand +0.0063* pctSilt)*LN(10))*0.000098
40
        double psi_sat = -exp((1.54-0.0095 * pct_sand + 0.0063*pct_silt)*log(10.))*0.000098;
41
        mFieldCapacity = heightFromPsi(psi_sat);
431 werner 42
        if (logLevelDebug()) qDebug() << "psi: saturation " << psi_sat << "field capacity:" << mFieldCapacity;
379 werner 43
    }
44
 
266 werner 45
    mContent = mFieldCapacity; // start with full water content (in the middle of winter)
431 werner 46
    if (logLevelDebug()) qDebug() << "setup of water: Psi_ref (kPa)" << mPsi_ref << "Rho_ref" << mRho_ref << "coeff. b" << mPsi_koeff_b;
566 werner 47
    mCanopyConductance = 0.;
496 werner 48
    mLastYear = -1;
237 werner 49
}
50
 
331 werner 51
/** function to calculate the water pressure [saugspannung] for a given amount of water.
338 werner 52
    returns water potential in kPa.
53
  see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance */
266 werner 54
inline double WaterCycle::psiFromHeight(const double mm) const
55
{
56
    // psi_x = psi_ref * ( rho_x / rho_ref) ^ b
57
    if (mm<0.001)
58
        return -100000000;
59
    double psi_x = mPsi_ref * pow((mm / mSoilDepth / mRho_ref),mPsi_koeff_b);
338 werner 60
    return psi_x; // pis
266 werner 61
}
62
 
331 werner 63
/// calculate the height of the water column for a given pressure
338 werner 64
/// return water amount in mm
65
/// see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
266 werner 66
inline double WaterCycle::heightFromPsi(const double psi_kpa) const
67
{
68
    // rho_x = rho_ref * (psi_x / psi_ref)^(1/b)
69
    double h = mSoilDepth * mRho_ref * pow(psi_kpa / mPsi_ref, 1./mPsi_koeff_b);
70
    return h;
71
}
72
 
331 werner 73
/// get canopy characteristics of the resource unit.
74
/// It is important, that species-statistics are valid when this function is called (LAI)!
237 werner 75
void WaterCycle::getStandValues()
76
{
246 werner 77
    mLAINeedle=mLAIBroadleaved=0.;
237 werner 78
    mCanopyConductance=0.;
553 werner 79
    const double ground_vegetationCC = 0.02;
237 werner 80
    double lai;
455 werner 81
    foreach(ResourceUnitSpecies *rus, mRU->ruSpecies()) {
82
        lai = rus->constStatistics().leafAreaIndex();
83
        if (rus->species()->isConiferous())
237 werner 84
            mLAINeedle+=lai;
85
        else
86
            mLAIBroadleaved+=lai;
455 werner 87
        mCanopyConductance += rus->species()->canopyConductance() * lai; // weigh with LAI
237 werner 88
    }
89
    double total_lai = mLAIBroadleaved+mLAINeedle;
502 werner 90
 
91
    // handle cases with LAI < 1 (use generic "ground cover characteristics" instead)
92
    if (total_lai<1.) {
553 werner 93
        mCanopyConductance+=(ground_vegetationCC)*(1. - total_lai);
502 werner 94
        total_lai = 1.;
237 werner 95
    }
502 werner 96
    mCanopyConductance /= total_lai;
97
 
368 werner 98
    if (total_lai < Model::settings().laiThresholdForClosedStands) {
99
        // following Landsberg and Waring: when LAI is < 3 (default for laiThresholdForClosedStands), a linear "ramp" from 0 to 3 is assumed
299 werner 100
        // http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
368 werner 101
        mCanopyConductance *= total_lai / Model::settings().laiThresholdForClosedStands;
237 werner 102
    }
431 werner 103
    if (logLevelInfo()) qDebug() << "WaterCycle:getStandValues: LAI needle" << mLAINeedle << "LAI Broadl:"<< mLAIBroadleaved << "weighted avg. Conductance (m/2):" << mCanopyConductance;
237 werner 104
}
105
 
502 werner 106
/// calculate responses for ground vegetation, i.e. for "unstocked" areas.
107
/// this duplicates calculations done in Species.
108
/// @return Minimum of vpd and soilwater response for default
109
inline double WaterCycle::calculateBaseSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
110
{
111
    // constant parameters used for ground vegetation:
503 werner 112
    const double mPsiMin = 1.5; // MPa
502 werner 113
    const double mRespVpdExponent = -0.6;
114
    // see SpeciesResponse::soilAtmosphereResponses()
115
    double water_resp;
116
    // see Species::soilwaterResponse:
117
    const double psi_mpa = psi_kpa / 1000.; // convert to MPa
118
    water_resp = limit( 1. - psi_mpa / mPsiMin, 0., 1.);
119
    // see species::vpdResponse
120
 
121
    double vpd_resp;
122
    vpd_resp =  exp(mRespVpdExponent * vpd_kpa);
123
    return qMin(water_resp, vpd_resp);
124
}
125
 
367 werner 126
/// calculate combined VPD and soilwaterresponse for all species
127
/// on the RU. This is used for the calc. of the transpiration.
128
inline double WaterCycle::calculateSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
129
{
130
    double min_response;
502 werner 131
    double total_response = 0; // LAI weighted minimum response for all speices on the RU
132
    double total_lai_factor = 0.;
455 werner 133
    foreach(ResourceUnitSpecies *rus, mRU->ruSpecies()) {
134
        if (rus->LAIfactor()>0) {
502 werner 135
            // retrieve the minimum of VPD / soil water response for that species
455 werner 136
            rus->speciesResponse()->soilAtmosphereResponses(psi_kpa, vpd_kpa, min_response);
137
            total_response += min_response * rus->LAIfactor();
502 werner 138
            total_lai_factor += rus->LAIfactor();
367 werner 139
        }
140
    }
455 werner 141
 
502 werner 142
    if (total_lai_factor<1.) {
143
        // the LAI is below 1: the rest is considered as "ground vegetation"
144
        total_response += calculateBaseSoilAtmosphereResponse(psi_kpa, vpd_kpa) * (1. - total_lai_factor);
145
    }
146
 
377 werner 147
    // add an aging factor to the total response (averageAging: leaf area weighted mean aging value):
148
    // conceptually: response = min(vpd_response, water_response)*aging
503 werner 149
    if (total_lai_factor==1.)
150
        total_response *= mRU->averageAging(); // no ground cover: use aging value for all LA
151
    else if (total_lai_factor>0. && mRU->averageAging()>0.)
152
        total_response *= (1.-total_lai_factor)*1. + (total_lai_factor * mRU->averageAging()); // between 0..1: a part of the LAI is "ground cover" (aging=1)
502 werner 153
 
504 werner 154
    DBGMODE( if (mRU->averageAging()>1. || mRU->averageAging()<0. || total_response<0 || total_response>1.)
155
        qDebug() << "water cycle: average aging invalid. aging:" << mRU->averageAging() << "total response" << total_response;
484 werner 156
    );
482 werner 157
 
158
    //DBG_IF(mRU->averageAging()>1. || mRU->averageAging()<0.,"water cycle", "average aging invalid!" );
367 werner 159
    return total_response;
160
}
161
 
162
 
237 werner 163
/// Main Water Cycle function. This function triggers all water related tasks for
164
/// one simulation year.
299 werner 165
/// @sa http://iland.boku.ac.at/water+cycle
237 werner 166
void WaterCycle::run()
167
{
496 werner 168
    // necessary?
169
    if (GlobalSettings::instance()->currentYear() == mLastYear)
170
        return;
237 werner 171
    // preparations (once a year)
367 werner 172
    getStandValues(); // fetch canopy characteristics from iLand (including weighted average for mCanopyConductance)
237 werner 173
    mCanopy.setStandParameters(mLAINeedle,
174
                               mLAIBroadleaved,
175
                               mCanopyConductance);
176
 
177
    // main loop over all days of the year
239 werner 178
    double prec_mm, prec_after_interception, prec_to_soil, et, excess;
338 werner 179
    const Climate *climate = mRU->climate();
180
    const ClimateDay *day = climate->begin();
181
    const ClimateDay *end = climate->end();
237 werner 182
    int doy=0;
183
    double total_excess = 0.;
184
    for (; day<end; ++day, ++doy) {
185
        // (1) precipitation of the day
186
        prec_mm = day->preciptitation;
187
        // (2) interception by the crown
188
        prec_after_interception = mCanopy.flow(prec_mm, day->temperature);
189
        // (3) storage in the snow pack
190
        prec_to_soil = mSnowPack.flow(prec_after_interception, day->temperature);
191
        // (4) add rest to soil
192
        mContent += prec_to_soil;
266 werner 193
 
194
        excess = 0.;
195
        if (mContent>mFieldCapacity) {
196
            // excess water runoff
197
            excess = mContent - mFieldCapacity;
198
            total_excess += excess;
199
            mContent = mFieldCapacity;
200
        }
201
 
367 werner 202
        double current_psi = psiFromHeight(mContent);
203
        mPsi[doy] = current_psi;
553 werner 204
 
335 werner 205
        // (5) transpiration of the vegetation (and of water intercepted in canopy)
367 werner 206
        // calculate the LAI-weighted response values for soil water and vpd:
207
        double combined_response = calculateSoilAtmosphereResponse( current_psi, day->vpd);
208
        et = mCanopy.evapotranspiration3PG(day, climate->daylength_h(doy), combined_response);
238 werner 209
 
241 werner 210
        mContent -= et; // reduce content (transpiration)
338 werner 211
        // add intercepted water (that is *not* evaporated) again to the soil (or add to snow if temp too low -> call to snowpack)
212
        mContent += mSnowPack.add(mCanopy.interception(),day->temperature);
241 werner 213
 
338 werner 214
        // do not remove water below the PWP (fixed value)
266 werner 215
        if (mContent<mPermanentWiltingPoint) {
271 werner 216
            et -= mPermanentWiltingPoint - mContent; // reduce et (for bookkeeping)
266 werner 217
            mContent = mPermanentWiltingPoint;
237 werner 218
        }
266 werner 219
 
546 werner 220
 
271 werner 221
        //DBGMODE(
239 werner 222
            if (GlobalSettings::instance()->isDebugEnabled(GlobalSettings::dWaterCycle)) {
240 werner 223
                DebugList &out = GlobalSettings::instance()->debugList(day->id(), GlobalSettings::dWaterCycle);
239 werner 224
                // climatic variables
364 werner 225
                out << day->id() << mRU->index() << day->temperature << day->vpd << day->preciptitation << day->radiation;
368 werner 226
                out << combined_response; // combined response of all species on RU (min(water, vpd))
239 werner 227
                // fluxes
271 werner 228
                out << prec_after_interception << prec_to_soil << et << mCanopy.evaporationCanopy()
540 werner 229
                        << mContent << mPsi[doy] << excess;
239 werner 230
                // other states
231
                out << mSnowPack.snowPack();
364 werner 232
                //special sanity check:
233
                if (prec_to_soil>0. && mCanopy.interception()>0.)
234
                    if (mSnowPack.snowPack()==0. && day->preciptitation==0)
235
                        qDebug() << "watercontent increase without precipititaion";
239 werner 236
 
237
            }
271 werner 238
        //); // DBGMODE()
239 werner 239
 
237 werner 240
    }
496 werner 241
    mLastYear = GlobalSettings::instance()->currentYear();
242
 
237 werner 243
}
244
 
245
 
246
namespace Water {
247
 
248
/** calculates the input/output of water that is stored in the snow pack.
249
  The approach is similar to Picus 1.3 and ForestBGC (Running, 1988).
250
  Returns the amount of water that exits the snowpack (precipitation, snow melt) */
251
double SnowPack::flow(const double &preciptitation_mm, const double &temperature)
252
{
253
    if (temperature>0.) {
254
        if (mSnowPack==0.)
255
            return preciptitation_mm; // no change
256
        else {
257
            // snow melts
258
            const double melting_coefficient = 0.7; // mm/°C
240 werner 259
            double melt = qMin(temperature * melting_coefficient, mSnowPack);
260
            mSnowPack -=melt;
237 werner 261
            return preciptitation_mm + melt;
262
        }
263
    } else {
264
        // snow:
265
        mSnowPack += preciptitation_mm;
266
        return 0.; // no output.
267
    }
268
 
269
}
270
 
271
 
334 werner 272
inline double SnowPack::add(const double &preciptitation_mm, const double &temperature)
273
{
274
    // do nothing for temps > 0°
275
    if (temperature>0.)
276
        return preciptitation_mm;
277
 
278
    // temps < 0°: add to snow
279
    mSnowPack += preciptitation_mm;
280
    return 0.;
281
}
282
 
237 werner 283
/** Interception in crown canopy.
284
    Calculates the amount of preciptitation that does not reach the ground and
285
    is stored in the canopy. The approach is adopted from Picus 1.3.
299 werner 286
    Returns the amount of precipitation (mm) that surpasses the canopy layer.
287
    @sa http://iland.boku.ac.at/water+cycle#precipitation_and_interception */
237 werner 288
double Canopy::flow(const double &preciptitation_mm, const double &temperature)
289
{
290
    // sanity checks
291
    mInterception = 0.;
271 werner 292
    mEvaporation = 0.;
237 werner 293
    if (!mLAI)
294
        return preciptitation_mm;
295
    if (!preciptitation_mm)
296
        return 0.;
297
    double max_interception_mm=0.; // maximum interception based on the current foliage
298
    double max_storage_mm=0.; // maximum storage in canopy
299
 
300
    if (mLAINeedle>0.) {
301
        // (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
302
        double max_flow_needle = 0.9 * sqrt(1.03 - exp(-0.055*preciptitation_mm));
303
        max_interception_mm += preciptitation_mm *  (1. - max_flow_needle * mLAINeedle/mLAI);
304
        // (2) calculate maximum storage potential based on the current LAI
305
        double max_storage_needle = 4. * (1. - exp(-0.55*mLAINeedle) );
306
        max_storage_mm += max_storage_needle;
307
    }
308
 
309
    if (mLAIBroadleaved>0.) {
310
        // (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
299 werner 311
        double max_flow_broad = 0.9 * pow(1.22 - exp(-0.055*preciptitation_mm), 0.35);
237 werner 312
        max_interception_mm += preciptitation_mm *  (1. - max_flow_broad * mLAIBroadleaved/mLAI);
313
        // (2) calculate maximum storage potential based on the current LAI
299 werner 314
        double max_storage_broad = 2. * (1. - exp(-0.5*mLAIBroadleaved) );
237 werner 315
        max_storage_mm += max_storage_broad;
316
    }
317
 
318
    // (3) calculate actual interception and store for evaporation calculation
319
    mInterception = qMin( max_storage_mm, max_interception_mm );
320
 
335 werner 321
    // (4) limit interception with amount of precipitation
322
    mInterception = qMin( mInterception, preciptitation_mm);
323
 
324
    // (5) reduce preciptitaion by the amount that is intercepted by the canopy
237 werner 325
    return preciptitation_mm - mInterception;
326
 
327
}
328
 
239 werner 329
/// sets up the canopy. fetch some global parameter values...
330
void Canopy::setup()
237 werner 331
{
240 werner 332
    mAirDensity = Model::settings().airDensity; // kg / m3
237 werner 333
}
334
 
335
void Canopy::setStandParameters(const double LAIneedle, const double LAIbroadleave, const double maxCanopyConductance)
336
{
337
    mLAINeedle = LAIneedle;
338
    mLAIBroadleaved=LAIbroadleave;
339
    mLAI=LAIneedle+LAIbroadleave;
239 werner 340
    mAvgMaxCanopyConductance = maxCanopyConductance;
553 werner 341
 
342
    // clear aggregation containers
562 werner 343
    for (int i=0;i<12;++i) mET0[i]=0.;
553 werner 344
 
237 werner 345
}
346
 
347
 
348
 
256 werner 349
/** calculate the daily evaporation/transpiration using the Penman-Monteith-Equation.
350
   This version is based on 3PG. See the Visual Basic Code in 3PGjs.xls.
351
   Returns the total sum of evaporation+transpiration in mm of the day. */
367 werner 352
double Canopy::evapotranspiration3PG(const ClimateDay *climate, const double daylength_h, const double combined_response)
256 werner 353
{
354
    double vpd_mbar = climate->vpd * 10.; // convert from kPa to mbar
355
    double temperature = climate->temperature; // average temperature of the day (°C)
356
    double daylength = daylength_h * 3600.; // daylength in seconds (convert from length in hours)
357
    double rad = climate->radiation / daylength * 1000000; //convert from MJ/m2 (day sum) to average radiation flow W/m2 [MJ=MWs -> /s * 1,000,000
358
 
561 werner 359
    // the radiation: based on linear empirical function
360
    const double qa = -90.;
361
    const double qb = 0.8;
362
    double net_rad = qa + qb*rad;
363
 
256 werner 364
    //: Landsberg original: const double e20 = 2.2;  //rate of change of saturated VP with T at 20C
365
    const double VPDconv = 0.000622; //convert VPD to saturation deficit = 18/29/1000
366
    const double latent_heat = 2460000.; // Latent heat of vaporization. Energy required per unit mass of water vaporized [J kg-1]
367
 
368 werner 368
    double gBL  = Model::settings().boundaryLayerConductance; // boundary layer conductance
369
 
370
    // canopy conductance.
371
    // The species traits are weighted by LAI on the RU.
372
    // maximum canopy conductance: see getStandValues()
373
    // current response: see calculateSoilAtmosphereResponse(). This is basically a weighted average of min(water_response, vpd_response) for each species
367 werner 374
    double gC = mAvgMaxCanopyConductance * combined_response;
256 werner 375
 
367 werner 376
 
256 werner 377
    double defTerm = mAirDensity * latent_heat * (vpd_mbar * VPDconv) * gBL;
378
 
561 werner 379
    //  with temperature-dependent  slope of  vapor pressure saturation curve
380
    // (following  Allen et al. (1998),  http://www.fao.org/docrep/x0490e/x0490e07.htm#atmospheric%20parameters)
381
    // svp_slope in mbar.
382
    double svp_slope = 4098. * (6.1078 * exp(17.269 * temperature / (temperature + 237.3))) / ((237.3+temperature)*(237.3+temperature));
383
 
256 werner 384
    double div = (1. + svp_slope + gBL / gC);
561 werner 385
    double Etransp = (svp_slope * net_rad + defTerm) / div;
335 werner 386
    double canopy_transpiration = Etransp / latent_heat * daylength;
256 werner 387
 
562 werner 388
    // calculate reference evapotranspiration
389
    // see Adair et al 2008
390
    const double psychrometric_const = 0.0672718682328237; // kPa/degC
391
    const double windspeed = 2.; // m/s
392
    double net_rad_mj_day = net_rad*daylength/1000000.; // convert W/m2 again to MJ/m2*day
393
    double et0_day = 0.408*svp_slope*net_rad_mj_day  + psychrometric_const*900./(temperature+273.)*windspeed*climate->vpd;
394
    double et0_div = svp_slope+psychrometric_const*(1.+0.34*windspeed);
395
    et0_day = et0_day / et0_div;
396
    mET0[climate->month-1] += et0_day;
553 werner 397
 
256 werner 398
    if (mInterception>0.) {
399
        // we assume that for evaporation from leaf surface gBL/gC -> 0
562 werner 400
        double div_evap = 1. + svp_slope;
401
        double evap_canopy = (svp_slope*net_rad + defTerm) / div_evap / latent_heat * daylength;
402
        evap_canopy = qMin(evap_canopy, mInterception);
403
        mInterception -= evap_canopy; // reduce interception
404
        mEvaporation = evap_canopy; // evaporation from intercepted water
256 werner 405
    }
335 werner 406
    return canopy_transpiration;
256 werner 407
}
408
 
237 werner 409
} // end namespace