Calibrating AquaCrop.jl

Original sources

This tutorial is copied from an IJulia notebook hosted here. For the full documentation of the AquaCrop.jl package, please refer to its Github repository.

In this tutorial we show how to calibrate AquaCrop.jl to field data of yield over the years. We use the data provided by RDWD [1] and Thüringer Landesamt für Statistik [2].

Setup

We load the libraries that we will need for this tutorial. Note that CropGrowthTutorial is in the src/ folder of this repository. We use DrWatson through the repository for reproducibility.

using DrWatson
@quickactivate "CropGrowthTutorial"
using AquaCrop
using CropGrowthTutorial
using Dates
using DataFrames
using Unitful
using CairoMakie

Introduction

To make simulation of crop growth using AquaCrop.jl we need the following data

  1. Climate data
  2. Soil type
  3. Crop type
  4. Sowing date

Additionally we can have add more information, like sowing density, phenology dates, field data, that allows us to calibrate our crop for the specific region that we have.

In our specific case we use data from [1] and [2], from which we have data about:

  1. Crop type
  2. Soil type
  3. Sowing density
  4. Climate data
  5. Yield per crop type
  6. Phenology phases dates per crop type
# Stations information
stations_df = CropGrowthTutorial.get_phenology_stations()
8×4 DataFrame
Rowstations_phenology_iddescriptionstation_namesoil_type
Int64StringString31String15?
115215Eichsfeld = 2925Eichsfeldmissing
214233Hohenlohe = 3761Hohenlohemissing
310705Bodensee = 6263Bodenseemissing
410536Oberrhein = 5275Oberrheinmissing
57650Neustadt = 3897, 2950Neustadtmissing
611220Ergolding = 13710, 2831Ergoldingmissing
719732Jena (SHK): Großbockedra [19732] = 2444Jenasilty clay
815460Thüringer Becken (UHK): **Dachwig [15460]** = 896Thuringer_Beckenloam
# Crop type general information
general_crop_data_df = CropGrowthTutorial.get_crop_parameters()
4×3 DataFrame
Rowcrop_typeaquacrop_nameplantingdens
String15String15Int64
1winter_barleybarleyGDD3060000
2silage_maizemaizeGDD117000
3winter_raperapeseedGDD675000
4winter_wheatwheatGDD3600000

for a given station we can see the climate data

station_name = "Thuringer_Becken"; # Select a station's short_name

# Climate data for a given station
uhk_clim_df =  CropGrowthTutorial.get_climate_data(station_name);
uhk_clim_df |> head
5×5 DataFrame
Rowdateprecipitationmax_temperaturemin_temperaturepotential_evapotranspiration
DateFloat64Float64Float64Float64
11992-01-010.02.6-1.00.5
21992-01-020.05.52.00.7
31992-01-030.06.20.51.2
41992-01-040.47.93.61.3
51992-01-057.69.34.00.6

where the minimal climatedata required for AquaCrop.jl are daily:

  1. Precipitation (mm/day).
  2. Maximal temperature (C).
  3. Minimal temperature (C).
  4. Potential Evapotranspiration (mm/day) given by the FAO-Penman-Monteith equation.

We can also see the recorded yield data

# Field yield data for a given station
uhk_yield_df = CropGrowthTutorial.get_yield_data(station_name);
uhk_yield_df |> head
4×27 DataFrame
Rowcrop_typeunit1999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023
String15String3Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1silage_maizedt514.5495.0479.6489.4391.4451.5437.8388.2499.9379.7460.6397.3427.8470.9388.7459.9320.9350.3508.9278.8381.1408.0501.9318.4374.9
2winter_barleydt70.066.369.862.353.772.263.463.762.068.575.575.056.361.075.480.469.183.474.065.675.764.181.481.277.7
3winter_rapedt38.736.139.430.128.739.037.436.533.735.943.037.333.939.339.046.137.640.730.929.431.035.935.637.337.2
4winter_wheatdt76.473.979.062.362.781.773.769.667.981.079.868.667.372.081.788.071.788.980.668.775.979.375.781.380.7

For a given crop type we have some phenology data recorded too

crop_type = "silage_maize"; # Select a crop_type 

# Crop's phenology raw data for a given station
maize_phenology_raw_df = CropGrowthTutorial.get_crop_phenology_data(crop_type, station_name);
maize_phenology_raw_df |> head
5×8 DataFrame
Rowstations_idreferenzjahrqualitaetsniveauobjekt_idphase_ideintrittsdatumeintrittsdatum_qbjultag
Int64Int64Int64Int64Int64Int64Int64Int64
1154602014721039201409101253
215460201572105201507171198
3154602015721010201504291119
4154602015721012201505121132
5154602015721019201508131225

a simple simulation with AquaCrop's default parameters can be done like this

# get the soil_type for the given station
soil_type = filter(row -> row.station_name==station_name, stations_df)[1,:soil_type];

# get the aquacrop_name for the given crop_type
crop_name = filter(row -> row.crop_type==crop_type, general_crop_data_df)[1,:aquacrop_name];

# select a date for the sowing simulation
sowing_date = Date("2015-04-29");

# run a simulation
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# the daily result of the simulation
cropfield.dayout |> head      

