Changelog
Source:NEWS.md
kwb.raindrop (development version)
New features
New exported plot
plot_cost_vs_evaporation()— third cost view: scatterscost_total(EUR, x) against the element evapotranspiration share (element.WB_Evapotranspiration_, %, y). Points share the overflow-count palette of the sibling plots and are shaped by the storage type (filled square = infiltration box / Sickerbox, filled triangle = gravel trench / Schotterrigol); identical tooltip. Rendered as*_cost-vs-evaporation.htmlin the three case-study vignettes and linked fromvignettes/index.Rmdunder “Kosten vs. Evapotranspiration”.plot_cost_overflow_boxplot()gainsy_var = "cost_per_evap_pct"(y-axis = total cost per percentage point of evapotranspiration above the reference minimum — the lowest evapotranspiration among the scenarios satisfying the validity criterion (n_overflows <= x; fallback: complete run) —, EUR/%; the reference (minimum share, criterion and scenario id) is named on a second title line, andlabel_best = TRUEannotates the evapotranspiration gain"(+NN % Evapotranspiration)"after the price; titles, y-label and themin_costobjective/label follow) andfacet_storage_type = TRUE(two stacked storage-type panels — infiltration box on top, gravel trench below — each with its own best-per-box markers and frontier line;plotly::ggplotly()keeps the split as stacked subplots). When both storage types share one panel (no faceting) the overlaid points are shaped by the storage type like the scatter siblings; faceted panels keep plain circles for readability. The vignettes render the three existing boxplot variants with storage-type panels plus the new*_cost-per-evap-boxplot.html(cheapest EUR/% per class, point size = evapotranspiration), linked fromvignettes/index.Rmdunder “Boxplot – Kosten je Prozent Evapotranspiration”.plot_cost_vs_overflow_volume()points are now also shaped by the storage type (square/triangle, own legend under the colour legend).The shared cost tooltip gains a derived “Kosten je % Evapotranspiration (über Min. von X %) [€/%]” line right below the total cost: the total cost per percentage point of element evapotranspiration above the reference minimum (the lowest evapotranspiration among the scenarios satisfying the validity criterion
n_overflows <= x; fallback: complete run) — the baseline comes “for free”, only the gain is paid for. The reference value is named in the line; “-” at or below the minimum. Shown consistently in both cost scatters and all cost boxplot variants. German labels consistently say “Evapotranspiration” instead of “Verdunstung” throughout.New usable storage volume of the storage layer:
storage_volume_m3 = mulde_area * storage_height/1000 * (thetaS - thetaFC)(usable porosity 0.95 infiltration box / 0.3 gravel trench). The vignettes add the column to the parameter grid (grid datatable + results CSV) and the tooltips of all scenario plots show it as “Nutzbares Speichervolumen [m³]” — the cost scatters and boxplots (right below the storage type), the water-balance trade-off plot and the design-space plots (there sourced fromsim_results, since the plotting grid drops the helper columns) — computed on the fly from thestorage_theta*columns for existing result sets without the column. In the “Variierende Parameter” block the raw storage_type values are now translated too (Speichertyp=Schotterrigolinstead of=gravel_trench; shared value labels with theplot_main_effects()storage-type panel).The boxplots’ point-size legend keys match the plotted markers: with storage-type shapes in use (no faceting) they are drawn with the grey square/triangle instead of the default circle; the faceted variants use circular points and matching circular keys.
Storage-type names in legends and facet strips are now the short, language-specific ones (“Sickerbox” / “Schotterrigol” for
lang = "de", “Infiltration box” / “Gravel trench” for"en"); only the bold tooltip line keeps the long bilingual form.All cost plots now carry a caption naming the unit-cost rates they were computed with (new exported
cost_rates_caption(), built from [default_cost_rates()]: Aushub 70 €/m³ · Profilierung + Begrünung 10 €/m² · Bodenfilter 200 €/m³ · Sickerbox 350 €/m³ · Schotterrigol 50 €/m³, incl. installation). ggplot renders it at the bottom of the PDFs (captionargument,""to drop); sinceplotly::ggplotly()drops captions, the new exportedplotly_add_caption()re-adds it as a bottom annotation in the interactive HTMLs (wired up in all three vignettes).New exported helper
plotly_split_legend()— cleans up the interactive legends:plotly::ggplotly()flattens colour + shape into unreadable"(0,Sickerbox / Infiltration box)"tuple entries. The helper rebuilds the legend from legend-only keys with unambiguous glyphs: one neutral circle per overflow class in the class colour (clicking toggles both storage types of the class) plus two neutral grey square/triangle keys under a “Speichertyp” / “Storage type” group title (skippable viaadd_shape_legend = FALSEfor faceted plots) — a coloured square/triangle key would wrongly suggest one specific (colour, type) combination. The storage-type keys are individually clickable (a JavaScript handler toggles all traces with that marker symbol, since the traces’ only legend group is taken by the overflow class); the overlapping combined legend title is removed and the legend moves to a vertical layout on the right. Applied in all three vignettes to the cost-vs-overflow, cost-vs-evaporation, water-balance and design-space HTMLs.plot_wb_tradeoff_overflows()no longer crashes with “Can’t combinemulde_areaand storage_type” on two-type grids: its inline copy of the varying-parameter tooltip block was replaced by the shared build_varying_param_html()helper (the tooltip parameter names are now translated viadefault_param_labels(), as in the cost plots). When the results carry astorage_typecolumn, its points are shaped by the storage type (square/triangle) and the tooltip names the type; single-type result sets plot as before.plot_valid_design_space()gainsfacet_storage_type— two stacked storage-type panels with free y-scales, so disjoint per-type levels (storage_height: 300–1200 mm boxes vs. 900–3600 mm trenches) fill their own panel; duplicate-based alpha is then counted per panel and the points stay plain circles (the strips name the type). Without faceting, points are shaped by the storage type wheneverstorage_typevaries. The vignettes facet both design-space blocks.plot_main_effects()now supports character parameters (the pivot previously failed on mixed types), keeps numeric level ordering (“500” no longer sorts after “1000”), and rendersstorage_typeas its own panel with display names (Sickerbox/Schotterrigol or Infiltration box/Gravel trench). The vignettes addstorage_typeto the main-effects parameter set.Fixed
build_varying_param_html()(the shared tooltip helper): it errored with “Can’t combinestorage_heightand storage_type” as soon as the character column storage_typevaried across scenarios — i.e. for every grid sweeping both storage types (values_transform = as.characterin the pivot). Numbers in the varying-parameters tooltip block are now formatted element-wise, so one decimal-valued parameter no longer forces trailing “.00” onto every other value. The vignettes additionally drop the storage_theta* helper columns (fully determined bystorage_type) from the plottingparam_grid, keeping tooltips lean.New exported plot
plot_cost_vs_overflow_volume()— companion toplot_wb_tradeoff_overflows()for cost-aware optimisation. Scatterscost_total(EUR) against overflow volume (m³, computed fromsum_overflows[mm] andmulde_area[m²]), points coloured discretely byn_overflowswith the same0..x / ">x"palette and top legend as the water-balance plot. Both cost plots report the share of scenarios meeting the validity criterion (n_overflows≤x) in the plot title (e.g.(39 % mit ≤ 5 Überläufen)), since ggplotly drops ggplot subtitles. The plotly tooltip carries the element water balance (evapotranspiration, infiltration, overflow — all in %), the chosen storage type on its own bold line (bilingual,Sickerbox / Infiltration boxorSchotterrigol / Gravel trench), the full cost breakdown (excavation, profiling, filter, storage, total) plus the varyingparam_gridentries (translated viadefault_param_labels()). Rendered as HTML (*_cost-vs-overflow-volume.html) in the three case-study vignettes and linked fromvignettes/index.Rmdunder a new “Kosten vs. Überlaufvolumen” section.New exported plot
plot_cost_overflow_boxplot()— boxplot of the total construction cost (EUR, y) per number of overflow events (x), with the individual scenarios overlaid as jittered points whose size scales with the overflow volume (m³; the size scale is calibrated to the valid region so the many-overflow outliers do not shrink the valid-region points away, and a minimum size keeps every point visible). Counts up to the thresholdx(=max_n_overflows, as in the sibling plots) each get their own box; higher counts collapse into a single">x"catch-all box (furthest right, red), keeping the axis readable for the long-tailed 15-year runs (Wien / Bad Aussee reach several hundred overflow events); the">x"box highlights the scenario with the fewest overflow events abovex. One best scenario per box is highlighted with a black-outlined diamond in that box’s group colour (so its tooltip inherits the group colour), and the best scenarios of all boxes are joined by a frontier line (mark_best/connect_best). The point tooltip is identical toplot_cost_vs_overflow_volume().best_bypicks the objective (cost as tie-breaker) —"min_cost"(cheapest),"min_overflow"(smallest overflow volume) or"max_evapotranspiration"(highest evapotranspiration) — so the three variants trace three different frontier lines;label_bestannotates the marker (overflow volume + share"NN m³ / NN %", or evapotranspiration"NN %");size_byscales the points by overflow volume (default) or evapotranspiration. The three case-study vignettes render all three variants (*_cost-by-overflows-boxplot-cheapest.html,*-min-overflow.html,*-max-evap.html), each linked fromvignettes/index.Rmdunder the grouped “Kosten” section.New exported helper
default_param_labels()— German / English, unit-carrying labels for the parameter-grid columns. The “varying parameters” block of both cost-plot tooltips now shows e.g.Muldenfläche [m²]=125instead of the rawmulde_area=125; passparam_labels =to the plot functions to override.
Consistency
Non-ASCII characters in R code are now unicode-escaped: all string literals in
plot_cost_vs_overflow_volume()and in the vignette code chunks use\uxxxxescapes (rendered labels unchanged), and the few non-ASCII code comments were rewritten in plain ASCII. Markdown prose keeps UTF-8 (escapes are not interpreted there).Eisenstadt 2005 (
workflow_eisenstadt-2005.Rmdandworkflow_eisenstadt-2005_neu.Rmd) now pins//Massnahmenelemente/Mulde_Rigole/Parameter_Evapotranspiration/LAI_LeafAreaIndex = 3.9(Hörnschemeyer grass value) so all four case-study vignettes operate on the same LAI baseline. Wien uses it as one of the sweep levels (c(3.9, 8.5)), Bad Aussee identical to Wien.
Bug fixes
vignettes/workflow_eisenstadt-2005.Rmdnow pipes the joined optimisation results throughkwb.raindrop::compute_costs()like the Wien and Bad Aussee workflows already did. Without it the vignette’splot_cost_vs_overflow_volume()call aborted with “missing column(s): cost_excavation, …” — the cost-vs-overflow-volume PDF/HTML was never produced and the exported CSV lacked the cost columns.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: itwarning()s, names the scenario, and returnsNULLinstead of throwing. Because the Wien / Bad Aussee / Eisenstadt workflows now read results per run insiderun_one(), a single unreadable file used to abort the entirefuture_lapplybatch (seen as anH5File.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.Rmdandvignettes/workflow_badaussee.Rmdnow 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 minimal vignette’s timeseries-info summary now labels ET0 as mm/h and recovers its total viavalue * period_h.
New features
-
The Wien and Bad Aussee workflows now thin each run to its optimisation row inside
run_one()(viaget_simulation_results_optim(..., lean = TRUE)-
add_overflow_events_and_waterbalance()) andrun_scenarios()returns those one-row tibbles for a finaldplyr::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 aleanargument. WhenTRUEit reads only the fields consumed downstream (element$rates,element$water_balance,connected_area$water_balance) and leaves the unusedmeta/statesandconnected_area$ratesasNULL, minimising per-run memory and I/O. Its intro message is now gated behinddebug.inst/scripts/prepare_eisenstadt_swmm_timeseries.Rextracts 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 engine’s mm/h convention (rain → mm per interval for[RAINGAGES]VOLUME, ET0 → mm/day for[EVAPORATION]). Pre-generated files for the bundled Eisenstadt 2005 template ship underinst/extdata/models/eisenstadt-2005/swmm/together with a README that documents the mm/h-vs-mm/day pitfall: the kernel reads/Kurven/ET0as mm/h, so a daily ET0 (mm/d) written there without dividing by 24 is integrated 24× too high.download_engine()fetches the Tandler “Regenwasserbewirtschaftung” Windows executable from the companion repositoryKWB-R/kwb.raindrop.binariesand caches it undertools::R_user_dir("kwb.raindrop", "cache")/<version>/. Multiple engine versions can coexist side-by-side because the version is encoded in the release tag (engine-<YYYY-MM-DD>), not in the asset filename. The download is atomic (temp file + rename) and rejects obviously broken responses (< 1 KB ⇒ likely a 404 HTML page).compute_costs()anddefault_cost_rates()attach a per-scenario construction-cost breakdown (cost_excavation,cost_profiling,cost_filter,cost_storage,cost_total) to a parameter grid using the Austrian unit-cost rates supplied by Johannes Leimgruber (OeStaP, 2026-03-27). Passstorage_type = "infiltration_box"(Sickerbox, default, ~95 % porosity) or"gravel_trench"(Schotterrigol, ~30 % porosity) — or a per-rowstorage_typecolumn on the grid — to switch the storage layer. The new columns are wired into theexample_wien_minimal,workflow_wienandworkflow_badausseevignettes so the solution-space datatables become filterable and sortable by cost.New vignette
example_wien_minimal: a self-contained smoke test of the full input → engine → results loop on Wien. Now extended into an ET-diagnostics grid that sweeps three engine switches —keineEvapotranspirationBeiRegen,Hoernschemeyer_aktivand theET0ref_GrasReferenzverdunstungfactor (0,1,100) — at Daniel’s reference geometry (12 scenarios total). Daniel’s three XLSX-review corrections (Dach/Evapotranspiration_aktiv = 0,EvapPond = 0,LAI = 3.9) were applied briefly between PRs #11 and the one introducing this NEWS entry but made the Tandler engine return Status 1 for every scenario, so they are reverted for now. They will be re-introduced one-by-one as sweep dimensions in a follow-up diagnostic vignette so the failing combination can be isolated. After the model loop the per-scenario*.h5inputs are dumped to a single XLSX (raindrop_wien_minimal_params.xlsx) with one sheet per scenario plus abasesheet for the un-modified template, atimeseries_infosheet summarising the rain / ET0 series fed to every run (identical across scenarios), and anapplied_settingssheet listing the diff of every key the package writes on top ofbase.h5per scenario. Prints a complete static-parameter overview frombase.h5for review of every default that drives the model. Designed to render on Windows CI; the four heavy case-study vignettes only render their parameter grids on CI and skip the model runs.
Helper scripts
-
inst/scripts/prepare_wien_swmm_timeseries.Rconverts the shipped Wien rainfall (10-minute, mm) and reference ET0 (daily, mm/day) series to SWMM-5 external time-series files (wien_rain.dat,wien_et0.dat) for direct import into a SWMM[TIMESERIES]/[RAINGAGES]/[EVAPORATION]block. Output directory defaults totempdir(); passout_dir(R) or a positional CLI argument (Rscript) to redirect.
Inputs and data shipping
Per-scenario input data ships under
inst/extdata/models/<scenario>/for Wien, Bad Aussee and Eisenstadt 2005 (base.h5HDF5 model templates plusrain.csv.gzandet.csvtime series for the two GeoSphere-Austria sites). Vignettes read inputs viasystem.file()and write model outputs to atempdir()scratch root, so they are hermetic and reproducible.Rain timeseries are shipped gzipped (
rain.csv.gz);readr::read_csv()reads the compressed files transparently. Totalinst/extdata/footprint is ~6.8 MB (down from ~53 MB raw).Provenance documented in
inst/extdata/SOURCES.mdand an “Input data” section in each rendered vignette: precipitation and evapotranspiration come from GeoSphere Austria (Österreichischer Wetterdienst, formerly ZAMG); HDF5 templates are produced with the Tandler engine.
CI / packaging
GitHub Actions workflows for Claude Code (
@claudemention bot and automatic PR review) added; both pinned to Opus 4.7 withthink hardreasoning on the review prompt.R-CMD-checkmatrix restricted towindows-latest(devel/oldrel/release) — the calculation engine is a Windows.exe, so non-Windows runners cannot exercise the workflow. Aligns with the existingtest-coverageandpkgdownjobs.Bumped
actions/checkoutto v5 and pinnedactions/upload-artifactto v4 (Node.js 24 readiness ahead of the 2026-09-16 Node 20 removal).
Dependency hygiene
-
tidyr,rlang: moved toImports(used in package code). -
plotly: moved fromImportstoSuggests(only used in vignettes). -
htmlwidgets,readr,writexl: added toSuggests(used in vignettes).
Bug fixes
get_simulation_results_optim()andget_simulation_results_optim_parallel()no longer returnNULLwhen only the connected-area H5 (Dach.h5) is missing; they now return a partial result withconnected_area = NULLwhile still populating the element side. This unblocks scenarios where//Massnahmenelemente/Dach/Berechnungsparameter/Evapotranspiration_aktivis0and the engine consequently skips writing Dach.h5.add_overflow_events_and_waterbalance()tolerates per-scenarioNULLand missing components (element,connected_area,*$water_balance,element$rates). Affected scenarios still produce a row of the output tibble with the available metrics computed and the missing columns left asNA.add_overflow_events_and_waterbalance()now fabricates anNA-filled column stub when one side’s water balance is missing while the other side has data, by mirroring the populated side’s variable names. Previously the missing side’s columns were dropped entirely (dplyr::bind_rows()only adds columns that at least one scenario contributes), which left the results table with noconnectedarea.*_columns at all when every scenario disabled roof ET. The mirror keeps the column structure visible in the rendered datatable and the function emits one summarymessage()per fallback path (instead of one per scenario) naming all affected scenarios, so the user can match the diagnostic to the all-NA rows.New exported
default_canonical_wb_variables()returns the canonical set of water-balance variable names the Tandler engine writes (WB_Regen,WB_Evapotranspiration,WB_InfiltrationNetto,WB_Oberflaechenablauf_Ueberlauf,WB_Oberflaechenablauf_Verschaltungen). All five vignettes now pass it ascanonical_variables = default_canonical_wb_variables()toadd_overflow_events_and_waterbalance(), so the rendered datatables keep the expectedelement.WB_*_andconnectedarea.WB_*_columns even when every scenario in a batch isNULL(e.g. the engine returns Status 1 for every input and writes no result HDF5).example_wien_minimalvignette: the per-scenariorun_one()helper now wraps the H5 input write + engine call intryCatchand passesstrict = FALSE+scalar_strategy = "first"toh5_write_values(). A scenario that errors during write or whose engine returns a non-zero status no longer aborts the wholerun_scenariosloop; the failure is reported viamessage()and the remaining scenarios still execute, producing a results datatable with NA in the result columns for the failed rows (paired with the new mirror-stub above).R/plot_hpond_vs_ref.R: replace literal▲glyph in the caption with▲so the source file is ASCII-only (R-CMD-check WARNING).R/read_hdf5_timeseries.R: wrap array-indexing notation ([1, ],[2..k, ],[, 1],[, 2..k]) in backticks so roxygen2’s markdown parser does not turn them into broken\link{...}entries.