Subversion Repositories public iLand

Rev

Rev 540 | Rev 547 | 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;
496 werner 47
    mLastYear = -1;
237 werner 48
}
49
 
331 werner 50
/** function to calculate the water pressure [saugspannung] for a given amount of water.
338 werner 51
    returns water potential in kPa.
52
  see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance */
266 werner 53
inline double WaterCycle::psiFromHeight(const double mm) const
54
{
55
    // psi_x = psi_ref * ( rho_x / rho_ref) ^ b
56
    if (mm<0.001)
57
        return -100000000;
58
    double psi_x = mPsi_ref * pow((mm / mSoilDepth / mRho_ref),mPsi_koeff_b);
338 werner 59
    return psi_x; // pis
266 werner 60
}
61
 
331 werner 62
/// calculate the height of the water column for a given pressure
338 werner 63
/// return water amount in mm
64
/// see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
266 werner 65
inline double WaterCycle::heightFromPsi(const double psi_kpa) const
66
{
67
    // rho_x = rho_ref * (psi_x / psi_ref)^(1/b)
68
    double h = mSoilDepth * mRho_ref * pow(psi_kpa / mPsi_ref, 1./mPsi_koeff_b);
69
    return h;
70
}
71
 
331 werner 72
/// get canopy characteristics of the resource unit.
73
/// It is important, that species-statistics are valid when this function is called (LAI)!
237 werner 74
void WaterCycle::getStandValues()
75
{
246 werner 76
    mLAINeedle=mLAIBroadleaved=0.;
237 werner 77
    mCanopyConductance=0.;
502 werner 78
    const double GroundVegetationCC = 0.02;
237 werner 79
    double lai;
455 werner 80
    foreach(ResourceUnitSpecies *rus, mRU->ruSpecies()) {
81
        lai = rus->constStatistics().leafAreaIndex();
82
        if (rus->species()->isConiferous())
237 werner 83
            mLAINeedle+=lai;
84
        else
85
            mLAIBroadleaved+=lai;
455 werner 86
        mCanopyConductance += rus->species()->canopyConductance() * lai; // weigh with LAI
237 werner 87
    }
88
    double total_lai = mLAIBroadleaved+mLAINeedle;
502 werner 89
 
90
    // handle cases with LAI < 1 (use generic "ground cover characteristics" instead)
91
    if (total_lai<1.) {
92
        mCanopyConductance+=(GroundVegetationCC)*(1. - total_lai);
93
        total_lai = 1.;
237 werner 94
    }
502 werner 95
    mCanopyConductance /= total_lai;
96
 
368 werner 97
    if (total_lai < Model::settings().laiThresholdForClosedStands) {
98
        // following Landsberg and Waring: when LAI is < 3 (default for laiThresholdForClosedStands), a linear "ramp" from 0 to 3 is assumed
299 werner 99
        // http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
368 werner 100
        mCanopyConductance *= total_lai / Model::settings().laiThresholdForClosedStands;
237 werner 101
    }
431 werner 102
    if (logLevelInfo()) qDebug() << "WaterCycle:getStandValues: LAI needle" << mLAINeedle << "LAI Broadl:"<< mLAIBroadleaved << "weighted avg. Conductance (m/2):" << mCanopyConductance;
237 werner 103
}
104
 
502 werner 105
/// calculate responses for ground vegetation, i.e. for "unstocked" areas.
106
/// this duplicates calculations done in Species.
107
/// @return Minimum of vpd and soilwater response for default
108
inline double WaterCycle::calculateBaseSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
109
{
110
    // constant parameters used for ground vegetation:
503 werner 111
    const double mPsiMin = 1.5; // MPa
502 werner 112
    const double mRespVpdExponent = -0.6;
113
    // see SpeciesResponse::soilAtmosphereResponses()
114
    double water_resp;
115
    // see Species::soilwaterResponse:
116
    const double psi_mpa = psi_kpa / 1000.; // convert to MPa
117
    water_resp = limit( 1. - psi_mpa / mPsiMin, 0., 1.);
118
    // see species::vpdResponse
119
 
120
    double vpd_resp;
121
    vpd_resp =  exp(mRespVpdExponent * vpd_kpa);
122
    return qMin(water_resp, vpd_resp);
123
}
124
 
