Program Listing for File Simple_Canopy.cpp

Program Listing for File Simple_Canopy.cpp#

Return to documentation for file (src/modules/Simple_Canopy.cpp)

//
// Canadian Hydrological Model - The Canadian Hydrological Model (CHM) is a novel
// modular unstructured mesh based approach for hydrological modelling
// Copyright (C) 2018 Christopher Marsh
//
// This file is part of Canadian Hydrological Model.
//
// Canadian Hydrological Model 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.
//
// Canadian Hydrological Model 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 Canadian Hydrological Model.  If not, see
// <http://www.gnu.org/licenses/>.
//

#include "Simple_Canopy.hpp"
REGISTER_MODULE_CPP(Simple_Canopy);

Simple_Canopy::Simple_Canopy(config_file cfg)
        : module_base("Simple_Canopy", parallel::data, cfg)
{
    depends("p_rain");
    depends("p_snow");
    depends("iswr");
    depends("iswr_diffuse");
    depends("rh");
    depends("t");
    depends("U_R");
    depends("ilwr");
    depends("snowdepthavg");
    depends("snow_albedo"); //
    //depends("air_pressure"); // TODO: add to met interp (hard coded at 915 mbar for now)

    provides("snow_load");
    provides("rain_load");
    provides("ta_subcanopy");
    provides("rh_subcanopy");
    provides("vw_subcanopy");
    provides("p_subcanopy");
    provides("p_rain_subcanopy");
    provides("p_snow_subcanopy");
    provides("frac_precip_rain_subcanopy");
    provides("frac_precip_snow_subcanopy");
    provides("iswr_subcanopy");
    provides("ilwr_subcanopy");
    provides("ts_canopy");

}

Simple_Canopy::~Simple_Canopy()
{

}

