Find the cost-optimal swale design per overflow target
Source:R/optimise_swale_design.R
optimise_swale_design.RdCoordinate-descent optimiser built from a single primitive
(find_min_feasible(), bisection over one parameter): shrink the
expensive lever first (mulde_area), then the cheap one
(mulde_height); the storage layer starts at its smallest level and is
only escalated when the area is stuck at its upper bound. The filter
conductivity is expected to be fixed at the maximum via fixed (it is
cost-free and dominant, see the monotonicity analysis,
https://raindrop.kompetenz-wasser.io/optimisation/monotonicity_analysis/).
Every engine run is cached, so the sweep over all x_targets and both
storage types shares evaluations.
Usage
optimise_swale_design(
run_fn,
x_targets = 0:5,
area_bounds = c(25, 200),
area_tol = 2,
height_bounds = c(100, 300),
height_tol = 10,
storage_spec = default_storage_spec(),
fixed = list(connected_area = 1000, filter_height = 300, filter_hydraulicconductivity =
360, bottom_hydraulicconductivity = 12),
prior_results = NULL,
split_jitter = 0,
max_total_depth = NULL,
cost_rates = default_cost_rates(),
verbose = TRUE
)Arguments
- run_fn
function(params)running one scenario and returning at leastn_overflowsplussum_overflows(mm) oroverflow_volume_m3; typically created withmake_swale_runner().paramsis a named list ofmulde_area,mulde_height,storage_type,storage_heightplus everything infixed.- x_targets
Integer vector of overflow targets (feasible :<=>
n_overflows <= x), default0:5.- area_bounds, area_tol
Search range (m2) and resolution for
mulde_area. The found area sits up to onearea_tolabove the exact feasibility boundary (worst-case cost overshoot roughlyarea_tolx specific cost per m2, i.e. a few percent at the default 2 m2); thanks to the bisection, every halving ofarea_tolcosts only one additional engine run per area search – the cheapest precision lever of this optimiser. It also shrinks the tolerance-artefact part of the finalmulde_heighttrim (the values just below the height maximum at lowx_targets).- height_bounds, height_tol
Search range (mm) and resolution for
mulde_height.- storage_spec
Storage search space per type, see
default_storage_spec(): discretelevels(infiltration box) or continuousbounds+tol(gravel trench).- fixed
Named list of parameters passed unchanged to
run_fn(connected area, filter geometry, kf at maximum, ...). Must containfilter_heightfor the cost model.- prior_results
Optional data.frame with prior (grid) results in the workflow CSV schema, used as warm start (narrows the first area bracket to one grid step).
- split_jitter
Passed to
find_min_feasible(): 0 (default) = deterministic halving; > 0 randomises every bisection split point (Monte-Carlo of the search path – repeated runs with different seeds must agree within the search tolerances).- max_total_depth
Optional analytic depth constraint in mm:
mulde_height + filter_height + storage_height <= max_total_depth(e.g. from DWA-A 138 groundwater clearance or cover requirements). Enforced without any simulation runs.- cost_rates
Unit costs, see
default_cost_rates().- verbose
Print one progress line per solved cell.
Value
Tibble with one row per (storage type, x): the optimal design
(mulde_area, mulde_height, storage_height), its metrics
(n_overflows, overflow_volume_m3, et_pct), cost columns from
compute_costs(), a status ("ok" or "infeasible_within_bounds"),
monotonicity_warning (volume referee) and n_runs_new (fresh engine
runs spent on this cell). All evaluated designs are attached as
attribute "evaluations".
Details
The search order is derived from cost_rates via a
specific-cost proxy (EUR per mm of storage capacity, capacity model
V ~ area * (mulde_height + porosity * storage_height); the layer
porosity comes from storage_spec, see default_storage_spec()):
maximising mulde_height first is optimal for any rates under
this cost model (it costs only excavation, while area pays every
component), and the starting storage level is chosen as the
cheapest level per mm of capacity – the smallest level under the
default rates, a high level when e.g. the storage material is cheap.
Without a porosity entry in storage_spec the legacy order
(smallest level first) is used. The proxy is a first-order
heuristic: it assumes capacity-additive levers and cannot rank
parameters with nonlinear hydraulic effects (e.g. a variable filter
conductivity) – for those, and as the assumption-free cross-check,
use optimise_swale_design_simultaneous(), which carries
cost_rates directly inside its objective.
See also
optimise_swale_design_simultaneous() (alternative: all
parameters at once via penalised Nelder-Mead, as an independent
cross-check of the coordinate descent), find_min_feasible(),
make_swale_runner(), default_storage_spec()