# Import modules
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
55 Modeling wheat yield potential
In this exercise we will simulate the accumulation of above-ground plant dry biomass of winter wheat using a simple model driven by photosynthetically active radiation (PAR) and growing degree days (GDD). The model has two routines, one for simulating leaf area index and one for simulating biomass accumulation. This model assumes that there are no environmental limitations other than the input in solar radiation and the changes in air temperature.
The equation modeling the increase in aboveground dry biomass as a function of the intercepted incident solar radiation is:
B_t = B_{t-1} + E_b \; E_{imax} \Bigg[ 1 - e^{-K \; LAI_t} \Bigg] PAR_t
The equation modeling the leaf area index as a function of thermal time is:
LAI_t = L_{max} \Bigg[ \frac{1}{1+e^{-\alpha(T_t - T_1)}} -e^{\beta(T_t-T_2)} \Bigg]
where parameters are defined by:
T_2 = \frac{1}{\beta} log[1 + e^{(\alpha \; T_1)}]
B is above-ground dry biomass in g m^{-2}
T is cumulative growing degree days
t is time in days
LAI is the leaf area index
L{max} is the maximum leaf area index during the entire growing season
PAR is the photosynthetically active radiation
K is the coefficient of extiction
T_1 is a growth threshold
E_b is the radiation use efficiency
E_{imax} is the maximal value of intercepted to incident solar radiation
\alpha and \beta are empirical parameters
# Import data
= pd.read_csv('../datasets/KS_Manhattan_6_SSW.csv', na_values=[-99, -9999])
df 3) df.head(
WBANNO | LST_DATE | CRX_VN | LONGITUDE | LATITUDE | T_DAILY_MAX | T_DAILY_MIN | T_DAILY_MEAN | T_DAILY_AVG | P_DAILY_CALC | ... | SOIL_MOISTURE_5_DAILY | SOIL_MOISTURE_10_DAILY | SOIL_MOISTURE_20_DAILY | SOIL_MOISTURE_50_DAILY | SOIL_MOISTURE_100_DAILY | SOIL_TEMP_5_DAILY | SOIL_TEMP_10_DAILY | SOIL_TEMP_20_DAILY | SOIL_TEMP_50_DAILY | SOIL_TEMP_100_DAILY | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 53974 | 20031001 | 1.201 | -96.61 | 39.1 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 53974 | 20031002 | 1.201 | -96.61 | 39.1 | 18.9 | 2.5 | 10.7 | 11.7 | 0.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 53974 | 20031003 | 1.201 | -96.61 | 39.1 | 22.6 | 8.1 | 15.4 | 14.8 | 0.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 rows × 28 columns
# Convert LST_DATE to Pandas datetime format
2, "DATES", pd.to_datetime(df["LST_DATE"], format="%Y%m%d"))
df.insert(3) df.head(
WBANNO | LST_DATE | DATES | CRX_VN | LONGITUDE | LATITUDE | T_DAILY_MAX | T_DAILY_MIN | T_DAILY_MEAN | T_DAILY_AVG | ... | SOIL_MOISTURE_5_DAILY | SOIL_MOISTURE_10_DAILY | SOIL_MOISTURE_20_DAILY | SOIL_MOISTURE_50_DAILY | SOIL_MOISTURE_100_DAILY | SOIL_TEMP_5_DAILY | SOIL_TEMP_10_DAILY | SOIL_TEMP_20_DAILY | SOIL_TEMP_50_DAILY | SOIL_TEMP_100_DAILY | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 53974 | 20031001 | 2003-10-01 | 1.201 | -96.61 | 39.1 | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 53974 | 20031002 | 2003-10-02 | 1.201 | -96.61 | 39.1 | 18.9 | 2.5 | 10.7 | 11.7 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 53974 | 20031003 | 2003-10-03 | 1.201 | -96.61 | 39.1 | 22.6 | 8.1 | 15.4 | 14.8 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 rows × 29 columns
# Check if average temperature or solar radiation have missing values
print('TAVG:', df['T_DAILY_AVG'].isna().sum())
print('SRAD:', df['SOLARAD_DAILY'].isna().sum())
TAVG: 42
SRAD: 39
# Replace missing values using interpolation
'T_DAILY_AVG'].interpolate(method="pchip", inplace=True)
df['SOLARAD_DAILY'].interpolate(method="pchip", inplace=True) df[
# Let's examine our data with a plot
=(8,6))
plt.figure(figsize
2,1,1)
plt.subplot("DATES"],df["T_DAILY_AVG"], linewidth=0.5)
plt.plot(df[f'Air temperature ({chr(176)}C)')
plt.ylabel(
2,1,2)
plt.subplot("DATES"],df["SOLARAD_DAILY"], linewidth=0.5)
plt.plot(df['Solar radiation ($Mj \ m^{-1} day^{-1}$)')
plt.ylabel(
plt.show()
# Define crop parameters
= "2007-10-01"
planting_date = pd.to_datetime(planting_date)
planting_date = 250 # days
season_length = planting_date + pd.to_timedelta(season_length, unit='days') harvest_date
# Define model parameters
= 4
Tbase = 1.85
Eb = 0.94
Eimax = 0.7
K = 7
Lmax = 900
T1 = 0.005
alpha = 0.002
beta = 1/beta * np.log(1 + np.exp(alpha*T1))
T2 = 0.45 # Approximate harvest index HI
# Define model equations as lambda functions
= lambda x: np.maximum(Lmax*(1/(1+np.exp(-alpha*(x-T1))) - np.exp(beta*(x-T2))),0)
calculate_LAI = lambda LAI,PAR: Eb * Eimax * (1-np.exp(-K*LAI)) * PAR
calculate_B = lambda T, Tbase: np.maximum(T - Tbase, 0) calculate_GDD
0,2000), calculate_LAI(np.linspace(0,2000))) plt.plot(np.linspace(
Single growing season
For simulating a single crop growing season the easiest approach is to select the rows of the historic weather record matching the specified growing season and then iterating over each day. More advanced methods could use a while
loop to check if a condition, like the number of GDD, have been met to terminate the growing season.
# Select records for growing season
= (df["DATES"] >= planting_date) & (df["DATES"] <= harvest_date)
idx_growing_season = df.loc[idx_growing_season,:].reset_index(drop=True)
df_season 3) df_season.head(
WBANNO | LST_DATE | DATES | CRX_VN | LONGITUDE | LATITUDE | T_DAILY_MAX | T_DAILY_MIN | T_DAILY_MEAN | T_DAILY_AVG | ... | SOIL_MOISTURE_5_DAILY | SOIL_MOISTURE_10_DAILY | SOIL_MOISTURE_20_DAILY | SOIL_MOISTURE_50_DAILY | SOIL_MOISTURE_100_DAILY | SOIL_TEMP_5_DAILY | SOIL_TEMP_10_DAILY | SOIL_TEMP_20_DAILY | SOIL_TEMP_50_DAILY | SOIL_TEMP_100_DAILY | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 53974 | 20071001 | 2007-10-01 | 1.302 | -96.61 | 39.1 | 28.2 | 7.1 | 17.6 | 18.9 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 53974 | 20071002 | 2007-10-02 | 1.302 | -96.61 | 39.1 | 28.2 | 9.3 | 18.7 | 21.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 53974 | 20071003 | 2007-10-03 | 1.302 | -96.61 | 39.1 | 26.6 | 5.9 | 16.2 | 16.3 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 rows × 29 columns
# Initial conditions
= calculate_GDD(df_season["T_DAILY_AVG"].iloc[0], Tbase)
GDD = np.array([0])
LAI = np.array([0])
B
# Iterate over each day
# We start from day 1, since we depend on information of the previous day
for k,t in enumerate(range(1,df_season.shape[0])):
# Compute growing degree days
= calculate_GDD(df_season["T_DAILY_AVG"].iloc[t], Tbase)
GDD_day = np.append(GDD, GDD_day)
GDD
# Compute leaf area index
= calculate_LAI(GDD.sum())
LAI_day = np.append(LAI, LAI_day)
LAI
# Estimate PAR from solar radiation (about 48%)
= df_season["SOLARAD_DAILY"].iloc[t] * 0.48
PAR_day
# Compute daily biomass
= calculate_B(LAI[t], PAR_day)
B_day
# Compute cumulative biomass
= np.append(B, B[-1] + B_day) B
# Add variable to growing season dataframe,
# so that we have everything in one place.
'LAI'] = LAI
df_season['GDD'] = GDD
df_season['B'] = B
df_season[
3) df_season.head(
WBANNO | LST_DATE | DATES | CRX_VN | LONGITUDE | LATITUDE | T_DAILY_MAX | T_DAILY_MIN | T_DAILY_MEAN | T_DAILY_AVG | ... | SOIL_MOISTURE_50_DAILY | SOIL_MOISTURE_100_DAILY | SOIL_TEMP_5_DAILY | SOIL_TEMP_10_DAILY | SOIL_TEMP_20_DAILY | SOIL_TEMP_50_DAILY | SOIL_TEMP_100_DAILY | LAI | GDD | B | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 53974 | 20071001 | 2007-10-01 | 1.302 | -96.61 | 39.1 | 28.2 | 7.1 | 17.6 | 18.9 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.000000 | 14.9 | 0.000000 |
1 | 53974 | 20071002 | 2007-10-02 | 1.302 | -96.61 | 39.1 | 28.2 | 9.3 | 18.7 | 21.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.008062 | 17.0 | 0.065197 |
2 | 53974 | 20071003 | 2007-10-03 | 1.302 | -96.61 | 39.1 | 26.6 | 5.9 | 16.2 | 16.3 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.011653 | 12.3 | 0.198516 |
3 rows × 32 columns
# Generate figure for growing season LAI and Biomass
=(10,8))
plt.figure(figsize
# Leaf area index
2,1,1)
plt.subplot('DATES'], df_season['LAI'], '-g')
plt.plot(df_season['Leaf Area Index', color='g', size=16)
plt.ylabel(
# Biomass
plt.twinx()'DATES'], df_season['B'], '--b')
plt.plot(df_season['Biomass ($g/m^2$)', color='b', size=16)
plt.ylabel(
plt.show()
# Estimate grain yield
= df_season['B'].iloc[-1]*HI # grain yield in g/m^2
Y = Y * 10_000/1000 # Convert to kg per hectare (1 ha = 10,000 m^2) and (1 kg = 1,000 g)
Y print(f'Wheat yield potential is: {Y:.1f} kg per hectare')
Wheat yield potential is: 6090.5 kg per hectare
Practice
- Modify the code so that the model stops when the crop accumulated a total of 2400 GDD. Hint: You no longer a harvest date and you may want to consider using a
while
loop.
References
Brun, F., Wallach, D., Makowski, D. and Jones, J.W., 2006. Working with dynamic crop models: evaluation, analysis, parameterization, and applications. Elsevier.