367 werner 125
/// calculate combined VPD and soilwaterresponse for all species
126
/// on the RU. This is used for the calc. of the transpiration.
127
inline double WaterCycle::calculateSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
128
{
129
    double min_response;
502 werner 130
    double total_response = 0; // LAI weighted minimum response for all speices on the RU
131
    double total_lai_factor = 0.;
455 werner 132
    foreach(ResourceUnitSpecies *rus, mRU->ruSpecies()) {
133
        if (rus->LAIfactor()>0) {
502 werner 134
            // retrieve the minimum of VPD / soil water response for that species
455 werner 135
            rus->speciesResponse()->soilAtmosphereResponses(psi_kpa, vpd_kpa, min_response);
136
            total_response += min_response * rus->LAIfactor();
502 werner 137
            total_lai_factor += rus->LAIfactor();
367 werner 138
        }
139
    }
455 werner 140
 
502 werner 141
    if (total_lai_factor<1.) {
142
        // the LAI is below 1: the rest is considered as "ground vegetation"
143
        total_response += calculateBaseSoilAtmosphereResponse(psi_kpa, vpd_kpa) * (1. - total_lai_factor);
144
    }
145
 
377 werner 146
    // add an aging factor to the total response (averageAging: leaf area weighted mean aging value):
147
    // conceptually: response = min(vpd_response, water_response)*aging
503 werner 148
    if (total_lai_factor==1.)
149
        total_response *= mRU->averageAging(); // no ground cover: use aging value for all LA
150
    else if (total_lai_factor>0. && mRU->averageAging()>0.)
151
        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 152
 
504 werner 153
    DBGMODE( if (mRU->averageAging()>1. || mRU->averageAging()<0. || total_response<0 || total_response>1.)
154
        qDebug() << "water cycle: average aging invalid. aging:" << mRU->averageAging() << "total response" << total_response;
484 werner 155
    );
482 werner 156
 
157
    //DBG_IF(mRU->averageAging()>1. || mRU->averageAging()<0.,"water cycle", "average aging invalid!" );
367 werner 158
    return total_response;
159
}
160
 
161
 
237 werner 162
/// Main Water Cycle function. This function triggers all water related tasks for
163
/// one simulation year.
299 werner 164
/// @sa http://iland.boku.ac.at/water+cycle
237 werner 165
void WaterCycle::run()
166
{
496 werner 167
    // necessary?
168
    if (GlobalSettings::instance()->currentYear() == mLastYear)
169
        return;
237 werner 170
    // preparations (once a year)
367 werner 171
    getStandValues(); // fetch canopy characteristics from iLand (including weighted average for mCanopyConductance)
237 werner 172
    mCanopy.setStandParameters(mLAINeedle,
173
                               mLAIBroadleaved,
174
                               mCanopyConductance);
175
 
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;
546 werner 204
        mWaterDeficit_mm[doy] = mFieldCapacity - mContent;
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;
237 werner 341
}
342
 
343
 
344
 
256 werner 345
/** calculate the daily evaporation/transpiration using the Penman-Monteith-Equation.
346
   This version is based on 3PG. See the Visual Basic Code in 3PGjs.xls.
347
   Returns the total sum of evaporation+transpiration in mm of the day. */
367 werner 348
double Canopy::evapotranspiration3PG(const ClimateDay *climate, const double daylength_h, const double combined_response)
256 werner 349
{
350
    double vpd_mbar = climate->vpd * 10.; // convert from kPa to mbar
351
    double temperature = climate->temperature; // average temperature of the day (°C)
352
    double daylength = daylength_h * 3600.; // daylength in seconds (convert from length in hours)
353
    double rad = climate->radiation / daylength * 1000000; //convert from MJ/m2 (day sum) to average radiation flow W/m2 [MJ=MWs -> /s * 1,000,000
354
 
355
    //: Landsberg original: const double e20 = 2.2;  //rate of change of saturated VP with T at 20C
356
    const double VPDconv = 0.000622; //convert VPD to saturation deficit = 18/29/1000
357
    const double latent_heat = 2460000.; // Latent heat of vaporization. Energy required per unit mass of water vaporized [J kg-1]
358
 
368 werner 359
    double gBL  = Model::settings().boundaryLayerConductance; // boundary layer conductance
360
 
361
    // canopy conductance.
362
    // The species traits are weighted by LAI on the RU.
363
    // maximum canopy conductance: see getStandValues()
364
    // current response: see calculateSoilAtmosphereResponse(). This is basically a weighted average of min(water_response, vpd_response) for each species
367 werner 365
    double gC = mAvgMaxCanopyConductance * combined_response;
256 werner 366
 
367 werner 367
 
256 werner 368
    double defTerm = mAirDensity * latent_heat * (vpd_mbar * VPDconv) * gBL;
369
        // saturation vapor pressure (Running 1988, Eq. 1) in mbar
370
    double svp = 6.1078 * exp((17.269 * temperature) / (237.3 + temperature) );
371
    // the slope of svp is, thanks to http://www.wolframalpha.com/input/?i=derive+y%3D6.1078+exp+((17.269x)/(237.3%2Bx))
372
    double svp_slope = svp * ( 17.269/(237.3+temperature) - 17.269*temperature/((237.3+temperature)*(237.3+temperature)) );
373
 
374
    double div = (1. + svp_slope + gBL / gC);
375
    double Etransp = (svp_slope * rad + defTerm) / div;
335 werner 376
    double canopy_transpiration = Etransp / latent_heat * daylength;
256 werner 377
 
378
    if (mInterception>0.) {
379
        // we assume that for evaporation from leaf surface gBL/gC -> 0
380
        double div_evap = 1 + svp_slope;
381
        double evap = (svp_slope*rad + defTerm) / div_evap / latent_heat * daylength;
382
        evap = qMin(evap, mInterception);
335 werner 383
        mInterception -= evap; // reduce interception
384
        mEvaporation = evap; // evaporation from intercepted water
256 werner 385
    }
335 werner 386
    return canopy_transpiration;
256 werner 387
}
388
 
237 werner 389
} // end namespace