The QUEFTS model

Introduction

This chapter describes the QUEFTS model. QUEFTS stands for QUantitative Evaluation of the Fertility of Tropical Soils. It is a rule-based model that can be used to estimate crop yield from soil properties, the amount of fertilizer applied, and an estimate of the yield that could be obtained when soil nutrients are in ample supply. It can also be used to estimate the amount of fertilizer needed to reach a particular yield. QUEFTS was first described by Janssen et al., 1990. See Sattari et al., 2014 for an evaluation and updates of the model.

To run QUEFTS for a particular location, you need data on local soil quality, and we discuss these first. You also need crop parameters and the location specific attainable yield. In this context, attainable yield is the yield that would be reached if crop growth is not limited by low levels the three macro-nutrients nitrogen (N), phosphoros (P) and potassium (K).

Chapter requirements

We use R package Rquefts. You can install it from CRAN.

Native soil supply of nutrients

The first step in using a QUEFTS model is to estimate the native soil supply of the three macro-nutrients N, P, K. Other nutrients are ignored here, although the model could be extended to include them.

The native supply of nutrients is the amount that is availble to a crop when no fertilizer is applied to the soil. The native nutrient supply is estimated from chemical properties of the topsoil.

The equations used by QUEFTS to calculate the native supply of nutrients from soil properties were derived from empirical data of field trials in Suriname and Kenya. They were thought to be applicable to well drained, deep soils, that have a pH(H20) in the range 4.5-7.0, and values for organic carbon below 70 g/kg, P-Olsen below 30 mg/kg and exchangeable potassium below 30 mmol/kg Janssen et al., 1990. Sattari et al., 2014 expanded the equations to be applicable to a wider range of conditions, and we will use these improved equations here.

Below we compute the nutrient supply for a location with an average temperature of 25C, a pH of 6, the organic C concentration is 10 g/kg, and exchangeable potassium is 1 mmol/kg. Phosphorus extracted with the Olsen method is 10 mg/kg; and the total amount of P is is 500 mg/kg.

library(Rquefts)
## Warning: package 'Rquefts' was built under R version 4.3.1
s <- nutSupply2(temp=25, pH=6, SOC=15, Kex=1, Polsen=10, Ptotal=500)
s
##      N_base_supply P_base_supply K_base_supply
## temp      76.03381            12      18.32258

We see that the soils supply of N is 76 kg/ha, that the P supply is 12 kg/ha, and the K supply is 18 kg/ha.

Now let’s compute nutrient supply across a range of pH values, keeping the other values constant.

ph <- seq(from=4.5, to=8.5, by=0.1)
s <- nutSupply2(temp=25, pH=ph, SOC=15, Kex=1, Polsen=10, Ptotal=500)
phs <- cbind(ph, s)
head(phs)
##       ph N_base_supply P_base_supply K_base_supply
## [1,] 4.5      41.14771          5.14      25.80645
## [2,] 4.6      41.14771          5.14      25.20894
## [3,] 4.7      41.14771          5.14      24.61142
## [4,] 4.8      43.83126          6.26      24.01390
## [5,] 4.9      46.51480          7.38      23.41639
## [6,] 5.0      49.19835          8.50      22.81887
par(mfrow=c(1,3))
plot(phs[, c(1,2)])
plot(phs[, c(1,3)])
plot(phs[, c(1,4)])

image0

We see that the pH affters the supply of nutruents in different ways for N, P, and K. The P-supply is highest at a pH=6 – reflecting the fact that phosphoros becomes less available to plants at low or high pH. N-supply decreases with pH (everything else, such as the amount of soil carbon, being equal), and K-supply increases with pH (given the amount of exchangeble K).

Let’s do this once, more but with different levels of some of the other properties.

s1 <- nutSupply2(25, ph, SOC=15, Kex=5,  Polsen=5, Ptotal=5*55)
s2 <- nutSupply2(25, ph, SOC=30, Kex=10, Polsen=10, Ptotal=10*55)
s3 <- nutSupply2(25, ph, SOC=45, Kex=20, Polsen=20, Ptotal=20*55)

We can use the results to make plots of the effect of pH on N, P and K supply, for these three situations (low, medium and high levels of organic matter, exchangeble K, and Olsen-P).

par(mfrow=c(1,3))
sup <- c("N_base_supply", "P_base_supply", "K_base_supply")
for (i in 1:3) {
  yb <- paste(sup[i],"(kg/ha)")
  ym <- c(0, max(s3[, sup[i]]))
  plot(ph, s1[,sup[i]], xlab="pH", ylab=yb, type="l", ylim=ym, lty=1, las=2)
  lines(ph, s2[, sup[i]], lty=2)
  lines(ph, s3[, sup[i]], lty=3)
}
legend("bottomleft", paste("treatment", 1:3), lty=1:3)