void Simple_Canopy::run(mesh_elem &face)
{
    if(is_water(face))
    {
        set_all_nan_on_skip(face);
        return;
    }
    auto& data = face->get_module_data<Simple_Canopy::data>(ID);

    // Get meteorological data for current face
    double ta           = (*face)["t"_s];
    double rh           = (*face)["rh"_s];
    double U_R          = (*face)["U_R"_s];
    double iswr         = (*face)["iswr"_s]; // SW in above canopy
    double Qdfo         = (*face)["iswr_diffuse"_s]; // "clear-sky diffuse", "(W/m^2)"
    double ilwr         = (*face)["ilwr"_s]; // LW in above canopy
    double p_rain       = (*face)["p_rain"_s]; // rain (mm/timestep) above canopy
    double p_snow       = (*face)["p_snow"_s]; // snow (mm/timestep) above canopy
    double snowdepthavg = (*face)["snowdepthavg"_s];
    double Albedo       = (*face)["snow_albedo"_s]; // Broad band snow albedo
    double air_pressure = 915; //(*face)["air_pressure"_s]; //"Average surface pressure", "(kPa)" TODO: Get from face_data

    // Checks on boundary conditions

    // Albedo
    if (is_nan(Albedo)) // If it is not defined TODO: current hack, should be required to be initialized in mesher
        Albedo=0.1; // Assume no snow

    // Snow depth
    if (is_nan(snowdepthavg)) // If it is not defined TODO: current hack, should be required to be initialized in mesher
        snowdepthavg = 0;

    // declared observations

    double Ts; //", NHRU, "snow surface temperature IN CANOPY", "(°C)", &Ts);

    double Qsisn=0; //", NHRU, "incident short-wave at surface", "(W/m^2)", &Qsisn); Includes canopy impacts

    double Qlisn=0; //", NHRU, "incident long-wave at surface", "(W/m^2)", &Qlisn);

    // declared variables

    double k; //"", NHRU, "extinction coefficient", "()", &k);

    double Tauc; //"", NHRU, "short-wave transmissivity", "(W/m^2)", &Tauc);

    double ra; //"", NHRU, "", "(s/m)", &ra);

    double drip_Cpy; //"", NHRU, "canopy drip", "(mm/int)", &drip_Cpy);

    double direct_rain; //", NHRU, "direct rainfall through canopy", "(mm/int)", &direct_rain);

    double net_rain; //"", NHRU, " direct_rain + drip", "(mm/int)", &net_rain);

    double Subl_Cpy; //"", NHRU, "canopy snow sublimation", "(mm/int)", &Subl_Cpy);

    double Pevap; //"", NHRU, "used when ground is snow covered to calculate canopy evaporation (Priestley-Taylor)", "(mm)", &Pevap);

    double direct_snow; //\", NHRU, "snow 'direct' Thru", "(mm/int)", &direct_snow);

    double SUnload; //"", NHRU, "unloaded canopy snow", "(mm)", &SUnload);

    double SUnload_H2O; //"", NHRU, "unloaded canopy snow as water", "(mm)", &SUnload_H2O);

    double net_snow=0; //"", NHRU, "hru_snow minus interception", "(mm/int)", &net_snow);

    double net_p; //"", NHRU, "total precipitation after interception", "(mm/int)", &net_p);

    double u_FHt; //"", NHRU, "wind speed at forest top (z = FHt)", "(m/s)", &u_FHt);

    double Cc=0; //"", NHRU, "Canopy coverage", "()", &Cc); UNITS???

    double intcp_evap; //"", NHRU, "HRU Evaporation from interception", "(mm/int)", &intcp_evap);

    double Kstar_H;

    double Kd; // Not defined in CRHM


    // Parameters used

    // Default values used for now
    double Alpha_c          = Vegetation::alb_c; // "canopy albedo" 0.05-0.2 TODO: get from mesh parameter when available
    double B_canopy         = 0.038; //TODO: What is this? Where does it come from?", NHRU, "[0.038]", "0.0", "0.2", "canopy enhancement parameter. Suggestions are Colorado - 0.23 and Alberta - 0.038", "()", &B_canopy);
    double Zref             = 2; //", "0.01", "100.0", "temperature measurement height", "(m)", &Zref); TODO: Take from config
    double Zwind            = Atmosphere::Z_U_R; //", "0.01", "100.0", "wind measurement height", "(m)", &Zwind); // Set as defined above
    double Z0snow           = Snow::Z0_SNOW; //", "0.0001", "0.01", "snow roughness length", "(m)", &Z0snow);
    double Sbar             = 6.6; //", "0.0", "100.0", "maximum canopy snow interception load", "(kg/m^2)", &Sbar);
    double Zvent            = 0.75; //", "0.0", "1.0", "ventilation wind speed height (z/Ht)", "()", &Zvent);
    double unload_t         = 1.0; //", "-10.0", "20.0", "if ice-bulb temp >= t : canopy snow is unloaded as snow", "(°C)", &unload_t);
    double unload_t_water   = 4.0; //", "-10.0", "20.0", "if ice-bulb temp >= t: canopy snow is unloaded as water", "(°C)", &unload_t_water);
    double SolAng           = (*face)["solar_el"_s] * mio::Cst::to_rad; // degrees to radians (assumed horizontal)
    double cosxs            = (*face)["solar_angle"_s]; // "cosine of the angle of incidence on the slope", "()"
    double cosxsflat        = cos(SolAng); // "cosine of the angle of incidence on the horizontal"
    double Surrounding_Ht   = data.CanopyHeight; //""[0.1, 0.25, 1.0]", "0.001", "100.0", "surrounding canopy height", "()", &Surrounding_Ht);
    double Gap_diameter     = 100; // "[100]", "10", "1000", "representative gap diameter", "(m)", &Gap_diameter); TODO: hardcod gap diamter, need to get from lidar if available
    double Ht               = data.CanopyHeight; //", NHRU, "[0.1, 0.25, 1.0]", "0.001", "100.0", "forest/vegetation height", "(m)", &Ht);
    double LAI              = data.LAI; //", NHRU, "[2.2]", "0.1", "20.0", "leaf-area-index", "()", &LAI);

    // Initialize
    net_rain = 0.0;
    direct_rain = 0.0;
    drip_Cpy = 0.0;
    intcp_evap = 0.0;
    direct_snow = 0.0;
    SUnload = 0.0;
    SUnload_H2O = 0.0;
    Subl_Cpy = 0.0;

    // Canopy temperature is first approximated by the air temperature.
    double T1 = ta + mio::Cst::t_water_freezing_pt; // Canopy temperature (C to K)

    double rho = air_pressure*1000/(PhysConst::Rgas*T1); // density of Air (pressure kPa to Pa = *1000)

    double U1 = U_R; // Wind speed (m/s) at height Z_vw [m] (top of canopy)

    // Aerodynamic resistance of canopy
    ra = (log(Zref/Z0snow)*log(Zwind/Z0snow))/pow(PhysConst::kappa,2)/U1; // (s/m)

    double deltaX = 0.622*PhysConst::Ls*Qs(air_pressure, T1)/(PhysConst::Rgas*(pow(T1,2))); // Must be (kg K-1)

    double q = (rh/100)*Qs(air_pressure, T1); // specific humidity (kg/kg)

    // snow surface temperature of snow in canopy
    Ts = T1 + (Snow::emiss*(ilwr - PhysConst::sbc*pow(T1, 4.0)) + PhysConst::Ls*(q - Qs(air_pressure, T1))*rho/ra)/
              (4.0*Snow::emiss*PhysConst::sbc*pow(T1, 3.0) + (PhysConst::Cp + PhysConst::Ls*deltaX)*rho/ra);

    Ts -= mio::Cst::t_water_freezing_pt; // K to C

    // Check if Ts is above freezing
    if(Ts > 0.0 ) // || snowdepthavg <= 0.0 (removed dependece on snowdepthavg because it didn't make sense - NIC)
        Ts = 0.0;

    // Canopy type
    switch(data.canopyType){
        case 0: // 0 = canopy
        {
            // Get Exposure (how much is canopy above snowdepth)
            double Exposure = Ht - snowdepthavg; // (m)
            if (Exposure < 0.0)
                Exposure = 0.0;


            double LAI_ = LAI * Exposure / Ht; // Rescaling LAI???

            // terrain view factor (equivalent to 1-Vf), where Vf is the sky view factory. // Where does this equation come from?
            double Vf = 0.45 - 0.29 * log(LAI);

            // Rescaleing Vf ???
            double Vf_ = Vf + (1.0 - Vf) * sin((Ht - Exposure) / Ht * M_PI_2); // Where does equation come from?

            // Below code is the "updated" CRHM version (changed from Canopy)
            if (SolAng > 0.001 && cosxs > 0.001 && cosxsflat > 0.001) {
                k = 1.081 * SolAng * cos(SolAng) / sin(SolAng); // "extinction coefficient"
                double limit = cosxsflat / cosxs;
                if (limit > 2.0)
                    limit = 2.0;
                Tauc = exp(-k * LAI_ * limit); // "short-wave transmissivity", "(W/m^2)"
            }
            else {
                k = 0.0; // "extinction coefficient"
                Tauc = 0.0; // "short-wave transmissivity", "(W/m^2)"
            }

            Kstar_H = iswr * (1.0 - Alpha_c - Tauc * (1.0 - Albedo)); //  what is Kstar_H???

            // Incident long-wave at surface, "(W/m^2)"
            Qlisn = ilwr * Vf_ + (1.0 - Vf_) * Vegetation::emiss_c * PhysConst::sbc * pow(T1, 4.0) + B_canopy * Kstar_H;

            // Incident short-wave at surface, "(W/m^2)"
            Qsisn = iswr * Tauc;

            break;
        }
    case 1:  // 1 = clearing
    {
        // Simply pass radiation variables on

        Qlisn = ilwr;

        Qsisn = iswr;


        break;
    }
    case 2:  // 2 = gap
    {
        double Exposure = Ht - snowdepthavg; // (m)
        if (Exposure < 0.0)
            Exposure = 0.0;

        double LAI_ = LAI * Exposure / Surrounding_Ht; // Not used in CRHM orig code, is this a bug???

        // terrain view factor (equivalent to 1-Vf), where Vf is the sky view factory. // Where does this equation come from?
        double Vf = 0.45 - 0.29 * log(LAI);

        double Tau_d = Vf + (1.0 - Vf) * sin((Surrounding_Ht - Exposure) / Surrounding_Ht * M_PI_2); // previously Vf_

        // calculate forest clearing sky view factor (Vgap) via Reifsnyder and Lull’s (1965) expression:

        double Vgap = pow(sin(atan2(Gap_diameter, 2.0 * Surrounding_Ht)), 2); // note: changed sqr to pow

        // calculate beam pathlength correction (variable “Gap_beam_corr”) for gap:

        double Gap_beam_corr = 0;
        if (iswr > 0.0 && SolAng > 0.001) {
            double cosxsLim = 3;
            if (cosxs > 0.33)
                cosxsLim = 1.0 / cosxs;

            Gap_beam_corr = cosxsLim * Surrounding_Ht *
                            (1.0 / cos(SolAng) - Gap_diameter / (2.0 * Surrounding_Ht) / sin(SolAng));
            if (Gap_beam_corr > 10.0)
                Gap_beam_corr = 10.0;
            else if (Gap_beam_corr < 0.0)
                Gap_beam_corr = 0.0;
        }

        // calculate beam shortwave transmittance of the gap:

        double product = LAI * Gap_beam_corr;
        if (product > 50)
            product = 50;

        double Tau_b_gap = exp(-product);

        Kd = iswr * (1.0 - Alpha_c - Tau_b_gap * (1.0 - Albedo));

        Qlisn = Vgap * ilwr + (1.0 - Vgap) * ((ilwr * Tau_b_gap +
                                               (1.0 - Tau_b_gap) * Vegetation::emiss_c * PhysConst::sbc *
                                               pow(T1, 4.0f)) + B_canopy * Kd);

        Qsisn = cosxs * Qdfo * Tau_b_gap + Vgap * (iswr - Qdfo) + (1.0 - Vgap) * Tau_d * (iswr - Qdfo);
        if (Qsisn < 0.0)
            Qsisn = 0.0;

        break;
    }
    } // end switch



    // Canopy type
    switch(data.canopyType)
    {
        case 0: // 0 = canopy
        {
            //==============================================================================
            // coupled forest snow interception and sublimation routine:
            // after Hedstom & Pomeroy 1998?/ Parviainen & Pomeroy 2000:
            // calculate maximum canopy snow load (L*):

            if (data.Snow_load > 0.0 || p_snow > 0.0) { // handle snow
                double RhoS = 67.92 + 51.25 * exp(ta / 2.59);
                double LStar = Sbar * (0.27 + 46.0 / RhoS) * LAI;

                if (data.Snow_load > LStar) { // after increase in temperature
                    direct_snow = data.Snow_load - LStar;
                    data.Snow_load = LStar;
                }

                // calculate intercepted snowload

                // Calculate wind speed at canopy top //  use U1 instead of U_R here?
                if (Ht - 2.0 / 3.0 * Zwind > 1.0) // Find source of equations
                    u_FHt = U_R * log((Ht - 2.0 / 3.0 * Zwind) / 0.123 * Zwind) /
                            log((Zwind - 2.0 / 3.0 * Zwind) / 0.123 * Zwind);
                else
                    u_FHt = 0.0;

                double I1 = 0.0; // new Interecption ? [mm?]

                // calculate horizontal canopy-coverage (Cc):

                Cc = 0.29 * log(LAI) + 0.55; // Where do hard coded param comes from?
                if (Cc <= 0.0) {
                    Cc = 0.0;
                } else if (Cc > 1.0) {
                    Cc = 1.0;
                }

                if (p_snow > 0.0 && fabs(p_snow / LStar) < 50.0) { // Where do hard coded param comes from?
                    if (u_FHt <=
                        1.0)  // if wind speed at canopy top > 1 m/s // Where do hard coded param comes from?
                        I1 = (LStar - data.Snow_load) * (1.0 - exp(-Cc * p_snow / LStar));
                    else
                        I1 = (LStar - data.Snow_load) * (1.0 - exp(-p_snow / LStar));

                    if (I1 <= 0)
                        I1 = 0;

                    data.Snow_load += I1;

                    // calculate canopy snow throughfall before unloading:

                    direct_snow += (p_snow - I1);
                }

                // calculate snow ventilation windspeed:

                const double gamma = 1.15; // What is this param?
                double xi2 = 1 - Zvent;
                double windExt2 = (gamma * LAI * xi2);

                double uVent = u_FHt * exp(-1 * windExt2);


                // calculate sublimation of intercepted snow from ideal intercepted ice sphere (500 microns diameter):

                double Alpha, A1, B1, C1, J, D, Lamb, Mpm, Nu, Nr, SStar, Sigma2;

                double Es = 611.15 * exp(22.452 * ta / (ta + 273.0));  // {sat pressure}

                double SvDens = Es * PhysConst::M / (PhysConst::R * (ta + 273.0)); // {sat density}

                Lamb = 6.3e-4 * (ta + 273.0) + 0.0673;  // thermal conductivity of atmosphere
                Nr = 2.0 * Snow::Radius * uVent / Atmosphere::KinVisc;  // Reynolds number
                Nu = 1.79 + 0.606 * sqrt(Nr); // Nusselt number
                SStar = M_PI * pow(Snow::Radius, 2) * (1.0 - Snow::AlbedoIce) *
                        iswr;  // SW to snow particle !!!! changed
                A1 = Lamb * (ta + 273) * Nu;
                B1 = PhysConst::Ls * PhysConst::M / (PhysConst::R * (ta + 273.0)) - 1.0;
                J = B1 / A1;
                Sigma2 = rh / 100 - 1;
                D = 2.06e-5 * pow((ta + 273.0) / 273.0, -1.75); // diffusivity of water vapour
                C1 = 1.0 / (D * SvDens * Nu);

                Alpha = 5.0;
                Mpm = 4.0 / 3.0 * M_PI * PhysConst::rho_ice * pow(Snow::Radius, 3) *
                      (1.0 + 3.0 / Alpha + 2.0 / pow(Alpha, 2));

                // sublimation rate of single 'ideal' ice sphere:

                double Vs = (2.0 * M_PI * Snow::Radius * Sigma2 - SStar * J) / (PhysConst::Ls * J + C1) / Mpm;

                // snow exposure coefficient (Ce):

                double Ce;
                if ((data.Snow_load / LStar) <= 0.0)
                    Ce = 0.07;
                else
                    Ce = Vegetation::ks * pow((data.Snow_load / LStar), -Vegetation::Fract);

                // calculate 'potential' canopy sublimation:

                double Vi = Vs * Ce;

                // calculate 'ice-bulb' temperature of intercepted snow:

                double IceBulbT = ta - (Vi * PhysConst::Ls / 1e6 / PhysConst::Ci);

                // determine whether canopy snow is unloaded:

                if (IceBulbT >= unload_t) {
                    if (IceBulbT >= unload_t_water) {
                        drip_Cpy = data.Snow_load;
                        SUnload_H2O = data.Snow_load;
                    }
                    else {
                        SUnload = data.Snow_load * (IceBulbT - unload_t) / (unload_t_water - unload_t);
                        drip_Cpy = data.Snow_load - SUnload;
                        SUnload_H2O = drip_Cpy;
                    }

                    data.Snow_load = 0.0;
                    data.cum_SUnload_H2O += SUnload_H2O;
                }

                // limit sublimation to canopy snow available and take sublimated snow away from canopy snow at timestep start

                //Subl_Cpy = -data.Snow_load*Vi*Hs*Global::Interval*24*3600/Hs; // make W/m2 (original in CRHM)
                Subl_Cpy = -data.Snow_load * Vi * PhysConst::Ls * global_param->dt() /
                           PhysConst::Ls; // make W/m2 TODO: check Interval is same as dt() (in seconds
                // TODO: Hs/HS = 1 !!! (in CRHM, kept here for conistency...)

                if (Subl_Cpy > data.Snow_load) {
                    Subl_Cpy = data.Snow_load;
                    data.Snow_load = 0.0;
                }
                else {
                    data.Snow_load -= Subl_Cpy;
                    if (data.Snow_load < 0.0)
                        data.Snow_load = 0.0;
                }

                // calculate total sub-canopy snow:

                net_snow = direct_snow + SUnload;

            } // handle snow
            break;
        }
            // Clearing or Gap
        case 1:  // clearing
        {
            net_snow = p_snow;
            net_rain = p_rain;
            break;
        }
    case 2:  // gap
        {
            net_snow = p_snow;
            net_rain = p_rain;
            break;
        }
    }  // canopy switch

    double smax, Q;

    switch(data.canopyType){
        case 0: // 0 = canopy
        {
            smax = Cc * LAI * 0.2;

            //  Forest rain interception and evaporation model
            // 'sparse' Rutter interception model (i.e. Valente 1997):

            // calculate direct throughfall:

            if (p_rain > 0.0) {

                direct_rain = p_rain * (1 - Cc);

                // calculate rain accumulation on canopy before evap loss:

                if (data.rain_load + p_rain * Cc > smax) {
                    drip_Cpy += (data.rain_load + p_rain * Cc - smax);
                    data.rain_load = smax;
                }
                else
                    data.rain_load += p_rain * Cc;

            }

            // calculate 'actual evap' of water from canopy and canopy storage after evaporation::
            // TODO: I don't understand why two different potential evaporation calcs are used for snow in canopy or no snow in canopy?
            // For now just use PT method for all cases because I don't want to include the Classevap CRHM class


            // If Liquid water exists in canopy
            if (data.rain_load > 0.0) {
                /*if(data.Snow_load == 0){ // use Granger when no snowcover IN CANOPY // Changed to use Snow_load to check if canopy has snow
                    if(data.rain_load >= hru_evap*Cc){ // (evaporation in mm)
                        intcp_evap = hru_evap*Cc;  //
                        data.rain_load -= hru_evap*Cc;
                    }
                    else{
                        intcp_evap = data.rain_load;
                        data.rain_load = 0.0;
                    }
                }
                else{ */// use Priestley-Taylor when snowcover IN CANOPY
                //double Q = iswr*86400/Global::Freq/1e6/lambda(ta); // convert w/m2 to mm/m^2/int (original CRHM)
                double temp_Global_Freq = global_param->dt() / 86400.0; // time steps per day (following CRHM convention)
                double Q = iswr * 86400.0 / temp_Global_Freq / 1e6 / lambda(ta); // convert w/m2 to mm/m^2/int TODO: Units don't make sense here (missing density of water??)

                if (iswr > 0.0)
                    Pevap = 1.26 * delta(ta) * Q / (delta(ta) + gamma(air_pressure, ta));
                else
                    Pevap = 0.0;

                if (data.rain_load >= Pevap * Cc) {  // (evaporation in mm)
                    intcp_evap = Pevap * Cc;  // check
                    data.rain_load -= Pevap * Cc;
                }
                else {
                    intcp_evap = data.rain_load; // check
                    data.rain_load = 0.0;
                }

            }

            // cumulative amounts (canopy)
            net_rain = direct_rain + drip_Cpy;
            data.cum_intcp_evap += intcp_evap;
            data.cum_Subl_Cpy += Subl_Cpy;

            break; // end Canopy
        }

    // Clearing or Gap
    case 1:  // clearing
        {
            // Do nothing
            break;
        }
    case 2:  // gap
        {
            // Do nothing
            break;
        }
    }  // canopy switch

    // cumulative amounts (canopy or not)
    net_p = net_rain + net_snow;
    data.cum_net_rain += net_rain;
    data.cum_net_snow += net_snow;


    // Output computed canopy states and fluxes downward to snowpack and upward to atmosphere
    (*face)["snow_load"_s]=data.Snow_load;
    (*face)["rain_load"_s]=data.rain_load;
    (*face)["ts_canopy"_s]=Ts;
    (*face)["ta_subcanopy"_s]=ta;
    (*face)["rh_subcanopy"_s]=rh;
    (*face)["iswr_subcanopy"_s]=Qsisn; // (W/m^2)
    (*face)["ilwr_subcanopy"_s]=Qlisn; // (W/m^2)
    (*face)["p_rain_subcanopy"_s]=net_rain; // (mm/int)
    (*face)["p_snow_subcanopy"_s]=net_snow; // (mm/int)
    (*face)["p_subcanopy"_s]=net_p; // Total precip (mm/int)
    (*face)["frac_precip_rain_subcanopy"_s]=net_rain/net_p; // Fraction rain (-)
    (*face)["frac_precip_snow_subcanopy"_s]=net_snow/net_p; // Fraction snow (-)

}