# note that the simulation starts before the sowing_date.
# this is controlled by the parameter CropGrowthTutorial.days_before_sowing = 7 days
SIMULATION WENT WELL
5×90 DataFrame
RowRunNrDateDAPStageWC()RainIrriSurfInfiltRODrainCRZgwtExEE/ExTrxTrTr/TrxETxETET/ETxGDZStExpStStoStSenStSaltStWeedCCCCwStTrKc(Tr)TrWWPBiomassHIY(dry)Y(fresh)BrelativeWPetBinBoutWr()WrWr(SAT)Wr(FC)Wr(exp)Wr(sto)Wr(sen)Wr(PWP)SaltInSaltOutSaltUpSalt()SaltZECeECswStSalt_ECgwWC_1WC_2WC_3WC_4WC_5WC_6WC_7WC_8WC_9WC_10WC_11WC_12ECe_1ECe_2ECe_3ECe_4ECe_5ECe_6ECe_7ECe_8ECe_9ECe_10ECe_11ECe_12EToTminTavgTmaxCO2GDD
Int64DateInt64Int64Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Float64Quantity…Quantity…Float64Quantity…Quantity…Float64Float64Quantity…Float64Float64Float64Float64Float64Float64Float64Float64Float64Quantity…Quantity…Quantity…Float64Quantity…Quantity…Float64Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Quantity…Float64Quantity…Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Quantity…Quantity…Quantity…Quantity…Quantity…Float64
112015-04-22-90712.474 mm1.1 mm0.0 mm0.0 mm1.1 mm0.0 mm0.0 mm0.0 mm-9.9 m1.65 mm1.62606 mm99.00.0 mm0.0 mm100.01.65 mm1.62606 mm99.00.00.0 m-9.0-9.0-10.0-9.0-9.00.00.00.0-9.00.0 mm0.0 g m⁻²0.0 ton ha⁻¹-9.90.0 ton ha⁻¹0.0 ton ha⁻¹-9.00.0 kg m⁻³0.0 ton ha⁻¹0.0 ton ha⁻¹-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹-9.0 ton ha⁻¹-9.0 dS m⁻¹-9.0 dS m⁻¹0.0-9.9 dS m⁻¹30.473931.031.031.031.031.031.031.031.031.031.031.00.00.00.00.00.00.00.00.00.00.00.00.01.5 mm273.25 K278.75 K284.25 K402.715 ppm-17.5
212015-04-23-90710.07 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm-9.9 m2.97 mm2.40422 mm81.00.0 mm0.0 mm100.02.97 mm2.40422 mm81.00.00.0 m-9.0-9.0-10.0-9.0-9.00.00.00.0-9.00.0 mm0.0 g m⁻²0.0 ton ha⁻¹-9.90.0 ton ha⁻¹0.0 ton ha⁻¹-9.00.0 kg m⁻³0.0 ton ha⁻¹0.0 ton ha⁻¹-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹-9.0 ton ha⁻¹-9.0 dS m⁻¹-9.0 dS m⁻¹0.0-9.9 dS m⁻¹28.069731.031.031.031.031.031.031.031.031.031.031.00.00.00.00.00.00.00.00.00.00.00.00.02.7 mm271.65 K281.15 K290.65 K402.715 ppm-17.5
312015-04-24-90707.414 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm-9.9 m4.4 mm2.65536 mm60.00.0 mm0.0 mm100.04.4 mm2.65536 mm60.03.050.0 m-9.0-9.0-10.0-9.0-9.00.00.00.0-9.00.0 mm0.0 g m⁻²0.0 ton ha⁻¹-9.90.0 ton ha⁻¹0.0 ton ha⁻¹-9.00.0 kg m⁻³0.0 ton ha⁻¹0.0 ton ha⁻¹-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹-9.0 ton ha⁻¹-9.0 dS m⁻¹-9.0 dS m⁻¹0.0-9.9 dS m⁻¹25.414331.031.031.031.031.031.031.031.031.031.031.00.00.00.00.00.00.00.00.00.00.00.00.04.0 mm273.75 K284.2 K294.65 K402.715 ppm-14.45
412015-04-25-90705.954 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm0.0 mm-9.9 m3.08 mm1.46012 mm47.00.0 mm0.0 mm100.03.08 mm1.46012 mm47.04.70.0 m-9.0-9.0-10.0-9.0-9.00.00.00.0-9.00.0 mm0.0 g m⁻²0.0 ton ha⁻¹-9.90.0 ton ha⁻¹0.0 ton ha⁻¹-9.00.0 kg m⁻³0.0 ton ha⁻¹0.0 ton ha⁻¹-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹-9.0 ton ha⁻¹-9.0 dS m⁻¹-9.0 dS m⁻¹0.0-9.9 dS m⁻¹23.954231.031.031.031.031.031.031.031.031.031.031.00.00.00.00.00.00.00.00.00.00.00.00.02.8 mm277.95 K285.85 K293.75 K402.715 ppm-9.75
512015-04-26-90703.731 mm0.5 mm0.0 mm0.0 mm0.5 mm0.0 mm0.0 mm0.0 mm-9.9 m3.08 mm2.72349 mm88.00.0 mm0.0 mm100.03.08 mm2.72349 mm88.07.00.0 m-9.0-9.0-10.0-9.0-9.00.00.00.0-9.00.0 mm0.0 g m⁻²0.0 ton ha⁻¹-9.90.0 ton ha⁻¹0.0 ton ha⁻¹-9.00.0 kg m⁻³0.0 ton ha⁻¹0.0 ton ha⁻¹-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm-9.9 mm0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹0.0 ton ha⁻¹-9.0 ton ha⁻¹-9.0 dS m⁻¹-9.0 dS m⁻¹0.0-9.9 dS m⁻¹21.730731.031.031.031.031.031.031.031.031.031.031.00.00.00.00.00.00.00.00.00.00.00.00.02.8 mm281.05 K288.15 K295.25 K402.715 ppm-2.75
# plot the results
CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type)

