Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
03e1569
Add SoyFACE biomass data
eloch216 May 2, 2025
09aacfe
Merge pull request #1 from biocro/add-data
eloch216 May 2, 2025
344e1cf
Start writing `objective_function`
eloch216 May 2, 2025
3433ccc
Enable dependent parameter values
eloch216 May 2, 2025
952fa26
Enable post-processing simulation outputs and data definitions
eloch216 May 2, 2025
f1f626f
Find time indices
eloch216 May 3, 2025
8b9d731
Return the total error function
eloch216 May 3, 2025
9d4a04f
More tests
eloch216 May 3, 2025
cad412b
Add some default values
eloch216 May 3, 2025
6e59133
Rename a few things
eloch216 May 3, 2025
df77e58
Be more efficient with normalization
eloch216 May 3, 2025
edfb965
Be more efficient with initial model runner evaluations
eloch216 May 3, 2025
40cfead
Add regularization
eloch216 May 3, 2025
a014837
Reorganize file structure
eloch216 May 3, 2025
5e04df6
Start documenting `objective_function`
eloch216 May 6, 2025
3ca762c
Consolidate two input arguments
eloch216 May 6, 2025
86c766f
More documentation
eloch216 May 6, 2025
a0df150
More documentation and normalization methods
eloch216 May 6, 2025
741b115
Update objective_function.Rd
eloch216 May 6, 2025
a828a54
Don't allow drivers to be varied
eloch216 May 6, 2025
1d010a4
Add ability to return error metric terms
eloch216 May 6, 2025
a5d4ac9
Add verbose option
eloch216 May 6, 2025
2bd7388
Mention lambda in documentation
eloch216 May 6, 2025
c44ac52
Update NEWS.md
eloch216 May 6, 2025
cdfa724
Fix pkgdown math formatting
eloch216 May 6, 2025
66ac7d8
Merge pull request #2 from biocro/objective-function
eloch216 May 7, 2025
95b340d
Document the failure return value
eloch216 May 8, 2025
d6902a4
Add more verbose output
eloch216 May 8, 2025
578c1d4
Enable year-specific weights
eloch216 May 8, 2025
63a8277
Normalize by number of data-driver pairs
eloch216 May 9, 2025
af4e40c
Enable stdev as an optional part of the data-driver pairs
eloch216 May 9, 2025
de53a83
Add variance-based weights
eloch216 May 9, 2025
65b51da
Check data values and weights
eloch216 May 9, 2025
1a0e1d1
Change verbose defaults
eloch216 May 9, 2025
6fbc6ed
Document several commits
eloch216 May 9, 2025
64402cb
Allow and test R v3.6.0
eloch216 May 9, 2025
c54ef40
Check for multiple "closest" times
eloch216 May 9, 2025
e5aa56e
Include more info in time range error message
eloch216 May 9, 2025
43df536
Require newer BioCro
eloch216 May 9, 2025
4b3462e
Merge pull request #3 from biocro/more-objective-function
eloch216 May 9, 2025
b6a5889
Include full biomass data sets
eloch216 May 12, 2025
1ae0e1c
Start adding vignette
eloch216 May 13, 2025
4671308
Add more verbose printing
eloch216 May 20, 2025
3dd8882
Add more detailed terms to output
eloch216 May 21, 2025
feeb07c
Finish calculating objective function in vignette
eloch216 May 21, 2025
efce402
Add more error checking
eloch216 May 22, 2025
d50a431
Add debug mode option
eloch216 May 22, 2025
8e6374a
Add optimization example to vignette
eloch216 May 22, 2025
a64c7cd
Add `update_model` and finish vignette example
eloch216 May 22, 2025
388dc5b
Add `bounds_table` and use it in the vignette
eloch216 May 22, 2025
ffd4f85
Simplify vignette a bit
eloch216 May 22, 2025
fbeedeb
A little more error checking
eloch216 May 23, 2025
d89fe45
Fix silly mistake in DESCRIPTION
eloch216 May 23, 2025
3d8d11f
Merge pull request #4 from biocro/add-vignette
eloch216 May 23, 2025
382678f
Add more info about `bounds_table`
eloch216 May 23, 2025
43a0e74
Allow flexible `epsilon`
eloch216 May 23, 2025
4b3e349
Add "Getting Started" vignette
eloch216 May 23, 2025
6438a4a
Merge pull request #5 from biocro/misc-updates
eloch216 May 23, 2025
333d901
Update version information
eloch216 May 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# reason, there should not be any blank comment lines, because they would
# exclude any files with `#` in the filename, possibly causing confusing issues.

^Rhistory$
# History files

