class DecompFactors(object): """ Calculate C and N litter production rates """ def __init__(self, control, params, state, fluxes, met_data): """ Parameters ---------- control : integers, structure model control flags params: floats, structure model parameters state: floats, structure model state fluxes : floats, structure 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.wb = WaterBalance(self.control, self.params, self.state, self.fluxes, self.met_data) def decay_rates(self, project_day): """ Model decay rates - temperature dependency (i.e. increase with temp) [See section A8 in Comins and McMurtrie 1993]. Parameters: ----------- project_day : int current simulation day (index) """ # temperature and water factors for decomposition tempact = self.soil_temp_factor(project_day) wtfac = self.wb.calculate_soil_water_fac(topsoil=True) # decay rate of surface structural pool self.params.decayrate[0] = self.params.kdec1 * math.exp(-3.0 * self.params.ligshoot) * tempact * wtfac # decay rate of surface metabolic pool self.params.decayrate[1] = self.params.kdec2 * tempact * wtfac # decay rate of soil structural pool self.params.decayrate[2] = self.params.kdec3 * math.exp(-3.0 * self.params.ligroot) * tempact * wtfac # decay rate of soil metabolic pool self.params.decayrate[3] = self.params.kdec4 * tempact * wtfac # decay rate of active pool self.params.decayrate[4] = self.params.kdec5 * (1.0 - 0.75 * self.params.finesoil) * tempact * wtfac # decay rate of slow pool self.params.decayrate[5] = self.params.kdec6 * tempact * wtfac # decay rate of passive pool self.params.decayrate[6] = self.params.kdec7 * tempact * wtfac def soil_temp_factor(self, project_day): """Soil-temperature activity factor (A9). Parameters: ----------- project_day : int current simulation day (index) Returns: -------- tfac : float soil temperature factor [degC] """ tsoil = self.met_data["tsoil"][project_day] if float_gt(tsoil, 0.0): tfac = 0.0326 + 0.00351 * tsoil ** 1.652 - (tsoil / 41.748) ** 7.19 if float_lt(tfac, 0.0): tfac = 0.0 else: # negative number cannot be raised to a fractional power # number would need to be complex tfac = 0.0 return tfac
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 and precip data """ 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) def grow(self, day, date, fdecay, rdecay, daylen, doy=None): """Evolve plant state, photosynthesis, distribute N and C" Parameters: ----------- day : intefer simulation day date : date string object date object string (yr/mth/day) fdecay : float foliage decay rate rdecay : float fine root decay rate """ # calculate NPP self.carbon_production(date, 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(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) # figure out allocation fractions for C (alleaf, alroot, albranch, alstem, alroot_exudate) = self.allocate_carbon(nitfac) # Distribute new C and N through the system self.carbon_distribution(alleaf, alroot, albranch, alstem, alroot_exudate, nitfac, doy) (ncbnew, ncwimm, ncwnew) = self.calculate_ncwood_ratios(nitfac) self.nitrogen_distribution(ncbnew, ncwimm, ncwnew, fdecay, rdecay, alleaf, alroot, albranch, alstem, alroot_exudate, doy) self.update_plant_state(fdecay, rdecay) 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 = (0.0282 * self.state.shootnc + 0.000234) * self.params.fhw # New stem ring N:C at critical leaf N:C (mobile) ncwnew = 0.162 * self.state.shootnc - 0.00143 return (ncbnew, ncwimm, ncwnew) def carbon_production(self, date, day, daylen): """ Calculate GPP, NPP and plant respiration Parameters: ----------- day : intefer simulation day date : date string object date object string (yr/mth/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 - math.exp(-self.params.kext * self.state.lai / frac_gcover)) * frac_gcover) else: self.state.light_interception = 0.0 # 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.wb.calculate_soil_water_fac() # Estimate photosynthesis using an empirical model if self.control.assim_model >=0 and self.control.assim_model <= 4: self.pp.calculate_photosynthesis(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, date, 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(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. """ alleaf = (self.params.callocf + nitfac * (self.params.callocf - self.params.callocfz)) alroot = (self.params.callocr + nitfac * (self.params.callocr - self.params.callocrz)) 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 alroot_exudate = self.params.callocrx # allocate remainder to stem alstem = 1.0 - alleaf - alroot - albranch - alroot_exudate #print alleaf, alroot, albranch, alstem #sys.exit() return (alleaf, alroot, albranch, alstem, alroot_exudate) def nitrogen_distribution(self, ncbnew, ncwimm, ncwnew, fdecay, rdecay, alleaf, alroot, albranch, alstem, alroot_exudate, 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 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 exudation """ # 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() # N lost from system is proportional to the 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 * alroot_exudate * # self.params.vxfix) self.fluxes.nrootexudate = 0.0 if self.control.deciduous_model: self.fluxes.npleaf = (self.fluxes.lnrate * self.state.growing_days[doy]) self.fluxes.npbranch = 0.0 self.fluxes.npstemimm = self.fluxes.cpstem * self.state.nc_ws # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = (self.fluxes.cpstem * (self.state.nc_wnew - self.state.nc_ws)) self.fluxes.nproot = self.fluxes.cproot * self.state.rootnc else: # allocate N to pools with fixed N:C ratios self.fluxes.npbranch = self.fluxes.npp * albranch * ncbnew # N flux into new ring (immobile component -> structrual components) self.fluxes.npstemimm = self.fluxes.npp * alstem * ncwimm # N flux into new ring (mobile component -> can be retrans for new # woody tissue) self.fluxes.npstemmob = self.fluxes.npp * alstem * (ncwnew - ncwimm) # 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.nrootexudate) 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.nrootexudate)) self.fluxes.npbranch = self.fluxes.npp * albranch * ncbnew self.fluxes.npstemimm = self.fluxes.npp * alstem * ncwimm self.fluxes.npstemmob = self.fluxes.npp * alstem * (ncwnew - ncwimm) self.fluxes.nrootexudate = (self.fluxes.npp * alroot_exudate * self.params.vxfix) ntot -= (self.fluxes.npbranch + self.fluxes.npstemimm + self.fluxes.npstemmob + self.fluxes.nrootexudate) # allocate remaining N to flexible-ratio pools self.fluxes.npleaf = (ntot * alleaf / (alleaf + 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, alleaf, alroot, albranch, alstem, alroot_exudate, nitfac, doy): """ C distribution - allocate available C through system Parameters: ----------- 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 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.Hr * 1.0 / 365.25 else: self.fluxes.cpleaf = self.fluxes.npp * alleaf self.fluxes.cproot = self.fluxes.npp * alroot self.fluxes.cpbranch = self.fluxes.npp * albranch self.fluxes.cpstem = self.fluxes.npp * 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 * alroot_exudate self.fluxes.cprootexudate = 0.0 # rhizresp = 0.5, unless changed of course! 1/3--2/3 of C # fixed by plants is respired, so assuming a value of 0.5, i.e. the avg. # (Lambers and Poot, 2003) #self.fluxes.microbial_resp = (self.fluxes.cprootexudate * # self.params.rhizresp) #self.fluxes.cprootexudate -= self.fluxes.microbial_resp # 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) """ sla_tonnes_ha_C = (self.state.sla * const.M2_AS_HA / (const.KG_AS_TONNES * self.params.cfracts)) self.state.lai = self.state.shoot * sla_tonnes_ha_C """ 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) #(Cpleaf * sla_new_tonnes_ha_C - Deadleaves * Lai / Shoot ) def update_plant_state(self, fdecay, rdecay): """ 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 # 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 if self.control.deciduous_model: # update annual fluxes - store for next year self.state.clabile_store += self.fluxes.npp self.state.aroot_uptake += self.fluxes.nuptake self.state.aretrans += self.fluxes.retrans self.state.anloss += self.fluxes.nloss # update N:C of plant pools if float_eq(self.state.shoot, 0.0): self.state.shootnc = 0.0 else: self.state.shootnc = max(self.state.shootn / self.state.shoot, self.params.ncfmin) # N:C of stuctural wood as function of N:C of foliage # (kgN kg-1C)(Medlyn et al 2000) self.state.nc_ws = (self.params.nc_wsa + self.params.nc_wsb * self.state.shootnc) # N:C of new wood as function of N:C of foliage self.state.nc_wnew = (self.params.nc_wnewa + self.params.nc_wnewb * self.state.shootnc)