Skip to content
Open

Dev #14

Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
^codecov\.yml$
^index\.md$
^README\.md$
^vignettes/index\.Rmd$
30 changes: 27 additions & 3 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,39 @@

## Bug fixes

* `vignettes/example_wien_minimal.Rmd` now converts ET0 from mm/day to mm/h
* `get_simulation_results_optim()` now treats a result HDF5 that exists but
cannot be opened/read (e.g. the engine crashed mid-write for a scenario, or a
file briefly locked just after the run) like a missing file: it `warning()`s,
names the scenario, and returns `NULL` instead of throwing. Because the Wien /
Bad Aussee / Eisenstadt workflows now read results per run inside `run_one()`,
a single unreadable file used to abort the entire `future_lapply` batch (seen
as an `H5File.open()` "unable to open file" error mid-render); the run now
completes with NA rows for the affected scenarios.

* `vignettes/example_wien_minimal.Rmd`, `vignettes/workflow_wien.Rmd` and
`vignettes/workflow_badaussee.Rmd` now convert ET0 from mm/day to mm/h
(`value / period_et`) before writing `//Kurven/ET0`, mirroring the existing
rain conversion. The engine reads the ET0 curve as a mm/h rate, so the
unconverted daily values were integrated 24× too high — the cause of the
implausibly large modelled ET share. The timeseries-info summary now labels
ET0 as mm/h and recovers its total via `value * period_h`.
implausibly large modelled ET share. The minimal vignette's timeseries-info
summary now labels ET0 as mm/h and recovers its total via `value * period_h`.

## New features

* The Wien and Bad Aussee workflows now thin each run to its optimisation row
**inside** `run_one()` (via `get_simulation_results_optim(..., lean = TRUE)`
+ `add_overflow_events_and_waterbalance()`) and `run_scenarios()` returns
those one-row tibbles for a final `dplyr::bind_rows()`. This replaces the
previous "run everything, then read every run's full results into memory at
once" pass (`get_simulation_results_optim_parallel()`), drastically cutting
peak RAM for large parameter grids.

* `get_simulation_results_optim()` gains a `lean` argument. When `TRUE` it
reads only the fields consumed downstream (`element$rates`,
`element$water_balance`, `connected_area$water_balance`) and leaves the
unused `meta`/`states` and `connected_area$rates` as `NULL`, minimising
per-run memory and I/O. Its intro message is now gated behind `debug`.

