예제 #1
0
    class S(System):
        @system
        def t(self):
            return T

        @drive(alias='aa')
        def a(self):
            return self.t

        b = drive('t', alias='bb')
        c = derive('t.b')
예제 #2
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
예제 #3
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)
예제 #4
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