Skip to content

Commit d4dd87d

Browse files
kmdeckAlexisRenchon
authored andcommitted
Optimal LAI model
1 parent d1c6ea9 commit d4dd87d

File tree

12 files changed

+1084
-8
lines changed

12 files changed

+1084
-8
lines changed

docs/list_standalone_models.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ standalone_models = [
2020
]
2121
"Canopy structure" => [
2222
"Prescribed structure" => "standalone/pages/vegetation/canopy_structure/prescribed_structure.md",
23+
"Optimal LAI model" => "standalone/pages/vegetation/canopy_structure/optimal_lai.md",
2324
]
2425
"Canopy energy" => [
2526
"Physics" => "standalone/pages/vegetation/canopy_energy/canopy_energy.md",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Optimal LAI Model
2+
3+
The Optimal LAI model predicts seasonal to decadal dynamics of leaf area index based on optimality principles, balancing energy and water constraints.
4+
5+
This model is based on Zhou et al. (2025), which presents a general model for the seasonal to decadal dynamics of leaf area that combines predictions from both the light use efficiency (LUE) framework and optimization theory.
6+
7+
## Model Overview
8+
9+
The optimal LAI model computes LAI dynamically by:
10+
1. Calculating the seasonal maximum LAI (LAI$_{max}$) based on energy and water limitations
11+
2. Computing steady-state LAI from daily meteorological conditions
12+
3. Updating actual LAI using an exponential moving average to represent the lag in leaf development
13+
14+
## Seasonal Maximum LAI
15+
16+
The seasonal maximum LAI (LAI$_{max}$) is determined by the minimum of energy-limited and water-limited constraints:
17+
18+
```math
19+
\begin{align}
20+
\text{fAPAR}_{max} &= \min\left(\text{fAPAR}_{energy}, \text{fAPAR}_{water}\right) \\
21+
\text{fAPAR}_{energy} &= 1 - \frac{z}{k \cdot A_{0,annual}} \\
22+
\text{fAPAR}_{water} &= \frac{c_a(1-\chi)}{1.6 \cdot D_{growing}} \cdot \frac{f_0 \cdot P_{annual}}{A_{0,annual}} \\
23+
\text{LAI}_{max} &= -\frac{1}{k} \ln(1 - \text{fAPAR}_{max})
24+
\end{align}
25+
```
26+
27+
where:
28+
- $A_{0,annual}$ is the annual total potential GPP (mol m⁻² yr⁻¹)
29+
- $P_{annual}$ is the annual total precipitation (mol m⁻² yr⁻¹)
30+
- $D_{growing}$ is the mean vapor pressure deficit during the growing season (Pa)
31+
- $k$ is the light extinction coefficient (dimensionless)
32+
- $z$ is the unit cost of constructing and maintaining leaves (mol m⁻² yr⁻¹)
33+
- $c_a$ is the ambient CO₂ partial pressure (Pa)
34+
- $\chi$ is the ratio of leaf-internal to ambient CO₂ partial pressure (dimensionless)
35+
- $f_0$ is the fraction of annual precipitation used by plants (dimensionless)
36+
37+
## Daily Steady-State LAI
38+
39+
Given daily meteorological conditions, the steady-state LAI ($L_s$) represents the LAI that would be in equilibrium with GPP if conditions were held constant:
40+
41+
```math
42+
\begin{align}
43+
\mu &= m \cdot A_{0,daily} \\
44+
L_s &= \min\left\{\mu + \frac{1}{k} W_0[-k\mu \exp(-k\mu)], \text{LAI}_{max}\right\}
45+
\end{align}
46+
```
47+
48+
where:
49+
- $A_{0,daily}$ is the daily potential GPP (mol m⁻² day⁻¹)
50+
- $m$ is a parameter relating steady-state LAI to steady-state GPP, computed as:
51+
52+
```math
53+
m = \frac{\sigma \cdot \text{GSL} \cdot \text{LAI}_{max}}{A_{0,annual} \cdot \text{fAPAR}_{max}}
54+
```
55+
56+
where GSL is the growing season length (days) and $\sigma$ is a dimensionless parameter representing departure from square-wave LAI dynamics.
57+
58+
- $W_0$ is the principal branch of the Lambert W function
59+
60+
## LAI Update
61+
62+
The actual LAI is updated using an exponential weighted moving average to represent the time lag for photosynthate allocation to leaves:
63+
64+
```math
65+
\text{LAI}_{new} = \alpha \cdot L_s + (1-\alpha) \cdot \text{LAI}_{prev}
66+
```
67+
68+
where $\alpha$ is a smoothing factor (dimensionless, 0-1). Setting $\alpha = 0.067$ corresponds to approximately 15 days of memory.
69+
70+
## Parameters
71+
72+
| Parameter | Symbol | Unit | Typical Value | Description |
73+
| :--- | :---: | :---: | :---: | :--- |
74+
| Light extinction coefficient | $k$ | - | 0.5 | Controls light attenuation through canopy |
75+
| Leaf construction cost | $z$ | mol m⁻² yr⁻¹ | 12.227 | Unit cost of building and maintaining leaves |
76+
| CO₂ concentration ratio | $\chi$ | - | 0.7 | Ratio of leaf-internal to ambient CO₂ |
77+
| Precipitation fraction | $f_0$ | - | 0.62 | Fraction of precipitation used by plants |
78+
| LAI dynamics parameter | $\sigma$ | - | 0.771 | Departure from square-wave dynamics |
79+
| Smoothing factor | $\alpha$ | - | 0.067 | Controls LAI response time (~15 days) |
80+
81+
## Drivers
82+
83+
| Driver | Symbol | Unit | Description |
84+
| :--- | :---: | :---: | :--- |
85+
| Daily potential GPP | $A_{0,daily}$ | mol m⁻² day⁻¹ | GPP with fAPAR = 1 |
86+
| Annual potential GPP | $A_{0,annual}$ | mol m⁻² yr⁻¹ | Yearly integral of $A_0$ |
87+
| Annual precipitation | $P_{annual}$ | mol m⁻² yr⁻¹ | Total yearly precipitation |
88+
| Growing season VPD | $D_{growing}$ | Pa | Mean VPD when T > 0°C |
89+
| Growing season length | GSL | days | Length of continuous period T > 0°C |
90+
| CO₂ partial pressure | $c_a$ | Pa | Ambient CO₂ concentration |
91+
92+
## Output
93+
94+
| Output | Symbol | Unit | Range |
95+
| :--- | :---: | :---: | :---: |
96+
| Leaf Area Index | LAI | m² m⁻² | 0-10 |
97+
98+
## References
99+
100+
Zhou et al. (2025) "A General Model for the Seasonal to Decadal Dynamics of Leaf Area"
101+
Global Change Biology. https://onlinelibrary.wiley.com/doi/pdf/10.1111/gcb.70125

experiments/integrated/fluxnet/ozark_pmodel.jl

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ radiation_parameters = (;
163163
Ω,
164164
G_Function = CLMGFunction(χl),
165165
α_PAR_leaf,
166-
τ_PAR_leaf,
166+
#τ_PAR_leaf,
167167
α_NIR_leaf,
168-
τ_NIR_leaf,
168+
#τ_NIR_leaf,
169169
)
170-
radiative_transfer = Canopy.TwoStreamModel{FT}(
170+
radiative_transfer = Canopy.BeerLambertModel{FT}(
171171
surface_domain,
172172
toml_dict;
173173
radiation_parameters,
@@ -181,6 +181,11 @@ conductance = PModelConductance{FT}(toml_dict)
181181
is_c3 = FT(1)
182182
photosynthesis = PModel{FT}(surface_domain, toml_dict; is_c3)
183183

184+
# Set up optimal LAI model
185+
lai_model = Canopy.OptimalLAIModel{FT}(
186+
Canopy.OptimalLAIParameters{FT}(toml_dict)
187+
)
188+
184189
# Set up soil moisture stress using soil retention parameters
185190
soil_moisture_stress = PiecewiseMoistureStressModel{FT}(
186191
land_domain,
@@ -230,6 +235,7 @@ canopy = Canopy.CanopyModel{FT}(
230235
radiative_transfer,
231236
photosynthesis,
232237
conductance,
238+
lai_model,
233239
soil_moisture_stress,
234240
hydraulics,
235241
energy,
@@ -255,7 +261,7 @@ set_ic! = FluxnetSimulations.make_set_fluxnet_initial_conditions(
255261
land,
256262
)
257263
# Callbacks
258-
output_vars = ["gpp", "shf", "lhf", "swu", "lwu", "swc", "swe", "tsoil"]
264+
output_vars = ["gpp", "shf", "lhf", "swu", "lwu", "swc", "swe", "tsoil", "lai", "lai_pred"]
259265
diags = ClimaLand.default_diagnostics(
260266
land,
261267
start_date;
@@ -277,6 +283,7 @@ simulation = LandSimulation(
277283

278284
@time solve!(simulation)
279285

286+
#=
280287
comparison_data = FluxnetSimulations.get_comparison_data(site_ID, time_offset)
281288
savedir = joinpath(
282289
pkgdir(ClimaLand),
@@ -301,3 +308,57 @@ LandSimVis.make_timeseries(
301308
spinup_date = start_date + Day(20),
302309
comparison_data,
303310
)
311+
=#
312+
313+
# Need to solve!
314+
# can also look at simulation._integrator.p
315+
316+
short_names = [d.variable.short_name for d in simulation.diagnostics] # short_name_X_average e.g.
317+
diag_names = [d.output_short_name for d in simulation.diagnostics] # short_name_X_average e.g.
318+
diag_units = [d.variable.units for d in simulation.diagnostics]
319+
i = 10
320+
dn = diag_names[i]
321+
unit = diag_units[i]
322+
sn = short_names[i]
323+
model_time, LAI_opt = ClimaLand.Diagnostics.diagnostic_as_vectors(
324+
simulation.diagnostics[1].output_writer,
325+
dn,
326+
)
327+
LAI_opt
328+
329+
330+
i = 9
331+
dn = diag_names[i]
332+
unit = diag_units[i]
333+
sn = short_names[i]
334+
model_time, LAI_obs = ClimaLand.Diagnostics.diagnostic_as_vectors(
335+
simulation.diagnostics[1].output_writer,
336+
dn,
337+
)
338+
LAI_obs
339+
340+
save_Δt = model_time[2] - model_time[1] # in seconds since the start_date. if model_time is an Itime, the epoch should be start_date
341+
import ClimaUtilities.TimeManager: ITime, date
342+
function time_to_date(t::ITime, start_date)
343+
start_date != t.epoch &&
344+
@warn("$(start_date) is different from the simulation time epoch.")
345+
return isnothing(t.epoch) ? start_date + t.counter * t.period : date(t)
346+
end
347+
model_dates = time_to_date.(model_time, start_date)
348+
349+
350+
fig = Figure()
351+
ax = Axis(fig[1,1])
352+
p = lines!(ax, model_dates, LAI_obs, label = "LAI obs")
353+
p2 = lines!(ax, model_dates, LAI_opt, label = "LAI opt")
354+
ax.ylabel= "LAI m2 m-2"
355+
axislegend()
356+
save("test.png", fig)
357+
358+
359+
# Thoughts:
360+
# it makes no sense to have LAI < 0
361+
# maybe params (α, z, m) are not adequate with our units of A (GPP)
362+
# Solutions:
363+
# we could convert A internally
364+
# and force L to be 0 min

src/diagnostics/default_diagnostics.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ function get_possible_diagnostics(model::CanopyModel)
397397
# "fa", # return a Tuple
398398
"far",
399399
"lai",
400+
"lai_pred",
400401
"msf",
401402
"rai",
402403
"sai",
@@ -528,7 +529,7 @@ function get_short_diagnostics(model::SoilCO2Model)
528529
return ["sco2"]
529530
end
530531
function get_short_diagnostics(model::CanopyModel)
531-
return ["gpp", "ct", "lai", "trans", "er", "sif"]
532+
return ["gpp", "ct", "lai", "lai_pred","trans", "er", "sif"]
532533
end
533534
function get_short_diagnostics(model::SnowModel)
534535
return get_possible_diagnostics(model)

src/diagnostics/define_diagnostics.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ function define_diagnostics!(land_model)
309309
compute_leaf_area_index!(out, Y, p, t, land_model),
310310
)
311311

312+
add_diagnostic_variable!(
313+
short_name = "lai_pred",
314+
long_name = "Leaf area Index",
315+
standard_name = "leaf_area_index",
316+
units = "m^2 m^-2",
317+
comments = "The area index of leaves, expressed in surface area of leaves per surface area of ground.",
318+
compute! = (out, Y, p, t) ->
319+
compute_leaf_area_index_pred!(out, Y, p, t, land_model),
320+
)
321+
312322
# Moisture stress factor
313323
add_diagnostic_variable!(
314324
short_name = "msf",

src/diagnostics/land_compute_methods.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ end
257257
LandModel,
258258
CanopyModel,
259259
} p.canopy.biomass.area_index.leaf
260+
@diagnostic_compute "leaf_area_index_pred" Union{
261+
SoilCanopyModel,
262+
LandModel,
263+
CanopyModel,
264+
} p.canopy.lai_model.LAI
260265

261266
# Canopy - Soil moisture stress
262267
@diagnostic_compute "moisture_stress_factor" Union{

src/standalone/Vegetation/Canopy.jl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ include("./stomatalconductance.jl")
4848
include("./photosynthesis.jl")
4949
include("./photosynthesis_farquhar.jl")
5050
include("./pmodel.jl")
51+
include("./optimal_lai.jl")
5152
include("./radiation.jl")
5253
include("./solar_induced_fluorescence.jl")
5354
include("./pfts.jl")
@@ -98,8 +99,8 @@ and wilting point (θ_low) from the soil parameters.
9899
The low and high thresholds for the piecewise soil moisture stress function are given by
99100
the residual soil water content and the soil porosity, respectively.
100101
101-
The soil parameters should be a named tuple with keys of `ν` and `θ_r`
102-
(additional keys may be present but they will not be used). These may
102+
The soil parameters should be a named tuple with keys of `ν` and `θ_r`
103+
(additional keys may be present but they will not be used). These may
103104
be ClimaCore fields or floats, but must be consistently one or the other.
104105
105106
Note that unlike other Canopy components, this model is defined on the subsurface domain.
@@ -606,7 +607,7 @@ treated differently.
606607
607608
$(DocStringExtensions.FIELDS)
608609
"""
609-
struct CanopyModel{FT, AR, RM, PM, SM, SMSM, PHM, EM, SIFM, BM, B, PS, D} <:
610+
struct CanopyModel{FT, AR, RM, PM, SM, SMSM, PHM, EM, SIFM, LAI, BM, B, PS, D} <:
610611
ClimaLand.AbstractImExModel{FT}
611612
"Autotrophic respiration model, a canopy component model"
612613
autotrophic_respiration::AR
@@ -624,6 +625,8 @@ struct CanopyModel{FT, AR, RM, PM, SM, SMSM, PHM, EM, SIFM, BM, B, PS, D} <:
624625
energy::EM
625626
"SIF model, a canopy component model"
626627
sif::SIFM
628+
"LAI model, a canopy component model"
629+
lai_model::LAI
627630
"Biomass parameterization, a canopy component model"
628631
biomass::BM
629632
"Boundary Conditions"
@@ -644,6 +647,7 @@ end
644647
hydraulics::AbstractPlantHydraulicsModel{FT},
645648
energy::AbstractCanopyEnergyModel{FT},
646649
sif::AbstractSIFModel{FT},
650+
lai_model::AbstractLAIModel{FT},
647651
biomass::AbstractBiomassModel{FT},
648652
boundary_conditions::B,
649653
parameters::SharedCanopyParameters{FT, PSE},
@@ -669,6 +673,7 @@ function CanopyModel{FT}(;
669673
hydraulics::AbstractPlantHydraulicsModel{FT},
670674
soil_moisture_stress::AbstractSoilMoistureStressModel{FT},
671675
sif::AbstractSIFModel{FT},
676+
lai_model::AbstractLAIModel{FT},
672677
energy = PrescribedCanopyTempModel{FT}(),
673678
biomass::PrescribedBiomassModel{FT},
674679
boundary_conditions::B,
@@ -708,6 +713,7 @@ function CanopyModel{FT}(;
708713
hydraulics,
709714
energy,
710715
sif,
716+
lai_model,
711717
biomass,
712718
boundary_conditions,
713719
parameters,
@@ -780,6 +786,7 @@ function CanopyModel{FT}(
780786
energy = BigLeafEnergyModel{FT}(toml_dict),
781787
biomass = PrescribedBiomassModel{FT}(domain, LAI, toml_dict),
782788
sif = Lee2015SIFModel{FT}(toml_dict),
789+
lai_model = OptimalLAIModel{FT}(toml_dict),
783790
) where {FT}
784791
(; atmos, radiation, ground) = forcing
785792

@@ -799,6 +806,7 @@ function CanopyModel{FT}(
799806
energy,
800807
biomass,
801808
sif,
809+
lai_model,
802810
]
803811
# For component models without parameters, skip the check
804812
!hasproperty(component, :parameters) && continue
@@ -831,6 +839,7 @@ function CanopyModel{FT}(
831839
hydraulics,
832840
energy,
833841
sif,
842+
lai_model,
834843
biomass,
835844
boundary_conditions,
836845
parameters,
@@ -859,6 +868,7 @@ canopy_components(::CanopyModel) = (
859868
:autotrophic_respiration,
860869
:energy,
861870
:sif,
871+
:lai_model,
862872
:soil_moisture_stress,
863873
:biomass,
864874
)

0 commit comments

Comments
 (0)