예제 #1
0
class GasExchange(System):
    weather = system(alias='w')
    soil = system()
    leaf = system(PhotosyntheticLeaf, weather='weather', soil='soil')

    @constant
    def name(self):
        return ''

    @derive
    def A_gross(self):
        return self.leaf.A_gross

    @derive
    def A_net(self):
        return self.leaf.A_net

    @derive
    def ET(self):
        return self.leaf.ET

    @derive
    def T_leaf(self):
        return self.leaf.temperature

    @derive
    def VPD(self):
        #TODO: use Weather directly, instead of through PhotosyntheticLeaf
        return self.weather.VPD

    @derive
    def gs(self):
        return self.leaf.stomata.stomatal_conductance
예제 #2
0
class NodalUnit(Organ):
    rank = constant()
    leaf = system(Leaf, plant='self.plant', nodal_unit='self')
    sheath = system(Sheath, plant='self.plant', nodal_unit='self')

    @derive
    def mass(self):
        return self.leaf.mass + self.sheath.mass
예제 #3
0
파일: sheath.py 프로젝트: tomyun/cropbox
class Sheath(Organ):
    nodal_unit = system()

    @derive
    def rank(self):
        return self.nodal_unit.rank

    @derive
    def mass(self):
        #FIXME sheath biomass
        return 0
예제 #4
0
class GasExchange(System):
    #TODO: use externally initialized Weather / Soil
    @system(alias='w')
    def weather(self):
        return Weather

    @system
    def soil(self):
        return Soil

    # @system(weather='weather', soil='soil')
    # def leaf(self):
    #     return PhotosyntheticLeaf
    leaf = system(PhotosyntheticLeaf, weather='weather', soil='soil')

    @derive
    def A_gross(self):
        return self.leaf.A_gross

    @derive
    def A_net(self):
        return self.leaf.A_net

    @derive
    def ET(self):
        return self.leaf.ET

    @derive
    def T_leaf(self):
        return self.leaf.temperature

    @derive
    def VPD(self):
        #TODO: use Weather directly, instead of through PhotosyntheticLeaf
        return self.weather.VPD

    @derive
    def gs(self):
        return self.leaf.stomata.stomatal_conductance
