From d8624f47cb04d3cd82c88271e5879fc0612a1de2 Mon Sep 17 00:00:00 2001 From: hsonne Date: Mon, 20 Oct 2025 18:07:10 +0200 Subject: [PATCH 1/6] Add intensive green roofs --- R/run_rabimo.R | 63 ++++++++++++++----- R/stop_on_invalid_config.R | 9 ++- inst/extdata/column-names.csv | 1 + .../test-function-handle_missing_columns.R | 20 +++++- tests/testthat/test-function-run_rabimo.R | 13 +++- 5 files changed, 83 insertions(+), 23 deletions(-) diff --git a/R/run_rabimo.R b/R/run_rabimo.R index fca94ac6..695fc6f6 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -47,6 +47,7 @@ run_rabimo <- function( data <- kwb.rabimo::rabimo_inputs_2025$data config <- kwb.rabimo::rabimo_inputs_2025$config controls <- define_controls() + silent <- FALSE `%>%` <- magrittr::`%>%` } @@ -56,8 +57,11 @@ run_rabimo <- function( geometry <- attr(data, "geometry") # If road-area-specific columns are missing, create them - data <- handle_missing_columns(data) + data <- handle_missing_columns(data, silent = silent) + # If model parameters are missing, create them + config <- handle_missing_config_entries(config) + # Provide function to access the list of controls control <- create_accessor(controls) @@ -140,7 +144,7 @@ run_rabimo <- function( # total runoff of roof areas # (total runoff, contains both surface runoff and infiltration components) runoff_roof <- select_columns(runoff_all, "roof") - runoff_green_roof <- select_columns(runoff_all, "green_roof") + runoff_green_roof <- select_columns(runoff_all, c("green_roof", "green_roof_int")) # Provide runoff coefficients for impervious surfaces runoff_factors <- fetch_config("runoff_factors") @@ -154,20 +158,24 @@ run_rabimo <- function( # actual runoff from green roof surface (area based, with no infiltration) runoff_green_roof_actual <- with( data, - main_frac * roof * green_roof * swg_roof - ) * runoff_factors[["roof"]] * runoff_green_roof - + main_frac * roof * swg_roof * green_roof * cbind(1, green_roof_int) + ) * + runoff_factors[["roof"]] * + runoff_green_roof + # actual infiltration from roof surface (area based, with no runoff) infiltration_roof_actual <- with( - data, main_frac * roof * (1-green_roof) * (1-swg_roof) + data, + main_frac * roof * (1-green_roof) * (1-swg_roof) ) * runoff_roof # actual infiltration from green_roof surface (area based, with no runoff) infiltration_green_roof_actual <- with( data, - main_frac * roof * green_roof * (1-swg_roof) - ) * runoff_green_roof - + main_frac * roof * (1-swg_roof) * green_roof * cbind(1, green_roof_int) + ) * + runoff_green_roof + # Calculate runoff for all surface classes at once # (contains both surface runoff and infiltration components) @@ -227,14 +235,15 @@ run_rabimo <- function( # Calculate runoff 'ROW' for entire block area (FLGES + STR_FLGES) (mm/a) total_surface_runoff <- ( - runoff_roof_actual + runoff_green_roof_actual + + runoff_roof_actual + + rowSums(runoff_green_roof_actual) + #orig.: runoff_unsealed_roads <- was set to zero in the master branch rowSums(runoff_sealed_actual)) # Calculate infiltration rate 'RI' for entire block partial area (mm/a) total_infiltration <- (infiltration_roof_actual + - infiltration_green_roof_actual + + rowSums(infiltration_green_roof_actual) + infiltration_unsealed_surfaces + infiltration_unsealed_roads + rowSums(infiltration_sealed_actual)) @@ -356,8 +365,16 @@ run_rabimo <- function( } # handle_missing_columns ------------------------------------------------------- -handle_missing_columns <- function(data) +handle_missing_columns <- function(data, silent = FALSE) { + init_column <- function(data, column, default) { + if (!silent) { + message(sprintf("Initialising new column '%s' with %0.1f", column, default)) + } + data[[column]] <- default + data + } + road_specific_columns <- c( "road_frac", "pvd_r", "swg_pvd_r", "srf1_pvd_r", "srf2_pvd_r", "srf3_pvd_r", "srf4_pvd_r" @@ -367,17 +384,33 @@ handle_missing_columns <- function(data) if (length(missing_road_columns)) { for (column in missing_road_columns) { - data[[column]] <- 0 + data <- init_column(data, column, 0) } } - if (! "main_frac" %in% names(data)) { - data$main_frac <- 1 + column <- "main_frac" + if (! column %in% names(data)) { + data <- init_column(data, column, 1) } + column <- "green_roof_int" + if (! column %in% names(data)) { + data <- init_column(data, column, 0) + } + data } +# handle_missing_config_entries ------------------------------------------------ +handle_missing_config_entries <- function(config) +{ + if (!"green_roof_int" %in% names(config$bagrov_values)) { + config$bagrov_values["green_roof_int"] <- config$bagrov_values["green_roof"] + } + + config +} + # get_climate: provides climate relevant input data ---------------------------- get_climate <- function(input) { diff --git a/R/stop_on_invalid_config.R b/R/stop_on_invalid_config.R index 533b37bf..5d1d77ba 100644 --- a/R/stop_on_invalid_config.R +++ b/R/stop_on_invalid_config.R @@ -15,10 +15,9 @@ stop_on_invalid_config <- function(config) bagrov_values <- select_elements(config, "bagrov_values") runoff_factors <- select_elements(config, "runoff_factors") - x <- config$bagrov_values - check_values_for_surface_types(x) - - x <- config$runoff_factors - check_values_for_surface_types(x) + check_values_for_surface_types(x = bagrov_values) + stopifnot("green_roof" %in% names(bagrov_values)) + + check_values_for_surface_types(x = runoff_factors) } diff --git a/inst/extdata/column-names.csv b/inst/extdata/column-names.csv index 6c86f4cb..46c628de 100644 --- a/inst/extdata/column-names.csv +++ b/inst/extdata/column-names.csv @@ -9,6 +9,7 @@ total_area,,,Total block area,m2,required,numeric,100 main_frac,,,Non-road fraction of total_area,0..1,,numeric,1.0 roof,PROBAU,x,Roof fraction of non-road built area,0..1,required,numeric,0.2 green_roof,,,Green roof fraction of roof area,0..1,required,numeric,0.0 +green_roof_int,,,Fraction of green roof area that refers to intenive green roofs,0..1,,numeric,0.0 swg_roof,KAN_BEB,x,Fraction of roof area connected to the sewer ,0..1,required,numeric,1.0 pvd,PROVGU,x, Paved fraction of non-road area,0..1,required,numeric,0.6 swg_pvd,KAN_VGU,x,Fraction of paved area connected to the sewer,0..1,required,numeric,0.7 diff --git a/tests/testthat/test-function-handle_missing_columns.R b/tests/testthat/test-function-handle_missing_columns.R index 608a920a..a37e0b5f 100644 --- a/tests/testthat/test-function-handle_missing_columns.R +++ b/tests/testthat/test-function-handle_missing_columns.R @@ -1,5 +1,21 @@ # library(testthat) test_that("handle_missing_columns() works", { - f <- kwb.rabimo:::handle_missing_columns - expect_error(f()) + + handle_missing <- kwb.rabimo:::handle_missing_columns + + expect_error(handle_missing()) + + area_with_missing <- kwb.utils::removeColumns( + kwb.rabimo::generate_rabimo_area("code", green_roof_int = 0.1), + columnsToRemove = c("main_frac", "green_roof_int") + ) + + expect_message(expect_message(result_1 <- handle_missing(area_with_missing))) + expect_silent(result_2 <- handle_missing(area_with_missing, silent = TRUE)) + + expect_identical(result_1$green_roof_int, 0) + expect_identical(result_2$green_roof_int, 0) + + expect_identical(result_1$main_frac, 1) + expect_identical(result_2$main_frac, 1) }) diff --git a/tests/testthat/test-function-run_rabimo.R b/tests/testthat/test-function-run_rabimo.R index 2fa190e5..5ed671d5 100644 --- a/tests/testthat/test-function-run_rabimo.R +++ b/tests/testthat/test-function-run_rabimo.R @@ -107,9 +107,20 @@ test_that("Full connection to swales results in zero runoff", { generate("all_swale", to_swale = 1), generate("all_swale_plus_green_roof", green_roof = 1, to_swale = 1), generate("all_swale_plus_green_roof", pvd = 0, to_swale = 1), - kwb.rabimo::generate_rabimo_area("all_swale_plus_both", pvd = 0, green_roof = 1, to_swale = 1) + generate("all_swale_plus_both", pvd = 0, green_roof = 1, to_swale = 1) ) config <- kwb.rabimo::rabimo_inputs_2025$config result <- kwb.rabimo::run_rabimo(data, config, silent = TRUE) expect_true(all(result$runoff[startsWith(result$code, "all_swale")] == 0)) }) + +test_that("Abimo can simulate intensive green roofs", { + # generate <- kwb.rabimo::generate_rabimo_area + # data <- rbind( + # generate("area_0"), + # generate("area_1") + # ) + # config <- kwb.rabimo::rabimo_inputs_2025$config + # result <- kwb.rabimo::run_rabimo(data, config, silent = TRUE) + # expect_true(all(result$runoff[startsWith(result$code, "all_swale")] == 0)) +}) From d0c884fe10c0b60d10e573b35e33dec95ed5640c Mon Sep 17 00:00:00 2001 From: hsonne Date: Tue, 21 Oct 2025 18:11:53 +0200 Subject: [PATCH 2/6] Allow for different green roof and infiltration measures --- .Rbuildignore | 1 + R/reconfigure.R | 63 ++++ R/run_rabimo.R | 298 ++++++++++-------- R/stop_on_invalid_config.R | 9 +- inst/extdata/column-names.csv | 1 - .../test-function-handle_missing_columns.R | 9 +- tests/testthat/test-function-run_rabimo.R | 4 +- .../test-function-run_rabimo_with_measures.R | 2 +- 8 files changed, 252 insertions(+), 135 deletions(-) create mode 100644 R/reconfigure.R diff --git a/.Rbuildignore b/.Rbuildignore index b82695cf..e150a797 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -9,3 +9,4 @@ ^.*\.Rproj$ ^\.Rproj\.user$ ^data-raw$ +^\.idea$ diff --git a/R/reconfigure.R b/R/reconfigure.R new file mode 100644 index 00000000..01d0dffc --- /dev/null +++ b/R/reconfigure.R @@ -0,0 +1,63 @@ +if (FALSE) +{ + config_2020 <- kwb.rabimo::rabimo_inputs_2020$config + config_2025 <- kwb.rabimo::rabimo_inputs_2025$config + + str(config_2020) + str(kwb.rabimo:::reconfigure(config = config_2020)) + + str(config_2025) + str(kwb.rabimo:::reconfigure(config = config_2025)) + + config$green_roof <- list( + list(roof_fraction_column = "green_roof", bagrov_value = 0.5) + ) + + config$green_roof <- list( + list(roof_fraction_column = "green_roof_ext", bagrov_value = 0.5), + list(roof_fraction_column = "green_roof_int", bagrov_value = 0.5) + ) +} + +# reconfigure ------------------------------------------------------------------ +reconfigure <- function(config) +{ + #config <- config_2020 + + # Provide vector of Bagrov values + bagrov_values <- config$bagrov_values + + # Remove element "green_roof" from vector of Bagrov values + config$bagrov_values <- bagrov_values[names(bagrov_values) != "green_roof"] + + # Provide evaporation factor for infiltration method + evaporation_factor <- config$swale[["swale_evaporation_factor"]] + + # Remove config$swale + config$swale <- NULL + + config$measures <- list( + green_roof = list( + list( + roof_fraction_column = "green_roof", + bagrov_value = bagrov_values[["green_roof"]] + ) + ), + infiltration = list( + list( + area_fraction_column = "to_swale", + evaporation_factor = evaporation_factor + #, overflow_rate = 0 # not yet supported! + ) + # , list( + # name = "rigole", + # area_fraction_column = "to_inf_rigole", + # evaporation_factor = 0, + # overflow_rate = 0 + # #, rigole_specific_factor = 1 + # ) + ) + ) + + config +} diff --git a/R/run_rabimo.R b/R/run_rabimo.R index 695fc6f6..21e31c4b 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -36,7 +36,7 @@ #' #' plot(results_2025[, -1L]) run_rabimo <- function( - data, config, controls = define_controls(), silent = FALSE + data, config, controls = define_controls(), silent = FALSE ) { # Provide functions and variables for debugging @@ -45,44 +45,45 @@ run_rabimo <- function( { kwb.utils::assignPackageObjects("kwb.rabimo") data <- kwb.rabimo::rabimo_inputs_2025$data - config <- kwb.rabimo::rabimo_inputs_2025$config + config <- reconfigure(kwb.rabimo::rabimo_inputs_2025$config) + config$measures$green_roof[[2]] <- list( + roof_fraction_column = "green_roof_int", + bagrov_value = 0.7 + ) controls <- define_controls() silent <- FALSE `%>%` <- magrittr::`%>%` } - + data <- remove_geo_column_if_required(data) # Save geometry data that may have stored in attribute "geometry" geometry <- attr(data, "geometry") - + # If road-area-specific columns are missing, create them - data <- handle_missing_columns(data, silent = silent) - - # If model parameters are missing, create them - config <- handle_missing_config_entries(config) + data <- handle_missing_columns(data, silent = silent, measures = config$measures) # Provide function to access the list of controls control <- create_accessor(controls) - + # Check whether data and config have the expected structures if (isTRUE(control("check"))) { stop_on_invalid_data(data) stop_on_invalid_config(config) } - + # Get climate data climate <- cat_and_run( dbg = !silent, "Collecting climate related data", get_climate(data) ) - + # Create access functions to data columns and config elements fetch_data <- create_accessor(data) fetch_config <- create_accessor(config) fetch_climate <- create_accessor(climate) - + # Prepare soil properties for all rows. They are required to calculate the # actual evapotranspiration of unsealed areas. In the case of water bodies, # all values are 0.0. (hsonne: really?) @@ -98,12 +99,39 @@ run_rabimo <- function( dbg = FALSE ) ) - + # Precalculate actual evapotranspirations for impervious areas + + if (is.null(config$measures)) { + + # old configuration format: + + # - column name is fix + green_roof_columns <- "green_roof" + + # - Bagrov value for green roofs is stored within config$bagrov_values + bagrov_values <- fetch_config("bagrov_values") + + } else { + + # new configuration format: + + # - column names are explicitly given + green_roof_columns <- sapply( + config$measures$green_roof, "[[", "roof_fraction_column" + ) + + # - Bagrov values are stored within config$measures$green_roof + bagrov_values <- c(fetch_config("bagrov_values"), stats::setNames( + sapply(config$measures$green_roof, "[[", "bagrov_value"), + green_roof_columns + )) + } + evaporation_sealed <- cat_and_run( dbg = !silent, "Precalculating actual evapotranspirations for impervious areas", - expr = fetch_config("bagrov_values") %>% + expr = bagrov_values %>% lapply(function(x) { real_evapo_transpiration( potential_evaporation = fetch_climate("epot_yr"), @@ -114,7 +142,7 @@ run_rabimo <- function( }) %>% do.call(what = data.frame) ) - + # Precalculate actual evapotranspirations for waterbodies or pervious areas evaporation_unsealed <- cat_and_run( dbg = !silent, @@ -131,155 +159,181 @@ run_rabimo <- function( use_abimo_algorithm = control("use_abimo_bagrov_solver") ) ) - + runoff_all <- fetch_climate("prec_yr") - cbind( evaporation_sealed, unsealed = evaporation_unsealed ) - + # Runoff for all sealed areas (including roofs) - + # Calculate roof related variables - + # total runoff of roof areas # (total runoff, contains both surface runoff and infiltration components) runoff_roof <- select_columns(runoff_all, "roof") - runoff_green_roof <- select_columns(runoff_all, c("green_roof", "green_roof_int")) - + + # Selection of green-roof related columns (fractions of the roof area) + runoff_green_roof <- select_columns(runoff_all, green_roof_columns, drop = FALSE) + fractions_green_roof <- fetch_data(green_roof_columns, drop = FALSE) + # Provide runoff coefficients for impervious surfaces runoff_factors <- fetch_config("runoff_factors") - + # actual runoff from roof surface (area based, with no infiltration) - runoff_roof_actual <- with( - data, - main_frac * roof * (1 - green_roof) * swg_roof - ) * runoff_factors[["roof"]] * runoff_roof - + non_green_roof <- (1 - rowSums(fractions_green_roof)) + runoff_roof_actual <- with(data, main_frac * roof * swg_roof) * + non_green_roof * + runoff_factors[["roof"]] * + runoff_roof + # actual runoff from green roof surface (area based, with no infiltration) - runoff_green_roof_actual <- with( - data, - main_frac * roof * swg_roof * green_roof * cbind(1, green_roof_int) - ) * + runoff_green_roof_actual <- with(data, main_frac * roof * swg_roof) * + fractions_green_roof * runoff_factors[["roof"]] * runoff_green_roof # actual infiltration from roof surface (area based, with no runoff) - infiltration_roof_actual <- with( - data, - main_frac * roof * (1-green_roof) * (1-swg_roof) - ) * runoff_roof - + infiltration_roof_actual <- with(data, main_frac * roof * (1 - swg_roof)) * + non_green_roof * + runoff_roof + # actual infiltration from green_roof surface (area based, with no runoff) - infiltration_green_roof_actual <- with( - data, - main_frac * roof * (1-swg_roof) * green_roof * cbind(1, green_roof_int) - ) * + infiltration_green_roof_actual <- with(data, main_frac * roof * (1 - swg_roof)) * + fractions_green_roof * runoff_green_roof # Calculate runoff for all surface classes at once # (contains both surface runoff and infiltration components) - + # Identify active surface class columns in input data surface_cols_no_rd <- matching_names(data, pattern_no_roads()) surface_cols_rd <- matching_names(data, pattern_roads()) digits <- gsub("\\D", "", surface_cols_no_rd) surface_class_names <- paste0("surface",digits) - + # choose columns related to surface classes runoff_sealed <- select_columns(runoff_all, surface_class_names) # head(runoff_sealed) - + # Runoff from the actual partial areas that are sealed and connected # (road and non-road) areas (for all surface classes at once) - + runoff_factor_matrix <- expand_to_matrix( x = runoff_factors[surface_class_names], nrow = nrow(data) ) - + unbuilt_surface_fractions <- fetch_data(surface_cols_no_rd) road_surface_fractions <- fetch_data(surface_cols_rd) - + # add an empty column in road_surface_fraction to match dimension if needed if (!identical(length(surface_cols_no_rd), length(surface_cols_rd))) { road_surface_fractions$srf5_pvd_r <- 0 } - + runoff_sealed_actual <- runoff_sealed * ( with(data, main_frac * pvd * swg_pvd) * unbuilt_surface_fractions + with(data, road_frac * pvd_r * swg_pvd_r) * road_surface_fractions ) * runoff_factor_matrix - + # infiltration of sealed surfaces # (road and non-road) areas (for all surface classes at once) infiltration_sealed_actual <- runoff_sealed * ( with(data, main_frac * pvd) * unbuilt_surface_fractions + with(data, road_frac * pvd_r) * road_surface_fractions) - runoff_sealed_actual - + # Total Runoff of unsealed surfaces (unsealedSurface_RUV) - runoff_unsealed <- fetch_climate("prec_yr") - as.numeric(evaporation_unsealed) # why as.numeric()? - + # as.numeric() removes attribute "bagrovUnsealed" with intermediate values + runoff_unsealed <- fetch_climate("prec_yr") - as.numeric(evaporation_unsealed) + # Infiltration of road (unsealed areas) infiltration_unsealed_roads <- with(data, road_frac * (1 - pvd_r)) * runoff_sealed[, ncol(runoff_sealed)] # last (less sealed) surface class - + fraction_unsealed <- with( data, ifelse(control("reproduce_abimo_error"), 1, main_frac) * (1 - (roof + pvd)) ) - + infiltration_unsealed_surfaces <- fraction_unsealed * runoff_unsealed - + # Calculate runoff 'ROW' for entire block area (FLGES + STR_FLGES) (mm/a) - total_surface_runoff <- ( - runoff_roof_actual + - rowSums(runoff_green_roof_actual) + - #orig.: runoff_unsealed_roads <- was set to zero in the master branch - rowSums(runoff_sealed_actual)) - + total_surface_runoff <- runoff_roof_actual + + rowSums(runoff_green_roof_actual) + + #orig.: runoff_unsealed_roads <- was set to zero in the master branch + rowSums(runoff_sealed_actual) + # Calculate infiltration rate 'RI' for entire block partial area (mm/a) - total_infiltration <- - (infiltration_roof_actual + - rowSums(infiltration_green_roof_actual) + - infiltration_unsealed_surfaces + - infiltration_unsealed_roads + - rowSums(infiltration_sealed_actual)) - + total_infiltration <- infiltration_roof_actual + + rowSums(infiltration_green_roof_actual) + + infiltration_unsealed_surfaces + + infiltration_unsealed_roads + + rowSums(infiltration_sealed_actual) + + # Provide information on the infiltration measure(s) + infiltration_configs <- if (is.null(config$measures)) { + # old configuration format + list( + list( + area_fraction_column = "to_swale", + evaporation_factor = config$swale[["swale_evaporation_factor"]] + ) + ) + } else { + # new configuration format + config$measures$infiltration + } + + deltas <- lapply(infiltration_configs, function(pars) { + #pars <- infiltration_configs[[1L]] + # TODO: handle overflow + #pars$overflow... + area_fraction_connected <- fetch_data(pars$area_fraction_column) + total_surface_runoff * data.frame( + surface_runoff = area_fraction_connected * (-1), + infiltration = area_fraction_connected * (1 - pars$evaporation_factor) + ) + }) + + # name the entries according to the fraction columns, just for convenience + names(deltas) <- sapply(infiltration_configs, `[[`, "area_fraction_column") + + deltas_surface_runoff <- do.call(cbind, lapply(deltas, `[[`, "surface_runoff")) + deltas_infiltration <- do.call(cbind, lapply(deltas, `[[`, "infiltration")) + # Correct Surface Runoff and Infiltration if area has an infiltration swale - swale_delta <- total_surface_runoff * (fetch_data("to_swale")) - total_surface_runoff <- total_surface_runoff - swale_delta - total_infiltration <- total_infiltration + - swale_delta * (1 - fetch_config("swale")[["swale_evaporation_factor"]]) - + total_surface_runoff <- total_surface_runoff + rowSums(deltas_surface_runoff) + total_infiltration <- total_infiltration + rowSums(deltas_infiltration) + # Calculate "total system losses" 'R' due to runoff and infiltration # for entire block partial area total_runoff <- total_surface_runoff + total_infiltration - + # Calculate evaporation 'VERDUNST' by subtracting 'R', the sum of - # runoff and infiltration from precipitation of entire year, - # multiplied by precipitation correction factor + # runoff and infiltration from (corrected) precipitation of entire year total_evaporation <- climate[["prec_yr"]] - total_runoff - + # Provide total area for calculation of "flows" total_area <- fetch_data("total_area") - + # Calculate volume 'rowvol' from runoff (qcm/s) surface_runoff_flow <- yearly_height_to_volume_flow( total_surface_runoff, total_area ) - + # Calculate volume 'rivol' from infiltration rate (qcm/s) infiltration_flow <- yearly_height_to_volume_flow( total_infiltration, total_area ) - + # Calculate volume of "system losses" 'rvol' due to surface runoff and # infiltration total_runoff_flow <- surface_runoff_flow + infiltration_flow - + # Provide mapping between local variable names and ABIMO-output columns name_mapping <- list( code = "CODE", @@ -292,23 +346,23 @@ run_rabimo <- function( total_area = "FLAECHE", total_evaporation = "VERDUNSTUN" ) - + # Compose result data frame. Use mget() to get the result vectors from the # local environment and put them into the data frame result_data_raw <- cbind( fetch_data("code", drop = FALSE), mget(names(name_mapping)[-1L]) ) - + output_format <- control("output_format") - + result_data <- if (output_format == "abimo") { - + # Provide the same columns as Abimo does rename_columns(result_data_raw, name_mapping) - + } else if (output_format == "rabimo") { - + data.frame( code = result_data_raw$code, area = result_data_raw$total_area, @@ -316,23 +370,23 @@ run_rabimo <- function( infiltr = result_data_raw$total_infiltration, evapor = result_data_raw$total_evaporation ) - + } else { - + clean_stop("controls$output_format must be either 'abimo' or 'rabimo'.") } - + # Round all columns to three digits (skip first column: "code") result_data[-1L] <- lapply(result_data[-1L], round, 3L) - + result_data <- restore_geo_column_if_required( result_data, geometry = geometry ) - + if (isFALSE(control("intermediates"))) { return(result_data) } - + # Return intermediate results as attributes structure( result_data, @@ -365,7 +419,7 @@ run_rabimo <- function( } # handle_missing_columns ------------------------------------------------------- -handle_missing_columns <- function(data, silent = FALSE) +handle_missing_columns <- function(data, silent = FALSE, measures = NULL) { init_column <- function(data, column, default) { if (!silent) { @@ -375,49 +429,45 @@ handle_missing_columns <- function(data, silent = FALSE) data } - road_specific_columns <- c( - "road_frac", "pvd_r", "swg_pvd_r", - "srf1_pvd_r", "srf2_pvd_r", "srf3_pvd_r", "srf4_pvd_r" + defaults <- list( + # road_specific_columns + road_frac = 0, + pvd_r = 0, + swg_pvd_r = 0, + srf1_pvd_r = 0, + srf2_pvd_r = 0, + srf3_pvd_r = 0, + srf4_pvd_r = 0, + # (non-road) fraction + main_frac = 1 ) - - missing_road_columns <- setdiff(road_specific_columns, names(data)) - - if (length(missing_road_columns)) { - for (column in missing_road_columns) { - data <- init_column(data, column, 0) - } - } - - column <- "main_frac" - if (! column %in% names(data)) { - data <- init_column(data, column, 1) - } - - column <- "green_roof_int" - if (! column %in% names(data)) { - data <- init_column(data, column, 0) + + for (column in names(defaults)) { + if (!column %in% names(data)) { + data <- init_column(data, column, defaults[[column]]) + } } - data -} - -# handle_missing_config_entries ------------------------------------------------ -handle_missing_config_entries <- function(config) -{ - if (!"green_roof_int" %in% names(config$bagrov_values)) { - config$bagrov_values["green_roof_int"] <- config$bagrov_values["green_roof"] + if (!is.null(measures)) { + columns_green_roof <- sapply(measures$green_roof, "[[", "roof_fraction_column") + columns_infiltration <- sapply(measures$infiltration, "[[", "area_fraction_column") + for (column in c(columns_green_roof, columns_green_roof)) { + if (! column %in% names(data)) { + data <- init_column(data, column, 0) + } + } } - config + data } # get_climate: provides climate relevant input data ---------------------------- get_climate <- function(input) { climate <- select_columns(input, c("prec_yr", "prec_s", "epot_yr", "epot_s")) - + climate[["x_ratio"]] <- climate[["prec_yr"]] / climate[["epot_yr"]] - + climate } diff --git a/R/stop_on_invalid_config.R b/R/stop_on_invalid_config.R index 5d1d77ba..67b5dd8f 100644 --- a/R/stop_on_invalid_config.R +++ b/R/stop_on_invalid_config.R @@ -16,8 +16,13 @@ stop_on_invalid_config <- function(config) runoff_factors <- select_elements(config, "runoff_factors") check_values_for_surface_types(x = bagrov_values) - stopifnot("green_roof" %in% names(bagrov_values)) - check_values_for_surface_types(x = runoff_factors) + + if ((is_new_format <- !is.null(config$measures))) { + stopifnot(all(c("green_roof", "infiltration") %in% names(config$measures))) + } else { + stopifnot("green_roof" %in% names(bagrov_values)) + } + } diff --git a/inst/extdata/column-names.csv b/inst/extdata/column-names.csv index 46c628de..6c86f4cb 100644 --- a/inst/extdata/column-names.csv +++ b/inst/extdata/column-names.csv @@ -9,7 +9,6 @@ total_area,,,Total block area,m2,required,numeric,100 main_frac,,,Non-road fraction of total_area,0..1,,numeric,1.0 roof,PROBAU,x,Roof fraction of non-road built area,0..1,required,numeric,0.2 green_roof,,,Green roof fraction of roof area,0..1,required,numeric,0.0 -green_roof_int,,,Fraction of green roof area that refers to intenive green roofs,0..1,,numeric,0.0 swg_roof,KAN_BEB,x,Fraction of roof area connected to the sewer ,0..1,required,numeric,1.0 pvd,PROVGU,x, Paved fraction of non-road area,0..1,required,numeric,0.6 swg_pvd,KAN_VGU,x,Fraction of paved area connected to the sewer,0..1,required,numeric,0.7 diff --git a/tests/testthat/test-function-handle_missing_columns.R b/tests/testthat/test-function-handle_missing_columns.R index a37e0b5f..6120c6fc 100644 --- a/tests/testthat/test-function-handle_missing_columns.R +++ b/tests/testthat/test-function-handle_missing_columns.R @@ -6,16 +6,13 @@ test_that("handle_missing_columns() works", { expect_error(handle_missing()) area_with_missing <- kwb.utils::removeColumns( - kwb.rabimo::generate_rabimo_area("code", green_roof_int = 0.1), - columnsToRemove = c("main_frac", "green_roof_int") + kwb.rabimo::generate_rabimo_area("code"), + columns = c("main_frac") ) - expect_message(expect_message(result_1 <- handle_missing(area_with_missing))) + expect_message(result_1 <- handle_missing(area_with_missing)) expect_silent(result_2 <- handle_missing(area_with_missing, silent = TRUE)) - expect_identical(result_1$green_roof_int, 0) - expect_identical(result_2$green_roof_int, 0) - expect_identical(result_1$main_frac, 1) expect_identical(result_2$main_frac, 1) }) diff --git a/tests/testthat/test-function-run_rabimo.R b/tests/testthat/test-function-run_rabimo.R index 5ed671d5..745a2205 100644 --- a/tests/testthat/test-function-run_rabimo.R +++ b/tests/testthat/test-function-run_rabimo.R @@ -96,7 +96,9 @@ test_that("run_rabimo() keeps geometry if data inherits from 'sf'", { inputs <- kwb.rabimo::rabimo_inputs_2025 data <- inputs$data[sample(nrow(inputs$data), 10L), ] expect_true("sf" %in% class(data)) - expect_output(result <- kwb.rabimo::run_rabimo(data, config = inputs$config)) + expect_output(suppressMessages( + result <- kwb.rabimo::run_rabimo(data, config = inputs$config) + )) expect_true("sf" %in% class(result)) }) diff --git a/tests/testthat/test-function-run_rabimo_with_measures.R b/tests/testthat/test-function-run_rabimo_with_measures.R index 0aaac858..bae3a7ae 100644 --- a/tests/testthat/test-function-run_rabimo_with_measures.R +++ b/tests/testthat/test-function-run_rabimo_with_measures.R @@ -113,7 +113,7 @@ test_that("run_rabimo_with_measures(old_version = TRUE) works", { for (seed in sample(1e10, 5)) { #seed <- seeds[1L] - writeLines(paste("seed:", seed)) + #writeLines(paste("seed:", seed)) DATASETS <- lapply( X = list( From 4de8074685cadb7d170b41640b262a62e8aa9382 Mon Sep 17 00:00:00 2001 From: hsonne Date: Tue, 11 Nov 2025 16:29:27 +0100 Subject: [PATCH 3/6] Do not require columns green_roof, to_swale This is still work in progress. TODO: fix the tests --- R/generate_rabimo_area.R | 15 +++-- R/run_rabimo.R | 66 ++++++++----------- inst/extdata/column-names.csv | 2 - man/generate_rabimo_area.Rd | 7 +- .../test-function-generate_rabimo_area.R | 11 ++-- tests/testthat/test-function-run_rabimo.R | 18 ++--- .../test-function-run_rabimo_with_measures.R | 3 +- vignettes/tutorial.Rmd | 5 +- 8 files changed, 66 insertions(+), 61 deletions(-) diff --git a/R/generate_rabimo_area.R b/R/generate_rabimo_area.R index 80898d40..1c306ee1 100644 --- a/R/generate_rabimo_area.R +++ b/R/generate_rabimo_area.R @@ -4,11 +4,13 @@ #' #' All default values can be overridden by entering new key-value pairs. #' -#' @param code identifier of area +#' @param code vector of unique area identifiers. If NULL, default codes are +#' created: area_1, area_2, ... +#' @param n number of areas to be created. Default: length of code vector (1 if code is NULL) #' @param \dots key = value pairs overriding the default column values #' @param column_info data frame as returned by \code{\link{read_column_info}} #' @export -generate_rabimo_area <- function(code, ..., column_info = read_column_info()) +generate_rabimo_area <- function(code = NULL, ..., column_info = read_column_info()) { #kwb.utils::assignPackageObjects("kwb.rabimo");column_info=read_column_info();`%>%`<-magrittr::`%>%` @@ -39,7 +41,12 @@ generate_rabimo_area <- function(code, ..., column_info = read_column_info()) result <- do.call(data.frame, args) - # Add column "code" - result["code"] <- code + # Overwrite column "code" + result["code"] <- if (is.null(code)) { + paste0("area_", seq_len(nrow(result))) + } else { + code + } + result } diff --git a/R/run_rabimo.R b/R/run_rabimo.R index 21e31c4b..ce59fbfe 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -60,6 +60,11 @@ run_rabimo <- function( # Save geometry data that may have stored in attribute "geometry" geometry <- attr(data, "geometry") + # if config is provided in old format, convert to new format + if (is.null(config$measures)) { + config <- reconfigure(config) + } + # If road-area-specific columns are missing, create them data <- handle_missing_columns(data, silent = silent, measures = config$measures) @@ -101,32 +106,19 @@ run_rabimo <- function( ) # Precalculate actual evapotranspirations for impervious areas + # Here we expect the new config format (config$measures must exist!) + green_roof_columns <- sapply( + config$measures$green_roof, "[[", "roof_fraction_column" + ) - if (is.null(config$measures)) { - - # old configuration format: - - # - column name is fix - green_roof_columns <- "green_roof" - - # - Bagrov value for green roofs is stored within config$bagrov_values - bagrov_values <- fetch_config("bagrov_values") - - } else { - - # new configuration format: - - # - column names are explicitly given - green_roof_columns <- sapply( - config$measures$green_roof, "[[", "roof_fraction_column" - ) - - # - Bagrov values are stored within config$measures$green_roof - bagrov_values <- c(fetch_config("bagrov_values"), stats::setNames( + # - Bagrov values are stored within config$measures$green_roof + bagrov_values <- c( + fetch_config("bagrov_values"), + stats::setNames( sapply(config$measures$green_roof, "[[", "bagrov_value"), green_roof_columns - )) - } + ) + ) evaporation_sealed <- cat_and_run( dbg = !silent, @@ -274,19 +266,9 @@ run_rabimo <- function( infiltration_unsealed_roads + rowSums(infiltration_sealed_actual) + # Here we expect the new config format! # Provide information on the infiltration measure(s) - infiltration_configs <- if (is.null(config$measures)) { - # old configuration format - list( - list( - area_fraction_column = "to_swale", - evaporation_factor = config$swale[["swale_evaporation_factor"]] - ) - ) - } else { - # new configuration format - config$measures$infiltration - } + infiltration_configs <- config$measures$infiltration deltas <- lapply(infiltration_configs, function(pars) { #pars <- infiltration_configs[[1L]] @@ -301,7 +283,7 @@ run_rabimo <- function( # name the entries according to the fraction columns, just for convenience names(deltas) <- sapply(infiltration_configs, `[[`, "area_fraction_column") - + deltas_surface_runoff <- do.call(cbind, lapply(deltas, `[[`, "surface_runoff")) deltas_infiltration <- do.call(cbind, lapply(deltas, `[[`, "infiltration")) @@ -448,10 +430,20 @@ handle_missing_columns <- function(data, silent = FALSE, measures = NULL) } } + # measures + # $green_roof + # [[1]] + # $roof_fraction_column = "green_roof_ext" + # [[2]] + # $roof_fraction_column = "green_roof_int" + # $infiltration + # [[1]] + # $area_fraction_column = "to_swale" + if (!is.null(measures)) { columns_green_roof <- sapply(measures$green_roof, "[[", "roof_fraction_column") columns_infiltration <- sapply(measures$infiltration, "[[", "area_fraction_column") - for (column in c(columns_green_roof, columns_green_roof)) { + for (column in c(columns_green_roof, columns_infiltration)) { if (! column %in% names(data)) { data <- init_column(data, column, 0) } diff --git a/inst/extdata/column-names.csv b/inst/extdata/column-names.csv index 6c86f4cb..c0a438ca 100644 --- a/inst/extdata/column-names.csv +++ b/inst/extdata/column-names.csv @@ -8,7 +8,6 @@ district,BEZIRK,,Specific to Berlin: identifier of city district,-,,character,0 total_area,,,Total block area,m2,required,numeric,100 main_frac,,,Non-road fraction of total_area,0..1,,numeric,1.0 roof,PROBAU,x,Roof fraction of non-road built area,0..1,required,numeric,0.2 -green_roof,,,Green roof fraction of roof area,0..1,required,numeric,0.0 swg_roof,KAN_BEB,x,Fraction of roof area connected to the sewer ,0..1,required,numeric,1.0 pvd,PROVGU,x, Paved fraction of non-road area,0..1,required,numeric,0.6 swg_pvd,KAN_VGU,x,Fraction of paved area connected to the sewer,0..1,required,numeric,0.7 @@ -24,7 +23,6 @@ srf1_pvd_r,STR_BELAG1,x,Fraction of road area belonging to surface class 1,0..1, srf2_pvd_r,STR_BELAG2,x,Fraction of road area belonging to surface class 2,0..1,,numeric,0.1 srf3_pvd_r,STR_BELAG3,x,Fraction of road area belonging to surface class 3,0..1,,numeric,0.0 srf4_pvd_r,STR_BELAG4,x,Fraction of road area belonging to surface class 4,0..1,,numeric,0.0 -to_swale,,,Fraction of total area connected to an infiltration swale,0..1,required,numeric,0.0 gw_dist,FLUR,,Depth to the water table,m,required,numeric,3.0 ufc30,FELD_30,,Usable field capacity 0..30 cm,% by volume,required,numeric,13.0 ufc150,FELD_150,,Usable field capacity 0..150 cm,% by volume,required,numeric,13.0 diff --git a/man/generate_rabimo_area.Rd b/man/generate_rabimo_area.Rd index c378d581..26e8679d 100644 --- a/man/generate_rabimo_area.Rd +++ b/man/generate_rabimo_area.Rd @@ -4,14 +4,17 @@ \alias{generate_rabimo_area} \title{Generate an area in R-Abimo format with default values} \usage{ -generate_rabimo_area(code, ..., column_info = read_column_info()) +generate_rabimo_area(code = NULL, ..., column_info = read_column_info()) } \arguments{ -\item{code}{identifier of area} +\item{code}{vector of unique area identifiers. If NULL, default codes are +created: area_1, area_2, ...} \item{\dots}{key = value pairs overriding the default column values} \item{column_info}{data frame as returned by \code{\link{read_column_info}}} + +\item{n}{number of areas to be created. Default: length of code vector (1 if code is NULL)} } \description{ All default values can be overridden by entering new key-value pairs. diff --git a/tests/testthat/test-function-generate_rabimo_area.R b/tests/testthat/test-function-generate_rabimo_area.R index 41f30c23..d7443d5a 100644 --- a/tests/testthat/test-function-generate_rabimo_area.R +++ b/tests/testthat/test-function-generate_rabimo_area.R @@ -4,12 +4,13 @@ test_that("generate_rabimo_area() works", { f <- kwb.rabimo::generate_rabimo_area - expect_error(f()) + expect_no_error(data <- f()) - expect_no_error(expect_output(kwb.rabimo::run_rabimo( - data = f(code = "a_code"), - config = kwb.rabimo::rabimo_inputs_2020$config, + expect_no_error(kwb.rabimo::run_rabimo( + silent = TRUE, + data = data, + config = kwb.rabimo::rabimo_inputs_2025$config, controls = kwb.rabimo::define_controls() - ))) + )) }) diff --git a/tests/testthat/test-function-run_rabimo.R b/tests/testthat/test-function-run_rabimo.R index 745a2205..d18a5bfb 100644 --- a/tests/testthat/test-function-run_rabimo.R +++ b/tests/testthat/test-function-run_rabimo.R @@ -11,12 +11,12 @@ test_that("run_rabimo() reproduces previous results", { test_that("run_rabimo() works", { - f <- kwb.rabimo::run_rabimo + run <- kwb.rabimo::run_rabimo - expect_error(f()) + expect_error(run()) data <- data.frame( - code = "a", + code = "area_1", land_type = "a", prec_yr = 100L, prec_s = 100L, @@ -74,10 +74,10 @@ test_that("run_rabimo() works", { ) expect_output( - result_1 <- f(data, config, controls = define_controls()) + result_1 <- run(data, config, controls = define_controls()) ) expect_silent( - result_2 <- f(data, config, controls = define_controls(), silent = TRUE) + result_2 <- run(data, config, controls = define_controls(), silent = TRUE) ) expect_s3_class(result_1, "data.frame") @@ -105,11 +105,11 @@ test_that("run_rabimo() keeps geometry if data inherits from 'sf'", { test_that("Full connection to swales results in zero runoff", { generate <- kwb.rabimo::generate_rabimo_area data <- rbind( - generate("area_0"), - generate("all_swale", to_swale = 1), + generate("area_0", green_roof = 0, to_swale = 0), + generate("all_swale", green_roof = 0, to_swale = 1), generate("all_swale_plus_green_roof", green_roof = 1, to_swale = 1), - generate("all_swale_plus_green_roof", pvd = 0, to_swale = 1), - generate("all_swale_plus_both", pvd = 0, green_roof = 1, to_swale = 1) + generate("all_swale_plus_green_roof", green_roof = 0, to_swale = 1, pvd = 0), + generate("all_swale_plus_both", green_roof = 1, to_swale = 1, pvd = 0) ) config <- kwb.rabimo::rabimo_inputs_2025$config result <- kwb.rabimo::run_rabimo(data, config, silent = TRUE) diff --git a/tests/testthat/test-function-run_rabimo_with_measures.R b/tests/testthat/test-function-run_rabimo_with_measures.R index bae3a7ae..5e744035 100644 --- a/tests/testthat/test-function-run_rabimo_with_measures.R +++ b/tests/testthat/test-function-run_rabimo_with_measures.R @@ -109,8 +109,9 @@ test_that("run_rabimo_with_measures(old_version = TRUE) works", { expect_error(RUN()) sample_size <- 100L + seeds <- sample(1e10, 5) - for (seed in sample(1e10, 5)) { + for (seed in seeds) { #seed <- seeds[1L] #writeLines(paste("seed:", seed)) diff --git a/vignettes/tutorial.Rmd b/vignettes/tutorial.Rmd index 02c06f78..f88dd71f 100644 --- a/vignettes/tutorial.Rmd +++ b/vignettes/tutorial.Rmd @@ -210,7 +210,10 @@ art_blocks <- kwb.rabimo::generate_rabimo_area( art_blocks # Run R-Abimo on the block areas -art_water_balance <- kwb.rabimo::run_rabimo(art_blocks, config = abimo_inputs$config) +art_water_balance <- kwb.rabimo::run_rabimo( + data = art_blocks, + config = abimo_inputs$config +) # How does the roof area influence the runoff? plot(art_blocks$roof, art_water_balance$runoff) From eb4e2cbbf751406bf122f0d2a94c4cecd70d699c Mon Sep 17 00:00:00 2001 From: hsonne Date: Mon, 17 Nov 2025 16:26:29 +0100 Subject: [PATCH 4/6] Consider overflow factor, check config --- R/run_rabimo.R | 17 +++-- R/stop_on_invalid_config.R | 28 +++++++- R/stop_on_invalid_data.R | 21 +++++- .../test-function-stop_on_invalid_config.R | 64 +++++++++++++++---- 4 files changed, 108 insertions(+), 22 deletions(-) diff --git a/R/run_rabimo.R b/R/run_rabimo.R index ce59fbfe..525ed31b 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -44,12 +44,19 @@ run_rabimo <- function( if (FALSE) { kwb.utils::assignPackageObjects("kwb.rabimo") - data <- kwb.rabimo::rabimo_inputs_2025$data + data <- kwb.utils::removeColumns(kwb.rabimo::rabimo_inputs_2025$data, "to_swale") config <- reconfigure(kwb.rabimo::rabimo_inputs_2025$config) config$measures$green_roof[[2]] <- list( roof_fraction_column = "green_roof_int", bagrov_value = 0.7 ) + config$measures$infiltration[[1]]$overflow_factor <- 0.2 + config$measures$infiltration[[2]] <- list( + area_fraction_column = "to_swale_2", + evaporation_factor = 0.2, + overflow_factor = 0.15 + ) + #str(config$measures$infiltration) controls <- define_controls() silent <- FALSE `%>%` <- magrittr::`%>%` @@ -73,8 +80,8 @@ run_rabimo <- function( # Check whether data and config have the expected structures if (isTRUE(control("check"))) { - stop_on_invalid_data(data) stop_on_invalid_config(config) + stop_on_invalid_data(data, measures = config$measures) } # Get climate data @@ -202,7 +209,7 @@ run_rabimo <- function( surface_cols_no_rd <- matching_names(data, pattern_no_roads()) surface_cols_rd <- matching_names(data, pattern_roads()) digits <- gsub("\\D", "", surface_cols_no_rd) - surface_class_names <- paste0("surface",digits) + surface_class_names <- paste0("surface", digits) # choose columns related to surface classes runoff_sealed <- select_columns(runoff_all, surface_class_names) @@ -272,10 +279,8 @@ run_rabimo <- function( deltas <- lapply(infiltration_configs, function(pars) { #pars <- infiltration_configs[[1L]] - # TODO: handle overflow - #pars$overflow... area_fraction_connected <- fetch_data(pars$area_fraction_column) - total_surface_runoff * data.frame( + total_surface_runoff * (1 - pars$overflow_factor) * data.frame( surface_runoff = area_fraction_connected * (-1), infiltration = area_fraction_connected * (1 - pars$evaporation_factor) ) diff --git a/R/stop_on_invalid_config.R b/R/stop_on_invalid_config.R index 67b5dd8f..336eac2f 100644 --- a/R/stop_on_invalid_config.R +++ b/R/stop_on_invalid_config.R @@ -18,8 +18,32 @@ stop_on_invalid_config <- function(config) check_values_for_surface_types(x = bagrov_values) check_values_for_surface_types(x = runoff_factors) - if ((is_new_format <- !is.null(config$measures))) { - stopifnot(all(c("green_roof", "infiltration") %in% names(config$measures))) + if (is_new_format <- !is.null(config$measures)) { + green_roof_configs <- select_elements(config$measures, "green_roof") + infiltration_configs <- select_elements(config$measures, "infiltration") + columns_green_roof <- sapply( + green_roof_configs, + FUN = select_elements, + elements = "roof_fraction_column" + ) + columns_infiltration <- sapply( + infiltration_configs, + FUN = select_elements, + elements = "area_fraction_column" + ) + if (length(columns_green_roof) != length(unique(columns_green_roof))) { + kwb.utils::stopFormatted( + "The s in config$measures$green_roof (%s) are not unique as expected.", + kwb.utils::stringList(columns_green_roof) + ) + } + if (length(columns_infiltration) != length(unique(columns_infiltration))) { + kwb.utils::stopFormatted( + "The s in config$measures$infiltration (%s) are not unique as expected.", + kwb.utils::stringList(columns_infiltration) + ) + } + } else { stopifnot("green_roof" %in% names(bagrov_values)) } diff --git a/R/stop_on_invalid_data.R b/R/stop_on_invalid_data.R index 7de7ce08..51e971b7 100644 --- a/R/stop_on_invalid_data.R +++ b/R/stop_on_invalid_data.R @@ -1,7 +1,7 @@ # stop_on_invalid_data --------------------------------------------------------- #' @importFrom rlang .data #' @importFrom kwb.utils stopFormatted -stop_on_invalid_data <- function(data) +stop_on_invalid_data <- function(data, measures = NULL) { # Read information on column names and types column_info <- read_column_info() @@ -40,7 +40,7 @@ stop_on_invalid_data <- function(data) convert = FALSE ) - # Do not accept any NA + # Do not accept any NA in required columns of type numeric check_columns( data = data, columns = names(data) %>% @@ -81,6 +81,23 @@ stop_on_invalid_data <- function(data) if (length(columns <- matching_names(data, pattern_roads()))) { check_sum_up_to_1_or_0(data, columns) } + + # If measures are given, check that related fractions do not sum up to + # value above 1 + if (!is.null(measures)) { + columns_green_roof <- sapply( + select_elements(measures, "green_roof"), + FUN = select_elements, + "roof_fraction_column" + ) + columns_infiltration <- sapply( + select_elements(measures, "infiltration"), + FUN = select_elements, + "area_fraction_column" + ) + check_sum_up_to_1_or_0(data, columns_green_roof) + check_sum_up_to_1_or_0(data, columns_infiltration) + } } # get_expected_data_type ------------------------------------------------------- diff --git a/tests/testthat/test-function-stop_on_invalid_config.R b/tests/testthat/test-function-stop_on_invalid_config.R index 113041f4..42762dee 100644 --- a/tests/testthat/test-function-stop_on_invalid_config.R +++ b/tests/testthat/test-function-stop_on_invalid_config.R @@ -1,17 +1,57 @@ -# -# This file was generated by kwb.test::create_test_files(), -# launched by hsonne on 2024-03-07 19:06:24.082509. -# Please modify the dummy functions so that real cases are -# tested. Then, delete this comment. -# - +#library(testthat) test_that("stop_on_invalid_config() works", { - + f <- kwb.rabimo:::stop_on_invalid_config - + + expect_error(f()) + expect_error(f(list())) + + base_config <- list( + bagrov_values = c( + roof = 1, + surface1 = 1, + surface2 = 1, + surface3 = 1, + surface4 = 1, + surface5 = 1 + ), + runoff_factors = c( + roof = 1, + surface1 = 1, + surface2 = 1, + surface3 = 1, + surface4 = 1, + surface5 = 1 + ) + ) + + expect_error( + f(c(base_config, list( + measures = list( + green_roof = list( + list(roof_fraction_column = "column-1"), + list(roof_fraction_column = "column-1") + ), + infiltration = list() + ) + ))), + "roof_fraction_column.*are not unique as expected" + ) + expect_error( - f() - # Argument "config" fehlt (ohne Standardwert) + f(c(base_config, list( + measures = list( + green_roof = list( + list(roof_fraction_column = "column-1"), + list(roof_fraction_column = "column-2") + ), + infiltration = list( + list(area_fraction_column = "column-1"), + list(area_fraction_column = "column-1") + ) + ) + ))), + "area_fraction_column.*are not unique" ) - + }) From b2244b3dbfddc9a372eab100aa575bcc4fa998a4 Mon Sep 17 00:00:00 2001 From: hsonne Date: Mon, 17 Nov 2025 17:36:48 +0100 Subject: [PATCH 5/6] Check for sum < 1 instead of == 1 or == 0 --- R/run_rabimo.R | 6 +++++ R/stop_on_invalid_data.R | 58 +++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/R/run_rabimo.R b/R/run_rabimo.R index 525ed31b..a43aed5d 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -279,6 +279,12 @@ run_rabimo <- function( deltas <- lapply(infiltration_configs, function(pars) { #pars <- infiltration_configs[[1L]] + # check for all required elements + pars <- select_elements(pars, c( + "area_fraction_column", + "evaporation_factor", + "overflow_factor" + )) area_fraction_connected <- fetch_data(pars$area_fraction_column) total_surface_runoff * (1 - pars$overflow_factor) * data.frame( surface_runoff = area_fraction_connected * (-1), diff --git a/R/stop_on_invalid_data.R b/R/stop_on_invalid_data.R index 51e971b7..046be049 100644 --- a/R/stop_on_invalid_data.R +++ b/R/stop_on_invalid_data.R @@ -95,8 +95,8 @@ stop_on_invalid_data <- function(data, measures = NULL) FUN = select_elements, "area_fraction_column" ) - check_sum_up_to_1_or_0(data, columns_green_roof) - check_sum_up_to_1_or_0(data, columns_infiltration) + check_sum_is_below_1(data, columns_green_roof) + check_sum_is_below_1(data, columns_infiltration) } } @@ -121,6 +121,20 @@ get_expected_data_type <- function(columns = NULL) type_info[intersect(names(type_info), columns)] } +# stop_on_non_numeric_columns -------------------------------------------------- +stop_on_non_numeric_columns <- function(data) +{ + is_numeric <- sapply(data, is.numeric) + + if (any(!is_numeric)) { + kwb.utils::stopFormatted( + "There are non-numeric columns in %s: %s", + deparse(substitute(data)), + kwb.utils::stringList(names(data)[!is_numeric]) + ) + } +} + # check_sum_up_to_1_or_0 ------------------------------------------------------- #' @importFrom kwb.utils stopFormatted stringList check_sum_up_to_1_or_0 <- function(data, columns, tolerance = 0.005) @@ -132,14 +146,7 @@ check_sum_up_to_1_or_0 <- function(data, columns, tolerance = 0.005) column_data <- select_columns(data, columns, drop = FALSE) - # Check for non-numeric columns - is_numeric <- sapply(column_data, is.numeric) - if (any(!is_numeric)) { - clean_stop( - "There are non-numeric columns in check_sum_up_to_1_or_0(): ", - kwb.utils::stringList(columns[!is_numeric]) - ) - } + stop_on_non_numeric_columns(column_data) sums <- rowSums(column_data) ok <- equals(sums, 0) | equals(sums, 1) @@ -159,3 +166,34 @@ check_sum_up_to_1_or_0 <- function(data, columns, tolerance = 0.005) "(see above). The tolerance was: %f" )) } + +# check_sum_is_below_1 ------------------------------------------------------- +check_sum_is_below_1 <- function(data, columns) +{ + select_columns <- kwb.utils::selectColumns + + column_data <- select_columns(data, columns, drop = FALSE) + + stop_on_non_numeric_columns(column_data) + + sums <- rowSums(column_data) + ok <- sums < 1 + + if (all(ok)) { + return() + } + + cat("(First) invalid rows:\n") + + select_columns(data, c("code", columns))[!ok, ] %>% + utils::head() %>% + print() + + kwb.utils::stopFormatted( + paste( + "The sum of columns %s is not less than or equal to 1 in each row", + "as expected (see above)." + ), + kwb.utils::stringList(columns) + ) +} From 05c9b80b10fb039ab0fb8362e6a384af145f4e5d Mon Sep 17 00:00:00 2001 From: hsonne Date: Fri, 30 Jan 2026 17:32:07 +0100 Subject: [PATCH 6/6] Add measure "retention" (water tank) as an infiltration swale with only evaporation and no infiltration. Fix bug in checking green roof column(s) for value sum <= 1 (not < 1!) --- R/reconfigure.R | 39 ++++---- R/run_rabimo.R | 91 ++++++++++++------- R/stop_on_invalid_config.R | 8 +- R/stop_on_invalid_data.R | 15 ++- man/run_rabimo.Rd | 2 +- .../test-function-stop_on_invalid_config.R | 16 ++-- 6 files changed, 93 insertions(+), 78 deletions(-) diff --git a/R/reconfigure.R b/R/reconfigure.R index 01d0dffc..f38ef311 100644 --- a/R/reconfigure.R +++ b/R/reconfigure.R @@ -10,54 +10,49 @@ if (FALSE) str(kwb.rabimo:::reconfigure(config = config_2025)) config$green_roof <- list( - list(roof_fraction_column = "green_roof", bagrov_value = 0.5) + list(input_column = "green_roof", bagrov_value = 0.5) ) config$green_roof <- list( - list(roof_fraction_column = "green_roof_ext", bagrov_value = 0.5), - list(roof_fraction_column = "green_roof_int", bagrov_value = 0.5) + list(input_column = "green_roof_ext", bagrov_value = 0.5), + list(input_column = "green_roof_int", bagrov_value = 0.5) ) } # reconfigure ------------------------------------------------------------------ reconfigure <- function(config) { - #config <- config_2020 - # Provide vector of Bagrov values bagrov_values <- config$bagrov_values # Remove element "green_roof" from vector of Bagrov values config$bagrov_values <- bagrov_values[names(bagrov_values) != "green_roof"] - # Provide evaporation factor for infiltration method - evaporation_factor <- config$swale[["swale_evaporation_factor"]] - - # Remove config$swale - config$swale <- NULL - config$measures <- list( green_roof = list( list( - roof_fraction_column = "green_roof", + input_column = "green_roof", bagrov_value = bagrov_values[["green_roof"]] ) ), infiltration = list( list( - area_fraction_column = "to_swale", - evaporation_factor = evaporation_factor - #, overflow_rate = 0 # not yet supported! + input_column = "to_swale", + # Use evaporation factor from given config + evaporation_factor = config$swale[["swale_evaporation_factor"]], + overflow_factor = 0 + ) + ), + retention = list( + list( + input_column = "to_storage", + overflow_factor = 0.5 ) - # , list( - # name = "rigole", - # area_fraction_column = "to_inf_rigole", - # evaporation_factor = 0, - # overflow_rate = 0 - # #, rigole_specific_factor = 1 - # ) ) ) + # Remove old config$swale + config$swale <- NULL + config } diff --git a/R/run_rabimo.R b/R/run_rabimo.R index a43aed5d..e7db7739 100644 --- a/R/run_rabimo.R +++ b/R/run_rabimo.R @@ -36,7 +36,7 @@ #' #' plot(results_2025[, -1L]) run_rabimo <- function( - data, config, controls = define_controls(), silent = FALSE + data, config, controls = define_controls(), silent = TRUE ) { # Provide functions and variables for debugging @@ -47,16 +47,26 @@ run_rabimo <- function( data <- kwb.utils::removeColumns(kwb.rabimo::rabimo_inputs_2025$data, "to_swale") config <- reconfigure(kwb.rabimo::rabimo_inputs_2025$config) config$measures$green_roof[[2]] <- list( - roof_fraction_column = "green_roof_int", + # column is expected to contain fractions of roof fraction + input_column = "green_roof_int", bagrov_value = 0.7 ) config$measures$infiltration[[1]]$overflow_factor <- 0.2 config$measures$infiltration[[2]] <- list( - area_fraction_column = "to_swale_2", + input_column = "to_swale_2", evaporation_factor = 0.2, overflow_factor = 0.15 ) - #str(config$measures$infiltration) + config$measures$retention <- list( + list( + input_column <- "watertank_1", + overflow_factor = 0.3 + ), + list( + input_column <- "watertank_2", + overflow_factor = 0.5 + ) + ) controls <- define_controls() silent <- FALSE `%>%` <- magrittr::`%>%` @@ -69,6 +79,7 @@ run_rabimo <- function( # if config is provided in old format, convert to new format if (is.null(config$measures)) { + message("You are using an old configuration. No problem, I convert it.") config <- reconfigure(config) } @@ -115,7 +126,7 @@ run_rabimo <- function( # Precalculate actual evapotranspirations for impervious areas # Here we expect the new config format (config$measures must exist!) green_roof_columns <- sapply( - config$measures$green_roof, "[[", "roof_fraction_column" + config$measures$green_roof, "[[", "input_column" ) # - Bagrov values are stored within config$measures$green_roof @@ -275,25 +286,38 @@ run_rabimo <- function( # Here we expect the new config format! # Provide information on the infiltration measure(s) - infiltration_configs <- config$measures$infiltration - - deltas <- lapply(infiltration_configs, function(pars) { - #pars <- infiltration_configs[[1L]] - # check for all required elements - pars <- select_elements(pars, c( - "area_fraction_column", - "evaporation_factor", - "overflow_factor" - )) - area_fraction_connected <- fetch_data(pars$area_fraction_column) - total_surface_runoff * (1 - pars$overflow_factor) * data.frame( - surface_runoff = area_fraction_connected * (-1), - infiltration = area_fraction_connected * (1 - pars$evaporation_factor) - ) + infiltration_configs <- select_elements(config$measures, "infiltration") + + # For simplicity, we treat the retention as a form of infiltration measure. + # Here, the evaporation factor is always one (100 %), i.e. everything + # evaporates, and nothing actually infiltrates + retention_configs <- lapply( + select_elements(config$measures, "retention"), + function(pars) { + pars$evaporation_factor <- 1 + pars + } + ) + + # Combine the configurations of both measure types + infiltration_or_retention_configs <- c( + infiltration_configs, + retention_configs + ) + + deltas <- lapply(infiltration_or_retention_configs, function(pars) { + area_fraction_connected <- fetch_data(select_elements(pars, "input_column")) + total_surface_runoff * + (1 - select_elements(pars, "overflow_factor")) * + data.frame( + surface_runoff = area_fraction_connected * (-1), + infiltration = area_fraction_connected * + (1 - select_elements(pars, "evaporation_factor")) + ) }) # name the entries according to the fraction columns, just for convenience - names(deltas) <- sapply(infiltration_configs, `[[`, "area_fraction_column") + names(deltas) <- sapply(infiltration_or_retention_configs, `[[`, "input_column") deltas_surface_runoff <- do.call(cbind, lapply(deltas, `[[`, "surface_runoff")) deltas_infiltration <- do.call(cbind, lapply(deltas, `[[`, "infiltration")) @@ -412,7 +436,7 @@ run_rabimo <- function( } # handle_missing_columns ------------------------------------------------------- -handle_missing_columns <- function(data, silent = FALSE, measures = NULL) +handle_missing_columns <- function(data, silent = TRUE, measures = NULL) { init_column <- function(data, column, default) { if (!silent) { @@ -441,20 +465,17 @@ handle_missing_columns <- function(data, silent = FALSE, measures = NULL) } } - # measures - # $green_roof - # [[1]] - # $roof_fraction_column = "green_roof_ext" - # [[2]] - # $roof_fraction_column = "green_roof_int" - # $infiltration - # [[1]] - # $area_fraction_column = "to_swale" - + # Columns that appear as "input_column" fields in the argument "measures" + # are required and initialised with zero if missing if (!is.null(measures)) { - columns_green_roof <- sapply(measures$green_roof, "[[", "roof_fraction_column") - columns_infiltration <- sapply(measures$infiltration, "[[", "area_fraction_column") - for (column in c(columns_green_roof, columns_infiltration)) { + + required_columns <- c( + sapply(measures$green_roof, "[[", "input_column"), + sapply(measures$infiltration, "[[", "input_column"), + sapply(measures$retention, "[[", "input_column") + ) + + for (column in required_columns) { if (! column %in% names(data)) { data <- init_column(data, column, 0) } diff --git a/R/stop_on_invalid_config.R b/R/stop_on_invalid_config.R index 336eac2f..3e2546d8 100644 --- a/R/stop_on_invalid_config.R +++ b/R/stop_on_invalid_config.R @@ -24,22 +24,22 @@ stop_on_invalid_config <- function(config) columns_green_roof <- sapply( green_roof_configs, FUN = select_elements, - elements = "roof_fraction_column" + elements = "input_column" ) columns_infiltration <- sapply( infiltration_configs, FUN = select_elements, - elements = "area_fraction_column" + elements = "input_column" ) if (length(columns_green_roof) != length(unique(columns_green_roof))) { kwb.utils::stopFormatted( - "The s in config$measures$green_roof (%s) are not unique as expected.", + "The s in config$measures$green_roof (%s) are not unique as expected.", kwb.utils::stringList(columns_green_roof) ) } if (length(columns_infiltration) != length(unique(columns_infiltration))) { kwb.utils::stopFormatted( - "The s in config$measures$infiltration (%s) are not unique as expected.", + "The s in config$measures$infiltration (%s) are not unique as expected.", kwb.utils::stringList(columns_infiltration) ) } diff --git a/R/stop_on_invalid_data.R b/R/stop_on_invalid_data.R index 046be049..72b7f25c 100644 --- a/R/stop_on_invalid_data.R +++ b/R/stop_on_invalid_data.R @@ -88,15 +88,15 @@ stop_on_invalid_data <- function(data, measures = NULL) columns_green_roof <- sapply( select_elements(measures, "green_roof"), FUN = select_elements, - "roof_fraction_column" + "input_column" ) columns_infiltration <- sapply( select_elements(measures, "infiltration"), FUN = select_elements, - "area_fraction_column" + "input_column" ) - check_sum_is_below_1(data, columns_green_roof) - check_sum_is_below_1(data, columns_infiltration) + check_sum_is_less_equal_1(data, columns = columns_green_roof) + check_sum_is_less_equal_1(data, columns = columns_infiltration) } } @@ -167,8 +167,8 @@ check_sum_up_to_1_or_0 <- function(data, columns, tolerance = 0.005) )) } -# check_sum_is_below_1 ------------------------------------------------------- -check_sum_is_below_1 <- function(data, columns) +# check_sum_is_less_equal_1 ---------------------------------------------------- +check_sum_is_less_equal_1 <- function(data, columns) { select_columns <- kwb.utils::selectColumns @@ -176,8 +176,7 @@ check_sum_is_below_1 <- function(data, columns) stop_on_non_numeric_columns(column_data) - sums <- rowSums(column_data) - ok <- sums < 1 + ok <- (rowSums(column_data) <= 1) if (all(ok)) { return() diff --git a/man/run_rabimo.Rd b/man/run_rabimo.Rd index 0cc32180..e4ed8392 100644 --- a/man/run_rabimo.Rd +++ b/man/run_rabimo.Rd @@ -4,7 +4,7 @@ \alias{run_rabimo} \title{Run R-Abimo, the R-implementation of Water Balance Model Abimo} \usage{ -run_rabimo(data, config, controls = define_controls(), silent = FALSE) +run_rabimo(data, config, controls = define_controls(), silent = TRUE) } \arguments{ \item{data}{data frame similar to diff --git a/tests/testthat/test-function-stop_on_invalid_config.R b/tests/testthat/test-function-stop_on_invalid_config.R index 42762dee..b078524e 100644 --- a/tests/testthat/test-function-stop_on_invalid_config.R +++ b/tests/testthat/test-function-stop_on_invalid_config.R @@ -29,29 +29,29 @@ test_that("stop_on_invalid_config() works", { f(c(base_config, list( measures = list( green_roof = list( - list(roof_fraction_column = "column-1"), - list(roof_fraction_column = "column-1") + list(input_column = "column-1"), + list(input_column = "column-1") ), infiltration = list() ) ))), - "roof_fraction_column.*are not unique as expected" + "input_column.*are not unique as expected" ) expect_error( f(c(base_config, list( measures = list( green_roof = list( - list(roof_fraction_column = "column-1"), - list(roof_fraction_column = "column-2") + list(input_column = "column-1"), + list(input_column = "column-2") ), infiltration = list( - list(area_fraction_column = "column-1"), - list(area_fraction_column = "column-1") + list(input_column = "column-1"), + list(input_column = "column-1") ) ) ))), - "area_fraction_column.*are not unique" + "input_column.*are not unique as expected" ) })