#' Reads and modifies the SWAT+ projects' files that define the basic settings
#' of the simulation and set them according tu respective input parameters
#'
#' @param project_path Path to the SWAT project folder (i.e. TxtInOut)
#' @param start_date Start date of the SWAT simulation. Provided as character
#' string in any ymd format (e.g. 'yyyy-mm-dd') or in Date format project are
#' located
#' @param end_date End date of the SWAT simulation. Provided as character string
#' in any ymd format (e.g. 'yyyy-mm-dd') or in Date format project are located
#' @param years_skip Integer value that provides the numbe of years to be
#' skipped during writing the SWAT model outputs
#' @importFrom lubridate as_date ceiling_date int_end int_start interval
#' leap_year yday year years ymd
#' @importFrom dplyr case_when mutate select %>%
#' @importFrom purrr map_chr set_names
#' @importFrom readr read_lines read_table
#' @importFrom stringr str_sub
#' @keywords internal
#'
setup_swatplus <- function(project_path, parameter, output,
start_date, end_date, start_date_print,
years_skip, unit_cons) {
## Read unmodified file.cio, print.prt, and time.sim
options(readr.num_columns = 0)
model_setup <- list()
model_setup$file.cio <- read_lines(project_path%//%"file.cio", lazy = FALSE)
model_setup$print.prt <- read_lines(project_path%//%"print.prt", lazy = FALSE)
model_setup$time.sim <- read_lines(project_path%//%"time.sim", lazy = FALSE)
## Add calibration.cal to the file.cio
## This ensures that the calibration.cal that is added to indicate
## parameter changes are read by SWAT+
model_setup$file.cio[22] <-
'chg cal_parms.cal calibration.cal null null null null null null null'
print_table <- model_setup$print.prt[- c(1:10)] %>%
str_trim(.) %>%
str_split(., pattern = '[:space:]+') %>%
map_df(., ~tibble(objects = .x[1], day = .x[2],
mon = .x[3], yr = .x[3], aa = .x[4]))
## Define simulation period
if(xor(is.null(start_date), is.null(end_date))) {
stop("'start_date' and 'end_date' must be provided together!")
} else if (!is.null(start_date)) {
## Determine required date indices for writing to time.sim
start_date <- ymd(start_date)
end_date <- ymd(end_date)
time_interval <- interval(start_date, end_date)
start_year <- year(int_start(time_interval))
start_jdn <- yday(int_start(time_interval))
end_year <- year(int_end(time_interval))
end_jdn <- yday(int_end(time_interval))
model_setup$time.sim[3] <- c(start_jdn, start_year, end_jdn, end_year, 0) %>%
sprintf("%10d",.) %>%
paste(., collapse = "")
} else {
t_sim <- model_setup$time.sim[3] %>%
strsplit(., "\\s+") %>%
unlist(.) %>%
.[nchar(.) > 0] %>%
as.numeric(.)
start_year <- t_sim[2]
end_year <- t_sim[4]
start_day <- ifelse(t_sim[1] > 0, t_sim[1], 1)
is_leap <- leap_year(end_year)
end_day <- ifelse(t_sim[3] > 0, t_sim[3], ifelse(is_leap, 366, 365))
start_date <- as_date(x = start_year%//%start_day, format = "%Y/%j")
end_date <- as_date(x = end_year%//%end_day, format = "%Y/%j")
}
model_setup$start_date <- start_date
model_setup$end_date <- end_date
## Check the numbers of years to skip with the simulation interval Overwrite
## number of years to skip if value was provided otherwise get value from
## print file
## Other print options e.g. printing smaller time frame or other intervals
## than 1 are omitted, as any information loss should be avoided. Filtering
## of time windows can be done later in a modeling workflow
if(is.null(years_skip) & is.null(start_date_print)) {
print_time <- model_setup$print.prt[3] %>%
strsplit(., "\\s+") %>%
unlist(.) %>%
.[nchar(.) > 0] %>%
as.numeric(.)
years_skip <- print_time[1]
if (years_skip > 0) {
print_jdn <- 0
print_year <- 0
start_date_print <- NA_real_
} else {
print_jdn <- max(print_time[2], 1)
print_year <- max(print_time[3], year(start_date))
start_date_print <- as_date(x = print_year%//%print_jdn, format = "%Y/%j")
years_skip <- NA_integer_
}
} else if(!is.null(start_date_print)){
start_date_print <- ymd(start_date_print)
print_jdn <- yday(start_date_print)
print_year <- year(start_date_print)
years_skip <- NA_integer_
} else {
print_jdn <- 0
print_year <- 0
start_date_print <- NA_real_
}
if (!is.na(years_skip)) {
if (years_skip < 0) {
stop("'years_skip' must be a positive value.")
}
if (years_skip + year(start_date) > year(end_date)) {
stop("'start_date' + 'years_skip' is later than 'end_date'.")
}
}
if(!is.na(start_date_print)) {
if(start_date_print < start_date) {
stop("'start_date_print' is an earlier date than 'start_date'.")
}
if(start_date_print > end_date) {
stop("'start_date_print' is a later date than 'end_date'.")
}
}
model_setup$print.prt[3] <-
c(max(years_skip, 0, na.rm = TRUE), print_jdn, print_year, 0, 0, 1) %>%
sprintf("%-11d", .) %>%
paste(., collapse = '')
model_setup$years_skip <- years_skip
model_setup$start_date_print <- start_date_print
## Output interval settings
## Set output_interval to 'daily' as default if not provided by user.
# if(is.null(output_interval)) output_interval <- "d"
#
# if(output_interval == 'm') {
# t_int <- seq(start_date_print, end_date, by = 'm')
# if(length(t_int) == 1 & end_date != ceiling_date(end_date, unit = 'm')-1) {
# stop('Monthly simulation outputs require a simulation period of ',
# 'at least one full month!')
# }
# }
print_table[,2:5] <- "n"
# Remove crop, mgt, and FDC outputs from objects as they are not defined there
objects_in_tbl <- filter(output, ! file %in% c('basin_crop_yld', 'fdcout', 'mgtout'))
# Change from channel_sdmorph to channel_sd as the first is written when the
# second is defined
objects_in_tbl[objects_in_tbl == 'channel_sdmorph'] <- 'channel_sd'
if(nrow(objects_in_tbl) > 0) {
for(i in 1:nrow(objects_in_tbl)) {
print_table[print_table$objects == objects_in_tbl$file[i],
objects_in_tbl$time_interval[i]] <- 'y'
}
}
# Set all outputs to no, except the output files defined in output and only
# for the defined time interval
# print_table[print_table$objects %in% object_names, output_interval] <- "y"
print_table <- print_table %>%
mutate(objects = sprintf("%-16s", objects),
day = sprintf("%14s", day),
mon = sprintf("%14s", mon),
yr = sprintf("%14s", yr),
aa = sprintf("%14s", aa)) %>%
apply(., 1, paste, collapse = "")
model_setup$print.prt <- c(model_setup$print.prt[1:10], print_table)
# So far avoid any other output files types to be written
model_setup$print.prt[7] <- "n n n "
# Print also FDC and mgt output files if defined in outputs
prt_9 <- rep('n', 4)
if ('mgtout' %in% output$file) prt_9[2] <- 'y'
if ('fdcout' %in% output$file) prt_9[4] <- 'y'
prt_9 <- paste(sprintf('%-14s',prt_9), collapse = '')
model_setup$print.prt[9] <- prt_9
if(!is.null(parameter)) {
parameter$definition <- filter(parameter$definition, file_name != 'pdb')
model_setup$calibration.cal <- map(1:nrow(parameter$definition),
~ parameter$definition[.x,]) %>%
map(., ~ setup_calibration_cal(.x, unit_cons)) %>%
bind_rows(.)
}
return(model_setup)
}
#' Setup the calibration.cal file and include all parameter conditions
#' of the simulation and set them according to respective input parameters
#'
#' @param par_def Tibble with one line that includes the parameter definition of
#' parameter i
#' @param unit_cons List of tibbles that contains the meta information of hru, aqu,
#' etc. units and constrain variables (e.g. texture, plant)
#'
#' @importFrom dplyr bind_rows select %>%
#' @importFrom purrr map map2_df
#' @importFrom tibble tibble
#' @importFrom tidyselect any_of
#'
#' @keywords internal
#'
setup_calibration_cal <- function(par_def, unit_cons) {
par_cal <- init_cal(par_def)
if('lyr' %in% names(par_def)) {
if(!is.na(par_def$lyr)) {
par_cal <- add_value_range(par_cal, par_def$lyr, 'LYR')
}
}
if('year' %in% names(par_def)) {
if(!is.na(par_def$year)) {
par_cal <- add_value_range(par_cal, par_def$year, 'YEAR')
}
}
if('day' %in% names(par_def)) {
if(!is.na(par_def$day)) {
par_cal <- add_value_range(par_cal, par_def$day, 'DAY')
}
}
if('unit' %in% names(par_def)) {
if(!is.na(par_def$unit)) {
par_cal <- add_obj(par_cal, par_def$unit,
unit_cons$units[[par_def$file_name]])
}
}
if(any(c('hsg', 'texture', 'plant', 'landuse') %in% names(par_def))) {
soil_luse <- select(par_def, any_of(c('hsg', 'texture', 'plant', 'landuse'))) %>%
select(., !tidyselect:::where(is.na)) %>% # will be replaced when where is in the tidyselect namespace
map(., ~ .x)
cond_tbl <- map2_df(soil_luse, names(soil_luse), ~ add_soil_luse(.x, .y, unit_cons$conds))
} else {
cond_tbl <- tibble()
}
if(any('slope' %in% names(par_def))) {
if(!is.na(par_def$slope)) {
cond_tbl <- add_slope(par_def$slope, cond_tbl)
}
}
if(any(c('hsg', 'texture', 'plant', 'landuse', 'slope') %in% names(par_def))) {
par_cal$CONDS <- as.character(nrow(cond_tbl))
par_cal <- bind_rows(par_cal, cond_tbl)
}
return(par_cal)
}
#' Initialize the calibration.cal table
#'
#' @param par_def Tibble with one line that includes the parameter definition of
#' parameter i
#' @importFrom dplyr mutate select %>%
#' @importFrom purrr set_names
#' @keywords internal
#'
init_cal <- function(par_def) {
par_def %>%
select(., parameter, change) %>%
set_names(c("NAME", "CHG_TYPE")) %>%
mutate(VAL = NA_real_, CONDS = '0', LYR1 = 0, LYR2 = 0, YEAR1 = 0, YEAR2 = 0,
DAY1 = 0, DAY2 = 0, OBJ_TOT = 0)
}
#' Add the value range 'val' for the condition variable 'var' for a parameter to
#' calibration.cal
#'
#' @param par_cal The calibration.cal tibble for the parameter i
#' @param val Vector that defines the value range
#' @param var Character string. Name of the variable
#'
#' @keywords internal
#'
add_value_range <- function(par_cal, val, var) {
val_range <- get_value_range(val)
par_cal[var%&%'1'] <- val_range[1]
par_cal[var%&%'2'] <- val_range[2]
return(par_cal)
}
#' Get the value range from a condition in the par_def table
#'
#' @param condition Character string that defines a condition
#'
#' @importFrom dplyr %>%
#' @importFrom stringr str_detect str_remove
#' @importFrom purrr set_names
#' @keywords internal
#'
get_value_range <- function(condition) {
if(str_detect(condition, '%in%')) {
condition <- condition %>%
str_remove(., '%in%') %>%
parse(text = .) %>%
eval(.)
} else if (str_detect(condition, '==')) {
condition <- condition %>%
str_remove(., '==') %>%
as.numeric(.)
} else {
stop("For parameter conditioning with 'lyr', 'year', and 'day' only single",
"values or upper lower bound implemented yet!")
}
return(c(min(condition), max(condition)))
}
#' Add the unit values for which objects the parameter change should be applied
#'
#' @param par_cal The calibration.cal tibble for the parameter i
#' @param unit Character string that defines condition for the object units (ids of e.g. hru objects)
#' @param unit_all List of vectors that define the unit ids of all object types.
#'
#' @importFrom dplyr bind_cols bind_rows %>%
#' @importFrom purrr set_names
#'
#' @keywords internal
#'
add_obj <- function(par_cal, unit, unit_all) {
eval_unit <- paste('unit_all', unit) %>%
parse(text = .) %>%
eval(.)
unit <- unit_all[eval_unit] %>%
unique(.) %>%
sort(.) %>%
identify_sequence(.) %>%
set_names(., 'OBJ'%_%1:length(.)) %>%
bind_rows()
par_cal$OBJ_TOT <- length(unit)
par_cal <- bind_cols(par_cal, unit)
return(par_cal)
}
#' Group sequences of units together for writing the OBJ columns in calibration.cal
#'
#' @param val Numeric vector with the unit values
#'
#' @importFrom dplyr %>%
#' @importFrom purrr map map2 reduce
#'
#' @keywords internal
#'
identify_sequence <- function(val) {
split_end <- which(diff(val) > 1)
split_start <- c(1, split_end + 1)
split_end <- c(split_end, length(val))
map2(split_start, split_end, ~ val[.x:.y]) %>%
map(., ~ translate_sequence(.x)) %>%
reduce(., c)
}
#' Translate the unit sequences to calibration.cal syntax
#'
#' @param val_seq Numeric vector with the unit values
#'
#' @keywords internal
#'
translate_sequence <- function(val_seq) {
if (length(val_seq) > 1) {
val_seq <- c(min(val_seq), -max(val_seq))
}
return(val_seq)
}
#' Add condition lines based on soil and land use variables
#'
#' @param cond Character string that defines condition for the variable 'var'
#' @param var Character string. Variable for which condition is applied
#' @param cond_all List of vectors that define all possible values for the
#' condition variables.
#'
#' @importFrom dplyr %>%
#' @importFrom tibble tibble
#'
#' @keywords internal
#'
add_soil_luse <- function(cond, var, cond_all) {
eval_cond <- paste('cond_all[[var]]', cond) %>%
parse(text = .) %>%
eval(.)
cond <- cond_all[[var]][eval_cond]
tbl <- tibble(NAME = var,
CHG_TYPE = '=',
VAL = 0,
CONDS = cond)
return(tbl)
}
#' Add condition lines based on slope
#'
#' @param cond Character string that defines condition for the variable 'var'
#' @param cond_tbl Tibble with conditions defined based on soil and land use
#'
#' @importFrom dplyr bind_rows %>%
#' @importFrom stringr str_extract str_remove str_sub str_trim
#' @importFrom tibble tibble
#'
#' @keywords internal
#'
add_slope <- function(cond, cond_tbl) {
cond_op <- str_extract(cond, '<\\=|\\>\\=|\\=\\=|\\=|\\<|\\>')
cond_val <- str_remove(cond, cond_op) %>% str_trim(.) %>% as.numeric()
if(is.na(cond_val)) {
stop("Parameter condition 'slope ", cond, "' is not allowed.")
}
slp_tbl <- tibble(NAME = 'slope',
CHG_TYPE = str_sub(cond_op, 1, 1),
VAL = cond_val)
cond_tbl <- bind_rows(cond_tbl, slp_tbl)
return(cond_tbl)
}
#' Write the updated init files to all parallel folders
#'
#' @param run_path Path to the .model_run folder
#' @param model_setup List of files that define the SWAT+ model setup
#'
#' @keywords internal
#'
write_swatplus_setup <- function(run_path, model_setup) {
thread_i <- dir(run_path) %>%
.[. %in% ("thread"%_%1:999)]
## Write modified file_cio into thread folder and respective Backup folder
for(i in thread_i) {
writeLines(model_setup$file.cio, run_path%//%i%//%"file.cio")
writeLines(model_setup$time.sim, run_path%//%i%//%"time.sim")
writeLines(model_setup$print.prt, run_path%//%i%//%"print.prt")
}
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.