---
title: "Monotonie-Check der Optimierungsergebnisse"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Monotonie-Check der Optimierungsergebnisse}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE, eval = TRUE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 3.2
)

# Diese Vignette wertet die Ergebnis-CSVs der drei Workflow-Vignetten aus
# (workflow_badaussee, workflow_eisenstadt-2005, workflow_wien). Sie ist
# bedingt: alle Analyse-Chunks laufen nur, wenn die CSVs neben dieser
# Datei liegen -- also NACH einem lokalen Lauf der Workflows (Windows +
# Engine). Auf CI/GitHub Actions wird nur der Text gerendert.
result_files <- c(
  BadAussee  = "simulation_results_optimisation_BadAussee.csv",
  Eisenstadt = "simulation_results_optimisation_Eisenstadt_2005.csv",
  Wien       = "simulation_results_optimisation_Wien.csv"
)
results_available <- all(file.exists(result_files))

# Auf GH Actions (pkgdown-Deploy) liegt der Ergebnisbericht (index.html)
# nicht neben dem gerenderten Artikel -- der Link darauf wird dann nicht
# gesetzt (nur beim lokalen Rendern in den Ordner monotonicity_analysis/).
is_ghactions <- tolower(Sys.getenv("GITHUB_ACTIONS")) == "true" ||
  tolower(Sys.getenv("CI")) %in% c("true", "1", "yes")
```

## Worum geht es?

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&#8239;704 Vergleichspaare — insgesamt 5&#8239;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).

## Voraussetzungen

Diese Vignette **nach den drei Workflow-Vignetten ausführen** — sie liest
deren Ergebnisdateien (`simulation_results_optimisation_*.csv`) aus dem
`vignettes/`-Verzeichnis.

```{r availability_note, echo = FALSE, results = 'asis', eval = !results_available}
cat(paste0(
  "> **Hinweis:** Es wurden keine Ergebnisdateien gefunden — die ",
  "Analyse-Chunks wurden übersprungen. Bitte zuerst die Workflow-Vignetten ",
  "(Bad Aussee, Eisenstadt 2005, Wien) lokal ausführen (Windows + Engine) ",
  "und diese Vignette anschließend erneut rendern.\n"
))
```

### Verwendete Dateien und Ablage-Struktur

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:

```r
rmarkdown::render("monotonicity_analysis.Rmd",
                  output_dir = "monotonicity_analysis")
```

(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](../brute-force/simulation_results_optimisation_BadAussee.csv) | [HTML-Tabelle](../brute-force/simulation_results_optimisation_BadAussee.html) |
| Eisenstadt 2005 | [CSV](../brute-force/simulation_results_optimisation_Eisenstadt_2005.csv) | [HTML-Tabelle](../brute-force/simulation_results_optimisation_Eisenstadt_2005.html) |
| Wien | [CSV](../brute-force/simulation_results_optimisation_Wien.csv) | [HTML-Tabelle](../brute-force/simulation_results_optimisation_Wien.html) |

```{r report_link, echo = FALSE, results = 'asis', eval = !is_ghactions}
cat(paste0(
  "Der zusammenfassende, allgemeinverständliche Ergebnisbericht (Artefakt, ",
  "Titel: „Monotonie-Analyse RAINDROP“) liegt als [index.html](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.\n"
))
```

```{r libraries, eval = results_available, message = FALSE}
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)
  })
})
```

## 1 Schrittverteilung: fällt, Plateau oder steigt?

Plateaus (keine Änderung) sind unkritisch — sie sind die flachen Stufen der
Treppenfunktion und entstehen vor allem dort, wo bereits n&#8239;=&#8239;0
erreicht ist. Kritisch sind nur Anstiege ("mehr Überläufe trotz größer").

```{r step_distribution, eval = results_available}
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)
```

## 2 Die Verletzungen im Detail

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 &gt; 4&#8239;h teilen ein langes
Überlaufereignis in zwei gezählte), keine echte Verschlechterung.

```{r violations, eval = results_available}
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&#8239;mm, kf 360&#8239;mm/h, Sickerbox 300&#8239;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.

