├── .gitignore ├── docs ├── src │ ├── assets │ │ ├── favicon.ico │ │ └── logo.svg │ └── index.md ├── Project.toml └── make.jl ├── test ├── Project.toml └── runtests.jl ├── Project.toml ├── src ├── diffusion.jl ├── nitrogen.jl ├── intercellularspace.jl ├── irradiance.jl ├── weather.jl ├── LeafGasExchange.jl ├── vaporpressure.jl ├── base.jl ├── boundarylayer.jl ├── energybalance.jl ├── canopy.jl ├── stomata.jl ├── c3.jl ├── c4.jl ├── sun.jl └── radiation.jl ├── .github └── workflows │ ├── compat.yml │ ├── docs.yml │ └── test.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Manifest.toml 2 | docs/build/ 3 | -------------------------------------------------------------------------------- /docs/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/LeafGasExchange.jl/main/docs/src/assets/favicon.ico -------------------------------------------------------------------------------- /test/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Cropbox = "a904b226-abf1-11e9-2713-059ba252a964" 3 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 4 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Cropbox = "a904b226-abf1-11e9-2713-059ba252a964" 3 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 4 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "LeafGasExchange" 2 | uuid = "eed59905-05f1-464e-afba-94c39a4505fd" 3 | authors = ["Kyungdahm Yun "] 4 | version = "0.1.7-DEV" 5 | 6 | [deps] 7 | Cropbox = "a904b226-abf1-11e9-2713-059ba252a964" 8 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 9 | 10 | [compat] 11 | Cropbox = "0.3" 12 | julia = "1.6" 13 | -------------------------------------------------------------------------------- /src/diffusion.jl: -------------------------------------------------------------------------------- 1 | @system Diffusion begin 2 | Dw: diffusion_coeff_for_water_vapor_in_air_at_20 => 24.2 ~ preserve(u"mm^2/s", parameter) 3 | Dc: diffusion_coeff_for_co2_in_air_at_20 => 14.7 ~ preserve(u"mm^2/s", parameter) 4 | Dh: diffusion_coeff_for_heat_in_air_at_20 => 21.5 ~ preserve(u"mm^2/s", parameter) 5 | Dm: diffusion_coeff_for_momentum_in_air_at_20 => 15.1 ~ preserve(u"mm^2/s", parameter) 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/compat.yml: -------------------------------------------------------------------------------- 1 | name: compat 2 | on: 3 | schedule: 4 | - cron: '00 00 * * *' 5 | workflow_dispatch: 6 | jobs: 7 | help: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | run: julia -e 'using CompatHelper; CompatHelper.main()' 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+' 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: julia-actions/setup-julia@v1 14 | - uses: julia-actions/julia-buildpkg@v1 15 | env: 16 | PYTHON: "" 17 | - uses: julia-actions/julia-docdeploy@v1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /src/nitrogen.jl: -------------------------------------------------------------------------------- 1 | @system Nitrogen begin 2 | SPAD: SPAD_greenness => 60 ~ preserve(parameter) 3 | _a: SPAD_N_coeff_a => 0.0004 ~ preserve(u"g/m^2", parameter) 4 | _b: SPAD_N_coeff_b => 0.012 ~ preserve(u"g/m^2", parameter) 5 | _c: SPAD_N_coeff_c => 0 ~ preserve(u"g/m^2", parameter) 6 | N(SPAD, _a, _b, _c): leaf_nitrogen_content => begin 7 | a*SPAD^2 + b*SPAD + c 8 | end ~ preserve(u"g/m^2", parameter) 9 | 10 | Np(N, SLA) => N * SLA ~ track(u"percent") 11 | SLA: specific_leaf_area => 200 ~ preserve(u"cm^2/g") 12 | end 13 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using Documenter 2 | using Cropbox 3 | 4 | makedocs( 5 | format = Documenter.HTML( 6 | prettyurls = get(ENV, "CI", nothing) == "true", 7 | canonical = "https://cropbox.github.io/LeafGasExchange.jl/stable/", 8 | assets = ["assets/favicon.ico"], 9 | analytics = "UA-192782823-1", 10 | ), 11 | sitename = "LeafGasExchange.jl", 12 | pages = [ 13 | "Home" => "index.md", 14 | ], 15 | ) 16 | 17 | deploydocs( 18 | repo = "github.com/cropbox/LeafGasExchange.jl.git", 19 | devbranch = "main", 20 | ) 21 | -------------------------------------------------------------------------------- /src/intercellularspace.jl: -------------------------------------------------------------------------------- 1 | @system IntercellularSpace(Weather) begin 2 | A_net ~ hold 3 | #TODO: interface between boundary/stomata/intercellular space (i.e. soil layers?) 4 | gvc ~ hold 5 | 6 | #FIXME: duplicate in Stomata 7 | Ca(CO2, P_air): co2_air => (CO2 * P_air) ~ track(u"μbar") 8 | 9 | #HACK: high temperature simulation requires higher upper bound 10 | Cimax(Ca): intercellular_co2_upper_limit => 2Ca ~ track(u"μbar") 11 | Cimin: intercellular_co2_lower_limit => 0 ~ preserve(u"μbar") 12 | Ci(Ca, Ci, A_net, gvc): intercellular_co2 => begin 13 | Ca - Ci ⩵ A_net / gvc 14 | end ~ bisect(min=Cimin, upper=Cimax, u"μbar") 15 | end 16 | -------------------------------------------------------------------------------- /src/irradiance.jl: -------------------------------------------------------------------------------- 1 | @system Irradiance begin 2 | PFD ~ hold 3 | 4 | #HACK: should be PPFD from Radiation 5 | PPFD(PFD): photosynthetic_photon_flux_density ~ track(u"μmol/m^2/s") 6 | 7 | #FIXME: duplicate? already considered in Radiation? 8 | #FIXME: how is (1 - δ) related to α in EnergyBalance? 9 | # leaf reflectance + transmittance 10 | δ: leaf_scattering => 0.15 ~ preserve(parameter) 11 | f: leaf_spectral_correction => 0.15 ~ preserve(parameter) 12 | 13 | Ia(PPFD, δ): absorbed_irradiance => begin 14 | PPFD * (1 - δ) 15 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 16 | 17 | I2(Ia, f): effective_irradiance => begin 18 | Ia * (1 - f) / 2 # useful light absorbed by PSII 19 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 20 | end -------------------------------------------------------------------------------- /src/weather.jl: -------------------------------------------------------------------------------- 1 | @system Weather begin 2 | vp(context): vapor_pressure ~ ::VaporPressure 3 | 4 | PFD: photon_flux_density ~ preserve(u"μmol/m^2/s", parameter) #Quanta 5 | CO2: carbon_dioxide ~ preserve(u"μmol/mol", parameter) 6 | RH: relative_humidity ~ preserve(u"percent", parameter) 7 | T_air: air_temperature ~ preserve(u"°C", parameter) 8 | Tk_air(T_air): absolute_air_temperature ~ track(u"K") 9 | wind: wind_speed ~ preserve(u"m/s", parameter) 10 | P_air: air_pressure => 100 ~ preserve(u"kPa", parameter) 11 | 12 | VPD(T_air, RH, D=vp.D): vapor_pressure_deficit => D(T_air, RH) ~ track(u"kPa") 13 | VPD_Δ(T_air, Δ=vp.Δ): vapor_pressure_saturation_slope_delta => Δ(T_air) ~ track(u"kPa/K") 14 | VPD_s(T_air, P_air, s=vp.s): vapor_pressure_saturation_slope => s(T_air, P_air) ~ track(u"K^-1") 15 | end 16 | -------------------------------------------------------------------------------- /src/LeafGasExchange.jl: -------------------------------------------------------------------------------- 1 | module LeafGasExchange 2 | 3 | using Cropbox 4 | 5 | include("vaporpressure.jl") 6 | include("weather.jl") 7 | include("diffusion.jl") 8 | include("nitrogen.jl") 9 | include("base.jl") 10 | include("c3.jl") 11 | include("c4.jl") 12 | include("boundarylayer.jl") 13 | include("stomata.jl") 14 | include("intercellularspace.jl") 15 | include("irradiance.jl") 16 | include("energybalance.jl") 17 | 18 | @system ModelBase( 19 | Weather, Nitrogen, 20 | BoundaryLayer, StomataBase, IntercellularSpace, Irradiance, EnergyBalance 21 | ) 22 | 23 | @system ModelC3BB(ModelBase, StomataBallBerry, C3, Controller) 24 | @system ModelC4BB(ModelBase, StomataBallBerry, C4, Controller) 25 | 26 | @system ModelC3MD(ModelBase, StomataMedlyn, C3, Controller) 27 | @system ModelC4MD(ModelBase, StomataMedlyn, C4, Controller) 28 | 29 | export ModelC3BB, ModelC3MD, ModelC4BB, ModelC4MD 30 | 31 | include("canopy.jl") 32 | 33 | end 34 | -------------------------------------------------------------------------------- /src/vaporpressure.jl: -------------------------------------------------------------------------------- 1 | @system VaporPressure begin 2 | # Campbell and Norman (1998), p 41 Saturation vapor pressure in kPa 3 | a => 0.611 ~ preserve(u"kPa", parameter) 4 | b => 17.502 ~ preserve(parameter) 5 | c => 240.97 ~ preserve(parameter) # °C 6 | 7 | es(a, b, c; T(u"°C")): saturation => (t = Cropbox.deunitfy(T); a*exp((b*t)/(c+t))) ~ call(u"kPa") 8 | ea(es; T(u"°C"), RH(u"percent")): ambient => es(T) * RH ~ call(u"kPa") 9 | D(es; T(u"°C"), RH(u"percent")): deficit => es(T) * (1 - RH) ~ call(u"kPa") 10 | RH(es; T(u"°C"), VPD(u"kPa")): relative_humidity => 1 - VPD / es(T) ~ call(u"NoUnits") 11 | 12 | # slope of the sat vapor pressure curve: first order derivative of Es with respect to T 13 | Δ(es, b, c; T(u"°C")): saturation_slope_delta => (e = es(T); t = Cropbox.deunitfy(T); e*(b*c)/(c+t)^2 / u"K") ~ call(u"kPa/K") 14 | s(Δ; T(u"°C"), P(u"kPa")): saturation_slope => Δ(T) / P ~ call(u"K^-1") 15 | end 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ${{ matrix.os }} 6 | continue-on-error: ${{ matrix.experimental }} 7 | strategy: 8 | matrix: 9 | version: ['1'] 10 | os: [macos-latest, ubuntu-latest, windows-latest] 11 | arch: [x64] 12 | experimental: [false] 13 | include: 14 | - version: 'nightly' 15 | os: macos-latest 16 | arch: x64 17 | experimental: true 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: julia-actions/setup-julia@v1 21 | with: 22 | version: ${{ matrix.version }} 23 | arch: ${{ matrix.arch }} 24 | - uses: julia-actions/julia-buildpkg@v1 25 | env: 26 | PYTHON: "" 27 | - uses: julia-actions/julia-runtest@v1 28 | with: 29 | depwarn: error 30 | - uses: julia-actions/julia-processcoverage@v1 31 | - uses: codecov/codecov-action@v3 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Kyungdahm Yun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cropbox 2 | 3 | # LeafGasExchange.jl 4 | 5 | [![Stable Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://cropbox.github.io/LeafGasExchange.jl/stable/) 6 | [![Latest Documentation](https://img.shields.io/badge/docs-dev-blue.svg)](https://cropbox.github.io/LeafGasExchange.jl/dev/) 7 | 8 | [LeafGasExchange.jl](https://github.com/cropbox/LeafGasExchange.jl) implements a coupled leaf gas-exchange model using [Cropbox](https://github.com/cropbox/Cropbox.jl) modeling framework. Biochemical photosynthesis models (C₃, C₄) are coupled with empirical stomatal conductance models (Ball--Berry, Medlyn) in addition to radiative energy balance model. Users can run model simulations with custom configuration and visualize simulation results. Model parameters may be calibrated with observation dataset if provided. 9 | 10 | ## Examples 11 | 12 | - [plants2020](https://github.com/cropbox/plants2020): coupled leaf gas-exchange model for C₄ leaves comparing stomatal conductance models 13 | -------------------------------------------------------------------------------- /src/base.jl: -------------------------------------------------------------------------------- 1 | @system TemperatureDependence begin 2 | T: leaf_temperature ~ hold 3 | Tk(T): absolute_leaf_temperature ~ track(u"K") 4 | 5 | Tb: base_temperature => 25 ~ preserve(u"°C", parameter) 6 | Tbk(Tb): absolute_base_temperature ~ preserve(u"K") 7 | 8 | kT(T, Tk, Tb, Tbk; Ea(u"kJ/mol")): arrhenius_equation => begin 9 | exp(Ea * (T - Tb) / (u"R" * Tk * Tbk)) 10 | end ~ call 11 | 12 | kTpeak(Tk, Tbk, kT; Ea(u"kJ/mol"), H(u"kJ/mol"), ΔS(u"J/mol/K")): peaked_function => begin 13 | kT(Ea) * (1 + exp((ΔS*Tbk - H) / (u"R"*Tbk))) / (1 + exp((ΔS*Tk - H) / (u"R"*Tk))) 14 | end ~ call 15 | 16 | ΔS(; Ha(u"kJ/mol"), Hd(u"kJ/mol"), To(u"K")): entropy_factor => begin 17 | Hd / To + u"R" * log(Ha / (Hd - Ha)) 18 | end ~ call(u"J/mol/K") 19 | 20 | kTpeakT(kTpeak, ΔS; Ha(u"kJ/mol"), Hd(u"kJ/mol"), To(u"K")): peaked_function_with_optimal_temperature => begin 21 | kTpeak(Ha, Hd, ΔS(Ha, Hd, To)) 22 | end ~ call 23 | 24 | Q10 => 2 ~ preserve(parameter) 25 | kTQ10(T, Tb, Q10): q10_rate => begin 26 | Q10^((T - Tb) / 10u"K") 27 | end ~ track 28 | end 29 | 30 | @system NitrogenDependence begin 31 | N: leaf_nitrogen_content => 4.0 ~ preserve(u"g/m^2", parameter) 32 | 33 | s => 2.9 ~ preserve(u"m^2/g", parameter) 34 | N0 => 0.25 ~ preserve(u"g/m^2", parameter) 35 | 36 | kN(N, s, N0): nitrogen_limited_rate => begin 37 | 2 / (1 + exp(-s * (max(N0, N) - N0))) - 1 38 | end ~ track 39 | end 40 | 41 | @system CBase(TemperatureDependence, NitrogenDependence) begin 42 | Ci: intercellular_co2 ~ hold 43 | I2: effective_irradiance ~ hold 44 | end 45 | -------------------------------------------------------------------------------- /src/boundarylayer.jl: -------------------------------------------------------------------------------- 1 | @system BoundaryLayer(Weather, Diffusion) begin 2 | w: leaf_width => 10 ~ preserve(u"cm", parameter) 3 | 4 | sr: stomatal_ratio => begin 5 | # maize is an amphistomatous species, assume 1:1 (adaxial:abaxial) ratio. 6 | #sr = 1.0 7 | # switchgrass adaxial : abaxial (Awada 2002) 8 | # https://doi.org/10.4141/P01-031 9 | #sr = 1.28 10 | 1.0 11 | end ~ preserve(parameter) 12 | scr(sr): sides_conductance_ratio => ((sr + 1)^2 / (sr^2 + 1)) ~ preserve 13 | 14 | # multiply by 1.4 for outdoor condition, Campbell and Norman (1998), p109 15 | ocr: outdoor_conductance_ratio => 1.4 ~ preserve 16 | 17 | u(u=wind): wind_velocity ~ track(u"m/s", min=0.1) 18 | # characteristic dimension of a leaf, leaf width in m 19 | d(w): characteristic_dimension => 0.72w ~ track(u"m") 20 | v(Dm): kinematic_viscosity_of_air ~ preserve(u"m^2/s", parameter) 21 | κ(Dh): thermal_diffusivity_of_air ~ preserve(u"m^2/s", parameter) 22 | Re(u, d, v): reynolds_number => u*d/v ~ track 23 | Nu(Re): nusselt_number => 0.60sqrt(Re) ~ track 24 | gh(κ, Nu, d, scr, ocr, P_air, Tk_air): boundary_layer_heat_conductance => begin 25 | g = κ * Nu / d 26 | # multiply by ratio to get the effective blc (per projected area basis), licor 6400 manual p 1-9 27 | g *= scr * ocr 28 | # including a multiplier for conversion from mm s-1 to mol m-2 s-1 29 | g * P_air / (u"R" * Tk_air) 30 | end ~ track(u"mmol/m^2/s") 31 | # 1.1 is the factor to convert from heat conductance to water vapor conductance, an average between still air and laminar flow (see Table 3.2, HG Jones 2014) 32 | rhw(Dw, Dh): ratio_from_heat_to_water_vapor => (Dw / Dh)^(2/3) ~ preserve 33 | gb(rhw, gh, P_air): boundary_layer_conductance => rhw * gh / P_air ~ track(u"mol/m^2/s/bar" #= H2O =#) 34 | end 35 | -------------------------------------------------------------------------------- /docs/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/energybalance.jl: -------------------------------------------------------------------------------- 1 | @system EnergyBalance(Weather) begin 2 | gv ~ hold 3 | gh ~ hold 4 | PPFD ~ hold 5 | 6 | ϵ: leaf_thermal_emissivity => 0.97 ~ preserve(parameter) 7 | σ: stefan_boltzmann_constant => u"σ" ~ preserve(u"W/m^2/K^4") 8 | λ: latent_heat_of_vaporization_at_25 => 44 ~ preserve(u"kJ/mol", parameter) 9 | Cp: specific_heat_of_air => 29.3 ~ preserve(u"J/mol/K", parameter) 10 | 11 | k: radiation_conversion_factor => (1 / 4.55) ~ preserve(u"J/μmol") 12 | PAR(PPFD, k): photosynthetically_active_radiation => (PPFD * k) ~ track(u"W/m^2") 13 | 14 | # NIR(PAR): near_infrared_radiation => begin 15 | # #FIXME: maybe δ or similar ratio supposed to be applied here? 16 | # # If total solar radiation unavailable, assume NIR the same energy as PAR waveband 17 | # PAR 18 | # end ~ track(u"W/m^2") 19 | 20 | # solar radiation absorptivity of leaves: =~ 0.5 21 | #FIXME: is α different from (1 - δ) in Irradiance? 22 | α_s: absorption_coefficient => 0.5 ~ preserve(parameter) 23 | 24 | #R_sw(PAR, NIR, α_s, δ): shortwave_radiation_absorbed => begin 25 | R_sw(PAR, α_s): shortwave_radiation_absorbed => begin 26 | #FIXME: why δ needed here? α should already take care of scattering 27 | # shortwave radiation (PAR (=0.85) + NIR (=0.15)) 28 | #α_s*((1-δ)*PAR + δ*NIR) 29 | α_s*PAR 30 | end ~ track(u"W/m^2") 31 | 32 | R_wall(ϵ, σ, Tk_air): thermal_radiation_absorbed_from_wall => 2ϵ*σ*Tk_air^4 ~ track(u"W/m^2") 33 | R_leaf(ϵ, σ, Tk): thermal_radiation_emitted_by_leaf => 2ϵ*σ*Tk^4 ~ track(u"W/m^2") 34 | R_thermal(R_wall, R_leaf): thermal_radiation_absorbed => R_wall - R_leaf ~ track(u"W/m^2") 35 | R_net(R_sw, R_thermal): net_radiation_absorbed => R_sw + R_thermal ~ track(u"W/m^2") 36 | 37 | Δw(T, T_air, RH, #= P_air, =# ea=vp.ambient, es=vp.saturation): leaf_vapor_pressure_gradient => begin 38 | Es = es(T) 39 | Ea = ea(T_air, RH) 40 | Es - Ea # MAIZSIM: / (1 - (Es + Ea) / P_air) 41 | end ~ track(u"kPa") 42 | E(gv, Δw): transpiration => gv*Δw ~ track(u"mmol/m^2/s" #= H2O =#) 43 | 44 | H(Cp, gh, ΔT): sensible_heat_flux => Cp*gh*ΔT ~ track(u"W/m^2") 45 | λE(λ, E): latent_heat_flux => λ*E ~ track(u"W/m^2") 46 | 47 | ΔT(R_net, H, λE): temperature_adjustment => begin 48 | R_net ⩵ H + λE 49 | end ~ bisect(lower=-5, upper=5, u"K", evalunit=u"W/m^2") 50 | 51 | T(T_air, ΔT): leaf_temperature => (T_air + ΔT) ~ track(u"°C") 52 | Tk(T): absolute_leaf_temperature ~ track(u"K") 53 | end 54 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | # LeafGasExchange.jl 2 | 3 | [LeafGasExchange.jl](https://github.com/cropbox/LeafGasExchange.jl) implements a coupled leaf gas-exchange model using [Cropbox](https://github.com/cropbox/Cropbox.jl) modeling framework. Biochemical photosynthesis models (C₃, C₄) are coupled with empirical stomatal conductance models (Ball--Berry, Medlyn) in addition to radiative energy balance model. Users can run model simulations with custom configuration and visualize simulation results. Model parameters may be calibrated with observation dataset if provided. 4 | 5 | ## Installation 6 | 7 | ```julia 8 | using Pkg 9 | Pkg.add("LeafGasExchange") 10 | ``` 11 | 12 | ## Getting Started 13 | 14 | ```@setup aci 15 | ENV["UNITFUL_FANCY_EXPONENTS"] = true 16 | ``` 17 | 18 | ```@example aci 19 | using Cropbox 20 | using LeafGasExchange 21 | ``` 22 | 23 | The package exports four combinations of model (`ModelC3BB`, `ModelC3MD`, `ModelC4BB`, `ModelC4MD`) from two photosynthesis models (`C3`, `C4`) and two stomatal conductance models (`BB` for Ball-Berry, `MD` for Medlyn). 24 | 25 | ```@example aci 26 | parameters(ModelC4MD; alias = true) 27 | ``` 28 | 29 | By default, most parameters are already filled in for a casual run, except driving variables which describe surrounding environment. 30 | 31 | ```@example aci 32 | config = :Weather => ( 33 | PFD = 1500, 34 | CO2 = 400, 35 | RH = 60, 36 | T_air = 30, 37 | wind = 2.0, 38 | ) 39 | ; # hide 40 | ``` 41 | 42 | Let's define a wide range of atmospheric CO₂ for simulation. 43 | 44 | ```@example aci 45 | xstep = :Weather => :CO2 => 50:10:1500 46 | ; # hide 47 | ``` 48 | 49 | Then, net photosynthesis rate (`A_net`), Rubisco-limited rate (`Ac`), and electron transport-limited rate (`Aj`) responding to the range of CO₂ can be simulated and visualized in a plot. 50 | 51 | ```@example aci 52 | visualize(ModelC4MD, :Ci, [:Ac, :Aj, :A_net]; config, xstep, kind = :line) 53 | ``` 54 | 55 | The output of simulation can be visualized in terms of other variables such as stomatal conductance (`gs`) whose coupling with photosynthesis is solved via vapor pressure at the leaf surface. 56 | 57 | ```@example aci 58 | visualize(ModelC4MD, :Ci, :gs; config, xstep, kind = :line) 59 | ``` 60 | 61 | Similarly, we can visualize changes in leaf temperature (`T`) which has to be numerically optimized through energy balance equation. 62 | 63 | ```@example aci 64 | visualize(ModelC4MD, :Ci, :T; config, xstep, kind = :line) 65 | ``` 66 | 67 | For more information about using the framework such as `parameters()` and `visualize()` functions, please refer to the [Cropbox documentation](http://cropbox.github.io/Cropbox.jl/stable/). 68 | -------------------------------------------------------------------------------- /src/canopy.jl: -------------------------------------------------------------------------------- 1 | include("sun.jl") 2 | include("radiation.jl") 3 | 4 | @system ModelAdapter(ModelBase) begin 5 | weather ~ bring::Weather(override) 6 | 7 | PPFD: photosynthetic_photon_flux_density ~ track(u"μmol/m^2/s" #= Quanta =#, override) 8 | LAI: leaf_area_index ~ track(override) 9 | 10 | A_net_total(A_net, LAI): net_photosynthesis_total => A_net * LAI ~ track(u"μmol/m^2/s" #= CO2 =#) 11 | A_gross_total(A_gross, LAI): gross_photosynthesis_total => A_gross * LAI ~ track(u"μmol/m^2/s" #= CO2 =#) 12 | E_total(E, LAI): transpiration_total => E * LAI ~ track(u"mmol/m^2/s" #= H2O =#) 13 | end 14 | 15 | @system C3BBAdapter(ModelAdapter, StomataBallBerry, C3) 16 | @system C4BBAdapter(ModelAdapter, StomataBallBerry, C4) 17 | 18 | @system C3MDAdapter(ModelAdapter, StomataMedlyn, C3) 19 | @system C4MDAdapter(ModelAdapter, StomataMedlyn, C4) 20 | 21 | @system Canopy{GasExchange => C3BBAdapter} begin 22 | weather(context) ~ ::Weather 23 | sun(context, weather) ~ ::Sun 24 | radiation(context, sun, LAI) ~ ::Radiation 25 | 26 | LAI: leaf_area_index => 5 ~ preserve(parameter) 27 | PD: planting_density => 55 ~ preserve(parameter, u"m^-2") 28 | 29 | H2O_weight => 18.01528 ~ preserve(u"g/mol") 30 | CO2_weight => 44.0098 ~ preserve(u"g/mol") 31 | CH2O_weight => 30.031 ~ preserve(u"g/mol") 32 | 33 | sunlit_gasexchange(context, weather, PPFD=Q_sun, LAI=LAI_sunlit) ~ ::GasExchange 34 | shaded_gasexchange(context, weather, PPFD=Q_sh, LAI=LAI_shaded) ~ ::GasExchange 35 | 36 | leaf_width => begin 37 | # to be calculated when implemented for individal leaves 38 | #5.0 # for maize 39 | 1.5 # for garlic 40 | end ~ preserve(u"cm", parameter) 41 | 42 | LAI_sunlit(radiation.sunlit_leaf_area_index): sunlit_leaf_area_index ~ track 43 | LAI_shaded(radiation.shaded_leaf_area_index): shaded_leaf_area_index ~ track 44 | 45 | Q_sun(radiation.irradiance_Q_sunlit): sunlit_irradiance ~ track(u"μmol/m^2/s" #= Quanta =#) 46 | Q_sh(radiation.irradiance_Q_shaded): shaded_irradiance ~ track(u"μmol/m^2/s" #= Quanta =#) 47 | 48 | A_gross(a=sunlit_gasexchange.A_gross_total, b=shaded_gasexchange.A_gross_total): gross_CO2_umol_per_m2_s => begin 49 | a + b 50 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 51 | 52 | A_net(a=sunlit_gasexchange.A_net_total, b=shaded_gasexchange.A_net_total): net_CO2_umol_per_m2_s => begin 53 | a + b 54 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 55 | 56 | gross_assimilation(A_gross, PD, w=CH2O_weight) => begin 57 | # grams carbo per plant per hour 58 | #FIXME check unit conversion between C/CO2 to CH2O 59 | A_gross / PD * w 60 | end ~ track(u"g/d") 61 | 62 | net_assimilation(A_net, PD, w=CH2O_weight) => begin 63 | # grams carbo per plant per hour 64 | #FIXME check unit conversion between C/CO2 to CH2O 65 | A_net / PD * w 66 | end ~ track(u"g/d") 67 | 68 | conductance(gs_sun=sunlit_gasexchange.gs, LAI_sunlit, gs_sh=shaded_gasexchange.gs, LAI_shaded, LAI) => begin 69 | #HACK ensure 0 when one of either LAI is 0, i.e., night 70 | # average stomatal conductance Yang 71 | c = ((gs_sun * LAI_sunlit) + (gs_sh * LAI_shaded)) / LAI 72 | #c = max(zero(c), c) 73 | iszero(LAI) ? zero(c) : c 74 | end ~ track(u"mol/m^2/s/bar") 75 | end 76 | 77 | @system ModelC3BBC{GasExchange => C3BBAdapter}(Canopy, Controller) 78 | @system ModelC3MDC{GasExchange => C3MDAdapter}(Canopy, Controller) 79 | 80 | @system ModelC4BBC{GasExchange => C4BBAdapter}(Canopy, Controller) 81 | @system ModelC4MDC{GasExchange => C4MDAdapter}(Canopy, Controller) 82 | 83 | export ModelC3BBC, ModelC3MDC, ModelC4BBC, ModelC4MDC 84 | -------------------------------------------------------------------------------- /src/stomata.jl: -------------------------------------------------------------------------------- 1 | @system StomataBase(Weather, Diffusion) begin 2 | gs: stomatal_conductance ~ hold 3 | gb: boundary_layer_conductance ~ hold 4 | A_net: net_photosynthesis ~ hold 5 | T: leaf_temperature ~ hold 6 | 7 | drb(Dw, Dc): diffusivity_ratio_boundary_layer => (Dw / Dc)^(2/3) ~ preserve(#= u"H2O/CO2", =# parameter) 8 | dra(Dw, Dc): diffusivity_ratio_air => (Dw / Dc) ~ preserve(#= u"H2O/CO2", =# parameter) 9 | 10 | Ca(CO2, P_air): co2_air => (CO2 * P_air) ~ track(u"μbar") 11 | Cs(Ca, A_net, gbc): co2_at_leaf_surface => begin 12 | Ca - A_net / gbc 13 | end ~ track(u"μbar") 14 | 15 | gv(gs, gb): total_conductance_h2o => (gs * gb / (gs + gb)) ~ track(u"mol/m^2/s/bar" #= H2O =#) 16 | 17 | rbc(gb, drb): boundary_layer_resistance_co2 => (drb / gb) ~ track(u"m^2*s/mol*bar") 18 | rsc(gs, dra): stomatal_resistance_co2 => (dra / gs) ~ track(u"m^2*s/mol*bar") 19 | rvc(rbc, rsc): total_resistance_co2 => (rbc + rsc) ~ track(u"m^2*s/mol*bar") 20 | 21 | gbc(rbc): boundary_layer_conductance_co2 => (1 / rbc) ~ track(u"mol/m^2/s/bar") 22 | gsc(rsc): stomatal_conductance_co2 => (1 / rsc) ~ track(u"mol/m^2/s/bar") 23 | gvc(rvc): total_conductance_co2 => (1 / rvc) ~ track(u"mol/m^2/s/bar") 24 | end 25 | 26 | @system StomataTuzet begin 27 | WP_leaf: leaf_water_potential => 0 ~ preserve(u"MPa", parameter) 28 | Ψv(WP_leaf): bulk_leaf_water_potential ~ track(u"MPa") 29 | Ψf: reference_leaf_water_potential => -2.0 ~ preserve(u"MPa", parameter) 30 | sf: stomata_sensitivity_param => 2.3 ~ preserve(u"MPa^-1", parameter) 31 | fΨv(Ψv, Ψf, sf): stomata_sensitivty => begin 32 | (1 + exp(sf*Ψf)) / (1 + exp(sf*(Ψf-Ψv))) 33 | end ~ track 34 | end 35 | 36 | @system StomataBallBerry(StomataBase, StomataTuzet) begin 37 | # Set default Ball-Berry model parameter values for C3 plants. Assume g0 is not different from 0. 38 | # See Franks et al (2017) Plant Physiology and Miner et al (2017) Plant Cell Environ 39 | # For temperate C4 species, use g1 = 5.2. 40 | g0 => 0.0 ~ preserve(u"mol/m^2/s/bar" #= H2O =#, parameter) 41 | g1 => 13.1 ~ preserve(parameter) 42 | 43 | #HACK: avoid scaling issue with dimensionless unit 44 | hs(g0, g1, gb, A_net, Cs, fΨv, RH): relative_humidity_at_leaf_surface => begin 45 | gs = g0 + g1*(A_net*hs/Cs) * fΨv 46 | (hs - RH)*gb ⩵ (1 - hs)*gs 47 | end ~ solve(lower=0, upper=1) #, u"percent") 48 | Ds(D=vp.D, T, hs): vapor_pressure_deficit_at_leaf_surface => begin 49 | D(T, hs) 50 | end ~ track(u"kPa") 51 | 52 | gs(g0, g1, A_net, hs, Cs, fΨv): stomatal_conductance => begin 53 | g0 + g1*(A_net*hs/Cs) * fΨv 54 | end ~ track(u"mol/m^2/s/bar" #= H2O =#, min=g0) 55 | end 56 | 57 | @system StomataMedlyn(StomataBase, StomataTuzet) begin 58 | # Set default Medlyn model parameters for C3 plants. 59 | # See Franks et al (2017) Plant Physiology (http://www.plantphysiol.org/cgi/doi/10.1104/pp.17.00287) 60 | # See also Lin et al. (2015) Nature Climate Change 61 | g0 => 0.0 ~ preserve(u"mol/m^2/s/bar" #= H2O =#, parameter) 62 | g1 => 4.45 ~ preserve(u"√kPa", parameter) 63 | 64 | wa(ea=vp.ea, T_air, RH): vapor_pressure_at_air => ea(T_air, RH) ~ track(u"kPa") 65 | wi(es=vp.es, T): vapor_pressure_at_intercellular_space => es(T) ~ track(u"kPa") 66 | ws(Ds, wi): vapor_pressure_at_leaf_surface => (wi - Ds) ~ track(u"kPa") 67 | Ds¹ᐟ²(g0, g1, gb, A_net, Cs, fΨv, wi, wa) => begin 68 | #HACK: SymPy couldn't extract polynomial coeffs for ps inside √ 69 | gs = g0 + (1 + g1 / Ds¹ᐟ²) * (A_net / Cs) * fΨv 70 | ws = wi - Ds¹ᐟ²^2 71 | (ws - wa)*gb ⩵ (wi - ws)*gs 72 | end ~ solve(lower=0, upper=√wi', u"√kPa") 73 | Ds(Ds¹ᐟ²): vapor_pressure_deficit_at_leaf_surface => Ds¹ᐟ²^2 ~ track(u"kPa", min=1u"Pa") 74 | hs(RH=vp.RH, T, Ds): relative_humidity_at_leaf_surface => RH(T, Ds) ~ track 75 | 76 | gs(g0, g1, A_net, Ds, Cs, fΨv): stomatal_conductance => begin 77 | g0 + (1 + g1/√Ds)*(A_net/Cs) * fΨv 78 | end ~ track(u"mol/m^2/s/bar" #= H2O =#, min=g0) 79 | end 80 | -------------------------------------------------------------------------------- /src/c3.jl: -------------------------------------------------------------------------------- 1 | @system C3Base(CBase) 2 | 3 | @system C3c(C3Base) begin 4 | # Michaelis constant of rubisco for CO2 of C3 plants, ubar, from Bernacchi et al. (2001) 5 | Kc25: rubisco_constant_for_co2_at_25 => 404.9 ~ preserve(u"μbar", parameter) 6 | # Activation energy for Kc, Bernacchi (2001) 7 | Eac: activation_energy_for_co2 => 79.43 ~ preserve(u"kJ/mol", parameter) 8 | Kc(Kc25, kT, Eac): rubisco_constant_for_co2 => begin 9 | Kc25 * kT(Eac) 10 | end ~ track(u"μbar") 11 | 12 | # Michaelis constant of rubisco for O2, mbar, from Bernacchi et al., (2001) 13 | Ko25: rubisco_constant_for_o2_at_25 => 278.4 ~ preserve(u"mbar", parameter) 14 | # Activation energy for Ko, Bernacchi (2001) 15 | Eao: activation_energy_for_o2 => 36.38 ~ preserve(u"kJ/mol", parameter) 16 | Ko(Ko25, kT, Eao): rubisco_constant_for_o2 => begin 17 | Ko25 * kT(Eao) 18 | end ~ track(u"mbar") 19 | 20 | # mesophyll O2 partial pressure 21 | Om: mesophyll_o2_partial_pressure => 210 ~ preserve(u"mbar", parameter) 22 | # effective M-M constant for Kc in the presence of O2 23 | Km(Kc, Om, Ko): rubisco_constant_for_co2_with_o2 => begin 24 | Kc * (1 + Om / Ko) 25 | end ~ track(u"μbar") 26 | 27 | Vcm25: maximum_carboxylation_rate_at_25 => 108.4 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 28 | EaVc: activation_energy_for_carboxylation => 52.1573 ~ preserve(u"kJ/mol", parameter) 29 | Vcmax(Vcm25, kT, EaVc, kN): maximum_carboxylation_rate => begin 30 | Vcm25 * kT(EaVc) * kN 31 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 32 | end 33 | 34 | @system C3j(C3Base) begin 35 | Jm25: maximum_electron_transport_rate_at_25 => 169.0 ~ preserve(u"μmol/m^2/s" #= Electron =#, parameter) 36 | Eaj: activation_enthalpy_for_electron_transport => 23.9976 ~ preserve(u"kJ/mol", parameter) 37 | Hj: deactivation_enthalpy_for_electron_transport => 200 ~ preserve(u"kJ/mol", parameter) 38 | Sj: sensitivity_for_electron_transport => 616.4 ~ preserve(u"J/mol/K", parameter) 39 | Jmax(Jm25, kTpeak, Eaj, Hj, Sj, kN): maximum_electron_transport_rate => begin 40 | Jm25 * kTpeak(Eaj, Hj, Sj) * kN 41 | end ~ track(u"μmol/m^2/s" #= Electron =#) 42 | 43 | # θ: sharpness of transition from light limitation to light saturation 44 | θ: light_transition_sharpness => 0.7 ~ preserve(parameter) 45 | J(I2, Jmax, θ): electron_transport_rate => begin 46 | a = θ 47 | b = -(I2+Jmax) 48 | c = I2*Jmax 49 | a*J^2 + b*J + c ⩵ 0 50 | end ~ solve(lower=0, upper=Jmax, u"μmol/m^2/s") 51 | end 52 | 53 | @system C3p(C3Base) begin 54 | Tp25: triose_phosphate_limitation_at_25 => 16.03 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 55 | EaTp: activation_energy_for_Tp => 47.10 ~ preserve(u"kJ/mol", parameter) 56 | Tp(Tp25, kT, EaTp, kN): triose_phosphate_limitation => begin 57 | Tp25 * kT(EaTp) * kN 58 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 59 | end 60 | 61 | @system C3r(C3Base) begin 62 | Rd25: dark_respiration_at_25 => 1.08 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 63 | Ear: activation_energy_for_respiration => 49.39 ~ preserve(u"kJ/mol", parameter) 64 | Rd(Rd25, kT, Ear): dark_respiration => begin 65 | Rd25 * kT(Ear) 66 | end ~ track(u"μmol/m^2/s") 67 | #Rm(Rd) => 0.5Rd ~ track(u"μmol/m^2/s") 68 | 69 | # CO2 compensation point in the absence of day respiration, value from Bernacchi (2001) 70 | Γ25: co2_compensation_point_at_25 => 42.75 ~ preserve(u"μbar", parameter) 71 | Eag: activation_energy_for_co2_compensation_point => 37.83 ~ preserve(u"kJ/mol", parameter) 72 | Γ(Γ25, kT, Eag): co2_compensation_point => begin 73 | Γ25 * kT(Eag) 74 | end ~ track(u"μbar") 75 | end 76 | 77 | @system C3Rate(C3c, C3j, C3p, C3r) begin 78 | Ac(Vcmax, Ci, Γ, Km, Rd): enzyme_limited_photosynthesis_rate => begin 79 | Vcmax * (Ci - Γ) / (Ci + Km) - Rd 80 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 81 | 82 | # light and electron transport limited A mediated by J 83 | Aj(J, Ci, Γ, Rd): transport_limited_photosynthesis_rate => begin 84 | J * (Ci - Γ) / 4(Ci + 2Γ) - Rd 85 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 86 | 87 | Ap(Tp, Rd): triose_phosphate_limited_photosynthesis_rate => begin 88 | 3Tp - Rd 89 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 90 | 91 | A_net(Ac, Aj, Ap): net_photosynthesis => begin 92 | min(Ac, Aj, Ap) 93 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 94 | 95 | A_gross(A_net, Rd): gross_photosynthesis => begin 96 | # gets negative when PFD = 0, Rd needs to be examined, 10/25/04, SK 97 | A_gross = A_net + Rd 98 | #max(A_gross, zero(A_gross)) 99 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 100 | end 101 | 102 | @system C3(C3Rate) 103 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Cropbox 2 | using LeafGasExchange 3 | using Test 4 | 5 | "Kim et al. (2007), Kim et al. (2006)" 6 | ge_maize1 = :C4 => ( 7 | Vpm25 = 70, Vcm25 = 50, Jm25 = 300, 8 | Rd25 = 2 9 | ) 10 | 11 | "In von Cammerer (2000)" 12 | ge_maize2 = :C4 => ( 13 | Vpm25 = 120, Vcm25 = 60, Jm25 = 400, 14 | ) 15 | 16 | "In Kim et al.(2006), under elevated CO2, YY" 17 | ge_maize3 = :C4 => ( 18 | Vpm25 = 91.9, Vcm25 = 71.6, Jm25 = 354.2, 19 | Rd25 = 2, # Values in Kim (2006) are for 31C, and the values here are normalized for 25C. SK 20 | ) 21 | 22 | """ 23 | switchgrass params from Albaugha et al. (2014) 24 | https://doi.org/10.1016/j.agrformet.2014.02.013 25 | """ 26 | ge_switchgrass1 = :C4 => (Vpm25 = 52, Vcm25 = 26, Jm25 = 145) 27 | 28 | """ 29 | switchgrass Vcmax from Le et al. (2010), 30 | others multiplied from Vcmax (x2, x5.5) 31 | """ 32 | ge_switchgrass2 = :C4 => (Vpm25 = 96, Vcm25 = 48, Jm25 = 264) 33 | 34 | ge_switchgrass3 = :C4 => (Vpm25 = 100, Vcm25 = 50, Jm25 = 200) 35 | ge_switchgrass4 = :C4 => (Vpm25 = 70, Vcm25 = 50, Jm25 = 180.8) 36 | 37 | "switchgrass params from Albaugha et al. (2014)" 38 | ge_switchgrass_base = :C4 => ( 39 | Rd25 = 3.6, # not sure if it was normalized to 25 C 40 | θ = 0.79, 41 | ) 42 | 43 | "In Sinclair and Horie, Crop Sciences, 1989" 44 | ge_ndep1 = :NitrogenDependence => (s = 4, N0 = 0.2) 45 | 46 | "In J Vos et al. Field Crop Research, 2005" 47 | ge_ndep2 = :NitrogenDependence => (s = 2.9, N0 = 0.25) 48 | 49 | "In Lindquist, Weed Science, 2001" 50 | ge_ndep3 = :NitrogenDependence => (s = 3.689, N0 = 0.5) 51 | 52 | """ 53 | in P. J. Sellers, et al.Science 275, 502 (1997) 54 | g0 is b, of which the value for c4 plant is 0.04 55 | and g1 is m, of which the value for c4 plant is about 4 YY 56 | """ 57 | ge_stomata1 = :StomataBallBerry => (g0 = 0.04, g1 = 4.0) 58 | 59 | """ 60 | Ball-Berry model parameters from Miner and Bauerle 2017, 61 | used to be 0.04 and 4.0, respectively (2018-09-04: KDY) 62 | """ 63 | ge_stomata2 = :StomataBallBerry => (g0 = 0.017, g1 = 4.53) 64 | 65 | "calibrated above for our switchgrass dataset" 66 | ge_stomata3 = :StomataBallBerry => (g0 = 0.04, g1 = 1.89) 67 | ge_stomata4 = :StomataBallBerry => (g0 = 0.02, g1 = 2.0) 68 | 69 | "parameters from Le et. al (2010)" 70 | ge_stomata5 = :StomataBallBerry => (g0 = 0.008, g1 = 8.0) 71 | 72 | "for garlic" 73 | ge_stomata6 = :StomataBallBerry => (g0 = 0.0096, g1 = 6.824) 74 | 75 | ge_water1 = :StomataTuzet => ( 76 | sf = 2.3, # sensitivity parameter Tuzet et al. 2003 Yang 77 | ϕf = -1.2, # reference potential Tuzet et al. 2003 Yang 78 | ) 79 | 80 | "switchgrass params from Le et al. (2010)" 81 | ge_water2 = :StomataTuzet => ( 82 | #? = -1.68, # minimum sustainable leaf water potential (Albaugha 2014) 83 | sf = 6.5, 84 | ϕf = -1.3, 85 | ) 86 | 87 | """ 88 | August-Roche-Magnus formula gives slightly different parameters 89 | https://en.wikipedia.org/wiki/Clausius–Clapeyron_relation 90 | """ 91 | ge_vaporpressure1 = :VaporPressure => ( 92 | a = 0.61094, # kPa 93 | b = 17.625, # C 94 | c = 243.04, # C 95 | ) 96 | 97 | ge_weather = :Weather => ( 98 | PFD = 1500, 99 | CO2 = 400, 100 | RH = 60, 101 | T_air = 30, 102 | wind = 2.0, 103 | ) 104 | 105 | ge_spad = :Nitrogen => ( 106 | _a = 0.0004, 107 | _b = 0.0120, 108 | _c = 0, 109 | SPAD = 60, 110 | ) 111 | 112 | ge_water = :StomataTuzet => ( 113 | #WP_leaf = 0, 114 | sf = 2.3, 115 | Ψf = -1.2, 116 | ) 117 | 118 | ge_base = (ge_weather, ge_spad, ge_water) 119 | 120 | ge_canopy = @config( 121 | ge_base, 122 | :Sun => (; 123 | d = 1, 124 | h = 12, 125 | ), 126 | :Canopy => (; 127 | LAI = 5, 128 | ), 129 | :Radiation => (; 130 | leaf_angle_factor = 3, 131 | leaf_angle = LeafGasExchange.horizontal, 132 | ) 133 | ) 134 | 135 | #HACK: zero CO2 prevents convergence of bisection method 136 | ge_step_c = :Weather => :CO2 => 10:10:1500 137 | ge_step_q = :Weather => :PFD => 0:20:2000 138 | ge_step_t = :Weather => :T_air => -10:1:50 139 | 140 | @testset "gasexchange" begin 141 | @testset "C3" begin 142 | @testset "A-Ci" begin 143 | visualize(LeafGasExchange.ModelC3MD, :Ci, [:A_net, :Ac, :Aj, :Ap]; config=ge_base, xstep=ge_step_c) |> println 144 | end 145 | 146 | @testset "A-Q" begin 147 | visualize(LeafGasExchange.ModelC3MD, :PFD, [:A_net, :Ac, :Aj, :Ap]; config=ge_base, xstep=ge_step_q) |> println 148 | end 149 | 150 | @testset "A-T" begin 151 | visualize(LeafGasExchange.ModelC3MD, :T_air, [:A_net, :Ac, :Aj, :Ap]; config=ge_base, xstep=ge_step_t) |> println 152 | end 153 | end 154 | 155 | @testset "C4" begin 156 | @testset "A-Ci" begin 157 | visualize(LeafGasExchange.ModelC4MD, :Ci, [:A_net, :Ac, :Aj]; config=ge_base, xstep=ge_step_c) |> println 158 | end 159 | 160 | @testset "A-Q" begin 161 | visualize(LeafGasExchange.ModelC4MD, :PFD, [:A_net, :Ac, :Aj]; config=ge_base, xstep=ge_step_q) |> println 162 | end 163 | 164 | @testset "A-T" begin 165 | visualize(LeafGasExchange.ModelC4MD, :T_air, [:A_net, :Ac, :Aj]; config=ge_base, xstep=ge_step_t) |> println 166 | end 167 | end 168 | 169 | @testset "N vs Ψv" begin 170 | visualize(LeafGasExchange.ModelC4MD, :N, :Ψv, :A_net; 171 | config=ge_base, 172 | kind=:heatmap, 173 | xstep=:Nitrogen=>:N=>0:0.05:2, 174 | ystep=:StomataTuzet=>:WP_leaf=>-2:0.05:0, 175 | ) |> display 176 | end 177 | 178 | @testset "canopy" begin 179 | @testset "C3" begin 180 | @testset "CO2" begin 181 | visualize(LeafGasExchange.ModelC3MDC, "weather.CO2", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_c) |> println 182 | end 183 | 184 | @testset "PFD" begin 185 | visualize(LeafGasExchange.ModelC3MDC, "weather.PFD", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_q) |> println 186 | end 187 | 188 | @testset "T_air" begin 189 | visualize(LeafGasExchange.ModelC3MDC, "weather.T_air", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_t) |> println 190 | end 191 | end 192 | 193 | @testset "C4" begin 194 | @testset "CO2" begin 195 | visualize(LeafGasExchange.ModelC4MDC, "weather.CO2", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_c) |> println 196 | end 197 | 198 | @testset "PFD" begin 199 | visualize(LeafGasExchange.ModelC4MDC, "weather.PFD", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_q) |> println 200 | end 201 | 202 | @testset "T_air" begin 203 | visualize(LeafGasExchange.ModelC4MDC, "weather.T_air", [:A_net, "sunlit_gasexchange.A_net", "shaded_gasexchange.A_net"]; config = ge_canopy, xstep = ge_step_t) |> println 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /src/c4.jl: -------------------------------------------------------------------------------- 1 | @system C4Base(CBase) begin 2 | Cm(Ci): mesophyll_co2 ~ track(u"μbar") 3 | 4 | gbs: bundle_sheath_conductance => 0.003 ~ preserve(u"mol/m^2/s/bar" #= CO2 =#, parameter) # bundle sheath conductance to CO2, mol m-2 s-1 5 | # gi => 1.0 ~ preserve(parameter) # conductance to CO2 from intercelluar to mesophyle, mol m-2 s-1, assumed 6 | end 7 | 8 | @system C4c(C4Base) begin 9 | # Kp25: Michaelis constant for PEP caboxylase for CO2 10 | Kp25: pep_carboxylase_constant_for_co2_at_25 => 80 ~ preserve(u"μbar", parameter) 11 | Kp(Kp25, kTQ10): pep_carboxylase_constant_for_co2 => begin 12 | Kp25 * kTQ10 13 | end ~ track(u"μbar") 14 | 15 | Vpm25: maximum_pep_carboxylation_rate_for_co2_at_25 => 70 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 16 | EaVp: activation_energy_for_pep_carboxylation => 75.1 ~ preserve(u"kJ/mol", parameter) 17 | Vpmax(Vpm25, kT, EaVp, kN): maximum_pep_carboxylation_rate => begin 18 | Vpm25 * kT(EaVp) * kN 19 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 20 | 21 | # PEP regeneration limited Vp, value adopted from vC book 22 | Vpr25: pep_regeneration_rate_for_co2_at_25 => 80 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 23 | Vpr(Vpr25, kTQ10): pep_regeneration_rate => begin 24 | Vpr25 * kTQ10 25 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 26 | Vp(Vpmax, Vpr, Cm, Kp): pep_carboxylation_rate => begin 27 | # PEP carboxylation rate, that is the rate of C4 acid generation 28 | (Cm * Vpmax) / (Cm + Kp) 29 | end ~ track(u"μmol/m^2/s" #= CO2 =#, max=Vpr) 30 | 31 | Vcm25: maximum_carboxylation_rate_at_25 => 50 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 32 | # EaVc: Sage (2002) JXB 33 | EaVc: activation_energy_for_carboxylation => 55.9 ~ preserve(u"kJ/mol", parameter) 34 | Vcmax(Vcm25, kT, EaVc, kN): maximum_carboxylation_rate => begin 35 | Vcm25 * kT(EaVc) * kN 36 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 37 | end 38 | 39 | @system C4j(C4Base) begin 40 | Jm25: maximum_electron_transport_rate_at_25 => 300 ~ preserve(u"μmol/m^2/s" #= Electron =#, parameter) 41 | Eaj: activation_enthalpy_for_electron_transport => 32.8 ~ preserve(u"kJ/mol", parameter) 42 | Hj: deactivation_enthalpy_for_electron_transport => 220 ~ preserve(u"kJ/mol", parameter) 43 | Sj: sensitivity_for_electron_transport => 702.6 ~ preserve(u"J/mol/K", parameter) 44 | Jmax(Jm25, kTpeak, Eaj, Hj, Sj, kN): maximum_electron_transport_rate => begin 45 | Jm25 * kTpeak(Eaj, Hj, Sj) * kN 46 | end ~ track(u"μmol/m^2/s" #= Electron =#) 47 | 48 | # θ: sharpness of transition from light limitation to light saturation 49 | θ: light_transition_sharpness => 0.5 ~ preserve(parameter) 50 | J(I2, Jmax, θ): electron_transport_rate => begin 51 | a = θ 52 | b = -(I2+Jmax) 53 | c = I2*Jmax 54 | a*J^2 + b*J + c ⩵ 0 55 | end ~ solve(lower=0, upper=Jmax, u"μmol/m^2/s") 56 | end 57 | 58 | @system C4r(C4Base) begin 59 | # Kc25: Michaelis constant of rubisco for CO2 of C4 plants (2.5 times that of tobacco), ubar, Von Caemmerer 2000 60 | Kc25: rubisco_constant_for_co2_at_25 => 650 ~ preserve(u"μbar", parameter) 61 | Eac: activation_energy_for_co2 => 59.4 ~ preserve(u"kJ/mol", parameter) 62 | Kc(kT, Kc25, Eac): rubisco_constant_for_co2 => begin 63 | Kc25 * kT(Eac) 64 | end ~ track(u"μbar") 65 | 66 | # Ko25: Michaelis constant of rubisco for O2 (2.5 times C3), mbar 67 | Ko25: rubisco_constant_for_o2_at_25 => 450 ~ preserve(u"mbar", parameter) 68 | # Activation energy for Ko, Bernacchi (2001) 69 | Eao: activation_energy_for_o2 => 36 ~ preserve(u"kJ/mol", parameter) 70 | Ko(Ko25, kT, Eao): rubisco_constant_for_o2 => begin 71 | Ko25 * kT(Eao) 72 | end ~ track(u"mbar") 73 | 74 | # mesophyll O2 partial pressure 75 | Om: mesophyll_o2_partial_pressure => 210 ~ preserve(u"mbar", parameter) 76 | Km(Kc, Om, Ko): rubisco_constant_for_co2_with_o2 => begin 77 | # effective M-M constant for Kc in the presence of O2 78 | Kc * (1 + Om / Ko) 79 | end ~ track(u"μbar") 80 | 81 | # Rd25: Values in Kim (2006) are for 31C, and the values here are normalized for 25C. SK 82 | Rd25: dark_respiration_at_25 => 2 ~ preserve(u"μmol/m^2/s" #= CO2 =#, parameter) 83 | Ear: activation_energy_for_respiration => 39.8 ~ preserve(u"kJ/mol", parameter) 84 | Rd(Rd25, kT, Ear): dark_respiration => begin 85 | Rd25 * kT(Ear) 86 | end ~ track(u"μmol/m^2/s") 87 | Rm(Rd) => 0.5Rd ~ track(u"μmol/m^2/s") 88 | end 89 | 90 | @system C4Rate(C4c, C4j, C4r) begin 91 | # Enzyme limited A (Rubisco or PEP carboxylation) 92 | Ac1(Vp, gbs, Cm, Rm) => (Vp + gbs*Cm - Rm) ~ track(u"μmol/m^2/s" #= CO2 =#) 93 | Ac2(Vcmax, Rd) => (Vcmax - Rd) ~ track(u"μmol/m^2/s" #= CO2 =#) 94 | Ac(Ac1, Ac2): enzyme_limited_photosynthesis_rate => begin 95 | #Ac1 = max(0, Ac1) # prevent Ac1 from being negative Yang 9/26/06 96 | min(Ac1, Ac2) 97 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 98 | 99 | # x: Partitioning factor of J, yield maximal J at this value 100 | x: electron_transport_partitioning_factor => 0.4 ~ preserve(parameter) 101 | # Light and electron transport limited A mediated by J 102 | Aj1(x, J, Rm, gbs, Cm) => (x * J/2 - Rm + gbs*Cm) ~ track(u"μmol/m^2/s" #= CO2 =#) 103 | Aj2(x, J, Rd) => (1-x) * J/3 - Rd ~ track(u"μmol/m^2/s" #= CO2 =#) 104 | Aj(Aj1, Aj2): transport_limited_photosynthesis_rate => begin 105 | min(Aj1, Aj2) 106 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 107 | 108 | # smoothing the transition between Ac and Aj 109 | β: photosynthesis_transition_factor => 0.99 ~ preserve(parameter) 110 | A_net(Ac, Aj, β): net_photosynthesis => begin 111 | x = A_net 112 | a = β 113 | b = -(Ac+Aj) 114 | c = Ac*Aj 115 | a*x^2 + b*x + c ⩵ 0 116 | end ~ solve(pick=:minimum, u"μmol/m^2/s") 117 | 118 | A_gross(A_net, Rd): gross_photosynthesis => begin 119 | A_gross = A_net + Rd 120 | # gets negative when PFD = 0, Rd needs to be examined, 10/25/04, SK 121 | #max(A_gross, zero(A_gross)) 122 | end ~ track(u"μmol/m^2/s" #= CO2 =#) 123 | end 124 | 125 | @system C4(C4Rate) begin 126 | #FIXME: currently not used variables 127 | 128 | # FIXME are they even used? 129 | # beta_ABA => 1.48e2 # Tardieu-Davies beta, Dewar (2002) Need the references !? 130 | # delta => -1.0 131 | # alpha_ABA => 1.0e-4 132 | # lambda_r => 4.0e-12 # Dewar's email 133 | # lambda_l => 1.0e-12 134 | # K_max => 6.67e-3 # max. xylem conductance (mol m-2 s-1 MPa-1) from root to leaf, Dewar (2002) 135 | 136 | # alpha: fraction of PSII activity in the bundle sheath cell, very low for NADP-ME types 137 | α: bundle_sheath_PSII_activity_fraction => 0.0001 ~ preserve(parameter) 138 | # Bundle sheath O2 partial pressure, mbar 139 | Os(A_net, gbs, Om, α): bundle_sheath_o2 => begin 140 | α * A_net / (0.047gbs) + Om 141 | end ~ track(u"mbar") 142 | 143 | Cbs(A_net, Vp, Cm, Rm, gbs): bundle_sheath_co2 => begin 144 | Cm + (Vp - A_net - Rm) / gbs # Bundle sheath CO2 partial pressure, ubar 145 | end ~ track(u"μbar") 146 | 147 | # half the reciprocal of rubisco specificity, to account for O2 dependence of CO2 comp point, 148 | # note that this become the same as that in C3 model when multiplied by [O2] 149 | Γ1 => 0.193 ~ preserve(parameter) 150 | Γ★(Γ1, Os) => Γ1 * Os ~ track(u"μbar") 151 | Γ(Rd, Km, Vcmax, Γ★): co2_compensation_point => begin 152 | (Rd*Km + Vcmax*Γ★) / (Vcmax - Rd) 153 | end ~ track(u"μbar") 154 | end 155 | -------------------------------------------------------------------------------- /src/sun.jl: -------------------------------------------------------------------------------- 1 | # Unit to calculate solar geometry including solar elevation, declination, 2 | # azimuth etc using TSolar class. Data are hidden. 03/15/00 SK 3 | # - 1st Revision 10/10/00: Changed to dealing only upto the top of the canopy. Radiation transter within the canopy is now a separate module. 4 | # - added functins to calculate global radiation, atmospheric emissivity, etc as in Spitters et al. (1986), 3/18/01, SK 5 | # 24Dec03, SK 6 | # - added a function to calculate day length based on Campbell and Norman (1998) p 170, 7 | # - added overloads to SetVal 8 | # 2Aug04, SK 9 | # - Translated to C++ from Delphi 10 | # - revised some functions according to "An introduction to solar radiaiton" by Iqbal (1983) 11 | # - (To Do) Add algorithms for instantaneous diffuse and direct radiation predictions from daily global solar radiation for given time 12 | # - (To Do) This can be done by first applying sinusoidal model to the daily data to simulate hourly global solar radiation 13 | # - (To Do) Then the division model of diffuse and direct radiations was applied 14 | # - added direct and diffuse separation functions according to Weiss and Norman (1985), 3/16/05 15 | 16 | import Dates 17 | 18 | @system Sun begin 19 | weather ~ ::Weather(override) 20 | 21 | d: day ~ preserve::int(parameter, u"d") 22 | h: hour ~ preserve::int(parameter, u"hr") 23 | 24 | ϕ: latitude => 37.5u"°" ~ preserve(parameter, u"°") 25 | λ: longitude => 126.9u"°" ~ preserve(parameter, u"°") 26 | alt: altitude => 0 ~ preserve(parameter, u"m") 27 | 28 | Q: photosynthetic_active_radiation_conversion_factor => begin 29 | # 4.55 is a conversion factor from W to photons for solar radiation, Goudriaan and van Laar (1994) 30 | # some use 4.6 i.e., Amthor 1994, McCree 1981, Challa 1995. 31 | 4.6 32 | end ~ preserve(u"μmol/J", parameter) 33 | 34 | solrad(weather.PFD, Q): solar_radiation => begin 35 | PFD / Q 36 | end ~ track(u"W/m^2") 37 | 38 | "atmospheric transmissivity, Goudriaan and van Laar (1994) p 30" 39 | τ: transmissivity => 0.5 ~ preserve(parameter) 40 | 41 | ##################### 42 | # Solar Coordinates # 43 | ##################### 44 | 45 | #HACK always use degrees for consistency and easy tracing 46 | #FIXME pascal version of LightEnv uses iqbal() 47 | δ(declination_angle_spencer): declination_angle ~ track(u"°") 48 | 49 | "Goudriaan 1977" 50 | declination_angle_goudriaan(d) => begin 51 | g = 2pi * (d + 10u"d") / 365u"d" 52 | -23.45u"°" * cos(g) 53 | end ~ track(u"°") 54 | 55 | "Resenberg, blad, verma 1982" 56 | declination_angle_resenberg(d) => begin 57 | g = 2pi * (d - 172u"d") / 365u"d" 58 | 23.5u"°" * cos(g) 59 | end ~ track(u"°") 60 | 61 | "Iqbal (1983) Pg 10 Eqn 1.3.3, and sundesign.com" 62 | declination_angle_iqbal(d) => begin 63 | g = 2pi * (d + 284u"d") / 365u"d" 64 | 23.45u"°" * sin(g) 65 | end ~ track(u"°") 66 | 67 | "Campbell and Norman, p168" 68 | declination_angle_campbell(d) => begin 69 | a = deg2rad(356.6 + 0.9856u"d^-1" * d) 70 | b = deg2rad(278.97 + 0.9856u"d^-1" * d + 1.9165sin(a)) 71 | asind(0.39785sin(b)) 72 | end ~ track(u"°") 73 | 74 | "Spencer equation, Iqbal (1983) Pg 7 Eqn 1.3.1. Most accurate among all" 75 | declination_angle_spencer(d) => begin 76 | # gamma: day angle 77 | g = 2pi * (d - 1u"d") / 365u"d" 78 | 0.006918 - 0.399912cos(g) + 0.070257sin(g) - 0.006758cos(2g) + 0.000907sin(2g) -0.002697cos(3g) + 0.00148sin(3g) 79 | end ~ track(u"rad") 80 | 81 | dph: degree_per_hour => 360u"°" / 24u"hr" ~ preserve(u"°/hr") 82 | 83 | "longitude correction for Light noon, Wohlfart et al, 2000; Campbell & Norman 1998" 84 | LC(λ, dph): longitude_correction => begin 85 | # standard meridian for pacific time zone is 120 W, Eastern Time zone : 75W 86 | # LC is positive if local meridian is east of standard meridian, i.e., 76E is east of 75E 87 | #standard_meridian = -120 88 | meridian = round(u"hr", λ / dph) * dph 89 | #FIXME use standard longitude sign convention 90 | #(long - meridian) / dph 91 | #HACK this assumes inverted longitude sign that MAIZSIM uses 92 | (meridian - λ) / dph 93 | end ~ track(u"hr") 94 | 95 | ET(d): equation_of_time => begin 96 | f = (279.575 + 0.9856u"d^-1" * d)*u"°" 97 | (-104.7sin(f) + 596.2sin(2f) + 4.3sin(3f) - 12.7sin(4f) -429.3cos(f) - 2.0cos(2f) + 19.3cos(3f)) / (60 * 60) 98 | end ~ track(u"hr") 99 | 100 | solar_noon(LC, ET) => 12u"hr" - LC - ET ~ track(u"hr") 101 | 102 | "θs: zenith angle" 103 | hour_angle_at(ϕ, δ; θs(u"°")) => begin 104 | # this value should never become negative because -90 <= latitude <= 90 and -23.45 < decl < 23.45 105 | #HACK is this really needed for crop models? 106 | # preventing division by zero for N and S poles 107 | #denom = fmax(denom, 0.0001) 108 | # sunrise/sunset hour angle 109 | #TODO need to deal with lat_bound to prevent tan(90)? 110 | #lat_bound = radians(68)? radians(85)? 111 | # cos(h0) at cos(theta_s) = 0 (solar zenith angle = 90 deg == elevation angle = 0 deg) 112 | #-tan(ϕ) * tan(δ) 113 | c = (cos(θs) - sin(ϕ) * sin(δ)) / (cos(ϕ) * cos(δ)) 114 | # c > 1: in the polar region during the winter, sun does not rise 115 | # c < -1: white nights during the summer in the polar region 116 | c = clamp(c, -1, 1) 117 | acosd(c) 118 | end ~ call(u"°") 119 | 120 | hour_angle_at_horizon(hour_angle_at) => hour_angle_at(90u"°") ~ track(u"°") 121 | 122 | "from Iqbal (1983) p 16" 123 | half_day_length(hour_angle_at_horizon, dph) => (hour_angle_at_horizon / dph) ~ track(u"hr") 124 | day_length(half_day_length) => 2half_day_length ~ track(u"hr") 125 | 126 | sunrise(solar_noon, half_day_length) => (solar_noon - half_day_length) ~ track(u"hr") 127 | sunset(solar_noon, half_day_length) => (solar_noon + half_day_length) ~ track(u"hr") 128 | 129 | hour_angle(h, solar_noon, dph) => ((h - solar_noon) * dph) ~ track(u"°") 130 | 131 | αs(h=hour_angle, δ, ϕ): elevation_angle => begin 132 | #FIXME When time gets the same as solarnoon, this function fails. 3/11/01 ?? 133 | asind(cos(h) * cos(δ) * cos(ϕ) + sin(δ) * sin(ϕ)) 134 | end ~ track(u"°") 135 | 136 | ts(αs): positive_elevation_angle ~ track(u"°", min=0) 137 | 138 | θs(αs): zenith_angle => (90u"°" - αs) ~ track(u"°") 139 | 140 | """ 141 | The solar azimuth angle is the angular distance between due South and the 142 | projection of the line of sight to the sun on the ground. 143 | View point from south, morning: +, afternoon: - 144 | See An introduction to solar radiation by Iqbal (1983) p 15-16 145 | Also see https://www.susdesign.com/sunangle/ 146 | """ 147 | ϕs(αs, δ, ϕ): azimuth_angle => begin 148 | acosd((sin(δ) - sin(αs) * sin(ϕ)) / (cos(αs) * cos(ϕ))) 149 | end ~ track(u"°") 150 | 151 | ################### 152 | # Solar Radiation # 153 | ################### 154 | 155 | "atmospheric pressure in kPa; campbell and Norman (1998), p 41" 156 | p(altitude): atmospheric_pressure => begin 157 | 101.3exp(-altitude / 8200u"m") 158 | end ~ track(u"kPa") 159 | 160 | m(p, ts): optical_air_mass_number => begin 161 | #FIXME check 101.3 is indeed in kPa 162 | #iszero(t_s) ? 0. : p / (101.3u"kPa" * sin(t_s)) 163 | p / (101.3u"kPa" * sin(ts)) 164 | end ~ track 165 | 166 | SC: solar_constant => 1370 ~ preserve(u"W/m^2", parameter) 167 | 168 | "Campbell and Norman's global solar radiation" 169 | insolation(ts, d, SC) => begin 170 | # solar constant, Iqbal (1983) 171 | #FIXME better to be 1361 or 1362 W/m-2? 172 | g = 2pi * (d - 10u"d") / 365u"d" 173 | SC * sin(ts) * (1 + 0.033cos(g)) 174 | end ~ track(u"W/m^2") 175 | 176 | directional_solar_radiation(Fdir, solar_radiation) => begin 177 | Fdir * solar_radiation 178 | end ~ track(u"W/m^2") 179 | 180 | diffusive_solar_radiation(Fdif, solar_radiation) => begin 181 | Fdif * solar_radiation 182 | end ~ track(u"W/m^2") 183 | 184 | Fdir(τ, m): directional_coeff => begin 185 | # Goudriaan and van Laar's global solar radiation 186 | #FIXME should be goudriaan() version 187 | goudriaan(τ) = τ * (1 - diffusive_coeff) 188 | #FIXME: check if equation is same as campbell() 189 | # Takakura (1993), p 5.11 190 | takakura(τ, m) = τ^m 191 | # Campbell and Norman (1998), p 173 192 | campbell(τ, m) = τ^m 193 | campbell(τ, m) 194 | end ~ track 195 | 196 | "Fraction of diffused light" 197 | Fdif(τ, m): diffusive_coeff => begin 198 | # Goudriaan and van Laar's global solar radiation 199 | goudriaan(τ) = begin 200 | # clear sky : 20% diffuse 201 | if τ >= 0.7 202 | 0.2 203 | # cloudy sky: 100% diffuse 204 | elseif τ <= 0.3 205 | 1 206 | # inbetween 207 | else 208 | 1.6 - 2τ 209 | end 210 | end 211 | # Takakura (1993), p 5.11 212 | takakura(τ, m) = (1 - τ^m) / (1 - 1.4log(τ)) / 2 213 | # Campbell and Norman (1998), p 173 214 | campbell(τ, m) = 0.3(1 - τ^m) 215 | campbell(τ, m) 216 | end ~ track 217 | 218 | directional_fraction(Fdif, Fdir) => (1 / (1 + Fdif/Fdir)) ~ track 219 | diffusive_fraction(Fdir, Fdif) => (1 / (1 + Fdir/Fdif)) ~ track 220 | 221 | # PARfr 222 | #TODO better naming: extinction? transmitted_fraction? 223 | PARfr(τ): photosynthetic_coeff => begin 224 | #if self.elevation_angle <= 0: 225 | # 0 226 | #TODO: implement Weiss and Norman (1985), 3/16/05 227 | weiss() = nothing 228 | # Goudriaan and van Laar (1994) 229 | goudriaan(τ) = begin 230 | # clear sky (τ >= 0.7): 45% is PAR 231 | if τ >= 0.7 232 | 0.45 233 | # cloudy sky (τ <= 0.3): 55% is PAR 234 | elseif τ <= 0.3 235 | 0.55 236 | else 237 | 0.625 - 0.25τ 238 | end 239 | end 240 | goudriaan(τ) 241 | end ~ track 242 | 243 | "total PAR (umol m-2 s-1) on horizontal surface (PFD)" 244 | PARtot(solar_radiation, PARfr, Q): photosynthetic_active_radiation_total => begin 245 | # 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, 246 | # see Campbell and Norman (1994) p 149 247 | solar_radiation * PARfr * Q 248 | end ~ track(u"μmol/m^2/s") # Quanta 249 | 250 | PARdir(PARtot, directional_fraction): directional_photosynthetic_radiation => (directional_fraction * PARtot) ~ track(u"μmol/m^2/s") # Quanta 251 | 252 | PARdif(PARtot, diffusive_fraction): diffusive_photosynthetic_radiation => (diffusive_fraction * PARtot) ~ track(u"μmol/m^2/s") # Quanta 253 | end 254 | -------------------------------------------------------------------------------- /src/radiation.jl: -------------------------------------------------------------------------------- 1 | # Basic canopy architecture parameters, 10/10/00 S.Kim 2 | # modified to represent heterogeneous canopies 3 | # Uniform continuouse canopy: Width2 = 0 4 | # Hedgerow canopy : Width1 = row width, Width2 = interrow, Height1 = hedge height, Height2 = 0 5 | # Intercropping canopy: Height1, Width1, LA1 for Crop1, and so on 6 | # Rose bent canopy: Height1=Upright canopy, Height2 = bent portion height, 10/16/02 S.Kim 7 | 8 | # absorptance not explicitly considered here because it's a leaf characteristic not canopy 9 | # Scattering is considered as canopy characteristics and reflectance is computed based on canopy scattering 10 | # 08-20-12, SK 11 | 12 | @enum LeafAngle begin 13 | spherical = 1 14 | horizontal = 2 15 | vertical = 3 16 | diaheliotropic = 4 17 | empirical = 5 18 | ellipsoidal = 6 19 | end 20 | 21 | #HACK not used 22 | @enum Cover begin 23 | glass = 1 24 | acrylic = 2 25 | polyethyl = 3 26 | doublepoly = 4 27 | whitewashed = 5 28 | no_cover = 6 29 | end 30 | 31 | @enum WaveBand begin 32 | photosynthetically_active_radiation = 1 33 | near_infrared = 2 34 | longwave = 3 35 | end 36 | 37 | @system Radiation begin 38 | sun ~ ::Sun(override) 39 | LAI: leaf_area_index ~ track(override) 40 | 41 | leaf_angle => ellipsoidal ~ preserve::LeafAngle(parameter) 42 | 43 | "ratio of horizontal to vertical axis of an ellipsoid" 44 | LAF: leaf_angle_factor => begin 45 | #1 46 | # leaf angle factor for corn leaves, Campbell and Norman (1998) 47 | #1.37 48 | # leaf angle factor for garlic canopy, from Rizzalli et al. (2002), X factor in Campbell and Norman (1998) 49 | 0.7 50 | end ~ preserve(parameter) 51 | 52 | wave_band => photosynthetically_active_radiation ~ preserve::WaveBand(parameter) 53 | 54 | "scattering coefficient (reflectance + transmittance)" 55 | s: scattering => 0.15 ~ preserve(parameter) 56 | 57 | "clumping index" 58 | clumping => 1.0 ~ preserve(parameter) 59 | 60 | #FIXME reflectance? 61 | #r_h => 0.05 ~ preserve(parameter) 62 | 63 | # Forward from Sun 64 | 65 | #TODO better name to make it drive? 66 | current_zenith_angle(sun.zenith_angle) ~ track(u"°") 67 | elevation_angle(sun.αs) ~ track(u"°") 68 | I0_dr(sun.PARdir): directional_photosynthetic_radiation ~ track(u"μmol/m^2/s" #= Quanta =#) 69 | I0_df(sun.PARdif): diffusive_photosynthetic_radiation ~ track(u"μmol/m^2/s" #= Quanta =#) 70 | 71 | # Leaf angle stuff? 72 | 73 | #TODO better name? 74 | leaf_angle_coeff(a=leaf_angle, leaf_angle_factor; zenith_angle(u"°")) => begin 75 | elevation_angle = 90u"°" - zenith_angle 76 | #FIXME need to prevent zero like sin_beta / cot_beta? 77 | α = elevation_angle 78 | t = zenith_angle 79 | # leaf angle distribution parameter 80 | x = leaf_angle_factor 81 | if a == spherical 82 | # When Lt accounts for total path length, division by sin(elev) isn't necessary 83 | 1 / (2sin(α)) 84 | elseif a == horizontal 85 | 1. 86 | elseif a == vertical 87 | 1 / (tan(α) * π/2) 88 | elseif a == empirical 89 | 0.667 90 | elseif a == diaheliotropic 91 | 1 / sin(α) 92 | elseif a == ellipsoidal 93 | sqrt(x^2 + tan(t)^2) / (x + 1.774 * (x+1.182)^-0.733) 94 | else 95 | 1. 96 | end 97 | end ~ call 98 | 99 | #TODO make it @property if arg is not needed 100 | # Kb: Campbell, p 253, Ratio of projected area to hemi-surface area for an ellipsoid 101 | #TODO rename to extinction_coeff? 102 | "extinction coefficient assuming spherical leaf dist" 103 | Kb_at(leaf_angle_coeff, clumping; zenith_angle(u"°")): projection_ratio_at => begin 104 | leaf_angle_coeff(zenith_angle) * clumping 105 | end ~ call 106 | 107 | Kb(Kb_at, current_zenith_angle): projection_ratio => begin 108 | Kb_at(current_zenith_angle) 109 | end ~ track 110 | 111 | "diffused light ratio to ambient, integrated over all incident angles from 0 to 90" 112 | Kd_F(leaf_angle_coeff, LAI; a): diffused_fraction_for_Kd => begin 113 | c = leaf_angle_coeff(a) 114 | x = exp(-c * LAI) 115 | # Why multiplied by 2? 116 | 2x * sin(a) * cos(a) 117 | end ~ integrate(from=0, to=π/2) 118 | 119 | "K for diffuse light, the same literature as above" 120 | Kd(F=Kd_F, LAI, clumping): diffusion_ratio => begin 121 | K = -log(F) / LAI 122 | K * clumping 123 | end ~ track 124 | 125 | ############################### 126 | # de Pury and Farquhar (1997) # 127 | ############################### 128 | 129 | #TODO better name 130 | "Kb prime in de Pury and Farquhar(1997)" 131 | Kb1(Kb, s): projection_ratio_prime => (Kb * sqrt(1 - s)) ~ track 132 | 133 | #TODO better name 134 | "Kd1: Kd prime in de Pury and Farquhar(1997)" 135 | Kd1(Kd, s): diffusion_ratio_prime => (Kd * sqrt(1 - s)) ~ track 136 | 137 | ################ 138 | # Reflectivity # 139 | ################ 140 | 141 | #TODO better name? 142 | reflectivity(rho_h, Kb, Kd) => begin 143 | rho_h * (2Kb / (Kb + Kd)) 144 | end ~ track 145 | 146 | # rho_h: canopy reflectance of beam irradiance on horizontal leaves, de Pury and Farquhar (1997) 147 | # also see Campbell and Norman (1998) p 255 for further info on potential problems 148 | """ 149 | canopy reflection coefficients for beam horizontal leaves, beam uniform leaves, and diffuse radiations 150 | """ 151 | rho_h(s): canopy_reflectivity_horizontal_leaf => begin 152 | (1 - sqrt(1 - s)) / (1 + sqrt(1 - s)) 153 | end ~ track 154 | 155 | #TODO make consistent interface with siblings 156 | "canopy reflectance of beam irradiance for uniform leaf angle distribution, de Pury and Farquhar (1997)" 157 | rho_cb_at(rho_h, Kb_at; zenith_angle): canopy_reflectivity_uniform_leaf_at => begin 158 | Kb = Kb_at(zenith_angle) 159 | 1 - exp(-2rho_h * Kb / (1 + Kb)) 160 | end ~ call 161 | 162 | rho_cb(rho_cb_at, current_zenith_angle): canopy_reflectivity_uniform_leaf => begin 163 | rho_cb_at(current_zenith_angle) 164 | end ~ track 165 | 166 | rho_cd_F(rho_cb_at; a): diffused_fraction_for_rho_cd => begin 167 | x = rho_cb_at(a) 168 | # Why multiplied by 2? 169 | 2x * sin(a) * cos(a) 170 | end ~ integrate(from=0, to=π/2) 171 | 172 | "canopy reflectance of diffuse irradiance, de Pury and Farquhar (1997) Table A2" 173 | rho_cd(I0_df, rho_cd_F): canopy_reflectivity_diffusion => begin 174 | # Probably the eqn A21 in de Pury is missing the integration terms of the angles?? 175 | iszero(I0_df) ? 0. : rho_cd_F 176 | end ~ track 177 | 178 | "soil reflectivity for PAR band" 179 | rho_soil: soil_reflectivity => 0.10 ~ preserve(parameter) 180 | 181 | ####################### 182 | # I_l?: dePury (1997) # 183 | ####################### 184 | 185 | "dePury (1997) eqn A3" 186 | I_lb(I0_dr, rho_cb, Kb1; L): irradiance_lb => begin 187 | I0_dr * (1 - rho_cb) * Kb1 * exp(-Kb1 * L) 188 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 189 | 190 | "dePury (1997) eqn A5" 191 | I_ld(I0_df, rho_cb, Kd1; L): irradiance_ld => begin 192 | I0_df * (1 - rho_cb) * Kd1 * exp(-Kd1 * L) 193 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 194 | 195 | "dePury (1997) eqn A5" 196 | I_l(; L): irradiance_l => (I_lb(L) + I_ld(L)) ~ call(u"μmol/m^2/s" #= Quanta =#) 197 | 198 | "dePury (1997) eqn A5" 199 | I_lbSun(I0_dr, s, Kb, I_lSh; L): irradiance_l_sunlit => begin 200 | I_lb_sunlit = I0_dr * (1 - s) * Kb 201 | #TODO: check name I_lbSun vs. I_l_sunlit? 202 | I_l_sunlit = I_lb_sunlit + I_lSh(L) 203 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 204 | 205 | "dePury (1997) eqn A5" 206 | I_lSh(I_ld, I_lbs; L): irradiance_l_shaded => begin 207 | I_ld(L) + I_lbs(L) 208 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 209 | 210 | #FIXME: check name I_lbs vs. I_lbSun 211 | "dePury (1997) eqn A5" 212 | I_lbs(I0_dr, rho_cb, s, Kb1, Kb; L): irradiance_lbs => begin 213 | I0_dr * ((1 - rho_cb) * Kb1 * exp(-Kb1 * L) - (1 - s) * Kb * exp(-Kb * L)) 214 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 215 | 216 | """ 217 | total irradiance at the top of the canopy, 218 | passed over from either observed PAR or TSolar or TIrradiance 219 | """ 220 | I0_tot(I0_dr, I0_df): irradiance_I0_tot => (I0_dr + I0_df) ~ track(u"μmol/m^2/s" #= Quanta =#) 221 | 222 | ######## 223 | # I_c? # 224 | ######## 225 | 226 | # I_tot, I_sun, I_shade: absorbed irradiance integrated over LAI per ground area 227 | #FIXME: not used, but seems producing very low values, need to check equations 228 | 229 | "Total irradiance absorbed by the canopy, de Pury and Farquhar (1997)" 230 | I_c(rho_cb, I0_dr, I0_df, Kb1, Kd1, LAI): canopy_irradiance => begin 231 | #I_c = I_cSun + I_cSh 232 | I(I0, K) = (1 - rho_cb) * I0 * (1 - exp(-K * LAI)) 233 | I_tot = I(I0_dr, Kb1) + I(I0_df, Kd1) 234 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 235 | 236 | "The irradiance absorbed by the sunlit fraction, de Pury and Farquhar (1997)" 237 | I_cSun(s, rho_cb, rho_cd, I0_dr, I0_df, Kb, Kb1, Kd1, LAI): canopy_sunlit_irradiance => begin 238 | # should this be the same os Qsl? 03/02/08 SK 239 | I_c_sunlit = begin 240 | I0_dr * (1 - s) * (1 - exp(-Kb * LAI)) + 241 | I0_df * (1 - rho_cd) * (1 - exp(-(Kd1 + Kb) * LAI)) * Kd1 / (Kd1 + Kb) + 242 | I0_dr * ((1 - rho_cb) * (1 - exp(-(Kb1 + Kb) * LAI)) * Kb1 / (Kb1 + Kb) - (1 - s) * (1 - exp(-2Kb * LAI)) / 2) 243 | end 244 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 245 | 246 | "The irradiance absorbed by the shaded fraction, de Pury and Farquhar (1997)" 247 | I_cSh(I_c, I_cSun): canopy_shaded_irradiance => (I_c - I_cSun) ~ track(u"μmol/m^2/s" #= Quanta =#) 248 | 249 | ###### 250 | # Q? # 251 | ###### 252 | 253 | # sunlit_photon_flux_density(_sunlit_Q) ~ track 254 | # shaded_photon_flux_density(_shaded_Q) ~ track 255 | 256 | "total irradiance (dir + dif) at depth L, simple empirical approach" 257 | Q_tot(I0_tot, s, Kb, Kd; L): irradiance_Q_tot => begin 258 | I0_tot * exp(-sqrt(1 - s) * ((Kb + Kd) / 2) * L) 259 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 260 | 261 | "total beam radiation at depth L" 262 | Q_bt(I0_dr, s, Kb; L): irradiance_Q_bt => begin 263 | I0_dr * exp(-sqrt(1 - s) * Kb * L) 264 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 265 | 266 | "net diffuse flux at depth of L within canopy" 267 | Q_d(I0_dr, s, Kd; L): irradiance_Q_d => begin 268 | I0_df * exp(-sqrt(1 - s) * Kd * L) 269 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 270 | 271 | """ 272 | weighted average absorbed diffuse flux over depth of L within canopy 273 | accounting for exponential decay, Campbell p261 274 | """ 275 | Q_dm(LAI, I0_df, s, Kd): irradiance_Q_dm => begin 276 | # Integral Qd / Integral L 277 | Q = I0_df * (1 - exp(-sqrt(1 - s) * Kd * LAI)) / (sqrt(1 - s) * Kd * LAI) 278 | isnan(Q) ? zero(Q) : Q 279 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 280 | 281 | "unintercepted beam (direct beam) flux at depth of L within canopy" 282 | Q_b(I0_dr, Kb; L): irradiance_Q_b => begin 283 | I0_dr * exp(-Kb * L) 284 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 285 | 286 | "mean flux density on sunlit leaves" 287 | Q_sun(I0_dr, Kb, Q_sh): irradiance_Q_sunlit => begin 288 | I0_dr * Kb + Q_sh 289 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 290 | 291 | "flux density on sunlit leaves at depth L" 292 | Q_sun_at(I0_dr, Kb; L): irradiance_Q_sunlit_at => begin 293 | I0_dr * Kb + Q_sh_at(L) 294 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 295 | 296 | "mean flux density on shaded leaves over LAI" 297 | Q_sh(Q_dm, Q_scm): irradiance_Q_shaded => begin 298 | # It does not include soil reflection 299 | Q_dm + Q_scm 300 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 301 | 302 | "diffuse flux density on shaded leaves at depth L" 303 | Q_sh_at(Q_d, Q_sc; L): irradiance_Q_shaded_at => begin 304 | # It does not include soil reflection 305 | Q_d(L) + Q_sc(L) 306 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 307 | 308 | "weighted average of Soil reflectance over canopy accounting for exponential decay" 309 | Q_soilm(LAI, rho_soil, s, Kd, Q_soil): irradiance_Q_soilm => begin 310 | # Integral Qd / Integral L 311 | Q = Q_soil * rho_soil * (1 - exp(-sqrt(1 - s) * Kd * LAI)) / (sqrt(1 - s) * Kd * LAI) 312 | isnan(Q) ? zero(Q) : Q 313 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 314 | 315 | "weighted average scattered radiation within canopy" 316 | Q_scm(LAI, I0_dr, s, Kb): irradiance_Q_scm => begin 317 | # total beam including scattered absorbed by canopy 318 | #FIXME should the last part be multiplied by LAI like others? 319 | #TODO simplify by using existing variables (i.e. Q_bt, Q_b) 320 | total_beam = I0_dr * (1 - exp(-sqrt(1 - s) * Kb * LAI)) / (sqrt(1 - s) * Kb) 321 | # non scattered beam absorbed by canopy 322 | nonscattered_beam = I0_dr * (1 - exp(-Kb * LAI)) / Kb 323 | Q = (total_beam - nonscattered_beam) / LAI 324 | # Campbell and Norman (1998) p 261, Average between top (where scattering is 0) and bottom. 325 | #(self.Q_bt(LAI) - Q_b(LAI)) / 2 326 | isnan(Q) ? zero(Q) : Q 327 | end ~ track(u"μmol/m^2/s" #= Quanta =#) 328 | 329 | "scattered radiation at depth L in the canopy" 330 | Q_sc(Q_bt, Q_b; L): irradiance_Q_sc => begin 331 | # total beam - nonscattered beam at depth L 332 | Q_bt(L) - Q_b(L) 333 | end ~ call(u"μmol/m^2/s" #= Quanta =#) 334 | 335 | "total PFD at the soil surface under the canopy" 336 | Q_soil(LAI, Q_tot): irradiance_Q_soil => Q_tot(LAI) ~ track(u"μmol/m^2/s" #= Quanta =#) 337 | 338 | ################### 339 | # Leaf Area Index # 340 | ################### 341 | 342 | sunrisen(elevation_angle, minimum_elevation_angle=5u"°") => begin 343 | elevation_angle > minimum_elevation_angle 344 | end ~ flag 345 | 346 | "sunlit LAI assuming closed canopy; thus not accurate for row or isolated canopy" 347 | LAI_sunlit(sunrisen, Kb, LAI): sunlit_leaf_area_index => begin 348 | sunrisen ? (1 - exp(-Kb * LAI)) / Kb : 0. 349 | end ~ track 350 | 351 | "shaded LAI assuming closed canopy" 352 | LAI_shaded(LAI, LAI_sunlit): shaded_leaf_area_index => begin 353 | LAI - LAI_sunlit 354 | end ~ track 355 | 356 | "sunlit fraction of current layer" 357 | sunlit_fraction(sunrisen, Kb; L) => begin 358 | sunrisen ? exp(-Kb * L) : 0. 359 | end ~ call 360 | 361 | shaded_fraction(sunlit_fraction; L) => begin 362 | 1 - sunlit_fraction(L) 363 | end ~ call 364 | end 365 | --------------------------------------------------------------------------------