Get Started with the OptirrigCORE package

Introduction

The Optirrig model in a nutshell

Optirrig is a one-dimensional conceptual model that ties together thermal-time crop development, soil–plant water balance, and management rules to reproduce irrigated systems. It tracks plant status (LAI, biomass, yield), soil water reserves (RU), and nitrogen demand while applying irrigation rules that can be user-defined or phenology-driven.

What is the OptirrigCORE package?

OptirrigCORE is the R interface that wraps the Optirrig engine and makes the workflow reproducible:

  • Data prep: import or generate crop, soil, climate, irrigation, and nitrogen descriptions.
  • Configuration: build consistent parameter tables and configuration files.
  • Execution: run single scenarios or big batches from one command.
  • Analysis: output objects plug straight into ggplot2 and tidy pipelines.
  • Reproducibility: YAML + CSV inputs keep everything versionable.

Use OptirrigCORE for research, teaching exercises, rapid prototyping of decision rules, or operational campaign planning. The default assets get you started fast, and every component can be swapped for custom data whenever you need it.

Let’s Get Started!

This vignette walks you through a minimal OptirrigCORE workflow. You will learn how to:

  1. Install OptirrigCORE (and remotes if missing).
  2. Import climate and optional irrigation data.
  3. Build a parameter object that describes your configuration(s).
  4. Call run_model() and capture the outputs.
  5. Plot LAI, biomass, yield, soil water, and irrigation.

Prerequisites

  • R >= 4.1 plus remotes.
  • Write access to a directory that will store configs, inputs, and outputs.
  • (Optional) A clone of the repository if you plan to edit templates locally.

Step 1 : Install the OptirrigCORE Package

To install OptirrigCORE from the INRAE GitLab repository:

install.packages("remotes")
remotes::install_git("https://forge.inrae.fr/OptirrigHIVE/OptirrigCORE.git", build_vignettes = TRUE, dependencies = TRUE)

Once installed, load the package:

library(OptirrigCORE)

Step 2 : Load Climate & Irrigation data

OptirrigCORE requires daily weather data (rainfall, temperature, radiation, and reference evapotranspiration) to simulate crop growth and soil water balance.

In this vignette, we shall use example datasets shipped with the package, representing a maize experiment conducted in 2021 at the Lavalette experimental site (Montpellier, France). Using data() avoids file paths and makes the example fully reproducible.

data("MAIZE_21LAVALETTE_clim")
str(MAIZE_21LAVALETTE_clim)
#> spc_tbl_ [365 × 5] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>  $ Date: chr [1:365] "01/01/2021" "02/01/2021" "03/01/2021" "04/01/2021" ...
#>  $ P   : num [1:365] 0 0.5 1.5 0 0.5 0 0 0.5 0 0 ...
#>  $ etp : num [1:365] 0 0.3 0.6 0 0 0 0.1 0.1 1.1 0.9 ...
#>  $ Rg  : num [1:365] 711 178 346 643 734 816 716 837 497 234 ...
#>  $ T   : num [1:365] 3.7 2.6 3.6 2.4 0.9 -1.3 -1 1.4 4.7 2.6 ...
#>  - attr(*, "spec")=List of 3
#>   ..$ cols   :List of 5
#>   .. ..$ Date: list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
#>   .. ..$ P   : list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. ..$ etp : list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. ..$ Rg  : list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. ..$ T   : list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   ..$ default: list()
#>   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
#>   ..$ delim  : chr ","
#>   ..- attr(*, "class")= chr "col_spec"
#>  - attr(*, "problems")=<pointer: (nil)>

The climate dataset follows the standard Optirrig input format, with the columns below. In practice the model expects a daily time step and a date column that fully covers your simulation period.

Column Name Description
Date Date in “dd/mm/yyyy” format
P Daily rainfall (mm)
T Daily temperature (°C)
Rg Daily global solar radiation (J/cm²)
etp Daily reference evapotranspiration (mm)

As the model is a thermal-time driven, the Date column is essential to assure that the climate data aligns with the simulation period. In fact simulations period need to be fully included in the climate data.

Internally, OptirrigCORE derives as . Both and are handled in , while is a dimensionless intercepted fraction and biomass outputs (, ) are expressed in .

If your Date column is stored as text, it’s fine: OptirrigCORE will parse common formats (including "dd/mm/yyyy"). Still, it’s a good habit to verify it once:

As an optional step, you can also provide an irrigation input. The model can simulate irrigation using either simple decision rules (e.g., water-stress thresholds, water-turns, quotas, etc …) or a predefined calendar.