예제 #5
0
class Radiation(System):
    sun = system()

    #FIXME: chance to remove ref here? only LAI is used
    photosynthesis = system()

    # cumulative LAI at the layer
    @drive(alias='LAI', unit='cm^2 / m^2')
    def leaf_area_index(self):
        return self.photosynthesis

    @parameter
    def leaf_angle(self):
        return LeafAngle.ellipsoidal

    # ratio of horizontal to vertical axis of an ellipsoid
    @parameter(alias='LAF')
    def leaf_angle_factor(self):
        #return 1
        # leaf angle factor for corn leaves, Campbell and Norman (1998)
        #return 1.37
        # leaf angle factor for garlic canopy, from Rizzalli et al. (2002),  X factor in Campbell and Norman (1998)
        return 0.7

    @parameter
    def wave_band(self):
        return WaveBand.photothetically_active_radiation

    # scattering coefficient (reflectance + transmittance)
    @parameter(alias='s')
    def scattering(self):
        return 0.15

    # clumping index
    @parameter
    def clumping(self):
        return 1

    #r_h=0.05, #FIXME reflectance?

    # Forward from Sun

    @derive(unit='deg')
    def current_zenith_angle(self):
        return self.sun.zenith_angle

    @derive(alias='I0_dr', unit='umol/m^2/s Quanta')
    def directional_photosynthetic_radiation(self):
        return self.sun.directional_photosynthetic_radiation

    @derive(alias='I0_df', unit='umol/m^2/s Quanta')
    def diffusive_photosynthetic_radiation(self):
        return self.sun.diffusive_photosynthetic_radiation

    # Leaf angle stuff?

    #TODO better name?
    @derive
    def leaf_angle_coeff(self, zenith_angle):
        elevation_angle = U(90, 'deg') - zenith_angle
        #FIXME need to prevent zero like sin_beta / cot_beta?
        a = elevation_angle.to('rad')
        t = zenith_angle.to('rad')
        # leaf angle distribution parameter
        x = self.leaf_angle_factor
        return {
            # When Lt accounts for total path length, division by sin(elev) isn't necessary
            LeafAngle.spherical:
            1 / (2 * sin(a)),
            LeafAngle.horizontal:
            1,
            LeafAngle.vertical:
            1 / (tan(a) * pi / 2),
            LeafAngle.empirical:
            0.667,
            LeafAngle.diaheliotropic:
            1 / sin(a),
            LeafAngle.ellipsoidal:
            sqrt(x**2 + tan(t)**2) / (x + 1.774 * (x + 1.182)**-0.733),
        }[self.leaf_angle]

    #TODO make it @property if arg is not needed
    # Kb: Campbell, p 253, Ratio of projected area to hemi-surface area for an ellisoid
    #TODO rename to extinction_coeff?
    # extiction coefficient assuming spherical leaf dist
    @derive
    def projection_ratio_at(self, zenith_angle):
        return self.leaf_angle_coeff(zenith_angle) * self.clumping

    @derive(alias='Kb')
    def projection_ratio(self, current_zenith_angle):
        return self.projection_ratio_at(current_zenith_angle)

    # diffused light ratio to ambient, itegrated over all incident angles from -90 to 90
    @constant(unit='rad')
    def _angles(self):
        return array([(pi / 4) * (g + 1) for g in GAUSS3])

    # diffused fraction (fdf)
    #FIXME name it
    @derive
    def _F(self, x, angles):
        # Why multiplied by 2?
        return ((pi / 4) * (2 * x * sin(angles) * cos(angles)) * WEIGHT3).sum()

    # Kd: K for diffuse light, the same literature as above
    @derive(alias='Kd')
    def diffusion_ratio(self, LAI):
        angles = self._angles
        coeffs = array([self.leaf_angle_coeff(a) for a in angles])
        F = self._F(exp(-coeffs * LAI), angles)
        K = -log(F) / LAI
        return K * self.clumping

    ##############################
    # dePury and Farquhar (1997) #
    ##############################

    # Kb1: Kb prime in de Pury and Farquhar(1997)
    @derive(alias='Kb1')
    #TODO better name
    def projection_ratio_prime(self, Kb, s):
        return Kb * sqrt(1 - s)

    # Kd1: Kd prime in de Pury and Farquhar(1997)
    @derive(alias='Kd1')
    #TODO better name
    def diffusion_ratio_prime(self, Kd, s):
        return Kd * sqrt(1 - s)

    ################
    # Reflectivity #
    ################

    @derive
    #TODO better name?
    def reflectivity(self, rho_h, Kb, Kd):
        return rho_h * (2 * Kb / (Kb + Kd))

    # canopy reflection coefficients for beam horizontal leaves, beam uniform leaves, and diffuse radiations

    # rho_h: canopy reflectance of beam irradiance on horizontal leaves, de Pury and Farquhar (1997)
    # also see Campbell and Norman (1998) p 255 for further info on potential problems
    @derive(alias='rho_h')
    def canopy_reflectivity_horizontal_leaf(self, s):
        return (1 - sqrt(1 - s)) / (1 + sqrt(1 - s))

    #TODO make consistent interface with siblings
    # rho_cb: canopy reflectance of beam irradiance for uniform leaf angle distribution, de Pury and Farquhar (1997)
    @derive
    def canopy_reflectivity_uniform_leaf_at(self, zenith_angle):
        rho_h = self.canopy_reflectivity_horizontal_leaf
        Kb = self.projection_ratio_at(zenith_angle)
        rho_cb = 1 - exp(-2 * rho_h * Kb / (1 + Kb))
        return rho_cb

    @derive(alias='rho_cb')
    def canopy_reflectivity_uniform_leaf(self, current_zenith_angle):
        return self.canopy_reflectivity_uniform_leaf_at(current_zenith_angle)

    # rho_cd: canopy reflectance of diffuse irradiance, de Pury and Farquhar (1997) Table A2
    @derive(alias='rho_cd')
    def canopy_reflectivity_diffusion(self):
        if self.sun.diffusive_photosynthetic_radiation == 0:
            return 0
        angles = self._angles
        rhos = array(
            [self.canopy_reflectivity_uniform_leaf_at(a) for a in angles])
        # Probably the eqn A21 in de Pury is missing the integration terms of the angles??
        return self._F(rhos, angles)

    # rho_soil: soil reflectivity for PAR band
    @parameter
    def soil_reflectivity(self):
        return 0.10

    #######################
    # I_l?: dePury (1997) #
    #######################

    # I_lb: dePury (1997) eqn A3
    @derive(alias='I_lb', unit='umol/m^2/s Quanta')
    def irradiance_lb(self, L):
        I0_dr = self.sun.directional_photosynthetic_radiation
        rho_cb = self.canopy_reflectivity_uniform_leaf
        Kb1 = self.projection_ratio_prime
        return I0_dr * (1 - rho_cb) * Kb1 * exp(-Kb1 * L)

    # I_ld: dePury (1997) eqn A5
    @derive(alias='I_ld', unit='umol/m^2/s Quanta')
    def irradiance_ld(self, L):
        I0_df = self.sun.diffusive_photosynthetic_radiation
        rho_cb = self.canopy_reflectivity_uniform_leaf
        Kd1 = self.diffusion_ratio_prime
        return I0_df * (1 - rho_cb) * Kd1 * exp(-Kd1 * L)

    # I_l: dePury (1997) eqn A5
    @derive(alias='I_l', unit='umol/m^2/s Quanta')
    def irradiance_l(self, L):
        return self.irradiance_lb(L) + self.irradiance_ld(L)

    # I_lbSun: dePury (1997) eqn A5
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_l_sunlit(self, L):
        I0_dr = self.sun.directional_photosynthetic_radiation
        s = self.scattering
        Kb = self.projection_ratio
        I_lb_sunlit = I0_dr * (1 - s) * Kb
        I_l_sunlit = I_lb_sunlit + self.irradiance_l_shaded(L)
        return I_l_sunlit

    # I_lSH: dePury (1997) eqn A5
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_l_shaded(self, L):
        return self.irradiance_ld(L) + self.irradiance_lbs(L)

    # I_lbs: dePury (1997) eqn A5
    @derive(alias='I_lbs', unit='umol/m^2/s Quanta')
    def irradiance_lbs(self, L):
        I0_dr = self.sun.directional_photosynthetic_radiation
        rho_cb = self.canopy_reflectivity_uniform_leaf
        s = self.scattering
        Kb1 = self.projection_ratio_prime
        Kb = self.projection_ratio
        return I0_dr * ((1 - rho_cb) * Kb1 * exp(-Kb1 * L) -
                        (1 - s) * Kb * exp(-Kb * L))

    # I0tot: total irradiance at the top of the canopy,
    # passed over from either observed PAR or TSolar or TIrradiance
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_I0_tot(self):
        I0_dr = self.sun.directional_photosynthetic_radiation
        I0_df = self.sun.diffusive_photosynthetic_radiation
        return I0_dr + I0_df

    ########
    # I_c? #
    ########

    # I_tot, I_sun, I_shade: absorved irradiance integrated over LAI per ground area
    #FIXME: not used, but seems producing very low values, need to check equations

    # I_c: Total irradiance absorbed by the canopy, de Pury and Farquhar (1997)
    @derive(unit='umol/m^2/s Quanta')
    def canopy_irradiance(self, rho_cb, I0_dr, I0_df, Kb1, Kd1, LAI):
        #I_c = self.canopy_sunlit_irradiance + self.canopy_shaded_irradiance
        def I(I0, K):
            return (1 - rho_cb) * I0 * (1 - exp(-K * LAI))

        I_tot = I(I0_dr, Kb1) + I(I0_df, Kd1)
        return I_tot

    # I_cSun: The irradiance absorbed by the sunlit fraction, de Pury and Farquhar (1997)
    # should this be the same os Qsl? 03/02/08 SK
    @derive(unit='umol/m^2/s Quanta')
    def canopy_sunlit_irradiance(self, s, rho_cb, rho_cd, I0_dr, I0_df, Kb,
                                 Kb1, Kd1, LAI):
        I_c_sunlit = \
            I0_dr * (1 - s) * (1 - exp(-Kb * LAI)) + \
            I0_df * (1 - rho_cd) * (1 - exp(-(Kd1 + Kb) * LAI)) * Kd1 / (Kd1 + Kb) + \
            I0_dr * ((1 - rho_cb) * (1 - exp(-(Kb1 + Kb) * LAI)) * Kb1 / (Kb1 + Kb) - (1 - s) * (1 - exp(-2*Kb * LAI)) / 2)
        return I_c_sunlit

    # I_cSh: The irradiance absorbed by the shaded fraction, de Pury and Farquhar (1997)
    @derive(unit='umol/m^2/s Quanta')
    def canopy_shaded_irradiance(self):
        I_c = self.canopy_irradiance
        I_c_sunlit = self.canopy_sunlit_irradiance
        I_c_shaded = I_c - I_c_sunlit
        return I_c_shaded

    ######
    # Q? #
    ######

    # @derive
    # def sunlit_photon_flux_density(self):
    #     return self._sunlit_Q
    #
    # @derive
    # def shaded_photon_flux_density(self):
    #     return self._shaded_Q

    # Qtot: total irradiance (dir + dif) at depth L, simple empirical approach
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_tot(self, L):
        I0_tot = self.irradiance_I0_tot
        s = self.scattering
        Kb = self.projection_ratio
        Kd = self.diffusion_ratio
        Q_tot = I0_tot * exp(-sqrt(1 - s) * ((Kb + Kd) / 2) * L)
        return Q_tot

    # Qbt: total beam radiation at depth L
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_bt(self, L):
        I0_dr = self.sun.directional_photosynthetic_radiation
        s = self.scattering
        Kb = self.projection_ratio
        Q_bt = I0_dr * exp(-sqrt(1 - s) * Kb * L)
        return Q_bt

    # net diffuse flux at depth of L within canopy
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_d(self, L):
        I0_df = self.sun.diffusive_photosynthetic_radiation
        s = self.scattering
        Kd = self.diffusion_ratio
        Q_d = I0_df * exp(-sqrt(1 - s) * Kd * L)
        return Q_d

    # weighted average absorved diffuse flux over depth of L within canopy
    # accounting for exponential decay, Campbell p261
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_dm(self, LAI):
        if LAI > 0:
            # Integral Qd / Integral L
            I0_df = self.sun.diffusive_photosynthetic_radiation
            s = self.scattering
            Kd = self.diffusion_ratio
            Q_dm = I0_df * (1 - exp(-sqrt(1 - s) * Kd * LAI)) / (sqrt(1 - s) *
                                                                 Kd * LAI)
        else:
            Q_dm = 0
        return Q_dm

    # unintercepted beam (direct beam) flux at depth of L within canopy
    @derive(unit='umol/m^2/s Quanta')
    def irradinace_Q_b(self, L):
        I0_dr = self.sun.directional_photosynthetic_radiation
        Kb = self.projection_ratio
        Q_b = I0_dr * exp(-Kb * L)
        return Q_b

    # mean flux density on sunlit leaves
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_sunlit(self, I0_dr, Kb):
        return I0_dr * Kb + self.irradiance_Q_shaded

    # flux density on sunlit leaves at delpth L
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_sunlit_at(self, L, *, I0_dr, Kb):
        return I0_dr * Kb + self.irradiance_Q_shaded(L)

    # mean flux density on shaded leaves over LAI
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_shaded(self):
        # It does not include soil reflection
        return self.irradiance_Q_dm + self.irradiance_Q_scm

    # diffuse flux density on shaded leaves at depth L
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_shaded_at(self, L):
        # It does not include soil reflection
        return self.irradiance_Q_d(L) + self.irradiance_Q_sc(L)

    # weighted average of Soil reflectance over canopy accounting for exponential decay
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_soilm(self, LAI):
        if LAI > 0:
            # Integral Qd / Integral L
            rho_soil = self.soil_reflectivity
            s = self.scattering
            Kd = self.diffusion_ratio
            Q_soil = self.irradiance_Q_soil
            Q_soilm = Q_soil * rho_soil * (
                1 - exp(-sqrt(1 - s) * Kd * LAI)) / (sqrt(1 - s) * Kd * LAI)
        else:
            Q_soilm = 0
        return Q_soilm

    # weighted average scattered radiation within canopy
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_scm(self, LAI):
        if LAI > 0:
            # Integral Qd / Integral L
            rho_soil = self.soil_reflectivity
            s = self.scattering
            Kd = self.diffusion_ratio
            Q_soil = self.irradiance_Q_soil
            Q_soilm = Q_soil * rho_soil * (
                1 - exp(-sqrt(1 - s) * Kd * LAI)) / (sqrt(1 - s) * Kd * LAI)

            I0_dr = self.sun.directional_photosynthetic_radiation
            s = self.scattering
            Kb = self.projection_ratio

            # total beam including scattered absorbed by canopy
            #FIXME should the last part be multiplied by LAI like others?
            total_beam = I0_dr * (1 - exp(-sqrt(1 - s) * Kb * LAI)) / (
                sqrt(1 - s) * Kb)
            # non scattered beam absorbed by canopy
            nonscattered_beam = I0_dr * (1 - exp(-Kb * LAI)) / Kb
            Q_scm = (total_beam - nonscattered_beam) / LAI
            # Campbell and Norman (1998) p 261, Average between top (where scattering is 0) and bottom.
            #Q_scm = (self.irradiance_Q_bt(LAI) - self.irradiance_Q_b(LAI)) / 2
        else:
            Q_scm = 0
        return Q_scm

    # scattered radiation at depth L in the canopy
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_sc(self, L):
        Q_bt = self.irradiance_Q_bt(L)
        Q_b = self.irradiance_Q_b(L)
        # total beam - nonscattered beam at depth L
        return Q_bt - Q_b

    # total PFD at the soil sufrace under the canopy
    @derive(unit='umol/m^2/s Quanta')
    def irradiance_Q_soil(self, LAI):
        return self.irradiance_Q_tot(LAI)

    ###################
    # Leaf Area Index #
    ###################

    @constant(unit='deg')
    def sunlit_minimum_elevation_angle(self):
        return 5

    # sunlit LAI assuming closed canopy; thus not accurate for row or isolated canopy
    @derive(unit='cm^2 / m^2')
    def sunlit_leaf_area_index(self):
        if self.sun.elevation_angle <= self.sunlit_minimum_elevation_angle:
            return 0
        else:
            Kb = self.projection_ratio
            LAI = self.leaf_area_index
            return (1 - exp(-Kb * LAI)) / Kb

    # shaded LAI assuming closed canopy
    @derive(unit='cm^2 / m^2')
    def shaded_leaf_area_index(self):
        return self.leaf_area_index - self.sunlit_leaf_area_index

    # sunlit fraction of current layer
    @derive
    def sunlit_fraction(self, L):
        if self.sun.elevation_angle <= self.sunlit_minimum_elevation_angle:
            return 0
        else:
            Kb = self.projection_ratio
            return exp(-Kb * L)

    @derive
    def shaded_fraction(self, L):
        return 1 - self.sunlit_fraction(L)
