Full version

Theory

This is based on McMurtrie & Näsholm, 2017 New Phytologist (doi: 10.1111/nph.14927).

The root length density per unit soil volume is a function of root biomass \(C_r\) and morphological parameters (density: \(\rho_r\), rooting depth \(z_r\), root radius \(r_0\)). \[ l_r = \frac{C_r}{2 \pi r_0 \rho_r \ z_r} \] The half-distance between root hairs follows from \(l_r\): \[ r_x = \sqrt{\frac{1}{\pi l_r} + r_0^2} \] And the total root surface area is \[ A_r = 2 \pi r_0 l_r \]

The uptake flux through a unit root surface area can be modelled for a steady-state condition where a demand and supply are set equal. The “demand” flux can be modelled based on Eq. 2 in McMurtrie & Näsholm as \[ j_r = \frac{j_\text{rmax}}{1 + \frac{j_\text{rmax}}{k c_{s0}}} \] The supply flux is the sum of a diffusive and an advective component. The diffusive component is taken here as being driven by the difference between inorganic N concentrations at the root surface (\(c_{s0}\)) and in the bulk soil \(c_x\), divided by the “travel distance” of N in the soil, i.e., the half-distance between roots \(r_x\). The advective component is determined by the water uptake velocity \(v_0\) and the N concentration in the bulk soil. \[ j_s = \frac{D(c_x - c_{s0})}{r_x} + v_0 c_x \] Setting \(j_r = j_s\) yields a solution fo \(c_{s0}\) from which the root area-specific N uptake flux can be calculated either from the supply or the demand function.

The N concentration in the bulk soil is taken to be determined by the amount of inorganic N per unit soil surface area (the units corresponding to the CN-model formulation) divided by the root surface area and soil depth: \[ c_x = \frac{N_\text{inorg}}{z_r A_r} \] (The last point is confusing to me).

This is implemented below.

Implementation

par_fnup <- list(
  D = 3, #D = 0.0005, # m d-1, rhizosphere diffusivity
  j = 2000, #j = 0.0056, # Jrmax = maximum root-N influx, g N m2 d-1
  k = 1,  #k = 0.025,  # initial slope of MM for N uptake flux = root absorbing power, m d-1, McMurtrie & Näsholm, 2017
  f = 0       # transpiration velocity
)
calc_conc <- function(N, root_distance, par){
  (sqrt((par$D*par$j - par$D*par$k*N - par$f*par$k*N*root_distance + par$j*par$k*root_distance)^2 
    - 4*par$D*par$k*(-par$D*par$j*N - par$f*par$j*N*root_distance)) - par$D*par$j + par$D*par$k*N 
    + par$f*par$k*N*root_distance - par$j*par$k*root_distance)/(2*par$D*par$k)  
}
calc_fnup_demand <- function(conc, par){
  par$k * par$j * conc / (par$k * conc + par$j)
}
calc_fnup_supply <- function(conc, N, root_distance, par){
  par$D * (N - conc) / root_distance + par$f * N
}

calc_fnup <- function(ninorg, root_distance, par){
  conc <- calc_conc(ninorg, root_distance, par)
  nup_demand <- calc_fnup_demand(conc, par)
  nup_supply <- calc_fnup_supply(conc, ninorg, root_distance, par)
  # if (abs(nup_demand - nup_supply) > 0.001){
  #   warning("N uptake based on supply and demand equations not identical.")
  #   out <- NA
  # } else {
  #   out <- nup_demand
  # }
  out <- nup_demand
  return(out)
}

Express it all as a function of root biomass.

# rough values based on Weemstra et al., 2020 https://doi.org/10.1111/1365-2435.13520
par_rootmorph <- list(
  zroot = 1,
  rho = 0.2 * 100^3,  # g m-3
  r0 = 0.3 / (2 * 1000) # m, radius
)

calc_root_length_density <- function(croot, par){
  croot / (par$rho * par$zroot * 2 * pi * par$r0)
}

calc_a_root <- function(croot, par){
  2 * pi * par$r0 * calc_root_length_density(croot, par)
}

calc_root_distance <- function(root_length_density, par){
  sqrt(1/(pi * root_length_density) + par$r0^2)
}

calc_nup <- function(croot, ninorg, par_fnup, par_rootmorph){
  a_root <- calc_a_root(croot, par_rootmorph)
  n_conc <- ninorg /( par_rootmorph$zroot * a_root)
  root_length_density <- calc_root_length_density(croot, par_rootmorph)
  root_distance <- calc_root_distance(root_length_density, par_rootmorph)
  fnup   <- calc_fnup(n_conc, root_distance, par_fnup)
  a_root * fnup
}

Visualisations

ggplot() +
  geom_function(fun = 
                  function(x) 
                    calc_fnup(x, 
                              root_distance = 1,
                              par = list(
                                D = 0.0005,
                                j = 0.0056,
                                k = 0.025, 
                                f = 0
                                )
                               )) +
  geom_function(fun = 
                  function(x) 
                    calc_fnup(x, 
                              root_distance = 1,
                              par = list(
                                D = 0.0005,
                                j = 0.056,
                                k = 0.025, 
                                f = 0
                                )
                               ), color = "blue") +
  labs(title = "N uptake as a function of Ninorg") +
  xlim(0, 100)

Root distance as a function of root mass.