In this ex post example, we use the observed irrigation calendar from the 2021 experiment.

data("MAIZE_21LAVALETTE_irrig")
str(MAIZE_21LAVALETTE_irrig)
#> spc_tbl_ [12 × 2] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>  $ Date: chr [1:12] "15/06/2021" "17/06/2021" "25/06/2021" "30/06/2021" ...
#>  $ Dose: num [1:12] 16.1 9.1 20.2 18.6 5.6 19.4 18.5 27.2 59.9 4 ...
#>  - attr(*, "spec")=List of 3
#>   ..$ cols   :List of 2
#>   .. ..$ Date: list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
#>   .. ..$ Dose: list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   ..$ default: list()
#>   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
#>   ..$ delim  : chr ","
#>   ..- attr(*, "class")= chr "col_spec"
#>  - attr(*, "problems")=<pointer: (nil)>

Step 3 — Describe the simulation configuration

Now that climate (and optionally irrigation) data are loaded, the next step is to describe what you want to simulate: crop, soil, dates, and a few key options.

OptirrigCORE accepts several input styles (CSV files, data.frames, or lists). For a single scenario, the simplest and most explicit approach is to build a flat named list directly in R. You can provide climate/irrigation either as in-memory tables (as we do here) or as paths to files.

In this vignette we’ll keep it minimal:

  • one crop ("maize") and one soil profile ("Lavalette" in this example);
  • a start/end window that is fully included in the climate data;
  • an observed irrigation calendar.

Which crops and soils are available?

OptirrigCORE ships with a set of pre-configured crops and soils that can be used directly, without any additional setup. When you specify a crop name (crop) or a soil code/profile (soil) in your input object, OptirrigCORE automatically loads the corresponding parameter set behind the scenes.

👉 You do not need to worry about the underlying parameterization: the name simply acts as a key pointing to a consistent, ready-to-use configuration.

Importantly, these presets are not restrictive: even when using built-in crops or soils, you can override or add any parameter you want at simulation setup time to explore alternative assumptions or management options.

Available crops : The table below lists the crops currently available in the installed version of the package. The value to use in the crop argument corresponds directly to the crop name shown.

cultures Calibration Status
maize need
beetroot done
potato need
soybean need
wheat need

For example, setting crop = “maize” automatically selects the parameter set describing maize phenology, growth, and water demand.

Available soils : Similarly, OptirrigCORE includes several standard soil profiles. Each soil is identified by a short code that reflects its dominant texture.

sols Description
C Clay
CL Clay Loam
L Loam
LSa Loamy Sand
Sa Sand
SaC Sandy Clay
SaCL Sandy Clay Loam
SaL Sandy Loam
SiC Silty Clay
SiCL Silty Clay Loam
SiL Silty Loam

For instance, using soil = “CL” automatically selects a clay loam soil profile with its associated hydraulic properties.

💡 Tip These built-in crops and soils are ideal for getting started and exploring the model. Advanced users can later introduce custom parameterizations if needed.

At minimum, a scenario need to include:

Parameter Description
run_id Unique scenario name
crop Crop identifier available in the internal library (e.g., maize, beetroot)
soil Soil identifier from the bundled database or a custom soil (e.g., CL, SaC)
date_start Simulation start date (string “dd/mm/yyyy”)
date_end Simulation end date (string “dd/mm/yyyy”)
date_sowing Sowing date start (string “dd/mm/yyyy”)
date_harvest Harvest date end (string “dd/mm/yyyy”)
zmax Maximum rooting depth (m)
ratio_R_init Initial relative soil water reserve (0–1)
climate Climate input (path or data.frame)
irrigation Optional irrigation input (path or data.frame)
irrig_mod 0 = rainfed/calendar, 1 = rule-driven irrigation

Therefore, several methods exist to prepare simulation scenarios and use them with run_model(). The function can accept either a path to a CSV file, a data.frame, or a list, provided that the objects already exist in the R environment and follow the expected input format.

path: a CSV file where each row corresponds to a distinct simulation scenario. data.frame: each row of the data frame corresponds to a distinct simulation scenario. list: either a flat named list describing one scenario, or a named list of scenarios for batch runs.

For a first run, a flat list is the most direct option, as shown below:

