Subversion Repositories public iLand

Rev

Rev 546 | Rev 553 | 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");
547 werner 25
    mTopLayerWaterContent = xml.valueDouble("model.site.topLayerWaterContent",50);
338 werner 26
    if (pct_sand + pct_silt + pct_clay != 100.)
27
        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));
28
 
29
    // calculate soil characteristics based on empirical functions (Schwalm & Ek, 2004)
30
    // note: the variables are percentages [0..100]
31
    mPsi_ref = -exp((1.54 - 0.0095*pct_sand + 0.0063*pct_silt) * log(10)) * 0.000098; // Eq. 83
32
    mPsi_koeff_b = -( 3.1 + 0.157*pct_clay - 0.003*pct_sand );  // Eq. 84
33
    mRho_ref = 0.01 * (50.5 - 0.142*pct_sand - 0.037*pct_clay); // Eq. 78
240 werner 34
    mCanopy.setup();
339 werner 35
 
36
    mPermanentWiltingPoint = heightFromPsi(-4000); // maximum psi is set to a constant of -4MPa
379 werner 37
    if (xml.valueBool("model.settings.waterUseSoilSaturation",false)==false) {
38
        mFieldCapacity = heightFromPsi(-15);
39
    } else {
40
        // =-EXP((1.54-0.0095* pctSand +0.0063* pctSilt)*LN(10))*0.000098
41
        double psi_sat = -exp((1.54-0.0095 * pct_sand + 0.0063*pct_silt)*log(10.))*0.000098;
42
        mFieldCapacity = heightFromPsi(psi_sat);
431 werner 43
        if (logLevelDebug()) qDebug() << "psi: saturation " << psi_sat << "field capacity:" << mFieldCapacity;
379 werner 44
    }
45
 
266 werner 46
    mContent = mFieldCapacity; // start with full water content (in the middle of winter)
431 werner 47
    if (logLevelDebug()) qDebug() << "setup of water: Psi_ref (kPa)" << mPsi_ref << "Rho_ref" << mRho_ref << "coeff. b" << mPsi_koeff_b;
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.;
502 werner 79
    const double GroundVegetationCC = 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.) {
93
        mCanopyConductance+=(GroundVegetationCC)*(1. - total_lai);
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
 
178
    // main loop over all days of the year
239 werner 179
    double prec_mm, prec_after_interception, prec_to_soil, et, excess;
338 werner 180
    const Climate *climate = mRU->climate();
181
    const ClimateDay *day = climate->begin();
182
    const ClimateDay *end = climate->end();
237 werner 183
    int doy=0;
184
    double total_excess = 0.;
185
    for (; day<end; ++day, ++doy) {
186
        // (1) precipitation of the day
187
        prec_mm = day->preciptitation;
188
        // (2) interception by the crown
189
        prec_after_interception = mCanopy.flow(prec_mm, day->temperature);
190
        // (3) storage in the snow pack
191
        prec_to_soil = mSnowPack.flow(prec_after_interception, day->temperature);
192
        // (4) add rest to soil
193
        mContent += prec_to_soil;
266 werner 194
 
195
        excess = 0.;
196
        if (mContent>mFieldCapacity) {
197
            // excess water runoff
198
            excess = mContent - mFieldCapacity;
199
            total_excess += excess;
200
            mContent = mFieldCapacity;
201
        }
202
 
367 werner 203
        double current_psi = psiFromHeight(mContent);
204
        mPsi[doy] = current_psi;
546 werner 205
        mWaterDeficit_mm[doy] = mFieldCapacity - mContent;
335 werner 206
        // (5) transpiration of the vegetation (and of water intercepted in canopy)
367 werner 207
        // calculate the LAI-weighted response values for soil water and vpd:
208
        double combined_response = calculateSoilAtmosphereResponse( current_psi, day->vpd);
209
        et = mCanopy.evapotranspiration3PG(day, climate->daylength_h(doy), combined_response);
238 werner 210
 
241 werner 211
        mContent -= et; // reduce content (transpiration)
338 werner 212
        // add intercepted water (that is *not* evaporated) again to the soil (or add to snow if temp too low -> call to snowpack)
213
        mContent += mSnowPack.add(mCanopy.interception(),day->temperature);
241 werner 214
 
338 werner 215
        // do not remove water below the PWP (fixed value)
266 werner 216
        if (mContent<mPermanentWiltingPoint) {
271 werner 217
            et -= mPermanentWiltingPoint - mContent; // reduce et (for bookkeeping)
266 werner 218
            mContent = mPermanentWiltingPoint;
237 werner 219
        }
266 werner 220
 
546 werner 221
 
271 werner 222
        //DBGMODE(
239 werner 223
            if (GlobalSettings::instance()->isDebugEnabled(GlobalSettings::dWaterCycle)) {
240 werner 224
                DebugList &out = GlobalSettings::instance()->debugList(day->id(), GlobalSettings::dWaterCycle);
239 werner 225
                // climatic variables
364 werner 226
                out << day->id() << mRU->index() << day->temperature << day->vpd << day->preciptitation << day->radiation;
368 werner 227
                out << combined_response; // combined response of all species on RU (min(water, vpd))
239 werner 228
                // fluxes
271 werner 229
                out << prec_after_interception << prec_to_soil << et << mCanopy.evaporationCanopy()
540 werner 230
                        << mContent << mPsi[doy] << excess;
239 werner 231
                // other states
232
                out << mSnowPack.snowPack();
364 werner 233
                //special sanity check:
234
                if (prec_to_soil>0. && mCanopy.interception()>0.)
235
                    if (mSnowPack.snowPack()==0. && day->preciptitation==0)
236
                        qDebug() << "watercontent increase without precipititaion";
239 werner 237
 
238
            }
271 werner 239
        //); // DBGMODE()
239 werner 240
 
237 werner 241
    }
496 werner 242
    mLastYear = GlobalSettings::instance()->currentYear();
243
 
237 werner 244
}
245
 