"figure"

we can pass information about some crop parameters using a dictionary like this:

# get the planting density for the given crop_type
plantingdens = filter(row -> row.crop_type==crop_type, general_crop_data_df)[1,:plantingdens];

# create a kw tuple with the additional information that we wish to pass to AquaCrop
crop_dict = Dict{String,Any}("PlantingDens" => plantingdens);
kw = (
        crop_dict = crop_dict,
     );

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

println("Data planting density ", plantingdens)
println("Crop's planting density ", cropfield.crop.PlantingDens)
println("")

# plot the results
CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type)
SIMULATION WENT WELL

Data planting density 117000
Crop's planting density 117000

"figure"

Step by step Crop Calibration

In AquaCrop.jl there are lot's of parameters that control the crop's growth, they need to be calibrated following the models causality. For our specific case we will assume the following:

  1. The climate data is complete.
  2. The soil is well characterized.
  3. We have field phenology, biomass and yield data, and was collected correctly.

And follow these steps for the calibration:

  1. Crop stages using phenology data.
  2. Soil-root and Canopy cover behaviour.
  3. Biomass.
  4. Yield.

Crop Phenology Stages calibration

To do this we will use the phenology field data from [1], where we have dates of several crop phases. Some of these phases are related to AquaCrop's parameters, with these dates and the climate data we can find better values of the Growing Degree Days until each stage of the crop.

The minimal data for this is:

  1. Phenology dates recorded from field.
  2. Climate data.
  3. Crop's minimal and maximal temperature of development.

The phenology phases that we are interested when calibrating the crop's stages are:

  1. Crop's emergence, with the days from sowing to emergence we can calibrate AquaCrop'sGDDaysToGermination parameter.
  2. Crop's begin of flowering, with the days from sowing to begin of flowering we can calibrate AquaCrop's GDDaysToFlowering parameter.
  3. Crop's end of flowering, with the days from sowing to end of flowering we can calibrate AquaCrop's GDDaysLengthlowering parameter.
  4. Crop's harvest, with the days from sowing to harvest we can calibrate AquaCrop's GDDaysToHarvest parameter.

Additionaly some other phenology phases important for AquaCrop are:

  1. Crop's 90% Canopy cover, with the days from sowing to 90% canopy cover we can calibrate GDDaysToFullCanopy.
  2. Crop's senescence, with the days from sowing to senescence we can calibrate to set GDDaysToSenescence.
  3. Crop's max rooting, with the days from sowing to max rooting we can calibrate GDDaysToMaxRooting.

In case we are missing this data we can estimate the parameters in a different way.

# process phenology raw data phases
sowing_phase = 10; # sowing phase id
harvest_phase = 39; # harvest phase id for silage maize
maize_phenology_processed_df = CropGrowthTutorial.process_crop_phenology(maize_phenology_raw_df, sowing_phase, harvest_phase)
8×8 DataFrame
Rowbeginfloweringdatedaystoharvestemergencedateendfloweringdateharvestdatesowingdatestations_idyear
Date?Int64Date?Date?DateDateInt64Int64
12015-07-171242015-05-12missing2015-08-312015-04-29154602015
22016-07-161242016-05-13missing2016-08-312016-04-29154602016
32017-07-131242017-05-17missing2017-09-062017-05-05154602017
42018-07-071052018-05-10missing2018-08-102018-04-27154602018
52019-07-171142019-05-21missing2019-08-282019-05-06154602019
62020-07-131162020-05-19missing2020-08-292020-05-05154602020
72021-07-221222021-05-20missing2021-09-052021-05-06154602021
82022-07-19982022-05-16missing2022-08-152022-05-09154602022
# get the days and growing degree days for each phenology phase from the actual data
maize_phenology_actual_df = CropGrowthTutorial.process_crop_phenology_actual_gdd(crop_name, maize_phenology_processed_df, uhk_clim_df)
8×11 DataFrame
Rowsowingdateharvestdateyearharvest_actualdaysharvest_actualgddbeginflowering_actualdaysbeginflowering_actualgddendflowering_actualdaysendflowering_actualgddemergence_actualdaysemergence_actualgdd
DateDateInt64Int64?Float64?Int64?Float64?Int64?Float64?Int64?Float64?
12015-04-292015-08-3120151241142.279604.7missingmissing1368.75
22016-04-292016-08-3120161241159.978638.95missingmissing1464.1
32017-05-052017-09-0620171241211.969648.1missingmissing1264.75
42018-04-272018-08-1020181051104.0571647.2missingmissing1367.55
52019-05-062019-08-2820191141113.472599.0missingmissing1543.8
62020-05-052020-08-2920201161084.869530.2missingmissing1445.45
72021-05-062021-09-0520211221140.477702.95missingmissing1473.6
82022-05-092022-08-152022981026.8571688.4missingmissing761.15
# plot the distribution of gddays until the phenology phase from the actual data
CropGrowthTutorial.plot_GDD_stats_violin(maize_phenology_actual_df, "UHK")