예제 #6
0
class Sun(System):
    # conversion factor from W/m2 to PFD (umol m-2 s-1) for PAR waveband (median 550 nm of 400-700 nm) of solar radiation,
    # see Campbell and Norman (1994) p 149
    # 4.55 is a conversion factor from W to photons for solar radiation, Goudriaan and van Laar (1994)
    # some use 4.6 i.e., Amthor 1994, McCree 1981, Challa 1995.
    PHOTON_UMOL_PER_J = constant(4.6, unit='umol/J Quanta')

    # solar constant, Iqbal (1983)
    #FIXME better to be 1361 or 1362 W/m-2?
    SOLAR_CONSTANT = parameter(1370.0, alias='SC', unit='W/m^2')

    #TODO make Location external
    location = system(Location)
    weather = system()

    # @derive time? -- takes account different Julian day conventions (03-01 vs. 01-01)
    @derive(alias='t', init=None)
    def datetime(self):
        #FIXME: how to drive time variable?
        return self.context.datetime

    @derive(alias='d', init=None)
    def day(self, t):
        #FIXME: properly drive time
        return t.day

    @derive(alias='h', init=None, unit='hr')
    def hour(self, t):
        #FIXME: properly drive time
        return t.hour

    latitude = drive('location', alias='lat',
                     unit='deg')  # DO NOT convert to radians for consistency
    longitude = drive(
        'location', alias='long', unit='deg'
    )  # leave it as in degrees, used only once for solar noon calculation
    altitude = drive('location', alias='alt', unit='m')

    #TODO: fix inconsistent naming of PAR vs. PFD
    photosynthetic_active_radiation = drive(
        'weather', alias='PAR', key='PFD',
        unit='umol/m^2/s Quanta')  # unit='umol m-2 s-1'
    transmissivity = parameter(
        0.5, alias='tau'
    )  # atmospheric transmissivity, Goudriaan and van Laar (1994) p 30

    #####################
    # Solar Coordinates #
    #####################

    #HACK always use degrees for consistency and easy tracing
    @derive(alias='dec', unit='deg')
    def declination_angle(self):
        #FIXME pascal version of LightEnv uses iqbal()
        return self._declination_angle_spencer

    # Goudriaan 1977
    @derive(unit='deg')
    def _declination_angle_goudriaan(self, d):
        g = 2 * pi * (d + 10) / 365
        return -23.45 * cos(g)

    # Resenberg, blad, verma 1982
    @derive(unit='deg')
    def _declination_angle_resenberg(self, d):
        g = 2 * pi * (d - 172) / 365
        return 23.5 * cos(g)

    # Iqbal (1983) Pg 10 Eqn 1.3.3, and sundesign.com
    @derive(unit='deg')
    def _declination_angle_iqbal(self, d):
        g = 2 * pi * (d + 284) / 365
        return 23.45 * sin(g)

    # Campbell and Norman, p168
    @derive(unit='deg')
    def _declination_angle_campbell(self, d):
        a = radians(356.6 + 0.9856 * d)
        b = radians(278.97 + 0.9856 * d + 1.9165 * sin(a))
        r = arcsin(0.39785 * sin(b))
        return degrees(r)

    # Spencer equation, Iqbal (1983) Pg 7 Eqn 1.3.1. Most accurate among all
    @derive(unit='deg')
    def _declination_angle_spencer(self, d):
        # gamma: day angle
        g = 2 * pi * (d - 1) / 365
        r = 0.006918 - 0.399912 * cos(g) + 0.070257 * sin(g) - 0.006758 * cos(
            2 * g) + 0.000907 * sin(2 * g) - 0.002697 * cos(
                3 * g) + 0.00148 * sin(3 * g)
        return degrees(r)

    @constant(alias='dph', unit='deg/hr')  # (unit='degree')
    def degree_per_hour(self):
        return 360 / 24

    # LC is longitude correction for Light noon, Wohlfart et al, 2000; Campbell & Norman 1998
    @derive(alias='LC', unit='hr')
    def longitude_correction(self, long, dph):
        # standard meridian for pacific time zone is 120 W, Eastern Time zone : 75W
        # LC is positive if local meridian is east of standard meridian, i.e., 76E is east of 75E
        #standard_meridian = -120
        meridian = round(long / dph) * dph
        #FIXME use standard longitude sign convention
        #return (long - meridian) / dph
        #HACK this assumes inverted longitude sign that MAIZSIM uses
        return (meridian - long) / dph

    @derive(alias='ET', unit='hr')
    def equation_of_time(self, d):
        f = radians(279.575 + 0.9856 * d)
        return (-104.7*sin(f) + 596.2*sin(2*f) + 4.3*sin(3*f) - 12.7*sin(4*f) \
                -429.3*cos(f) - 2.0*cos(2*f) + 19.3*cos(3*f)) / (60 * 60)

    @derive(unit='hr')
    def solar_noon(self, LC, ET):
        return U(12, 'hr') - LC - ET

    @derive
    def _cos_hour_angle(self, angle, latitude, declination_angle):
        # this value should never become negative because -90 <= latitude <= 90 and -23.45 < decl < 23.45
        #HACK is this really needed for crop models?
        # preventing division by zero for N and S poles
        #denom = fmax(denom, 0.0001)
        # sunrise/sunset hour angle
        #TODO need to deal with lat_bound to prevent tan(90)?
        #lat_bound = radians(68)? radians(85)?
        # cos(h0) at cos(theta_s) = 0 (solar zenith angle = 90 deg == elevation angle = 0 deg)
        #return -tan(latitude) * tan(declination_angle)
        w_s = angle.to('rad')  # zenith angle
        p = latitude.to('rad')
        d = declination_angle.to('rad')
        return (cos(w_s) - sin(p) * sin(d)) / (cos(p) * cos(d))

    @derive(unit='deg')
    def hour_angle_at_horizon(self):
        c = self._cos_hour_angle(angle=U(90, 'deg'))
        # in the polar region during the winter, sun does not rise
        if c > 1:
            return 0
        # white nights during the summer in the polar region
        elif c < -1:
            return 180
        else:
            return degrees(arccos(c))

    @derive(unit='hr')
    def half_day_length(self):
        # from Iqbal (1983) p 16
        return self.hour_angle_at_horizon / self.degree_per_hour

    @derive(unit='hr')
    def day_length(self):
        return self.half_day_length * 2

    @derive(unit='hr')
    def sunrise(self):
        return self.solar_noon - self.half_day_length

    @derive(unit='hr')
    def sunset(self):
        return self.solar_noon + self.half_day_length

    @derive(unit='deg')
    def hour_angle(self):
        return (self.hour - self.solar_noon) * self.degree_per_hour

    @derive(unit='deg')
    def elevation_angle(self, hour_angle, declination_angle, latitude):
        #FIXME When time gets the same as solarnoon, this function fails. 3/11/01 ??
        h = hour_angle.to('rad')
        p = latitude.to('rad')
        d = declination_angle.to('rad')
        r = arcsin(cos(h) * cos(d) * cos(p) + sin(d) * sin(p))
        #return degrees(r)
        return r

    @derive(unit='deg')
    def zenith_angle(self):
        return 90 - self.elevation_angle

    # The solar azimuth angle is the angular distance between due South and the
    # projection of the line of sight to the sun on the ground.
    # View point from south, morning: +, afternoon: -

