# Strategy: LPJmL benchmarking
## History
* (-2017) there used to be an [R benchmarking script](https://gitlab.pik-potsdam.de/lpjml/LPJmL_internal/-/wikis/Benchmarking) that could compare the output of two different LPJmL versions regarding:
1. global sums (with literature data)
2. global sums time series
3. difference maps
4. cell time series
* with eddy flux data (NEP, ET)
* with runoff/dischage
6. regional validation with FAO data
* (2018-today) [LPJmLBenchmark function](https://gitlab.pik-potsdam.de/lpjml/LPJmL_internal/-/wikis/LPJmLBenchmark) was established to serve same functionality but be more flexible
* flexibility: in principle every LPJmL output could be used
* new versions of LPJmL (nitrogen, 5arcmin, ...) could be benchmarked
* functionality as before (with exception of FAO data)
## Status quo
`LPJmLBenchmark()` is *outdated*, *unflexible*, *smelly*, *slow* and not *maintainable* ...
* *outdated* it is based on the `LandMark` package, an internal extension of the `raster` and the meanwhile deprecated `sp` package
* *unflexible* it uses a procedural approach that handles the processing of data in long functions without any abstraction to improve understandability. Also function can handle only the rendering of static pdf reports
* [*code smells*](https://en.wikipedia.org/wiki/Code_smell) the code is not DRY, functions are way to long and complicated, too much parameters and object handling, abuse of global environment and more
* *slow* `raster` package lacks performance when it comes to time series data, 30 minutes for a benchmark is too long
* *maintainable* missing strategy in the beginning of `LPJmLBenchmark()` development lead to an acretion of new features rather than a structure that allowed new features to be added, adjusted and maintained with little effort
## Requirements
#### Revised PDF report
* better overview: e.g. table of contents, more accessible and intiuitive content (usage of latest rmarkdown tools)
* reduced complexity or better description of metrics
* additional metrics
* country maps: e.g. FAO data
* lat plots: e.g. NPP data
* ...
* assessment column of sums data (e.g. <5% of soil carbon deviation OK , > 5% not OK)
* revised literature data with additional bibliography at end of report
* ...
####
#### OOP approach based on lpjmlkit
* Why object oriented programming (OOP)?
* human thinking is bound to things/objects and its *natural* composition, e.g. *a room object usually has furniture objects inside of it.*
* modularity facilitates troubleshooting
* reuse of code through inheritance
* flexibility through polymorphism
* in general effective problem solving
* ...
* inherit lpmlkit's `LPJmLData` class to write a new and more generic `LandData` class as well as `LandaDataSet` to execute methods for multiple outputs at once
* to read and process other data types, e.g. region/country-wise FAO data
* inherit `LPJmLMetaData` class to write `LandMetaData` class with less and more generic attributes
* add arithmetics (+, -, *, /) to `LandData` class
* `aggregate()` & `disaggregate()` methods to switch between (to be introduced) new space formats: global, country, region, ... , cell
* `Metric` class to include required data as `LandDataSet`s for each metric with generic (polymorph) method `create_metric()` to e.g. create a sum table or a sum time series
* `MetricSet` class to (analogously to LandDataSet) execute methods for multiple Metrics at once
* `LPJmLBenchmark` as the main benchmark object to store all metrics and the required meta data for a benchmark as well as methods to `print()`, `report()` and more
#### Fast Metrics based on preprocessed data
Each metric is based on a required data format. The underlying conversion and processing should be easily doable also outside of the benchmark reporting to also calculate them easily independently:
Regarding the mentioned metrics this could look like the following:
* **global sums** *following lpjmlkit syntax* e.g.:
```R
# test dataset (the output to be benchmarked)
# area contains the area for every cell of x
test_sums <- aggregate(x = test_dataset * area,
cell = sum,
year = mean)
# reference dataset (the benchmark)
ref_sums <- aggregate(x = ref_dataset * area,
cell = sum,
year = mean)
```
* **global sums timeseries** *following lpjmlkit syntax* e.g.:
```R
# plot ts of test dataset
plot(x = test_dataset * area,
aggregate = list(cell = sum))
# add ts of reference dataset (the benchmark) to plot
plot(x = ref_dataset * area,
aggregate = list(cell = sum),
add = TRUE)
```
* **raster (difference) maps** *following lpjmlkit syntax* e.g.:
```R
# absolute maps -------------------------------------- #
# plot map of test dataset
plot(x = test_dataset * area,
aggregate = list(year = mean))
# add ts of reference dataset (the benchmark) to plot
plot(x = ref_dataset * area,
aggregate = list(year = mean),
add = TRUE)
# difference maps ------------------------------------ #
# plot difference map of test and reference dataset
plot(x = test_dataset * area - ref_dataset * area,
aggregate = list(year = mean))
```
* ...
## Implementation
#### Generic `LandData` model
- use the [R6 class system of package R6](https://r6.r-lib.org) as already done for [`lpjmlkit::LPJmLData`](https://github.com/PIK-LPJmL/lpjmlkit/blob/master/R/LPJmLData.R)
- `LandData`: child class of the `LPJmLData` class that is to be extended thus inheriting its functionality that can then be adjusted if required. An example is the [`LPJmLGridData` class](https://github.com/PIK-LPJmL/lpjmlkit/blob/master/R/LPJmLGridData.R)
- Arithmetics: add arithmetics to `LandData` objects e.g. (`harvest <- yield * cftfrac * cellarea`)
- `aggregate()` function to calculate metrics e.g. for
- global sums `aggregate(harvest, cell = sum, time = mean)`
- global sums time series `aggregate(harvest, cell = sum)`
- regional validation, e.g. FAO data: `aggregate(harvest, cell = sum, use_regions = TRUE)`
- ...
- `disaggregate()` function (regions to cells)
- `LandMetaData`: child class of the [`LPJmLMetaData` class](https://github.com/PIK-LPJmL/lpjmlkit/blob/master/R/LPJmLMetaData.R) (e.g. [`LPJmLGridData` class](https://github.com/PIK-LPJmL/lpjmlkit/blob/master/R/LPJmLGridData.R))
- you may delete attributes such as `sim_name`, `order` or `map`
- you may add new attributes such as ...
- `LandDataSet` is a set of multiple `LandData` objects
- `units` package implementation for data and meta data
```mermaid
%%{ init: { 'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryBorderColor': '#000000'} } }%%
flowchart TD
classDef new fill:#FFFF00
subgraph " "
lpjmlkit::LPJmLMetaData-."inherit".-LandMetaData:::new
LandMetaData-."compose \n (1:1)".-LandData:::new
lpjmlkit::LPJmLData-."inherit".-LandData:::new
grid_file[grid file]-.-> lpjmlkit::LPJmLGridData
lpjmlkit::LPJmLGridData-."compose \n (1:1) \n optional".-LandData
cow_file[COW file]-.-> lpjmlkit::LPJmLRegionsData:::new
lpjmlkit::LPJmLRegionsData:::new-."compose \n (1:1) \n optional".-LandData:::new
LandData---Arithmetics("Arithmetics (+, -, *, /)"):::new
LandData---aggregate("aggregate()"):::new
LandData---disaggregate("disaggregate()"):::new
LandData---create_table("create_table()"):::new
LandData-."compose \n (n:1)".-LandDataSet:::new
NEW{{TODO}}:::new
end
```
#### Generic `read_data()` function
```mermaid
%%{ init: { 'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryBorderColor': '#000000'} } }%%
flowchart
classDef new fill:#FFFF00
subgraph " "
%% data type handling
read_data:::new-.->type{type}
type{type}-. "output (clm/meta/cdf)" .->read_io("lpjmlkit::read_io()")
type{type}-. "tabular (csv)" .->read_table("read_table()"):::new
cow_file[lpjmlkit::LPJmLRegionsData]:::new-.-> read_table
grid_file[lpjmlkit::LPJmLGridData]-.-> read_table
read_io-.->LandData$new("LandData$new()"):::new-.->LandData:::new
read_table:::new -.-> LandData:::new
NEW{{TODO}}:::new
EXISTING{{EXISTING}}
end
```
#### `benchmark()` initialization
The `benchmark()` function could look like the following:
```r
# Benchmarking function definition, returns an object of class LPJmLBenchmark, that includes required LandDataSets for every metric
benchmark <- function(x,
ref,
subset = NULL, # list(year = as.character(1990:2019)),
# define either output set flags ("default" benchmarking output set, "all" intersection of x and ref outputs, "landuse" with additional/only landuse outputs) or define vector of output ids to be benchmarked
output = "default" #, "all", "landuse", ..., c("vegc", "soilc", "litc", "runoff", ...)
metric = c("sum_table",
"sum_ts",
"diff_map",
"abs_map",
"site_ts",
"region_map",
"lat_dist") # ...
dataset = c("fao",
"flux",
"runoff"), # ...
...) {
...
}
```
The `benchmark()` initialization will initialize the benchmarking by creating a `LPJmLBenchmark` object, that includes the processed data and the methods to e.g. generate a benchmark report based on the data
1. `benchmark()` will use the information available in the configurations (json files) to generate meta information as attributes (`author`, `data`, `description`, etc.) of the returned `LPJmLBenchmark` object
2. `benchmark()` will intialize a `MetricSet`, a set of multiple (or a single) `Metric`s that are set via the metric argument, e.g. sum table or sum time series
* to save memory benchmark will iterate over the outputs, read each and process it directly be store it in the corresponding `Metric`'s `LandDataSet` (x, ref, ...) - this step may be parallelized.
* some objects may be hold in memory that are used to calculate intermediate products/sums, like `cftfrac` and `grid`/cellarea for `harvest`
3. If all metrics in `MetricSet` are filled with processed `LPJmLDataSet`s the LPJmLBenchmark data object is returned
Idealized structure of `benchmark(x, ref, subset, output, metric, dataset, ...)` as a flow chart:
```mermaid
%%{ init: { 'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryBorderColor': '#000000'} } }%%
flowchart TD
classDef new fill:#FFFF00
subgraph " "
%% output reading
test_outputs["to test \n outputs"]-->read_data2("read_data()"):::new
read_data2("read_data()"):::new-->LandData2["LandData"]:::new
LandData2:::new-."compose \n (n:1)".-LandDataSet:::new
ref_outputs["reference \n outputs"]-->read_data3("read_data()"):::new
read_data3("read_data()"):::new-->LandData3["LandData"]:::new
LandData3:::new-."compose \n (n:1)".-LandDataSet2["LandDataSet"]:::new
%% LandData:::new<-."to be harmonized \n for each metric".->LandData2["LandData"]:::new
fao_data[FAO data]-->read_data("read_data()"):::new
discharge_data[Discharge data]-->read_data("read_data()"):::new
flux_data[Flux data]-->read_data("read_data()"):::new-->LandData:::new
dots["..."]-->read_data("read_data()"):::new
Metric:::new-."inherit".-SumTable
LandDataSet:::new--"x"-->SumTable["SumTable(x,ref, ...)"]
LandDataSet2:::new--"ref"-->SumTable["SumTable(x,ref, ...)"]
LandData:::new--"..."-->SumTable["SumTable(x,ref, ...)"]
SumTable:::new--"compose \n (n:1)"-->MetricSet:::new
MetricSet:::new--"compose \n (1:1)"-->LPJmLBenchmark:::new
SumTable:::new-."OR".-SumTS:::new
SumTable:::new-."OR".-DiffMap:::new
SumTable:::new-."OR".-AbsMap:::new
SumTable:::new-."OR".-SiteTS:::new
SumTable:::new-."OR".-RegionMap:::new
SumTable:::new-."OR".-LatDist:::new
EXISTING{{EXISTING}}
NEW{{TODO}}:::new
end
```
Notes:
* `benchmark()` knows about ideal order of data processing to save additional processing (information could be stored in a yaml file), e.g.:
1. `SumTS` object first (space aggregation)
2. `SumTable` based on `SumTS$x`, `SumTS$ref` and `SumTS$...` (+ time aggregation)
* `Metric` (and child classes) initialization (`$new()`) uses harmonization function to ensure LPJmLData formats are equal/compatible between `x`, `ref` and `...`
* `Metric` (and child classes) initialization (`$new()`) also ensures that required intermediate products/sums of variables (e.g. "harvest" or "net ecosystem production") are calculated
* ...
#### Benchmark datasets storage
* Examples are:
* FAO data
* flux data
* dischage/runoff data
* lat distribution data (@sybills)
* face data (?)
* remote sensing data (?)
* ...
* the data would either have to be readable (within `read_data()`) via internal `read_table()` or lpjmlkit::`read_io()` function
* it will be tested to store these datasets as RData/RDS or CSV (or bin/clm/NetCDF) files in the the package
* using lazy loading this should not affect the package loading and memory: https://rstudio.github.io/r-manuals/r-exts/Creating-R-packages.html#data-in-packages
* otherwise easy, non error-prone alternatives have to be found (cluster, database, ...)!
* Also additional functionality should be provided to generate an overview over the already available data.
* for time series that would have to steadily be updated (e.g. FAO data) dataset deprecation warnings could be printed to request maintainers to update the data sets from time to time
#### Create benchmark report (and more)
An `LPJmLBenchmark` object is the main benchmarking interface that should provide multiple methods to enable different types of output generation
* `print()` method
* `report()` generation
* and more (e.g. [shiny dashboard](https://shiny.rstudio.com)):
```mermaid
%%{ init: { 'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryBorderColor': '#000000', 'lineColor': '#000000'} } }%%
flowchart TD
classDef new fill:#FFFF00
classDef future fill:#FF6800
subgraph " "
benchmark("benchmark(x, ref)"):::new --> LPJmLBenchmark["LPJmLBenchmark"]
LPJmLBenchmark["LPJmLBenchmark"]:::new --> print("print()"):::new
LPJmLBenchmark["LPJmLBenchmark"]:::new --> report("report(format = 'pdf')"):::new
LPJmLBenchmark["LPJmLBenchmark"]:::new --> dashboard("dashboard(...)"):::future
LPJmLBenchmark["LPJmLBenchmark"]:::new --> gitlab_runner("gitlab_runner(...)"):::future
LPJmLBenchmark["LPJmLBenchmark"]:::new --> other("..."):::future
NEW{{TODO}}:::new
FUTURE{{FUTURE \n WORK}}:::future
end
```
*Not all of functions are to be implemented now but might be at a later stage (FUTURE WORK)*
A typical benchmark workflow could look like this:
```r
# Usually only x and ref argument for configs are required
my_benchmark <- benchmark(x = "./config_x.json", ref = "./config_ref.json")
# Check auto generated information
my_benchmark$author
# jannesbr
my_benchmark$date
my_benchmark$description
# And adjust if required
my_benchmark$description <- "Compare LPJmL 4 with LPJmL 5"
# Save LPJmLBenchmark object to later reproduce report, use in gitlab runner or for a shiny dashboard
saveRDS(my_benchmark, "my_benchmark.rds")
# Generate pdf report
report(my_benchmark, "./my_benchmark_report.pdf")
```
> TEST COMMENT [color=#e2449e]
> [name=Jannes Breier]
> [time=Tue, Apr 4, 2023 1:18 PM]