"figure"

# get the days and growing degree days for each phenology phase from the simulated data
maize_phenology_simulated_df = CropGrowthTutorial.process_crop_phenology_simulated_gdd(crop_name, maize_phenology_processed_df, uhk_clim_df)
8×9 DataFrame
Rowsowingdateharvest_simulateddaysharvest_simulatedgddbeginflowering_simulateddaysbeginflowering_simulatedgddendflowering_simulateddaysendflowering_simulatedgddemergence_simulateddaysemergence_simulatedgdd
DateInt64?Float64?Int64?Float64?Int64?Float64?Int64?Float64?
12015-04-294001694.95102883.651171060.151680.8
22016-04-293911702.397877.351161063.72080.25
32017-05-053711699.489874.251061057.51377.35
42018-04-273571700.390878.61021059.61583.9
52019-05-063881701.3594882.01101055.152177.5
62020-05-053981699.1599875.751141064.42078.55
72021-05-063941698.6593878.71121063.31581.25
82022-05-093641700.9586879.71001055.55984.0
# compare the day difference between the actual data and the simulated data
maize_phenology_df = leftjoin(maize_phenology_actual_df, maize_phenology_simulated_df; on=:sowingdate);
CropGrowthTutorial.plot_GDD_stats_years(maize_phenology_df, "UHK")

"figure"

In the last figure we use the default AquaCrop parameters for the phenology phases. We see that the actual phase happens long before the simulated phase, specially for harvest.

In order to calibrate the crop stages with the actual data we will use a simple heuristic, we will set the crop's parameters equal to the median of the actual observed stage (in case we do not have a median value we use the default value from AquaCrop).

# get a statistical distribution of the actual gdd data
describe(maize_phenology_actual_df)
11×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyAnyAnyInt64Type
1sowingdate2015-04-292018-10-312022-05-090Date
2harvestdate2015-08-312022-08-150Date
3year2018.520152018.520220Int64
4harvest_actualdays115.87598119.01240Union{Missing, Int64}
5harvest_actualgdd1122.941026.851126.91211.90Union{Missing, Float64}
6beginflowering_actualdays73.256971.5790Union{Missing, Int64}
7beginflowering_actualgdd632.438530.2643.075702.950Union{Missing, Float64}
8endflowering_actualdaysNaN8Union{Missing, Int64}
9endflowering_actualgddNaN8Union{Missing, Float64}
10emergence_actualdays12.75713.5150Union{Missing, Int64}
11emergence_actualgdd61.143743.864.42573.60Union{Missing, Float64}
# create a kw tuple with the additional information that we wish to pass to AquaCrop
# consider the median of the actual gdd distribution for each phenology phase
crop_dict = merge(crop_dict, Dict(
            "GDDaysToHarvest" => 1127, # harvest actual median
            "GDDaysToFlowering" => 643, # beginflowering actual median
            "GDDaysToGermination" => 64, # emergence actual median
            "GDDLengthFlowering" => 180 # default value of aquacrop since we do not have endflowering
        ));
kw = (
        crop_dict = crop_dict,
     );

# get the days and growing degree days for each phenology phase from the simulated data using the additional information
maize_phenology_simulated_df = CropGrowthTutorial.process_crop_phenology_simulated_gdd(crop_name, maize_phenology_df, uhk_clim_df; kw...)

# compare the day difference between the actual data and the simulated data
maize_phenology_df = leftjoin(maize_phenology_actual_df, maize_phenology_simulated_df; on=:sowingdate);
CropGrowthTutorial.plot_GDD_stats_years(maize_phenology_df, "UHK")

"figure"

In the last figure we use the calibrated parameters for the phenology phases. We see that the actual phase and the simulated phase are not far away.

CropGrowthTutorial.plot_correlation(maize_phenology_df[:,:harvest_actualdays], maize_phenology_df[:,:harvest_simulateddays], crop_type, "UHK", "harvest date")

"figure"

Soil-root and Canopy Cover

Once we have calibrated the crop stages, we can calibrate the parameters that control the canopy growth, canopy decay and root growth.

Canopy Cover

To calibrate the canopy cover we will see the qualitative behaviour of or sim

# use the parameters that we have calibrated until now
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# send information about the actual phenology dates to the plot function
kw_plot = (
        end_day = maize_phenology_processed_df[i,:harvestdate] - Date("0001-01-01"),
        emergence_day = maize_phenology_processed_df[i,:emergencedate] - Date("0001-01-01"),
        beginflowering_day = maize_phenology_processed_df[i,:beginfloweringdate] - Date("0001-01-01"),
        endflowering_day = maize_phenology_processed_df[i,:endfloweringdate] - Date("0001-01-01"),
)