# See An introduction to solar radiation by Iqbal (1983) p 15-16
# Also see http://www.susdesign.com/sunangle/index.html

    @derive(unit='deg')
    def azimuth_angle(self, elevation_angle, declination_angle, latitude):
        d = declination_angle.to('rad')
        t_s = elevation_angle.to('rad')
        p = latitude.to('rad')
        r = arccos((sin(d) - sin(t_s) * sin(p)) / (cos(t_s) * cos(p)))
        #return degrees(abs(r))
        return abs(r)

    ###################
    # Solar Radiation #
    ###################

    # atmospheric pressure in kPa
    @derive(unit='kPa')
    def atmospheric_pressure(self, altitude):
        try:
            # campbell and Norman (1998), p 41
            return 101.3 * exp(-U.magnitude(altitude, 'm') / 8200)
        except:
            return 100

    @derive(unit='')
    def optical_air_mass_number(self, elevation_angle):
        t_s = clip(elevation_angle, lower=0, unit='rad')
        #FIXME need to do max(0.0001, sin(t_s))?
        try:
            #FIXME check 101.3 is indeed in kPa
            return self.atmospheric_pressure / (U(101.3, 'kPa') * sin(t_s))
        except:
            return 0

    @derive(unit='W/m^2')
    # Campbell and Norman's global solar radiation, this approach is used here
    #TODO rename to insolation? (W/m2)
    def solar_radiation(self, elevation_angle, day, SC):
        t_s = clip(elevation_angle, lower=0, unit='rad')
        g = 2 * pi * (day - 10) / 365
        return SC * sin(t_s) * (1 + 0.033 * cos(g))

    @derive(unit='W/m^2')
    def directional_solar_radiation(self):
        return self.directional_coeff * self.solar_radiation

    @derive(unit='W/m^2')
    def diffusive_solar_radiation(self):
        return self.diffusive_coeff * self.solar_radiation

    @derive(unit='')
    def directional_coeff(self, transmissivity, optical_air_mass_number):
        # Goudriaan and van Laar's global solar radiation
        def goudriaan(tau):
            #FIXME should be goudriaan() version
            return tau * (1 - self.diffusive_coeff)

        # Takakura (1993), p 5.11
        def takakura(tau, m):
            return tau**m

        # Campbell and Norman (1998), p 173
        def campbell(tau, m):
            return tau**m

        return campbell(transmissivity, optical_air_mass_number)

    # Fdif: Fraction of diffused light
    @derive(unit='')
    def diffusive_coeff(self, transmissivity, optical_air_mass_number):
        # Goudriaan and van Laar's global solar radiation
        def goudriaan(tau):
            # clear sky : 20% diffuse
            if tau >= 0.7:
                return 0.2
            # cloudy sky: 100% diffuse
            elif tau <= 0.3:
                return 1
            # inbetween
            else:
                return 1.6 - 2 * tau

        # Takakura (1993), p 5.11
        def takakura(tau, m):
            return (1 - tau**m) / (1 - 1.4 * log(tau)) / 2

        # Campbell and Norman (1998), p 173
        def campbell(tau, m):
            return (1 - tau**m) * 0.3

        return campbell(transmissivity, optical_air_mass_number)

    @derive(unit='')
    def directional_fraction(self):
        try:
            return 1 / (1 + self.diffusive_coeff / self.directional_coeff)
        except:
            return 0

    @derive(unit='')
    def diffusive_fraction(self):
        try:
            return 1 / (1 + self.directional_coeff / self.diffusive_coeff)
        except:
            return 0

    # PARfr
    @derive(unit='')
    #TODO better naming: extinction? transmitted_fraction?
    def photosynthetic_coeff(self, transmissivity):
        #if self.elevation_angle <= 0:
        #    return 0
        #TODO: implement Weiss and Norman (1985), 3/16/05
        def weiss():
            pass

        # Goudriaan and van Laar (1994)
        def goudriaan(tau):
            # clear sky (tau >= 0.7): 45% is PAR
            if tau >= 0.7:
                return 0.45
            # cloudy sky (<= 0.3): 55% is PAR
            elif tau <= 0.3:
                return 0.55
            else:
                return 0.625 - 0.25 * tau

        return goudriaan(transmissivity)

    # PARtot: total PAR (umol m-2 s-1) on horizontal surface (PFD)
    @derive(alias='PARtot', unit='umol/m^2/s Quanta')
    def photosynthetic_active_radiation_total(self, PAR):
        if self.PAR is not None:
            return self.PAR
        else:
            return self.solar_radiation * self.photosynthetic_coeff * self.PHOTON_UMOL_PER_J

    # PARdir
    @derive(unit='umol/m^2/s Quanta')
    def directional_photosynthetic_radiation(self, PARtot):
        return self.directional_fraction * PARtot

    # PARdif
    @derive(unit='umol/m^2/s Quanta')
    def diffusive_photosynthetic_radiation(self, PARtot):
        return self.diffusive_fraction * PARtot
예제 #7
0
파일: trait.py 프로젝트: tomyun/cropbox
class Trait(System):
    plant = system(alias='p')
예제 #8
0
파일: phenology.py 프로젝트: tomyun/cropbox
class Phenology(System):
    weather = system(Weather)
    soil = system(Soil)

    @parameter
    def storage_days(self):
        pass

    @parameter
    def initial_leaf_number_at_harvest(self):
        return 4

    @parameter(alias='R_max_LIR')
    def maximum_leaf_initiation_rate(self):
        pass

    @parameter(alias='R_max_LTAR')
    def maximum_leaf_tip_appearance_rate(self):
        pass

    @parameter(alias='T_opt')
    def optimal_temperature(self):
        return 22.28

    @parameter(alias='T_ceil')
    def ceiling_temperature(self):
        return 34.23

    @parameter
    def emergence_date(self):
        pass

    @parameter
    def critical_photoperiod(self):
        pass

    @parameter
    def scape_removal_date(self):
        pass

    # def setup(self):
    #     # mean growing season temperature since germination, SK 1-19-12
    #     self.gst_recorder = gstr = GstRecorder(self)
    #     self.gdd_recorder = gddr = GddRecorder(self)
    #     self.gti_recorder = gtir = GtiRecorder(self)

    #     self.germination = g = Germination(self)
    #     self.emergence = e = Emergence(self, R_max=R_max_LTAR, T_opt=T_opt, T_ceil=T_ceil, emergence_date=emergence_date)
    #     self.leaf_initiation = li = LeafInitiationWithStorage(self, initial_leaves_at_harvest=initial_leaf_number_at_harvest, R_max=R_max_LIR, T_opt=T_opt, T_ceil=T_ceil, storage_days=storage_days)
    #     self.leaf_appearance = la = LeafAppearance(self, R_max=R_max_LTAR, T_opt=T_opt, T_ceil=T_ceil)
    #     self.floral_initiation = fi = FloralInitiation(self, critical_photoperiod=critical_photoperiod)
    #     self.bulbing = bi = Bulbing(self)
    #     self.scape = s = Scape(self, R_max=R_max_LTAR, T_opt=T_opt, T_ceil=T_ceil)
    #     self.scape_appearance = sa = ScapeAppearance(self, s)
    #     self.scape_removal = sr = ScapeRemoval(self, s, scape_removal_date=scape_removal_date)
    #     self.flowering = f = Flowering(self, s)
    #     self.bulbiling = b = Bulbiling(self, s)
    #     self.death = d = Death(self)

    #     self.stages = [
    #         gstr, gddr, gtir,
    #         g, e, li, la, fi, bi, s, sa, sr, f, b, d,
    #     ]

    # def update(self, t):
    #     #queue = self._queue()
    #     [s.update(t) for s in self.stages if s.ready]

    #     #FIXME remove finish() for simplicity
    #     [s.finish() for s in self.stages if s.over]

    #     self.stages = [s for s in self.stages if not s.over]

    # #TODO some methods for event records? or save them in Stage objects?
    # #def record(self, ...):
    # #    pass

    germination = system(Germination, phenology='self')
    emergence = system(Emergence, phenology='self')
    leaf_initiation = system(LeafInitiationWithStorage, phenology='self')
    leaf_appearance = system(LeafAppearance, phenology='self')
    floral_initiation = system(FloralInitiation, phenology='self')
    bulbing = system(Bulbing, phenology='self')
    scape = system(Scape, phenology='self')
    scape_appearance = system(ScapeAppearance, phenology='self', scape='scape')
    scape_removal = system(ScapeRemoval, phenology='self', scape='scape')
    flowering = system(Flowering, phenology='self', scape='scape')
    bulbiling = system(Bulbiling, phenology='self', scape='scape')
    death = system(Death, phenology='self')

    ############
    # Accessor #
    ############

    @derive
    def leaves_potential(self):
        return max(self.leaves_generic, self.leaves_total)

    @parameter
    def leaves_generic(self):
        return 10

    @derive
    def leaves_total(self):
        return self.leaves_initiated

    @derive
    def leaves_initiated(self):
        return self.leaf_initiation.leaves

    @derive
    def leaves_appeared(self):
        return self.leaf_appearance.leaves

    @derive(alias='T')
    def temperature(self):
        if self.leaves_appeared < 9:
            #FIXME soil module is not implemented yet
            #T = self.soil.T_soil
            #HACK garlic model does not use soil temperature
            T = self.weather.T_air
        else:
            T = self.weather.T_air
        #FIXME T_cur doesn't go below zero, but is it fair assumption?
        return T if T > 0 else 0

    # @derive
    # def growing_temperature(self):
    #     return self.gst_recorder.rate

    # common

    @flag
    def germinating(self):
        return self.germination.ing

    @flag
    def germinated(self):
        return self.germination.over

    @flag
    def emerging(self):
        return self.emergence.ing

    @flag
    def emerged(self):
        return self.emergence.over

    # garlic

    @flag
    def floral_initiated(self):
        return self.floral_initiation.over

    @flag
    def scaping(self):
        return self.scape.ing

    @flag
    def scape_appeared(self):
        return self.scape_appearance.over

    @flag
    def scape_removed(self):
        return self.scape_removal.over

    @flag
    def flowered(self):
        return self.flowering.over

    @flag
    def bulb_maturing(self):
        #FIXME clear definition of bulb maturing
        return self.scape_removed or self.bulbiling.over

    # common

    @flag
    def dead(self):
        return self.death.over