* `inst/scripts/prepare_eisenstadt_swmm_timeseries.R` extracts the rain
(`/Kurven/Regen`) and ET0 (`/Kurven/ET0`) curves from an engine HDF5 and
writes SWMM-5 external time-series files. It converts **out** of the
Expand Down
124 changes: 74 additions & 50 deletions R/get_simulation_results_optim.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
#' @param simulation_names Character vector of simulation run identifiers
#' (e.g. \code{c("s00001", "s00002")}).
#' @param debug print debug messages (default: TRUE)
#' @param lean Logical. If \code{TRUE}, read only the fields consumed by
#' \code{\link{add_overflow_events_and_waterbalance}} -- \code{element$rates},
#' \code{element$water_balance} and \code{connected_area$water_balance} -- and
#' leave \code{meta}/\code{states} (both sides) and \code{connected_area$rates}
#' as \code{NULL}. This keeps per-run memory and I/O minimal when each run is
#' thinned to its optimisation row immediately instead of collecting every
#' run's full results first. Defaults to \code{FALSE} (read everything).
#' @return A named list with one entry per \code{simulation_names}. Each entry is
#' either \code{NULL} (element HDF5 missing) or a nested list:
#' \describe{
Expand Down Expand Up @@ -53,13 +60,16 @@
#' @importFrom stats setNames
#' @importFrom hdf5r H5File
get_simulation_results_optim <- function(paths,
path_list,
path_list,
simulation_names,
debug = TRUE) {

message(sprintf("Reading results files ('%s') for %d model runs",
paste0(c(paths$file_results_hdf5_element, paths$file_results_hdf5_flaeche), collapse = "|"),
length(simulation_names)))
debug = TRUE,
lean = FALSE) {

if (isTRUE(debug)) {
message(sprintf("Reading results files ('%s') for %d model runs",
paste0(c(paths$file_results_hdf5_element, paths$file_results_hdf5_flaeche), collapse = "|"),
length(simulation_names)))
}
stats::setNames(lapply(simulation_names, function(s_name) {

paths <- kwb.utils::resolve(path_list, dir_target = s_name)
Expand All @@ -82,56 +92,70 @@ get_simulation_results_optim <- function(paths,
return(NULL)
}

# Open H5 handles outside catAndRun so on.exit binds to *this* lambda's
# frame, not catAndRun's internal frame. Handles are guaranteed to close
# whichever way the iteration unwinds.
res_hdf5_element <- hdf5r::H5File$new(paths$path_results_hdf5_element, mode = "r")
on.exit(try(res_hdf5_element$close_all(), silent = TRUE), add = TRUE)
# Open + read in an inner function so its on.exit() handle-closing binds to
# *its own* frame and always fires (even on error) before we decide what to
# return. The surrounding tryCatch makes a result file that exists but is
# unreadable -- e.g. the engine crashed mid-write for that scenario --
# behave like a missing file (NULL + warning) instead of aborting the whole
# (possibly parallel) batch. Downstream add_overflow_events_and_waterbalance()
# then emits an NA row for the scenario.
read_result <- function() {
res_hdf5_element <- hdf5r::H5File$new(paths$path_results_hdf5_element, mode = "r")
on.exit(try(res_hdf5_element$close_all(), silent = TRUE), add = TRUE)

res_hdf5_flaeche <- if (has_flaeche) {
h <- hdf5r::H5File$new(paths$path_results_hdf5_flaeche, mode = "r")
on.exit(try(h$close_all(), silent = TRUE), add = TRUE)
h
} else {
if (isTRUE(debug)) {
message(sprintf(
"No connected_area H5 for %s ('%s') -> connected_area = NULL",
s_name, paths$path_results_hdf5_flaeche
))
res_hdf5_flaeche <- if (has_flaeche) {
h <- hdf5r::H5File$new(paths$path_results_hdf5_flaeche, mode = "r")
on.exit(try(h$close_all(), silent = TRUE), add = TRUE)
h
} else {
if (isTRUE(debug)) {
message(sprintf(
"No connected_area H5 for %s ('%s') -> connected_area = NULL",
s_name, paths$path_results_hdf5_flaeche
))
}
NULL
}
NULL
}

kwb.utils::catAndRun(
messageText = sprintf("(%d/%d)) Reading results files for model run %s",
which(simulation_names == s_name),
length(simulation_names),
paths$dir_target_output),
expr = {
element <- list(
meta = kwb.raindrop::read_hdf5_scalars(res_hdf5_element[["Metainfo"]],
numeric_only = FALSE),
rates = kwb.raindrop::read_hdf5_timeseries(res_hdf5_element[["Raten"]]),
water_balance = kwb.raindrop::read_hdf5_scalars(res_hdf5_element[["Wasserbilanz"]]),
states = kwb.raindrop::read_hdf5_timeseries(res_hdf5_element[["Zustandsvariablen"]])
)

connected_area <- if (!is.null(res_hdf5_flaeche)) {
list(
meta = kwb.raindrop::read_hdf5_scalars(res_hdf5_flaeche[["Metainfo"]],
kwb.utils::catAndRun(
messageText = sprintf("(%d/%d)) Reading results files for model run %s",
which(simulation_names == s_name),
length(simulation_names),
paths$dir_target_output),
expr = {
element <- list(
meta = if (lean) NULL else kwb.raindrop::read_hdf5_scalars(res_hdf5_element[["Metainfo"]],
numeric_only = FALSE),
rates = kwb.raindrop::read_hdf5_timeseries(res_hdf5_flaeche[["Raten"]]),
water_balance = kwb.raindrop::read_hdf5_scalars(res_hdf5_flaeche[["Wasserbilanz"]]),
states = kwb.raindrop::read_hdf5_timeseries(res_hdf5_flaeche[["Zustandsvariablen"]])
rates = kwb.raindrop::read_hdf5_timeseries(res_hdf5_element[["Raten"]]),
water_balance = kwb.raindrop::read_hdf5_scalars(res_hdf5_element[["Wasserbilanz"]]),
states = if (lean) NULL else kwb.raindrop::read_hdf5_timeseries(res_hdf5_element[["Zustandsvariablen"]])
)
} else {
NULL
}

list(element = element, connected_area = connected_area)
},
dbg = debug
)
connected_area <- if (!is.null(res_hdf5_flaeche)) {
list(
meta = if (lean) NULL else kwb.raindrop::read_hdf5_scalars(res_hdf5_flaeche[["Metainfo"]],
numeric_only = FALSE),
rates = if (lean) NULL else kwb.raindrop::read_hdf5_timeseries(res_hdf5_flaeche[["Raten"]]),
water_balance = kwb.raindrop::read_hdf5_scalars(res_hdf5_flaeche[["Wasserbilanz"]]),
states = if (lean) NULL else kwb.raindrop::read_hdf5_timeseries(res_hdf5_flaeche[["Zustandsvariablen"]])
)
} else {
NULL
}

list(element = element, connected_area = connected_area)
},
dbg = debug
)
}

tryCatch(read_result(), error = function(e) {
warning(sprintf(
"Scenario '%s': result HDF5 unreadable ('%s'): %s -- treating as missing (NULL).",
s_name, paths$path_results_hdf5_element, conditionMessage(e)
), call. = FALSE)
NULL
})
}), nm = simulation_names)
}

16 changes: 15 additions & 1 deletion man/get_simulation_results_optim.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions vignettes/index.Rmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: "RainDrop Optimierung – Brute Force"
author: "Michael Rustler"
date: "2026-02-25"
output:
html_document:
toc: true
toc_depth: 3
number_sections: true
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)

sites <- c("Eisenstadt_2005", "Wien", "BadAussee")

# index.html liegt im gleichen Verzeichnis wie der Ordner "brute-force"
base_dir <- "."

design_spaces <- paste0(
"mulde-area_vs_",
c("filter_hydraulicconductivity", "mulde_height", "storage_height")
)

rel <- function(...) file.path(..., fsep = "/")

md_link_line <- function(label, href) sprintf("- [%s](%s)", label, href)

md_list <- function(lines) knitr::asis_output(paste(lines, collapse = "\n"))
```

# Hintergrund

xxx

# Methodik

Die Modellierung erfolgte in R mit dem R Paket [kwb.raindrop](https://github.com/kwb-r/kwb.raindrop).
Das genaue Vorgehen ist für jede Fallstudie im folgenden im R Markdown reproduzierbar
dokumentiert.

```{r brute_force_rmarkdown, echo = FALSE, results='asis'}
for (site in sites) {
cat(sprintf(
"- [%s](%s/workflow_%s.html)\n",
site,
base_dir,
site
))
}
```

# Ergebnisse

Die Ergebnisse für die einjährige Berechnung (Eisenstadt für Jahr 2005) und die
beiden 15 jährigen Zeitreihen (2011-2025) für Wien und Bad Aussee finden sich in
unten stehenden Links:

## Tabellen

```{r brute_force_tabelle, echo = FALSE, results='asis'}
for (site in sites) {
cat(sprintf(
"- [%s](%s/simulation_results_optimisation_%s.html)\n",
site,
base_dir,
site
))
}

```

## CSV

Die in den [obenstehenden Tabellen](#tabellen) dargestellten Ergebnisse können auch als `.csv` Datei
heruntergeladen werden.

```{r brute_force_csv, echo = FALSE, results='asis'}
for (site in sites) {
cat(sprintf(
"- [%s](%s/simulation_results_optimisation_%s.csv)\n",
site,
base_dir,
site
))
}
```

## Interaktive Visualisierungen

### Sensitive Modellparameter

```{r brute_force_plots_main, echo = FALSE, results='asis'}
for (site in sites) {
cat(sprintf(
"- [%s](%s/simulation_results_optimisation_%s_main-effects.html)\n",
site,
base_dir,
site
))
}
```

### Design Spaces

In den nachfolgende Abbildungen wird die **Muldenfläche** (x-Achse) mit
**einem weiteren Parameter** (y-Achse) dargestellt. Diese sind im folgenden:

- ***Muldenhöhe***

- ***Speicherhöhe***

- ***hydraulische Leitfähigkeit*** des Bodenfilters

```{r brute_force_plots_design-spaches, echo = FALSE, results='asis'}
cat("| Design Space |", paste(sites, collapse = " | "), "|\n")
cat("|---|", paste(rep("---", length(sites)), collapse = "|"), "|\n")

for (ds in design_spaces) {

ds_label <- sub("^mulde-area_vs_", "", ds)

row_links <- sapply(sites, function(site) {
sprintf(
"[%s](%s/simulation_results_optimisation_%s_design-space_%s.html)",
ds_label,
base_dir,
site,
ds
)
})

cat("|", ds_label, "|", paste(row_links, collapse = " | "), "|\n")
}
```

### Wasserbilanz

```{r brute_force_plots_water-balance, echo = FALSE, results='asis'}
for (site in sites) {
cat(sprintf(
"- [%s](%s/simulation_results_optimisation_%s_water-balance.html)\n",
site,
base_dir,
site
))
}
```


Loading
Loading