ggplot() +
  geom_function(fun = 
                  function(x) 
                    calc_root_distance(
                      calc_root_length_density(x, par_rootmorph),
                      par_rootmorph
                      )) +
  xlim(0, 10000)

The total soil column N uptake as a function of total root biomass.

ggplot() +
  geom_function(fun = 
                  function(x) 
                    calc_nup(x,
                             ninorg = 1,
                             par_fnup,
                             par_rootmorph
                            )) +
  geom_function(fun = 
                  function(x) 
                    calc_nup(x,
                             ninorg = 1,
                             par_fnup,
                             list(
                               zroot = 1,
                               rho = 0.2 * 100^3,  # g m-3
                               r0 = 0.03 / (2 * 1000) # m, radius
                             )
                            ), col = "royalblue") +
  labs(title = "Total N uptake as a function of Croot",
       x = "Croot") +
  xlim(0, 1000)
## Warning: Removed 1 row(s) containing missing values (geom_path).
## Removed 1 row(s) containing missing values (geom_path).

The N uptake fraction as a function of the root volume density.

df <- tibble(croot = seq(1000)) |> 
  mutate(volume_density = croot / (par_rootmorph$rho * par_rootmorph$zroot),
         nup = calc_nup(croot, 
                        ninorg = 0.2, 
                        list(
                             D = 3,
                             j = 2000,
                             k = 1,
                             f = 0
                             ), 
                        par_rootmorph
                        ),
         nup2 = calc_nup(croot, 
                        ninorg = 0.2, 
                        list(
                             D = 3,
                             j = 500,
                             k = 1,
                             f = 0
                             ), 
                        par_rootmorph
                        )
         ) |> 
  mutate(nup_fraction = nup/0.2,
         nup_fraction2 = nup2/0.2)

df |> 
  ggplot() +
  geom_line(aes(croot, nup_fraction)) +
  geom_line(aes(croot, nup_fraction2), col = "royalblue")

df |> 
  ggplot() +
  geom_line(aes(volume_density, nup_fraction)) +
  geom_line(aes(volume_density, nup_fraction2), col = "royalblue")

The N uptake fraction as a function of (bulk soil) inorganic N.

df <- tibble(ninorg = seq(0.1, 50, by = 0.01)) |> 
  mutate(nup = calc_nup(croot = 500, 
                        ninorg, 
                        par_fnup, 
                        par_rootmorph
                        )) |> 
  mutate(nup_fraction = nup/ninorg)

df |> 
  ggplot(aes(ninorg, nup_fraction)) +
  geom_line()

df |> 
  ggplot(aes(ninorg, nup)) +
  geom_line()

df <- expand.grid(croot = seq(from = 1, to = 1000, by = 10), 
                  ninorg = seq(from = 1, to = 50, by = 0.5)) |> 
  as_tibble() |> 
  mutate(fnup = purrr::map2_dbl(croot, 
                                ninorg, 
                                ~calc_nup(.x, .y, par_fnup, par_rootmorph)))

gg_full <- ggplot(df, aes(croot, ninorg)) + 
  geom_tile(aes(fill = fnup)) +
  geom_contour(aes(z = fnup), color = "grey50") +
  scale_fill_scico(palette = 'roma') +
  theme_classic() +
  labs(title = "N uptake", x = "Croot", y = "Ninorg")
gg_full

Simple version

Theory

The simplest representation of the fact that N uptake is saturating both with respect to the root biomass and with respect to soil inorganic N (the latter is often ignored in models) is the following: \[ N_\text{up} = V_\text{C}(N_\text{inorg}) \frac{C_r}{k_C + C_r} \] where \(k_C(N_\text{inorg})\) is also saturating: \[ V_C(N_\text{inorg}) = V_\text{max}\frac{N_\text{inorg}}{k_V + N_\text{inorg}} \]

Implementation

calc_vmax <- function(ninorg, par){
  par$vmax * ninorg / (par$kv + ninorg)
}

calc_nup_simpl <- function(croot, ninorg, par){
  vmax = calc_vmax(ninorg, par)
  vmax * croot / (par$kc + croot)
}
par <- list(
  kc = 800,
  kv = 10,
  vmax = 20
)

ggplot() +
  geom_function(fun = function(x) calc_nup_simpl(croot = 15, ninorg = x, par)) +
  xlim(0, 200) + 
  labs(x = "Ninorg", y = "Nup", title = "Croot = 15")

ggplot() +
  geom_function(fun = function(x) calc_nup_simpl(x, ninorg = 10, par)) +
  xlim(0, 100) + 
  labs(x = "Croot", y = "Nup", title = "Ninorg = 10")

par <- list(
  kc = 800,
  kv = 10,
  vmax = 20
)

df <- expand.grid(croot = seq(from = 1, to = 1000, by = 10), 
                  ninorg = seq(from = 1, to = 50, by = 0.5)) |> 
  as_tibble() |> 
  mutate(fnup = purrr::map2_dbl(croot, 
                                ninorg, 
                                ~calc_nup_simpl(.x, .y, par)))

gg_simpl <- ggplot(df, aes(croot, ninorg)) + 
  geom_tile(aes(fill = fnup)) +
  geom_contour(aes(z = fnup), color = "grey50") +
  scale_fill_scico(palette = 'roma') +
  theme_classic() +
  labs(title = "N uptake", x = "Croot", y = "Ninorg")
gg_simpl

Comparison

library(patchwork)
gg_full + gg_simpl