# Example: flat list (all parameters at the same level)
input <- list(
  run_id = "Run_1",
  crop = "maize",
  soil = "CL",
  climate = MAIZE_21LAVALETTE_clim,
  irrigation = MAIZE_21LAVALETTE_irrig,
  fertilisation = NA,
  date_sowing = "20/04/2021",
  date_harvest = "06/09/2021",
  date_start = "01/04/2021",
  date_end = "06/09/2021",
  irrig_mod = 0,
    zmax = 1.2,
  ratio_R_init = 0.7,
  irrig_strategy_start = "25/04/2021",
  irrig_strategy_end = "01/08/2021"
)
str(input)
#> List of 15
#>  $ run_id              : chr "Run_1"
#>  $ crop                : chr "maize"
#>  $ soil                : chr "CL"
#>  $ climate             : spc_tbl_ [365 × 5] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..$ Date: chr [1:365] "01/01/2021" "02/01/2021" "03/01/2021" "04/01/2021" ...
#>   ..$ P   : num [1:365] 0 0.5 1.5 0 0.5 0 0 0.5 0 0 ...
#>   ..$ etp : num [1:365] 0 0.3 0.6 0 0 0 0.1 0.1 1.1 0.9 ...
#>   ..$ Rg  : num [1:365] 711 178 346 643 734 816 716 837 497 234 ...
#>   ..$ T   : num [1:365] 3.7 2.6 3.6 2.4 0.9 -1.3 -1 1.4 4.7 2.6 ...
#>   ..- attr(*, "spec")=List of 3
#>   .. ..$ cols   :List of 5
#>   .. .. ..$ Date: list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
#>   .. .. ..$ P   : list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. .. ..$ etp : list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. .. ..$ Rg  : list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. .. ..$ T   : list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. ..$ default: list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
#>   .. ..$ delim  : chr ","
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<pointer: (nil)> 
#>  $ irrigation          : spc_tbl_ [12 × 2] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..$ Date: chr [1:12] "15/06/2021" "17/06/2021" "25/06/2021" "30/06/2021" ...
#>   ..$ Dose: num [1:12] 16.1 9.1 20.2 18.6 5.6 19.4 18.5 27.2 59.9 4 ...
#>   ..- attr(*, "spec")=List of 3
#>   .. ..$ cols   :List of 2
#>   .. .. ..$ Date: list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
#>   .. .. ..$ Dose: list()
#>   .. .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#>   .. ..$ default: list()
#>   .. .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
#>   .. ..$ delim  : chr ","
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<pointer: (nil)> 
#>  $ fertilisation       : logi NA
#>  $ date_sowing         : chr "20/04/2021"
#>  $ date_harvest        : chr "06/09/2021"
#>  $ date_start          : chr "01/04/2021"
#>  $ date_end            : chr "06/09/2021"
#>  $ irrig_mod           : num 0
#>  $ zmax                : num 1.2
#>  $ ratio_R_init        : num 0.7
#>  $ irrig_strategy_start: chr "25/04/2021"
#>  $ irrig_strategy_end  : chr "01/08/2021"

For batch runs, you can also pass a named list of scenario objects or a parameter table with one row per simulation.

If you’re unsure which parameters exist (or want to see defaults), these help pages are the quickest entry point: https://optirrighive.pages-forge.inrae.fr/OptirrigCORE/dev/articles/v00_params_overview.html

For more details on run_model() and plot(), check the docs:

?run_model
?plot

Step 4 — Run the simulation

run_model() can take a path to a parameter CSV, a data.frame, or a list.
Here we pass the in-memory object created as a simple list (one scenario).

The returned object is a named list with one element per scenario (run_id), each containing time series (states/fluxes) and summary variables that you can plot or post-process.