246
 
247
namespace Water {
248
 
249
/** calculates the input/output of water that is stored in the snow pack.
250
  The approach is similar to Picus 1.3 and ForestBGC (Running, 1988).
251
  Returns the amount of water that exits the snowpack (precipitation, snow melt) */
252
double SnowPack::flow(const double &preciptitation_mm, const double &temperature)
253
{
254
    if (temperature>0.) {
255
        if (mSnowPack==0.)
256
            return preciptitation_mm; // no change
257
        else {
258
            // snow melts
259
            const double melting_coefficient = 0.7; // mm/°C
240 werner 260
            double melt = qMin(temperature * melting_coefficient, mSnowPack);
261
            mSnowPack -=melt;
237 werner 262
            return preciptitation_mm + melt;
263
        }
264
    } else {
265
        // snow:
266
        mSnowPack += preciptitation_mm;
267
        return 0.; // no output.
268
    }
269
 
270
}
271
 
272
 
334 werner 273
inline double SnowPack::add(const double &preciptitation_mm, const double &temperature)
274
{
275
    // do nothing for temps > 0°
276
    if (temperature>0.)
277
        return preciptitation_mm;
278
 
279
    // temps < 0°: add to snow
280
    mSnowPack += preciptitation_mm;
281
    return 0.;
282
}
283
 
237 werner 284
/** Interception in crown canopy.
285
    Calculates the amount of preciptitation that does not reach the ground and
286
    is stored in the canopy. The approach is adopted from Picus 1.3.
299 werner 287
    Returns the amount of precipitation (mm) that surpasses the canopy layer.
288
    @sa http://iland.boku.ac.at/water+cycle#precipitation_and_interception */
237 werner 289
double Canopy::flow(const double &preciptitation_mm, const double &temperature)
290
{
291
    // sanity checks
292
    mInterception = 0.;
271 werner 293
    mEvaporation = 0.;
237 werner 294
    if (!mLAI)
295
        return preciptitation_mm;
296
    if (!preciptitation_mm)
297
        return 0.;
298
    double max_interception_mm=0.; // maximum interception based on the current foliage
299
    double max_storage_mm=0.; // maximum storage in canopy
300
 
301
    if (mLAINeedle>0.) {
302
        // (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
303
        double max_flow_needle = 0.9 * sqrt(1.03 - exp(-0.055*preciptitation_mm));
304
        max_interception_mm += preciptitation_mm *  (1. - max_flow_needle * mLAINeedle/mLAI);
305
        // (2) calculate maximum storage potential based on the current LAI
306
        double max_storage_needle = 4. * (1. - exp(-0.55*mLAINeedle) );
307
        max_storage_mm += max_storage_needle;
308
    }
309
 
310
    if (mLAIBroadleaved>0.) {
311
        // (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
299 werner 312
        double max_flow_broad = 0.9 * pow(1.22 - exp(-0.055*preciptitation_mm), 0.35);
237 werner 313
        max_interception_mm += preciptitation_mm *  (1. - max_flow_broad * mLAIBroadleaved/mLAI);
314
        // (2) calculate maximum storage potential based on the current LAI
299 werner 315
        double max_storage_broad = 2. * (1. - exp(-0.5*mLAIBroadleaved) );
237 werner 316
        max_storage_mm += max_storage_broad;
317
    }
318
 
319
    // (3) calculate actual interception and store for evaporation calculation
320
    mInterception = qMin( max_storage_mm, max_interception_mm );
321
 
335 werner 322
    // (4) limit interception with amount of precipitation
323
    mInterception = qMin( mInterception, preciptitation_mm);
324
 
325
    // (5) reduce preciptitaion by the amount that is intercepted by the canopy
237 werner 326
    return preciptitation_mm - mInterception;
327
 
328
}
329
 
239 werner 330
/// sets up the canopy. fetch some global parameter values...
331
void Canopy::setup()
237 werner 332
{
240 werner 333
    mAirDensity = Model::settings().airDensity; // kg / m3
237 werner 334
}
335
 
336
void Canopy::setStandParameters(const double LAIneedle, const double LAIbroadleave, const double maxCanopyConductance)
337
{
338
    mLAINeedle = LAIneedle;
339
    mLAIBroadleaved=LAIbroadleave;
340
    mLAI=LAIneedle+LAIbroadleave;
239 werner 341
    mAvgMaxCanopyConductance = maxCanopyConductance;
237 werner 342
}
343
 
344
 
345
 
256 werner 346
/** calculate the daily evaporation/transpiration using the Penman-Monteith-Equation.
347
   This version is based on 3PG. See the Visual Basic Code in 3PGjs.xls.
348
   Returns the total sum of evaporation+transpiration in mm of the day. */
367 werner 349
double Canopy::evapotranspiration3PG(const ClimateDay *climate, const double daylength_h, const double combined_response)
256 werner 350
{
351
    double vpd_mbar = climate->vpd * 10.; // convert from kPa to mbar
352
    double temperature = climate->temperature; // average temperature of the day (°C)
353
    double daylength = daylength_h * 3600.; // daylength in seconds (convert from length in hours)
354
    double rad = climate->radiation / daylength * 1000000; //convert from MJ/m2 (day sum) to average radiation flow W/m2 [MJ=MWs -> /s * 1,000,000
355
 
356
    //: Landsberg original: const double e20 = 2.2;  //rate of change of saturated VP with T at 20C
357
    const double VPDconv = 0.000622; //convert VPD to saturation deficit = 18/29/1000
358
    const double latent_heat = 2460000.; // Latent heat of vaporization. Energy required per unit mass of water vaporized [J kg-1]
359
 
368 werner 360
    double gBL  = Model::settings().boundaryLayerConductance; // boundary layer conductance
361
 
362
    // canopy conductance.
363
    // The species traits are weighted by LAI on the RU.
364
    // maximum canopy conductance: see getStandValues()
365
    // current response: see calculateSoilAtmosphereResponse(). This is basically a weighted average of min(water_response, vpd_response) for each species
367 werner 366
    double gC = mAvgMaxCanopyConductance * combined_response;
256 werner 367
 
367 werner 368
 
256 werner 369
    double defTerm = mAirDensity * latent_heat * (vpd_mbar * VPDconv) * gBL;
370
        // saturation vapor pressure (Running 1988, Eq. 1) in mbar
371
    double svp = 6.1078 * exp((17.269 * temperature) / (237.3 + temperature) );
372
    // the slope of svp is, thanks to http://www.wolframalpha.com/input/?i=derive+y%3D6.1078+exp+((17.269x)/(237.3%2Bx))
373
    double svp_slope = svp * ( 17.269/(237.3+temperature) - 17.269*temperature/((237.3+temperature)*(237.3+temperature)) );
374
 
375
    double div = (1. + svp_slope + gBL / gC);
376
    double Etransp = (svp_slope * rad + defTerm) / div;
335 werner 377
    double canopy_transpiration = Etransp / latent_heat * daylength;
256 werner 378
 
379
    if (mInterception>0.) {
380
        // we assume that for evaporation from leaf surface gBL/gC -> 0
381
        double div_evap = 1 + svp_slope;
382
        double evap = (svp_slope*rad + defTerm) / div_evap / latent_heat * daylength;
383
        evap = qMin(evap, mInterception);
335 werner 384
        mInterception -= evap; // reduce interception
385
        mEvaporation = evap; // evaporation from intercepted water
256 werner 386
    }
335 werner 387
    return canopy_transpiration;
256 werner 388
}
389
 
237 werner 390
} // end namespace