Der geplante Optimierer sucht die günstigste Muldenkonfiguration per Bisektion (Intervallhalbierung): “Zu klein” → größer probieren, “reicht” → kleiner probieren. Das funktioniert nur, wenn eine Regel gilt:
Monotonie: Wird das Bauwerk größer (mehr Fläche, mehr Tiefe, mehr Speicher, durchlässigerer Filter), darf es niemals mehr Überlaufereignisse geben als vorher.
Diese Vignette prüft die Regel an allen Nachbarpaaren des
Brute-Force-Rasters (je Standort 576 Läufe, 1 704 Vergleichspaare —
insgesamt 5 112). Verglichen werden immer zwei Läufe, die sich
nur in einem Parameter um eine Stufe unterscheiden.
Zusätzlich geprüft: das Überlaufvolumen in m³
(sum_overflows ist mm Wassersäule über der Muldenfläche,
daher m³ = mm × Muldenfläche / 1000), die Verdunstung sowie zwei
Struktur-Eigenschaften, die der Optimierer nutzt (Ambiguitätsband,
Schwellentreppe, kf-Dominanz).
Diese Vignette nach den drei Workflow-Vignetten
ausführen — sie liest deren Ergebnisdateien
(simulation_results_optimisation_*.csv) aus dem
vignettes/-Verzeichnis.
Die Links folgen der Deploy-Struktur auf dem Server: Diese Analyse,
der Ergebnisbericht und die Detailtabellen liegen unter
…/optimisation/monotonicity_analysis/, die Ergebnisse der
Workflow-Vignetten unter …/optimisation/brute-force/. Damit
die Links auch lokal stimmen, wird diese Vignette in den gleichnamigen
Unterordner gerendert:
(Eingelesen werden die CSVs unverändert aus vignettes/,
wo die Workflows sie ablegen — nur die Links zeigen auf die
Deploy-Orte.)
| Standort | Eingangsdaten (CSV) | Interaktive Ergebnistabelle |
|---|---|---|
| Bad Aussee | CSV | HTML-Tabelle |
| Eisenstadt 2005 | CSV | HTML-Tabelle |
| Wien | CSV | HTML-Tabelle |
Der zusammenfassende, allgemeinverständliche Ergebnisbericht
(Artefakt, Titel: „Monotonie-Analyse RAINDROP“) liegt als index.html direkt neben dieser Seite — als
Verzeichnis-Startseite von
…/optimisation/monotonicity_analysis/. Nicht zu verwechseln
mit dem „Brute-Force“-Linkhub: Dessen Quelle index.Rmd
liegt in vignettes/, sein gerendertes
index.html eine Ebene über dieser Seite.
library(dplyr)
library(ggplot2)
geom_params <- c("mulde_area", "mulde_height", "filter_hydraulicconductivity",
"storage_type", "storage_height")
check_params <- setdiff(geom_params, "storage_type")
results <- lapply(result_files, function(f) {
readr::read_csv(f, show_col_types = FALSE) %>%
# Die Engine liefert die Ueberlaufrate in mm/h bezogen auf die
# Muldenflaeche; sum_overflows ist damit mm Wassersaeule.
# Volumen: m3 = mm x Muldenflaeche / 1000.
mutate(overflow_volume_m3 = sum_overflows * mulde_area / 1000)
})
# Alle Nachbarschritte entlang eines Parameters: alle uebrigen Parameter
# fixieren ("Gruppe"), nach dem Parameter sortieren, jeden Uebergang zum
# Nachbarwert als Schritt ausgeben. Verletzung :<=> dn > 0.
step_table <- function(d, v) {
grp <- setdiff(geom_params, v)
d %>%
group_by(across(all_of(grp))) %>%
arrange(.data[[v]], .by_group = TRUE) %>%
mutate(val_from = lag(.data[[v]]),
val_to = .data[[v]],
n_from = lag(n_overflows),
vol_from = lag(overflow_volume_m3),
et_from = lag(element.WB_Evapotranspiration_),
is_last = row_number() == n()) %>%
ungroup() %>%
filter(!is.na(val_from)) %>%
mutate(dn = n_overflows - n_from,
dvol = overflow_volume_m3 - vol_from,
det = element.WB_Evapotranspiration_ - et_from)
}
steps_all <- purrr::map_dfr(names(results), function(site) {
purrr::map_dfr(check_params, function(v) {
step_table(results[[site]], v) %>% mutate(site = site, param = v)
})
})Plateaus (keine Änderung) sind unkritisch — sie sind die flachen Stufen der Treppenfunktion und entstehen vor allem dort, wo bereits n = 0 erreicht ist. Kritisch sind nur Anstiege (“mehr Überläufe trotz größer”).
step_distribution <- steps_all %>%
group_by(site, param) %>%
summarise(steps = n(),
faellt = sum(dn < 0),
plateau = sum(dn == 0),
steigt = sum(dn > 0),
steigt_anteil_pct = round(100 * mean(dn > 0), 2),
max_sprung = max(dn),
.groups = "drop")
knitr::kable(step_distribution)| site | param | steps | faellt | plateau | steigt | steigt_anteil_pct | max_sprung |
|---|---|---|---|---|---|---|---|
| BadAussee | filter_hydraulicconductivity | 384 | 282 | 102 | 0 | 0.00 | 0 |
| BadAussee | mulde_area | 504 | 398 | 94 | 12 | 2.38 | 1 |
| BadAussee | mulde_height | 384 | 301 | 83 | 0 | 0.00 | 0 |
| BadAussee | storage_height | 432 | 311 | 121 | 0 | 0.00 | 0 |
| Eisenstadt | filter_hydraulicconductivity | 384 | 102 | 282 | 0 | 0.00 | 0 |
| Eisenstadt | mulde_area | 504 | 160 | 344 | 0 | 0.00 | 0 |
| Eisenstadt | mulde_height | 384 | 143 | 241 | 0 | 0.00 | 0 |
| Eisenstadt | storage_height | 432 | 48 | 384 | 0 | 0.00 | 0 |
| Wien | filter_hydraulicconductivity | 384 | 321 | 63 | 0 | 0.00 | 0 |
| Wien | mulde_area | 504 | 441 | 63 | 0 | 0.00 | 0 |
| Wien | mulde_height | 384 | 334 | 50 | 0 | 0.00 | 0 |
| Wien | storage_height | 432 | 208 | 223 | 1 | 0.23 | 1 |
Jede Verletzung wird mit ihrer Volumen-Gegenprobe (in m³) gezeigt: Fällt das Überlaufvolumen am selben Schritt weiter, ist der Anstieg des Zählers ein Artefakt der Ereignistrennung (Pausen > 4 h teilen ein langes Überlaufereignis in zwei gezählte), keine echte Verschlechterung.
violations <- steps_all %>%
filter(dn > 0) %>%
transmute(site, param,
kontext = paste0("h_m ", mulde_height,
" | kf ", filter_hydraulicconductivity,
" | ", storage_type,
ifelse(param == "storage_height", "",
paste0(" ", storage_height))),
von = val_from, nach = val_to,
n = paste0(n_from, " → ", n_overflows),
volumen_m3 = paste0(round(vol_from, 1), " → ",
round(overflow_volume_m3, 1)),
volumen_delta_pct = round(100 * dvol / vol_from, 1),
am_rasterrand = is_last)
DT::datatable(violations, filter = "top",
options = list(pageLength = 25, autoWidth = TRUE))Zur Anschauung dieselbe Bauweise an allen drei Standorten (Muldentiefe 300 mm, kf 360 mm/h, Sickerbox 300 mm): Der Zähler fällt überall steil — nur in Bad Aussee springt er am letzten Rasterschritt von 1 auf 2 (roter Punkt), während das Volumen auch dort weiter fällt.
example_series <- purrr::map_dfr(names(results), function(site) {
results[[site]] %>%
filter(mulde_height == 300, filter_hydraulicconductivity == 360,
storage_type == "infiltration_box", storage_height == 300) %>%
arrange(mulde_area) %>%
mutate(site = site,
verletzung = n_overflows > lag(n_overflows, default = Inf))
})
ggplot(example_series, aes(mulde_area, n_overflows)) +
geom_line(colour = "grey50") +
geom_point(aes(colour = verletzung), size = 2, show.legend = FALSE) +
scale_colour_manual(values = c(`FALSE` = "steelblue4", `TRUE` = "firebrick")) +
scale_y_sqrt() +
facet_wrap(~ site, scales = "free_y") +
labs(title = "Ueberlaufereignisse je Muldenflaeche (Wurzel-Skala)",
x = "Muldenflaeche [m2]", y = "Anzahl Ueberlaufereignisse") +
theme_bw()
ggplot(example_series, aes(mulde_area, overflow_volume_m3)) +
geom_line(colour = "grey50") +
geom_point(colour = "springgreen4", size = 2) +
scale_y_sqrt() +
facet_wrap(~ site, scales = "free_y") +
labs(title = "Ueberlaufvolumen je Muldenflaeche (Wurzel-Skala): faellt ausnahmslos",
x = "Muldenflaeche [m2]", y = "Ueberlaufvolumen [m3]") +
theme_bw()Auch in m³ gerechnet (mm Wassersäule × Muldenfläche / 1000 — entlang der Fläche wächst der Umrechnungsfaktor mit, die Monotonie ist also nicht automatisch übertragbar) gilt das Ergebnis:
volume_check <- steps_all %>%
group_by(site) %>%
summarise(steps = n(),
volumen_steigt = sum(dvol > 1e-9),
plateau = sum(abs(dvol) <= 1e-9),
volumen_faellt = sum(dvol < -1e-9),
.groups = "drop")
knitr::kable(volume_check)| site | steps | volumen_steigt | plateau | volumen_faellt |
|---|---|---|---|---|
| BadAussee | 1704 | 0 | 314 | 1390 |
| Eisenstadt | 1704 | 0 | 1146 | 558 |
| Wien | 1704 | 0 | 220 | 1484 |
et_check <- steps_all %>%
group_by(site, param) %>%
summarise(steigt_pct = round(100 * mean(det > 1e-9), 1),
flach_pct = round(100 * mean(abs(det) <= 1e-9), 1),
faellt_pct = round(100 * mean(det < -1e-9), 1),
.groups = "drop")
knitr::kable(et_check)| site | param | steigt_pct | flach_pct | faellt_pct |
|---|---|---|---|---|
| BadAussee | filter_hydraulicconductivity | 0.0 | 100.0 | 0.0 |
| BadAussee | mulde_area | 100.0 | 0.0 | 0.0 |
| BadAussee | mulde_height | 0.0 | 100.0 | 0.0 |
| BadAussee | storage_height | 0.0 | 100.0 | 0.0 |
| Eisenstadt | filter_hydraulicconductivity | 0.0 | 100.0 | 0.0 |
| Eisenstadt | mulde_area | 100.0 | 0.0 | 0.0 |
| Eisenstadt | mulde_height | 0.0 | 100.0 | 0.0 |
| Eisenstadt | storage_height | 0.0 | 100.0 | 0.0 |
| Wien | filter_hydraulicconductivity | 0.0 | 99.2 | 0.8 |
| Wien | mulde_area | 100.0 | 0.0 | 0.0 |
| Wien | mulde_height | 0.5 | 99.5 | 0.0 |
| Wien | storage_height | 0.0 | 99.3 | 0.7 |
Konsequenz für das Sekundärziel “Verdunstung maximieren”: Der Trade-off Kosten ↔︎ Verdunstung verläuft eindimensional entlang der Muldenfläche — mehr Verdunstung gibt es nur über mehr Fläche.
Für die Bisektion relevant: Gibt es Konfigurationsgruppen, in denen oberhalb der ersten zulässigen Fläche wieder eine unzulässige liegt (Ambiguitätsband)? Und ist die Schwellentreppe a*(x) — die kleinste zulässige Fläche je Überlaufziel x — monoton (ein lockereres Ziel verlangt nie mehr Fläche)?
grp_area <- setdiff(geom_params, "mulde_area")
ambiguity <- purrr::map_dfr(names(results), function(site) {
purrr::map_dfr(0:5, function(x) {
results[[site]] %>%
group_by(across(all_of(grp_area))) %>%
summarise(first_ok = ifelse(any(n_overflows <= x),
min(mulde_area[n_overflows <= x]), NA),
last_bad = ifelse(any(n_overflows > x),
max(mulde_area[n_overflows > x]), NA),
.groups = "drop") %>%
summarise(site = site, x = x,
gruppen = n(),
mit_loesung = sum(!is.na(first_ok)),
ambig = sum(!is.na(first_ok) & !is.na(last_bad) &
last_bad > first_ok),
max_band_m2 = max(c(0, (last_bad - first_ok)[
!is.na(first_ok) & !is.na(last_bad)]), na.rm = TRUE))
})
})
knitr::kable(ambiguity)| site | x | gruppen | mit_loesung | ambig | max_band_m2 |
|---|---|---|---|---|---|
| BadAussee | 0 | 72 | 42 | 0 | 0 |
| BadAussee | 1 | 72 | 60 | 12 | 50 |
| BadAussee | 2 | 72 | 64 | 0 | 0 |
| BadAussee | 3 | 72 | 64 | 0 | 0 |
| BadAussee | 4 | 72 | 64 | 0 | 0 |
| BadAussee | 5 | 72 | 70 | 0 | 0 |
| Eisenstadt | 0 | 72 | 72 | 0 | 0 |
| Eisenstadt | 1 | 72 | 72 | 0 | 0 |
| Eisenstadt | 2 | 72 | 72 | 0 | 0 |
| Eisenstadt | 3 | 72 | 72 | 0 | 0 |
| Eisenstadt | 4 | 72 | 72 | 0 | 0 |
| Eisenstadt | 5 | 72 | 72 | 0 | 0 |
| Wien | 0 | 72 | 36 | 0 | 0 |
| Wien | 1 | 72 | 48 | 0 | 0 |
| Wien | 2 | 72 | 54 | 0 | 0 |
| Wien | 3 | 72 | 62 | 0 | 0 |
| Wien | 4 | 72 | 64 | 0 | 0 |
| Wien | 5 | 72 | 64 | 0 | 0 |
staircase <- purrr::map_dfr(names(results), function(site) {
results[[site]] %>%
group_by(across(all_of(grp_area))) %>%
reframe(x = 0:5,
a_star = sapply(0:5, function(x)
ifelse(any(n_overflows <= x),
min(mulde_area[n_overflows <= x]), NA))) %>%
group_by(across(all_of(grp_area))) %>%
summarise(treppen_verletzungen = {
a <- a_star[order(x)]
a <- a[!is.na(a)]
if (length(a) > 1) sum(diff(a) > 0) else 0L
}, .groups = "drop") %>%
summarise(site = site, branches = n(),
treppen_verletzungen = sum(treppen_verletzungen))
})
knitr::kable(staircase)| site | branches | treppen_verletzungen |
|---|---|---|
| BadAussee | 72 | 0 |
| Eisenstadt | 72 | 0 |
| Wien | 72 | 0 |
Die Filterdurchlässigkeit kostet nichts (sie taucht in
compute_costs() nicht auf). Wenn sie gleichzeitig die
Verdunstung nicht verändert und die Überläufe nie erhöht, kann der
Optimierer sie fest auf das Maximum setzen.
kf_dominance <- purrr::map_dfr(names(results), function(site) {
results[[site]] %>%
group_by(filter_hydraulicconductivity) %>%
summarise(site = site,
mittlere_ET_pct = round(mean(element.WB_Evapotranspiration_), 2),
mittlere_n_overflows = round(mean(n_overflows), 1),
mittleres_volumen_m3 = round(mean(overflow_volume_m3), 1),
.groups = "drop")
})
knitr::kable(kf_dominance)| filter_hydraulicconductivity | site | mittlere_ET_pct | mittlere_n_overflows | mittleres_volumen_m3 |
|---|---|---|---|---|
| 36 | BadAussee | 5.65 | 129.9 | 1165.4 |
| 180 | BadAussee | 5.65 | 73.1 | 800.3 |
| 360 | BadAussee | 5.65 | 65.7 | 766.6 |
| 36 | Eisenstadt | 23.59 | 3.7 | 19.5 |
| 180 | Eisenstadt | 23.59 | 1.5 | 9.3 |
| 360 | Eisenstadt | 23.59 | 1.3 | 8.5 |
| 36 | Wien | 13.13 | 69.0 | 605.3 |
| 180 | Wien | 13.13 | 32.4 | 329.1 |
| 360 | Wien | 13.13 | 25.8 | 284.2 |
Diese Tabelle ist der Startpunkt der Bisektion: das jeweils günstigste zulässige Design aus dem vorhandenen Raster (bei kf = Maximum), je Überlaufziel x und Speichertyp. Der Optimierer verfeinert nur noch im 25-m²-Bracket darunter.
warmstart <- purrr::map_dfr(names(results), function(site) {
d <- results[[site]] %>%
filter(filter_hydraulicconductivity == max(filter_hydraulicconductivity))
purrr::map_dfr(0:5, function(x) {
d %>%
filter(n_overflows <= x) %>%
group_by(storage_type) %>%
slice_min(cost_total, n = 1, with_ties = FALSE) %>%
ungroup() %>%
transmute(site = site, x = x, storage_type, mulde_area, mulde_height,
storage_height, cost_total,
overflow_volume_m3 = round(overflow_volume_m3, 1),
ET_pct = round(element.WB_Evapotranspiration_, 1))
})
})
DT::datatable(warmstart, filter = "top",
options = list(pageLength = 12, autoWidth = TRUE))Stand der letzten vollständigen Auswertung (2026-07-10): 5 112 Vergleichspaare, 13 Verletzungen (0,25 %), alle mit Sprunghöhe genau +1 und fallendem Volumen — Eisenstadt 0, Wien 1 (bei n ≈ 287, irrelevant für x ≤ 5), Bad Aussee 12 (alle am Rasterrand 175 → 200 m², Niveau n = 1). Das Überlaufvolumen stieg in keinem einzigen Vergleich. Die Monotonie-Voraussetzung der Bisektion ist damit erfüllt, abgesichert durch drei Regeln:
first_ok aus dem Raster verfeinern — dort ist
die Grenze an allen drei Standorten eindeutig.sum_overflows weiter
gefallen ist. Ja → bekannter Zähl-Wobble, weiterrechnen. Nein → Warnung
(bisher nie aufgetreten) — der eingebaute Rauchmelder für künftige
Standorte.Beim Hinzufügen eines neuen Standorts: Workflows laufen lassen und diese Vignette erneut rendern. Zeigt Abschnitt 1 Sprünge > +1 oder Abschnitt 3 steigendes Volumen, ist die Bisektion für diesen Standort nicht abgesichert.
save_table_html <- function(df, file, title) {
htmlwidgets::saveWidget(
DT::datatable(df, filter = "top",
options = list(pageLength = 25, autoWidth = TRUE)),
file = file, selfcontained = TRUE, title = title
)
# saveWidget() laesst bei Zielpfaden ausserhalb des Arbeitsverzeichnisses
# den "<name>_files"-lib-Ordner stehen, obwohl die Datei selfcontained
# ist -> redundante Duplikate aufraeumen.
unlink(sub("\\.html$", "_files", file), recursive = TRUE)
}
exports <- list(
mono_step_distribution = list(df = step_distribution,
titel = "Monotonie: Schrittverteilung"),
mono_violations_detail = list(df = violations,
titel = "Monotonie: Verletzungen im Detail"),
mono_volume = list(df = volume_check,
titel = "Monotonie: Ueberlaufvolumen (m3)"),
mono_et = list(df = et_check,
titel = "Monotonie: Verdunstung"),
mono_ambiguity = list(df = ambiguity,
titel = "Ambiguitaetsband je Ziel x"),
mono_staircase = list(df = staircase,
titel = "Schwellentreppe a*(x)"),
mono_kf_dominance = list(df = kf_dominance,
titel = "kf-Dominanz"),
mono_warmstart_designs = list(df = warmstart,
titel = "Warmstart-Designs")
)
# Exporte in den Unterordner des Ergebnisberichts (index.html), damit der
# gesamte Ordner monotonicity_analysis/ als eine Einheit deploybar ist.
out_dir <- "monotonicity_analysis"
dir.create(out_dir, showWarnings = FALSE)
for (name in names(exports)) {
readr::write_csv(exports[[name]]$df, file.path(out_dir, paste0(name, ".csv")))
save_table_html(exports[[name]]$df, file.path(out_dir, paste0(name, ".html")),
title = exports[[name]]$titel)
}
# Den Rmd-Quellcode mit in den Deploy-Ordner kopieren, damit der Link
# "monotonicity_analysis.Rmd" des Ergebnisberichts (index.html) dort
# funktioniert.
invisible(file.copy("monotonicity_analysis.Rmd",
file.path(out_dir, "monotonicity_analysis.Rmd"),
overwrite = TRUE))Die exportierten Detailtabellen liegen neben dieser Seite (im
Unterordner monotonicity_analysis/), jeweils als CSV und
als interaktive HTML-Tabelle:
| Tabelle | Inhalt | CSV | HTML |
|---|---|---|---|
| Schrittverteilung | fällt / Plateau / steigt je Standort × Parameter | CSV | HTML |
| Verletzungen | alle 13 Fälle mit Volumen-Gegenprobe (m³) | CSV | HTML |
| Überlaufvolumen | Monotonie des Volumens in m³ | CSV | HTML |
| Verdunstung | ET-Richtung je Parameter | CSV | HTML |
| Ambiguitätsband | Eindeutigkeit der Zulässigkeitsgrenze je Ziel x | CSV | HTML |
| Schwellentreppe | Monotonie von a*(x) je Branch | CSV | HTML |
| kf-Dominanz | ET / Überläufe / Volumen je kf-Stufe | CSV | HTML |
| Warmstart-Designs | billigstes zulässiges Raster-Design je x und Speichertyp | CSV | HTML |