outputs <- run_model(
  input,
  options = list(log = FALSE, force = TRUE, return_outputs = TRUE, save = FALSE)
)
str(outputs)
#> List of 1
#>  $ Run_1:'data.frame':   159 obs. of  123 variables:
#>   ..$ Date              : Date[1:159], format: "2021-04-01" "2021-04-02" ...
#>   ..$ P                 : num [1:159] 0 0 0 0 0 0 0 0 9.5 9 ...
#>   ..$ ETP               : num [1:159] 2.9 3.2 3.8 3.1 3.6 3.9 3.2 2.7 2.1 0.2 ...
#>   ..$ Rg                : num [1:159] 2259 2145 2405 2116 2214 ...
#>   ..$ T                 : num [1:159] 10.8 13.3 15.3 12.1 13.8 14.1 7.8 7.4 11.8 9.6 ...
#>   ..$ TT                : num [1:159] 4.8 12.1 21.4 27.5 35.3 43.4 45.2 46.6 52.4 56 ...
#>   ..$ TT_sowing         : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TT_LAI            : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TT_root           : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TT_LAI_norm       : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ alpha             : num [1:159] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ LT                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ LAIp              : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ LAI               : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ degred            : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ zrootp            : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ Tcrit             : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ B                 : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ Bp                : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ B_root            : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ Bp_root           : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_sowing       : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_harvest      : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_emerg        : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ iday_root_install : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ iday_crop_dev     : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ stress_ES         : num [1:159] 1 0 0 0 0 0 0 0 0 1 ...
#>   ..$ stress_TP         : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ stress_ETR        : num [1:159] 1 0 0 0 0 0 0 0 0 1 ...
#>   ..$ stress_T          : num [1:159] 1 1 1 1 1 1 1 1 1 1 ...
#>   ..$ stress_LAI        : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ stress_B          : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ stress_noci_LAI   : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ stress_noci_B     : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ taum              : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ crac              : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ ks_root           : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ dzrootp           : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ dzroot            : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ zroot             : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ case              : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ Kc                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ IR                : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ IRp               : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ PAR               : num [1:159] 1130 1072 1202 1058 1107 ...
#>   ..$ Cp                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ sugar_accumulation: num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ Y_sugar           : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ Yp_sugar          : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_strategy     : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_constraints  : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_no_irrig     : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ iday_waterturn    : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ iday_tbis         : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ W_need            : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ RU_daily_perc     : num [1:159] NA NA NA NA NA NA NA NA NA NA ...
#>   ..$ W_eff             : num [1:159] 2.72 2.72 2.72 2.72 2.72 ...
#>   ..$ ET0               : num [1:159] 2.9 3.2 3.8 3.1 3.6 3.9 3.2 2.7 2.1 0.2 ...
#>   ..$ ES0               : num [1:159] 0.87 0.96 1.14 0.93 1.08 1.17 0.96 0.81 0.63 0.06 ...
#>   ..$ TP0               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ ETM               : num [1:159] 0.87 0.96 1.14 0.93 1.08 1.17 0.96 0.81 0.63 0.06 ...
#>   ..$ theta1_init       : num [1:159] 0.00226 0 0 0 0 ...
#>   ..$ theta2_init       : num [1:159] 0.00226 0.00226 0.00226 0.00226 0.00226 ...
#>   ..$ theta3_init       : num [1:159] 0.00226 0.00226 0.00226 0.00226 0.00226 ...
#>   ..$ z1_init           : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ z2_init           : num [1:159] 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 ...
#>   ..$ z3_init           : num [1:159] 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 ...
#>   ..$ z1                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ z2                : num [1:159] 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 0.15 ...
#>   ..$ z3                : num [1:159] 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 ...
#>   ..$ R1                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ R2                : num [1:159] 0.339 0.339 0.339 0.339 0.339 ...
#>   ..$ R3                : num [1:159] 2.38 2.38 2.38 2.38 2.38 ...
#>   ..$ theta1            : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ theta2            : num [1:159] 0.00226 0.00226 0.00226 0.00226 0.00226 ...
#>   ..$ theta3            : num [1:159] 0.00226 0.00226 0.00226 0.00226 0.00226 ...
#>   ..$ I1                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ Kd1               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ Kd2               : num [1:159] 4.4e-48 4.4e-48 4.4e-48 4.4e-48 4.4e-48 ...
#>   ..$ Kd3               : num [1:159] 4.4e-48 4.4e-48 4.4e-48 4.4e-48 4.4e-48 ...
#>   ..$ Rgravity1         : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ Rgravity2         : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ Rgravity3         : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ d1                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ d2                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ d3                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ RU1               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ RU2               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ RU3               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ KES1              : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ KES2              : num [1:159] 0 0 0 0 0 0 0 0 1 1 ...
#>   ..$ ES1               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ ES2               : num [1:159] 0 0 0 0 0 0 0 0 0.63 0.06 ...
#>   ..$ ES                : num [1:159] 0 0 0 0 0 0 0 0 0.63 0.06 ...
#>   ..$ KTP1              : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ KTP2              : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TP1               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TP2               : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   ..$ TP                : num [1:159] 0 0 0 0 0 0 0 0 0 0 ...
#>   .. [list output truncated]
#>  - attr(*, "class")= chr [1:2] "model_outputs" "list"

✅ Tip: for batch runs, you can either add more rows to a parameter table or pass a named list of scenario objects in a single call.

Step 5 — Visualize the outputs

Simulation results are regular R objects, so any plotting stack works. The chunk below builds a simple ggplot2 dashboard showing leaf area, biomass, yield, soil water reserve, and irrigation.

library(ggplot2)
library(patchwork)