# plot the results
CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...)
SIMULATION WENT WELL

"figure"

In last figure we show that the simulated crop stages go in hand with the actual recorded phenolgy stages (dashed lines). Nevertheless, the Canopy Cover grows slowly and decays abruptly on the harvest date, to calibrate this behaviours we do the following:

  1. Canopy Growth: This is influenced by the crop parameters GDDaysToFullCanopy and GDDCGC (Canopy Growth Coefficient per Growing Degree Day). We adjust GDDaysToFullCanopy using the calibrated values of GDDaysToHarvest and GDDaysToFlowering (see the function CropGrowthTutorial.proportional_adjust). The idea is that these helper parameters reflect the overall pace of the crop's development, so we scale the target parameter (GDDaysToFullCanopy) in proportion to their changes. This way, the adjustment stays consistent with the crop's developmental timeline. The Canopy Growth Coefficient (GDDCGC) is calibrated using the time to full canopy, the maximum canopy cover, and AquaCrop’s canopy growth formula (see CropGrowthTutorial.cgc_adjust).
  2. Canopy Decay: It is controlled by the crop's parameters GDDaysToSenescence, GDDaysToHarvest and GDDCDC (Canopy Decay Coefficient per Growing Degree Day). We calibrate GDDaysToSenescence with the same idea behind the calibration of GDDaysToFullCanopy in last paragraph. The Canopy Decay Coefficient (GDDCDC) is calibrated similarly to GDDCGC, considering the canopy decay process from we use AquaCrop's formula (see CropGrowthTutorial.cdc_adjust).
# add the canopy growth and decay calibrated parameters
crop_dict = merge(crop_dict, Dict(
            "GDDaysToSenescence"  => 950,  # proportional_adjust with default value of this parameter and the new flowering and harvest times
            "GDDCDC"              => 0.0167875, # adjusted using new senescence and harvest times
            "GDDaysToFullCanopy"  => 519,  # proportional_adjust with default value of this parameter and the new flowering and harvest times
            "GDDCGC"              => 0.0161843, # adjusted using new time to full canopy
        ));

# use the parameters that we have calibrated until now
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# send information about the actual phenology dates to the plot function
kw_plot = (
        end_day = maize_phenology_processed_df[i,:harvestdate] - Date("0001-01-01"),
        emergence_day = maize_phenology_processed_df[i,:emergencedate] - Date("0001-01-01"),
        beginflowering_day = maize_phenology_processed_df[i,:beginfloweringdate] - Date("0001-01-01"),
        endflowering_day = maize_phenology_processed_df[i,:endfloweringdate] - Date("0001-01-01"),
)

# plot the results
CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...)
SIMULATION WENT WELL

"figure"

Note that in last figure the full canopy cover happens before flowering, and the canopy decay goes slowly from senescence to harvest. It is important to note that a further calibration on GDDCGC and GDDCDC can be done if we have more data about the canopy cover over the dates, or the final biomass. If we have phenological data about the dates of senescence and canopy cover we can calibrate GDDaysToSenescence and GDDaysToFullCanopy.

Root expansion

Now we focus on the root growth, to do this we will analyze the root zone expansion and the water content on it.

# Using calibration up to now
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

The previous figures show the soil water content in the root zone and the water-related stresses experienced by the crop. We can see that the root zone keeps expanding until the crop reaches the end of its life cycle, and that the crop doesn't suffer from significant water stress—whether due to lack or excess of water.

To calibrate how the root zone grows, we focus on two key parameters:

Time to Maximum Rooting (GDDaysToMaxRooting) – This represents how long it takes for the roots to reach their maximum depth. We adjust this parameter in proportion to the crop's overall development timeline, similar to how we adjust GDDaysToFullCanopy. Maximum Effective Rooting Depth (RootMax) – This defines how deep the roots can go. We calibrate this value by observing the crop's water stress patterns: if the root zone is too shallow or too deep, it can lead to water deficits or excesses that cause premature canopy decline—something that doesn’t match the experimental behavior of our crop. The following figures show how changing RootMax affects the crop’s access to water and its stress levels.

# use the calibration of time to max rooting
crop_dict = merge(crop_dict, Dict(
            "GDDaysToMaxRooting"  => 955 # proportional_adjust with default value of this parameter and the new flowering and harvest times
        ));
kw = (
        crop_dict = crop_dict,
     );

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield)) 

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

In last figures we see the root stops growing before the end of crop cycle

# shallow root
crop_dict["RootMax"] = 0.8; # default 2.3 - 1.5
kw = (
        crop_dict = crop_dict,
     );
# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

In last figures we see that a shallow root leaves to a water deficit and triggers early senscence.

# deep root
crop_dict["RootMax"] = 3.8; # default 2.3 + 1.5
kw = (
        crop_dict = crop_dict,
     );
# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

In last figure we see that a deep root leaves to less water stresses on this soil type, but it may be that in some other soils this causes an excess of water.

In the following figures we choose two root sizes and change the soil type to "clay"