```{r example_series, eval = results_available, warning = FALSE}
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()
```

## 3 Gegenprobe: das Überlaufvolumen (m³)

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:

```{r volume, eval = results_available}
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)
```

## 4 Verdunstung: hängt nur an der Fläche

```{r et, eval = results_available}
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)
```

Konsequenz für das Sekundärziel "Verdunstung maximieren": Der Trade-off
Kosten&#8239;&#8596;&#8239;Verdunstung verläuft eindimensional entlang der
Muldenfläche — mehr Verdunstung gibt es nur über mehr Fläche.

## 5 Ambiguitätsband und Schwellentreppe

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)?

```{r ambiguity, eval = results_available}
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)

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)
```

## 6 kf-Dominanz: der Filter ist ein Gratis-Hebel

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.

```{r kf_dominance, eval = results_available}
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)
```

## 7 Warmstart: billigstes zulässiges Raster-Design je Ziel x

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.

```{r warmstart, eval = results_available}
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))
```

## Fazit: Regeln für den Optimierer

Stand der letzten vollständigen Auswertung (2026-07-10): 5&#8239;112
Vergleichspaare, 13 Verletzungen (0,25&#8239;%), alle mit Sprunghöhe genau +1
und fallendem Volumen — Eisenstadt 0, Wien 1 (bei n&#8239;&#8776;&#8239;287,
irrelevant für x&#8239;&#8804;&#8239;5), Bad Aussee 12 (alle am Rasterrand
175&#8239;&#8594;&#8239;200&#8239;m², Niveau n&#8239;=&#8239;1). Das
Überlaufvolumen stieg in keinem einzigen Vergleich. **Die
Monotonie-Voraussetzung der Bisektion ist damit erfüllt**, abgesichert durch
drei Regeln:

1. **Grid-Warmstart:** Bisektion nur im 25-m²-Bracket um das bekannte
   `first_ok` aus dem Raster verfeinern — dort ist die Grenze an allen drei
   Standorten eindeutig.
2. **Rand-Guard statt Abbruch:** Fällt der obere Intervallrand nur um +1
   über das Ziel (n = x&#8239;+&#8239;1), erst Cache-/Rasterpunkte darunter
   prüfen, bevor "keine Lösung" gemeldet wird.
3. **Volumen als Schiedsrichter:** Bei jedem nicht-monotonen Flip prüfen, ob
   `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 &gt; +1 oder Abschnitt 3
steigendes Volumen, ist die Bisektion für diesen Standort nicht abgesichert.

```{r export, eval = results_available, message = FALSE}
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](mono_step_distribution.csv) | [HTML](mono_step_distribution.html) |
| Verletzungen | alle 13 Fälle mit Volumen-Gegenprobe (m³) | [CSV](mono_violations_detail.csv) | [HTML](mono_violations_detail.html) |
| Überlaufvolumen | Monotonie des Volumens in m³ | [CSV](mono_volume.csv) | [HTML](mono_volume.html) |
| Verdunstung | ET-Richtung je Parameter | [CSV](mono_et.csv) | [HTML](mono_et.html) |
| Ambiguitätsband | Eindeutigkeit der Zulässigkeitsgrenze je Ziel x | [CSV](mono_ambiguity.csv) | [HTML](mono_ambiguity.html) |
| Schwellentreppe | Monotonie von a*(x) je Branch | [CSV](mono_staircase.csv) | [HTML](mono_staircase.html) |
| kf-Dominanz | ET / Überläufe / Volumen je kf-Stufe | [CSV](mono_kf_dominance.csv) | [HTML](mono_kf_dominance.html) |
| Warmstart-Designs | billigstes zulässiges Raster-Design je x und Speichertyp | [CSV](mono_warmstart_designs.csv) | [HTML](mono_warmstart_designs.html) |
