def jmax_and_vcmax_func(self, temp): """ Maximum rate of electron transport (jmax) and of rubisco activity Parameters: ----------- temp : float air temperature Returns: -------- jmax : float maximum rate of electron transport vcmax : float maximum rate of Rubisco activity """ if float_gt(temp, 10.0): jmax = (self.params.jmaxna * (1.0 + (temp - 25.0) * (0.05 + (temp - 25.0) * (-1.81 * 1E-3 + (temp - 25.0) * (-1.37 * 1E-4))))) vcmax = (self.params.vcmaxna * (1.0 + (temp - 25.0) * (0.0485 + (temp - 25.0) * (-6.93 * 1E-4 + (temp - 25.0) * (-3.9 * 1E-5))))) elif float_gt(temp, 0.0): jmax = self.params.jmaxna * 0.0305 * temp vcmax = self.params.vcmaxna * 0.0238 * temp else: jmax = 0.0 vcmax = 0.0 return jmax, vcmax
def partition_plant_litter_n(self, nsurf, nsoil): """ Partition litter N from the plant (surface) and roots into metabolic and structural pools Parameters: ----------- nsurf : float N input from surface pool nsoil : float N input from soil pool """ # constant structural input n:c as per century if not self.control.strfloat: # dead plant litter -> structural pool # n flux -> surface structural pool self.fluxes.n_surf_struct_litter = (self.fluxes.surf_struct_litter / self.params.structcn) # n flux -> soil structural pool self.fluxes.n_soil_struct_litter = (self.fluxes.soil_struct_litter / self.params.structcn) # if not enough N for structural, all available N goes to structural if float_gt( self.fluxes.n_surf_struct_litter, nsurf): self.fluxes.n_surf_struct_litter = nsurf if float_gt(self.fluxes.n_soil_struct_litter, nsoil): self.fluxes.n_soil_struct_litter = nsoil # structural input n:c is a fraction of metabolic else: c_surf_struct_litter = (self.fluxes.surf_struct_litter * self.params.structrat + self.fluxes.surf_metab_litter) if float_eq(c_surf_struct_litter, 0.0): self.fluxes.n_surf_struct_litter = 0.0 else: self.fluxes.n_surf_struct_litter = (nsurf * self.fluxes.surf_struct_litter * self.params.structrat / c_surf_struct_litter) c_soil_struct_litter = (self.fluxes.soil_struct_litter * self.params.structrat + self.fluxes.soil_metab_litter) if float_eq(c_soil_struct_litter, 0.0): self.fluxes.n_soil_struct_litter = 0. else: self.fluxes.n_soil_struct_litter = (nsurf * self.fluxes.soil_struct_litter * self.params.structrat / c_soil_struct_litter) # remaining N goes to metabolic pools self.fluxes.n_surf_metab_litter = (nsurf - self.fluxes.n_surf_struct_litter) self.fluxes.n_soil_metab_litter = (nsoil - self.fluxes.n_soil_struct_litter)
def soil_temp_factor(self, project_day): """Soil-temperature activity factor (A9). Parameters: ----------- project_day : int current simulation day (index) Returns: -------- tfac : float soil temperature factor [degC] """ tsoil = self.met_data["tsoil"][project_day] if float_gt(tsoil, 0.0): tfac = 0.0326 + 0.00351 * tsoil ** 1.652 - (tsoil / 41.748) ** 7.19 if float_lt(tfac, 0.0): tfac = 0.0 else: # negative number cannot be raised to a fractional power # number would need to be complex tfac = 0.0 return tfac
def epsilon(self, asat, par, daylen, alpha): """ Canopy scale LUE using method from Sands 1995, 1996. Sands derived daily canopy LUE from Asat by modelling the light response of photosysnthesis as a non-rectangular hyperbola with a curvature (theta) and a quantum efficiency (alpha). Assumptions of the approach are: - horizontally uniform canopy - PAR varies sinusoidally during daylight hours - extinction coefficient is constant all day - Asat and incident radiation decline through the canopy following Beer's Law. - leaf transmission is assumed to be zero. * Numerical integration of "g" is simplified to 6 intervals. Parameters: ---------- asat : float Light-saturated photosynthetic rate at the top of the canopy par : float photosyntetically active radiation (umol m-2 d-1) daylen : float length of day (hrs). theta : float curvature of photosynthetic light response curve alpha : float quantum yield of photosynthesis (mol mol-1) Returns: ------- lue : float integrated light use efficiency over the canopy (umol C umol-1 PAR) References: ----------- See assumptions above... * Sands, P. J. (1995) Australian Journal of Plant Physiology, 22, 601-14. """ delta = 0.16666666667 # subintervals scaler, i.e. 6 intervals h = daylen * const.SECS_IN_HOUR # number of seconds of daylight if float_gt(asat, 0.0): # normalised daily irradiance q = pi * self.params.kext * alpha * par / (2.0 * h * asat) integral_g = 0.0 for i in xrange(1, 13, 2): sinx = sin(pi * i / 24.0) arg1 = sinx arg2 = 1.0 + q * sinx arg3 = sqrt((1.0 + q * sinx) ** 2.0 - 4.0 * self.params.theta * q * sinx) integral_g += arg1 / (arg2 + arg3) * delta lue = alpha * integral_g * pi else: lue = 0.0 return lue
def calc_soil_evaporation(self, tavg, net_rad, press, daylen, sw_rad): """ Use Penman eqn to calculate top soil evaporation flux at the potential rate. Soil evaporation is dependent upon soil wetness and plant cover. The net radiation term is scaled for the canopy cover passed to this func and the impact of soil wetness is accounted for in the wtfac term. As the soil dries the evaporation component reduces significantly. Key assumptions from Ritchie... * When plant provides shade for the soil surface, evaporation will not be the same as bare soil evaporation. Wind speed, net radiation and VPD will all belowered in proportion to the canopy density. Following Ritchie role ofwind, VPD are assumed to be negligible and are therefore ignored. These assumptions are based on work with crops and whether this holds for tree shading where the height from the soil to the base of the crown is larger is questionable. units = (mm/day) References: ----------- * Ritchie, 1972, Water Resources Research, 8, 1204-1213. Parameters: ----------- tavg : float average daytime temp [degC] net_rad : float net radiation [mj m-2 day-1] press : float average daytime pressure [kPa] Returns: -------- soil_evap : float soil evaporation [mm d-1] """ P = Penman() soil_evap = P.calc_evaporation(net_rad, tavg, press) # Surface radiation is reduced by overstory LAI cover. This empirical # fit comes from Ritchie (1972) and is formed by a fit between the LAI # of 5 crops types and the fraction of observed net radiation at the # surface. Whilst the LAI does cover a large range, nominal 0–6, there # are only 12 measurements and only three from LAI > 3. So this might # not hold as well for a forest canopy? # Ritchie 1972, Water Resources Research, 8, 1204-1213. if float_gt(self.state.lai, 0.0): soil_evap *= exp(-0.398 * self.state.lai) # reduce soil evaporation if top soil is dry soil_evap *= self.state.wtfac_topsoil tconv = 60.0 * 60.0 * daylen # seconds to day return soil_evap * tconv
def soil_temp_factor(self, project_day): """Soil-temperature activity factor (A9). Fit to Parton's fig 2a Parameters: ----------- project_day : int current simulation day (index) Returns: -------- tfac : float soil temperature factor [degC] """ tsoil = self.met_data['tsoil'][project_day] if float_gt(tsoil, 0.0): self.fluxes.tfac_soil_decomp = (0.0326 + 0.00351 * tsoil**1.652 - (tsoil / 41.748)**7.19) if float_lt(self.fluxes.tfac_soil_decomp, 0.0): self.fluxes.tfac_soil_decomp = 0.0 else: # negative number cannot be raised to a fractional power # number would need to be complex self.fluxes.tfac_soil_decomp = 0.0 return self.fluxes.tfac_soil_decomp
def calc_transpiration(self): """ units mm/day """ if float_gt(self.fluxes.wue, 0.0): self.fluxes.transpiration = self.fluxes.gpp_gCm2 / self.fluxes.wue else: self.fluxes.transpiration = 0.0
def epsilon(self, asat, par, daylen, alpha): """ Canopy scale LUE using method from Sands 1995, 1996. Sands derived daily canopy LUE from Asat by modelling the light response of photosysnthesis as a non-rectangular hyperbola with a curvature (theta) and a quantum efficiency (alpha). Assumptions of the approach are: - horizontally uniform canopy - PAR varies sinusoidally during daylight hours - extinction coefficient is constant all day - Asat and incident radiation decline through the canopy following Beer's Law. - leaf transmission is assumed to be zero. * Numerical integration of "g" is simplified to 6 intervals. Parameters: ---------- asat : float photosynthetic rate at the top of the canopy par : float incident photosyntetically active radiation daylen : float length of day (hrs). theta : float curvature of photosynthetic light response curve alpha : float quantum yield of photosynthesis (mol mol-1) Returns: ------- lue : float integrated light use efficiency over the canopy (mol C mol-1 PAR) References: ----------- See assumptions above... * Sands, P. J. (1995) Australian Journal of Plant Physiology, 22, 601-14. """ delta = 0.16666666667 # subintervals scaler, i.e. 6 intervals h = daylen * const.HRS_TO_SECS theta = self.params.theta # local var if float_gt(asat, 0.0): q = pi * self.params.kext * alpha * par / (2.0 * h * asat) integral_g = 0.0 for i in xrange(1, 13, 2): sinx = sin(pi * i / 24.) arg1 = sinx arg2 = 1.0 + q * sinx arg3 = sqrt((1.0 + q * sinx)**2.0 - 4.0 * theta * q * sinx) integral_g += arg1 / (arg2 + arg3) * delta lue = alpha * integral_g * pi else: lue = 0.0 return lue
def decay_in_dry_soils(self, decay_rate, decay_rate_dry): """Decay rates (e.g. leaf litterfall) can increase in dry soil, adjust decay param Parameters: ----------- decay_rate : float default model parameter decay rate [tonnes C/ha/day] decay_rate_dry : float default model parameter dry deacy rate [tonnes C/ha/day] Returns: -------- decay_rate : float adjusted deacy rate if the soil is dry [tonnes C/ha/day] """ # turn into fraction... smc_root = self.state.pawater_root / self.params.wcapac_root new_decay_rate = (decay_rate_dry - (decay_rate_dry - decay_rate) * (smc_root - self.params.watdecaydry) / (self.params.watdecaywet - self.params.watdecaydry)) if float_lt(new_decay_rate, decay_rate): new_decay_rate = decay_rate if float_gt(new_decay_rate, decay_rate_dry): new_decay_rate = decay_rate_dry return new_decay_rate
def nc_limit(self, cpool, npool, ncmin, ncmax): """ Release N to 'Inorgn' pool or fix N from 'Inorgn', in order to keep the N:C ratio of a litter pool within the range 'ncmin' to 'ncmax'. Parameters: ----------- cpool : float various C pool (state) npool : float various N pool (state) ncmin : float maximum N:C ratio ncmax : float minimum N:C ratio Returns: -------- fix/rel : float amount of N to be added/released from the inorganic pool """ nmax = cpool * ncmax nmin = cpool * ncmin if float_gt(npool, nmax): #release rel = npool - nmax self.fluxes.nlittrelease += rel return -rel elif float_lt(npool, nmin): #fix fix = nmin - npool self.fluxes.nlittrelease -= fix return fix else: return 0.0
def calc_infiltration(self, rain): """ Estimate "effective" rain, or infiltration I guess. Simple assumption that infiltration relates to leaf area and therefore canopy storage capacity (wetloss). Interception is likely to be ("more") erroneous if a canopy is subject to frequent daily rainfall I would suggest. Parameters: ------- rain : float rainfall [mm d-1] """ if float_gt(rain, 0.0): self.fluxes.interception = self.state.lai * self.params.wetloss self.fluxes.interception = clip(self.fluxes.interception, min=self.fluxes.interception, max=rain) self.fluxes.erain = (rain * self.params.rfmult - self.fluxes.interception) else: self.fluxes.interception = 0.0 self.fluxes.erain = 0.0
def inputs_from_structrual_pool(self, nsurf, nsoil): """structural pool input fluxes Parameters: ----------- nsurf : float N input from surface pool nsoil : float N input from soil pool """ # constant structural input n:c as per century if not self.control.strfloat: # dead plant -> structural # surface self.fluxes.nresid[0] = self.fluxes.cresid[0] / self.params.structcn # soil self.fluxes.nresid[1] = self.fluxes.cresid[1] / self.params.structcn # if not enough N for structural, all available N goes to structural if float_gt(self.fluxes.nresid[0], nsurf): self.fluxes.nresid[0] = nsurf if float_gt(self.fluxes.nresid[1], nsoil): self.fluxes.nresid[1] = nsoil else: # structural input n:c is a fraction of metabolic cwgtsu = (self.fluxes.cresid[0] * self.params.structrat + self.fluxes.cresid[2]) if float_eq(cwgtsu, 0.0): self.fluxes.nresid[0] = 0.0 else: self.fluxes.nresid[0] = (nsurf * self.fluxes.cresid[0] * self.params.structrat / cwgtsu) cwgtsl = (self.fluxes.cresid[1] * self.params.structrat + self.fluxes.cresid[3]) if float_eq(cwgtsl, 0.0): self.fluxes.nresid[1] = 0. else: self.fluxes.nresid[1] = (nsurf * self.fluxes.cresid[1] * self.params.structrat / cwgtsl)
def calculate_top_of_canopy_n(self): """ Calculate the canopy N at the top of the canopy (g N m-2), N0. See notes and Chen et al 93, Oecologia, 93,63-69. """ if float_gt(self.state.lai, 0.0): # calculation for canopy N content at the top of the canopy N0 = self.state.ncontent * self.params.kext / (1.0 - exp(-self.params.kext * self.state.lai)) else: N0 = 0.0 return N0
def adjust_cproduction(self, option): """ select model? It seems hybrid is the same as biomass, so check this and remove. I have set it up so that a call to hybrid just calls biomass Parameters: ----------- option : integer model option """ if option == 1 or option == 3: self.monteith_rescap_model() elif option == 2 and float_gt(self.params.fwpmax, self.params.fwpmin): self.biomass_model() elif option == 3 and float_gt(self.params.fwpmax, self.params.fwpmin): self.hybrid_model() # same as calling biomass model! else: err_msg = "Unknown water bal model (try 1-3): %s\n" % option raise RuntimeError, err_msg
def clip(value, min=None, max=None): """ clip value btw defined range """ if float_lt(value, min): value = min elif float_gt(value, max): value = max return value
def calculate_top_of_canopy_n(self): """ Calculate the canopy N at the top of the canopy (g N m-2), N0. See notes and Chen et al 93, Oecologia, 93,63-69. """ if float_gt(self.state.lai, 0.0): # calculation for canopy N content at the top of the canopy N0 = (self.state.ncontent * self.params.kext / (1.0 - exp(-self.params.kext * self.state.lai))) else: N0 = 0.0 return N0
def calc_evaporation(self, vpd, wind, gs, net_rad, tavg, press, canht=None, ga=None): """ Parameters: ----------- vpd : float vapour pressure def [kPa] wind : float average daytime wind speed [m s-1] gs : float stomatal conductance [m s-1] net_rad : float net radiation [mj m-2 s-1] tavg : float daytime average temperature [degC] press : float average daytime pressure [kPa] Returns: -------- et : float evapotranspiration [mm d-1] """ # if not read from met file calculate atmospheric pressure from sea lev if press == None: press = self.calc_atmos_pressure() lambdax = self.calc_latent_heat_of_vapourisation(tavg) gamma = self.calc_pyschrometric_constant(lambdax, press) slope = self.calc_slope_of_saturation_vapour_pressure_curve(tavg) rho = self.calc_density_of_air(tavg) if ga is None: ga = self.canopy_boundary_layer_conductance(wind, canht) if float_gt(gs, 0.0): # decoupling coefficent, Jarvis and McNaughton, 1986 # when omega is close to zero, it is said to be well coupled and # gs is the dominant controller of water loss (gs<ga). e = slope / gamma # chg of latent heat relative to sensible heat of air omega = (e + 1.0) / (e + 1.0 + (ga / gs)) arg1 = ((slope * net_rad ) + (rho * self.cp * vpd * ga)) arg2 = slope + gamma * (1.0 + (ga / gs)) et = (arg1 / arg2) / lambdax else: et = 0.0 omega = 0.0 return et, omega
def calc_evaporation(self, vpd, wind, gs, net_rad, tavg, press): """ Parameters: ----------- vpd : float vapour pressure def [kPa] wind : float average daytime wind speed [m s-1] gs : float stomatal conductance [m s-1] net_rad : float net radiation [mj m-2 s-1] tavg : float daytime average temperature [degC] press : float average daytime pressure [kPa] Returns: -------- et : float evapotranspiration [mm d-1] """ # if not read from met file calculate atmospheric pressure from sea lev if press == None: press = self.calc_atmos_pressure() lambdax = self.calc_latent_heat_of_vapourisation(tavg) gamma = self.calc_pyschrometric_constant(lambdax, press) slope = self.calc_slope_of_saturation_vapour_pressure_curve(tavg) rho = self.calc_density_of_air(tavg) ga = self.calc_atmos_boundary_layer_conductance(wind) # our model is a big leaf, so canopy conductance, gc = gs gc = gs if float_gt(gc, 0.0): # decoupling coefficent, Jarvis and McNaughton, 1986 e = slope / gamma # chg of latent heat relative to sensible heat of air omega = (e + 1.0) / (e + 1.0 + (ga / gc)) arg1 = ((slope * net_rad ) + (rho * self.cp * vpd * ga)) arg2 = slope + gamma * (1.0 + ga / gc) et = (arg1 / arg2) / lambdax else: et = 0.0 omega = 0.0 return et
def epsilon(self, amax, par, daylen, alpha): """ Canopy scale LUE using method from Sands 1995, 1996. Numerical integration of g is simplified to 6 intervals. Leaf transmission is assumed to be zero. Parameters: ---------- amax : float photosynthetic rate at the top of the canopy par : float incident photosyntetically active radiation daylen : float length of day in hours. theta : float curvature of photosynthetic light response curve alpha : float quantum yield of photosynthesis (mol mol-1) Returns: ------- lue : float integrated light use efficiency over the canopy References: ----------- See assumptions above... * Sands, P. J. (1995) Australian Journal of Plant Physiology, 22, 601-14. * LUE stuff comes from Sands 1996 """ delta = 0.16666666667 # subintervals scaler, i.e. 6 intervals h = daylen * const.HRS_TO_SECS theta = self.params.theta # local var if float_gt(amax, 0.0): q = pi * self.params.kext * alpha * par / (2.0 * h * amax) integral = 0.0 for i in xrange(1, 13, 2): sinx = sin(pi * i / 24.) arg1 = sinx arg2 = 1.0 + q * sinx arg3 = sqrt((1.0 + q * sinx)**2.0 - 4.0 * theta * q * sinx) integral += arg1 / (arg2 + arg3) * delta lue = alpha * integral * pi else: lue = 0.0 return lue
def calc_wue(self, vpd, ca, amb_co2): """water use efficiency Not sure of units conversions here, have to ask BM Parameters: ----------- vpd : float average daily vpd [kPa] ca : float atmospheric co2, depending on flag set in param file this will be ambient or elevated. [umol mol-1] """ if self.control.wue_model == 0: # Gday original implementation # (gC / kg H20) if float_gt(vpd, 0.0): # WUE Power law dependence on co2, Pepper et al 2005. co2_ratio = (ca / amb_co2) co2_adjustment = co2_ratio**self.params.co2_effect_on_wue # wue inversely proportional to daily mean vpd self.fluxes.wue = self.params.wue0 * co2_adjustment / vpd else: self.fluxes.wue = 0.0 elif self.control.wue_model == 1 and self.control.assim_model == 7: conv = const.MOL_C_TO_GRAMS_C / const.MOL_WATER_TO_GRAMS_WATER self.fluxes.wue = (conv * 1000.0 * (ca * const.UMOL_TO_MOL * (1.0 - self.fluxes.cica_avg) / (1.6 * vpd / 101.0))) #if self.fluxes.wue > 20.0: self.fluxes.wue = 20.0 # FIX THIS!!! # what is this? ask BM elif self.control.wue_model == 2: self.fluxes.wue = (self.params.wue0 * 0.27273 / vpd * ca / amb_co2) elif self.control.wue_model == 3 : if float_eq(self.fluxes.transpiration, 0.0): self.fluxes.wue = 0.0 else: self.fluxes.wue = (self.fluxes.gpp_gCm2 / self.fluxes.transpiration) else: raise AttributeError('Unknown WUE calculation option')
def epsilon(self, amax, par, daylen, alpha): """ Canopy scale LUE using method from Sands 1995, 1996. Parameters: ---------- amax : float photosynthetic rate at the top of the canopy par : float incident photosyntetically active radiation daylen : float length of day in hours. theta : float curvature of photosynthetic light response curve alpha : float quantum yield of photosynthesis (mol mol-1) Returns: ------- lue : float integrated light use efficiency over the canopy References: ----------- See assumptions above... * Sands, P. J. (1995) Australian Journal of Plant Physiology, 22, 601-14. """ if float_gt(amax, 0.0): q = (math.pi * self.params.kext * alpha * par / (2.0 * daylen * const.HRS_TO_SECS * amax)) # check sands but shouldn't it be 2 * q * sin x on the top? f = (lambda x: x / (1.0 + q * x + math.sqrt((1.0 + q * x)**2.0 - 4.0 * self.params.theta * q * x))) g = [f(math.sin(math.pi * i / 24.)) for i in xrange(1, 13, 2)] #Trapezoidal rule - seems more accurate gg = 0.16666666667 * sum(g) lue = alpha * gg * math.pi else: lue = 0.0 return lue
def day_length(date, latitude): """ Figure out number of sunlight hours, (hours day-1) Routine from sdgvm. date is a python object, see datetime library for more info Parameters: ----------- date : date format string date object, yr/month/day latitude : float latitude [degrees] Returns: -------- dayl : float daylength [hrs] """ conv = math.pi / 180.0 # day of year 1-365/366 doy = int(date.strftime('%j')) # Total number of days in year if calendar.isleap(date.year): yr_days = 366. else: yr_days = 365. solar_declin = -23.4 * math.cos(conv * yr_days * (doy + 10.0) / yr_days) temx = -math.tan(latitude * conv) * math.tan(solar_declin * conv) if float_lt(math.fabs(temx), 1.0): has = math.acos(temx) / conv dayl = 2.0 * has / 15.0 elif float_gt(temx, 0.0): dayl = 0.0 else: dayl = 24.0 return dayl
def calculate_mineralisation(self): """Mineralisation calculations. Plant N uptake, soil N loss, N gross mineralisation, N immobilisation """ # gross n mineralisation (t/ha/yr) self.fluxes.ngross = (sum(self.fluxes.nstruct) + sum(self.fluxes.nmetab) + sum(self.fluxes.nactive) + sum(self.fluxes.nslow) + self.fluxes.npassive) # N:C new SOM - slope is an object containing active, slow and passive slope = self.calc_new_nc_slope_params() # Mineral N pool (numer1, numer2, denom) = self.calc_mineral_npool(slope) # evaluate n immobilisation in new SOM self.fluxes.nimmob = numer1 + denom * self.state.inorgn if float_gt(self.fluxes.nimmob, numer2): self.fluxes.nimmob = numer2
def grazer_inputs(self): """ Grazer inputs from faeces and urine, flux detd by faeces c:n """ if self.control.grazing: self.params.faecesn = self.fluxes.faecesc / self.params.faecescn else: self.params.faecesn = 0.0 # make sure faecesn <= total n input to soil from grazing arg = self.fluxes.neaten * self.params.fractosoil if float_gt(self.params.faecesn, arg): self.params.faecesn = self.fluxes.neaten * self.params.fractosoil # urine=total-faeces if self.control.grazing: self.fluxes.nurine = self.fluxes.neaten * self.params.fractosoil - self.params.faecesn else: self.fluxes.nurine = 0.0 if float_lt(self.fluxes.nurine, 0.0): self.fluxes.nurine = 0.0
def grazer_inputs(self): """ Grazer inputs from faeces and urine, flux detd by faeces c:n """ if self.control.grazing: self.params.faecesn = self.fluxes.faecesc / self.params.faecescn else: self.params.faecesn = 0.0 # make sure faecesn <= total n input to soil from grazing arg = self.fluxes.neaten * self.params.fractosoil if float_gt(self.params.faecesn, arg): self.params.faecesn = self.fluxes.neaten * self.params.fractosoil # urine=total-faeces if self.control.grazing: self.fluxes.nurine = (self.fluxes.neaten * self.params.fractosoil - self.params.faecesn) else: self.fluxes.nurine = 0.0 if float_lt(self.fluxes.nurine, 0.0): self.fluxes.nurine = 0.0
def model_one(self, day): """ old g'day n:c and temperature factors References: ----------- * mcmurtrie et al 1992 aust j bot Parameters: ----------- day : integer simulation day Returns: -------- npp : float net primary productivity """ (sw_rad, ca, temp) = self.get_met_data(day) if float_eq(sw_rad, 0.0): sw_rad = 0.000001 rco2 = 1.632 * (ca - 60.9) / (ca + 121.8) gpp_max = ((self.params.cfracts * sw_rad / const.M2_AS_HA * self.params.epsilon * const.G_AS_TONNES) * rco2) # leaf n:c effect on photosynthetic efficiency (theta = 1). if float_gt(self.state.shootnc, self.params.n_crit): lue = 1.0 else: rat = self.state.shootnc / self.params.n_crit if float_eq(self.params.ncpower, 1.0): lue = rat else: lue = pow(rat, self.params.ncpower) return lue * gpp_max * self.state.light_interception
def decay_in_dry_soils(self, decay_rate, decay_rate_dry): """Decay rates (e.g. leaf litterfall) can increase in dry soil, adjust decay param. This is based on field measurements by F. J. Hingston (unpublished) cited in Corbeels. Parameters: ----------- decay_rate : float default model parameter decay rate [tonnes C/ha/day] decay_rate_dry : float default model parameter dry deacy rate [tonnes C/ha/day] Returns: -------- decay_rate : float adjusted deacy rate if the soil is dry [tonnes C/ha/day] Reference: ---------- Corbeels et al. (2005) Ecological Modelling, 187, 449-474. """ # turn into fraction... smc_root = self.state.pawater_root / self.params.wcapac_root new_decay_rate = decay_rate_dry - (decay_rate_dry - decay_rate) * (smc_root - self.params.watdecaydry) / ( self.params.watdecaywet - self.params.watdecaydry ) if float_lt(new_decay_rate, decay_rate): new_decay_rate = decay_rate if float_gt(new_decay_rate, decay_rate_dry): new_decay_rate = decay_rate_dry return new_decay_rate
def decay_in_dry_soils(self, decay_rate, decay_rate_dry): """Decay rates (e.g. leaf litterfall) can increase in dry soil, adjust decay param. This is based on field measurements by F. J. Hingston (unpublished) cited in Corbeels. Parameters: ----------- decay_rate : float default model parameter decay rate [tonnes C/ha/day] decay_rate_dry : float default model parameter dry deacy rate [tonnes C/ha/day] Returns: -------- decay_rate : float adjusted deacy rate if the soil is dry [tonnes C/ha/day] Reference: ---------- Corbeels et al. (2005) Ecological Modelling, 187, 449-474. """ # turn into fraction... smc_root = self.state.pawater_root / self.params.wcapac_root new_decay_rate = (decay_rate_dry - (decay_rate_dry - decay_rate) * (smc_root - self.params.watdecaydry) / (self.params.watdecaywet - self.params.watdecaydry)) if float_lt(new_decay_rate, decay_rate): new_decay_rate = decay_rate if float_gt(new_decay_rate, decay_rate_dry): new_decay_rate = decay_rate_dry return new_decay_rate
def calculate_nimmobilisation(self): """ Calculated N immobilised in new soil organic matter General equation for new soil N:C ratio vs Nmin, expressed as linear equation passing through point Nmin0, actncmin (etc). Values can be Nmin0=0, Actnc0=Actncmin if Nmin < Nmincrit: New soil N:C = soil N:C (when Nmin=0) + slope * Nmin if Nmin > Nmincrit New soil N:C = max soil N:C NB N:C ratio of new passive SOM can change even if assume Passiveconst Returns: -------- nimob : float N immobilsed """ # N:C new SOM - active, slow and passive self.state.actncslope = self.calculate_nc_slope(self.params.actncmax, self.params.actncmin) self.state.slowncslope = self.calculate_nc_slope(self.params.slowncmax, self.params.slowncmin) self.state.passncslope = self.calculate_nc_slope(self.params.passncmax, self.params.passncmin) nmin = self.params.nmin0 * const.G_M2_2_TONNES_HA arg1 = ((self.fluxes.cactive[1] + self.fluxes.cslow[1]) * (self.params.passncmin - self.state.passncslope * nmin)) arg2 = ((self.fluxes.cstruct[0] + self.fluxes.cstruct[2] + self.fluxes.cactive[0]) * (self.params.slowncmin - self.state.slowncslope * nmin)) arg3 = ((self.fluxes.cstruct[1] + self.fluxes.cstruct[3] + sum(self.fluxes.cmetab) + self.fluxes.cslow[0] + self.fluxes.passive) * (self.params.actncmin - self.state.actncslope * nmin)) numer1 = arg1 + arg2 + arg3 arg1 = ((self.fluxes.cactive[1] + self.fluxes.cslow[1]) * self.params.passncmax) arg2 = ((self.fluxes.cstruct[0] + self.fluxes.cstruct[2] + self.fluxes.cactive[0]) * self.params.slowncmax) arg3 = ((self.fluxes.cstruct[1] + self.fluxes.cstruct[3] + sum(self.fluxes.cmetab) + self.fluxes.cslow[0] + self.fluxes.passive) * self.params.actncmax) numer2 = arg1 + arg2 + arg3 arg1 = ((self.fluxes.cactive[1] + self.fluxes.cslow[1]) * self.state.passncslope) arg2 = ((self.fluxes.cstruct[0] + self.fluxes.cstruct[2] + self.fluxes.cactive[0]) * self.state.slowncslope) arg3 = ((self.fluxes.cstruct[1] + self.fluxes.cstruct[3] + sum(self.fluxes.cmetab) + self.fluxes.cslow[0] + self.fluxes.passive) * self.state.actncslope) denom = arg1 + arg2 + arg3 # evaluate N immobilisation in new SOM nimmob = numer1 + denom * self.state.inorgn if float_gt(nimmob, numer2): nimmob = numer2 return nimmob
def calculate_n_immobilisation(self): """ N immobilised in new soil organic matter, the reverse of mineralisation. Micro-organisms in the soil compete with plants for N. Immobilisation is the process by which nitrate and ammonium are taken up by the soil organisms and thus become unavailable to the plant (->organic N). When C:N ratio is high the microorganisms need more nitrogen from the soil to decompose the carbon in organic materials. This N will be immobilised until these microorganisms die and the nitrogen is released. General equation for new soil N:C ratio vs Nmin, expressed as linear equation passing through point Nmin0, actncmin (etc). Values can be Nmin0=0, Actnc0=Actncmin if Nmin < Nmincrit: New soil N:C = soil N:C (when Nmin=0) + slope * Nmin if Nmin > Nmincrit New soil N:C = max soil N:C NB N:C ratio of new passive SOM can change even if assume Passiveconst Returns: -------- nimob : float N immobilsed """ # N:C new SOM - active, slow and passive active_nc_slope = self.calculate_nc_slope(self.params.actncmax, self.params.actncmin) slow_nc_slope = self.calculate_nc_slope(self.params.slowncmax, self.params.slowncmin) passive_nc_slope = self.calculate_nc_slope(self.params.passncmax, self.params.passncmin) # C flux entering SOM pools - use short names active_influxes = self.fluxes.c_into_active slow_influxes = self.fluxes.c_into_slow passive_influxes = self.fluxes.c_into_passive # convert units nmin = self.params.nmin0 / const.M2_AS_HA * const.G_AS_TONNES arg1 = ((self.params.passncmin - passive_nc_slope * nmin) * passive_influxes) arg2 = (self.params.slowncmin - slow_nc_slope * nmin) * slow_influxes arg3 = active_influxes * (self.params.actncmin - active_nc_slope * nmin) numer1 = arg1 + arg2 + arg3 arg1 = passive_influxes * self.params.passncmax arg2 = slow_influxes * self.params.slowncmax arg3 = active_influxes * self.params.actncmax numer2 = arg1 + arg2 + arg3 arg1 = passive_influxes * passive_nc_slope arg2 = slow_influxes * slow_nc_slope arg3 = active_influxes * active_nc_slope denom = arg1 + arg2 + arg3 # evaluate N immobilisation in new SOM nimmob = numer1 + denom * self.state.inorgn if float_gt(nimmob, numer2): nimmob = numer2 return (nimmob, active_nc_slope, slow_nc_slope, passive_nc_slope)
def calculate_npools(self, active_nc_slope, slow_nc_slope, passive_nc_slope): """ Update N pools in the soil Parameters ---------- active_nc_slope : float active NC slope slow_nc_slope: float slow NC slope passive_nc_slope : float passive NC slope """ # net N release implied by separation of litter into structural # & metabolic. The following pools only fix or release N at their # limiting n:c values. # N released or fixed from the N inorganic pool is incremented with # each call to nc_limit and stored in self.fluxes.nlittrelease self.fluxes.nlittrelease = 0.0 self.state.structsurfn += (self.fluxes.n_surf_struct_litter - (self.fluxes.n_surf_struct_to_slow + self.fluxes.n_surf_struct_to_active)) if not self.control.strfloat: self.state.structsurfn += self.nc_limit(self.state.structsurf, self.state.structsurfn, 1.0/self.params.structcn, 1.0/self.params.structcn) self.state.structsoiln += (self.fluxes.n_soil_struct_litter - (self.fluxes.n_soil_struct_to_slow + self.fluxes.n_soil_struct_to_active)) if not self.control.strfloat: self.state.structsoiln += self.nc_limit(self.state.structsoil, self.state.structsoiln, 1.0/self.params.structcn, 1.0/self.params.structcn) self.state.metabsurfn += (self.fluxes.n_surf_metab_litter - self.fluxes.n_surf_metab_to_active) self.state.metabsurfn += self.nc_limit(self.state.metabsurf, self.state.metabsurfn, 1.0/25.0, 1.0/10.0) self.state.metabsoiln += (self.fluxes.n_soil_metab_litter - self.fluxes.n_soil_metab_to_active) self.state.metabsoiln += self.nc_limit(self.state.metabsoil, self.state.metabsoiln, 1.0/25.0, 1.0/10.0) # When nothing is being added to the metabolic pools, there is the # potential scenario with the way the model works for tiny bits to be # removed with each timestep. Effectively with time this value which is # zero can end up becoming zero but to a silly decimal place self.precision_control() # Update SOM pools n_into_active = (self.fluxes.n_surf_struct_to_active + self.fluxes.n_soil_struct_to_active + self.fluxes.n_surf_metab_to_active + self.fluxes.n_soil_metab_to_active + self.fluxes.n_slow_to_active + self.fluxes.n_passive_to_active) n_out_of_active = (self.fluxes.n_active_to_slow + self.fluxes.n_active_to_passive) n_into_slow = (self.fluxes.n_surf_struct_to_slow + self.fluxes.n_soil_struct_to_slow + self.fluxes.n_active_to_slow) n_out_of_slow = (self.fluxes.n_slow_to_active + self.fluxes.n_slow_to_passive) n_into_passive = (self.fluxes.n_active_to_passive + self.fluxes.n_slow_to_passive) n_out_of_passive = self.fluxes.n_passive_to_active # N:C of the SOM pools increases linearly btw prescribed min and max # values as the Nconc of the soil increases. arg = (self.state.inorgn - self.params.nmin0 / const.M2_AS_HA * const.G_AS_TONNES) # active active_nc = self.params.actncmin + active_nc_slope * arg if float_gt(active_nc, self.params.actncmax): active_nc = self.params.actncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_active, n_into_active, active_nc) self.state.activesoiln += n_into_active + fixn - n_out_of_active # slow slow_nc = self.params.slowncmin + slow_nc_slope * arg if float_gt(slow_nc, self.params.slowncmax): slow_nc = self.params.slowncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_slow, n_into_slow, slow_nc) self.state.slowsoiln += n_into_slow + fixn - n_out_of_slow # passive, update passive pool only if passiveconst=0 pass_nc = self.params.passncmin + passive_nc_slope * arg if float_gt(pass_nc, self.params.passncmax): pass_nc = self.params.passncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_passive, n_into_passive, pass_nc) self.state.passivesoiln += n_into_passive + fixn - n_out_of_passive # Daily increment of soil inorganic N pool, diff btw in and effluxes # (grazer urine n goes directly into inorganic pool) nb inorgn may be # unstable if rateuptake is large self.state.inorgn += ((self.fluxes.ngross + self.fluxes.ninflow + self.fluxes.nurine - self.fluxes.nimmob - self.fluxes.nloss - self.fluxes.nuptake) + self.fluxes.nlittrelease)
def update_plant_state(self, fdecay, rdecay, project_day, doy): """ Daily change in C content Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate """ # # Carbon pools # self.state.shoot += (self.fluxes.cpleaf - self.fluxes.deadleaves - self.fluxes.ceaten) self.state.root += self.fluxes.cproot - self.fluxes.deadroots self.state.croot += self.fluxes.cpcroot - self.fluxes.deadcroots self.state.branch += self.fluxes.cpbranch - self.fluxes.deadbranch self.state.stem += self.fluxes.cpstem - self.fluxes.deadstems # annoying but can't see an easier way with the code as it is. # If we are modelling grases, i.e. no stem them without this # the sapwood will end up being reduced to a silly number as # deadsapwood will keep being removed from the pool, even though there # is no wood. if self.state.stem <= 0.01: self.state.sapwood = 0.01 else: self.state.sapwood += self.fluxes.cpstem - self.fluxes.deadsapwood # # Nitrogen pools # if self.control.deciduous_model: self.state.shootn += (self.fluxes.npleaf - (self.fluxes.lnrate * self.state.remaining_days[doy]) - self.fluxes.neaten) else: self.state.shootn += (self.fluxes.npleaf - fdecay * self.state.shootn - self.fluxes.neaten) self.state.branchn += (self.fluxes.npbranch - self.params.bdecay * self.state.branchn) self.state.rootn += self.fluxes.nproot - rdecay * self.state.rootn self.state.crootn += self.fluxes.npcroot - self.params.crdecay * self.state.crootn self.state.stemnimm += (self.fluxes.npstemimm - self.params.wdecay * self.state.stemnimm) self.state.stemnmob += (self.fluxes.npstemmob - self.params.wdecay * self.state.stemnmob - self.params.retransmob * self.state.stemnmob) self.state.stemn = self.state.stemnimm + self.state.stemnmob if self.control.deciduous_model: self.calculate_cn_store() #============================ # Enforce maximum N:C ratios. # =========================== # This doesn't make sense for the deciduous model because of the ramp # function. The way the deciduous logic works we now before we start # how much N we have to allocate so it is impossible (well) to allocate in # excess. Therefore this is only relevant for evergreen model. if not self.control.deciduous_model: # If foliage or root N/C exceeds its max, then N uptake is cut back # maximum leaf n:c ratio is function of stand age # - switch off age effect by setting ncmaxfyoung = ncmaxfold age_effect = ((self.state.age - self.params.ageyoung) / (self.params.ageold - self.params.ageyoung)) ncmaxf = (self.params.ncmaxfyoung - (self.params.ncmaxfyoung - self.params.ncmaxfold) * age_effect) if float_lt(ncmaxf, self.params.ncmaxfold): ncmaxf = self.params.ncmaxfold if float_gt(ncmaxf, self.params.ncmaxfyoung): ncmaxf = self.params.ncmaxfyoung extras = 0.0 if self.state.lai > 0.0: if float_gt(self.state.shootn, (self.state.shoot * ncmaxf)): extras = self.state.shootn - self.state.shoot * ncmaxf # Ensure N uptake cannot be reduced below zero. if float_gt(extras, self.fluxes.nuptake): extras = self.fluxes.nuptake self.state.shootn -= extras self.fluxes.nuptake -= extras # if root N:C ratio exceeds its max, then nitrogen uptake is cut # back. n.b. new ring n/c max is already set because it is related # to leaf n:c ncmaxr = ncmaxf * self.params.ncrfac # max root n:c extrar = 0.0 if float_gt(self.state.rootn, (self.state.root * ncmaxr)): extrar = self.state.rootn - self.state.root * ncmaxr # Ensure N uptake cannot be reduced below zero. if float_gt((extras + extrar), self.fluxes.nuptake): extrar = self.fluxes.nuptake - extras self.state.rootn -= extrar self.fluxes.nuptake -= extrar
def calc_carbon_allocation_fracs(self, nitfac, yr_index, project_day): """Carbon allocation fractions to move photosynthate through the plant. Parameters: ----------- nitfac : float leaf N:C as a fraction of 'Ncmaxfyoung' (max 1.0) Returns: -------- alleaf : float allocation fraction for shoot alroot : float allocation fraction for fine roots albranch : float allocation fraction for branches alstem : float allocation fraction for stem References: ----------- Corbeels, M. et al (2005) Ecological Modelling, 187, 449-474. McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. """ if self.control.alloc_model == "FIXED": self.state.alleaf = ( self.params.c_alloc_fmax + nitfac * (self.params.c_alloc_fmax - self.params.c_alloc_fmin)) self.state.alroot = ( self.params.c_alloc_rmax + nitfac * (self.params.c_alloc_rmax - self.params.c_alloc_rmin)) self.state.albranch = ( self.params.c_alloc_bmax + nitfac * (self.params.c_alloc_bmax - self.params.c_alloc_bmin)) # allocate remainder to stem self.state.alstem = (1.0 - self.state.alleaf - self.state.alroot - self.state.albranch) #print self.state.alleaf, self.state.alstem, self.state.albranch, self.state.alroot elif self.control.alloc_model == "GRASSES": # calculate the N limitation based on available canopy N # this logic appears counter intuitive, but it works out when # applied with the perhaps backwards logic below nf = self.state.shootnc # case - completely limited by N availability if nf < self.params.nf_min: nlim = 0.0 elif nf < self.params.nf_crit: nlim = ((nf - self.params.nf_min) / (self.params.nf_crit - self.params.nf_min)) # case - no N limitation else: nlim = 1.0 # no constraint on water uptake via root mass, so makes no sense #limitation = self.sma(min(nlim, self.state.wtfac_root)) # to increase allocation if water stressed. # dependent on lifespan of the roots... limitation = self.sma(nlim) self.state.prev_sma = limitation # figure out root allocation given available water & nutrients # hyperbola shape to allocation self.state.alroot = ( self.params.c_alloc_rmax * self.params.c_alloc_rmin / (self.params.c_alloc_rmin + (self.params.c_alloc_rmax - self.params.c_alloc_rmin) * limitation)) self.state.alstem = 0.0 self.state.albranch = 0.0 self.state.alleaf = (1.0 - self.state.alroot) #print nlim, limitation, self.state.alleaf, self.state.alroot elif self.control.alloc_model == "ALLOMETRIC": # calculate the N limitation based on available canopy N # this logic appears counter intuitive, but it works out when # applied with the perhaps backwards logic below nf = self.state.shootnc # case - completely limited by N availability if nf < self.params.nf_min: nlim = 0.0 elif nf < self.params.nf_crit: nlim = ((nf - self.params.nf_min) / (self.params.nf_crit - self.params.nf_min)) # case - no N limitation else: nlim = 1.0 # no constraint on water uptake via root mass, so makes no sense #limitation = self.sma(min(nlim, self.state.wtfac_root)) # to increase allocation if water stressed. # dependent on lifespan of the roots... limitation = self.sma(nlim) self.state.prev_sma = limitation # figure out root allocation given available water & nutrients # hyperbola shape to allocation self.state.alroot = ( self.params.c_alloc_rmax * self.params.c_alloc_rmin / (self.params.c_alloc_rmin + (self.params.c_alloc_rmax - self.params.c_alloc_rmin) * limitation)) #self.state.alroot = (self.params.c_alloc_rmin + # (self.params.c_alloc_rmax - # self.params.c_alloc_rmin) * limitation) # Calculate tree height: allometric reln using the power function # (Causton, 1985) self.state.canht = (self.params.heighto * self.state.stem**self.params.htpower) # LAI to stem sapwood cross-sectional area (As m-2 m-2) # (dimensionless) # Assume it varies between LS0 and LS1 as a linear function of tree # height (m) sap_cross_sec_area = ( ((self.state.sapwood * const.TONNES_AS_KG * const.M2_AS_HA) / self.params.cfracts) / self.state.canht / self.params.density) leaf2sap = self.state.lai / sap_cross_sec_area # Allocation to leaves dependant on height. Modification of pipe # theory, leaf-to-sapwood ratio is not constant above a certain # height, due to hydraulic constraints (Magnani et al 2000; Deckmyn # et al. 2006). if self.params.leafsap0 < self.params.leafsap1: min_target = self.params.leafsap0 else: min_target = self.params.leafsap1 if self.params.leafsap0 > self.params.leafsap1: max_target = self.params.leafsap0 else: max_target = self.params.leafsap1 leaf2sa_target = (self.params.leafsap0 + (self.params.leafsap1 - self.params.leafsap0) * (self.state.canht - self.params.height0) / (self.params.height1 - self.params.height0)) leaf2sa_target = clip(leaf2sa_target, min=min_target, max=max_target) self.state.alleaf = self.alloc_goal_seek(leaf2sap, leaf2sa_target, self.params.c_alloc_fmax, self.params.targ_sens) # Allocation to branch dependent on relationship between the stem # and branch target_branch = (self.params.branch0 * self.state.stem**self.params.branch1) self.state.albranch = self.alloc_goal_seek( self.state.branch, target_branch, self.params.c_alloc_bmax, self.params.targ_sens) #target_coarse_roots = 0.34 * self.state.stem**0.84 #self.state.alcroot = self.alloc_goal_seek(self.state.croot, # target_coarse_roots, # self.params.c_alloc_crmax, # self.params.targ_sens) # allocation to stem is the residual self.state.alstem = (1.0 - self.state.alroot - self.state.albranch - self.state.alleaf) #print self.state.alleaf, self.state.albranch, self.state.alstem, self.state.alroot else: raise AttributeError('Unknown C allocation model') # Total allocation should be one, if not print warning: total_alloc = (self.state.alroot + self.state.alleaf + self.state.albranch + self.state.alstem) if float_gt(total_alloc, 1.0): raise RuntimeError, "Allocation fracs > 1"
def nitrogen_allocation(self, ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr, project_day): """ Nitrogen distribution - allocate available N through system. N is first allocated to the woody component, surplus N is then allocated to the shoot and roots with flexible ratios. References: ----------- McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. Parameters: ----------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem fdecay : float foliage decay rate rdecay : float fine root decay rate """ # N retranslocated proportion from dying plant tissue and stored within # the plant self.fluxes.retrans = self.nitrogen_retrans(fdecay, rdecay, doy) self.fluxes.nuptake = self.calculate_nuptake(project_day) # Ross's Root Model. if self.control.model_optroot == True: # convert t ha-1 day-1 to gN m-2 year-1 nsupply = (self.calculate_nuptake() * const.TONNES_HA_2_G_M2 * const.DAYS_IN_YRS) # covnert t ha-1 to kg DM m-2 rtot = (self.state.root * const.TONNES_HA_2_KG_M2 / self.params.cfracts) self.fluxes.nuptake_old = self.fluxes.nuptake (self.state.root_depth, self.fluxes.nuptake, self.fluxes.rabove) = self.rm.main(rtot, nsupply, depth_guess=1.0) #umax = self.rm.calc_umax(self.fluxes.nuptake) #print umax # covert nuptake from gN m-2 year-1 to t ha-1 day-1 self.fluxes.nuptake = (self.fluxes.nuptake * const.G_M2_2_TONNES_HA * const.YRS_IN_DAYS) # covert from kg DM N m-2 to t ha-1 self.fluxes.deadroots = (self.params.rdecay * self.fluxes.rabove * self.params.cfracts * const.KG_M2_2_TONNES_HA) self.fluxes.deadrootn = (self.state.rootnc * (1.0 - self.params.rretrans) * self.fluxes.deadroots) # Mineralised nitrogen lost from the system by volatilisation/leaching self.fluxes.nloss = self.params.rateloss * self.state.inorgn # total nitrogen to allocate ntot = self.fluxes.nuptake + self.fluxes.retrans if self.control.deciduous_model: # allocate N to pools with fixed N:C ratios # N flux into new ring (immobile component -> structrual components) self.fluxes.npstemimm = (self.fluxes.wnimrate * self.state.growing_days[doy]) # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = (self.fluxes.wnmobrate * self.state.growing_days[doy]) self.fluxes.nproot = self.state.n_to_alloc_root / days_in_yr self.fluxes.npleaf = (self.fluxes.lnrate * self.state.growing_days[doy]) self.fluxes.npbranch = (self.fluxes.bnrate * self.state.growing_days[doy]) else: # allocate N to pools with fixed N:C ratios # N flux into new ring (immobile component -> structural components) self.fluxes.npstemimm = self.fluxes.npp * self.state.alstem * ncwimm # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = (self.fluxes.npp * self.state.alstem * (ncwnew - ncwimm)) self.fluxes.npbranch = (self.fluxes.npp * self.state.albranch * ncbnew) # If we have allocated more N than we have available # - cut back N prodn arg = (self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npbranch) if float_gt(arg, ntot) and self.control.fixleafnc == False: self.fluxes.npp *= ( ntot / (self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npbranch)) # need to adjust growth values accordingly as well self.fluxes.cpleaf = self.fluxes.npp * self.state.alleaf self.fluxes.cproot = self.fluxes.npp * self.state.alroot self.fluxes.cpbranch = self.fluxes.npp * self.state.albranch self.fluxes.cpstem = self.fluxes.npp * self.state.alstem self.fluxes.npbranch = (self.fluxes.npp * self.state.albranch * ncbnew) self.fluxes.npstemimm = (self.fluxes.npp * self.state.alstem * ncwimm) self.fluxes.npstemmob = (self.fluxes.npp * self.state.alstem * (ncwnew - ncwimm)) ntot -= (self.fluxes.npbranch + self.fluxes.npstemimm + self.fluxes.npstemmob) # allocate remaining N to flexible-ratio pools self.fluxes.npleaf = ( ntot * self.state.alleaf / (self.state.alleaf + self.state.alroot * self.params.ncrfac)) self.fluxes.nproot = ntot - self.fluxes.npleaf
def update_plant_state(self, fdecay, rdecay, project_day, doy): """ Daily change in C content Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate """ # # Carbon pools # self.state.shoot += (self.fluxes.cpleaf - self.fluxes.deadleaves - self.fluxes.ceaten) self.state.root += self.fluxes.cproot - self.fluxes.deadroots self.state.branch += self.fluxes.cpbranch - self.fluxes.deadbranch self.state.stem += self.fluxes.cpstem - self.fluxes.deadstems # annoying but can't see an easier way with the code as it is. # If we are modelling grases, i.e. no stem them without this # the sapwood will end up being reduced to a silly number as # deadsapwood will keep being removed from the pool, even though there # is no wood. if self.state.stem <= 0.01: self.state.sapwood = 0.01 else: self.state.sapwood += self.fluxes.cpstem - self.fluxes.deadsapwood # # Nitrogen pools # if self.control.deciduous_model: self.state.shootn += ( self.fluxes.npleaf - (self.fluxes.lnrate * self.state.remaining_days[doy]) - self.fluxes.neaten) else: self.state.shootn += (self.fluxes.npleaf - fdecay * self.state.shootn - self.fluxes.neaten) self.state.branchn += (self.fluxes.npbranch - self.params.bdecay * self.state.branchn) self.state.rootn += self.fluxes.nproot - rdecay * self.state.rootn self.state.stemnimm += (self.fluxes.npstemimm - self.params.wdecay * self.state.stemnimm) self.state.stemnmob += (self.fluxes.npstemmob - self.params.wdecay * self.state.stemnmob - self.params.retransmob * self.state.stemnmob) #print self.state.stemnmob, self.fluxes.npstemmob - self.params.wdecay * self.state.stemnmob, self.fluxes.npstemmob, self.params.wdecay * self.state.stemnmob self.state.stemn = self.state.stemnimm + self.state.stemnmob if self.control.deciduous_model: self.calculate_cn_store() #============================ # Enforce maximum N:C ratios. # =========================== # This doesn't make sense for the deciduous model because of the ramp # function. The way the deciduous logic works we now before we start # how much N we have to allocate so it is impossible to allocate in # excess. Therefore this is only relevant for evergreen model. if not self.control.deciduous_model: # If foliage or root N/C exceeds its max, then N uptake is cut back # maximum leaf n:c ratio is function of stand age # - switch off age effect by setting ncmaxfyoung = ncmaxfold age_effect = ((self.state.age - self.params.ageyoung) / (self.params.ageold - self.params.ageyoung)) ncmaxf = ( self.params.ncmaxfyoung - (self.params.ncmaxfyoung - self.params.ncmaxfold) * age_effect) if float_lt(ncmaxf, self.params.ncmaxfold): ncmaxf = self.params.ncmaxfold if float_gt(ncmaxf, self.params.ncmaxfyoung): ncmaxf = self.params.ncmaxfyoung extras = 0.0 if self.state.lai > 0.0: if float_gt(self.state.shootn, (self.state.shoot * ncmaxf)): extras = self.state.shootn - self.state.shoot * ncmaxf # Ensure N uptake cannot be reduced below zero. if float_gt(extras, self.fluxes.nuptake): extras = self.fluxes.nuptake self.state.shootn -= extras self.fluxes.nuptake -= extras # if root N:C ratio exceeds its max, then nitrogen uptake is cut # back. n.b. new ring n/c max is already set because it is related # to leaf n:c ncmaxr = ncmaxf * self.params.ncrfac # max root n:c extrar = 0.0 if float_gt(self.state.rootn, (self.state.root * ncmaxr)): extrar = self.state.rootn - self.state.root * ncmaxr # Ensure N uptake cannot be reduced below zero. if float_gt((extras + extrar), self.fluxes.nuptake): extrar = self.fluxes.nuptake - extras self.state.rootn -= extrar self.fluxes.nuptake -= extrar
def nitrogen_allocation(self, ncbnew, nccnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr, project_day): """ Nitrogen distribution - allocate available N through system. N is first allocated to the woody component, surplus N is then allocated to the shoot and roots with flexible ratios. References: ----------- McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. Parameters: ----------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem fdecay : float foliage decay rate rdecay : float fine root decay rate """ # default is we don't need to recalculate the water balance, # however if we cut back on NPP due to available N below then we do # need to do this recalc_wb = False # N retranslocated proportion from dying plant tissue and stored within # the plant self.fluxes.retrans = self.nitrogen_retrans(fdecay, rdecay, doy) self.fluxes.nuptake = self.calculate_nuptake(project_day) # Ross's Root Model. if self.control.model_optroot == True: # convert t ha-1 day-1 to gN m-2 year-1 nsupply = (self.calculate_nuptake() * const.TONNES_HA_2_G_M2 * const.DAYS_IN_YRS) # covnert t ha-1 to kg DM m-2 rtot = (self.state.root * const.TONNES_HA_2_KG_M2 / self.params.cfracts) self.fluxes.nuptake_old = self.fluxes.nuptake (self.state.root_depth, self.fluxes.nuptake, self.fluxes.rabove) = self.rm.main(rtot, nsupply, depth_guess=1.0) #umax = self.rm.calc_umax(self.fluxes.nuptake) #print umax # covert nuptake from gN m-2 year-1 to t ha-1 day-1 self.fluxes.nuptake = (self.fluxes.nuptake * const.G_M2_2_TONNES_HA * const.YRS_IN_DAYS) # covert from kg DM N m-2 to t ha-1 self.fluxes.deadroots = (self.params.rdecay * self.fluxes.rabove * self.params.cfracts * const.KG_M2_2_TONNES_HA) self.fluxes.deadrootn = (self.state.rootnc * (1.0 - self.params.rretrans) * self.fluxes.deadroots) # Mineralised nitrogen lost from the system by volatilisation/leaching self.fluxes.nloss = self.params.rateloss * self.state.inorgn # total nitrogen to allocate ntot = max(0.0, self.fluxes.nuptake + self.fluxes.retrans) if self.control.deciduous_model: # allocate N to pools with fixed N:C ratios # N flux into new ring (immobile component -> structrual components) self.fluxes.npstemimm = (self.fluxes.wnimrate * self.state.growing_days[doy]) # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = (self.fluxes.wnmobrate * self.state.growing_days[doy]) self.fluxes.nproot = self.state.n_to_alloc_root / days_in_yr self.fluxes.npcroot = (self.fluxes.cnrate * self.state.growing_days[doy]) self.fluxes.npleaf = (self.fluxes.lnrate * self.state.growing_days[doy]) self.fluxes.npbranch = (self.fluxes.bnrate * self.state.growing_days[doy]) else: # allocate N to pools with fixed N:C ratios # N flux into new ring (immobile component -> structural components) self.fluxes.npstemimm = self.fluxes.npp * self.fluxes.alstem * ncwimm # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = (self.fluxes.npp * self.fluxes.alstem * (ncwnew - ncwimm)) self.fluxes.npbranch = (self.fluxes.npp * self.fluxes.albranch * ncbnew) self.fluxes.npcroot = (self.fluxes.npp * self.fluxes.alcroot * nccnew) # If we have allocated more N than we have available # - cut back N prodn arg = (self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npbranch + self.fluxes.npcroot) if float_gt(arg, ntot) and self.control.fixleafnc == False: self.fluxes.npp *= (ntot / (self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npbranch )) # need to adjust growth values accordingly as well self.fluxes.cpleaf = self.fluxes.npp * self.fluxes.alleaf self.fluxes.cproot = self.fluxes.npp * self.fluxes.alroot self.fluxes.cpcroot = self.fluxes.npp * self.fluxes.alcroot self.fluxes.cpbranch = self.fluxes.npp * self.fluxes.albranch self.fluxes.cpstem = self.fluxes.npp * self.fluxes.alstem self.fluxes.npbranch = (self.fluxes.npp * self.fluxes.albranch * ncbnew) self.fluxes.npstemimm = (self.fluxes.npp * self.fluxes.alstem * ncwimm) self.fluxes.npstemmob = (self.fluxes.npp * self.fluxes.alstem * (ncwnew - ncwimm)) self.fluxes.npcroot = (self.fluxes.npp * self.fluxes.alcroot * nccnew) # Also need to recalculate GPP and thus Ra and return a flag # so that we know to recalculate the water balance. self.fluxes.gpp = self.fluxes.npp / self.params.cue conv = const.G_AS_TONNES / const.M2_AS_HA self.fluxes.gpp_gCm2 = self.fluxes.gpp / conv self.fluxes.gpp_am_pm[0] = self.fluxes.gpp_gCm2 / 2.0 self.fluxes.gpp_am_pm[1] = self.fluxes.gpp_gCm2 / 2.0 # New respiration flux self.fluxes.auto_resp = self.fluxes.gpp - self.fluxes.npp recalc_wb = True ntot -= (self.fluxes.npbranch + self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npcroot) ntot = max(0.0, ntot) # allocate remaining N to flexible-ratio pools self.fluxes.npleaf = (ntot * self.fluxes.alleaf / (self.fluxes.alleaf + self.fluxes.alroot * self.params.ncrfac)) self.fluxes.nproot = ntot - self.fluxes.npleaf return recalc_wb
def calculate_npools(self): """ Calculate new soil N pools. """ # net source fluxes. nstsu = self.fluxes.nresid[0] # s surf nstsl = self.fluxes.nresid[1] # s soil nmtsu = self.fluxes.nresid[2] # m surf nmtsl = self.fluxes.nresid[3] # m soil nact = (self.fluxes.nstruct[1] + self.fluxes.nstruct[3] + self.fluxes.nmetab[0] + self.fluxes.nmetab[1] + self.fluxes.nslow[0] + self.fluxes.npassive) nslo = (self.fluxes.nstruct[0] + self.fluxes.nstruct[2] + self.fluxes.nactive[0]) npas = self.fluxes.nactive[1] + self.fluxes.nslow[1] # net effluxes. lstsu = (self.fluxes.nstruct[0] + self.fluxes.nstruct[1]) # s surf lstsl = (self.fluxes.nstruct[2] + self.fluxes.nstruct[3]) # s soil lmtsu = self.fluxes.nmetab[0] # m surf lmtsl = self.fluxes.nmetab[1] # m soil lact = (self.fluxes.nactive[0] + self.fluxes.nactive[1]) lslo = (self.fluxes.nslow[0] + self.fluxes.nslow[1]) lpas = self.fluxes.npassive # net N release implied by separation of litter into structural # & metabolic. The following pools only fix or release N at their # limiting n:c values. # N released or fixed from the N inorganic pool is incremented with # each call to nclimit and stored in self.fluxes.nlittrelease self.fluxes.nlittrelease = 0.0 self.state.structsurfn += nstsu - lstsu if not self.control.strfloat: self.state.structsurfn += self.nclimit(self.state.structsurf, self.state.structsurfn, 1.0/self.params.structcn, 1.0/self.params.structcn) self.state.structsoiln += nstsl - lstsl if not self.control.strfloat: self.state.structsoiln += self.nclimit(self.state.structsoil, self.state.structsoiln, 1.0/self.params.structcn, 1.0/self.params.structcn) self.state.metabsurfn += nmtsu - lmtsu self.state.metabsurfn += self.nclimit(self.state.metabsurf, self.state.metabsurfn, 1.0/25.0, 1.0/10.0) self.state.metabsoiln += nmtsl - lmtsl self.state.metabsoiln += self.nclimit(self.state.metabsoil, self.state.metabsoiln, 1.0/25.0, 1.0/10.0) # When nothing is being added to the metabolic pools, there is the # potential scenario with the way the model works for tiny bits to be # removed with each timestep. Effectively with time this value which is # zero can end up becoming zero but to a silly decimal place self.precision_control() # N:C of the SOM pools increases linearly btw prescribed min and max # values as the Nconc of the soil increases. arg = (self.state.inorgn - self.params.nmin0 / const.M2_AS_HA * const.G_AS_TONNES) # active actnc = self.params.actncmin + self.state.actncslope * arg if float_gt(actnc, self.params.actncmax): actnc = self.params.actncmax fixn = ncflux(self.fluxes.cact, nact, actnc) self.state.activesoiln += nact + fixn - lact # slow slownc = self.params.slowncmin + self.state.slowncslope * arg if float_gt(slownc, self.params.slowncmax): slownc = self.params.slowncmax fixn = ncflux(self.fluxes.cslo, nslo, slownc) self.state.slowsoiln += nslo + fixn - lslo # passive passnc = self.params.passncmin + self.state.passncslope * arg if float_gt(passnc, self.params.passncmax): passnc = self.params.passncmax fixn = ncflux(self.fluxes.cpas, npas, passnc) # update passive pool only if passiveconst=0 self.state.passivesoiln += npas + fixn - lpas # Daily increment of soil inorganic N pool, diff btw in and effluxes # (grazer urine n goes directly into inorganic pool) nb inorgn may be # unstable if rateuptake is large self.state.inorgn += ((self.fluxes.ngross + self.fluxes.ninflow + self.fluxes.nurine - self.fluxes.nimmob - self.fluxes.nloss - self.fluxes.nuptake) + self.fluxes.nlittrelease)
def calculate_npools(self, active_nc_slope, slow_nc_slope, passive_nc_slope): """ Update N pools in the soil Parameters ---------- active_nc_slope : float active NC slope slow_nc_slope: float slow NC slope passive_nc_slope : float passive NC slope """ # net N release implied by separation of litter into structural # & metabolic. The following pools only fix or release N at their # limiting n:c values. # N released or fixed from the N inorganic pool is incremented with # each call to nc_limit and stored in self.fluxes.nlittrelease self.fluxes.nlittrelease = 0.0 self.state.structsurfn += (self.fluxes.n_surf_struct_litter - (self.fluxes.n_surf_struct_to_slow + self.fluxes.n_surf_struct_to_active)) if not self.control.strfloat: self.state.structsurfn += self.nc_limit(self.state.structsurf, self.state.structsurfn, 1.0 / self.params.structcn, 1.0 / self.params.structcn) self.state.structsoiln += (self.fluxes.n_soil_struct_litter - (self.fluxes.n_soil_struct_to_slow + self.fluxes.n_soil_struct_to_active)) if not self.control.strfloat: self.state.structsoiln += self.nc_limit(self.state.structsoil, self.state.structsoiln, 1.0 / self.params.structcn, 1.0 / self.params.structcn) self.state.metabsurfn += (self.fluxes.n_surf_metab_litter - self.fluxes.n_surf_metab_to_active) self.state.metabsurfn += self.nc_limit(self.state.metabsurf, self.state.metabsurfn, 1.0 / 25.0, 1.0 / 10.0) self.state.metabsoiln += (self.fluxes.n_soil_metab_litter - self.fluxes.n_soil_metab_to_active) self.state.metabsoiln += self.nc_limit(self.state.metabsoil, self.state.metabsoiln, 1.0 / 25.0, 1.0 / 10.0) # When nothing is being added to the metabolic pools, there is the # potential scenario with the way the model works for tiny bits to be # removed with each timestep. Effectively with time this value which is # zero can end up becoming zero but to a silly decimal place self.precision_control() # Update SOM pools n_into_active = (self.fluxes.n_surf_struct_to_active + self.fluxes.n_soil_struct_to_active + self.fluxes.n_surf_metab_to_active + self.fluxes.n_soil_metab_to_active + self.fluxes.n_slow_to_active + self.fluxes.n_passive_to_active) n_out_of_active = (self.fluxes.n_active_to_slow + self.fluxes.n_active_to_passive) n_into_slow = (self.fluxes.n_surf_struct_to_slow + self.fluxes.n_soil_struct_to_slow + self.fluxes.n_active_to_slow) n_out_of_slow = (self.fluxes.n_slow_to_active + self.fluxes.n_slow_to_passive) n_into_passive = (self.fluxes.n_active_to_passive + self.fluxes.n_slow_to_passive) n_out_of_passive = self.fluxes.n_passive_to_active # N:C of the SOM pools increases linearly btw prescribed min and max # values as the Nconc of the soil increases. arg = (self.state.inorgn - self.params.nmin0 / const.M2_AS_HA * const.G_AS_TONNES) # active active_nc = self.params.actncmin + active_nc_slope * arg if float_gt(active_nc, self.params.actncmax): active_nc = self.params.actncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_active, n_into_active, active_nc) self.state.activesoiln += n_into_active + fixn - n_out_of_active # slow slow_nc = self.params.slowncmin + slow_nc_slope * arg if float_gt(slow_nc, self.params.slowncmax): slow_nc = self.params.slowncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_slow, n_into_slow, slow_nc) self.state.slowsoiln += n_into_slow + fixn - n_out_of_slow # passive, update passive pool only if passiveconst=0 pass_nc = self.params.passncmin + passive_nc_slope * arg if float_gt(pass_nc, self.params.passncmax): pass_nc = self.params.passncmax # release N to Inorganic pool or fix N from the Inorganic pool in order # to normalise the N:C ratio of a net flux fixn = self.nc_flux(self.fluxes.c_into_passive, n_into_passive, pass_nc) self.state.passivesoiln += n_into_passive + fixn - n_out_of_passive # Daily increment of soil inorganic N pool, diff btw in and effluxes # (grazer urine n goes directly into inorganic pool) nb inorgn may be # unstable if rateuptake is large self.state.inorgn += ( (self.fluxes.ngross + self.fluxes.ninflow + self.fluxes.nurine - self.fluxes.nimmob - self.fluxes.nloss - self.fluxes.nuptake) + self.fluxes.nlittrelease)