# deep root on soil_type = clay
crop_dict["RootMax"] = 3.8; # default 2.3 + 1.5
kw = (
        crop_dict = crop_dict,
     );
# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation("clay", crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, "clay"; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}
# default root on soil_type = clay
crop_dict["RootMax"] = 2.3; # default 2.3
kw = (
        crop_dict = crop_dict,
     );
# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation("clay", crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, "clay"; kw_plot...))

# plot soil water content on root zone
display(CropGrowthTutorial.plot_soil_wc(cropfield))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :water))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

we see that a deeper root causes bigger water stress when the soil type is "clay", which affects the biomass production.

## The root canopy calibration can be done by the following function
CropGrowthTutorial.calibrate_canopy_root_parameters!(crop_dict, crop_name);
crop_dict
# for now this function does not calibrate RootMax.
Dict{String, Any} with 11 entries:
  "GDDCGC"              => 0.0161843
  "GDDaysToGermination" => 64
  "GDDaysToFlowering"   => 643
  "GDDaysToMaxRooting"  => 955
  "GDDLengthFlowering"  => 180
  "GDDCDC"              => 0.0167875
  "RootMax"             => 2.3
  "GDDaysToHarvest"     => 1127
  "GDDaysToFullCanopy"  => 519
  "PlantingDens"        => 117000
  "GDDaysToSenescence"  => 950

Biomass

On our dataset of maize we have the Biomass production over a span of years. We want to see if we can reproduce the actual biomass data with our simulations.

# base line of biomass production

# use the calibrated parameters that we have until now
kw = (
        crop_dict = crop_dict,
     );


biomass_actual = [];
biomass_simulated_baseline = [];
years = [];

# select the biomass data for the given crop_type
yield_df = filter(row->row.crop_type==crop_type, uhk_yield_df);

# for each year that we have data on
for rowi in eachrow(maize_phenology_processed_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end


    append!(years,yeari)
    append!(biomass_actual, yield_df[1,string(yeari)]/10)
    append!(biomass_simulated_baseline, ustrip(cropfield.dayout[end,:Biomass]))
end

CropGrowthTutorial.plot_yearly_data([biomass_actual, biomass_simulated_baseline], ["Biomass data", "simulated biomass baseline"],
                      years, uppercase(crop_type), uppercase(soil_type), station_name)

"figure"

On last figure we see that our simulated biomass is much lower than the actual value, this can be happening due a lot of factors, from which we will consider the following ones at the moment of calibrating the crop parameters.

  1. The crop goes through stresses that reduce the productivity, we have seen the water stresses but we will also analyze the temperature stresses, that are measured by the threshold parameter GDtranspLow.
  2. The crop variety can have a bigger water productivity than the one considered by default in AquaCrop, the parameter that we care in this case is WP.
  3. The crop variety grows and dies faster at a faster rate than the simulated, this is controlled by the parameter GDDCGC and GDDCDC, which we previusly calibrated with a simple heuristic that can be improved with additionaly data about the crop's development.

Temperature stress

Let us see the temperature effect on the biomass production

# Crop growth with base line parameters
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type, ["CC", "Biomass"]; kw_plot...))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :temperature))

# plot temperature 
display(CropGrowthTutorial.plot_temp(cropfield))
SIMULATION WENT WELL

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

on last figures we see the crop goes through a lot of temperature stress, this is measured by the parameter GDtranspLow. For such a high biomass production we do not expect to have a stressed crop, so we can change it's value such that the crop's simulation behaviour is closer to the observed data.

# Crop growth with adjusted GDtranspLow
# for maize default value of GDtranspLow is 12 Celcius avobe the base temperature that is 15, so the crop
# is stressed at 27 Celcius, this is a high temperature for Germany. We change the value to 2 Celcius, then
# the stress beggins at 17 Celcius
crop_dict["GDtranspLow"] = 2.0

kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type, ["CC", "Biomass"]; kw_plot...))

# plot stresses on crop
display(CropGrowthTutorial.plot_crop_stress(cropfield, :temperature))
SIMULATION WENT WELL

"figure"

"figure"

CairoMakie.Screen{IMAGE}
# Biomass production without temperature stress

# use the calibrated parameters that we have until now
kw = (
        crop_dict = crop_dict,
     );

biomass_simulated_temp = [];

# select the biomass data for the given crop_type
yield_df = filter(row->row.crop_type==crop_type, uhk_yield_df);