예제 #9
0
class PhotosyntheticLeaf(System):
    weather = system()
    soil = system()

    @system(leaf='self')
    def stomata(self):
        return Stomata

    photosynthesis = system(C4, leaf='self')  # for maize
    #photosynthesis = system(C3, leaf='self') # for garlic

    #TODO organize leaf properties like water (LWP), nitrogen content?
    #TODO introduce a leaf geomtery class for leaf_width
    #TODO introduce a soil class for ET_supply

    ###########
    # Drivers #
    ###########

    # static properties
    nitrogen = parameter(2.0)

    # geometry
    width = parameter(10 / 100, unit='m')  # meters

    # soil?
    ET_supply = parameter(
        0, unit='mol/m^2/s H2O')  # actual water uptake rate (mol H2O m-2 s-1)

    # dynamic properties

    # mesophyll CO2 partial pressure, ubar, one may use the same value as Ci assuming infinite mesohpyle conductance
    @derive(alias='Cm,Ci', unit='umol/mol CO2')
    def co2_mesophyll(self, A_net, w='weather', rvc='stomata.rvc'):
        P = w.P_air / U(100, 'kPa')
        Ca = w.CO2 * P  # conversion to partial pressure
        Cm = Ca - A_net * rvc * P
        #print(f"+ Cm = {Cm}, Ca = {Ca}, A_net = {A_net}, gs = {self.stomata.gs}, gb = {self.stomata.gb}, rvc = {rvc}, P = {P}")
        return clip(Cm, 0, 2 * Ca)
        #return Cm

    #FIXME is it right place? maybe need coordination with geometry object in the future
    @derive(alias='I2', unit='umol/m^2/s Quanta')
    def light(self):
        #FIXME make scatt global parameter?
        scatt = 0.15  # leaf reflectance + transmittance
        f = 0.15  # spectral correction

        Ia = self.weather.PPFD * (1 - scatt)  # absorbed irradiance
        I2 = Ia * (1 - f) / 2  # useful light absorbed by PSII
        return I2

    @optimize(alias='A_net', unit='umol/m^2/s CO2')
    def net_photosynthesis(self):
        #I2 = self.light
        A_net0 = self.A_net
        #print(f"A_net0 = {A_net0}")
        #Cm0 = self.co2_mesophyll
        A_net1 = self.photosynthesis.net_photosynthesis
        #Cm1 = co2_mesophyll(A_net1)
        #print(f"- I2 = {I2}, Cm0 = {Cm0}, T_leaf = {T_leaf}, A_net0 = {A_net0}, A_net1 = {A_net1}, Cm1 = {Cm1}")
        #             return A_net1
        return (A_net1 - A_net0)**2

    @derive(alias='Rd', unit='umol/m^2/s O2')
    def dark_respiration(self):
        return self.photosynthesis.dark_respiration

    @derive(alias='A_gross', unit='umol/m^2/s CO2')
    def gross_photosynthesis(self):
        return clip(
            self.A_net + self.Rd, lower=0
        )  # gets negative when PFD = 0, Rd needs to be examined, 10/25/04, SK

    @derive(alias='gs', unit='umol/m^2/s H2O')
    def stomatal_conductance(self):
        return self.stomata.stomatal_conductance

    #TODO: use @optimize
    @derive(unit='delta_degC')
    def temperature_adjustment(
            self,
            w='weather',
            s='stomata',
            # see Campbell and Norman (1998) pp 224-225
            # because Stefan-Boltzman constant is for unit surface area by denifition,
            # all terms including sbc are multilplied by 2 (i.e., gr, thermal radiation)
            lamda=U(44.0, 'kJ/mol'),  # KJ mole-1 at 25oC
            Cp=U(
                29.3, 'J/mol/degC'
            ),  # thermodynamic psychrometer constant and specific heat of air (J mol-1 C-1)
            epsilon=0.97,
            sbc=U(5.6697e-8,
                  'J/m^2/s/degK^4'),  # Stefan-Boltzmann constant (W m-2 K-4)
    ):
        T_air = w.T_air
        Tk = T_air.to('degK')
        PFD = w.PPFD
        P_air = w.P_air
        Jw = self.ET_supply

        gha = s.boundary_layer_conductance * (
            0.135 / 0.147
        )  # heat conductance, gha = 1.4*.135*sqrt(u/d), u is the wind speed in m/s} Mol m-2 s-1 ?
        gv = s.total_conductance_h2o
        gr = 4 * epsilon * sbc * Tk**3 / Cp * 2  # radiative conductance, 2 account for both sides
        ghr = gha + gr
        thermal_air = epsilon * sbc * Tk**4 * 2  # emitted thermal radiation
        psc = Cp / lamda  # psychrometric constant (C-1)
        psc1 = psc * ghr / gv  # apparent psychrometer constant

        PAR = U(PFD.to('umol/m^2/s Quanta').magnitude / 4.55,
                'J/m^2/s')  # W m-2
        # If total solar radiation unavailable, assume NIR the same energy as PAR waveband
        NIR = PAR
        scatt = 0.15
        # shortwave radiation (PAR (=0.85) + NIR (=0.15) solar radiation absorptivity of leaves: =~ 0.5
        # times 2 for projected area basis
        R_abs = (1 - scatt) * PAR + scatt * NIR + 2 * (epsilon * sbc * Tk**4)

        # debug dt I commented out the changes that yang made for leaf temperature for a test. I don't think they work
        if Jw == 0:
            # (R_abs - thermal_air - lamda * gv * w.VPD / P_air) / (Cp * ghr + lamda * w.saturation_slope * gv) # eqn 14.6a
            # eqn 14.6b linearized form using first order approximation of Taylor series
            return (psc1 /
                    (w.saturation_slope + psc1)) * ((R_abs - thermal_air) /
                                                    (ghr * Cp) - w.VPD /
                                                    (psc1 * P_air))
        else:
            return (R_abs - thermal_air - lamda * Jw) / (Cp * ghr)

    @derive(alias='T', unit='degC')
    def temperature(self):
        T_air = self.weather.T_air
        T_leaf = T_air + self.temperature_adjustment
        return T_leaf

    #TODO: expand @optimize decorator to support both cost function and variable definition
    # @temperature.optimize or minimize?
    # def temperature(self):
    #     return (self.temperature - self.new_temperature)**2

    @derive(alias='ET', unit='umol/m^2/s H2O')
    def evapotranspiration(self, vp='weather.vp'):
        gv = self.stomata.total_conductance_h2o
        ea = vp.ambient(self.weather.T_air, self.weather.RH)
        es_leaf = vp.saturation(self.temperature)
        ET = gv * ((es_leaf - ea) /
                   self.weather.P_air) / (1 -
                                          (es_leaf + ea) / self.weather.P_air)
        return clip(
            ET,
            lower=0)  # 04/27/2011 dt took out the 1000 everything is moles now
예제 #10
0
class C4(System):
    #TODO: more robust interface to connect Systems (i.e. type check, automatic prop defines)
    leaf = system()

    @derive(alias='Cm', unit='umol/mol CO2')
    def co2_mesophyll(self):
        Cm = self.leaf.co2_mesophyll
        return clip(Cm, lower=0)

    @derive(alias='I2', unit='umol/m^2/s Quanta')
    def light(self):
        I2 = self.leaf.light
        return clip(I2, lower=0)

    @drive(alias='T', unit='degC')
    def temperature(self):
        return self.leaf

    nitrogen = drive('leaf', alias='N')

    ##############
    # Parameters #
    ##############

    # FIXME are they even used?
    # self.beta_ABA = 1.48e2 # Tardieu-Davies beta, Dewar (2002) Need the references !?
    # self.delta = -1.0
    # self.alpha_ABA = 1.0e-4
    # self.lambda_r = 4.0e-12 # Dewar's email
    # self.lambda_l = 1.0e-12
    # self.K_max = 6.67e-3 # max. xylem conductance (mol m-2 s-1 MPa-1) from root to leaf, Dewar (2002)

    gbs = parameter(
        0.003,
        unit='mol/m^2/s CO2')  # bundle sheath conductance to CO2, mol m-2 s-1
    # gi = parameter(1.0) # conductance to CO2 from intercelluar to mesophyle, mol m-2 s-1, assumed

    # Arrhenius equation
    @derive(alias='T_dep')
    def temperature_dependence_rate(self, Ea, T, Tb=U(25, 'degC')):
        R = U(8.314, 'J/K/mol')  # universal gas constant (J K-1 mol-1)
        #HACK handle too low temperature values during optimization
        Tk = clip(T, lower=0, unit='degK')
        Tbk = clip(Tb, lower=0, unit='degK')
        try:
            return np.exp(Ea * (T - Tb) / (Tbk * R * Tk))
        except ZeroDivisionError:
            return 0

    @derive(alias='N_dep')
    def nitrogen_limited_rate(self, N, s=2.9, N0=0.25):
        return 2 / (1 + np.exp(-s * (max(N0, N) - N0))) - 1

    # Rd25: Values in Kim (2006) are for 31C, and the values here are normalized for 25C. SK
    @derive(alias='Rd')
    def dark_respiration(self,
                         T_dep,
                         Rd25=U(2, 'umol/m^2/s O2'),
                         Ear=U(39800, 'J/mol')):
        return Rd25 * T_dep(Ear)

    @derive
    def Rm(self, Rd):
        return 0.5 * Rd

    @derive(alias='Jmax', unit='umol/m^2/s Electron')
    def maximum_electron_transport_rate(self,
                                        T,
                                        T_dep,
                                        N_dep,
                                        Jm25=U(300, 'umol/m^2/s Electron'),
                                        Eaj=U(32800, 'J/mol'),
                                        Sj=U(702.6, 'J/mol/degK'),
                                        Hj=U(220000, 'J/mol')):
        R = U(8.314, 'J/K/mol')

        Tb = U(25, 'degC')
        Tk = T.to('degK')
        Tbk = Tb.to('degK')

        r = Jm25 * N_dep \
                 * T_dep(Eaj) \
                 * (1 + np.exp((Sj*Tbk - Hj) / (R*Tbk))) \
                 / (1 + np.exp((Sj*Tk  - Hj) / (R*Tk)))
        return clip(r, lower=0)

    @parameter(unit='mbar')
    def Om(self):
        # mesophyll O2 partial pressure
        O = 210  # gas units are mbar
        return O

    # Kp25: Michaelis constant for PEP caboxylase for CO2
    @derive(unit='ubar')
    def Kp(self, Kp25=U(80, 'ubar')):
        return Kp25  # T dependence yet to be determined

    # Kc25: Michaelis constant of rubisco for CO2 of C4 plants (2.5 times that of tobacco), ubar, Von Caemmerer 2000
    @derive(unit='ubar')
    def Kc(self, T_dep, Kc25=U(650, 'ubar'), Eac=U(59400, 'J/mol')):
        return Kc25 * T_dep(Eac)

    # Ko25: Michaelis constant of rubisco for O2 (2.5 times C3), mbar
    @derive(unit='mbar')
    def Ko(self, T_dep, Ko25=U(450, 'mbar'), Eao=U(36000, 'J/mol')):
        return Ko25 * T_dep(Eao)

    @derive(unit='ubar')
    def Km(self, Kc, Om, Ko):
        # effective M-M constant for Kc in the presence of O2
        return Kc * (1 + Om / Ko)

    @derive(unit='umol/m^2/s CO2')
    def Vpmax(self,
              N_dep,
              T_dep,
              Vpm25=U(70, 'umol/m^2/s CO2'),
              EaVp=U(75100, 'J/mol')):
        return Vpm25 * N_dep * T_dep(EaVp)

    @derive(unit='umol/m^2/s CO2')
    def Vp(self, Vpmax, Cm, Kp):
        # PEP carboxylation rate, that is the rate of C4 acid generation
        Vp = (Cm * Vpmax) / (Cm + Kp / U(1, 'atm'))
        Vpr = U(80, 'umol/m^2/s CO2'
                )  # PEP regeneration limited Vp, value adopted from vC book
        Vp = clip(Vp, 0, Vpr)
        return Vp

    # EaVc: Sage (2002) JXB
    @derive(unit='umol/m^2/s CO2')
    def Vcmax(self,
              N_dep,
              T_dep,
              Vcm25=U(50, 'umol/m^2/s CO2'),
              EaVc=U(55900, 'J/mol')):
        return Vcm25 * N_dep * T_dep(EaVc)

    @derive(alias='Ac', unit='umol/m^2/s CO2')
    def enzyme_limited_photosynthesis_rate(self, Vp, gbs, Cm, Rm, Vcmax, Rd):
        # Enzyme limited A (Rubisco or PEP carboxylation)
        Ac1 = Vp + gbs * Cm - Rm
        #Ac1 = max(0, Ac1) # prevent Ac1 from being negative Yang 9/26/06
        Ac2 = Vcmax - Rd
        #print(f'Ac1 = {Ac1}, Ac2 = {Ac2}')
        Ac = min(Ac1, Ac2)
        return Ac

    # Light and electron transport limited A mediated by J
    # theta: sharpness of transition from light limitation to light saturation
    # x: Partitioning factor of J, yield maximal J at this value
    @derive(alias='Aj', unit='umol/m^2/s CO2')
    def transport_limited_photosynthesis_rate(self,
                                              T,
                                              Jmax,
                                              Rd,
                                              Rm,
                                              I2,
                                              gbs,
                                              Cm,
                                              theta=0.5,
                                              x=0.4):
        J = quadratic_solve_lower(theta, -(I2 + Jmax), I2 * Jmax)
        #print(f'Jmax = {Jmax}, J = {J}')
        Aj1 = x * J / 2 - Rm + gbs * Cm
        Aj2 = (1 - x) * J / 3 - Rd
        Aj = min(Aj1, Aj2)
        return Aj

    @derive(alias='A_net', unit='umol/m^2/s CO2')
    def net_photosynthesis(self, Ac, Aj, beta=0.99):
        # smooting the transition between Ac and Aj
        A_net = ((Ac + Aj) -
                 ((Ac + Aj)**2 - 4 * beta * Ac * Aj)**0.5) / (2 * beta)
        #print(f'Ac = {Ac}, Aj = {Aj}, A_net = {A_net}')
        return A_net

    #FIXME: currently not used variables

    # alpha: fraction of PSII activity in the bundle sheath cell, very low for NADP-ME types
    @derive(alias='Os', unit='mbar')
    def bundle_sheath_o2(self, A_net, gbs, Om, alpha=0.0001):
        return alpha * A_net / (0.047 * gbs) * U(
            1, 'atm') + Om  # Bundle sheath O2 partial pressure, mbar

    @derive(alias='Cbs', unit='ubar')
    def bundle_sheath_co2(self, A_net, Vp, Cm, Rm, gbs):
        return Cm + (Vp - A_net -
                     Rm) / gbs  # Bundle sheath CO2 partial pressure, ubar

    @derive(unit='ubar')
    def gamma(self, Rd, Km, Vcmax, Os):
        # half the reciprocal of rubisco specificity, to account for O2 dependence of CO2 comp point,
        # note that this become the same as that in C3 model when multiplied by [O2]
        gamma1 = 0.193
        gamma_star = gamma1 * Os
        return (Rd * Km + Vcmax * gamma_star) / (Vcmax - Rd)