image1

Now let’s look at the nutrient supply for differnt levels of soil Carbon.

# unit is g/kg
om <- seq(0, 70, 5)
# three combinations
s1 <- nutSupply2(temp=25, pH=5.5, SOC=om, Kex=5, Polsen=5, Ptotal=5*55)
s2 <- nutSupply2(temp=25, pH=5.5, SOC=om, Kex=10, Polsen=10, Ptotal=10*55)
s3 <- nutSupply2(temp=25, pH=5.5, SOC=om, Kex=20, Polsen=20, Ptotal=20*55)
# and plot
par(mfrow=c(1,3))
for (supply in c("N_base_supply", "P_base_supply", "K_base_supply")) {
  plot(om, s3[,supply], xlab="Soil Carbon (g/kg)", ylab=paste(supply, "(kg/ha)"), type="l", ylim=c(0,max(s3[,supply])), lty=1)
  lines(om, s2[,supply], lty=2)
  lines(om, s1[,supply], lty=3)
}

image2

This shows that organic carbon has a strong positive effect on available N and P, but a negative effect on K availability (given the amount of exchangable K).

To use QUEFTS we need to assume that these relationships are robust — that is, not too far off. But with experimental data, these relationships could be re-estimated and improved.

Nutrient uptake and production

With the native soil nutrient supply, and possibly supply from fertilizer, QUEFTS computes the nutrient uptake by a crop, and from that it estimates crop yield.

Uptake of each nutrient is estimated from the soil supply of that nutrient, the supply of the other two nutrients, and the maximum nutrient concentrations in the vegetative and generative organs of the crop. These maximum concentrations are crop-specfic parameters that we will use below when we run the model.

The estimated uptake of N, P, and K is then used to estimated biomass production ranges for each nutrient. For each pair of nutrients two yield estimates are calculated. For example, production for the N uptake is dependent on the production range for the P and the for the K uptake. This leads to six combinations describing the uptake of one nutrient given maximum dilution or accumulation of another. The nutrient-limited production is then estimated by averaging these six estimates for paired nutrients. An estimate based on two nutrients may not exceed the upper limit of the yield range of the third nutrient, that is, the concentration of a nutrient cannot be lower than its maximum dilution level. Neither may the production estimates exceed the specified attainable (maximum) production.

Create and run a model

Quick start

To run QUEFTS, the first step is to create a model. You can create a model with default parameters like this

library(Rquefts)
q <- quefts()

And then run the model

run(q)
##   leaf_lim   stem_lim  store_lim   N_supply   P_supply   K_supply   N_uptake
## 1928.19667 1928.19667 3756.94929   90.00000   11.00000   90.00000   81.66872
##   P_uptake   K_uptake      N_gap      P_gap      K_gap
##   10.88951   77.38765   60.00000  205.00000    0.00000

Normally you would initialize a model with parameters of your choice.

Soil parameters

soil <- quefts_soil()
class(soil)
## [1] "list"
str(soil)
## List of 7
##  $ N_base_supply: num 60
##  $ P_base_supply: num 10
##  $ K_base_supply: num 60
##  $ N_recovery   : num 0.5
##  $ P_recovery   : num 0.1
##  $ K_recovery   : num 0.5
##  $ UptakeAdjust : num [1:7, 1:2] 0 40 80 120 240 360 1000 0 0.4 0.7 ...
soil$UptakeAdjust
##      [,1] [,2]
## [1,]    0  0.0
## [2,]   40  0.4
## [3,]   80  0.7
## [4,]  120  1.0
## [5,]  240  1.6
## [6,]  360  2.0
## [7,] 1000  2.0

We see that there are 7 soil parameters (one if which is a matrix). The first three give the native soil supply, as we discussed above. The “recovery” parameters set the fraction of avaiable nutrients that a crop may take up (the remainder is either lost or chemically or biologically captured). The UptakeAdjst matrix serves to adjust the nutrient availability given the length of the growing season. The standard season length is 120 days. The longer the season, the higher the total nutrient supply.

Crop parameters

The Rquefts package comes with a set of standard parameters for a number of crops. We get a list of them if we request a crop that does not exist.

quefts_crop(name="x")
## Error in quefts_crop(name = "x"): x not found. Choose from:
## Barley, Cassava, Chickpea, Chillies, Cotton, Cowpea, Field bean
## Groundnut, Jute, Kenaf, Lentil, Maize, Millet (bulrush), Mungbean
## Onion, Pigeonpea, Potato, Rapeseed, Rice, Sesame, Sorghum
## Soyabean, Sugarbeet, Sugarcane, Sunflower, Sweetpotato, Tobacco, Wheat