# for each year that we have data on
for rowi in eachrow(maize_phenology_processed_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end

    append!(biomass_simulated_temp, ustrip(cropfield.dayout[end,:Biomass]))
end

CropGrowthTutorial.plot_yearly_data([biomass_actual, biomass_simulated_baseline, biomass_simulated_temp],
                        ["Biomass data", "simulated biomass baseline", "simulated biomass no temp stress"],
                      years, uppercase(crop_type), uppercase(soil_type), station_name)

"figure"

Water Productivity

Another important factor is the water productivity. AquaCrop's default value is conservative, such that the crop simulation works on several regions. On our case in Germany there isn't usually water deficits, so we can have crops that convert more water to biomass without negative effects.

# biomass production increasing Water Productivity

# use the calibrated parameters that we have until now
# the default value of AquaCrop is 33.7, we are changing it in a 33%
crop_dict["WP"] = 33.7 * 1.33
kw = (
        crop_dict = crop_dict,
     );

biomass_simulated_wp = [];

# select the biomass data for the given crop_type
yield_df = filter(row->row.crop_type==crop_type, uhk_yield_df);

# for each year that we have data on
for rowi in eachrow(maize_phenology_processed_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end

    append!(biomass_simulated_wp, ustrip(cropfield.dayout[end,:Biomass]))
end

CropGrowthTutorial.plot_yearly_data([biomass_actual, biomass_simulated_baseline, biomass_simulated_temp, biomass_simulated_wp],
                        ["Biomass data", "simulated biomass baseline", "simulated biomass no temp stress", "simulated biomass more wp"],
                      years, uppercase(crop_type), uppercase(soil_type), station_name)

"figure"

Canopy growth-death reate

Finally, the crop's growth rate directly affetcs the biomass production.

# Crop growth before changing canopy growth rate
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type, ["CC", "Biomass"]; kw_plot...))

# plot crop's evapotranspiration
display(CropGrowthTutorial.plot_eto_tr(cropfield))
SIMULATION WENT WELL

"figure"

"figure"

CairoMakie.Screen{IMAGE}

The biomass production each day is proportional to the crop's actual transpiration that given day Tact, which at the same time it is roughly proportional to the product of the canopy cover and the recorder evapotranspiration ETo (or from the climate data the potential_evapotranspiration). Then, a faster crop development allows to have more days with bigger canopy cover, given a bigger biomass. This increased canopy growth rate in a summer variety, as the case of maize, can be justified with more experimental data.

# Crop growth after changing canopy growth-death rate
crop_dict["GDDCGC"] *= 1.33  # say it grows 33% faster than previous simulations
#crop_dict["GDDCDC"] *= 0.9  # say it decays 10% slower than previous simulations

kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end

# plot the results
display(CropGrowthTutorial.plot_daily_stuff_one_year(cropfield, crop_type, soil_type, ["CC", "Biomass"]; kw_plot...))

# plot crop's evapotranspiration
display(CropGrowthTutorial.plot_eto_tr(cropfield))
SIMULATION WENT WELL

"figure"

"figure"

CairoMakie.Screen{IMAGE}
# biomass production increasing CGC

kw = (
        crop_dict = crop_dict,
     );

biomass_simulated_cgc = [];

# select the biomass data for the given crop_type
yield_df = filter(row->row.crop_type==crop_type, uhk_yield_df);