예제 #11
0
파일: plant.py 프로젝트: tomyun/cropbox
class Plant(System):
    weather = system(Weather)
    soil = system(Soil)

    phenology = system(Phenology, alias='pheno', plant='self')
    photosynthesis = system(Photosynthesis, plant='self')

    mass = system(Mass, plant='self')
    area = system(Area, plant='self')
    count = system(Count, plant='self')
    ratio = system(Ratio, plant='self')
    #carbon = system(Carbon, plant='self')
    #nitrogen = system(Nitrogen, plant='self')
    water = system(Water, plant='self')

    #TODO pass PRIMORDIA as initial_leaves
    primordia = parameter(5)

    bulb = system(None)
    scape = system(None)
    root = system(None)
    nodal_units = system([])

    #TODO find a better place?
    @parameter(unit='1/m^2')
    def planting_density(self):
        return 55

    @produce(target='nodal_units')
    def initiate_primordia(self):
        if len(self.nodal_units) == 0:
            return [(NodalUnit, {
                'plant': self,
                'rank': i + 1
            }) for i in range(self.primordia)]

    @produce(target='root')
    def initiate_root(self):
        if not self.root:
            if self.pheno.emerging:
                #TODO import_carbohydrate(self.soil.total_root_weight)
                return (Root, {'plant': self})

    @produce(target='nodal_units')
    def initiate_leaves(self):
        if not (self.pheno.germinating or self.pheno.dead):

            def f(i):
                try:
                    self.nodal_units[i]
                except IndexError:
                    return (NodalUnit, {'plant': self, 'rank': i + 1})
                else:
                    return None

            return [f(i) for i in range(self.pheno.leaves_initiated)]
