|
35 | 35 | #' - `stage = "aggregated"`: `flowpaths` (sf), `divides` (sf), `network` |
36 | 36 | #' (data.frame, optional). |
37 | 37 | #' - `stage = "ngen"`: `flowpaths` (sf with `fp-` IDs), `divides` (sf with |
38 | | -#' `cat-` IDs). |
| 38 | +#' `cat-` IDs), and optionally `nexus` (data.frame with `nexus_id` / |
| 39 | +#' `nexus_toid`, enabling the `fp -> nexus -> fp` DAG check), `flowlines` |
| 40 | +#' and/or `network` (a data.frame carrying the `So` channel-slope routing |
| 41 | +#' attribute), and `lakes` (sf with a `lake_id` column and waterbody |
| 42 | +#' polygons). When the relevant columns are present, the ngen stage adds: |
| 43 | +#' `slope_valid` and `So_valid` (channel `slope` / `So` must be strictly |
| 44 | +#' positive everywhere they appear, since routing requires it; an all-`NA` |
| 45 | +#' column reports as info rather than failing), and `lake_spatial_consistent` |
| 46 | +#' (every flowpath stamped with a `lake_id` must lie within ~one lake-extent, |
| 47 | +#' 1 km floor, of that lake -- catching `wbareacomi` VAA mis-indexing without |
| 48 | +#' demanding exact geometry overlap). |
39 | 49 | #' |
40 | 50 | #' @return Invisibly a list with `ok` (logical), `stage`, and `checks` |
41 | 51 | #' (named list of per-check results). |
@@ -309,6 +319,75 @@ hf_check_invariants <- function(stage, ..., strict = TRUE, |
309 | 319 | flowpaths, divides, coverage_min) |
310 | 320 | } |
311 | 321 |
|
| 322 | + # -- slope_valid (ported from hfrefactor 2026-06) ------------------------- |
| 323 | + # Channel routing needs a strictly positive slope on every flowpath. All-NA = |
| 324 | + # slope step not run for this build (info); once present, every value must > 0. |
| 325 | + if (!is.null(flowpaths) && "slope" %in% names(flowpaths)) { |
| 326 | + sl <- suppressWarnings(as.numeric(flowpaths$slope)) |
| 327 | + if (all(is.na(sl))) { |
| 328 | + checks$slope_valid <- .hf_info("slope not computed (all NA) for this build") |
| 329 | + } else { |
| 330 | + checks$slope_valid <- .hf_ok( |
| 331 | + sum(is.na(sl)) == 0L && sum(sl < 0, na.rm = TRUE) == 0L && |
| 332 | + sum(sl == 0, na.rm = TRUE) == 0L, |
| 333 | + sprintf("%d null + %d negative + %d zero slope(s) (routing needs slope > 0)", |
| 334 | + sum(is.na(sl)), sum(sl < 0, na.rm = TRUE), sum(sl == 0, na.rm = TRUE))) |
| 335 | + } |
| 336 | + } |
| 337 | + |
| 338 | + # -- So_valid -- channel-bottom slope (routing attr) must be > 0 wherever it |
| 339 | + # lands (flowpaths / flowlines / network attribute table). .calculate_flow_atts |
| 340 | + # floors it; any non-positive So in output is a regression. |
| 341 | + if (!is.null(flowpaths)) { |
| 342 | + So_src <- c( |
| 343 | + if ("So" %in% names(flowpaths)) suppressWarnings(as.numeric(flowpaths$So)), |
| 344 | + if (!is.null(args$flowlines) && "So" %in% names(args$flowlines)) |
| 345 | + suppressWarnings(as.numeric(args$flowlines$So)), |
| 346 | + if (!is.null(args$network) && "So" %in% names(args$network)) |
| 347 | + suppressWarnings(as.numeric(args$network$So))) |
| 348 | + if (length(So_src) > 0L) { |
| 349 | + checks$So_valid <- .hf_ok( |
| 350 | + sum(is.na(So_src)) == 0L && sum(So_src < 0, na.rm = TRUE) == 0L && |
| 351 | + sum(So_src == 0, na.rm = TRUE) == 0L, |
| 352 | + sprintf("%d null + %d negative + %d zero So value(s) (routing needs So > 0)", |
| 353 | + sum(is.na(So_src)), sum(So_src < 0, na.rm = TRUE), |
| 354 | + sum(So_src == 0, na.rm = TRUE))) |
| 355 | + } |
| 356 | + } |
| 357 | + |
| 358 | + # -- lake_spatial_consistent -- the lake_id crosswalk (member_comid -> |
| 359 | + # wbareacomi) is authoritative, but NWM waterbody polygons and reference |
| 360 | + # flowlines are DIFFERENT geometries that rarely overlap, so exact intersection |
| 361 | + # is the wrong test. A stamp is invalid only when PHYSICALLY IMPOSSIBLE: the |
| 362 | + # reach lies more than ~one lake-extent (1 km floor) from its lake (genuine |
| 363 | + # wbareacomi VAA mis-index). |
| 364 | + lakes <- args$lakes |
| 365 | + if (!is.null(flowpaths) && !is.null(lakes) && |
| 366 | + "lake_id" %in% names(flowpaths) && inherits(flowpaths, "sf") && |
| 367 | + inherits(lakes, "sf") && nrow(lakes) > 0L) { |
| 368 | + has_lake <- !is.na(flowpaths$lake_id) & as.character(flowpaths$lake_id) != "" |
| 369 | + if (any(has_lake)) { |
| 370 | + n_bad <- tryCatch({ |
| 371 | + fp_l <- flowpaths[has_lake, ] |
| 372 | + lk_geom <- sf::st_geometry(lakes) |
| 373 | + lk_chr <- as.character(lakes$lake_id) |
| 374 | + ext <- vapply(lk_geom, function(g) { |
| 375 | + b <- sf::st_bbox(g) |
| 376 | + sqrt((b[["xmax"]] - b[["xmin"]])^2 + (b[["ymax"]] - b[["ymin"]])^2) |
| 377 | + }, numeric(1L)) |
| 378 | + li <- match(as.character(fp_l$lake_id), lk_chr) |
| 379 | + d <- as.numeric(sf::st_distance( |
| 380 | + sf::st_geometry(fp_l), lk_geom[li], by_element = TRUE)) |
| 381 | + sum(is.finite(d) & d > pmax(1000, ext[li])) |
| 382 | + }, error = function(e) NA_integer_) |
| 383 | + checks$lake_spatial_consistent <- if (is.na(n_bad)) |
| 384 | + .hf_info("lake_spatial_consistent: not computable (geometry error)") |
| 385 | + else .hf_ok(n_bad == 0L, sprintf( |
| 386 | + "%d flowpath(s) stamped with a lake_id physically too far from it (>1 lake-extent; wbareacomi VAA mis-index)", |
| 387 | + n_bad)) |
| 388 | + } |
| 389 | + } |
| 390 | + |
312 | 391 | checks |
313 | 392 | } |
314 | 393 |
|
|
0 commit comments