(root)/src/core/watercycle.cpp - Rev 1222
Rev 1221 |
Blame |
Compare with Previous |
Last modification |
View Log
| RSS feed
/********************************************************************************************
** iLand - an individual based forest landscape and disturbance model
** http://iland.boku.ac.at
** Copyright (C) 2009- Werner Rammer, Rupert Seidl
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
********************************************************************************************/
#include "global.h"
#include "watercycle.h"
#include "climate.h"
#include "resourceunit.h"
#include "species.h"
#include "model.h"
#include "debugtimer.h"
#include "modules.h"
/** @class WaterCycle
@ingroup core
simulates the water cycle on a ResourceUnit.
The WaterCycle is simulated with a daily time step on the spatial level of a ResourceUnit. Related are
the snow module (SnowPack), and Canopy module that simulates the interception (and evaporation) of precipitation and the
transpiration from the canopy.
The WaterCycle covers the "soil water bucket". Main entry function is run().
See http://iland.boku.ac.at/water+cycle
*/
WaterCycle::WaterCycle()
{
mSoilDepth = 0;
mLastYear = -1;
}
void WaterCycle::setup(const ResourceUnit *ru)
{
mRU = ru;
// get values...
mFieldCapacity = 0.; // on top
const XmlHelper &xml=GlobalSettings::instance()->settings();
mSoilDepth = xml.valueDouble("model.site.soilDepth", 0.) * 10; // convert from cm to mm
double pct_sand = xml.valueDouble("model.site.pctSand");
double pct_silt = xml.valueDouble("model.site.pctSilt");
double pct_clay = xml.valueDouble("model.site.pctClay");
if (fabs(100. - (pct_sand + pct_silt + pct_clay)) > 0.01)
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));
// calculate soil characteristics based on empirical functions (Schwalm & Ek, 2004)
// note: the variables are percentages [0..100]
mPsi_sat = -exp((1.54 - 0.0095*pct_sand + 0.0063*pct_silt) * log(10)) * 0.000098; // Eq. 83
mPsi_koeff_b = -( 3.1 + 0.157*pct_clay - 0.003*pct_sand ); // Eq. 84
mTheta_sat = 0.01 * (50.5 - 0.142*pct_sand - 0.037*pct_clay); // Eq. 78
mCanopy.setup();
mPermanentWiltingPoint = heightFromPsi(-4000); // maximum psi is set to a constant of -4MPa
if (xml.valueBool("model.settings.waterUseSoilSaturation",false)==false) {
mFieldCapacity = heightFromPsi(-15);
} else {
// =-EXP((1.54-0.0095* pctSand +0.0063* pctSilt)*LN(10))*0.000098
double psi_sat = -exp((1.54-0.0095 * pct_sand + 0.0063*pct_silt)*log(10.))*0.000098;
mFieldCapacity = heightFromPsi(psi_sat);
if (logLevelDebug()) qDebug() << "psi: saturation " << psi_sat << "field capacity:" << mFieldCapacity;
}
mContent = mFieldCapacity; // start with full water content (in the middle of winter)
if (logLevelDebug()) qDebug() << "setup of water: Psi_sat (kPa)" << mPsi_sat << "Theta_sat" << mTheta_sat << "coeff. b" << mPsi_koeff_b;
mCanopyConductance = 0.;
mLastYear = -1;
// canopy settings
mCanopy.mNeedleFactor = xml.valueDouble("model.settings.interceptionStorageNeedle", 4.);
mCanopy.mDecidousFactor = xml.valueDouble("model.settings.interceptionStorageBroadleaf", 2.);
mSnowPack.mSnowTemperature = xml.valueDouble("model.settings.snowMeltTemperature", 0.);
mTotalET = mTotalExcess = mSnowRad = 0.;
mSnowDays = 0;
}
/** function to calculate the water pressure [saugspannung] for a given amount of water.
returns water potential in kPa.
see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance */
inline double WaterCycle::psiFromHeight(const double mm) const
{
// psi_x = psi_ref * ( rho_x / rho_ref) ^ b
if (mm<0.001)
return -100000000;
double psi_x = mPsi_sat * pow((mm / mSoilDepth / mTheta_sat),mPsi_koeff_b);
return psi_x; // pis
}
/// calculate the height of the water column for a given pressure
/// return water amount in mm
/// see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
inline double WaterCycle::heightFromPsi(const double psi_kpa) const
{
// rho_x = rho_ref * (psi_x / psi_ref)^(1/b)
double h = mSoilDepth * mTheta_sat * pow(psi_kpa / mPsi_sat, 1./mPsi_koeff_b);
return h;
}
/// get canopy characteristics of the resource unit.
/// It is important, that species-statistics are valid when this function is called (LAI)!
void WaterCycle::getStandValues()
{
mLAINeedle=mLAIBroadleaved=0.;
mCanopyConductance=0.;
const double ground_vegetationCC = 0.02;
double lai;
foreach(const ResourceUnitSpecies *rus, mRU->ruSpecies()) {
lai = rus->constStatistics().leafAreaIndex();
if (rus->species()->isConiferous())
mLAINeedle+=lai;
else
mLAIBroadleaved+=lai;
mCanopyConductance += rus->species()->canopyConductance() * lai; // weigh with LAI
}
double total_lai = mLAIBroadleaved+mLAINeedle;
// handle cases with LAI < 1 (use generic "ground cover characteristics" instead)
/* The LAI used here is derived from the "stockable" area (and not the stocked area).
If the stand has gaps, the available trees are "thinned" across the whole area. Otherwise (when stocked area is used)
the LAI would overestimate the transpiring canopy. However, the current solution overestimates e.g. the interception.
If the "thinned out" LAI is below one, the rest (i.e. the gaps) are thought to be covered by ground vegetation.
*/
if (total_lai<1.) {
mCanopyConductance+=(ground_vegetationCC)*(1. - total_lai);
total_lai = 1.;
}
mCanopyConductance /= total_lai;
if (total_lai < Model::settings().laiThresholdForClosedStands) {
// following Landsberg and Waring: when LAI is < 3 (default for laiThresholdForClosedStands), a linear "ramp" from 0 to 3 is assumed
// http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
mCanopyConductance *= total_lai / Model::settings().laiThresholdForClosedStands;
}
if (logLevelInfo()) qDebug() << "WaterCycle:getStandValues: LAI needle" << mLAINeedle << "LAI Broadl:"<< mLAIBroadleaved << "weighted avg. Conductance (m/2):" << mCanopyConductance;
}
/// calculate responses for ground vegetation, i.e. for "unstocked" areas.
/// this duplicates calculations done in Species.
/// @return Minimum of vpd and soilwater response for default
inline double WaterCycle::calculateBaseSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
{
// constant parameters used for ground vegetation:
const double mPsiMin = 1.5; // MPa
const double mRespVpdExponent = -0.6;
// see SpeciesResponse::soilAtmosphereResponses()
double water_resp;
// see Species::soilwaterResponse:
const double psi_mpa = psi_kpa / 1000.; // convert to MPa
water_resp = limit( 1. - psi_mpa / mPsiMin, 0., 1.);
// see species::vpdResponse
double vpd_resp;
vpd_resp = exp(mRespVpdExponent * vpd_kpa);
return qMin(water_resp, vpd_resp);
}
/// calculate combined VPD and soilwaterresponse for all species
/// on the RU. This is used for the calc. of the transpiration.
inline double WaterCycle::calculateSoilAtmosphereResponse(const double psi_kpa, const double vpd_kpa)
{
double min_response;
double total_response = 0; // LAI weighted minimum response for all speices on the RU
double total_lai_factor = 0.;
foreach(const ResourceUnitSpecies *rus, mRU->ruSpecies()) {
if (rus->LAIfactor()>0.) {
// retrieve the minimum of VPD / soil water response for that species
rus->speciesResponse()->soilAtmosphereResponses(psi_kpa, vpd_kpa, min_response);
total_response += min_response * rus->LAIfactor();
total_lai_factor += rus->LAIfactor();
}
}
if (total_lai_factor<1.) {
// the LAI is below 1: the rest is considered as "ground vegetation"
total_response += calculateBaseSoilAtmosphereResponse(psi_kpa, vpd_kpa) * (1. - total_lai_factor);
}
// add an aging factor to the total response (averageAging: leaf area weighted mean aging value):
// conceptually: response = min(vpd_response, water_response)*aging
if (total_lai_factor==1.)
total_response *= mRU->averageAging(); // no ground cover: use aging value for all LA
else if (total_lai_factor>0. && mRU->averageAging()>0.)
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)
DBGMODE(
if (mRU->averageAging()>1. || mRU->averageAging()<0. || total_response<0 || total_response>1.)
qDebug() << "water cycle: average aging invalid. aging:" << mRU->averageAging() << "total response" << total_response << "total lai factor:" << total_lai_factor;
);
//DBG_IF(mRU->averageAging()>1. || mRU->averageAging()<0.,"water cycle", "average aging invalid!" );
return total_response;
}
/// Main Water Cycle function. This function triggers all water related tasks for
/// one simulation year.
/// @sa http://iland.boku.ac.at/water+cycle
void WaterCycle::run()
{
// necessary?
if (GlobalSettings::instance()->currentYear() == mLastYear)
return;
DebugTimer tw("water:run");
WaterCycleData add_data;
// preparations (once a year)
getStandValues(); // fetch canopy characteristics from iLand (including weighted average for mCanopyConductance)
mCanopy.setStandParameters(mLAINeedle,
mLAIBroadleaved,
mCanopyConductance);
// main loop over all days of the year
double prec_mm, prec_after_interception, prec_to_soil, et, excess;
const Climate *climate = mRU->climate();
const ClimateDay *day = climate->begin();
const ClimateDay *end = climate->end();
int doy=0;
mTotalExcess = 0.;
mTotalET = 0.;
mSnowRad = 0.;
mSnowDays = 0;
for (; day<end; ++day, ++doy) {
// (1) precipitation of the day
prec_mm = day->preciptitation;
// (2) interception by the crown
prec_after_interception = mCanopy.flow(prec_mm);
// (3) storage in the snow pack
prec_to_soil = mSnowPack.flow(prec_after_interception, day->temperature);
// save extra data (used by e.g. fire module)
add_data.water_to_ground[doy] = prec_to_soil;
add_data.snow_cover[doy] = mSnowPack.snowPack();
if (mSnowPack.snowPack()>0.) {
mSnowRad += day->radiation;
mSnowDays++;
}
// (4) add rest to soil
mContent += prec_to_soil;
excess = 0.;
if (mContent>mFieldCapacity) {
// excess water runoff
excess = mContent - mFieldCapacity;
mTotalExcess += excess;
mContent = mFieldCapacity;
}
double current_psi = psiFromHeight(mContent);
mPsi[doy] = current_psi;
// (5) transpiration of the vegetation (and of water intercepted in canopy)
// calculate the LAI-weighted response values for soil water and vpd:
double interception_before_transpiration = mCanopy.interception();
double combined_response = calculateSoilAtmosphereResponse( current_psi, day->vpd);
et = mCanopy.evapotranspiration3PG(day, climate->daylength_h(doy), combined_response);
// if there is some flow from intercepted water to the ground -> add to "water_to_the_ground"
if (mCanopy.interception() < interception_before_transpiration)
add_data.water_to_ground[doy]+= interception_before_transpiration - mCanopy.interception();
mContent -= et; // reduce content (transpiration)
// add intercepted water (that is *not* evaporated) again to the soil (or add to snow if temp too low -> call to snowpack)
mContent += mSnowPack.add(mCanopy.interception(),day->temperature);
// do not remove water below the PWP (fixed value)
if (mContent<mPermanentWiltingPoint) {
et -= mPermanentWiltingPoint - mContent; // reduce et (for bookkeeping)
mContent = mPermanentWiltingPoint;
}
mTotalET += et;
//DBGMODE(
if (GlobalSettings::instance()->isDebugEnabled(GlobalSettings::dWaterCycle)) {
DebugList &out = GlobalSettings::instance()->debugList(day->id(), GlobalSettings::dWaterCycle);
// climatic variables
out << day->id() << mRU->index() << mRU->id() << day->temperature << day->vpd << day->preciptitation << day->radiation;
out << combined_response; // combined response of all species on RU (min(water, vpd))
// fluxes
out << prec_after_interception << prec_to_soil << et << mCanopy.evaporationCanopy()
<< mContent << mPsi[doy] << excess;
// other states
out << mSnowPack.snowPack();
//special sanity check:
if (prec_to_soil>0. && mCanopy.interception()>0.)
if (mSnowPack.snowPack()==0. && day->preciptitation==0)
qDebug() << "watercontent increase without precipititaion";
}
//); // DBGMODE()
}
// call external modules
GlobalSettings::instance()->model()->modules()->calculateWater(mRU, &add_data);
mLastYear = GlobalSettings::instance()->currentYear();
}
namespace Water {
/** calculates the input/output of water that is stored in the snow pack.
The approach is similar to Picus 1.3 and ForestBGC (Running, 1988).
Returns the amount of water that exits the snowpack (precipitation, snow melt) */
double SnowPack::flow(const double &preciptitation_mm, const double &temperature)
{
if (temperature>mSnowTemperature) {
if (mSnowPack==0.)
return preciptitation_mm; // no change
else {
// snow melts
const double melting_coefficient = 0.7; // mm/C
double melt = qMin( (temperature-mSnowTemperature) * melting_coefficient, mSnowPack);
mSnowPack -=melt;
return preciptitation_mm + melt;
}
} else {
// snow:
mSnowPack += preciptitation_mm;
return 0.; // no output.
}
}
inline double SnowPack::add(const double &preciptitation_mm, const double &temperature)
{
// do nothing for temps > 0 C
if (temperature>mSnowTemperature)
return preciptitation_mm;
// temps < 0 C: add to snow
mSnowPack += preciptitation_mm;
return 0.;
}
/** Interception in crown canopy.
Calculates the amount of preciptitation that does not reach the ground and
is stored in the canopy. The approach is adopted from Picus 1.3.
Returns the amount of precipitation (mm) that surpasses the canopy layer.
@sa http://iland.boku.ac.at/water+cycle#precipitation_and_interception */
double Canopy::flow(const double &preciptitation_mm)
{
// sanity checks
mInterception = 0.;
mEvaporation = 0.;
if (!mLAI)
return preciptitation_mm;
if (!preciptitation_mm)
return 0.;
double max_interception_mm=0.; // maximum interception based on the current foliage
double max_storage_mm=0.; // maximum storage in canopy (current LAI)
double max_storage_potentital = 0.; // storage capacity at very high LAI
if (mLAINeedle>0.) {
// (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
double max_flow_needle = 0.9 * sqrt(1.03 - exp(-0.055*preciptitation_mm));
max_interception_mm += preciptitation_mm * (1. - max_flow_needle * mLAINeedle/mLAI);
// (2) calculate maximum storage potential based on the current LAI
// by weighing the needle/decidious storage capacity
max_storage_potentital += mNeedleFactor * mLAINeedle/mLAI;
}
if (mLAIBroadleaved>0.) {
// (1) calculate maximum fraction of thru-flow the crown (based on precipitation)
double max_flow_broad = 0.9 * pow(1.22 - exp(-0.055*preciptitation_mm), 0.35);
max_interception_mm += preciptitation_mm * (1. - max_flow_broad) * mLAIBroadleaved/mLAI;
// (2) calculate maximum storage potential based on the current LAI
max_storage_potentital += mDecidousFactor * mLAIBroadleaved/mLAI;
}
// the extent to which the maximum stoarge capacity is exploited, depends on LAI:
max_storage_mm = max_storage_potentital * (1. - exp(-0.5 * mLAI));
// (3) calculate actual interception and store for evaporation calculation
mInterception = qMin( max_storage_mm, max_interception_mm );
// (4) limit interception with amount of precipitation
mInterception = qMin( mInterception, preciptitation_mm );
// (5) reduce preciptitaion by the amount that is intercepted by the canopy
return preciptitation_mm - mInterception;
}
/// sets up the canopy. fetch some global parameter values...
void Canopy::setup()
{
mAirDensity = Model::settings().airDensity; // kg / m3
}
void Canopy::setStandParameters(const double LAIneedle, const double LAIbroadleave, const double maxCanopyConductance)
{
mLAINeedle = LAIneedle;
mLAIBroadleaved=LAIbroadleave;
mLAI=LAIneedle+LAIbroadleave;
mAvgMaxCanopyConductance = maxCanopyConductance;
// clear aggregation containers
for (int i=0;i<12;++i) mET0[i]=0.;
}
/** calculate the daily evaporation/transpiration using the Penman-Monteith-Equation.
This version is based on 3PG. See the Visual Basic Code in 3PGjs.xls.
Returns the total sum of evaporation+transpiration in mm of the day. */
double Canopy::evapotranspiration3PG(const ClimateDay *climate, const double daylength_h, const double combined_response)
{
double vpd_mbar = climate->vpd * 10.; // convert from kPa to mbar
double temperature = climate->temperature; // average temperature of the day (degree C)
double daylength = daylength_h * 3600.; // daylength in seconds (convert from length in hours)
double rad = climate->radiation / daylength * 1000000; //convert from MJ/m2 (day sum) to average radiation flow W/m2 [MJ=MWs -> /s * 1,000,000
// the radiation: based on linear empirical function
const double qa = -90.;
const double qb = 0.8;
double net_rad = qa + qb*rad;
//: Landsberg original: const double e20 = 2.2; //rate of change of saturated VP with T at 20C
const double VPDconv = 0.000622; //convert VPD to saturation deficit = 18/29/1000 = molecular weight of H2O/molecular weight of air
const double latent_heat = 2460000.; // Latent heat of vaporization. Energy required per unit mass of water vaporized [J kg-1]
double gBL = Model::settings().boundaryLayerConductance; // boundary layer conductance
// canopy conductance.
// The species traits are weighted by LAI on the RU.
// maximum canopy conductance: see getStandValues()
// current response: see calculateSoilAtmosphereResponse(). This is basically a weighted average of min(water_response, vpd_response) for each species
double gC = mAvgMaxCanopyConductance * combined_response;
double defTerm = mAirDensity * latent_heat * (vpd_mbar * VPDconv) * gBL;
// with temperature-dependent slope of vapor pressure saturation curve
// (following Allen et al. (1998), http://www.fao.org/docrep/x0490e/x0490e07.htm#atmospheric%20parameters)
// svp_slope in mbar.
//double svp_slope = 4098. * (6.1078 * exp(17.269 * temperature / (temperature + 237.3))) / ((237.3+temperature)*(237.3+temperature));
// alternatively: very simple variant (following here the original 3PG code). This
// keeps yields +- same results for summer, but slightly lower values in winter (2011/03/16)
double svp_slope = 2.2;
double div = (1. + svp_slope + gBL / gC);
double Etransp = (svp_slope * net_rad + defTerm) / div;
double canopy_transpiration = Etransp / latent_heat * daylength;
// calculate reference evapotranspiration
// see Adair et al 2008
const double psychrometric_const = 0.0672718682328237; // kPa/degC
const double windspeed = 2.; // m/s
double net_rad_mj_day = net_rad*daylength/1000000.; // convert W/m2 again to MJ/m2*day
double et0_day = 0.408*svp_slope*net_rad_mj_day + psychrometric_const*900./(temperature+273.)*windspeed*climate->vpd;
double et0_div = svp_slope+psychrometric_const*(1.+0.34*windspeed);
et0_day = et0_day / et0_div;
mET0[climate->month-1] += et0_day;
if (mInterception>0.) {
// we assume that for evaporation from leaf surface gBL/gC -> 0
double div_evap = 1. + svp_slope;
double evap_canopy_potential = (svp_slope*net_rad + defTerm) / div_evap / latent_heat * daylength;
// reduce the amount of transpiration on a wet day based on the approach of
// Wigmosta et al (1994). see http://iland.boku.ac.at/water+cycle#transpiration_and_canopy_conductance
double ratio_T_E = canopy_transpiration / evap_canopy_potential;
double evap_canopy = qMin(evap_canopy_potential, mInterception);
// for interception -> 0, the canopy transpiration is unchanged
canopy_transpiration = (evap_canopy_potential - evap_canopy) * ratio_T_E;
mInterception -= evap_canopy; // reduce interception
mEvaporation = evap_canopy; // evaporation from intercepted water
}
return canopy_transpiration;
}
} // end namespace