예제 #12
0
class Leaf(Organ):
    nodal_unit = system()

    @derive
    def rank(self):
        return self.nodal_unit.rank

    # cm dd-1 Fournier and Andrieu 1998 Pg239.
    # This is the "potential" elongation rate with no water stress Yang
    # @property
    # def elongation_rate(self):
    #     return 0.564

    # max elongation rate (cm per day) at optipmal temperature
    # (Topt: 31C with Tbase = 9.8C using 0.564 cm/dd rate from Fournier 1998 paper above
    @parameter(alias='LER', unit='cm/day')
    def maximum_elongation_rate(self):
        return 12.0

    @parameter(alias='LM_min', unit='cm')
    def minimum_length_of_longest_leaf(self):
        return 60.0

    # leaf lamina width to length ratio
    @parameter
    def length_to_width_ratio(self):
        #return 0.106 # for maize
        return 0.05  # for garlic

    # leaf area coeff with respect to L*W (A_LW)
    @parameter
    def area_ratio(self):
        return 0.75

    # staygreen trait of the hybrid
    # stay green for this value times growth period after peaking before senescence begins
    # An analogy for this is that with no other stresses involved,
    # it takes 15 years to grow up, stays active for 60 years,
    # and age the last 15 year if it were for a 90 year life span creature.
    # Once fully grown, the clock works differently so that the hotter it is quicker it ages
    @parameter
    def stay_green(self):
        return 3.5

    @parameter(unit='cm/day')
    def aging_rate(self):
        return self.maximum_elongation_rate

    #############
    # Variables #
    #############

    #FIXME
    @derive
    def potential_leaves(self):
        return self.p.pheno.leaves_potential

    #FIXME
    @derive
    def extra_leaves(self):
        return self.p.pheno.leaves_potential - self.p.pheno.leaves_generic

    @derive(alias='maximum_length', unit='cm')
    def maximum_length_of_longest_leaf(self, LM_min, extra_leaves, k=0):
        # no length adjustment necessary for garlic, unlike MAIZE (KY, 2016-10-12)
        #k = 0 # 24.0
        return sqrt(LM_min**2 + k * extra_leaves)

    @derive(unit='cm')
    def maximum_width(self):
        # Fournier and Andrieu(1998) Pg242 YY
        return self.maximum_length * self.length_to_width_ratio

    @derive(unit='cm^2')
    def maximum_area(self):
        # daughtry and hollinger (1984) Fournier and Andrieu(1998) Pg242 YY
        return self.maximum_length * self.maximum_width * self.area_ratio

    @derive(unit='cm^2', nounit='l')
    def area_from_length(self, l):
        #HACK ensure zero area for zero length
        if l == 0:
            return 0
        else:
            # for garlic, see JH's thesis
            return 0.639945 + 0.954957 * l + 0.005920 * l**2

    @derive(unit='cm^2', nounit='length')
    def area_increase_from_length(self, length):
        # for garlic, see JH's thesis
        return 0.954957 + 2 * 0.005920 * length

    #TODO better name, shared by growth_duration and pontential_area
    #TODO should be a plant parameter not leaf (?)
    @derive
    def rank_effect(self, potential_leaves, rank, weight=1):
        l = potential_leaves
        n_m = 5.93 + 0.33 * l  # the rank of the largest leaf. YY
        a = (-10.61 + 0.25 * l) * weight
        b = (-5.99 + 0.27 * l) * weight
        # equation 7 in Fournier and Andrieu (1998). YY

        # equa 8(b)(Actually eqn 6? - eqn 8 deals with leaf age - DT)
        # in Fournier and Andrieu(1998). YY
        scale = rank / n_m - 1
        return exp(a * scale**2 + b * scale**3)

    @derive(unit='cm')
    def potential_length(self):
        # for MAIZSIM
        #return self.maximum_length * self.rank_effect(weight=0.5)
        # for beta fn calibrated from JH's thesis for SP and KM varieties, 8/10/15, SK
        #FIXME: leaves_potential is already max(leaves_generic, leaves_total)?
        n = max(self.p.pheno.leaves_potential, self.p.pheno.leaves_generic)
        l_t = 1.64 * n
        l_pk = 0.88 * n
        R_max = self.maximum_length
        return R_max * beta_thermal_func(T=self.rank, T_opt=l_pk, T_max=l_t)

    # from CLeaf::calc_dimensions()
    # LM_min is a length characteristic of the longest leaf,in Fournier and Andrieu 1998, it was 90 cm
    # LA_max is a fn of leaf no (Birch et al, 1998 fig 4) with largest reported value near 1000cm2. This is implemented as lfno_effect below, SK
    # LM_min of 115cm gives LA of largest leaf 1050cm2 when totalLeaves are 25 and Nt=Ng, SK 1-20-12
    # Without lfno_effect, it can be set to 97cm for the largest leaf area to be at 750 cm2 with Nt ~= Ng (Lmax*Wmax*0.75) based on Muchow, Sinclair, & Bennet (1990), SK 1-18-2012
    # Eventually, this needs to be a cultivar parameter and included in input file, SK 1-18-12
    # the unit of k is cm^2 (Fournier and Andrieu 1998 Pg239). YY
    # L_max is the length of the largest leaf when grown at T_peak. Here we assume LM_min is determined at growing Topt with minmal (generic) leaf no, SK 8/2011
    # If this routine runs before TI, totalLeaves = genericLeafNo, and needs to be run with each update until TI and total leaves are finalized, SK
    @derive(unit='day')
    def growth_duration(self):
        # shortest possible linear phase duration in physiological time (days instead of GDD) modified
        days = self.potential_length / self.maximum_elongation_rate
        # for garlic
        return 1.5 * days

    @derive
    def phase1_delay(self, rank):
        # not used in MAIZSIM because LTAR is used to initiate leaf growth.
        # Fournier's value : -5.16+1.94*rank;equa 11 Fournier and Andrieu(1998) YY, This is in plastochron unit
        return clip(-5.16 + 1.94 * rank, lower=0)

    @derive
    def leaf_number_effect(self, potential_leaves):
        # Fig 4 of Birch et al. (1998)
        return clip(exp(-1.17 + 0.047 * potential_leaves), 0.5, 1.0)

    @derive(unit='cm^2')
    def potential_area(self):
        # for MAIZSIM
        # equa 6. Fournier and Andrieu(1998) multiplied by Birch et al. (1998) leaf no effect
        # LA_max the area of the largest leaf
        # PotentialArea potential final area of a leaf with rank "n". YY
        #return self.maximum_area * self.leaf_number_effect * self.rank_effect(weight=1)
        # for garlic
        return self.area_from_length(self.potential_length)

    @derive
    def green_ratio(self):
        return 1 - self.senescence_ratio

    @derive(unit='cm^2')
    def green_area(self):
        return self.green_ratio * self.area

    @accumulate(unit='day')
    def elongation_age(self):  #TODO add td in the args
        #TODO implement Parent and Tardieu (2011, 2012) approach for leaf elongation in response to T and VPD, and normalized at 20C, SK, Nov 2012
        # elongAge indicates where it is now along the elongation stage or duration.
        # duration is determined by totallengh/maxElongRate which gives the shortest duration to reach full elongation in the unit of days.
        #FIXME no need to check here, as it will be compared against duration later anyways
        #return min(self._elongation_tracker.rate, self.growth_duration)
        if self.appeared and not self.mature:
            R_max = 1.0
            return R_max * beta_thermal_func(
                T=self.p.pheno.temperature,
                T_opt=self.p.pheno.optimal_temperature,
                T_max=self.p.pheno.ceiling_temperature)

    #TODO move to common module (i.e. Organ?)
    def _beta_growth(self, t, c_m, t_e, t_m=None, t_b=0, delta=1):
        #FIXME clipping necessary?
        #t = clip(t, 0., t_e)
        if not 0 <= t <= t_e:
            breakpoint()
        t_m = t_e / 2 if t_m is None else t_m
        t_et = t_e - t
        t_em = t_e - t_m
        t_tb = t - t_b
        t_mb = t_m - t_b
        return c_m * ((t_et / t_em) * (t_tb / t_mb)**(t_mb / t_em))**delta

    @derive(unit='cm/day')
    def potential_elongation_rate(self):
        if self.growing:
            #TODO proper integration with scipy.integrate
            return self._beta_growth(
                t=self.elongation_age,
                c_m=self.maximum_elongation_rate,
                t_e=self.growth_duration,
            )
        else:
            return 0

    @derive
    def _temperature_effect(self, T_grow, T_peak, T_base):
        # T_peak is the optimal growth temperature at which the potential leaf size determined in calc_mophology achieved.
        # Similar concept to fig 3 of Fournier and Andreiu (1998)

        # phyllochron corresponds to PHY in Lizaso (2003)
        # phyllochron needed for next leaf appearance in degree days (GDD8) - 08/16/11, SK.
        #phyllochron = (dv->get_T_Opt()- Tb)/(dv->get_Rmax_LTAR());

        T_ratio = (T_grow - T_base) / (T_peak - T_base)
        # final leaf size is adjusted by growth temperature determining cell size during elongation
        return clip(T_ratio * exp(1 - T_ratio), lower=0)

    @derive
    def temperature_effect(self):
        #return self._temperature_effect(T_grow=self.p.pheno.growing_temperature, T_peak=18.7, T_base=8.0) # for MAIZSIM
        #FIXME garlic model uses current temperature, not average growing temperature
        #return self._temperature_effect(T_grow=self.p.pheno.temperature, T_peak=self.p.pheno.optimal_temperature, T_base=0) # for garlic
        #FIXME garlic model does not actually use tempeature effect on final leaf size calculation
        return 1.0  # for garlic

    @derive(unit='cm^2/day')
    def potential_expansion_rate(self):
        t = self.elongation_age
        t_e = self.growth_duration  # 1.5 * w_max / c_m
        t = clip(t, upper=t_e)
        #FIXME can we introduce new w_max here when w_max in t_e (growth duration) supposed to be potential length?
        w_max = self.potential_area
        # c_m from Eq. 9, r (= dw/dt / c_m) from Eq. 7 of Yin (2003)
        #HACK can be more simplified
        #c_m = 1.5 / t_e * w_max
        #r = 4 * t * (t_e - t) / t_e**2
        t_m = t_e / 2
        c_m = (2 * t_e -
               t_m) / (t_e * (t_e - t_m)) * (t_m / t_e)**(t_m /
                                                          (t_e - t_m)) * w_max
        r = (t_e - t) / (t_e - t_m) * (t / t_m)**(t_m / (t_e - t_m))
        #FIXME dt here is physiological time, whereas timestep multiplied in potential_area_increase is chronological time
        return c_m * r  # dw/dt

    @derive(unit='cm^2')
    def potential_area_increase(self):
        ##area = max(0, water_effect * T_effect * self.potential_area * (1 + (t_e - self.elongation_age) / (t_e - t_m)) * (self.elongation_age / t_e)**(t_e / (t_e - t_m)))
        #maximum_expansion_rate = T_effect * self.potential_area * (2*t_e - t_m) / (t_e * (t_e - t_m)) * (t_m / t_e)**(t_m / (t_e - t_m))
        # potential leaf area increase without any limitations
        #return max(0, maximum_expansion_rate * max(0, (t_e - self.elongation_age) / (t_e - t_m) * (self.elongation_age / t_m)**(t_m / (t_e - t_m))) * self.timestep)
        if self.growing:
            # for MAIZSIM
            #return self.potential_expansion_rate * self.timestep
            # for garlic
            #TODO need common framework dealing with derivatives
            #return self.area_increase_from_length(self.actual_length_increase)
            return self.area_from_length(
                self.length + self.actual_length_increase) - self.area
        else:
            return 0

    # create a function which simulates the reducing in leaf expansion rate
    # when predawn leaf water potential decreases. Parameterization of rf_psil
    # and rf_sensitivity are done with the data from Boyer (1970) and Tanguilig et al (1987) YY
    @derive
    def _water_potential_effect(self, psi_predawn, threshold):
        #psi_predawn = self.p.soil.WP_leaf_predawn
        psi_th = threshold  # threshold wp below which stress effect shows up

        # DT Oct 10, 2012 changed this so it was not as sensitive to stress near -0.5 lwp
        # SK Sept 16, 2014 recalibrated/rescaled parameter estimates in Yang's paper. The scale of Boyer data wasn't set correctly
        # sensitivity = 1.92, LeafWPhalf = -1.86, the sensitivity parameter may be raised by 0.3 to 0.5 to make it less sensitivy at high LWP, SK
        s_f = 0.4258  # 0.5
        psi_f = -1.4251  # -1.0
        e = (1 + exp(s_f * psi_f)) / (1 + exp(s_f * (psi_f -
                                                     (psi_predawn - psi_th))))
        return clip(e, upper=1.0)

    @derive
    def water_potential_effect(self, threshold):
        # for MAIZSIM
        #return self._water_potential_effect(self.p.soil.WP_leaf_predawn, threshold)
        # for garlic
        return 1.0

    @derive
    def carbon_effect(self):
        return 1.0

    @accumulate(time='elongation_age', unit='cm')
    def length(self):
        #TODO: incorporate stress effects as done in actual_area_increase()
        return self.potential_elongation_rate

    @difference(time='elongation_age', unit='cm')
    def actual_length_increase(self):
        return self.potential_elongation_rate

    # actual area
    @derive(unit='cm^2')
    def area(self):
        # See Kim et al. (2012) Agro J. for more information on how this relationship has been derermined basned on multiple studies and is applicable across environments
        water_effect = self.water_potential_effect(-0.8657)

        # place holder
        carbon_effect = self.carbon_effect

        # growth temperature effect is now included here, outside of potential area increase calculation
        #TODO water and carbon effects are not multiplicative?
        return min(
            water_effect,
            carbon_effect) * self.temperature_effect * self.area_from_length(
                self.length)

    #TODO remove if unnecessary
    # @property
    # def actual_area_increase(self):
    #     #FIXME area increase tracking should be done by some gobal state tracking manager
    #     raise NotImplementedError("actual_area_increase")
    #
    # @property
    # def relative_area_increase(self):
    #     #HACK meaning changed from 'relative to other leaves' (spatial) to 'relative to previous state' (temporal)
    #     # adapted from CPlant::calcPerLeafRelativeAreaIncrease()
    #     #return self.potential_area_increase / self.nodal_unit.plant.area.potential_leaf_increase
    #     da = self.actual_area_increase
    #     a = self.area - da
    #     if a > 0:
    #         return da / a
    #     else:
    #         return 0

    @accumulate
    def stay_green_water_stress_duration(self, scale=0.5, threshold=-4.0):
        if self.mature:
            # One day of cumulative severe water stress (i.e., water_effect = 0.0 around -4MPa) would result in a reduction of leaf lifespan in relation staygreeness and growthDuration, SK
            # if scale is 1.0, one day of severe water stress shortens one day of stayGreenDuration
            #TODO remove WaterStress and use general Accumulator with a lambda function?
            return scale * (1 - self.water_potential_effect(threshold))

    @derive
    def stay_green_duration(self):
        # SK 8/20/10: as in Sinclair and Horie, 1989 Crop sciences, N availability index scaled between 0 and 1 based on
        #nitrogen_index = max(0, (2 / (1 + exp(-2.9 * (self.g_content - 0.25))) - 1))
        return clip(self.stay_green * self.growth_duration -
                    self.stay_green_water_stress_duration,
                    lower=0)

    @accumulate(unit='day')
    def active_age(self):
        # Assumes physiological time for senescence is the same as that for growth though this may be adjusted by stayGreen trait
        # a peaked fn like beta fn not used here because aging should accelerate with increasing T not slowing down at very high T like growth,
        # instead a q10 fn normalized to be 1 at T_opt is used, this means above Top aging accelerates.
        #TODO support clipping with @rate option or sub-decorator (i.e. @active_age.clip)
        #FIXME no need to check here, as it will be compared against duration later anyways
        #return min(self._aging_tracker.rate, self.stay_green_duration)
        if self.mature and not self.aging:
            #TODO only for MAIZSIM
            return q10_thermal_func(T=self.p.pheno.temperature,
                                    T_opt=self.p.pheno.optimal_temperature)

    @accumulate(unit='day')
    def senescence_water_stress_duration(self, scale=0.5, threshold=-4.0):
        if self.aging:
            # if scale is 0.5, one day of severe water stress at predawn shortens one half day of agingDuration
            return scale * (1 - self.water_potential_effect(threshold))

    @derive(unit='day')
    def senescence_duration(self):
        # end of growth period, time to maturity
        return clip(self.growth_duration -
                    self.senescence_water_stress_duration,
                    lower=0)

    #TODO active_age and senescence_age could share a tracker with separate intervals
    @accumulate(unit='day')
    def senescence_age(self):
        #TODO support clipping with @rate option or sub-decorator (i.e. @active_age.clip)
        #FIXME no need to check here, as it will be compared against duration later anyways
        #return min(self._senescence_tracker.rate, self.senescence_duration)
        #FIXME need to remove dependency cycle? (senescence_age -> senescence_ratio -> dead -> senescence_age)
        if self.aging and not self.dead:
            return q10_thermal_func(T=self.p.pheno.temperature,
                                    T_opt=self.p.pheno.optimal_temperature)

    @derive
    #TODO confirm if it really means the senescence ratio, not rate
    def senescence_ratio(self):
        # for MAIZSIM
        # t = self.senescence_age
        # t_e = self.senescence_duration
        # if t >= t_e:
        #     return 1
        # else:
        #     t_m = t_e / 2
        #     r = (1 + (t_e - t) / (t_e - t_m)) * (t / t_e)**(t_e / (t_e - t_m))
        #     return clip(r, 0., 1.)
        # for garlic
        #HACK prevents nan
        if self.length == 0:
            r = 0.
        else:
            r = self.aging_rate * self.senescence_age / self.length
        return clip(r, 0., 1.)

    @derive(unit='cm^2')
    def senescent_area(self):
        # Leaf senescence accelerates with drought and heat. see http://www.agry.purdue.edu/ext/corn/news/timeless/TopLeafDeath.html
        # rate = self._growth_rate(self.senescence_age, self.senescence_duration)
        # return rate * self.timestep * self.area
        return self.senescence_ratio * self.area

    @derive(unit='cm^2 / g')
    def specific_leaf_area(self):
        # temporary for now - it should vary by age. Value comes from some of Soo's work
        #return 200.0
        try:
            return self.area / self.mass
        except:
            return 0

    # Maturity

    @accumulate
    def maturity(self):
        #HACK: tracking should happen after plant emergence (due to implementation of original beginFromEmergence)
        if self.p.pheno.emerged and not self.mature:
            return growing_degree_days(T=self.p.pheno.temperature,
                                       T_base=4.0,
                                       T_max=40.0)

    # Nitrogen

    #FIXME avoid incorrect cycle detection (nitrogen member vs. module) - ?
    @derive
    def nitrogen(self):
        #TODO is this default value needed?
        # no N stress
        return 3.0
        #TODO remove self.p.* referencing
        #FIXME enable Nitrogen trait
        #return self.p.nitrogen.leaf_content

    ##########
    # States #
    ##########

    @flag
    def initiated(self):
        # no explicit initialize() here
        return True

    @flag
    def appeared(self):
        return self.rank <= self.p.pheno.leaves_appeared

    @flag
    def growing(self):
        return self.appeared and not self.mature

    @flag
    def mature(self):
        return self.elongation_age >= self.growth_duration or self.area >= self.potential_area

    @flag
    def aging(self):
        # for MAIZSIM
        #return self.active_age >= self.stay_green_duration
        # for garlic
        return self.mature and self.physiological_age > self.stay_green * self.maturity

    @flag
    def dead(self):
        #return self.senescent_area >= self.area
        return self.senescence_ratio >= 1
        #return self.senescence_age >= self.senescence_duration?

    @flag
    def dropped(self):
        return self.mature and self.dead
