library(rsofun)
library(dplyr)
lseq <- function(from=1, to=100000, length.out=6) {
# logarithmic spaced sequence
exp(seq(log(from), log(to), length.out = length.out))
}
Get the analytical solution for one specific set of arguments.
## Run for different temperatures and plot
tc <- 20
ppfd <- 800 / 0.05
vpd <- 100
co2 <- 400
elv <- 0
fapar <- 1
out_analytical <- rpmodel( tc = tc, vpd = vpd, co2 = co2, elv = elv, kphio = 0.05, fapar = fapar, ppfd = ppfd, method_optci="prentice14", method_jmaxlim = "none", do_ftemp_kphio = FALSE )
Is it internally consistent? \[ c_i = c_a - A / g_s \]
print( out_analytical$ci )
## [1] 35.45723
print( out_analytical$ca - (out_analytical$gpp / 12.0107) / out_analytical$gs )
## [1] 35.45723
Yes.
And what about … \[ A = V_{\text{cmax}} \frac{c_i-\Gamma^{\ast}}{c_i + K} \]
print( out_analytical$gpp / 12.0107 )
## [1] 609.8003
print( out_analytical$vcmax * (out_analytical$ci - out_analytical$gammastar) / (out_analytical$ci + out_analytical$kmm ))
## [1] 609.8003
print( out_analytical$gs * (out_analytical$ca - out_analytical$ci) )
## [1] 609.8003
Yes.
Maximise the following term numerically, subject to \(V_{\mathrm{cmax}}\) and \(g_s\): \[ A_n = A - aE -bV_{\text{cmax}} = max. \;\;\;\;\;\;\;\;\;\;\;\;(1) \]
Note, that this is (almost) equivalent to the optimality criterium in Prentice et al., 2014: \[ a \; \frac{\partial (E/A)}{\partial \chi} = -b \; \frac{\partial (V_{\mathrm{cmax}}/A)}{\partial \chi} \;\;\;\;\;\;\;\;\;\;\;\;(2) \] The twist is, that in above equation, \(V_{\mathrm{cmax}}\) cancels and we get “just” an optimal \(\chi\).
Instead of analytically taking the derivatives in Eq. 2 and solving for \(\chi\), we can apply a numerical optimisation algorithm to maximise the following expression (which is strictly equivalent to Eq. 2): \[ E/A + \beta V_{\mathrm{cmax}}/A = max. \] with \(\beta=b/a\). This can be written out as \[ \frac{1.6\;D}{c_a(1-\chi)} - \beta \frac{\chi c_a + K}{\chi c_a - \Gamma^{\ast}} = max. \] To find the maximum, let’s define the numerical optimization function:
calc_optimal_chi_num <- function( kmm, gammastar, ns_star, ca, vpd, beta ){
#-----------------------------------------------------------------------
# Input: - float, 'kmm' : Pa, Michaelis-Menten coeff.
# - float, 'ns_star' : (unitless) viscosity correction factor for water
# - float, 'vpd' : Pa, vapor pressure deficit
# Output: float, ratio of ci/ca (chi)
# Features: Returns an estimate of leaf internal to ambient CO2
# partial pressure following the "simple formulation".
# Depends: - kc
# - ns
# - vpd
#-----------------------------------------------------------------------
maximise_this <- function( chi, kmm, gammastar, ns_star, ca, vpd, beta ){
out <- 1.6 * ns_star * vpd / (ca * (1.0 - chi)) + beta * (chi * ca + kmm)/(chi * ca - gammastar)
return(out)
}
out_optim <- optimr::optimr(
par = 0.7,
lower = 0.01,
upper = 0.99,
fn = maximise_this,
kmm = kmm,
gammastar = gammastar,
ns_star = ns_star,
ca = ca,
vpd = vpd,
beta = beta,
method = "L-BFGS-B",
control = list( maxit = 100, maximize = TRUE )
)
return(out_optim$par)
}
Now, let’s calculate the optimal \(\chi\) with the same parameters as used to run the P-model above.
chi_opt <- calc_optimal_chi_num( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146 )
print(chi_opt)
## [1] 0.8748358
As a check: is this the same as returned by the P-model?
print(out_analytical$chi)
## [1] 0.8748392
Practically, yes.
Let’s call this \(\chi^{\ast}\). For the P-model, it is then assumed that \(A=A_J\) with \[ A_J = \phi_0 \; I_{\mathrm{abs}}\;\frac{c_a \chi^{\ast} - \Gamma^{\ast}}{c_a \chi^{\ast} + 2\Gamma^{\ast}} \] A check again: is this the same as returned by the P-model?
print( 0.05 * ppfd * fapar * (out_analytical$ca * chi_opt - out_analytical$gammastar)/(out_analytical$ca * chi_opt + 2 * out_analytical$gammastar))
## [1] 609.7997
print( out_analytical$gpp / 12.0107 )
## [1] 609.8003
Yes, practically.
The analytically calculated \(A_n\) from the P-model (rpmodel()
call above) is:
cost_scalar <- 0.001
cost_transp <- cost_scalar * 1.6 * out_analytical$ns_star * out_analytical$gs * vpd
cost_vcmax <- 146 * cost_scalar * out_analytical$vcmax
net_assim_analytical <- out_analytical$gpp / 12.0107 - cost_transp - cost_vcmax
print( net_assim_analytical )
## [1] 362.0813
Define the function which is to be maximised. This corresponds to Eq. 1.
calc_net_assim <- function( par, args, iabs, kphio, cost_scalar, maximize=FALSE, return_chi=FALSE, return_a_j_net=FALSE ){
kmm <- args[1]
gammastar <- args[2]
ns_star <- args[3]
ca <- args[4]
vpd <- args[5]
beta <- args[6]
vcmax <- par[1]
gs <- par[2]
## Get assimilation and ci, given Vcmax and gs
## by solving the equation system
## assim = vcmax * (ci - gammastar)/(ci + kmm)
## assim = gs * (ca - ci)
a_quad <- -1.0 * gs
b_quad <- gs * ca - gs * kmm - vcmax
c_quad <- gs * ca * kmm + vcmax * gammastar
root_ci <- try( polyroot( c(c_quad, b_quad, a_quad) ) )
if (class(root_ci)=="try-error"){
return( NA )
} else {
ci <- Re(root_ci)[which(Re(root_ci)>0)] # take positive root
# det <- sqrt( b_quad^2 - 4 * a_quad * c_quad )
# ci <- -b_quad - det / (2 * a_quad) # smaller root
## A_j
a_j <- iabs * kphio * (ci - gammastar) / (ci + 2 * gammastar)
## A_c
a_c <- vcmax * (ci - gammastar) / (ci + kmm)
# ## I don't understand if this is correct... It must be inconsistent with the Vcmax-related ci calculated above. But anyhow, this the logic of the P-model...
# assim <- a_j
## ... when it should actually be the minimum of A_j and A_c:
# assim <- min(a_j, a_c)
## All above is correct. That is, A and ci are correctly back-calculated from Vcmax and gs
## only cost ratio is defined. for this here we need absolute values. Set randomly
cost_transp <- cost_scalar * 1.6 * ns_star * gs * vpd
cost_vcmax <- cost_scalar * beta * vcmax
# ## Option A, Eq. 1. This does not give the same result as options B and C as the
# ## cost scalar (cost_scalar) does not cancel.
# net_assim <- assim - cost_transp - cost_vcmax
# ## Option B: This is equivalent to the P-model with its optimization of ci:ca.
# net_assim <- -(cost_transp + cost_vcmax) / a_c
## Option C: Equivalent (gives same result for ci:ca) as option B and the original P-model
## And: optimization target has ecological meaning.
## The trouble is that A_c is used to determine optimality here, but actual assimilation is
## later calculated following the light-limited function for A_j.
net_assim <- a_c - cost_transp - cost_vcmax
if (maximize) net_assim <- -net_assim
if (return_chi){
return(ci/ca)
} else if (return_a_j_net){
a_j_net <- a_j - cost_transp - cost_vcmax
return(a_j_net)
} else {
return(net_assim)
}
}
}
Use the analytically optimal vcmax and ci as starting values and calculate net assimilation with these
## starting values for par = [vcmax, gs]. Use the ones returned by rpmodel()
par <- c( out_analytical$vcmax, out_analytical$gs )
args <- c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 )
out_numerical <- calc_net_assim( par, args, iabs=(fapar * ppfd), kphio = 0.05, cost_scalar=0.0003 )
print(out_numerical)
## [1] 535.4846
print( calc_net_assim( par, args, iabs=(fapar * ppfd), kphio = 0.05, cost_scalar=0.0003, return_chi = TRUE ) )
## [1] 0.8748392
print(out_analytical$chi)
## [1] 0.8748392
Ok, this is calculated correctly.
Optimise \(V_{\text{cmax}}\) and \(g_s\) simultaneously so that net assimilation is maximised (Implementing Eq. 1). This should return the same values for Vcmax and gs as the starting values (which were taken from the analytical solution), and the same \(\chi\) as the analytical solution of the P-model.
out_optim <- optimr::optimr(
par = c( out_analytical$vcmax, out_analytical$gs ),
lower = c( out_analytical$vcmax*0.0001, out_analytical$gs*0.001 ),
upper = c( out_analytical$vcmax*20, out_analytical$gs*30 ),
fn = calc_net_assim,
args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ),
iabs = (fapar * ppfd),
kphio = 0.05,
cost_scalar = 0.0003,
method = "L-BFGS-B",
maximize = TRUE,
control = list( maxit = 1000 )
)
print( paste( "Vcmax, gs from gs/Vcmax optimization:", out_optim$par ) )
## [1] "Vcmax, gs from gs/Vcmax optimization: 30969.0621926482"
## [2] "Vcmax, gs from gs/Vcmax optimization: 3606.31687358757"
print( paste("A_n from gs/Vcmax optimization:", -out_optim$value) )
## [1] "A_n from gs/Vcmax optimization: 11004.0401416738"
print( paste( "Vcmax,gs from P-model:", c( out_analytical$vcmax, out_analytical$gs )) )
## [1] "Vcmax,gs from P-model: 1548.45310963241"
## [2] "Vcmax,gs from P-model: 120.210562452919"
print( paste( "chi from gs/Vcmax optimization:", calc_net_assim( par = out_optim$par, args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ), iabs = (fapar * ppfd), kphio = 0.05, cost_scalar=0.0003, return_chi = TRUE ) ))
## [1] "chi from gs/Vcmax optimization: 0.914101374016631"
print(paste("... this should be identical to ", out_analytical$chi))
## [1] "... this should be identical to 0.874839171881005"
No. This doesn’t work.
The problem of all of this is that the cost factor that scales both the cost of \(V_{\text{cmax}}\) and \(g_s\) equally is missing from the optimization criterium when $ E/A + ; Vcmax/A = max.$ is used. Let’s term this cost scalar \(x\) and find it numerically.
setzero <- function( x_cost, chi_target, out_analytical, fapar, ppfd, vpd ){
out_optim <- optimr::optimr(
par = c( out_analytical$vcmax, out_analytical$gs ),
lower = c( out_analytical$vcmax*0.0001, out_analytical$gs*0.001 ),
upper = c( out_analytical$vcmax*20, out_analytical$gs*30 ),
fn = calc_net_assim,
args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ),
iabs = (fapar * ppfd),
kphio = 0.05,
cost_scalar = x_cost,
maximize = TRUE,
method = "L-BFGS-B",
control = list( maxit = 100 )
)
args <- c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 )
chi_numerical <- calc_net_assim( out_optim$par, args, iabs=(fapar * ppfd), kphio = 0.05, cost_scalar=x_cost, return_chi = TRUE )
out <- chi_target - chi_numerical
return(abs(out))
}
sapply( lseq( from=0.001, to=0.003, length.out = 100), FUN = function(x) setzero( x, out_analytical$chi, out_analytical, fapar, ppfd, vpd ) ) %>%
plot( lseq( from=0.001, to=0.003, length.out = 100), ., type="l" )
abline(h=0, lty=2)
## Watch out: numerical noise is happening. For this example, chose starting point where visually determined in plot above
unitcost_root <- optimr::optimr(
par = 0.002,
# lower = 0.001,
# upper = 0.003,
fn = setzero,
chi_target = out_analytical$chi,
out_analytical = out_analytical,
fapar = fapar,
ppfd = ppfd,
vpd = vpd,
# method = "L-BFGS-B",
control = list( maxit = 100000 )
)
abline( v = unitcost_root$par, col="red" )
print( paste("My guess for the cost scalar is:", unitcost_root$par ) )
## [1] "My guess for the cost scalar is: 0.00246166076660156"
Now, try the same again and see if, with the determined fitted cost scalar, we get the same for the analytical P-model and the \(V_{\text{cmax}}\), \(g_s\) optimization: Optimise \(V_{\text{cmax}}\) and \(g_s\) simultaneously so that net assimilation is maximised (Implementing Eq. 1). This should now definitely return the same \(c_i\) as the analytical solution of the P-model.
out_optim <- optimr::optimr(
par = c( out_analytical$vcmax, out_analytical$gs ),
lower = c( out_analytical$vcmax*0.0001, out_analytical$gs*0.001 ),
upper = c( out_analytical$vcmax*5, out_analytical$gs*5 ),
fn = calc_net_assim,
args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ),
iabs = (fapar * ppfd),
kphio = 0.05,
cost_scalar = unitcost_root$par,
method = "L-BFGS-B",
maximize = TRUE,
control = list( maxit = 1000 )
)
print( paste( "chi from gs/Vcmax optimization:", calc_net_assim( par = out_optim$par, args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ), iabs = (fapar * ppfd), kphio = 0.05, cost_scalar=unitcost_root$par, return_chi = TRUE ) ))
## [1] "chi from gs/Vcmax optimization: 0.874839171997369"
print(paste("... this should be identical to ", out_analytical$chi))
## [1] "... this should be identical to 0.874839171881005"
Yes!!!
Plot a surface of \(A_n\) as a function of \(V_{\text{cmax}}\) and \(g_s\), with a red point where the numerical optimization found the maximum, and a green point, where the analytical solution lies.
library(dplyr)
len_vcmax <- 50
len_gs <- 50
vcmax_vec <- lseq(out_analytical$vcmax*0.0001, out_analytical$vcmax*5, length.out = len_vcmax)
gs_vec <- lseq(out_analytical$gs*0.001, out_analytical$gs*5, length.out = len_gs)
A_net <- expand.grid(vcmax_vec, gs_vec) %>%
setNames( c("vcmax", "gs") ) %>%
rowwise() %>%
do( net_assim = calc_net_assim(
c(.$vcmax, .$gs),
args=c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ),
iabs=(fapar * ppfd),
kphio = 0.05,
cost_scalar=unitcost_root$par,
maximize=FALSE
) ) %>%
tidyr::unnest(net_assim) %>%
as.matrix() %>%
array( dim=c(len_vcmax,len_gs) )
pmat <- persp( (vcmax_vec), (gs_vec), A_net,
phi = 10, # Azimut, 0 is along gs_vec axis
theta = 30,
col="lightblue"
)
points( trans3d( (out_optim$par[1]), (out_optim$par[2]), -out_optim$value, pmat ), pch=16, col="red", cex=1.8 )
points( trans3d( (out_analytical$vcmax), (out_analytical$gs), net_assim_analytical, pmat ), pch=16, col="green", cex=1 )
# numerical (of course, the value is not the same as analytical, because it's the optimization target and not A_j. Re-calculate A_j from optimized Vcmax and gs.):
print( c(out_optim$par[1], out_optim$par[2], -out_optim$value) )
## [1] 1.548453e+03 1.202106e+02 1.798418e-04
print( calc_net_assim(
par = c(out_optim$par[1], out_optim$par[2]),
args = c( out_analytical$kmm, out_analytical$gammastar, out_analytical$ns_star, out_analytical$ca, vpd, 146.0 ),
iabs = (fapar * ppfd),
kphio = 0.05,
cost_scalar = unitcost_root$par,
return_a_j_net = TRUE
))
## [1] 0.0001797671
# analytical:
cost_transp <- unitcost_root$par * 1.6 * out_analytical$ns_star * out_analytical$gs * vpd
cost_vcmax <- 146 * cost_scalar * out_analytical$vcmax
net_assim_analytical <- out_analytical$gpp / 12.0107 - cost_transp - cost_vcmax
print( c(out_analytical$vcmax, out_analytical$gs, net_assim_analytical) )
## [1] 1548.4531 120.2106 330.4439
And in 2D. For fixed (optimal) \(g_s\).
library(dplyr)
## cost_scalar is searched by hand
sapply( as.list(vcmax_vec), FUN = function(x) calc_net_assim( c(x, out_optim$par[2]), args, iabs=(fapar * ppfd), kphio = 0.05, cost_scalar = unitcost_root$par ) ) %>% plot( vcmax_vec, ., type="l", xlab="Vcmax", ylab="net A", ylim=c(-500,500), xlim=c(0,5000))
abline(v=out_optim$par[1], lty=3)
points( (out_optim$par[1]), -out_optim$value, pch=16, col="red", cex=1.8 )
points( (out_optim$par[1]), net_assim_analytical, pch=16, col="green", cex=1 )
And for fixed (optimal) \(V_{\text{cmax}}\):
library(dplyr)
## cost_scalar is searched by hand
sapply( as.list(gs_vec), FUN = function(x) calc_net_assim( c(out_optim$par[1], x), args, iabs=(fapar * ppfd), kphio = 0.05, cost_scalar = unitcost_root$par ) ) %>% plot( gs_vec, ., type="l", xlab="gs", ylab="net A", ylim=c(-500,500), xlim=c(0,300))
abline( v=out_optim$par[2], lty=3 )
points( (out_optim$par[2]), -out_optim$value, pch=16, col="red", cex=1.8 )
points( (out_optim$par[2]), net_assim_analytical, pch=16, col="green", cex=1 )
The reason why the red and green dots are not exactly on top of each other is because the optimisation (red) returns \(A_{n}=A_c - \psi_{\text{transp}} - \psi_{\text{Vcmax}}\), while the analytical (green point) returns \(A_n = A_j - \psi_{\text{transp}} - \psi_{\text{Vcmax}}\).