^.*\.Rproj$
# Designates the directory as an RStudio Project.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}
- {os: ubuntu-latest, r: '4.0.0'}
- {os: ubuntu-latest, r: '3.6.0'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
Expand Down
23 changes: 15 additions & 8 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
Package: BioCroValidation
Version: 0.1.0
Version: 0.2.0
Title: Tools for Validating BioCro Models
Description: A collection of tools for validating BioCro crop growth models.
Authors@R: c(
person("Edward B.", "Lochocki", role = c('cre', 'aut'),
email = "eloch@illinois.edu",
comment = c(ORCID = "0000-0002-4912-9783")),
person("BioCroField authors", role = "cph")
person("BioCroValidation authors", role = "cph")
)
Depends:
R (>= 3.6.0)
Imports:
BioCro (>= 3.2.0)
Suggests:
dfoptim,
DEoptim,
lattice,
knitr,
rmarkdown,
testthat (>= 3.0.0)
VignetteBuilder: knitr
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
Depends:
R (>= 4.0.0)
Suggests:
testthat (>= 3.0.0),
BioCro (>= 3.0.0)
URL: https://github.com/BioCro/BioCroField, https://biocro.github.io/BioCroValidation/
URL: https://github.com/BioCro/BioCroValidation, https://biocro.github.io/BioCroValidation/
Config/testthat/edition: 3
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
YEAR: 2025
COPYRIGHT HOLDER: BioCroField authors
COPYRIGHT HOLDER: BioCroValidation authors
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MIT License

Copyright (c) 2025 BioCroField authors
Copyright (c) 2025 BioCroValidation authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export(bounds_table)
export(objective_function)
export(update_model)
export(write_model)
12 changes: 12 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,22 @@ In the case of a hotfix, a short section headed by the new release number should
be directly added to this file to describe the related changes.
-->

# Changes in BioCroValidation Version 0.2.0 (2025-05-23)

- Added 2002 and 2005 SoyFACE biomass and standard deviation data.

- Added several new functions: `objective_function`, `update_model`, and
`bounds_table`.

- Added two new vignettes: a "Getting Started" article (`BioCroValidation.Rmd`)
and a user guide illustrating how to perform a model parameterization
(`parameterizing_soybean_biocro.Rmd`).

# Changes in BioCroValidation Version 0.1.0

- This is the first version of BioCroValidation. At this point, the package is
in a state of rapid development, and not all changes will be described here.

- We are reserving version `1.0.0` for a more stable and complete future
release; until then, major changes should only increase the minor version
number.
154 changes: 154 additions & 0 deletions R/bounds_table.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# A helper function for checking the bounds list for mistakes; if an issue is
# found, this function will throw an error; otherwise it will be silent with no
# return value.
check_bounds_list <- function(bounds_list, independent_args) {
# Must be a list of named elements
if (!is.list(bounds_list) | is.null(names(bounds_list))) {
stop('`bounds_list` must be a list of named elements')
}

# Must contain all elements in independent_args
missing_element <- sapply(names(independent_args), function(x) {
!x %in% names(bounds_list)
})

if (any(missing_element)) {
msg <- paste0(
'The following elements were included in ',
'`independent_args` but not `bounds_list`: ',
paste(names(independent_args)[missing_element], collapse = ', ')
)
stop(msg)
}

# Each element must have length 2
length_two <- sapply(bounds_list, function(x) {
xlen <- length(x)

if (is.finite(xlen)) {
length(x) == 2
} else {
FALSE
}
})

if (any(!length_two)) {
msg <- paste0(
'The following elements of `bounds_list` do not have a length of 2: ',
paste(names(bounds_list)[!length_two], collapse = ', ')
)
stop(msg)
}

# Each element must be numeric
not_numeric <- sapply(bounds_list, function(x) {!is.numeric(x)})

if (any(not_numeric)) {
msg <- paste0(
'The following elements of `bounds_list` are not numeric: ',
paste(names(bounds_list)[not_numeric], collapse = ', ')
)
stop(msg)
}

return(invisible(NULL))
}

# A helper function for checking the initial guess for mistakes; if an issue is
# found, this function will throw an error or a warning; otherwise it will be
# silent with no return value.
check_initial_ind_arg_values <- function(
independent_args,
lbounds,
ubounds,
initial_ind_arg_values
)
{
# Check the length
if (length(initial_ind_arg_values) != length(independent_args)) {
stop('`initial_ind_arg_values` must have the same length as `independent_args`')
}

# Check to make sure the initial values are not outside the bounds
outside_bounds <- sapply(seq_along(initial_ind_arg_values), function(i) {
initial_ind_arg_values[i] < lbounds[i] | initial_ind_arg_values[i] > ubounds[i]
})

if (any(outside_bounds)) {
msg <- paste0(
'The initial values for the following arguments lie outside the bounds: ',
paste(names(independent_args)[outside_bounds], collapse = ', ')
)
stop(msg)
}

# Check to see if any initial values are on the bounds
eps <- sqrt(.Machine$double.eps)

on_bounds <- sapply(seq_along(initial_ind_arg_values), function(i) {
abs(initial_ind_arg_values[i] - lbounds[i]) < eps |
abs(initial_ind_arg_values[i] - ubounds[i]) < eps
})

if (any(on_bounds)) {
msg <- paste0(
'The initial values for the following arguments lie on the ',
'bounds, which can be problematic for some optimizers: ',
paste(names(independent_args)[on_bounds], collapse = ', ')
)
warning(msg)
}

return(invisible(NULL))
}

bounds_table <- function(
independent_args,
bounds_list,
initial_ind_arg_values = NULL
)
{
# Check the bounds_list
check_bounds_list(bounds_list, independent_args)

# Get an ordering for the elements of `bounds_list` so they match the order
# of elements in `independent_args`; note that this will also exclude any
# elements of `bounds_list` that are not included in `independent_args`.
ordering <- match(
names(independent_args),
names(bounds_list)
)

bounds_list <- bounds_list[ordering]

# Get the lower and upper bounds
lbounds <- sapply(bounds_list, min)
ubounds <- sapply(bounds_list, max)

# Form the bounds table
bounds_table <- data.frame(
arg_name = names(independent_args),
lower = lbounds,
upper = ubounds,
stringsAsFactors = FALSE
)

# Include initial values in the table if they were provided
if (!is.null(initial_ind_arg_values)) {
# Check the values
check_initial_ind_arg_values(
independent_args,
lbounds,
ubounds,
initial_ind_arg_values
)

# Include the values
bounds_table$initial_value <- initial_ind_arg_values
}

# Remove row names and return the table
rownames(bounds_table) <- NULL

bounds_table
}
134 changes: 134 additions & 0 deletions R/objective_function.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
## Here we use several functions that are defined in
## `objective_function_input_checks.R` and `objective_function_helpers.R`

objective_function <- function(
base_model_definition,
data_driver_pairs,
independent_args,
quantity_weights,
data_definitions = list(),
normalization_method = 'mean_max',
normalization_param = NULL,
stdev_weight_method = 'equal',
stdev_weight_param = NULL,
regularization_method = 'none',
dependent_arg_function = NULL,
post_process_function = NULL,
extra_penalty_function = NULL,
verbose_startup = TRUE
)
{
# Check the data-driver pairs
check_data_driver_pairs(base_model_definition, data_driver_pairs)

# Check the arguments to be varied
check_args_to_vary(
independent_args,
dependent_arg_function,
data_driver_pairs,
verbose_startup
)

# Get the model runners
model_runners <- lapply(data_driver_pairs, function(ddp) {
get_model_runner(
base_model_definition,
independent_args,
dependent_arg_function,
post_process_function,
ddp
)
})

# Get the full data definition list
full_data_definitions <-
get_data_definition_list(data_driver_pairs, data_definitions)

# Print the full data definition list, if desired
if (verbose_startup) {
cat('\nThe full data definitions:\n\n')
utils::str(full_data_definitions)
}

# Check the model runners
check_runners(model_runners)

# Get initial model runner results
initial_runner_res <-
get_initial_runner_res(model_runners, independent_args)

# Check the initial model runner results
check_runner_results(
initial_runner_res,
full_data_definitions,
data_driver_pairs
)

# Get the long-form data
long_form_data <-
get_long_form_data(data_driver_pairs, full_data_definitions)

# Find indices corresponding to the measured time points
long_form_data <- add_time_indices(initial_runner_res, long_form_data)

# Add normalization factors
long_form_data <- add_norm(
long_form_data,
normalization_method,
normalization_param,
length(data_driver_pairs)
)

# Add variance-based weights
long_form_data <- add_w_var(
long_form_data,
stdev_weight_method,
stdev_weight_param
)

# Print the long form data, if desired. Do this before checking the data,
# so the printout will be available for troubleshooting
if (verbose_startup) {
cat('\nThe user-supplied data in long form:\n\n')
print(long_form_data)
}

# Check the processed long-form data
check_long_form_data(long_form_data)

# Process the quantity weights
full_quantity_weights <-
process_quantity_weights(quantity_weights, long_form_data)

# Print the quantity weights, if desired
if (verbose_startup) {
cat('The user-supplied quantity weights:\n\n')
utils::str(full_quantity_weights)
}

# Get the data-driver pair weights
ddp_weights <- get_ddp_weights(data_driver_pairs)

# Print the data-driver pair weights, if desired
if (verbose_startup) {
cat('\nThe user-supplied data-driver pair weights:\n\n')
utils::str(ddp_weights)
}

# Create the objective function
obj_fun <- get_obj_fun(
model_runners,
long_form_data,
full_quantity_weights,
ddp_weights,
normalization_method,
extra_penalty_function,
regularization_method
)

# Check the objective function
check_obj_fun(obj_fun, independent_args, verbose_startup)

# Return it
obj_fun
}
Loading