예제 #13
0
파일: organ.py 프로젝트: tomyun/cropbox
class Organ(System):
    plant = system(alias='p')

    # organ temperature, C
    temperature = drive('p.pheno', alias='T', unit='degC')

    # glucose, MW = 180.18 / 6 = 30.03 g
    @accumulate(alias='C', unit='g CH2O')
    def carbohydrate(self):
        return self.imported_carbohydrate  # * self.respiration_adjustment

    # nitrogen content, mg
    nitrogen = accumulate('imported_nitrogen', alias='N', unit='g Nitrogen')

    # physiological age accounting for temperature effect (in reference to endGrowth and lifeSpan, days)
    @accumulate(unit='day')
    def physiological_age(self, T):
        #HACK: tracking should happen after plant emergence (due to implementation of original beginFromEmergence)
        if self.p.pheno.emerged:
            #TODO support species/cultivar specific temperature parameters
            #return growing_degree_days(T=self.p.pheno.temperature, T_base=8.0, T_max=43.3)
            return growing_degree_days(T=T, T_base=4.0, T_max=40.0)

    # chronological age of an organ, days
    # @physiological_age.period
    # def chronological_age(self):
    #     pass

    # biomass, g
    # @derive
    # def mass(self):
    #     #FIXME isn't it just the amount of carbohydrate?
    #     #return self._carbohydrate / Weight.CH2O * Weight.C / Weight.C_to_CH2O_ratio
    #     return self._carbohydrate
    #FIXME need unit conversion from CH2O?
    @derive(unit='g CH2O')
    def mass(self):
        return self.carbohydrate

    # #TODO remove set_mass() and directly access carbohydrate
    # def set_mass(self, mass):
    #     #self._carbohydrate = mass * Weight.C_to_CH2O_ratio / Weight.C * Weight.CH2O
    #     self._carbohydrate = mass

    # physiological days to reach the end of growth (both cell division and expansion) at optimal temperature, days
    @parameter(unit='day')
    def growth_duration(self):
        return 10

    # life expectancy of an organ in days at optimal temperature (fastest growing temp), days
    #FIXME not used
    @parameter(unit='day')
    def longevity(self):
        return 50

    # carbon allocation to roots or leaves for time increment
    #FIXME not used
    @derive(unit='g/hr CH2O')
    def potential_carbohydrate_increment(self):
        return 0

    # carbon allocation to roots or leaves for time increment  gr C for roots, gr carbo dt-1
    #FIXME not used
    @derive(unit='g/hr CH2O')
    def actual_carbohydrate_increment(self):
        return 0

    #TODO to be overridden
    @derive(unit='g/hr CH2O')
    def imported_carbohydrate(self):
        return 0

    #TODO think about unit
    @derive
    def respiration_adjustment(self, Ka=0.1, Rm=0.02):
        # this needs to be worked on, currently not used at all
        # Ka: growth respiration
        # Rm: maintenance respiration
        return 1 - (Ka + Rm)

    #TODO to be overridden
    @derive(unit='g/hr Nitrogen')
    def imported_nitrogen(self):
        return 0