We’ll use barley

crop <- quefts_crop(name="Barley")
str(crop)
## List of 15
##  $ crop     : chr "Barley"
##  $ NminStore: num 0.011
##  $ NminVeg  : num 0.0035
##  $ NmaxStore: num 0.035
##  $ NmaxVeg  : num 0.012
##  $ PminStore: num 0.0016
##  $ PminVeg  : num 4e-04
##  $ PmaxStore: num 0.006
##  $ PmaxVeg  : num 0.0025
##  $ KminStore: num 0.003
##  $ KminVeg  : num 0.007
##  $ KmaxStore: num 0.008
##  $ KmaxVeg  : num 0.028
##  $ Yzero    : int 200
##  $ Nfix     : num 0

Again, we have a list of paramters. These are the mostly the minimum and maximum delution parameters, as described above, for the vegetative and generative (storage) organs, and for N, P, and K.

If the crop has biological nitrogen fixation (in symbiosis with Rhizobium) you can use the Nfix parameter to determine the fraction of the total nitrogen supply that is supplied by biological fixation. Note that Nfix is a constant, while in reality nitrogen fixation is amongst other things dependent on the amount of mineral nitrogen available in the soil.

Other parameters

You can set the fertilizer applied with a list like this

fertilizer <- list(N=50, P=0, K=0)

We set the attainable biomass accumlation to 2200 kg/ha for leaves, 2700 kg/ha for stems, and 4800 kg/ha for grain. All these numbers are expressed as dry matter weigts. As a rule of the thumb, for cereals, you can assume a harvest index of 0.5. That is, the grain yield is about half the biomass. The other half is divided by the leaves (45%) and stems (55%). We set the seasonlength to 110 days.

biomass <- list(leaf_att=2200, stem_att=2700, store_att=4800, SeasonLength=110)

Create model

And we can create a model with these parameters.

q <- quefts(soil, crop, fertilizer, biomass)

You can inspect the model with str (short for “structure”):

str(q)
## Reference class 'Rcpp_QueftsModel' [package "Rquefts"] with 9 fields
##  $ K           : num 0
##  $ N           : num 50
##  $ P           : num 0
##  $ SeasonLength: num 110
##  $ crop        :Reference class 'Rcpp_QueftsCrop' [package "Rquefts"] with 14 fields
##   ..$ KmaxStore: num 0.008
##   ..$ KmaxVeg  : num 0.028
##   ..$ KminStore: num 0.003
##   ..$ KminVeg  : num 0.007
##   ..$ Nfix     : num 0
##   ..$ NmaxStore: num 0.035
##   ..$ NmaxVeg  : num 0.012
##   ..$ NminStore: num 0.011
##   ..$ NminVeg  : num 0.0035
##   ..$ PmaxStore: num 0.006
##   ..$ PmaxVeg  : num 0.0025
##   ..$ PminStore: num 0.0016
##   ..$ PminVeg  : num 4e-04
##   ..$ Yzero    : num 200
##   ..and 16 methods, of which 2 are  possibly relevant:
##   ..  finalize, initialize
##  $ leaf_att    : num 2200
##  $ soil        :Reference class 'Rcpp_QueftsSoil' [package "Rquefts"] with 7 fields
##   ..$ K_base_supply: num 60
##   ..$ K_recovery   : num 0.5
##   ..$ N_base_supply: num 60
##   ..$ N_recovery   : num 0.5
##   ..$ P_base_supply: num 10
##   ..$ P_recovery   : num 0.1
##   ..$ UptakeAdjust : num [1:14] 0 40 80 120 240 360 1000 0 0.4 0.7 ...
##   ..and 16 methods, of which 2 are  possibly relevant:
##   ..  finalize, initialize
##  $ stem_att    : num 2700
##  $ store_att   : num 4800
##  and 18 methods, of which 4 are  possibly relevant:
##    finalize, initialize, run, runbatch

And you can inspect and change parameters.

q$N
## [1] 50
q$N <- 100
q$N
## [1] 100

model output

And, of course, you can run the model.

output <- run(q)
round(output, 1)
##  leaf_lim  stem_lim store_lim  N_supply  P_supply  K_supply  N_uptake  P_uptake
##    1354.8    1662.7    2877.4     105.5       9.2      55.5      95.6       9.0
##  K_uptake     N_gap     P_gap     K_gap
##      52.6      85.8     160.9     113.3