void Simple_Canopy::init(mesh& domain)
{

    #pragma omp parallel for
    // For each face
    for(size_t i=0;i<domain->size_local_faces();i++)
    {

        // Get current face
           auto face = domain->face(i);

           auto& d = face->make_module_data<Simple_Canopy::data>(ID);

           // Check if there is some vegetation spec  at this face
           if(face->has_vegetation() )
           {
                    d.CanopyHeight     = face->veg_attribute("CanopyHeight");
                    d.LAI              = face->veg_attribute("LAI");

                    // this parameterization just doesn't work well for LAI below 1.5, especially with tall trees
                    if(d.CanopyHeight > 0 && d.LAI < 1)
                    {
                        SPDLOG_ERROR("CanopyHeight was defined for triangle global_id={} but LAI < 1; Setting CanopyHeight to 0.25 (grass)", face->cell_global_id);

                        d.CanopyHeight = 0.25;
//
                    }

           // Get Canopy type (CRHM canop classifcation: Canopy, Clearing, or Gap)
                    // This might not exist if we are using distributed canopy heights
                    if(face->has_parameter("canopyType"))
                    {
                        d.canopyType       = face->veg_attribute("canopyType");
                    }
                    else
                    {
                        if(d.CanopyHeight < 0.3)
                            d.canopyType = 1; // clearing
                        else
                            d.canopyType = 0; // canopy
                    }


                   d.rain_load        = 0.0;
                   d.Snow_load        = 0.0;
                   d.cum_net_snow     = 0.0; // "Cumulative Canopy unload ", "(mm)"
                   d.cum_net_rain     = 0.0; // " direct_rain + drip", "(mm)"
                   d.cum_Subl_Cpy     = 0.0; //  "canopy snow sublimation", "(mm)"
                   d.cum_intcp_evap   = 0.0; // "HRU Evaporation from interception", "(mm)"
                   d.cum_SUnload_H2O  = 0.0; // "Cumulative unloaded canopy snow as water", "(mm)"

           } else
               {
         CHM_THROW_EXCEPTION(missing_value_error, "landcover not defined, but is required for simple_canopy module, please check the configuration file");
           }

    }

}