# for each year that we have data on
for rowi in eachrow(maize_phenology_processed_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end

    append!(biomass_simulated_cgc, ustrip(cropfield.dayout[end,:Biomass]))
end

CropGrowthTutorial.plot_yearly_data([biomass_actual, biomass_simulated_baseline, biomass_simulated_temp,
                                    biomass_simulated_wp, biomass_simulated_cgc],
                        ["Biomass data", "simulated biomass baseline", "simulated biomass no temp stress",
                          "simulated biomass more wp", "simulated biomass more cgc"],
                      years, uppercase(crop_type), uppercase(soil_type), station_name)

"figure"

CropGrowthTutorial.plot_correlation(biomass_actual, biomass_simulated_cgc, crop_type, station_name, "Biomass")

"figure"

In last figure we see the effect of calibrating each parameter on the biomass production. Further studies are required to reproduce the high production years.

Yield

On our database we are using silage maize, we do not have information about the corns/grains, nevertheless we will show the effect of the most important parameters on the Yield production of grain/frui crops:

  1. Harvest Index HI. Says te proportion of Biomass that is converted to Yield.
  2. The air temperature during pollination has the thresholds Tupper and Tcold, in temperatures outside this range the pollination starts to fail.

Harvest Index

In the following figures we show the effect of changing the default value of AquaCrop's maize Harvest Index. We can see that greater HI means greater Yield.

# Crop growth before changing Harvest Index
kw = (
        crop_dict = crop_dict,
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield_0, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end




# Crop growth increasing the Harvest index by 20 %
kw = (
        crop_dict = merge(crop_dict, Dict("HI" => round(Int,48*1.2))),
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield_M, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end



# Crop growth decreasing the Harvest index by 20 %
kw = (
        crop_dict = merge(crop_dict, Dict("HI" => round(Int,48*0.8))),
     );

# the simulation's date is one for which we have phenology data
i = 1;
sowing_date = maize_phenology_processed_df[i,:sowingdate]

# run a simulation sending the additional kw
cropfield_m, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, uhk_clim_df; kw...);

# check if all is ok with the simulation
if all_ok.logi
    println("\nSIMULATION WENT WELL\n")
else
    println("\nBAD SIMULATION, error\n")
    println(all_ok.msg)
    println("")
end


# plot the results

xx = findlast(x->x==4, cropfield_0.dayout.Stage) + 1
f = Figure()
coli = "Y(fresh)"
ax = Axis(f[1, 1],
    title = coli*" vs Date",
    xlabel = "Date",
    ylabel = coli,
)
x = cropfield_0.dayout[1:xx,"Date"]
lines!(ax, x, ustrip.(cropfield_0.dayout[1:xx, coli]), label="100% HI")
lines!(ax, x, ustrip.(cropfield_M.dayout[1:xx, coli]), label="120% HI")
lines!(ax, x, ustrip.(cropfield_m.dayout[1:xx, coli]), label="80% HI")
axislegend(position = :lt)


f
SIMULATION WENT WELL


SIMULATION WENT WELL


SIMULATION WENT WELL

"figure"

Temperature Thresholds

For grain/fruit corps we have some temperature thresholds during pollination, if the climate temperature is outside this range it causes crop stresses at the moment of producing yield. Although the threshold parameters are not calibrated, it is useful to plot their values and also the daily minimal and maximal temperature, in this way we can see if our crop has yield stresses during pollination. See the following figure

# Temperature variables and thresholds
CropGrowthTutorial.plot_temp(cropfield_0)

"figure"

Full calibration

In the following code we will collect the data for winter wheat, and make the crop's parameters full calibration.

## SET THE DATA ##

# Select a station's short_name
station_name = "Jena";

# Select a crop_type 
crop_type = "winter_wheat";

# Climate data for a given station
shk_clim_df =  CropGrowthTutorial.get_climate_data(station_name);

# Field yield data for a given station
shk_yield_df = CropGrowthTutorial.get_yield_data(station_name);

# Crop's phenology raw data for a given station
wwheat_phenology_raw_df = CropGrowthTutorial.get_crop_phenology_data(crop_type, station_name);

# get the soil_type for the given station
soil_type = filter(row -> row.station_name==station_name, stations_df)[1,:soil_type];

# get the aquacrop_name for the given crop_type
crop_name = filter(row -> row.crop_type==crop_type, general_crop_data_df)[1,:aquacrop_name];

# get the planting density for the given crop_type
plantingdens = filter(row -> row.crop_type==crop_type, general_crop_data_df)[1,:plantingdens];

# create a kw tuple with the additional information that we wish to pass to AquaCrop
kw = ( crop_dict = Dict{String,Any}("PlantingDens" => plantingdens),);

# select a sowing phase and harvest phase for the crop  start the phenology calibration
sowing_phase = 10;
harvest_phase = 24;


## CALIBRATION ##

# 1. Calibrate Phenology 
crop_dict, ww_phenology_df = CropGrowthTutorial.calibrate_phenology_parameters(wwheat_phenology_raw_df, crop_name, shk_clim_df, 
                                                  sowing_phase, harvest_phase; kw...)

# Plot the partial results until now
f = CropGrowthTutorial.plot_correlation(ww_phenology_df[:,:harvest_actualdays], ww_phenology_df[:,:harvest_simulateddays], crop_type, "SHK", "harvest date");
display(f)


# 2. Calibrate the canopy growth parameters
CropGrowthTutorial.calibrate_canopy_root_parameters!(crop_dict, crop_name);

# base line of yield production
# use the calibrated parameters that we have until now
kw = (
        crop_dict = crop_dict,
     );
yield_actual = [];
yield_simulated_baseline = [];
years = [];
# select the biomass data for the given crop_type
yield_df = filter(row->row.crop_type==crop_type, shk_yield_df);
# for each year that we have data on
for rowi in eachrow(ww_phenology_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, shk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end

    append!(years,yeari)
    append!(yield_actual, yield_df[1,string(yeari)]/10)
    append!(yield_simulated_baseline, ustrip(cropfield.dayout[end,"Y(fresh)"]))
end


# 3. Calibrate biomass-yield parameters
CropGrowthTutorial.calibrate_biomass_yield_parameters!(crop_dict, crop_name, soil_type, shk_clim_df, :Yield,  ww_phenology_df, yield_actual)

# new yield production after calibration
kw = (
        crop_dict = crop_dict,
     );
yield_simulated_calibrated = [];
# for each year that we have data on
for rowi in eachrow(ww_phenology_df)
    sowing_date = rowi.sowingdate
    yeari = rowi.year
    # run a simulation sending the additional kw
    cropfield, all_ok = CropGrowthTutorial.run_simulation(soil_type, crop_name, sowing_date, shk_clim_df; kw...);
    # check if all is ok with the simulation
    if !all_ok.logi
        println("\nBAD SIMULATION, error\n")
        println(all_ok.msg)
        println("on year ",yeari)
        println("")
        continue
    end
    append!(yield_simulated_calibrated, ustrip(cropfield.dayout[end,"Y(fresh)"]))
end

# Plot results of calibration
display(CropGrowthTutorial.plot_yearly_data([yield_actual, yield_simulated_baseline,
                                    yield_simulated_calibrated],
                        ["Yield data", "simulated yield baseline",
                          "simulated yield calibrated"],
                      years, uppercase(crop_type), uppercase(soil_type), station_name))

display(CropGrowthTutorial.plot_correlation(yield_actual, yield_simulated_calibrated, crop_type, station_name, "Yield"))

"figure"

"figure"

"figure"

CairoMakie.Screen{IMAGE}

In last figure see that we have problems simulating the years with low production of yield.

  • 1https://github.com/brry/rdwd
  • 2https://statistik.thueringen.de/datenbank/TabAnzeige.asp?tabelle=kr000516%7C%7C