df <- as.data.frame(outputs[[1]])
df$Scenario <- names(outputs)[1]
b_max <- max(df$Bp, na.rm = TRUE)
y_max <- max(df$Yp, na.rm = TRUE)
df$Biomass_rel <- if (b_max > 0) 100 * df$B / b_max else NA_real_
df$Biomass_pot_rel <- if (b_max > 0) 100 * df$Bp / b_max else NA_real_
df$Yield_rel <- if (y_max > 0) 100 * df$Y / y_max else NA_real_
df$Yield_pot_rel <- if (y_max > 0) 100 * df$Yp / y_max else NA_real_
df$RU_rel <- if ("RU_daily_perc" %in% names(df) && !all(is.na(df$RU_daily_perc))) {
  df$RU_daily_perc
} else {
  df$RU
}
df$CumDose <- cumsum(pmax(df$I1, 0))

dose_scale <- if (max(df$CumDose, na.rm = TRUE) > 0 && max(df$I1, na.rm = TRUE) > 0) {
  max(df$I1, na.rm = TRUE) / max(df$CumDose, na.rm = TRUE)
} else {
  1
}

base_theme <- theme_minimal(base_size = 11) +
  theme(
    legend.position = "top",
    panel.grid.minor = element_blank()
  )

p_lai <- ggplot(df, aes(Date)) +
  geom_line(aes(y = LAI, colour = "Observed"), linewidth = 0.8) +
  geom_line(aes(y = LAIp, colour = "Potential"), linewidth = 0.7, linetype = 2) +
  scale_colour_manual(values = c("Observed" = "#245b69", "Potential" = "#333333")) +
  labs(title = "Leaf Area Index", y = "LAI", x = NULL, colour = NULL) +
  base_theme

p_biomass <- ggplot(df, aes(Date, Biomass_rel)) +
  geom_line(colour = "#245b69", linewidth = 0.8) +
  geom_line(aes(y = Biomass_pot_rel), colour = "#333333", linewidth = 0.7, linetype = 2) +
  labs(title = "Biomass (relative to max potential)", y = "% of max potential", x = NULL) +
  base_theme

p_ru <- ggplot(df, aes(Date, RU_rel)) +
  geom_line(colour = "#111111", linewidth = 0.8) +
  labs(title = "Useful Water Reserve (%)", y = "RU (%)", x = NULL) +
  base_theme

p_yield <- ggplot(df, aes(Date, Yield_rel)) +
  geom_line(colour = "#245b69", linewidth = 0.8) +
  geom_line(aes(y = Yield_pot_rel), colour = "#333333", linewidth = 0.7, linetype = 2) +
  labs(title = "Yield (relative to max potential)", y = "% of max potential", x = NULL) +
  base_theme

p_irrig <- ggplot(df, aes(Date)) +
  geom_point(aes(y = I1), colour = "#245b69", size = 1.8, alpha = 0.8) +
  geom_line(aes(y = CumDose * dose_scale), colour = "#111111", linewidth = 0.8) +
  scale_y_continuous(
    name = "Irrigation (mm)",
    sec.axis = sec_axis(~ . / dose_scale, name = "Cumulative dose (mm)")
  ) +
  labs(title = "Irrigation & Cumulative Dose", x = "Date") +
  base_theme

p_lai / ((p_biomass | p_ru) / (p_yield | p_irrig))
#> Warning: Removed 32 rows containing missing values or values outside the scale range
#> (`geom_line()`).
#> Warning: Removed 19 rows containing missing values or values outside the scale range
#> (`geom_line()`).
#> Warning: Removed 32 rows containing missing values or values outside the scale range
#> (`geom_line()`).
#> Warning: Removed 19 rows containing missing values or values outside the scale range
#> (`geom_line()`).
#> Warning: Removed 32 rows containing missing values or values outside the scale range
#> (`geom_line()`).
#> Warning: Removed 19 rows containing missing values or values outside the scale range
#> (`geom_line()`).

Wrap-up

With less than thirty lines of code you installed OptirrigCORE, described a scenario, ran the model, and visualized the main state variables.

  • OptirrigCORE keeps simulations reproducible thanks to formatted inputs.
  • Switching crops, soils, or irrigation strategies is as easy as editing a row.
  • Outputs are standard R objects that can be plug into ggplot2 and tidy workflows.

Where to go next?

  1. Add new scenario rows and try rule-driven irrigation (irrig_mod = 1).
  2. Swap the soil for another profile from inst/config.soil.*.
  3. Read the “Evaluation of Irrigation Strategies” vignette for advanced automation ideas.