double Simple_Canopy::delta(double ta) // Slope of sat vap p vs t, kPa/°C
{
    if (ta > 0.0)
        return(2504.0*exp( 17.27 * ta/(ta+237.3)) / pow(ta+237.3,2));
    else
        return(3549.0*exp( 21.88 * ta/(ta+265.5)) / pow(ta+265.5,2));
}

double Simple_Canopy::lambda(double ta) // Latent heat of vaporization (mJ/(kg °C))
{
    return( 2.501 - 0.002361 * ta );
}

double Simple_Canopy::gamma(double air_pressure, double ta) // Psychrometric constant (kPa/°C)
{
    return( 0.00163 * air_pressure / lambda(ta)); // lambda (mJ/(kg °C))
}

double Simple_Canopy::Qs(double air_pressure, double T1) {
    /* INPUT
    air_pressure    - unadjusted air pressure in Pa
    T1              - Air temperature in K
    // OUTPUT
    Qs              - Saturated mixing ratio (kg/kg)
     */
    T1 = T1 - mio::Cst::t_water_freezing_pt; // K to C
    double es = 611.213*exp(22.4422*T1/(272.186+T1)); // Pa
    return(0.622 * ( es / (air_pressure - es) )); // kg/kg
}

void Simple_Canopy::checkpoint(mesh& domain,  netcdf& chkpt)
{

    chkpt.create_variable1D("Simple_Canopy:LAI", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:CanopyHeight", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:canopyType", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:rain_load", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:Snow_load", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:cum_net_snow", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:cum_net_rain", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:cum_Subl_Cpy", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:cum_intcp_evap", domain->size_local_faces());
    chkpt.create_variable1D("Simple_Canopy:cum_SUnload_H2O", domain->size_local_faces());


    //netcdf puts are not threadsafe.
    for (size_t i = 0; i < domain->size_local_faces(); i++)
    {
        auto face = domain->face(i);
        auto& d = face->get_module_data<data>(ID);

        chkpt.put_var1D("Simple_Canopy:LAI", i, d.LAI);
        chkpt.put_var1D("Simple_Canopy:CanopyHeight", i, d.CanopyHeight);
        chkpt.put_var1D("Simple_Canopy:canopyType", i, d.canopyType);
        chkpt.put_var1D("Simple_Canopy:rain_load", i, d.rain_load);
        chkpt.put_var1D("Simple_Canopy:Snow_load", i, d.Snow_load);
        chkpt.put_var1D("Simple_Canopy:cum_net_snow", i, d.cum_net_snow);
        chkpt.put_var1D("Simple_Canopy:cum_net_rain", i, d.cum_net_rain);
        chkpt.put_var1D("Simple_Canopy:cum_Subl_Cpy", i, d.cum_Subl_Cpy);
        chkpt.put_var1D("Simple_Canopy:cum_intcp_evap", i, d.cum_intcp_evap);
        chkpt.put_var1D("Simple_Canopy:cum_SUnload_H2O", i, d.cum_SUnload_H2O);

    }
}

void Simple_Canopy::load_checkpoint(mesh& domain, netcdf& chkpt)
{
    for (size_t i = 0; i < domain->size_local_faces(); i++)
    {
        auto face = domain->face(i);
        auto& d = face->get_module_data<data>(ID);

        d.LAI = chkpt.get_var1D("Simple_Canopy:LAI", i);
        d.CanopyHeight = chkpt.get_var1D("Simple_Canopy:CanopyHeight", i);
        d.canopyType = chkpt.get_var1D("Simple_Canopy:canopyType", i);
        d.rain_load = chkpt.get_var1D("Simple_Canopy:rain_load", i);
        d.Snow_load = chkpt.get_var1D("Simple_Canopy:Snow_load", i);
        d.cum_net_snow = chkpt.get_var1D("Simple_Canopy:cum_net_snow", i);
        d.cum_net_rain = chkpt.get_var1D("Simple_Canopy:cum_net_rain", i);
        d.cum_Subl_Cpy = chkpt.get_var1D("Simple_Canopy:cum_Subl_Cpy", i);
        d.cum_SUnload_H2O = chkpt.get_var1D("Simple_Canopy:cum_SUnload_H2O", i);

    }

}