def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) if self.control.ps_pathway == "C3": self.mt = MateC3(self.control, self.params, self.state, self.fluxes, self.met_data) else: self.mt = MateC4(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.sm.initialise_parameters() self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.topsoil_depth * const.MM_TO_M) # Window size = root lifespan in days... self.window_size = (int(1.0 / (self.params.rdecay * const.NDAYS_IN_YR) * const.NDAYS_IN_YR)) #self.window_size = 365 # If we don't have any information about the N&water limitation, i.e. # as would be the case with spin-up, assume that there is no limitation # to begin with. if self.state.prev_sma is None: self.state.prev_sma = 1.0 self.sma = MovingAverageFilter(self.window_size, self.state.prev_sma)
def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) self.mt = Mate(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.top_soil_depth)
def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) if self.control.ps_pathway == "C3": self.mt = MateC3(self.control, self.params, self.state, self.fluxes, self.met_data) else: self.mt = MateC4(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.sm.initialise_parameters() self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.topsoil_depth*const.MM_TO_M) # Window size = root lifespan in days... self.window_size = (int(1.0 / (self.params.rdecay * const.NDAYS_IN_YR)* const.NDAYS_IN_YR)) #self.window_size = 365 # If we don't have any information about the N&water limitation, i.e. # as would be the case with spin-up, assume that there is no limitation # to begin with. if self.state.prev_sma is None: self.state.prev_sma = 1.0 if self.state.grw_seas_stress is None: self.state.grw_seas_stress = 1.0 self.sma = SimpleMovingAverage(self.window_size, self.state.prev_sma)
class PlantGrowth(object): """ G'DAY plant growth module. Calls photosynthesis model, water balance and evolve plant state. Pools recieve C through allocation of accumulated photosynthate and N from both soil uptake and retranslocation within the plant. Key feedback through soil N mineralisation and plant N uptake * Note met_forcing is an object with radiation, temp, precip data, etc. """ def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) if self.control.ps_pathway == "C3": self.mt = MateC3(self.control, self.params, self.state, self.fluxes, self.met_data) else: self.mt = MateC4(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.sm.initialise_parameters() self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.topsoil_depth*const.MM_TO_M) # Window size = root lifespan in days... self.window_size = (int(1.0 / (self.params.rdecay * const.NDAYS_IN_YR)* const.NDAYS_IN_YR)) #self.window_size = 365 # If we don't have any information about the N&water limitation, i.e. # as would be the case with spin-up, assume that there is no limitation # to begin with. if self.state.prev_sma is None: self.state.prev_sma = 1.0 if self.state.grw_seas_stress is None: self.state.grw_seas_stress = 1.0 self.sma = SimpleMovingAverage(self.window_size, self.state.prev_sma) def calc_day_growth(self, project_day, fdecay, rdecay, daylen, doy, days_in_yr, yr_index): """Evolve plant state, photosynthesis, distribute N and C" Parameters: ----------- project_day : integer simulation day fdecay : float foliage decay rate rdecay : float fine root decay rate """ # if grazing took place need to reset "stress" running mean calculation # for grasses if self.control.grazing == 2 and self.params.disturbance_doy == doy: self.sma.reset_stream() # calculate NPP self.carbon_production(project_day, daylen) # calculate water balance. We also need to store the previous days # soil water store previous_topsoil_store = self.state.pawater_topsoil previous_rootzone_store = self.state.pawater_root self.wb.calculate_water_balance(project_day, daylen) # leaf N:C as a fraction of Ncmaxyoung, i.e. the max N:C ratio of # foliage in young stand nitfac = min(1.0, self.state.shootnc / self.params.ncmaxfyoung) # figure out the C allocation fractions if not self.control.deciduous_model: # daily allocation... self.calc_carbon_allocation_fracs(nitfac) else: # Allocation is annually for deciduous "tree" model, but we need to # keep a check on stresses during the growing season and the LAI # figure out limitations during leaf growth period. This also # applies for deciduous grasses, need to do the growth stress # calc for grasses here too. if self.state.leaf_out_days[doy] > 0.0: self.calculate_growth_stress_limitation() # Need to save max lai for pipe model because at the end of the # year LAI=0.0 if self.state.lai > self.state.max_lai: self.state.max_lai = self.state.lai # Distribute new C and N through the system self.carbon_allocation(nitfac, doy, days_in_yr) (ncbnew, nccnew, ncwimm, ncwnew) = self.calculate_ncwood_ratios(nitfac) recalc_wb = self.nitrogen_allocation(ncbnew, nccnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr, project_day) # If we didn't have enough N available to satisfy wood demand, NPP # is down-regulated and thus so is GPP. We also need to recalculate the # water balance given the lower GPP. if recalc_wb: self.state.pawater_topsoil = previous_topsoil_store self.state.pawater_root = previous_rootzone_store self.wb.calculate_water_balance(project_day, daylen) self.update_plant_state(fdecay, rdecay, project_day, doy) #if self.control.deciduous_model: self.precision_control() def calculate_ncwood_ratios(self, nitfac): """ Estimate the N:C ratio in the branch and stem. Option to vary the N:C ratio of the stem following Jeffreys (1999) or keep it a fixed fraction Parameters: ----------- nitfac : float leaf N:C as a fraction of the max N:C ratio of foliage in young stand Returns: -------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem References: ---------- * Jeffreys, M. P. (1999) Dynamics of stemwood nitrogen in Pinus radiata with modelled implications for forest productivity under elevated atmospheric carbon dioxide. PhD. """ # n:c ratio of new branch wood ncbnew = (self.params.ncbnew + nitfac * (self.params.ncbnew - self.params.ncbnewz)) # n:c ratio of new coarse root nccnew = (self.params.nccnew + nitfac * (self.params.nccnew - self.params.nccnewz)) # fixed N:C in the stemwood if self.control.fixed_stem_nc == 1: # n:c ratio of stemwood - immobile pool and new ring ncwimm = (self.params.ncwimm + nitfac * (self.params.ncwimm - self.params.ncwimmz)) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = (self.params.ncwnew + nitfac * (self.params.ncwnew - self.params.ncwnewz)) # vary stem N:C based on reln with foliage, see Jeffreys. Jeffreys 1999 # showed that N:C ratio of new wood increases with foliar N:C ratio, # modelled here based on evidence as a linear function. else: ncwimm = max(0.0, (0.0282 * self.state.shootnc + 0.000234) * self.params.fhw) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = max(0.0, 0.162 * self.state.shootnc - 0.00143) return (ncbnew, nccnew, ncwimm, ncwnew) def carbon_production(self, project_day, daylen): """ Calculate GPP, NPP and plant respiration Parameters: ----------- project_day : integer simulation day daylen : float daytime length (hrs) References: ----------- * Jackson, J. E. and Palmer, J. W. (1981) Annals of Botany, 47, 561-565. """ if self.state.lai > 0.0: # average leaf nitrogen content (g N m-2 leaf) leafn = (self.state.shootnc * self.params.cfracts / self.params.sla * const.KG_AS_G) # total nitrogen content of the canopy self.state.ncontent = leafn * self.state.lai else: self.state.ncontent = 0.0 # When canopy is not closed, canopy light interception is reduced cf = min(1.0, self.state.lai / self.params.lai_cover) # fIPAR - the fraction of intercepted PAR = IPAR/PAR incident at the # top of the canopy, accounting for partial closure based on Jackson # and Palmer (1981), derived from beer's law if self.state.lai > 0.0: self.state.fipar = ((1.0 - exp(-self.params.kext * self.state.lai / cf)) * cf) else: self.state.fipar = 0.0 # Canopy extinction coefficient if the canopy is open #if cf < 1.0: # kext = -log(1.0 - self.state.fipar) / LAI if self.control.water_stress: # Calculate the soil moisture availability factors [0,1] in the # topsoil and the entire root zone (self.state.wtfac_topsoil, self.state.wtfac_root) = self.sm.calculate_soil_water_fac() else: # really this should only be a debugging option! self.state.wtfac_tsoil = 1.0 self.state.wtfac_root = 1.0 # Estimate photosynthesis if self.control.assim_model == "BEWDY": self.bw.calculate_photosynthesis(frac_gcover, project_day, daylen) elif self.control.assim_model == "MATE": self.mt.calculate_photosynthesis(project_day, daylen) else: raise AttributeError('Unknown assimilation model') def calc_carbon_allocation_fracs(self, nitfac): """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.fluxes.alleaf = (self.params.c_alloc_fmax + nitfac * (self.params.c_alloc_fmax - self.params.c_alloc_fmin)) self.fluxes.alroot = (self.params.c_alloc_rmax + nitfac * (self.params.c_alloc_rmax - self.params.c_alloc_rmin)) self.fluxes.albranch = (self.params.c_alloc_bmax + nitfac * (self.params.c_alloc_bmax - self.params.c_alloc_bmin)) # allocate remainder to stem self.fluxes.alstem = (1.0 - self.fluxes.alleaf - self.fluxes.alroot - self.fluxes.albranch) self.fluxes.alcroot = self.params.c_alloc_cmax * self.fluxes.alstem self.fluxes.alstem -= self.fluxes.alcroot elif self.control.alloc_model == "GRASSES": # if combining grasses with the deciduous model this calculation # is done only during the leaf out period. See above. if not self.control.deciduous_model: self.calculate_growth_stress_limitation() # figure out root allocation given available water & nutrients # hyperbola shape to allocation min_root_alloc = 0.4 self.fluxes.alroot = (self.params.c_alloc_rmax * min_root_alloc / (min_root_alloc + (self.params.c_alloc_rmax - min_root_alloc) * self.state.prev_sma)) self.fluxes.alstem = 0.0 self.fluxes.albranch = 0.0 self.fluxes.alcroot = 0.0 self.fluxes.alleaf = (1.0 - self.fluxes.alroot) elif self.control.alloc_model == "ALLOMETRIC": if not self.control.deciduous_model: self.calculate_growth_stress_limitation() else: # reset the buffer at the end of the growing season self.sma.reset_stream() # figure out root allocation given available water & nutrients # hyperbola shape to allocation min_root_alloc = 0.1 self.fluxes.alroot = (self.params.c_alloc_rmax * min_root_alloc / (min_root_alloc + (self.params.c_alloc_rmax - min_root_alloc) * self.state.prev_sma)) #self.fluxes.alroot = (self.params.c_alloc_rmin + # (self.params.c_alloc_rmax - # self.params.c_alloc_rmin) * # self.state.prev_sma) # 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) arg1 = self.state.sapwood * const.TONNES_AS_KG * const.M2_AS_HA arg2 = self.state.canht * self.params.density * self.params.cfracts sap_cross_sec_area = arg1 / arg2 if not self.control.deciduous_model: leaf2sap = self.state.lai / sap_cross_sec_area else: leaf2sap = self.state.max_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 float_le(self.state.canht, self.params.height0): leaf2sa_target = self.params.leafsap0 elif float_ge(self.state.canht, self.params.height1): leaf2sa_target = self.params.leafsap1 else: arg1 = self.params.leafsap0 arg2 = self.params.leafsap1 - self.params.leafsap0 arg3 = self.state.canht - self.params.height0 arg4 = self.params.height1 - self.params.height0 leaf2sa_target = arg1 + (arg2 * arg3 / arg4) self.fluxes.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.fluxes.albranch = self.alloc_goal_seek(self.state.branch, target_branch, self.params.c_alloc_bmax, self.params.targ_sens) coarse_root_target = (self.params.croot0 * self.state.stem**self.params.croot1) self.fluxes.alcroot = self.alloc_goal_seek(self.state.croot, coarse_root_target, self.params.c_alloc_cmax, self.params.targ_sens) self.fluxes.alstem = (1.0 - self.fluxes.alroot - self.fluxes.albranch - self.fluxes.alleaf - self.fluxes.alcroot) # allocation to stem is the residual #self.fluxes.alstem = (1.0 - self.fluxes.alroot - # self.fluxes.albranch - # self.fluxes.alleaf) #self.fluxes.alcroot = 0.2 * self.fluxes.alstem #self.fluxes.alstem -= self.fluxes.alcroot # Because I have allowed the max fracs sum > 1, possibility # stem frac would be negative. Perhaps the above shouldn't be # allowed...? But this will stop wood allocation in such a # situation. #if self.fluxes.alstem < 0.0: # extra = self.fluxes.alstem # self.fluxes.alstem = 0.0 # self.fluxes.alleaf -= extra # minimum allocation to leaves - without it tree would die, as this # is done annually. if self.control.deciduous_model: if self.fluxes.alleaf < 0.1: min_leaf_alloc = 0.1 self.fluxes.alstem -= min_leaf_alloc self.fluxes.alleaf = min_leaf_alloc else: raise AttributeError('Unknown C allocation model') #print self.fluxes.alleaf, self.fluxes.alstem, self.fluxes.albranch, \ # self.fluxes.alroot, self.state.prev_sma, self.state.canht # Total allocation should be one, if not print warning: total_alloc = (self.fluxes.alroot + self.fluxes.alleaf + self.fluxes.albranch + self.fluxes.alstem + self.fluxes.alcroot) if float_gt(total_alloc, 1.0): raise RuntimeError, "Allocation fracs > 1" def alloc_goal_seek(self, simulated, target, alloc_max, sensitivity): # Sensitivity parameter characterises how allocation fraction respond # when the leaf:sapwood area ratio departs from the target value # If sensitivity close to 0 then the simulated leaf:sapwood area ratio # will closely track the target value frac = 0.5 + 0.5 * (1.0 - simulated / target) / sensitivity return max(0.0, alloc_max * min(1.0, frac)) def calculate_growth_stress_limitation(self): """ Calculate level of stress due to nitrogen or water availability """ # 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 # Limitation by nitrogen and water. Water constraint is implicit, # in that, water stress results in an increase of root mass, # which are assumed to spread horizontally within the rooting zone. # So in effect, building additional root mass doesnt alleviate the # water limitation within the model. However, it does more # accurately reflect an increase in root C production at a water # limited site. This implementation is also consistent with other # approaches, e.g. LPJ. In fact I dont see much evidence for models # that have a flexible bucket depth. current_limitation = min(nlim, self.state.wtfac_root) self.state.prev_sma = self.sma(current_limitation) def allocate_stored_c_and_n(self, init): """ Allocate stored C&N. This is either down as the model is initialised for the first time or at the end of each year. """ # JUST here for FACE stuff as first year of ele should have last years # alloc fracs #if init == True: # self.fluxes.alleaf = 0.26 # self.fluxes.alroot = 0.11 # self.fluxes.albranch = 0.06 # self.fluxes.alstem = 0.57 # ======================== # Carbon - fixed fractions # ======================== self.state.c_to_alloc_shoot = self.fluxes.alleaf * self.state.cstore self.state.c_to_alloc_root = self.fluxes.alroot * self.state.cstore self.state.c_to_alloc_croot = self.fluxes.alcroot * self.state.cstore self.state.c_to_alloc_branch = self.fluxes.albranch * self.state.cstore self.state.c_to_alloc_stem = self.fluxes.alstem * self.state.cstore # ========================================================= # Nitrogen - Fixed ratios N allocation to woody components. # ========================================================= # N flux into new ring (immobile component -> structrual components) self.state.n_to_alloc_stemimm = (self.state.cstore * self.fluxes.alstem * self.params.ncwimm) # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.state.n_to_alloc_stemmob = (self.state.cstore * self.fluxes.alstem * (self.params.ncwnew - self.params.ncwimm)) self.state.n_to_alloc_branch = (self.state.cstore * self.fluxes.albranch * self.params.ncbnew) self.state.n_to_alloc_croot = (self.state.cstore * self.fluxes.alcroot * self.params.nccnew) # Calculate remaining N left to allocate to leaves and roots ntot = max(0.0,(self.state.nstore - self.state.n_to_alloc_stemimm - self.state.n_to_alloc_stemmob - self.state.n_to_alloc_branch)) # allocate remaining N to flexible-ratio pools self.state.n_to_alloc_shoot = (ntot * self.fluxes.alleaf / (self.fluxes.alleaf + self.fluxes.alroot * self.params.ncrfac)) self.state.n_to_alloc_root = ntot - self.state.n_to_alloc_shoot 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 nitrogen_retrans(self, fdecay, rdecay, doy): """ Nitrogen retranslocated from senesced plant matter. Constant rate of n translocated from mobile pool Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate Returns: -------- N retrans : float N retranslocated plant matter """ if self.control.deciduous_model: leafretransn = (self.params.fretrans * self.fluxes.lnrate * self.state.remaining_days[doy]) else: leafretransn = self.params.fretrans * fdecay * self.state.shootn rootretransn = self.params.rretrans * rdecay * self.state.rootn crootretransn = (self.params.cretrans * self.params.crdecay * self.state.crootn) branchretransn = (self.params.bretrans * self.params.bdecay * self.state.branchn) stemretransn = (self.params.wretrans * self.params.wdecay * self.state.stemnmob + self.params.retransmob * self.state.stemnmob) # store for NCEAS output self.fluxes.leafretransn = leafretransn return (leafretransn + rootretransn + crootretransn + branchretransn + stemretransn) def calculate_nuptake(self, project_day): """ N uptake depends on the rate at which soil mineral N is made available to the plants. Returns: -------- nuptake : float N uptake References: ----------- * Dewar and McMurtrie, 1996, Tree Physiology, 16, 161-171. * Raich et al. 1991, Ecological Applications, 1, 399-429. """ if self.control.nuptake_model == 0: # Constant N uptake nuptake = self.params.nuptakez elif self.control.nuptake_model == 1: # evaluate nuptake : proportional to dynamic inorganic N pool nuptake = self.params.rateuptake * self.state.inorgn elif self.control.nuptake_model == 2: # N uptake is a saturating function on root biomass following # Dewar and McMurtrie, 1996. # supply rate of available mineral N U0 = self.params.rateuptake * self.state.inorgn Kr = self.params.kr nuptake = max(U0 * self.state.root / (self.state.root + Kr), 0.0) elif self.control.nuptake_model == 3: # N uptake is a function of available soil N, soil moisture # following a Michaelis-Menten approach # See Raich et al. 1991, pg. 423. vcn = 1.0 / 0.0215 # 46.4 arg1 = (vcn * self.state.shootn) - self.state.shoot arg2 = (vcn * self.state.shootn) + self.state.shoot self.params.ac += self.params.adapt * arg1 / arg2 self.params.ac = max(min(1.0, self.params.ac), 0.0) # soil moisture is assumed to influence nutrient diffusion rate # through the soil, ks [0,1] theta = self.state.pawater_root / self.params.wcapac_root ks = 0.9 * theta**3.0 + 0.1 arg1 = self.params.nmax * ks * self.state.inorgn arg2 = self.params.knl + (ks * self.state.inorgn) arg3 = exp(0.0693 * self.met_data['tair'][project_day]) arg4 = 1.0 - self.params.ac nuptake = (arg1 / arg2) * arg3 * arg4 #print self.params.nmax, self.params.knl, ks, exp(0.0693 * tavg) else: raise AttributeError('Unknown N uptake option') # Stop N uptake if C:N falls below 10 #if self.state.plantnc > 0.1: # nuptake = 0.0 return nuptake def carbon_allocation(self, nitfac, doy, days_in_yr): """ C distribution - allocate available C through system Parameters: ----------- nitfac : float leaf N:C as a fraction of 'Ncmaxfyoung' (max 1.0) """ if self.control.deciduous_model: days_left = self.state.growing_days[doy] self.fluxes.cpleaf = self.fluxes.lrate * days_left self.fluxes.cpbranch = self.fluxes.brate * days_left self.fluxes.cpstem = self.fluxes.wrate * days_left self.fluxes.cproot = self.state.c_to_alloc_root * 1.0 / days_in_yr self.fluxes.cpcroot = self.fluxes.crate * days_left else: 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 # evaluate SLA of new foliage accounting for variation in SLA # with tree and leaf age (Sands and Landsberg, 2002). Assume # SLA of new foliage is linearly related to leaf N:C ratio # via nitfac. Based on date from two E.globulus stands in SW Aus, see # Corbeels et al (2005) Ecological Modelling, 187, 449-474. # (m2 onesided/kg DW) self.params.sla = (self.params.slazero + nitfac * (self.params.slamax - self.params.slazero)) if self.control.deciduous_model: if float_eq(self.state.shoot, 0.0): self.state.lai = 0.0 elif self.state.leaf_out_days[doy] > 0.0: self.state.lai += (self.fluxes.cpleaf * (self.params.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) else: self.state.lai = 0.0 else: # update leaf area [m2 m-2] if float_eq(self.state.shoot, 0.0): self.state.lai = 0.0 else: self.state.lai += (self.fluxes.cpleaf * (self.params.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) def precision_control(self, tolerance=1E-08): """ Detect very low values in state variables and force to zero to avoid rounding and overflow errors """ # C & N state variables if self.state.shoot < tolerance: self.fluxes.deadleaves += self.state.shoot self.fluxes.deadleafn += self.state.shootn self.state.shoot = 0.0 self.state.shootn = 0.0 if self.state.branch < tolerance: self.fluxes.deadbranch += self.state.branch self.fluxes.deadbranchn += self.state.branchn self.state.branch = 0.0 self.state.branchn = 0.0 if self.state.root < tolerance: self.fluxes.deadrootn += self.state.rootn self.fluxes.deadroots += self.state.root self.state.root = 0.0 self.state.rootn = 0.0 if self.state.croot < tolerance: self.fluxes.deadcrootn += self.state.crootn self.fluxes.deadcroots += self.state.croot self.state.croot = 0.0 self.state.crootn = 0.0 # Not setting these to zero as this just leads to errors with desert # regrowth...instead seeding them to a small value with a CN~25. if self.state.stem < tolerance: self.fluxes.deadstems += self.state.stem self.fluxes.deadstemn += self.state.stemn self.state.stem = 0.001 self.state.stemn = 0.00004 self.state.stemnimm = 0.00004 self.state.stemnmob = 0.0 # need separate one as this will become very small if there is no # mobile stem N if self.state.stemnmob < tolerance: self.fluxes.deadstemn += self.state.stemnmob self.state.stemnmob = 0.0 if self.state.stemnimm < tolerance: self.fluxes.deadstemn += self.state.stemnimm self.state.stemnimm = 0.00004 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 calculate_cn_store(self): """ Deciduous trees store carbohydrate during the winter which they then use in the following year to build new leaves (buds & budburst are implied) """ # Total C & N storage to allocate annually. self.state.cstore += self.fluxes.npp self.state.nstore += self.fluxes.nuptake + self.fluxes.retrans self.state.anpp += self.fluxes.npp
class PlantGrowth(object): """ G'DAY plant growth module. Calls photosynthesis model, water balance and evolve plant state. Pools recieve C through allocation of accumulated photosynthate and N from both soil uptake and retranslocation within the plant. Key feedback through soil N mineralisation and plant N uptake * Note met_forcing is an object with radiation, temp, precip data, etc. """ def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) self.mt = Mate(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.top_soil_depth) def calc_day_growth(self, project_day, fdecay, rdecay, daylen, doy, days_in_yr, yr_index): """Evolve plant state, photosynthesis, distribute N and C" Parameters: ----------- project_day : integer simulation day fdecay : float foliage decay rate rdecay : float fine root decay rate """ # calculate NPP self.carbon_production(project_day, daylen) # calculate water balance self.wb.calculate_water_balance(project_day, daylen) # leaf N:C as a fraction of Ncmaxyoung, i.e. the max N:C ratio of # foliage in young stand nitfac = min(1.0, self.state.shootnc / self.params.ncmaxfyoung) # figure out the C allocation fractions self.calc_carbon_allocation_fracs(nitfac, yr_index) # Distribute new C and N through the system self.carbon_allocation(nitfac, doy, days_in_yr) (ncbnew, ncwimm, ncwnew) = self.calculate_ncwood_ratios(nitfac) self.nitrogen_allocation(ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr) self.update_plant_state(fdecay, rdecay, project_day, doy) if self.control.deciduous_model: self.precision_control() def calculate_ncwood_ratios(self, nitfac): """ Estimate the N:C ratio in the branch and stem. Option to vary the N:C ratio of the stem following Jeffreys (1999) or keep it a fixed fraction Parameters: ----------- nitfac : float leaf N:C as a fraction of the max N:C ratio of foliage in young stand Returns: -------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem References: ---------- * Jeffreys, M. P. (1999) Dynamics of stemwood nitrogen in Pinus radiata with modelled implications for forest productivity under elevated atmospheric carbon dioxide. PhD. """ # n:c ratio of new branch wood ncbnew = (self.params.ncbnew + nitfac * (self.params.ncbnew - self.params.ncbnewz)) self.state.branchnc = ncbnew # fixed N:C in the stemwood if self.control.fixed_stem_nc == 1: # n:c ratio of stemwood - immobile pool and new ring ncwimm = (self.params.ncwimm + nitfac * (self.params.ncwimm - self.params.ncwimmz)) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = (self.params.ncwnew + nitfac * (self.params.ncwnew - self.params.ncwnewz)) # vary stem N:C based on reln with foliage, see Jeffreys. Jeffreys 1999 # showed that N:C ratio of new wood increases with foliar N:C ratio, # modelled here based on evidence as a linear function. else: ncwimm = max(0.0, (0.0282 * self.state.shootnc + 0.000234) * self.params.fhw) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = max(0.0, 0.162 * self.state.shootnc - 0.00143) return (ncbnew, ncwimm, ncwnew) def carbon_production(self, project_day, daylen): """ Calculate GPP, NPP and plant respiration Parameters: ----------- project_day : integer simulation day daylen : float daytime length (hrs) References: ----------- * Jackson, J. E. and Palmer, J. W. (1981) Annals of Botany, 47, 561-565. """ if self.state.lai > 0.0: # average leaf nitrogen content (g N m-2 leaf) leafn = (self.state.shootnc * self.params.cfracts / self.state.sla * const.KG_AS_G) # total nitrogen content of the canopy self.state.ncontent = leafn * self.state.lai else: self.state.ncontent = 0.0 # fractional ground cover. if float_lt(self.state.lai, self.params.lai_cover): frac_gcover = self.state.lai / self.params.lai_cover else: frac_gcover = 1.0 # Radiance intercepted by the canopy, accounting for partial closure # Jackson and Palmer (1981), derived from beer's law if self.state.lai > 0.0: self.state.light_interception = ((1.0 - exp(-self.params.kext * self.state.lai / frac_gcover)) * frac_gcover) else: self.state.light_interception = 0.0 if self.control.water_stress: # Calculate the soil moisture availability factors [0,1] in the # topsoil and the entire root zone (self.state.wtfac_tsoil, self.state.wtfac_root) = self.sm.calculate_soil_water_fac() else: # really this should only be a debugging option! self.state.wtfac_tsoil = 1.0 self.state.wtfac_root = 1.0 # Estimate photosynthesis if self.control.assim_model == "BEWDY": self.bw.calculate_photosynthesis(frac_gcover, project_day, daylen) elif self.control.assim_model == "MATE": self.mt.calculate_photosynthesis(project_day, daylen) else: raise AttributeError('Unknown assimilation model') def calc_carbon_allocation_fracs(self, nitfac, yr_index): """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 alroot_exudate : float allocation fraction for root exudate References: ----------- McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. """ #self.state.alleaf = (self.params.callocf + nitfac * # (self.params.callocf - self.params.callocfz)) if type(self.params.callocr) == type(list()): aw = self.params.callocr[yr_index] awz = self.params.callocrz[yr_index] else: aw = self.params.callocr awz = self.params.callocrz self.state.alroot = aw + nitfac * (aw - awz) if type(self.params.callocf) == type(list()): af = self.params.callocf[yr_index] afz = self.params.callocrz[yr_index] else: af = self.params.callocf afz = self.params.callocfz self.state.alleaf = af + nitfac * (af - afz) #self.state.alroot = (self.params.callocr + nitfac * # (self.params.callocr - self.params.callocrz)) self.state.albranch = (self.params.callocb + nitfac * (self.params.callocb - self.params.callocbz)) # Remove some of the allocation to wood and instead allocate it to # root exudation. Following McMurtrie et al. 2000 self.state.alroot_exudate = self.params.callocrx # allocate remainder to stem self.state.alstem = (1.0 - self.state.alleaf - self.state.alroot - self.state.albranch - self.state.alroot_exudate) def allocate_stored_c_and_n(self, init): """ Allocate stored C&N. This is either down as the model is initialised for the first time or at the end of each year. """ """# JUST here for phase stuff as first year of ele should have last years alloc fracs if init == True: self.state.alleaf = 0.26 self.state.alroot = 0.11 self.state.albranch = 0.06 self.state.alstem = 0.57 """ # ======================== # Carbon - fixed fractions # ======================== self.state.c_to_alloc_shoot = self.state.alleaf * self.state.cstore self.state.c_to_alloc_root = self.state.alroot * self.state.cstore self.state.c_to_alloc_branch = self.state.albranch * self.state.cstore self.state.c_to_alloc_stem = self.state.alstem * self.state.cstore #self.state.c_to_alloc_rootexudate = (self.state.alroot_exudate * # self.state.cstore) #print self.state.alleaf , self.state.alroot, self.state.albranch, self.state.alstem, # ========= # Nitrogen # ========= # Fixed ratios N allocation to woody components. # N flux into new ring (immobile component -> structrual components) self.state.n_to_alloc_stemimm = (self.state.cstore * self.state.alstem * self.params.ncwimm) # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.state.n_to_alloc_stemmob = (self.state.cstore * self.state.alstem * (self.params.ncwnew - self.params.ncwimm)) self.state.n_to_alloc_branch = (self.state.cstore * self.state.albranch * self.params.ncbnew) # Calculate remaining N left to allocate to leaves and roots ntot = (self.state.nstore - self.state.n_to_alloc_stemimm - self.state.n_to_alloc_stemmob - self.state.n_to_alloc_branch) # allocate remaining N to flexible-ratio pools self.state.n_to_alloc_shoot = (ntot * self.state.alleaf / (self.state.alleaf + self.state.alroot * self.params.ncrfac)) self.state.n_to_alloc_root = ntot - self.state.n_to_alloc_shoot def nitrogen_allocation(self, ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr): """ 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() # Ross's Root Model. # NOT WORKING YET 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 * 365.25 # covnert t ha-1 to kg m-2 rtot = self.state.root * const.TONNES_HA_2_KG_M2 (self.state.root_depth, self.fluxes.nuptake, self.fluxes.rabove) = self.rm.main(rtot, nsupply, depth_guess=1.0) # 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 / 365.25 # covert from kg N m-2 to t ha-1 self.fluxes.deadroots = (self.params.rdecay * self.fluxes.rabove * const.KG_M2_2_TONNES_HA) self.fluxes.deadrootn = (self.state.rootnc * (1.0 - self.params.rretrans) * self.fluxes.deadroots) #print self.fluxes.gpp*100, self.state.lai, self.state.root*100, \ # self.fluxes.nuptake *100. #print self.fluxes.gpp*100, self.state.lai, self.state.root*100, \ # root_depth, self.fluxes.nuptake *100. #print self.fluxes.nuptake* 365.25, self.fluxes.deadroots # N lost from system is proportional to the soil inorganic N pool, # where the rate constant empirically defines gaseous and leaching # losses, see McMurtrie et al. 2001. self.fluxes.nloss = self.params.rateloss * self.state.inorgn # total nitrogen to allocate ntot = self.fluxes.nuptake + self.fluxes.retrans # N flux into root exudation, see McMurtrie et al. 2000 self.fluxes.nrootexudate = (self.fluxes.npp * self.state.alroot_exudate * self.params.vxfix) 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 )) 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 nitrogen_retrans(self, fdecay, rdecay, doy): """ Nitrogen retranslocated from senesced plant matter. Constant rate of n translocated from mobile pool Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate Returns: -------- N retrans : float N retranslocated plant matter """ if self.control.deciduous_model: leafretransn = (self.params.fretrans * self.fluxes.lnrate * self.state.remaining_days[doy]) else: leafretransn = self.params.fretrans * fdecay * self.state.shootn arg1 = (leafretransn + self.params.rretrans * rdecay * self.state.rootn + self.params.bretrans * self.params.bdecay * self.state.branchn) arg2 = (self.params.wretrans * self.params.wdecay * self.state.stemnmob + self.params.retransmob * self.state.stemnmob) return arg1 + arg2 def calculate_nuptake(self): """ N uptake from the soil, note as it stands root biomass does not affect N uptake. Returns: -------- nuptake : float N uptake References: ----------- * Dewar and McMurtrie, 1996, Tree Physiology, 16, 161-171. """ if self.control.nuptake_model == 0: # Constant N uptake nuptake = self.params.nuptakez elif self.control.nuptake_model == 1: # evaluate nuptake : proportional to dynamic inorganic N pool nuptake = self.params.rateuptake * self.state.inorgn elif self.control.nuptake_model == 2: # Assume N uptake depends on the rate at which soil mineral # N is made available (self.params.Uo) and the value or root C # at which 50% of the available N is taken up (Dewar and McM). arg = (self.params.uo * self.state.inorgn * (self.state.root / (self.state.root + self.params.kr))) nuptake = max(arg, 0.0) else: raise AttributeError('Unknown N uptake assumption') return nuptake def carbon_allocation(self, nitfac, doy, days_in_yr): """ C distribution - allocate available C through system Parameters: ----------- nitfac : float leaf N:C as a fraction of 'Ncmaxfyoung' (max 1.0) References: ----------- * Hale, M. G. et al. (1981) Factors affecting root exudation and significance for the rhizosphere ecoystems. Biological and chemical interactions in the rhizosphere. Stockholm. Sweden: Ecological Research Committee of NFR. pg. 43--71. * Lambers, J. T. and Poot, P. (2003) Structure and Functioning of Cluster Roots and Plant Responses to Phosphate Deficiency. * Martin, J. K. and Puckjeridge, D. W. (1982) Carbon flow through the rhizosphere of wheat crops in South Australia. The cyclcing of carbon, nitrogen, sulpher and phosphorous in terrestrial and aquatic ecosystems. Canberra: Australian Academy of Science. pg 77--82. * McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. Also see: * Rovira, A. D. (1969) Plant Root Exudates. Botanical Review, 35, pg 35--57. """ if self.control.deciduous_model: days_left = self.state.growing_days[doy] self.fluxes.cpleaf = self.fluxes.lrate * days_left self.fluxes.cpbranch = self.fluxes.brate * days_left self.fluxes.cpstem = self.fluxes.wrate * days_left self.fluxes.cproot = self.state.c_to_alloc_root * 1.0 / days_in_yr else: 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 # C flux into root exudation, see McMurtrie et al. 2000. There is no # reference given for the 0.15 in McM, however 14c work by Hale et al and # Martin and Puckeridge suggest values range between 10-20% of NPP. So # presumably this is where this values of 0.15 (i.e. the average) comes # from self.fluxes.cprootexudate = self.fluxes.npp * self.state.alroot_exudate #self.fluxes.cprootexudate = 0.0 # evaluate SLA of new foliage accounting for variation in SLA # with tree and leaf age (Sands and Landsberg, 2002). Assume # SLA of new foliage is linearly related to leaf N:C ratio # via nitfac # (m2 onesided/kg DW) self.state.sla = (self.params.slazero + nitfac * (self.params.slamax - self.params.slazero)) if self.control.deciduous_model: if float_eq(self.state.shoot, 0.0): self.state.lai = 0.0 elif self.state.leaf_out_days[doy] > 0.0: self.state.lai += (self.fluxes.cpleaf * (self.state.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) else: self.state.lai = 0.0 else: # update leaf area [m2 m-2] self.state.lai += (self.fluxes.cpleaf * (self.state.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) def precision_control(self, tolerance=1E-08): """ Detect very low values in state variables and force to zero to avoid rounding and overflow errors """ # C & N state variables if self.state.shoot < tolerance: self.fluxes.deadleaves += self.state.shoot self.fluxes.deadleafn += self.state.shootn self.fluxes.deadbranch += self.state.branch self.fluxes.deadbranchn += self.state.branchn self.state.shoot = 0.0 self.state.shootn = 0.0 self.state.branch = 0.0 self.state.branchn = 0.0 if self.state.root < tolerance: self.fluxes.deadrootn += self.state.rootn self.fluxes.deadroots += self.state.root self.state.root = 0.0 self.state.rootn = 0.0 if self.state.stem < tolerance: self.fluxes.deadstemn += self.state.stem self.state.stem = 0.0 self.state.stemnimm = 0.0 self.state.stemnmob = 0.0 # should add check for soil pools - excess goes where? 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 # # 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) self.state.stemn = self.state.stemnimm + self.state.stemnmob #self.state.exu_pool += self.fluxes.cprootexudate #self.fluxes.microbial_resp = self.calc_microbial_resp(project_day) #self.state.exu_pool -= self.fluxes.microbial_resp 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 calculate_cn_store(self): # Total C & N storage to allocate annually. self.state.cstore += self.fluxes.npp self.state.nstore += self.fluxes.nuptake + self.fluxes.retrans self.state.anpp += self.fluxes.npp def calc_microbial_resp(self, project_day): """ Based on LPJ-why References: * Wania (2009) Integrating peatlands and permafrost into a dynamic global vegetation model: 1. Evaluation and sensitivity of physical land surface processes. GBC, 23, GB3014. * Also part 2 and the geosci paper in 2010 """ tsoil = self.met_data['tsoil'][project_day] # Lloyd and Taylor, 1994 if tsoil < -10.0: temp_resp = 0.0 else: temp_resp = (exp(308.56 * ((1.0 / 56.02) - (1.0 / (tsoil + const.DEG_TO_KELVIN - 227.13))))) moist_resp = ((1.0 - exp(-1.0 * self.state.wtfac_tsoil)) / (1.0 - exp(-1.0))) # Pool turnover rate = every 2 weeks -> days. Check you have this right # and turn into a parameter if so k_exu10 = 0.0714128571 k_exu = k_exu10 * temp_resp * moist_resp return self.state.exu_pool * (1.0 - exp(-k_exu))
class PlantGrowth(object): """ G'DAY plant growth module. Calls photosynthesis model, water balance and evolve plant state. Pools recieve C through allocation of accumulated photosynthate and N from both soil uptake and retranslocation within the plant. Key feedback through soil N mineralisation and plant N uptake * Note met_forcing is an object with radiation, temp, precip data, etc. """ def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) if self.control.ps_pathway == "C3": self.mt = MateC3(self.control, self.params, self.state, self.fluxes, self.met_data) else: self.mt = MateC4(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.sm.initialise_parameters() self.rm = RootingDepthModel(d0x=self.params.d0x, r0=self.params.r0, top_soil_depth=self.params.topsoil_depth * const.MM_TO_M) # Window size = root lifespan in days... self.window_size = (int(1.0 / (self.params.rdecay * const.NDAYS_IN_YR) * const.NDAYS_IN_YR)) #self.window_size = 365 # If we don't have any information about the N&water limitation, i.e. # as would be the case with spin-up, assume that there is no limitation # to begin with. if self.state.prev_sma is None: self.state.prev_sma = 1.0 self.sma = MovingAverageFilter(self.window_size, self.state.prev_sma) def calc_day_growth(self, project_day, fdecay, rdecay, daylen, doy, days_in_yr, yr_index): """Evolve plant state, photosynthesis, distribute N and C" Parameters: ----------- project_day : integer simulation day fdecay : float foliage decay rate rdecay : float fine root decay rate """ # calculate NPP self.carbon_production(project_day, daylen) # calculate water balance self.wb.calculate_water_balance(project_day, daylen) # leaf N:C as a fraction of Ncmaxyoung, i.e. the max N:C ratio of # foliage in young stand nitfac = min(1.0, self.state.shootnc / self.params.ncmaxfyoung) # figure out the C allocation fractions self.calc_carbon_allocation_fracs(nitfac, yr_index, project_day) # Distribute new C and N through the system self.carbon_allocation(nitfac, doy, days_in_yr) (ncbnew, ncwimm, ncwnew) = self.calculate_ncwood_ratios(nitfac) self.nitrogen_allocation(ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy, days_in_yr, project_day) self.update_plant_state(fdecay, rdecay, project_day, doy) #if self.control.deciduous_model: self.precision_control() def calculate_ncwood_ratios(self, nitfac): """ Estimate the N:C ratio in the branch and stem. Option to vary the N:C ratio of the stem following Jeffreys (1999) or keep it a fixed fraction Parameters: ----------- nitfac : float leaf N:C as a fraction of the max N:C ratio of foliage in young stand Returns: -------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem References: ---------- * Jeffreys, M. P. (1999) Dynamics of stemwood nitrogen in Pinus radiata with modelled implications for forest productivity under elevated atmospheric carbon dioxide. PhD. """ # n:c ratio of new branch wood ncbnew = (self.params.ncbnew + nitfac * (self.params.ncbnew - self.params.ncbnewz)) # fixed N:C in the stemwood if self.control.fixed_stem_nc == 1: # n:c ratio of stemwood - immobile pool and new ring ncwimm = (self.params.ncwimm + nitfac * (self.params.ncwimm - self.params.ncwimmz)) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = (self.params.ncwnew + nitfac * (self.params.ncwnew - self.params.ncwnewz)) # vary stem N:C based on reln with foliage, see Jeffreys. Jeffreys 1999 # showed that N:C ratio of new wood increases with foliar N:C ratio, # modelled here based on evidence as a linear function. else: ncwimm = max(0.0, (0.0282 * self.state.shootnc + 0.000234) * self.params.fhw) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = max(0.0, 0.162 * self.state.shootnc - 0.00143) return (ncbnew, ncwimm, ncwnew) def carbon_production(self, project_day, daylen): """ Calculate GPP, NPP and plant respiration Parameters: ----------- project_day : integer simulation day daylen : float daytime length (hrs) References: ----------- * Jackson, J. E. and Palmer, J. W. (1981) Annals of Botany, 47, 561-565. """ if self.state.lai > 0.0: # average leaf nitrogen content (g N m-2 leaf) leafn = (self.state.shootnc * self.params.cfracts / self.state.sla * const.KG_AS_G) # total nitrogen content of the canopy self.state.ncontent = leafn * self.state.lai else: self.state.ncontent = 0.0 # When canopy is not closed, canopy light interception is reduced cf = min(1.0, self.state.lai / self.params.lai_cover) # fIPAR - the fraction of intercepted PAR = IPAR/PAR incident at the # top of the canopy, accounting for partial closure based on Jackson # and Palmer (1981), derived from beer's law if self.state.lai > 0.0: self.state.fipar = ( (1.0 - exp(-self.params.kext * self.state.lai / cf)) * cf) else: self.state.fipar = 0.0 # Canopy extinction coefficient if the canopy is open #if cf < 1.0: # kext = -log(1.0 - self.state.fipar) / LAI if self.control.water_stress: # Calculate the soil moisture availability factors [0,1] in the # topsoil and the entire root zone (self.state.wtfac_topsoil, self.state.wtfac_root) = self.sm.calculate_soil_water_fac() else: # really this should only be a debugging option! self.state.wtfac_tsoil = 1.0 self.state.wtfac_root = 1.0 # Estimate photosynthesis if self.control.assim_model == "BEWDY": self.bw.calculate_photosynthesis(frac_gcover, project_day, daylen) elif self.control.assim_model == "MATE": self.mt.calculate_photosynthesis(project_day, daylen) else: raise AttributeError('Unknown assimilation model') 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" #print self.state.alleaf, self.state.alstem, self.state.albranch, self.state.alroot def alloc_goal_seek(self, simulated, target, alloc_max, sensitivity): arg = 0.5 + 0.5 * ((1.0 - simulated / target) / sensitivity) return max(0.0, alloc_max * min(1.0, arg)) def allocate_stored_c_and_n(self, init): """ Allocate stored C&N. This is either down as the model is initialised for the first time or at the end of each year. """ # JUST here for FACE stuff as first year of ele should have last years alloc fracs #if init == True: # self.state.alleaf = 0.26 # self.state.alroot = 0.11 # self.state.albranch = 0.06 # self.state.alstem = 0.57 # ======================== # Carbon - fixed fractions # ======================== self.state.c_to_alloc_shoot = self.state.alleaf * self.state.cstore self.state.c_to_alloc_root = self.state.alroot * self.state.cstore self.state.c_to_alloc_branch = self.state.albranch * self.state.cstore self.state.c_to_alloc_stem = self.state.alstem * self.state.cstore # ========= # Nitrogen # ========= # Fixed ratios N allocation to woody components. # N flux into new ring (immobile component -> structrual components) self.state.n_to_alloc_stemimm = (self.state.cstore * self.state.alstem * self.params.ncwimm) # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.state.n_to_alloc_stemmob = ( self.state.cstore * self.state.alstem * (self.params.ncwnew - self.params.ncwimm)) self.state.n_to_alloc_branch = (self.state.cstore * self.state.albranch * self.params.ncbnew) # Calculate remaining N left to allocate to leaves and roots ntot = (self.state.nstore - self.state.n_to_alloc_stemimm - self.state.n_to_alloc_stemmob - self.state.n_to_alloc_branch) # allocate remaining N to flexible-ratio pools self.state.n_to_alloc_shoot = ( ntot * self.state.alleaf / (self.state.alleaf + self.state.alroot * self.params.ncrfac)) self.state.n_to_alloc_root = ntot - self.state.n_to_alloc_shoot 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 nitrogen_retrans(self, fdecay, rdecay, doy): """ Nitrogen retranslocated from senesced plant matter. Constant rate of n translocated from mobile pool Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate Returns: -------- N retrans : float N retranslocated plant matter """ if self.control.deciduous_model: leafretransn = (self.params.fretrans * self.fluxes.lnrate * self.state.remaining_days[doy]) else: leafretransn = self.params.fretrans * fdecay * self.state.shootn arg1 = (leafretransn + self.params.rretrans * rdecay * self.state.rootn + self.params.bretrans * self.params.bdecay * self.state.branchn) arg2 = ( self.params.wretrans * self.params.wdecay * self.state.stemnmob + self.params.retransmob * self.state.stemnmob) return arg1 + arg2 def calculate_nuptake(self, project_day): """ N uptake depends on the rate at which soil mineral N is made available to the plants. Returns: -------- nuptake : float N uptake References: ----------- * Dewar and McMurtrie, 1996, Tree Physiology, 16, 161-171. * Raich et al. 1991, Ecological Applications, 1, 399-429. """ if self.control.nuptake_model == 0: # Constant N uptake nuptake = self.params.nuptakez elif self.control.nuptake_model == 1: # evaluate nuptake : proportional to dynamic inorganic N pool nuptake = self.params.rateuptake * self.state.inorgn elif self.control.nuptake_model == 2: # N uptake is a saturating function on root biomass following # Dewar and McMurtrie, 1996. # supply rate of available mineral N U0 = self.params.rateuptake * self.state.inorgn Kr = self.params.kr nuptake = max(U0 * self.state.root / (self.state.root + Kr), 0.0) elif self.control.nuptake_model == 3: # N uptake is a function of available soil N, soil moisture # following a Michaelis-Menten approach # See Raich et al. 1991, pg. 423. vcn = 1.0 / 0.0215 # 46.4 arg1 = (vcn * self.state.shootn) - self.state.shoot arg2 = (vcn * self.state.shootn) + self.state.shoot self.params.ac += self.params.adapt * arg1 / arg2 self.params.ac = max(min(1.0, self.params.ac), 0.0) # soil moisture is assumed to influence nutrient diffusion rate # through the soil, ks [0,1] theta = self.state.pawater_root / self.params.wcapac_root ks = 0.9 * theta**3.0 + 0.1 arg1 = self.params.nmax * ks * self.state.inorgn arg2 = self.params.knl + (ks * self.state.inorgn) arg3 = exp(0.0693 * self.met_data['tair'][project_day]) arg4 = 1.0 - self.params.ac nuptake = (arg1 / arg2) * arg3 * arg4 #print self.params.nmax, self.params.knl, ks, exp(0.0693 * tavg) else: raise AttributeError('Unknown N uptake option') return nuptake def carbon_allocation(self, nitfac, doy, days_in_yr): """ C distribution - allocate available C through system Parameters: ----------- nitfac : float leaf N:C as a fraction of 'Ncmaxfyoung' (max 1.0) """ if self.control.deciduous_model: days_left = self.state.growing_days[doy] self.fluxes.cpleaf = self.fluxes.lrate * days_left self.fluxes.cpbranch = self.fluxes.brate * days_left self.fluxes.cpstem = self.fluxes.wrate * days_left self.fluxes.cproot = self.state.c_to_alloc_root * 1.0 / days_in_yr else: 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 #print self.fluxes.cpleaf, self.fluxes.npp, self.state.alleaf # evaluate SLA of new foliage accounting for variation in SLA # with tree and leaf age (Sands and Landsberg, 2002). Assume # SLA of new foliage is linearly related to leaf N:C ratio # via nitfac. Based on date from two E.globulus stands in SW Aus, see # Corbeels et al (2005) Ecological Modelling, 187, 449-474. # (m2 onesided/kg DW) self.state.sla = (self.params.slazero + nitfac * (self.params.slamax - self.params.slazero)) if self.control.deciduous_model: if float_eq(self.state.shoot, 0.0): self.state.lai = 0.0 elif self.state.leaf_out_days[doy] > 0.0: self.state.lai += ( self.fluxes.cpleaf * (self.state.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) else: self.state.lai = 0.0 else: # update leaf area [m2 m-2] if float_eq(self.state.shoot, 0.0): self.state.lai = 0.0 else: self.state.lai += ( self.fluxes.cpleaf * (self.state.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) def precision_control(self, tolerance=1E-08): """ Detect very low values in state variables and force to zero to avoid rounding and overflow errors """ # C & N state variables if self.state.shoot < tolerance: self.fluxes.deadleaves += self.state.shoot self.fluxes.deadleafn += self.state.shootn self.state.shoot = 0.0 self.state.shootn = 0.0 if self.state.branch < tolerance: self.fluxes.deadbranch += self.state.branch self.fluxes.deadbranchn += self.state.branchn self.state.branch = 0.0 self.state.branchn = 0.0 if self.state.root < tolerance: self.fluxes.deadrootn += self.state.rootn self.fluxes.deadroots += self.state.root self.state.root = 0.0 self.state.rootn = 0.0 if self.state.stem < tolerance: self.fluxes.deadstems += self.state.stem self.fluxes.deadstemn += self.state.stemn self.state.stem = 0.0 self.state.stemn = 0.0 self.state.stemnimm = 0.0 self.state.stemnmob = 0.0 # need separate one as this will become very small if there is no # mobile stem N if self.state.stemnmob < tolerance: self.fluxes.deadstemn += self.state.stemnmob self.state.stemnmob = 0.0 if self.state.stemnimm < tolerance: self.fluxes.deadstemn += self.state.stemnimm self.state.stemnimm = 0.0 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 calculate_cn_store(self): """ Deciduous trees store carbohydrate during the winter which they then use in the following year to build new leaves (buds & budburst are implied) """ # Total C & N storage to allocate annually. self.state.cstore += self.fluxes.npp self.state.nstore += self.fluxes.nuptake + self.fluxes.retrans self.state.anpp += self.fluxes.npp
class PlantGrowth(object): """ G'DAY plant growth module. Calls photosynthesis model, water balance and evolve plant state. Pools recieve C through allocation of accumulated photosynthate and N from both soil uptake and retranslocation within the plant. Key feedback through soil N mineralisation and plant N uptake * Note met_forcing is an object with radiation, temp, precip data, etc. """ def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, object model control flags params: floats, object model parameters state: floats, object model state fluxes : floats, object model fluxes met_data : floats, dictionary meteorological forcing data """ self.params = params self.fluxes = fluxes self.control = control self.state = state self.met_data = met_data self.bw = Bewdy(self.control, self.params, self.state, self.fluxes, self.met_data) self.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) self.pp = PlantProdModel(self.control, self.params, self.state, self.fluxes, self.met_data) self.wl = WaterLimitedNPP(self.control, self.params, self.state, self.fluxes) self.mt = Mate(self.control, self.params, self.state, self.fluxes, self.met_data) self.sm = SoilMoisture(self.control, self.params, self.state, self.fluxes) self.rm = RootingDepthModel(d0=0.35, r0=0.05, top_soil_depth=0.3) def calc_day_growth(self, project_day, fdecay, rdecay, daylen, doy, days_in_yr): """Evolve plant state, photosynthesis, distribute N and C" Parameters: ----------- project_day : integer simulation day fdecay : float foliage decay rate rdecay : float fine root decay rate """ # calculate NPP self.carbon_production(project_day, daylen) # calculate water balance and adjust C production for any water stress. # If we are using the MATE model then water stress is applied directly # through the Ci:Ca reln, so do not apply any scalar to production. if self.control.water_model == 1: self.wb.calculate_water_balance(project_day, daylen) # adjust carbon production for water limitations, all models except # MATE! if self.control.assim_model != 7: self.wl.adjust_cproduction(self.control.water_model) # leaf N:C as a fraction of Ncmaxyoung, i.e. the max N:C ratio of # foliage in young stand nitfac = min(1.0, self.state.shootnc / self.params.ncmaxfyoung) if not self.control.deciduous_model: # figure out allocation fractions for C for the evergreen model. For # the deciduous model these are calculated at the annual time step. self.allocate_carbon(nitfac) # Distribute new C and N through the system self.carbon_distribution(nitfac, doy, days_in_yr) (ncbnew, ncwimm, ncwnew) = self.calculate_ncwood_ratios(nitfac) self.nitrogen_distribution(ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy) self.update_plant_state(fdecay, rdecay, project_day) def calculate_ncwood_ratios(self, nitfac): """ Estimate the N:C ratio in the branch and stem. Option to vary the N:C ratio of the stem following Jeffreys (1999) or keep it a fixed fraction Parameters: ----------- nitfac : float leaf N:C as a fraction of the max N:C ratio of foliage in young stand Returns: -------- ncbnew : float N:C ratio of branch ncwimm : float N:C ratio of immobile stem ncwnew : float N:C ratio of mobile stem References: ---------- * Jeffreys, M. P. (1999) Dynamics of stemwood nitrogen in Pinus radiata with modelled implications for forest productivity under elevated atmospheric carbon dioxide. PhD. """ # n:c ratio of new branch wood ncbnew = (self.params.ncbnew + nitfac * (self.params.ncbnew - self.params.ncbnewz)) # fixed N:C in the stemwood if self.control.fixed_stem_nc == 1: # n:c ratio of stemwood - immobile pool and new ring ncwimm = (self.params.ncwimm + nitfac * (self.params.ncwimm - self.params.ncwimmz)) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = (self.params.ncwnew + nitfac * (self.params.ncwnew - self.params.ncwnewz)) # vary stem N:C based on reln with foliage, see Jeffreys. Jeffreys 1999 # showed that N:C ratio of new wood increases with foliar N:C ratio, # modelled here based on evidence as a linear function. else: ncwimm = max(0.0, (0.0282 * self.state.shootnc + 0.000234) * self.params.fhw) # New stem ring N:C at critical leaf N:C (mobile) ncwnew = max(0.0, 0.162 * self.state.shootnc - 0.00143) return (ncbnew, ncwimm, ncwnew) def carbon_production(self, project_day, daylen): """ Calculate GPP, NPP and plant respiration Parameters: ----------- project_day : integer simulation day daylen : float daytime length (hrs) References: ----------- * Jackson, J. E. and Palmer, J. W. (1981) Annals of Botany, 47, 561-565. """ # leaf nitrogen content if self.state.lai > 0.0: # Leaf N content (g m-2) self.state.ncontent = (self.state.shootnc * self.params.cfracts / self.state.sla * const.KG_AS_G) else: self.state.ncontent = 0.0 # fractional ground cover. if float_lt(self.state.lai, self.params.lai_cover): frac_gcover = self.state.lai / self.params.lai_cover else: frac_gcover = 1.0 # Radiance intercepted by the canopy, accounting for partial closure # Jackson and Palmer (1981), derived from beer's law if self.state.lai > 0.0: self.state.light_interception = ((1.0 - exp(-self.params.kext * self.state.lai / frac_gcover)) * frac_gcover) else: self.state.light_interception = 0.0 if self.control.water_stress: # Calculate the soil moisture availability factors [0,1] in the # topsoil and the entire root zone (self.state.wtfac_tsoil, self.state.wtfac_root) = self.sm.calculate_soil_water_fac() else: # really this should only be a debugging option! self.state.wtfac_tsoil = 1.0 self.state.wtfac_root = 1.0 # Estimate photosynthesis using an empirical model if self.control.assim_model >=0 and self.control.assim_model <= 4: self.pp.calculate_photosynthesis(project_day) # Estimate photosynthesis using the mechanistic BEWDY model elif self.control.assim_model >=5 and self.control.assim_model <= 6: # calculate plant C uptake using bewdy self.bw.calculate_photosynthesis(frac_gcover, project_day, daylen) # Estimate photosynthesis using the mechanistic MATE model. Also need to # calculate a water availability scalar to determine Ci:Ca reln. elif self.control.assim_model ==7: self.mt.calculate_photosynthesis(project_day, daylen) else: raise AttributeError('Unknown assimilation model') def allocate_carbon(self, nitfac): """Carbon allocation fractions to move photosynthate through the plant. Allocations to foliage tends to decrease with stand age and wood stock increases (Makela and Hari, 1986; Cannell and Dewar, 1994; Magnani et al, 2000). In stressed (soil/nutrient) regions fine root allocations increases. 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 alroot_exudate : float allocation fraction for root exudate References: ----------- McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. """ self.state.alleaf = (self.params.callocf + nitfac * (self.params.callocf - self.params.callocfz)) self.state.alroot = (self.params.callocr + nitfac * (self.params.callocr - self.params.callocrz)) self.state.albranch = (self.params.callocb + nitfac * (self.params.callocb - self.params.callocbz)) # Remove some of the allocation to wood and instead allocate it to # root exudation. Following McMurtrie et al. 2000 self.state.alroot_exudate = self.params.callocrx # allocate remainder to stem self.state.alstem = (1.0 - self.state.alleaf - self.state.alroot - self.state.albranch - self.state.alroot_exudate) def nitrogen_distribution(self, ncbnew, ncwimm, ncwnew, fdecay, rdecay, doy): """ 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) self.fluxes.nuptake = self.calculate_nuptake() # Ross's Root Model. # NOT WORKING YET if self.control.model_optroot == 1: # Attempt at floating rateuptake #slope = (20.0 - 0.1) / ((6.0) - 0.0) #y = slope * self.state.root + 0.0 #self.fluxes.nuptake = (y/365.25) * self.state.inorgn # have you got all these units conversion right, they don't appear # very consistent? you have grams and kg?! # convert t ha-1 d-1 to gN m-2 yr-1 nsupply = self.calculate_nuptake() * 365.25 * 100.0 # t ha-1->gN m-2 # covnert t ha-1 to kg m-2 rtot = self.state.root * 0.1 (root_depth, self.fluxes.nuptake, self.fluxes.rabove) = self.rm.main(rtot, nsupply, depth_guess=1.0) # covert nuptake from gN m-2 yr-1 to t ha-1 d-1 self.fluxes.nuptake = self.fluxes.nuptake * 0.01 / 365.25 # covert from kg N m-2 to t ha-1 self.fluxes.deadroots = self.params.rdecay * self.fluxes.rabove * 10 self.fluxes.deadrootn = (self.state.rootnc * (1.0 - self.params.rretrans) * self.fluxes.deadroots) print root_depth #print self.fluxes.gpp*100, self.state.lai, self.state.root*100, \ # self.fluxes.nuptake *100. #print self.fluxes.gpp*100, self.state.lai, self.state.root*100, \ # root_depth, self.fluxes.nuptake *100. # N lost from system is proportional to the soil inorganic N pool, # where the rate constant empirically defines gaseous and leaching # losses, see McMurtrie et al. 2001. self.fluxes.nloss = self.params.rateloss * self.state.inorgn # total nitrogen to allocate ntot = self.fluxes.nuptake + self.fluxes.retrans # N flux into root exudation, see McMurtrie et al. 2000 self.fluxes.nrootexudate = (self.fluxes.npp * self.state.alroot_exudate * self.params.vxfix) 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.cpstem * ncwimm # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = self.fluxes.cpstem * (ncwnew - ncwimm) self.fluxes.npbranch = 0.0 self.fluxes.nproot = self.fluxes.cproot * self.state.rootnc self.fluxes.npleaf = (self.fluxes.lnrate * self.state.growing_days[doy]) else: # allocate N to pools with fixed N:C ratios # N flux into new ring (immobile component -> structrual 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 not self.control.fixleafnc: self.fluxes.npp *= (ntot / (self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.npbranch )) 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 nitrogen_retrans(self, fdecay, rdecay): """ Nitrogen retranslocated from senesced plant matter. Constant rate of n translocated from mobile pool Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate Returns: -------- N retrans : float N retranslocated plant matter """ if self.control.deciduous_model: arg1 = (self.fluxes.leafretransn + self.params.rretrans * rdecay * self.state.rootn + self.params.bretrans * self.params.bdecay * self.state.branchn) arg2 = (self.params.wretrans * self.params.wdecay * self.state.stemnmob + self.params.retransmob * self.state.stemnmob) else: arg1 = (self.params.fretrans * fdecay * self.state.shootn + self.params.rretrans * rdecay * self.state.rootn + self.params.bretrans * self.params.bdecay * self.state.branchn) arg2 = (self.params.wretrans * self.params.wdecay * self.state.stemnmob + self.params.retransmob * self.state.stemnmob) return arg1 + arg2 def calculate_nuptake(self): """ N uptake from the soil, note as it stands root biomass does not affect N uptake. Returns: -------- nuptake : float N uptake References: ----------- * Dewar and McMurtrie, 1996, Tree Physiology, 16, 161-171. """ if self.control.nuptake_model == 0: # Constant N uptake nuptake = self.params.nuptakez elif self.control.nuptake_model == 1: # evaluate nuptake : proportional to dynamic inorganic N pool nuptake = self.params.rateuptake * self.state.inorgn elif self.control.nuptake_model == 2: # Assume N uptake depends on the rate at which soil mineral # N is made available (self.params.Uo) and the value or root C # at which 50% of the available N is taken up (Dewar and McM). arg = (self.params.uo * self.state.inorgn * (self.state.root / (self.state.root + self.params.kr))) nuptake = max(arg, 0.0) else: raise AttributeError('Unknown N uptake assumption') return nuptake def carbon_distribution(self, nitfac, doy, days_in_yr): """ C distribution - allocate available C through system Parameters: ----------- nitfac : float leaf N:C as a fraction of 'Ncmaxfyoung' (max 1.0) References: ----------- * Hale, M. G. et al. (1981) Factors affecting root exudation and significance for the rhizosphere ecoystems. Biological and chemical interactions in the rhizosphere. Stockholm. Sweden: Ecological Research Committee of NFR. pg. 43--71. * Lambers, J. T. and Poot, P. (2003) Structure and Functioning of Cluster Roots and Plant Responses to Phosphate Deficiency. * Martin, J. K. and Puckjeridge, D. W. (1982) Carbon flow through the rhizosphere of wheat crops in South Australia. The cyclcing of carbon, nitrogen, sulpher and phosphorous in terrestrial and aquatic ecosystems. Canberra: Australian Academy of Science. pg 77--82. * McMurtrie, R. E. et al (2000) Plant and Soil, 224, 135-152. Also see: * Rovira, A. D. (1969) Plant Root Exudates. Botanical Review, 35, pg 35--57. """ if self.control.deciduous_model: self.fluxes.cpleaf = self.fluxes.lrate * self.state.growing_days[doy] self.fluxes.cpbranch = 0.0 self.fluxes.cpstem = self.fluxes.wrate * self.state.growing_days[doy] self.fluxes.cproot = self.state.c_to_alloc_root * 1.0 / days_in_yr else: 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 # C flux into root exudation, see McMurtrie et al. 2000. There is no # reference given for the 0.15 in McM, however 14c work by Hale et al and # Martin and Puckeridge suggest values range between 10-20% of NPP. So # presumably this is where this values of 0.15 (i.e. the average) comes # from self.fluxes.cprootexudate = self.fluxes.npp * self.state.alroot_exudate #self.fluxes.cprootexudate = 0.0 # evaluate SLA of new foliage accounting for variation in SLA # with tree and leaf age (Sands and Landsberg, 2002). Assume # SLA of new foliage is linearly related to leaf N:C ratio # via nitfac sla_new = (self.params.slazero + nitfac * (self.params.slamax - self.params.slazero)) sla_new_tonnes_ha_C = (sla_new * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) if self.control.deciduous_model: if self.state.shoot == 0.0: self.state.lai = 0.0 elif self.state.leaf_out_days[doy] > 0.0: self.state.lai += (self.fluxes.cpleaf * sla_new_tonnes_ha_C - (self.fluxes.deadleaves + self.fluxes.ceaten)* self.state.lai / self.state.shoot) else: self.state.lai = 0.0 else: # update leaf area [m2 m-2] self.state.lai += (self.fluxes.cpleaf * sla_new_tonnes_ha_C - (self.fluxes.deadleaves + self.fluxes.ceaten) * self.state.lai / self.state.shoot) def update_plant_state(self, fdecay, rdecay, project_day): """ Daily change in C content Parameters: ----------- fdecay : float foliage decay rate rdecay : float fine root decay rate """ 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 if self.control.deciduous_model: self.state.shootn += (self.fluxes.npleaf - (self.fluxes.deadleafn - self.fluxes.neaten)) else: self.state.shootn += (self.fluxes.npleaf - fdecay * self.state.shootn - self.fluxes.neaten) self.state.shootn = max(0.0, self.state.shootn) self.state.rootn += self.fluxes.nproot - rdecay * self.state.rootn self.state.branchn += (self.fluxes.npbranch - self.params.bdecay * self.state.branchn) 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 self.state.exu_pool += self.fluxes.cprootexudate self.fluxes.microbial_resp = self.calc_microbial_resp(project_day) self.state.exu_pool -= self.fluxes.microbial_resp #print self.fluxes.microbial_resp, self.fluxes.cprootexudate self.calculate_cn_store() if not self.control.deciduous_model: # 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 # if foliage or 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 extrar = 0. extras = 0. if float_gt(self.state.shootn, (self.state.shoot * ncmaxf)): extras = self.state.shootn - self.state.shoot * ncmaxf #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 ncmaxr = ncmaxf * self.params.ncrfac # max root n:c if float_gt(self.state.rootn, (self.state.root * ncmaxr)): extrar = self.state.rootn - self.state.root * ncmaxr #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 calculate_cn_store(self, tolerance=1.0E-05): cgrowth = (self.fluxes.cpleaf + self.fluxes.cproot + self.fluxes.cpbranch + self.fluxes.cpstem) ngrowth = (self.fluxes.npleaf + self.fluxes.nproot + self.fluxes.npbranch + self.fluxes.npstemimm + self.fluxes.npstemmob) # C storage as TNC self.state.cstore += self.fluxes.npp - cgrowth self.state.nstore += self.fluxes.nuptake + self.fluxes.retrans - ngrowth def calc_microbial_resp(self, project_day): """ Based on LPJ-why References: * Wania (2009) Integrating peatlands and permafrost into a dynamic global vegetation model: 1. Evaluation and sensitivity of physical land surface processes. GBC, 23, GB3014. * Also part 2 and the geosci paper in 2010 """ tsoil = self.met_data['tsoil'][project_day] # Lloyd and Taylor, 1994 if tsoil < -10.0: temp_resp = 0.0 else: temp_resp = (exp(308.56 * ((1.0 / 56.02) - (1.0 / (tsoil + const.DEG_TO_KELVIN - 227.13))))) moist_resp = ((1.0 - exp(-1.0 * self.state.wtfac_tsoil)) / (1.0 - exp(-1.0))) # Pool turnover rate = every 2 weeks -> days. Check you have this right # and turn into a parameter if so k_exu10 = 0.0714128571 k_exu = k_exu10 * temp_resp * moist_resp return self.state.exu_pool * (1.0 - exp(-k_exu))