The results show the nutrient limited production of leaves, stems, and the storage organ, as well as the N, P, and K supply and uptake. The N, P, and K gap show the amounts of fertilizer that would be required to reach the attainable yield.

Explore

Let’s have a look how rice yield is affected by N fertilization. First set up some parameters

rice <- quefts_crop("Rice")
q <- quefts(crop=rice)
q$leaf_att <- 2651
q$stem_att <- 5053
q$store_att <- 8208
fert(q) <- list(N=0, P=0, K=0)
N <- seq(1, 200, 10)

Now set up an output matrix, and run the model for each fertlizer application.

results <- matrix(nrow=length(N), ncol=12)
colnames(results) <- names(run(q))
for (i in 1:length(N)) {
    q["N"] <- N[i]
    results[i,] <- run(q)
}
yield <- results[,"store_lim"]
yield
##  [1] 3162.406 3286.369 3402.153 3509.081 3604.427 3678.601 3744.550 3802.401
##  [9] 3858.675 3913.410 3966.648 4018.424 4068.778 4117.746 4165.363 4211.664
## [17] 4256.683 4300.454 4343.009 4384.379

And make a plot

plot(N, yield, type="l", lwd=2)

image3

We can also look at interactions by changing two nutrients. Here we change N and P fertilization levels, while keeping K constant (either 0 or 200 mmol/kg)

f <- expand.grid(N=seq(0,400,10), P=seq(0,400,10), K=c(0,200))
x <- rep(NA, nrow(f))
for (i in 1:nrow(f)) {0
    q$N <- f$N[i]
    q$P <- f$P[i]
    q$K <- f$K[i]
    x[i] <- run(q)["store_lim"]
}
x <- cbind(f, x)
head(x)
##    N P K        x
## 1  0 0 0 3149.509
## 2 10 0 0 3274.362
## 3 20 0 0 3390.910
## 4 30 0 0 3498.943
## 5 40 0 0 3596.358
## 6 50 0 0 3671.320

To display the results we can treat the values as a raster

library(terra)
## terra 1.7.36
r0 <- rast(x[x$K==0, -3], type="xyz")
r200 <- rast(x[x$K==200, -3], type="xyz")
par(mfrow=c(1,2))
plot(r0, xlab="N fertilizer", ylab="P fertilizer", las=1, main="K=0")
contour(r0, add=TRUE)
plot(r200, xlab="N fertilizer", ylab="P fertilizer", las=1, main="K=200")
contour(r200, add=TRUE)

image4

Callibration

If you have your own experimental data, you could callibrate QUEFTS. Here I illustrate some basic approaches that can be used for callibration.First I generate some data.

set.seed(99)
yldfun <- function(N, noise=TRUE) { 1000 +  500* log(N+1)/3 + noise * runif(length(N), -500, 500) }
N <- seq(0,300,25)
Y <- replicate(10, yldfun(N))
obs <- cbind(N, Y)

We will use Root Mean Square Error (RMSE) to assess model fit.

RMSE <- function(obs, pred) sqrt(mean((obs - pred)^2))

Let’s see how good the default parameters work for us.

q <- quefts()
q$P <- 0
q$K <- 0
results <- matrix(nrow=length(N), ncol=12)
colnames(results) <- names(run(q))
for (i in 1:length(N)) {
    q$N <- N[i]
    results[i,] <- run(q)
}
yield <- results[,'store_lim']
RMSE(obs[,-1], yield)
## [1] 1868.019

The RMSE is quite high.

We create a function to minimize with the optimizer. Here I try to improve the model by altering four parameters.

vars <- c('soil$N_base_supply', 'soil$N_recovery', 'crop$NminStore', 'crop$NmaxStore')
f <- function(p) {
    if (any (p < 0)) return(Inf)
    if (p['crop$NminStore'] >= p['crop$NmaxStore']) return(Inf)
    if (p['soil$N_recovery'] > .6 | p['soil$N_recovery'] < .3) return(Inf)
    q[names(p)] <- p
    pred <- run(q)
    RMSE(obs[,-1], pred['store_lim'])
}
# try the function with some initial values
x <- c(50, 0.5,  0.011, 0.035)
names(x) <- vars
f(x)
## [1] 1573.818

Now tht we have a working functon, we can use optimization methods to try to find the combination of parameter values that is the best fit to our data.

par <- optim(x, f)
# RMSE
par$value
## [1] 1478.384
# optimal parameters
par$par
## soil$N_base_supply    soil$N_recovery     crop$NminStore     crop$NmaxStore
##         0.01398939         0.59768399        26.37808069        37.05140516