diff --git a/DESCRIPTION b/DESCRIPTION index d0aeb02..d0c5dbb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ Imports: sf, terra (>= 1.4-21), philentropy (>= 0.6.0), + units Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.3 LinkingTo: diff --git a/NAMESPACE b/NAMESPACE index c29ed0f..b7d2867 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,12 +1,15 @@ # Generated by roxygen2: do not edit by hand +S3method(plot,sc_slic_convergence) export(sc_metrics_global) export(sc_metrics_pixels) export(sc_metrics_supercells) -export(sc_plot_iter_diagnostics) export(sc_slic) +export(sc_slic_convergence) export(sc_slic_points) export(sc_slic_raster) export(sc_tune_compactness) export(supercells) +export(use_adaptive) +export(use_meters) useDynLib(supercells, .registration = TRUE) diff --git a/NEWS.md b/NEWS.md index 62fe7f2..f628f9c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ # supercells 1.9 * Added `outcomes` argument to `sc_slic()`, `sc_slic_points()`, and `sc_slic_raster()`; replaces `metadata` for controlling returned fields +* Iteration diagnostics API redesigned: `iter_diagnostics` and `sc_plot_iter_diagnostics()` replaced by `sc_slic_convergence()` with a `plot()` method +* Added `use_meters()` for map-distance step values (replacing `in_meters()`) +* Added `use_adaptive()` for adaptive compactness mode (replacing `compactness = "auto"`) * Added experimental `sc_merge_supercells()` for adjacency-constrained greedy merging * Added `sc_dist_vec_cpp()` (C++ distance wrapper) to support merge utilities * Documentation and vignettes updated (pkgdown refresh, new articles, and revised examples) diff --git a/R/helpers-chunks.R b/R/helpers-chunks.R index cc8b599..654060d 100644 --- a/R/helpers-chunks.R +++ b/R/helpers-chunks.R @@ -28,7 +28,7 @@ r = r + max_id } n_centers = nrow(chunks[[i]][["centers"]]) - if (!is.na(n_centers) && n_centers > 0) { + if (n_centers > 0) { max_id = max_id + n_centers } rasters[[i]] = r @@ -143,14 +143,6 @@ as.integer(ceiling(nrows / step) * ceiling(ncols / step)) } -# deterministic per-chunk id offsets based on expected supercell counts -.sc_chunk_offsets = function(chunk_ext, step) { - expected = .sc_chunk_expected_max_ids(chunk_ext, step) - offsets = cumsum(c(0L, expected[-length(expected)])) - storage.mode(offsets) = "double" - offsets -} - # choose a compact integer datatype based on expected max id .sc_chunk_id_datatype = function(max_id) { if (max_id <= 255) { diff --git a/R/helpers-general.R b/R/helpers-general.R index 0b71fb8..aa8200b 100644 --- a/R/helpers-general.R +++ b/R/helpers-general.R @@ -1,8 +1,3 @@ -# check if raster is in memory -.sc_util_in_memory = function(x){ - terra::sources(x) == "" -} - # normalize input to SpatRaster .sc_util_prep_raster = function(x) { if (inherits(x, "SpatRaster")) { @@ -59,3 +54,51 @@ return(vec) } } + +# convert user step to internal cell units and return scaling metadata +.sc_util_step_to_cells = function(x, step) { + if (!inherits(step, "units")) { + return(list(step = step, step_meta = step, spatial_scale = 1, step_scale = step)) + } + + if (!identical(as.character(units::deparse_unit(step)), "m")) { + stop("A units-based 'step' must use meters ('m')", call. = FALSE) + } + if (terra::is.lonlat(x)) { + stop("A units-based 'step' requires a projected CRS; project the input raster first", call. = FALSE) + } + + res = terra::res(x) + if (!isTRUE(all.equal(res[[1]], res[[2]]))) { + warning("Map-unit step requires square cells; res(x) has different x/y resolution", call. = FALSE) + } + + crs_units = sf::st_crs(terra::crs(x))$units_gdal + if (is.null(crs_units) || is.na(crs_units) || !nzchar(crs_units)) { + stop("The raster CRS has unknown linear units; cannot use units-based 'step'", call. = FALSE) + } + crs_units_lc = tolower(trimws(crs_units)) + if (!(crs_units_lc %in% c("meter", "metre", "m"))) { + stop("A units-based 'step' requires a projected CRS with meter units", call. = FALSE) + } + + step_m = as.numeric(units::drop_units(step)) + step_cells = max(1, round(step_m / res[[1]])) + step_meta = units::set_units(step_cells * res[[1]], "m", mode = "standard") + spatial_scale = res[[1]] + + list(step = step_cells, step_meta = step_meta, + spatial_scale = spatial_scale, step_scale = step_cells * spatial_scale) +} + +# normalize compactness input for slic/metrics workflows +.sc_util_prep_compactness = function(compactness) { + if (is.numeric(compactness) && length(compactness) == 1 && !is.na(compactness) && compactness > 0) { + return(list(value = compactness, adaptive = FALSE, adaptive_method = NULL)) + } + + if (inherits(compactness, "sc_adaptive")) { + return(list(value = 0, adaptive = TRUE, adaptive_method = compactness$method)) + } + stop("The 'compactness' argument must be a single positive number or use_adaptive()", call. = FALSE) +} diff --git a/R/helpers-metrics.R b/R/helpers-metrics.R index c4419ab..2da4606 100644 --- a/R/helpers-metrics.R +++ b/R/helpers-metrics.R @@ -1,6 +1,31 @@ # .sc_metrics_prep: normalize inputs and parameters for metrics functions # Inputs: raster and supercells; outputs include prepared matrices and metadata # Handles missing metadata by deriving centers and ids from geometry + +.sc_metrics_resolve_dist_fun = function(sc, dist_fun) { + if (!is.null(dist_fun)) { + return(dist_fun) + } + dist_fun = attr(sc, "dist_fun") + if (is.null(dist_fun)) { + stop("The 'dist_fun' argument is required when it is not stored in 'sc'", call. = FALSE) + } + dist_fun +} + +.sc_metrics_scale_summary = function(value_dist, spatial_dist, out, prep, scale) { + if (!isTRUE(scale)) { + return(list(value_dist = value_dist, spatial_dist = spatial_dist)) + } + if (isTRUE(prep$adaptive_compactness)) { + value_dist = out[["mean_value_dist_scaled"]] + } else { + value_dist = value_dist / prep$compactness + } + spatial_dist = spatial_dist / prep$step_scale + list(value_dist = value_dist, spatial_dist = spatial_dist) +} + .sc_metrics_prep = function(x, sc, dist_fun, compactness, step, include = c("clusters", "centers", "vals", "dist", "raster")) { @@ -15,13 +40,12 @@ if (!inherits(sc, "sf")) { stop("The 'sc' argument must be an sf object returned by sc_slic()", call. = FALSE) } - adaptive_compactness = FALSE + if (missing(compactness)) { - method = attr(sc, "method") - adaptive_compactness = isTRUE(identical(method, "slic0")) compactness = attr(sc, "compactness") - } else if (is.character(compactness) && length(compactness) == 1 && !is.na(compactness) && compactness == "auto") { - adaptive_compactness = TRUE + } + adaptive_method = attr(sc, "adaptive_method") + if (!is.null(adaptive_method) && is.null(compactness)) { compactness = 0 } if (missing(step)) { @@ -30,10 +54,18 @@ if (is.null(compactness) || is.null(step)) { stop("Both 'compactness' and 'step' are required", call. = FALSE) } - step_unit = attr(sc, "step_unit") - if (!identical(step_unit, "map")) { - step_unit = "cells" + + if (!is.null(adaptive_method)) { + if (!is.character(adaptive_method) || length(adaptive_method) != 1 || is.na(adaptive_method) || + adaptive_method != "local_max") { + stop("The 'adaptive_method' attribute must be 'local_max' or NULL", call. = FALSE) + } + compactness_prep = list(value = 0, adaptive = TRUE, adaptive_method = adaptive_method) + } else { + compactness_prep = .sc_util_prep_compactness(compactness) } + step_prep = .sc_util_step_to_cells(raster, step) + step = step_prep$step # prepare data, including handling missing metadata sc_work = sc @@ -58,23 +90,16 @@ x_df = x_df[order(x_df[["supercells"]]), , drop = FALSE] # prepare matrices for C++ function - spatial_scale = 1 - step_scale = step - if (identical(step_unit, "map")) { - res = terra::res(raster) - if (!isTRUE(all.equal(res[[1]], res[[2]]))) { - warning("Map-unit spatial metrics require square cells; using x resolution for scaling.", call. = FALSE) - } - spatial_scale = res[[1]] - step_scale = step * spatial_scale - } + spatial_scale = step_prep$spatial_scale + step_scale = step_prep$step_scale result = list( sc = sc_work, step = step, - compactness = compactness, - adaptive_compactness = adaptive_compactness, - step_unit = step_unit, + step_meta = step_prep$step_meta, + compactness = compactness_prep$value, + adaptive_compactness = compactness_prep$adaptive, + adaptive_method = compactness_prep$adaptive_method, spatial_scale = spatial_scale, step_scale = step_scale ) diff --git a/R/helpers-runners.R b/R/helpers-runners.R index a2d46f1..c27b7e4 100644 --- a/R/helpers-runners.R +++ b/R/helpers-runners.R @@ -24,8 +24,7 @@ raster_ref = x list(centers = slic[[2]], centers_vals = slic[[3]], - iter_diagnostics = slic[[4]], names_x = names(x), - raster_ref = raster_ref) + names_x = names(x), raster_ref = raster_ref) } # run slic on the raster chunk defined by ext @@ -116,10 +115,6 @@ minarea, input_centers, iter_diagnostics = iter_diagnostics, verbose = verbose) points_sf = .sc_run_centers_points(res$centers, res$raster_ref, res$centers_vals, res$names_x) - # points_sf = stats::na.omit(points_sf) - if (iter_diagnostics && !is.null(res$iter_diagnostics)) { - attr(points_sf, "iter_diagnostics") = res$iter_diagnostics - } return(points_sf) } res = .sc_run_chunk_raster(ext, x, step, compactness, dist_name, @@ -132,9 +127,6 @@ points_sf = points_sf[points_sf[["supercells"]] %in% ids, , drop = FALSE] points_sf = points_sf[match(ids, points_sf[["supercells"]]), , drop = FALSE] points_sf = stats::na.omit(points_sf) - if (iter_diagnostics && !is.null(res$iter_diagnostics)) { - attr(points_sf, "iter_diagnostics") = res$iter_diagnostics - } return(points_sf) } @@ -162,9 +154,6 @@ } slic_sf = cbind(slic_sf, centers_df) slic_sf = suppressWarnings(sf::st_collection_extract(slic_sf, "POLYGON")) - if (iter_diagnostics && !is.null(res$iter_diagnostics)) { - attr(slic_sf, "iter_diagnostics") = res$iter_diagnostics - } return(slic_sf) } } diff --git a/R/helpers-sc_slic.R b/R/helpers-sc_slic.R index 7734e15..4c02b87 100644 --- a/R/helpers-sc_slic.R +++ b/R/helpers-sc_slic.R @@ -1,10 +1,10 @@ # shared helpers for sc_slic workflows # prepare and validate slic arguments -.sc_slic_prep_args = function(x, step, step_unit, compactness, dist_fun, avg_fun, clean, minarea, iter, - k, centers, outcomes, chunks, iter_diagnostics, verbose) { +.sc_slic_prep_args = function(x, step, compactness, dist_fun, avg_fun, clean, minarea, iter, + k, centers, outcomes, chunks, verbose) { # Validate core arguments and types - .sc_slic_validate_args(step, step_unit, compactness, k, centers, chunks, dist_fun, avg_fun, iter, minarea) + .sc_slic_validate_args(step, compactness, k, centers, chunks, dist_fun, avg_fun, iter, minarea) outcomes = .sc_slic_prep_outcomes(outcomes) # Normalize input to SpatRaster x = .sc_util_prep_raster(x) @@ -12,7 +12,9 @@ warning("The input raster uses a geographic (lon/lat) CRS; consider projecting it before using SLIC", call. = FALSE) } # Resolve step from k when needed - step = .sc_slic_prep_step(x, step, k, step_unit) + step_prep = .sc_slic_prep_step(x, step, k) + step = step_prep$step + step_meta = step_prep$step_meta # Adjust numeric chunks to match step size if (is.numeric(chunks)) { if (chunks < step) { @@ -32,35 +34,32 @@ minarea = .sc_slic_prep_minarea(minarea, step) # Compute chunk extents based on size/limits chunk_ext = .sc_chunk_extents(dim(x), limit = chunks, step = step) - # Disable iter diagnostics when chunking is active - if (iter_diagnostics && nrow(chunk_ext) > 1) { - warning("Iteration diagnostics are only available when chunks = FALSE (single chunk). Iteration diagnostics were disabled.", call. = FALSE) - iter_diagnostics = FALSE - } verbose_cpp = if (is.numeric(verbose) && length(verbose) == 1 && !is.na(verbose) && verbose >= 2) verbose else 0 - adaptive_compactness = is.character(compactness) && - length(compactness) == 1 && !is.na(compactness) && compactness == "auto" - if (adaptive_compactness) { - compactness = 0 - } + compactness_prep = .sc_util_prep_compactness(compactness) # Package prep results for downstream functions - return(list(x = x, step = step, step_unit = step_unit, + return(list(x = x, step = step, step_meta = step_meta, dist_fun_input = dist_fun, input_centers = input_centers, funs = funs, minarea = minarea, chunk_ext = chunk_ext, - iter_diagnostics = iter_diagnostics, outcomes = outcomes, - compactness = compactness, adaptive_compactness = adaptive_compactness, + outcomes = outcomes, + compactness = compactness_prep$value, + adaptive_compactness = compactness_prep$adaptive, + adaptive_method = compactness_prep$adaptive_method, clean = clean, iter = iter, verbose = verbose, verbose_cpp = verbose_cpp)) } # validate slic arguments and types -.sc_slic_validate_args = function(step, step_unit, compactness, k, centers, chunks, dist_fun, avg_fun, iter, minarea) { - if (!missing(step_unit)) { - if (!is.character(step_unit) || length(step_unit) != 1 || is.na(step_unit) || - !(step_unit %in% c("cells", "map"))) { - stop("The 'step_unit' argument must be 'cells' or 'map'", call. = FALSE) +.sc_slic_validate_args = function(step, compactness, k, centers, chunks, dist_fun, avg_fun, iter, minarea) { + if (!is.null(step) && !is.numeric(step)) { + stop("The 'step' argument must be numeric or a units object", call. = FALSE) + } + if (inherits(step, "units")) { + if (length(step) != 1 || any(is.na(step))) { + stop("The 'step' argument as units must be a single non-missing value", call. = FALSE) } + } else if (!is.null(step) && (length(step) != 1 || is.na(step) || step <= 0)) { + stop("The 'step' argument must be a positive numeric value", call. = FALSE) } if (!is.numeric(iter) || length(iter) != 1 || is.na(iter) || iter < 0) { stop("The 'iter' argument must be a non-negative numeric value", call. = FALSE) @@ -103,9 +102,6 @@ if (missing(compactness)) { stop("The 'compactness' argument is required", call. = FALSE) } - if (!is.numeric(compactness) && !(is.character(compactness) && length(compactness) == 1 && !is.na(compactness) && compactness == "auto")) { - stop("The 'compactness' argument must be numeric or 'auto'", call. = FALSE) - } if (!missing(minarea) && (!is.numeric(minarea) || length(minarea) != 1 || is.na(minarea) || minarea < 0)) { stop("The 'minarea' argument must be a non-negative numeric value", call. = FALSE) } @@ -113,23 +109,15 @@ } # derive step from k when needed -.sc_slic_prep_step = function(x, step, k, step_unit = "cells") { +.sc_slic_prep_step = function(x, step, k) { if (!is.null(step)) { - if (step_unit == "map") { - res = terra::res(x) - if (!isTRUE(all.equal(res[[1]], res[[2]]))) { - stop("Map-unit step requires square cells; res(x) has different x/y resolution", call. = FALSE) - } - step = round(step / res[[1]]) - if (step < 1) { - step = 1 - } - } - return(step) + step_prep = .sc_util_step_to_cells(x, step) + return(list(step = step_prep$step, step_meta = step_prep$step_meta)) } mat = dim(x)[1:2] superpixelsize = round((mat[1] * mat[2]) / k + 0.5) - return(round(sqrt(superpixelsize) + 0.5)) + step = round(sqrt(superpixelsize) + 0.5) + return(list(step = step, step_meta = step)) } # normalize custom centers or create placeholder @@ -201,30 +189,20 @@ return(do.call(apply, c(list(chunk_ext, MARGIN = 1, FUN = fun), args))) } -# add iter diagnostics attribute when enabled -.sc_slic_add_iter_attr = function(chunks, iter_diagnostics) { - if (isTRUE(iter_diagnostics) && length(chunks) > 0) { - return(attr(chunks[[1]], "iter_diagnostics")) - } - NULL -} - # finalize slic output with ids, metadata, and attributes -.sc_slic_post = function(chunks, prep, iter_attr) { +.sc_slic_post = function(chunks, prep) { slic_sf = .sc_chunk_update_ids(chunks) slic_sf = .sc_slic_select_outcomes(slic_sf, prep$outcomes) - attr(slic_sf, "step") = prep$step - attr(slic_sf, "step_unit") = prep$step_unit + attr(slic_sf, "step") = prep$step_meta attr(slic_sf, "compactness") = prep$compactness + attr(slic_sf, "adaptive_method") = prep$adaptive_method attr(slic_sf, "dist_fun") = prep$dist_fun_input - attr(slic_sf, "method") = if (isTRUE(prep$adaptive_compactness)) "slic0" else "slic" - class(slic_sf) = c(class(slic_sf), "supercells") - if (!is.null(iter_attr)) { - attr(slic_sf, "iter_diagnostics") = iter_attr - } + cls = class(slic_sf) + cls = c(setdiff(cls, "data.frame"), "supercells", "data.frame") + class(slic_sf) = unique(cls) return(slic_sf) } @@ -243,8 +221,7 @@ iter = prep$iter, minarea = prep$minarea, input_centers = prep$input_centers, - verbose = prep$verbose_cpp, - iter_diagnostics = prep$iter_diagnostics + verbose = prep$verbose_cpp ) if (nrow(prep$chunk_ext) == 1) { list(chunks = list(do.call(single_runner, args))) diff --git a/R/sc_metrics_global.R b/R/sc_metrics_global.R index 6eeae86..7ebaa8a 100644 --- a/R/sc_metrics_global.R +++ b/R/sc_metrics_global.R @@ -8,7 +8,7 @@ #' Use `outcomes = c("supercells", "coordinates", "values")` when calling #' `sc_slic()` or `supercells()` to preserve original centers and IDs. #' Metrics are averaged across supercells (each supercell has equal weight). -#' When using SLIC0 (set `compactness = "auto"` in [sc_slic()]), combined and balance metrics use per-supercell +#' When using SLIC0 (set `compactness = use_adaptive()` in [sc_slic()]), combined and balance metrics use per-supercell #' adaptive compactness (SLIC0), and scaled value distances are computed with the #' per-supercell max value distance. #' @@ -28,8 +28,10 @@ #' values indicate spatial dominance; positive values indicate value dominance.} #' } #' \describe{ -#' \item{step}{Step size used to generate supercells.} +#' \item{step}{Step size used to generate supercells. Returned in meters when +#' the input used `step = use_meters(...)`, otherwise in cells.} #' \item{compactness}{Compactness value used to generate supercells.} +#' \item{adaptive_method}{Adaptive compactness method; `NA` for fixed compactness.} #' \item{n_supercells}{Number of supercells with at least one non-missing pixel.} #' \item{mean_value_dist}{Mean per-supercell value distance from cells to their #' supercell centers, averaged across supercells. Returned as `mean_value_dist` @@ -37,7 +39,7 @@ #' \item{mean_spatial_dist}{Mean per-supercell spatial distance from cells to #' their supercell centers, averaged across supercells; units are grid cells #' (row/column index distance). If the input supercells were created with -#' `step_unit = "map"`, distances are reported in map units. Returned as +#' `step = use_meters(...)`, distances are reported in meters. Returned as #' `mean_spatial_dist` (or `mean_spatial_dist_scaled` when `scale = TRUE`).} #' \item{mean_combined_dist}{Mean per-supercell combined distance, computed from #' value and spatial distances using `compactness` and `step`, averaged across @@ -58,13 +60,9 @@ sc_metrics_global = function(x, sc, metrics = c("spatial", "value", "combined", "balance"), scale = TRUE, step, compactness, dist_fun = NULL) { - if (missing(dist_fun) || is.null(dist_fun)) { - dist_fun = attr(sc, "dist_fun") - if (is.null(dist_fun)) { - stop("The 'dist_fun' argument is required when it is not stored in 'sc'", call. = FALSE) - } - } - if (any(!metrics %in% c("spatial", "value", "combined", "balance"))) { + dist_fun = .sc_metrics_resolve_dist_fun(sc, dist_fun) + allowed_metrics = c("spatial", "value", "combined", "balance") + if (any(!metrics %in% allowed_metrics)) { stop("metrics must be one or more of: spatial, value, combined, balance", call. = FALSE) } @@ -78,15 +76,9 @@ sc_metrics_global = function(x, sc, mean_spatial_dist = out[["mean_spatial_dist"]] * prep$spatial_scale mean_combined_dist = out[["mean_combined_dist"]] balance = out[["balance"]] - - if (isTRUE(scale)) { - if (isTRUE(prep$adaptive_compactness)) { - mean_value_dist = out[["mean_value_dist_scaled"]] - } else { - mean_value_dist = mean_value_dist / prep$compactness - } - mean_spatial_dist = mean_spatial_dist / prep$step_scale - } + scaled = .sc_metrics_scale_summary(mean_value_dist, mean_spatial_dist, out, prep, scale) + mean_value_dist = scaled$value_dist + mean_spatial_dist = scaled$spatial_dist metric_values = list( spatial = mean_spatial_dist, @@ -102,8 +94,15 @@ sc_metrics_global = function(x, sc, balance = "balance" ) names(out_metrics) = unname(name_map[metrics]) + step_out = prep$step_meta + adaptive_method_out = prep$adaptive_method + if (is.null(adaptive_method_out)) { + adaptive_method_out = NA_character_ + } results = cbind( - data.frame(step = prep$step, compactness = prep$compactness, n_supercells = out[["n_supercells"]]), + data.frame(step = step_out, compactness = prep$compactness, + adaptive_method = adaptive_method_out, + n_supercells = out[["n_supercells"]]), out_metrics ) return(results) diff --git a/R/sc_metrics_pixels.R b/R/sc_metrics_pixels.R index 4594f12..6f65f17 100644 --- a/R/sc_metrics_pixels.R +++ b/R/sc_metrics_pixels.R @@ -12,14 +12,15 @@ #' @param step A step value used for the supercells #' If missing, uses `attr(sc, "step")` when available #' @param compactness A compactness value used for the supercells -#' If missing, uses `attr(sc, "compactness")` when available +#' If missing, uses `attr(sc, "compactness")` when available. +#' Adaptive mode is read from `attr(sc, "adaptive_method")` when available. #' @param dist_fun A distance function name or function, as in [sc_slic()]. #' If missing or `NULL`, uses `attr(sc, "dist_fun")` when available. #' #' @details #' If `sc` lacks `supercells`, `x`, or `y` columns, they are derived from geometry #' and row order, which may differ from the original centers. -#' When using SLIC0 (set `compactness = "auto"` in [sc_slic()]), combined and balance metrics use per-supercell +#' When using SLIC0 (set `compactness = use_adaptive()` in [sc_slic()]), combined and balance metrics use per-supercell #' adaptive compactness (SLIC0), and scaled value distances are computed with the #' per-supercell max value distance. #' @@ -36,7 +37,7 @@ #' \describe{ #' \item{spatial}{Spatial distance from each pixel to its supercell center #' in grid-cell units (row/column index distance). If the input supercells were -#' created with `step_unit = "map"`, distances are reported in map units.} +#' created with `step = use_meters(...)`, distances are reported in meters.} #' \item{value}{Value distance from each pixel to its supercell center in #' the raster value space.} #' \item{combined}{Combined distance using `compactness` and `step`.} @@ -57,17 +58,12 @@ sc_metrics_pixels = function(x, sc, metrics = c("spatial", "value", "combined", "balance"), scale = TRUE, step, compactness, dist_fun = NULL) { - if (missing(dist_fun) || is.null(dist_fun)) { - dist_fun = attr(sc, "dist_fun") - if (is.null(dist_fun)) { - stop("The 'dist_fun' argument is required when it is not stored in 'sc'", call. = FALSE) - } - } - - if (any(!metrics %in% c("spatial", "value", "combined", "balance"))) { + dist_fun = .sc_metrics_resolve_dist_fun(sc, dist_fun) + allowed_metrics = c("spatial", "value", "combined", "balance") + if (any(!metrics %in% allowed_metrics)) { stop("metrics must be one or more of: spatial, value, combined, balance", call. = FALSE) } - + prep = .sc_metrics_prep(x, sc, dist_fun, compactness, step) out = sc_metrics_pixels_cpp(prep$clusters, prep$centers_xy, prep$centers_vals, prep$vals, @@ -89,14 +85,12 @@ sc_metrics_pixels = function(x, sc, if ("balance" %in% metrics) { balance = log(value_scaled / spatial_scaled) } - } - + } + result = c(spatial, value, combined) + names(result) = c("spatial", "value", "combined") if ("balance" %in% metrics) { - result = c(spatial, value, combined, balance) - names(result) = c("spatial", "value", "combined", "balance") - } else { - result = c(spatial, value, combined) - names(result) = c("spatial", "value", "combined") + result = c(result, balance) + names(result)[4] = "balance" } result = result[[metrics]] diff --git a/R/sc_metrics_supercells.R b/R/sc_metrics_supercells.R index d04a3b9..045f506 100644 --- a/R/sc_metrics_supercells.R +++ b/R/sc_metrics_supercells.R @@ -11,7 +11,7 @@ #' @details #' If `sc` lacks `supercells`, `x`, or `y` columns, they are derived from geometry #' and row order, which may differ from the original centers -#' When using SLIC0 (set `compactness = "auto"` in [sc_slic()]), combined and balance metrics use per-supercell +#' When using SLIC0 (set `compactness = use_adaptive()` in [sc_slic()]), combined and balance metrics use per-supercell #' adaptive compactness (SLIC0), and scaled value distances are computed with the #' per-supercell max value distance. #' @return An sf object with one row per supercell and columns: @@ -28,7 +28,7 @@ #' \item{supercells}{Supercell ID.} #' \item{spatial}{Mean spatial distance from cells to the supercell center #' in grid-cell units (row/column index distance). If the input supercells were -#' created with `step_unit = "map"`, distances are reported in map units. +#' created with `step = use_meters(...)`, distances are reported in meters. #' Returned as `mean_spatial_dist` (or `mean_spatial_dist_scaled` when `scale = TRUE`).} #' \item{value}{Mean value distance from cells to the supercell center in #' value space. Returned as `mean_value_dist` (or `mean_value_dist_scaled` @@ -50,13 +50,9 @@ sc_metrics_supercells = function(x, sc, metrics = c("spatial", "value", "combined", "balance"), scale = TRUE, step, compactness, dist_fun = NULL) { - if (missing(dist_fun) || is.null(dist_fun)) { - dist_fun = attr(sc, "dist_fun") - if (is.null(dist_fun)) { - stop("The 'dist_fun' argument is required when it is not stored in 'sc'", call. = FALSE) - } - } - if (any(!metrics %in% c("spatial", "value", "combined", "balance"))) { + dist_fun = .sc_metrics_resolve_dist_fun(sc, dist_fun) + allowed_metrics = c("spatial", "value", "combined", "balance") + if (any(!metrics %in% allowed_metrics)) { stop("metrics must be one or more of: spatial, value, combined, balance", call. = FALSE) } @@ -73,15 +69,9 @@ sc_metrics_supercells = function(x, sc, mean_spatial_dist = out[["mean_spatial_dist"]] * prep$spatial_scale mean_combined_dist = out[["mean_combined_dist"]] balance = log(out[["balance"]]) - - if (isTRUE(scale)) { - if (isTRUE(prep$adaptive_compactness)) { - mean_value_dist = out[["mean_value_dist_scaled"]] - } else { - mean_value_dist = mean_value_dist / prep$compactness - } - mean_spatial_dist = mean_spatial_dist / prep$step_scale - } + scaled = .sc_metrics_scale_summary(mean_value_dist, mean_spatial_dist, out, prep, scale) + mean_value_dist = scaled$value_dist + mean_spatial_dist = scaled$spatial_dist x_ordered = prep$sc[order_idx, , drop = FALSE] x_keep = x_ordered[, "supercells", drop = FALSE] diff --git a/R/sc_plot_iter_diagnostics.R b/R/sc_plot_iter_diagnostics.R deleted file mode 100644 index e1299fc..0000000 --- a/R/sc_plot_iter_diagnostics.R +++ /dev/null @@ -1,31 +0,0 @@ -#' Plot iteration diagnostics -#' -#' Plot mean distance across iterations for a supercells run -#' -#' @param x A supercells object with an \code{iter_diagnostics} attribute, -#' or a diagnostics list containing \code{mean_distance} -#' -#' @return Invisibly returns \code{TRUE} when a plot is created -#' -#' @seealso [`sc_slic()`], [`sc_slic_points()`] -#' @export -#' -#' @examples -#' library(supercells) -#' vol = terra::rast(system.file("raster/volcano.tif", package = "supercells")) -#' vol_sc = sc_slic_points(vol, step = 8, compactness = 1, iter_diagnostics = TRUE) -#' sc_plot_iter_diagnostics(vol_sc) -sc_plot_iter_diagnostics = function(x) { - - iter = attr(x, "iter_diagnostics") - - if (is.null(iter) || is.null(iter$mean_distance) || length(iter$mean_distance) == 0) { - stop("No iter_diagnostics found", call. = FALSE) - } - - y = iter$mean_distance - graphics::plot(seq_along(y), y, type = "b", - xlab = "Iteration", ylab = "Mean distance", - main = "SLIC iteration diagnostics") - return(invisible(TRUE)) -} diff --git a/R/sc_slic.R b/R/sc_slic.R index c5ebeb7..61d1536 100644 --- a/R/sc_slic.R +++ b/R/sc_slic.R @@ -9,21 +9,21 @@ #' [`sc_slic_raster()`] and [`sc_slic_points()`]. #' Evaluation and diagnostic options: #' \itemize{ -#' \item Iteration diagnostics: set `iter_diagnostics = TRUE` to attach an -#' `iter_diagnostics` attribute (only available without chunking). Use -#' [`sc_plot_iter_diagnostics()`] to visualize the convergence over iterations. +#' \item Iteration convergence: use [`sc_slic_convergence()`] and plot its output. #' \item Pixel diagnostics: [sc_metrics_pixels()] for per-pixel spatial, value, #' and combined distances. #' \item Cluster diagnostics: [sc_metrics_supercells()] for per-supercell summaries. #' \item Global diagnostics: [sc_metrics_global()] for a single-row summary. #' } -#' @seealso [`sc_slic_raster()`], [`sc_slic_points()`], [`sc_plot_iter_diagnostics()`], +#' @seealso [use_meters()], [use_adaptive()], [`sc_slic_raster()`], [`sc_slic_points()`], [`sc_slic_convergence()`], #' [`sc_metrics_pixels()`], [`sc_metrics_supercells()`], [`sc_metrics_global()`] #' #' @param x An object of class SpatRaster (terra) or class stars (stars). -#' @param step The distance (number of cells) between initial centers (alternative is `k`). +#' @param step Initial center spacing (alternative is `k`). +#' Provide a plain numeric value for cell units, or use [use_meters()] for +#' map-distance steps in meters (automatically converted to cells using raster resolution). #' @param compactness A compactness value. Use [`sc_tune_compactness()`] to estimate it. -#' Set `compactness = "auto"` to enable adaptive compactness (SLIC0). +#' Use [use_adaptive()] to enable adaptive compactness (SLIC0). #' @param dist_fun A distance function name or a custom function. Supported names: #' "euclidean", "jsd", "dtw", "dtw2d", or any method from `philentropy::getDistMethods()`. #' A custom function must accept two numeric vectors and return a single numeric value. @@ -33,22 +33,20 @@ #' @param clean Should connectivity of the supercells be enforced? #' @param minarea Minimal size of a supercell (in cells). #' @param iter Number of iterations. -#' @param step_unit Units for `step`. Use "cells" for pixel units or "map" for map units -#' (converted to cells using raster resolution). #' @param k The number of supercells desired (alternative to `step`). #' @param centers Optional sf object of custom centers. Requires `step`. #' @param outcomes Character vector controlling which fields are returned. #' Allowed values are "supercells", "coordinates", and "values". Default is -#' "values". Use `outcomes = c("supercells", "coordinates", "values")` for full output. +#' full output (`c("supercells", "coordinates", "values")`). +#' Use `outcomes = "values"` for value summaries only. #' @param chunks Chunking option. Use `FALSE` for no chunking, `TRUE` for #' automatic chunking based on size, or a numeric value for a fixed chunk size #' (in number of cells per side). #' @param verbose Verbosity level. -#' @param iter_diagnostics Logical. If `TRUE`, attaches iteration diagnostics as an -#' attribute (`iter_diagnostics`) on the output. Only available when chunks are not used. #' #' @return An sf object with the supercell polygons and summary statistics. -#' Information on `step` and `compactness` are attached to the result as attributes. +#' Information on `step`, `compactness`, and `adaptive_method` are attached to +#' the result as attributes (`adaptive_method` is `NULL` for fixed compactness). #' #' @references Achanta, R., Shaji, A., Smith, K., Lucchi, A., Fua, P., & Süsstrunk, S. (2012). SLIC Superpixels Compared to State-of-the-Art Superpixel Methods. IEEE Transactions on Pattern Analysis and Machine Intelligence, 34(11), 2274–2282. https://doi.org/10.1109/tpami.2012.120 #' @references Nowosad, J., Stepinski, T. (2022). Extended SLIC superpixels algorithm for applications to non-imagery geospatial rasters. International Journal of Applied Earth Observation and Geoinformation, https://doi.org/10.1016/j.jag.2022.102935 @@ -64,20 +62,18 @@ #' plot(sf::st_geometry(vol_slic1), add = TRUE, lwd = 0.2) sc_slic = function(x, step = NULL, compactness, dist_fun = "euclidean", avg_fun = "mean", clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, - outcomes = "values", chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0) { + k = NULL, centers = NULL, + outcomes = c("supercells", "coordinates", "values"), chunks = FALSE, + verbose = 0) { if (iter == 0) { stop("iter = 0 returns centers only; polygon output is not available. Use sc_slic_points(iter = 0) to get initial centers.", call. = FALSE) } - prep_args = .sc_slic_prep_args(x, step, step_unit, compactness, dist_fun, avg_fun, clean, minarea, iter, - k, centers, outcomes, chunks, iter_diagnostics, verbose) + prep_args = .sc_slic_prep_args(x, step, compactness, dist_fun, avg_fun, clean, minarea, iter, + k, centers, outcomes, chunks, verbose) segment = .sc_slic_segment(prep_args, .sc_run_full_polygons, .sc_run_chunk_polygons) - iter_attr = .sc_slic_add_iter_attr(segment$chunks, prep_args$iter_diagnostics) - - result = .sc_slic_post(segment$chunks, prep_args, iter_attr) + result = .sc_slic_post(segment$chunks, prep_args) return(result) } diff --git a/R/sc_slic_convergence.R b/R/sc_slic_convergence.R new file mode 100644 index 0000000..307d627 --- /dev/null +++ b/R/sc_slic_convergence.R @@ -0,0 +1,73 @@ +#' SLIC convergence diagnostics +#' +#' Runs SLIC and returns per-iteration mean combined distance. +#' The output can be plotted directly with [plot()]. +#' +#' @inheritParams sc_slic +#' +#' @return A data frame with class `sc_slic_convergence` and columns: +#' \describe{ +#' \item{iter}{Iteration number.} +#' \item{mean_distance}{Mean combined distance across assigned cells at each iteration.} +#' } +#' +#' @seealso [sc_slic()], [plot()] +#' @export +#' +#' @examples +#' library(supercells) +#' vol = terra::rast(system.file("raster/volcano.tif", package = "supercells")) +#' conv = sc_slic_convergence(vol, step = 8, compactness = 5, iter = 10) +#' plot(conv) +sc_slic_convergence = function(x, step = NULL, compactness, dist_fun = "euclidean", + avg_fun = "mean", clean = TRUE, minarea, iter = 10, + k = NULL, centers = NULL, verbose = 0) { + if (iter == 0) { + stop("iter must be > 0 for convergence diagnostics", call. = FALSE) + } + + prep = .sc_slic_prep_args( + x, step, compactness, dist_fun, avg_fun, clean, minarea, iter, + k, centers, outcomes = "values", chunks = FALSE, verbose = verbose + ) + + res = .sc_run_full_raster( + x = prep$x, + step = prep$step, + compactness = prep$compactness, + dist_name = prep$funs$dist_name, + dist_fun = prep$funs$dist_fun, + adaptive_compactness = prep$adaptive_compactness, + avg_fun_fun = prep$funs$avg_fun_fun, + avg_fun_name = prep$funs$avg_fun_name, + clean = prep$clean, + iter = prep$iter, + minarea = prep$minarea, + input_centers = prep$input_centers, + iter_diagnostics = TRUE, + verbose = prep$verbose_cpp + ) + + iter_diag = res$iter_diagnostics + if (is.null(iter_diag) || is.null(iter_diag$mean_distance) || length(iter_diag$mean_distance) == 0) { + stop("No convergence diagnostics available", call. = FALSE) + } + + y = as.numeric(iter_diag$mean_distance) + out = data.frame(iter = seq_along(y), mean_distance = y) + class(out) = c("sc_slic_convergence", class(out)) + out +} + +#' @export +plot.sc_slic_convergence = function(x, ...) { + if (!all(c("iter", "mean_distance") %in% names(x))) { + stop("x must contain 'iter' and 'mean_distance' columns", call. = FALSE) + } + graphics::plot( + x$iter, x$mean_distance, type = "b", + xlab = "Iteration", ylab = "Mean distance", + main = "SLIC convergence", ... + ) + invisible(x) +} diff --git a/R/sc_slic_points.R b/R/sc_slic_points.R index 9aaa1e0..8300ab2 100644 --- a/R/sc_slic_points.R +++ b/R/sc_slic_points.R @@ -27,19 +27,17 @@ #' plot(sf::st_geometry(vol_pts), add = TRUE, pch = 16, col = "red") sc_slic_points = function(x, step = NULL, compactness, dist_fun = "euclidean", avg_fun = "mean", clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, + k = NULL, centers = NULL, outcomes = "values", chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0) { + verbose = 0) { if (iter == 0) { clean = FALSE } - prep_args = .sc_slic_prep_args(x, step, step_unit, compactness, dist_fun, avg_fun, clean, minarea, iter, - k, centers, outcomes, chunks, iter_diagnostics, verbose) + prep_args = .sc_slic_prep_args(x, step, compactness, dist_fun, avg_fun, clean, minarea, iter, + k, centers, outcomes, chunks, verbose) segment = .sc_slic_segment(prep_args, .sc_run_full_points, .sc_run_chunk_points) - iter_attr = .sc_slic_add_iter_attr(segment$chunks, prep_args$iter_diagnostics) - - results = .sc_slic_post(segment$chunks, prep_args, iter_attr) + results = .sc_slic_post(segment$chunks, prep_args) return(results) } diff --git a/R/sc_slic_raster.R b/R/sc_slic_raster.R index 4925ff5..21db502 100644 --- a/R/sc_slic_raster.R +++ b/R/sc_slic_raster.R @@ -20,9 +20,9 @@ #' terra::plot(vol_ids) sc_slic_raster = function(x, step = NULL, compactness, dist_fun = "euclidean", avg_fun = "mean", clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, + k = NULL, centers = NULL, outcomes = "supercells", chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0) { + verbose = 0) { if (iter == 0) { stop("iter = 0 returns centers only; raster output is not available. Use sc_slic_points(iter = 0) to get initial centers.", call. = FALSE) @@ -31,8 +31,8 @@ sc_slic_raster = function(x, step = NULL, compactness, dist_fun = "euclidean", stop("sc_slic_raster() supports only outcomes = 'supercells'", call. = FALSE) } # prep arguments - prep_args = .sc_slic_prep_args(x, step, step_unit, compactness, dist_fun, avg_fun, clean, minarea, iter, - k, centers, outcomes, chunks, iter_diagnostics, verbose) + prep_args = .sc_slic_prep_args(x, step, compactness, dist_fun, avg_fun, clean, minarea, iter, + k, centers, outcomes, chunks, verbose) # segment once (single) or per chunk (chunked), returning a list of chunk results if (nrow(prep_args$chunk_ext) > 1) { @@ -49,19 +49,29 @@ sc_slic_raster = function(x, step = NULL, compactness, dist_fun = "euclidean", message(sprintf("Processing chunk %d/%d", i, n_chunks)) } ext = prep_args$chunk_ext[i, ] - res = .sc_run_chunk_raster(ext, prep_args$x, prep_args$step, prep_args$compactness, - prep_args$funs$dist_name, prep_args$adaptive_compactness, - prep_args$funs$dist_fun, - prep_args$funs$avg_fun_fun, prep_args$funs$avg_fun_name, - prep_args$clean, prep_args$iter, prep_args$minarea, - prep_args$input_centers, prep_args$iter_diagnostics, - prep_args$verbose_cpp) + res = .sc_run_chunk_raster( + ext = ext, + x = prep_args$x, + step = prep_args$step, + compactness = prep_args$compactness, + dist_name = prep_args$funs$dist_name, + adaptive_compactness = prep_args$adaptive_compactness, + dist_fun = prep_args$funs$dist_fun, + avg_fun_fun = prep_args$funs$avg_fun_fun, + avg_fun_name = prep_args$funs$avg_fun_name, + clean = prep_args$clean, + iter = prep_args$iter, + minarea = prep_args$minarea, + input_centers = prep_args$input_centers, + iter_diagnostics = FALSE, + verbose = prep_args$verbose_cpp + ) r = res[["raster"]] if (max_id > 0) { r = r + max_id } n_centers = nrow(res[["centers"]]) - if (!is.na(n_centers) && n_centers > 0) { + if (n_centers > 0) { max_id = max_id + n_centers } chunk_files[i] = tempfile(fileext = ".tif") diff --git a/R/sc_tune_compactness.R b/R/sc_tune_compactness.R index acc22d7..b787e24 100644 --- a/R/sc_tune_compactness.R +++ b/R/sc_tune_compactness.R @@ -7,14 +7,18 @@ #' while the local estimate uses a median of per-center mean distances. #' #' @param raster A `SpatRaster`. -#' @param step The distance (number of cells) between initial centers (alternative is `k`). -#' @param step_unit Units for `step`. Use "cells" for pixel units or "map" for map units. +#' @param step Initial center spacing (alternative is `k`). +#' Provide a plain numeric value for cell units, or use [use_meters()] for +#' map-distance steps in meters (automatically converted to cells using raster resolution). #' @param compactness Starting compactness used for the initial short run. -#' @param metrics Which compactness metric to return: `"global"` or `"local"`. +#' @param metric Which compactness metric to return: `"global"` or `"local"`. #' Default: `"global"`. -#' @param value_scale Optional scale factor applied to the median value distance -#' before computing compactness. Use `"auto"` to divide by `sqrt(nlyr(raster))` -#' (useful for high-dimensional embeddings). Default: `"auto"`. +#' @param value_scale Scale factor for value distances during tuning. +#' Global metric: `compactness = (median(d_value) / value_scale) * step / median(d_spatial)`. +#' Local metric: `compactness = median(local_mean(d_value) / value_scale)`. +#' `"auto"` uses `sqrt(nlyr(raster))` (good for Euclidean-like distances); +#' for bounded/angular distances (e.g., cosine), `value_scale = 1` is often better. +#' Default: `"auto"`. #' @param dist_fun A distance function name or a custom function. Supported names: #' "euclidean", "jsd", "dtw", "dtw2d", or any method from `philentropy::getDistMethods()`. #' A custom function must accept two numeric vectors and return a single numeric value. @@ -29,9 +33,9 @@ #' @param sample_size Optional limit on the number of pixels used for the summary #' (passed to `terra::global()` as `maxcell`). #' -#' @return A one-row data frame with columns `step`, `metric`, and `compactness`. +#' @return A one-row data frame with columns `step`, `metric`, `dist_fun`, and `compactness`. #' -#' @seealso [`sc_slic()`] +#' @seealso [`sc_slic()`], [use_meters()], [use_adaptive()] #' #' @examples #' library(terra) @@ -40,8 +44,8 @@ #' tune$compactness #' #' @export -sc_tune_compactness = function(raster, step = NULL, step_unit = "cells", compactness = 1, - metrics = "global", +sc_tune_compactness = function(raster, step = NULL, compactness = 1, + metric = "global", dist_fun = "euclidean", avg_fun = "mean", clean = TRUE, minarea, iter = 1, k = NULL, centers = NULL, @@ -49,7 +53,7 @@ sc_tune_compactness = function(raster, step = NULL, step_unit = "cells", compact pts = sc_slic_points(raster, step = step, compactness = compactness, dist_fun = dist_fun, avg_fun = avg_fun, clean = clean, minarea = minarea, iter = iter, - step_unit = step_unit, k = k, centers = centers, + k = k, centers = centers, outcomes = c("supercells", "coordinates", "values"), chunks = FALSE) @@ -57,11 +61,16 @@ sc_tune_compactness = function(raster, step = NULL, step_unit = "cells", compact if (is.null(step_used)) { step_used = step } + step_used_num = step_used + if (inherits(step_used_num, "units")) { + step_used_num = as.numeric(units::drop_units(step_used_num)) + } - if (!is.character(metrics) || length(metrics) != 1 || is.na(metrics) || - !(metrics %in% c("global", "local"))) { - stop("metrics must be 'global' or 'local'", call. = FALSE) + if (!is.character(metric) || length(metric) != 1 || is.na(metric) || + !(metric %in% c("global", "local"))) { + stop("metric must be 'global' or 'local'", call. = FALSE) } + dist_fun_out = if (is.character(dist_fun)) as.character(dist_fun[[1]]) else "custom" if (identical(value_scale, "auto")) { value_scale = sqrt(terra::nlyr(raster)) @@ -70,7 +79,7 @@ sc_tune_compactness = function(raster, step = NULL, step_unit = "cells", compact stop("value_scale must be a single positive number or 'auto'", call. = FALSE) } - if (identical(metrics, "global")) { + if (identical(metric, "global")) { pix_metrics = sc_metrics_pixels(raster, pts, dist_fun = dist_fun, compactness = compactness, step = step_used, scale = FALSE, metrics = c("spatial", "value")) @@ -79,26 +88,24 @@ sc_tune_compactness = function(raster, step = NULL, step_unit = "cells", compact spatial_dist_median = med[1, 1] value_dist_median = med[2, 1] value_dist_median = value_dist_median / value_scale - compactness_value = value_dist_median * step_used / spatial_dist_median + compactness_value = value_dist_median * step_used_num / spatial_dist_median - result = data.frame(step = step_used, metric = "global", compactness = compactness_value) + return(data.frame(step = step_used, metric = "global", dist_fun = dist_fun_out, + compactness = compactness_value)) } - if (identical(metrics, "local")) { - prep = .sc_metrics_prep(raster, pts, dist_fun, compactness, step_used, - include = c("centers", "vals", "dist", "raster")) - mean_value_dist = sc_metrics_local_mean_cpp( - prep$centers_xy, prep$centers_vals, prep$vals, - rows = dim(prep$raster)[1], cols = dim(prep$raster)[2], - step = step_used, - dist_name = prep$dist_name, - dist_fun = prep$dist_fun - ) - mean_value_dist = mean_value_dist / value_scale - compactness_value = stats::median(mean_value_dist, na.rm = TRUE) - - result = data.frame(step = step_used, metric = "local", compactness = compactness_value) - } + prep = .sc_metrics_prep(raster, pts, dist_fun, compactness, step_used, + include = c("centers", "vals", "dist", "raster")) + mean_value_dist = sc_metrics_local_mean_cpp( + prep$centers_xy, prep$centers_vals, prep$vals, + rows = dim(prep$raster)[1], cols = dim(prep$raster)[2], + step = prep$step, + dist_name = prep$dist_name, + dist_fun = prep$dist_fun + ) + mean_value_dist = mean_value_dist / value_scale + compactness_value = stats::median(mean_value_dist, na.rm = TRUE) - return(result) + return(data.frame(step = step_used, metric = "local", dist_fun = dist_fun_out, + compactness = compactness_value)) } diff --git a/R/supercells.R b/R/supercells.R index 41d49aa..378e003 100644 --- a/R/supercells.R +++ b/R/supercells.R @@ -8,7 +8,7 @@ #' You can use either `k` or `step`. #' It is also possible to provide a set of points (an `sf` object) as `k` together with the `step` value to create custom cluster centers. #' @param compactness A compactness value. Larger values cause supercells to be more compact/even (square). -#' Set `compactness = "auto"` to enable adaptive compactness (SLIC0). +#' Use [use_adaptive()] to enable adaptive compactness (SLIC0). #' A compactness value depends on the range of input cell values and selected distance measure. #' @param dist_fun A distance function. Currently implemented distance functions are `"euclidean"`, `"jsd"`, `"dtw"` (dynamic time warping), name of any distance function from the `philentropy` package (see [philentropy::getDistMethods()]; "log2" is used in this case), or any user defined function accepting two vectors and returning one value. Default: `"euclidean"` #' @param avg_fun An averaging function - how the values of the supercells' centers are calculated? The algorithm internally implements common functions `"mean"` and `"median"` (provided with quotation marks), but also accepts any fitting R function (e.g., `base::mean()` or `stats::median()`, provided as plain function name: `mean`). Default: `"mean"`. See details for more information. @@ -17,7 +17,9 @@ #' @param minarea Specifies the minimal size of a supercell (in cells). Only works when `clean = TRUE`. #' By default, when `clean = TRUE`, average area (A) is calculated based on the total number of cells divided by a number of supercells #' Next, the minimal size of a supercell equals to A/(2^2) (A is being right shifted) -#' @param step The distance (number of cells) between initial supercells' centers. You can use either `k` or `step`. +#' @param step Initial center spacing. You can use either `k` or `step`. +#' Provide a plain numeric value for cell units, or use [use_meters()] for +#' map-distance steps in meters (automatically converted to cells using raster resolution). #' @param transform Transformation to be performed on the input. By default, no transformation is performed. Currently available transformation is "to_LAB": first, the conversion from RGB to the LAB color space is applied, then the supercells algorithm is run, and afterward, a reverse transformation is performed on the obtained results. (This argument is experimental and may be removed in the future). #' @param metadata Logical. Controls whether metadata columns #' ("supercells", "x", "y") are included. @@ -75,9 +77,6 @@ supercells = function(x, k, compactness, dist_fun = "euclidean", avg_fun = "mean } else { "values" } - if (iter == 0) { - clean = FALSE - } centers_arg = NULL if (!missing(k) && inherits(k, "sf")) { @@ -85,13 +84,7 @@ supercells = function(x, k, compactness, dist_fun = "euclidean", avg_fun = "mean k = NULL } - if (!inherits(x, "SpatRaster")) { - if (inherits(x, "stars")) { - x = terra::rast(x) - } else{ - stop("The SpatRaster class is expected as an input", call. = FALSE) - } - } + x = .sc_util_prep_raster(x) trans = .supercells_transform_to_lab(x, transform) x = trans$x @@ -105,7 +98,6 @@ supercells = function(x, k, compactness, dist_fun = "euclidean", avg_fun = "mean iter = iter, outcomes = outcomes, chunks = chunks, - iter_diagnostics = FALSE, verbose = verbose ) if (!missing(step)) { @@ -121,11 +113,8 @@ supercells = function(x, k, compactness, dist_fun = "euclidean", avg_fun = "mean args$centers = centers_arg } - if (iter == 0) { - slic_sf = do.call(sc_slic_points, args) - } else { - slic_sf = do.call(sc_slic, args) - } + slic_runner = if (iter == 0) sc_slic_points else sc_slic + slic_sf = do.call(slic_runner, args) if (isTRUE(trans$did_transform)) { names_x = names(x) slic_sf = .supercells_transform_from_lab(slic_sf, names_x) diff --git a/R/use_adaptive.R b/R/use_adaptive.R new file mode 100644 index 0000000..c79f84a --- /dev/null +++ b/R/use_adaptive.R @@ -0,0 +1,21 @@ +#' Use adaptive compactness mode +#' +#' Creates a compactness mode object for adaptive compactness. +#' The `"local_max"` method corresponds to SLIC0-style local scaling, +#' where compactness is adapted using local maximum value distances. +#' +#' @param method Adaptive compactness method. Currently only `"local_max"` is supported +#' (SLIC0-style). +#' +#' @return An adaptive compactness mode object for `compactness` arguments. +#' +#' @examples +#' use_adaptive() +#' +#' @export +use_adaptive = function(method = "local_max") { + if (!is.character(method) || length(method) != 1 || is.na(method) || method != "local_max") { + stop("The 'method' argument must be 'local_max' (SLIC0-style)", call. = FALSE) + } + structure(list(method = method), class = "sc_adaptive") +} diff --git a/R/use_meters.R b/R/use_meters.R new file mode 100644 index 0000000..fa0c909 --- /dev/null +++ b/R/use_meters.R @@ -0,0 +1,19 @@ +#' Mark step values as meters +#' +#' Creates a units value in meters for use in `step` arguments. +#' Use plain numerics for cell units, and `use_meters()` for map-distance steps. +#' +#' @param x A single positive numeric value. +#' +#' @return A [units::units] object in meters (`m`). +#' +#' @examples +#' use_meters(100) +#' +#' @export +use_meters = function(x) { + if (!is.numeric(x) || length(x) != 1 || is.na(x) || x <= 0) { + stop("The 'x' argument must be a single positive number", call. = FALSE) + } + units::set_units(as.numeric(x), "m") +} diff --git a/codecov.yml b/codecov.yml index 04c5585..60e9d9b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,9 @@ comment: false +ignore: + - "R/sc_merge_supercells.R" + - "**/R/sc_merge_supercells.R" + coverage: status: project: diff --git a/man/sc_metrics_global.Rd b/man/sc_metrics_global.Rd index ba737e4..07f2ac5 100644 --- a/man/sc_metrics_global.Rd +++ b/man/sc_metrics_global.Rd @@ -30,7 +30,8 @@ columns are named with the \verb{_scaled} suffix.} If missing, uses \code{attr(sc, "step")} when available} \item{compactness}{A compactness value used for the supercells -If missing, uses \code{attr(sc, "compactness")} when available} +If missing, uses \code{attr(sc, "compactness")} when available. +Adaptive mode is read from \code{attr(sc, "adaptive_method")} when available.} \item{dist_fun}{A distance function name or function, as in \code{\link[=sc_slic]{sc_slic()}}. If missing or \code{NULL}, uses \code{attr(sc, "dist_fun")} when available.} @@ -46,8 +47,10 @@ Interpretation: values indicate spatial dominance; positive values indicate value dominance.} } \describe{ -\item{step}{Step size used to generate supercells.} +\item{step}{Step size used to generate supercells. Returned in meters when +the input used \code{step = use_meters(...)}, otherwise in cells.} \item{compactness}{Compactness value used to generate supercells.} +\item{adaptive_method}{Adaptive compactness method; \code{NA} for fixed compactness.} \item{n_supercells}{Number of supercells with at least one non-missing pixel.} \item{mean_value_dist}{Mean per-supercell value distance from cells to their supercell centers, averaged across supercells. Returned as \code{mean_value_dist} @@ -55,7 +58,7 @@ supercell centers, averaged across supercells. Returned as \code{mean_value_dist \item{mean_spatial_dist}{Mean per-supercell spatial distance from cells to their supercell centers, averaged across supercells; units are grid cells (row/column index distance). If the input supercells were created with -\code{step_unit = "map"}, distances are reported in map units. Returned as +\code{step = use_meters(...)}, distances are reported in meters. Returned as \code{mean_spatial_dist} (or \code{mean_spatial_dist_scaled} when \code{scale = TRUE}).} \item{mean_combined_dist}{Mean per-supercell combined distance, computed from value and spatial distances using \code{compactness} and \code{step}, averaged across @@ -75,7 +78,7 @@ If they are missing, they are derived from geometry and row order. Use \code{outcomes = c("supercells", "coordinates", "values")} when calling \code{sc_slic()} or \code{supercells()} to preserve original centers and IDs. Metrics are averaged across supercells (each supercell has equal weight). -When using SLIC0 (set \code{compactness = "auto"} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell +When using SLIC0 (set \code{compactness = use_adaptive()} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell adaptive compactness (SLIC0), and scaled value distances are computed with the per-supercell max value distance. } diff --git a/man/sc_metrics_pixels.Rd b/man/sc_metrics_pixels.Rd index 7b2bd20..36355bf 100644 --- a/man/sc_metrics_pixels.Rd +++ b/man/sc_metrics_pixels.Rd @@ -30,7 +30,8 @@ distances (\code{spatial_scaled}, \code{value_scaled}).} If missing, uses \code{attr(sc, "step")} when available} \item{compactness}{A compactness value used for the supercells -If missing, uses \code{attr(sc, "compactness")} when available} +If missing, uses \code{attr(sc, "compactness")} when available. +Adaptive mode is read from \code{attr(sc, "adaptive_method")} when available.} \item{dist_fun}{A distance function name or function, as in \code{\link[=sc_slic]{sc_slic()}}. If missing or \code{NULL}, uses \code{attr(sc, "dist_fun")} when available.} @@ -49,7 +50,7 @@ Metrics: \describe{ \item{spatial}{Spatial distance from each pixel to its supercell center in grid-cell units (row/column index distance). If the input supercells were -created with \code{step_unit = "map"}, distances are reported in map units.} +created with \code{step = use_meters(...)}, distances are reported in meters.} \item{value}{Value distance from each pixel to its supercell center in the raster value space.} \item{combined}{Combined distance using \code{compactness} and \code{step}.} @@ -65,7 +66,7 @@ Computes per-pixel distance diagnostics from each pixel to its supercell center \details{ If \code{sc} lacks \code{supercells}, \code{x}, or \code{y} columns, they are derived from geometry and row order, which may differ from the original centers. -When using SLIC0 (set \code{compactness = "auto"} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell +When using SLIC0 (set \code{compactness = use_adaptive()} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell adaptive compactness (SLIC0), and scaled value distances are computed with the per-supercell max value distance. } diff --git a/man/sc_metrics_supercells.Rd b/man/sc_metrics_supercells.Rd index fd62e7f..b8bfb13 100644 --- a/man/sc_metrics_supercells.Rd +++ b/man/sc_metrics_supercells.Rd @@ -30,7 +30,8 @@ columns are named with the \verb{_scaled} suffix.} If missing, uses \code{attr(sc, "step")} when available} \item{compactness}{A compactness value used for the supercells -If missing, uses \code{attr(sc, "compactness")} when available} +If missing, uses \code{attr(sc, "compactness")} when available. +Adaptive mode is read from \code{attr(sc, "adaptive_method")} when available.} \item{dist_fun}{A distance function name or function, as in \code{\link[=sc_slic]{sc_slic()}}. If missing or \code{NULL}, uses \code{attr(sc, "dist_fun")} when available.} @@ -50,7 +51,7 @@ Metrics: \item{supercells}{Supercell ID.} \item{spatial}{Mean spatial distance from cells to the supercell center in grid-cell units (row/column index distance). If the input supercells were -created with \code{step_unit = "map"}, distances are reported in map units. +created with \code{step = use_meters(...)}, distances are reported in meters. Returned as \code{mean_spatial_dist} (or \code{mean_spatial_dist_scaled} when \code{scale = TRUE}).} \item{value}{Mean value distance from cells to the supercell center in value space. Returned as \code{mean_value_dist} (or \code{mean_value_dist_scaled} @@ -67,7 +68,7 @@ Computes per-supercell distance diagnostics \details{ If \code{sc} lacks \code{supercells}, \code{x}, or \code{y} columns, they are derived from geometry and row order, which may differ from the original centers -When using SLIC0 (set \code{compactness = "auto"} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell +When using SLIC0 (set \code{compactness = use_adaptive()} in \code{\link[=sc_slic]{sc_slic()}}), combined and balance metrics use per-supercell adaptive compactness (SLIC0), and scaled value distances are computed with the per-supercell max value distance. } diff --git a/man/sc_plot_iter_diagnostics.Rd b/man/sc_plot_iter_diagnostics.Rd deleted file mode 100644 index 2296de1..0000000 --- a/man/sc_plot_iter_diagnostics.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/sc_plot_iter_diagnostics.R -\name{sc_plot_iter_diagnostics} -\alias{sc_plot_iter_diagnostics} -\title{Plot iteration diagnostics} -\usage{ -sc_plot_iter_diagnostics(x) -} -\arguments{ -\item{x}{A supercells object with an \code{iter_diagnostics} attribute, -or a diagnostics list containing \code{mean_distance}} -} -\value{ -Invisibly returns \code{TRUE} when a plot is created -} -\description{ -Plot mean distance across iterations for a supercells run -} -\examples{ -library(supercells) -vol = terra::rast(system.file("raster/volcano.tif", package = "supercells")) -vol_sc = sc_slic_points(vol, step = 8, compactness = 1, iter_diagnostics = TRUE) -sc_plot_iter_diagnostics(vol_sc) -} -\seealso{ -\code{\link[=sc_slic]{sc_slic()}}, \code{\link[=sc_slic_points]{sc_slic_points()}} -} diff --git a/man/sc_slic.Rd b/man/sc_slic.Rd index 578ddb6..f8a7946 100644 --- a/man/sc_slic.Rd +++ b/man/sc_slic.Rd @@ -13,22 +13,22 @@ sc_slic( clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, - outcomes = "values", + outcomes = c("supercells", "coordinates", "values"), chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0 ) } \arguments{ \item{x}{An object of class SpatRaster (terra) or class stars (stars).} -\item{step}{The distance (number of cells) between initial centers (alternative is \code{k}).} +\item{step}{Initial center spacing (alternative is \code{k}). +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} \item{compactness}{A compactness value. Use \code{\link[=sc_tune_compactness]{sc_tune_compactness()}} to estimate it. -Set \code{compactness = "auto"} to enable adaptive compactness (SLIC0).} +Use \code{\link[=use_adaptive]{use_adaptive()}} to enable adaptive compactness (SLIC0).} \item{dist_fun}{A distance function name or a custom function. Supported names: "euclidean", "jsd", "dtw", "dtw2d", or any method from \code{philentropy::getDistMethods()}. @@ -44,29 +44,25 @@ function must accept a numeric vector and return a single numeric value.} \item{iter}{Number of iterations.} -\item{step_unit}{Units for \code{step}. Use "cells" for pixel units or "map" for map units -(converted to cells using raster resolution).} - \item{k}{The number of supercells desired (alternative to \code{step}).} \item{centers}{Optional sf object of custom centers. Requires \code{step}.} \item{outcomes}{Character vector controlling which fields are returned. Allowed values are "supercells", "coordinates", and "values". Default is -"values". Use \code{outcomes = c("supercells", "coordinates", "values")} for full output.} +full output (\code{c("supercells", "coordinates", "values")}). +Use \code{outcomes = "values"} for value summaries only.} \item{chunks}{Chunking option. Use \code{FALSE} for no chunking, \code{TRUE} for automatic chunking based on size, or a numeric value for a fixed chunk size (in number of cells per side).} -\item{iter_diagnostics}{Logical. If \code{TRUE}, attaches iteration diagnostics as an -attribute (\code{iter_diagnostics}) on the output. Only available when chunks are not used.} - \item{verbose}{Verbosity level.} } \value{ An sf object with the supercell polygons and summary statistics. -Information on \code{step} and \code{compactness} are attached to the result as attributes. +Information on \code{step}, \code{compactness}, and \code{adaptive_method} are attached to +the result as attributes (\code{adaptive_method} is \code{NULL} for fixed compactness). } \description{ Creates supercells from single- or multi-band rasters using an extended SLIC algorithm. @@ -78,9 +74,7 @@ Use \code{\link[=sc_slic]{sc_slic()}} for polygon outputs. For raster or point c \code{\link[=sc_slic_raster]{sc_slic_raster()}} and \code{\link[=sc_slic_points]{sc_slic_points()}}. Evaluation and diagnostic options: \itemize{ -\item Iteration diagnostics: set \code{iter_diagnostics = TRUE} to attach an -\code{iter_diagnostics} attribute (only available without chunking). Use -\code{\link[=sc_plot_iter_diagnostics]{sc_plot_iter_diagnostics()}} to visualize the convergence over iterations. +\item Iteration convergence: use \code{\link[=sc_slic_convergence]{sc_slic_convergence()}} and plot its output. \item Pixel diagnostics: \code{\link[=sc_metrics_pixels]{sc_metrics_pixels()}} for per-pixel spatial, value, and combined distances. \item Cluster diagnostics: \code{\link[=sc_metrics_supercells]{sc_metrics_supercells()}} for per-supercell summaries. @@ -102,6 +96,6 @@ Achanta, R., Shaji, A., Smith, K., Lucchi, A., Fua, P., & Süsstrunk, S. (2012). Nowosad, J., Stepinski, T. (2022). Extended SLIC superpixels algorithm for applications to non-imagery geospatial rasters. International Journal of Applied Earth Observation and Geoinformation, https://doi.org/10.1016/j.jag.2022.102935 } \seealso{ -\code{\link[=sc_slic_raster]{sc_slic_raster()}}, \code{\link[=sc_slic_points]{sc_slic_points()}}, \code{\link[=sc_plot_iter_diagnostics]{sc_plot_iter_diagnostics()}}, +\code{\link[=use_meters]{use_meters()}}, \code{\link[=use_adaptive]{use_adaptive()}}, \code{\link[=sc_slic_raster]{sc_slic_raster()}}, \code{\link[=sc_slic_points]{sc_slic_points()}}, \code{\link[=sc_slic_convergence]{sc_slic_convergence()}}, \code{\link[=sc_metrics_pixels]{sc_metrics_pixels()}}, \code{\link[=sc_metrics_supercells]{sc_metrics_supercells()}}, \code{\link[=sc_metrics_global]{sc_metrics_global()}} } diff --git a/man/sc_slic_convergence.Rd b/man/sc_slic_convergence.Rd new file mode 100644 index 0000000..68cc43d --- /dev/null +++ b/man/sc_slic_convergence.Rd @@ -0,0 +1,75 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sc_slic_convergence.R +\name{sc_slic_convergence} +\alias{sc_slic_convergence} +\alias{plot.sc_slic_convergence} +\title{SLIC convergence diagnostics} +\usage{ +sc_slic_convergence( + x, + step = NULL, + compactness, + dist_fun = "euclidean", + avg_fun = "mean", + clean = TRUE, + minarea, + iter = 10, + k = NULL, + centers = NULL, + verbose = 0 +) + +\method{plot}{sc_slic_convergence}(x, ...) +} +\arguments{ +\item{x}{An object of class SpatRaster (terra) or class stars (stars).} + +\item{step}{Initial center spacing (alternative is \code{k}). +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} + +\item{compactness}{A compactness value. Use \code{\link[=sc_tune_compactness]{sc_tune_compactness()}} to estimate it. +Use \code{\link[=use_adaptive]{use_adaptive()}} to enable adaptive compactness (SLIC0).} + +\item{dist_fun}{A distance function name or a custom function. Supported names: +"euclidean", "jsd", "dtw", "dtw2d", or any method from \code{philentropy::getDistMethods()}. +A custom function must accept two numeric vectors and return a single numeric value.} + +\item{avg_fun}{An averaging function name or custom function used to summarize +values within each supercell. Supported names: "mean" and "median". A custom +function must accept a numeric vector and return a single numeric value.} + +\item{clean}{Should connectivity of the supercells be enforced?} + +\item{minarea}{Minimal size of a supercell (in cells).} + +\item{iter}{Number of iterations.} + +\item{k}{The number of supercells desired (alternative to \code{step}).} + +\item{centers}{Optional sf object of custom centers. Requires \code{step}.} + +\item{verbose}{Verbosity level.} + +\item{...}{Additional arguments passed to \code{\link[graphics:plot.default]{graphics::plot()}}.} +} +\value{ +A data frame with class \code{sc_slic_convergence} and columns: +\describe{ +\item{iter}{Iteration number.} +\item{mean_distance}{Mean combined distance across assigned cells at each iteration.} +} +} +\description{ +Runs SLIC and returns per-iteration mean combined distance. +The output can be plotted directly with \code{\link[=plot.sc_slic_convergence]{plot()}}. +} +\examples{ +library(supercells) +vol = terra::rast(system.file("raster/volcano.tif", package = "supercells")) +conv = sc_slic_convergence(vol, step = 8, compactness = 5, iter = 10) +plot(conv) +} +\seealso{ +\code{\link[=sc_slic]{sc_slic()}}, \code{\link[=plot.sc_slic_convergence]{plot()}} +} diff --git a/man/sc_slic_points.Rd b/man/sc_slic_points.Rd index 4852846..c8e0db6 100644 --- a/man/sc_slic_points.Rd +++ b/man/sc_slic_points.Rd @@ -13,22 +13,22 @@ sc_slic_points( clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, outcomes = "values", chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0 ) } \arguments{ \item{x}{An object of class SpatRaster (terra) or class stars (stars).} -\item{step}{The distance (number of cells) between initial centers (alternative is \code{k}).} +\item{step}{Initial center spacing (alternative is \code{k}). +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} \item{compactness}{A compactness value. Use \code{\link[=sc_tune_compactness]{sc_tune_compactness()}} to estimate it. -Set \code{compactness = "auto"} to enable adaptive compactness (SLIC0).} +Use \code{\link[=use_adaptive]{use_adaptive()}} to enable adaptive compactness (SLIC0).} \item{dist_fun}{A distance function name or a custom function. Supported names: "euclidean", "jsd", "dtw", "dtw2d", or any method from \code{philentropy::getDistMethods()}. @@ -44,9 +44,6 @@ function must accept a numeric vector and return a single numeric value.} \item{iter}{Number of iterations.} -\item{step_unit}{Units for \code{step}. Use "cells" for pixel units or "map" for map units -(converted to cells using raster resolution).} - \item{k}{The number of supercells desired (alternative to \code{step}).} \item{centers}{Optional sf object of custom centers. Requires \code{step}.} @@ -59,9 +56,6 @@ Allowed values are "supercells", "coordinates", and "values". Default is automatic chunking based on size, or a numeric value for a fixed chunk size (in number of cells per side).} -\item{iter_diagnostics}{Logical. If \code{TRUE}, attaches iteration diagnostics as an -attribute (\code{iter_diagnostics}) on the output. Only available when chunks are not used.} - \item{verbose}{Verbosity level.} } \value{ diff --git a/man/sc_slic_raster.Rd b/man/sc_slic_raster.Rd index ea095ae..f0ed25c 100644 --- a/man/sc_slic_raster.Rd +++ b/man/sc_slic_raster.Rd @@ -13,22 +13,22 @@ sc_slic_raster( clean = TRUE, minarea, iter = 10, - step_unit = "cells", k = NULL, centers = NULL, outcomes = "supercells", chunks = FALSE, - iter_diagnostics = FALSE, verbose = 0 ) } \arguments{ \item{x}{An object of class SpatRaster (terra) or class stars (stars).} -\item{step}{The distance (number of cells) between initial centers (alternative is \code{k}).} +\item{step}{Initial center spacing (alternative is \code{k}). +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} \item{compactness}{A compactness value. Use \code{\link[=sc_tune_compactness]{sc_tune_compactness()}} to estimate it. -Set \code{compactness = "auto"} to enable adaptive compactness (SLIC0).} +Use \code{\link[=use_adaptive]{use_adaptive()}} to enable adaptive compactness (SLIC0).} \item{dist_fun}{A distance function name or a custom function. Supported names: "euclidean", "jsd", "dtw", "dtw2d", or any method from \code{philentropy::getDistMethods()}. @@ -44,9 +44,6 @@ function must accept a numeric vector and return a single numeric value.} \item{iter}{Number of iterations.} -\item{step_unit}{Units for \code{step}. Use "cells" for pixel units or "map" for map units -(converted to cells using raster resolution).} - \item{k}{The number of supercells desired (alternative to \code{step}).} \item{centers}{Optional sf object of custom centers. Requires \code{step}.} @@ -58,9 +55,6 @@ Only \code{"supercells"} is supported in \code{sc_slic_raster()}.} automatic chunking based on size, or a numeric value for a fixed chunk size (in number of cells per side).} -\item{iter_diagnostics}{Logical. If \code{TRUE}, attaches iteration diagnostics as an -attribute (\code{iter_diagnostics}) on the output. Only available when chunks are not used.} - \item{verbose}{Verbosity level.} } \value{ diff --git a/man/sc_tune_compactness.Rd b/man/sc_tune_compactness.Rd index ecc9ce4..ffe0d92 100644 --- a/man/sc_tune_compactness.Rd +++ b/man/sc_tune_compactness.Rd @@ -7,9 +7,8 @@ sc_tune_compactness( raster, step = NULL, - step_unit = "cells", compactness = 1, - metrics = "global", + metric = "global", dist_fun = "euclidean", avg_fun = "mean", clean = TRUE, @@ -24,13 +23,13 @@ sc_tune_compactness( \arguments{ \item{raster}{A \code{SpatRaster}.} -\item{step}{The distance (number of cells) between initial centers (alternative is \code{k}).} - -\item{step_unit}{Units for \code{step}. Use "cells" for pixel units or "map" for map units.} +\item{step}{Initial center spacing (alternative is \code{k}). +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} \item{compactness}{Starting compactness used for the initial short run.} -\item{metrics}{Which compactness metric to return: \code{"global"} or \code{"local"}. +\item{metric}{Which compactness metric to return: \code{"global"} or \code{"local"}. Default: \code{"global"}.} \item{dist_fun}{A distance function name or a custom function. Supported names: @@ -54,12 +53,15 @@ function must accept a numeric vector and return a single numeric value.} \item{sample_size}{Optional limit on the number of pixels used for the summary (passed to \code{terra::global()} as \code{maxcell}).} -\item{value_scale}{Optional scale factor applied to the median value distance -before computing compactness. Use \code{"auto"} to divide by \code{sqrt(nlyr(raster))} -(useful for high-dimensional embeddings). Default: \code{"auto"}.} +\item{value_scale}{Scale factor for value distances during tuning. +Global metric: \code{compactness = (median(d_value) / value_scale) * step / median(d_spatial)}. +Local metric: \code{compactness = median(local_mean(d_value) / value_scale)}. +\code{"auto"} uses \code{sqrt(nlyr(raster))} (good for Euclidean-like distances); +for bounded/angular distances (e.g., cosine), \code{value_scale = 1} is often better. +Default: \code{"auto"}.} } \value{ -A one-row data frame with columns \code{step}, \code{metric}, and \code{compactness}. +A one-row data frame with columns \code{step}, \code{metric}, \code{dist_fun}, and \code{compactness}. } \description{ Runs a short SLIC segmentation (default \code{iter = 1}) and uses cell-level @@ -76,5 +78,5 @@ tune$compactness } \seealso{ -\code{\link[=sc_slic]{sc_slic()}} +\code{\link[=sc_slic]{sc_slic()}}, \code{\link[=use_meters]{use_meters()}}, \code{\link[=use_adaptive]{use_adaptive()}} } diff --git a/man/supercells.Rd b/man/supercells.Rd index a64029b..c4b8ee4 100644 --- a/man/supercells.Rd +++ b/man/supercells.Rd @@ -28,7 +28,7 @@ You can use either \code{k} or \code{step}. It is also possible to provide a set of points (an \code{sf} object) as \code{k} together with the \code{step} value to create custom cluster centers.} \item{compactness}{A compactness value. Larger values cause supercells to be more compact/even (square). -Set \code{compactness = "auto"} to enable adaptive compactness (SLIC0). +Use \code{\link[=use_adaptive]{use_adaptive()}} to enable adaptive compactness (SLIC0). A compactness value depends on the range of input cell values and selected distance measure.} \item{dist_fun}{A distance function. Currently implemented distance functions are \code{"euclidean"}, \code{"jsd"}, \code{"dtw"} (dynamic time warping), name of any distance function from the \code{philentropy} package (see \code{\link[philentropy:getDistMethods]{philentropy::getDistMethods()}}; "log2" is used in this case), or any user defined function accepting two vectors and returning one value. Default: \code{"euclidean"}} @@ -41,7 +41,9 @@ A compactness value depends on the range of input cell values and selected dista \item{transform}{Transformation to be performed on the input. By default, no transformation is performed. Currently available transformation is "to_LAB": first, the conversion from RGB to the LAB color space is applied, then the supercells algorithm is run, and afterward, a reverse transformation is performed on the obtained results. (This argument is experimental and may be removed in the future).} -\item{step}{The distance (number of cells) between initial supercells' centers. You can use either \code{k} or \code{step}.} +\item{step}{Initial center spacing. You can use either \code{k} or \code{step}. +Provide a plain numeric value for cell units, or use \code{\link[=use_meters]{use_meters()}} for +map-distance steps in meters (automatically converted to cells using raster resolution).} \item{minarea}{Specifies the minimal size of a supercell (in cells). Only works when \code{clean = TRUE}. By default, when \code{clean = TRUE}, average area (A) is calculated based on the total number of cells divided by a number of supercells diff --git a/man/use_adaptive.Rd b/man/use_adaptive.Rd new file mode 100644 index 0000000..1398dfa --- /dev/null +++ b/man/use_adaptive.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_adaptive.R +\name{use_adaptive} +\alias{use_adaptive} +\title{Use adaptive compactness mode} +\usage{ +use_adaptive(method = "local_max") +} +\arguments{ +\item{method}{Adaptive compactness method. Currently only \code{"local_max"} is supported +(SLIC0-style).} +} +\value{ +An adaptive compactness mode object for \code{compactness} arguments. +} +\description{ +Creates a compactness mode object for adaptive compactness. +The \code{"local_max"} method corresponds to SLIC0-style local scaling, +where compactness is adapted using local maximum value distances. +} +\examples{ +use_adaptive() + +} diff --git a/man/use_meters.Rd b/man/use_meters.Rd new file mode 100644 index 0000000..8f61005 --- /dev/null +++ b/man/use_meters.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_meters.R +\name{use_meters} +\alias{use_meters} +\title{Mark step values as meters} +\usage{ +use_meters(x) +} +\arguments{ +\item{x}{A single positive numeric value.} +} +\value{ +A \link[units:units]{units::units} object in meters (\code{m}). +} +\description{ +Creates a units value in meters for use in \code{step} arguments. +Use plain numerics for cell units, and \code{use_meters()} for map-distance steps. +} +\examples{ +use_meters(100) + +} diff --git a/tests/testthat/test-sc-create.R b/tests/testthat/test-sc-create.R index fd8a60e..93cfe96 100644 --- a/tests/testthat/test-sc-create.R +++ b/tests/testthat/test-sc-create.R @@ -1,29 +1,29 @@ -test_that("sc_slic returns sf with attributes", { +test_that("sc_slic returns core output and attributes", { sc = sc_slic(v1, step = 8, compactness = 1) expect_s3_class(sc, "sf") - expect_false(any(c("supercells", "x", "y") %in% names(sc))) - expect_equal(attr(sc, "step"), attr(sc, "step")) - expect_equal(attr(sc, "compactness"), 1) - expect_equal(attr(sc, "method"), "slic") expect_true("supercells" %in% class(sc)) + expect_true(all(c("supercells", "x", "y") %in% names(sc))) + expect_false(is.null(attr(sc, "step"))) + expect_equal(attr(sc, "compactness"), 1) + expect_null(attr(sc, "adaptive_method")) + + sc_auto = sc_slic(v1, step = 8, compactness = use_adaptive()) + expect_equal(attr(sc_auto, "compactness"), 0) + expect_equal(attr(sc_auto, "adaptive_method"), "local_max") }) -test_that("sc_slic handles step and centers", { +test_that("sc_slic supports custom centers", { set.seed(2021-11-21) centers = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 50, type = "random")) - sc = sc_slic(v1, centers = centers, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) + sc = sc_slic(v1, centers = centers, step = 8, compactness = 1) expect_s3_class(sc, "sf") }) -test_that("sc_slic_raster returns raster with attributes", { +test_that("sc_slic_raster matches rasterized sc_slic output", { + sc = sc_slic(v1, step = 8, compactness = 1, outcomes = "supercells") sc_r = sc_slic_raster(v1, step = 8, compactness = 1) expect_s4_class(sc_r, "SpatRaster") -}) -test_that("sc_slic_raster matches rasterized sc_slic", { - sc = sc_slic(v1, step = 8, compactness = 1, outcomes = "supercells") - sc_r = sc_slic_raster(v1, step = 8, compactness = 1) ref_r = terra::rasterize(terra::vect(sc), v1, field = "supercells") ref_vals = terra::values(ref_r, mat = FALSE) out_vals = terra::values(sc_r, mat = FALSE) @@ -38,18 +38,18 @@ test_that("sc_slic_raster assigns unique ids across chunks", { "rounded up", fixed = TRUE ) + chunk_ext = .sc_chunk_extents(dim(v1), limit = ceiling(chunks / step) * step) ranges = lapply(seq_len(nrow(chunk_ext)), function(i) { ext = chunk_ext[i, ] chunk = sc_r[ext[1]:ext[2], ext[3]:ext[4], drop = FALSE] vals = terra::values(chunk, mat = FALSE) vals = vals[!is.na(vals)] - if (length(vals) == 0) { - return(NULL) - } + if (length(vals) == 0) return(NULL) c(min = min(vals), max = max(vals)) }) ranges = Filter(Negate(is.null), ranges) + if (length(ranges) > 1) { mins = vapply(ranges, function(x) x[["min"]], numeric(1)) maxs = vapply(ranges, function(x) x[["max"]], numeric(1)) @@ -57,19 +57,75 @@ test_that("sc_slic_raster assigns unique ids across chunks", { } }) -test_that("auto chunk size aligns to step", { - step = 8 +test_that("sc_slic_raster enforces raster-specific guardrails", { + expect_error( + sc_slic_raster(v1, step = 8, compactness = 1, iter = 0), + "iter = 0 returns centers only", + fixed = TRUE + ) + expect_error( + sc_slic_raster(v1, step = 8, compactness = 1, outcomes = "values"), + "supports only outcomes = 'supercells'", + fixed = TRUE + ) +}) + +test_that("sc_slic_raster works with chunks = TRUE", { old_opt = getOption("supercells.chunk_mem_gb") options(supercells.chunk_mem_gb = 0.001) on.exit(options(supercells.chunk_mem_gb = old_opt), add = TRUE) - wsize = .sc_chunk_optimize_size(dim(v1), getOption("supercells.chunk_mem_gb"), step = step) - expect_true(wsize %% step == 0) - expect_true(wsize >= step) + + sc_r = sc_slic_raster(v1, step = 8, compactness = 1, chunks = TRUE) + expect_s4_class(sc_r, "SpatRaster") + expect_equal(names(sc_r), "supercells") }) -test_that("sc_slic validates arguments", { +test_that("sc_slic validates key argument errors", { expect_error(sc_slic(v1, k = 10), "compactness", fixed = TRUE) expect_error(sc_slic(v1, k = 10, step = 5, compactness = 1), "either 'k' or 'step'", fixed = TRUE) expect_error(sc_slic(v1, centers = sf::st_sf(geom = sf::st_sfc()), compactness = 1), "step", fixed = TRUE) expect_error(sc_slic(v1, step = 8, compactness = 1, dist_fun = "not_a_dist"), "does not exist", fixed = TRUE) }) + +test_that("sc_slic handles step units and unit-related errors", { + step_map = use_meters(8 * terra::res(v1)[1]) + sc = sc_slic(v1, step = step_map, compactness = 1) + expect_s3_class(attr(sc, "step"), "units") + expect_equal(as.numeric(units::drop_units(attr(sc, "step"))), as.numeric(units::drop_units(step_map))) + + x_lonlat = terra::rast(nrows = 50, ncols = 50, xmin = 0, xmax = 1, ymin = 0, ymax = 1, crs = "EPSG:4326") + terra::values(x_lonlat) = seq_len(terra::ncell(x_lonlat)) + expect_error( + suppressWarnings(sc_slic(x_lonlat, step = use_meters(1000), compactness = 1)), + "projected CRS", + fixed = TRUE + ) + + expect_error( + sc_slic(v1, step = units::set_units(1, "km"), compactness = 1), + "must use meters", + fixed = TRUE + ) + + x_non_meter = terra::rast(nrows = 50, ncols = 50, xmin = 0, xmax = 5000, ymin = 0, ymax = 5000, crs = "EPSG:2277") + terra::values(x_non_meter) = seq_len(terra::ncell(x_non_meter)) + expect_error( + suppressWarnings(sc_slic(x_non_meter, step = use_meters(100), compactness = 1)), + "meter units", + fixed = TRUE + ) +}) + +test_that("use_meters validates inputs", { + x = use_meters(100) + expect_s3_class(x, "units") + expect_equal(as.character(units::deparse_unit(x)), "m") + expect_error(use_meters(-1), "single positive number", fixed = TRUE) + expect_error(use_meters(c(1, 2)), "single positive number", fixed = TRUE) +}) + +test_that("use_adaptive validates input", { + x = use_adaptive() + expect_s3_class(x, "sc_adaptive") + expect_error(use_adaptive("other"), "must be 'local_max'", fixed = TRUE) +}) diff --git a/tests/testthat/test-sc-metrics.R b/tests/testthat/test-sc-metrics.R index 62088f7..c05d37f 100644 --- a/tests/testthat/test-sc-metrics.R +++ b/tests/testthat/test-sc-metrics.R @@ -1,69 +1,63 @@ -test_that("sc_metrics_pixels returns raster with layers", { - sc = sc_slic(v1, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) - pix = sc_metrics_pixels(v1, sc) +sc_full = sc_slic(v1, step = 8, compactness = 1, + outcomes = c("supercells", "coordinates", "values")) +sc_values = sc_slic(v1, step = 8, compactness = 1, outcomes = "values") +sc_auto = sc_slic(v1, step = 8, compactness = use_adaptive(), + outcomes = c("supercells", "coordinates", "values")) +manhattan = function(a, b) sum(abs(a - b)) +sc_custom = sc_slic(v1, step = 8, compactness = 1, dist_fun = manhattan, + outcomes = c("supercells", "coordinates", "values")) + +test_that("metrics outputs have expected structure", { + pix = sc_metrics_pixels(v1, sc_full) expect_s4_class(pix, "SpatRaster") expect_equal(terra::nlyr(pix), 4) expect_true(all(c("spatial_scaled", "value_scaled", "combined", "balance") %in% names(pix))) -}) - -test_that("sc_metrics functions work without metadata columns", { - sc = sc_slic(v1, step = 8, compactness = 1) - pix = sc_metrics_pixels(v1, sc) - expect_s4_class(pix, "SpatRaster") - cl = sc_metrics_supercells(v1, sc) + cl = sc_metrics_supercells(v1, sc_full) expect_s3_class(cl, "sf") + expect_true(all(c("supercells", "mean_value_dist_scaled", "mean_spatial_dist_scaled", + "mean_combined_dist", "balance") %in% names(cl))) - gl = sc_metrics_global(v1, sc) + gl = sc_metrics_global(v1, sc_full) expect_s3_class(gl, "data.frame") + expect_equal(nrow(gl), 1) + expect_true(all(c("step", "compactness", "adaptive_method", "n_supercells", + "mean_value_dist_scaled", "mean_spatial_dist_scaled", + "mean_combined_dist", "balance") %in% names(gl))) }) -test_that("sc_metrics uses step and compactness from attributes", { - sc = sc_slic(v1, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) - gl = sc_metrics_global(v1, sc) - expect_equal(gl$step, attr(sc, "step")) - expect_equal(gl$compactness, attr(sc, "compactness")) +test_that("metrics work without metadata columns", { + expect_s4_class(sc_metrics_pixels(v1, sc_values), "SpatRaster") + expect_s3_class(sc_metrics_supercells(v1, sc_values), "sf") + expect_s3_class(sc_metrics_global(v1, sc_values), "data.frame") }) -test_that("sc_metrics_supercells returns sf with metrics", { - sc = sc_slic(v1, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) - cl = sc_metrics_supercells(v1, sc) - expect_s3_class(cl, "sf") - expect_true(all(c("supercells", "mean_value_dist_scaled", "mean_spatial_dist_scaled", - "mean_combined_dist", "balance") %in% names(cl))) - expect_true(all(is.finite(cl[["balance"]]) | is.na(cl[["balance"]]))) -}) +test_that("metrics use stored attributes and dist_fun defaults", { + gl = sc_metrics_global(v1, sc_full) + expect_equal(gl$step, attr(sc_full, "step")) + expect_equal(gl$compactness, attr(sc_full, "compactness")) + expect_true(is.na(gl$adaptive_method)) -test_that("sc_metrics_global returns single-row data.frame", { - sc = sc_slic(v1, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) - gl = sc_metrics_global(v1, sc) - expect_s3_class(gl, "data.frame") - expect_equal(nrow(gl), 1) - expect_true(all(c("step", "compactness", "n_supercells", - "mean_value_dist_scaled", "mean_spatial_dist_scaled", "mean_combined_dist", - "balance") %in% names(gl))) - expect_true(is.finite(gl[["balance"]]) | is.na(gl[["balance"]])) -}) + gl_auto = sc_metrics_global(v1, sc_auto) + expect_equal(gl_auto$compactness, 0) + expect_equal(gl_auto$adaptive_method, "local_max") -test_that("sc_metrics invalid dist_fun errors", { - sc = sc_slic(v1, step = 8, compactness = 1, - outcomes = c("supercells", "coordinates", "values")) - expect_error(sc_metrics_pixels(v1, sc, dist_fun = "not_a_dist"), "does not exist", fixed = TRUE) + g_attr = sc_metrics_global(v1, sc_custom) + g_explicit = sc_metrics_global(v1, sc_custom, dist_fun = manhattan) + expect_equal(g_attr$mean_value_dist_scaled, g_explicit$mean_value_dist_scaled, tolerance = 1e-8) + expect_equal(g_attr$mean_spatial_dist_scaled, g_explicit$mean_spatial_dist_scaled, tolerance = 1e-8) + expect_equal(g_attr$mean_combined_dist, g_explicit$mean_combined_dist, tolerance = 1e-8) }) -test_that("sc_metrics spatial units follow step_unit", { +test_that("metrics follow step encoding in cells vs meters", { v1_map = terra::aggregate(v1, fact = 2, fun = mean, na.rm = TRUE) res_map = terra::res(v1_map)[1] step_cells = 8 - step_map = step_cells * res_map + step_map = use_meters(step_cells * res_map) - sc_cells = sc_slic(v1_map, step = step_cells, compactness = 1, step_unit = "cells", + sc_cells = sc_slic(v1_map, step = step_cells, compactness = 1, outcomes = c("supercells", "coordinates", "values")) - sc_map = sc_slic(v1_map, step = step_map, compactness = 1, step_unit = "map", + sc_map = sc_slic(v1_map, step = step_map, compactness = 1, outcomes = c("supercells", "coordinates", "values")) g_cells = sc_metrics_global(v1_map, sc_cells, scale = FALSE) @@ -77,13 +71,39 @@ test_that("sc_metrics spatial units follow step_unit", { tolerance = 1e-6) }) -test_that("sc_metrics defaults to dist_fun attribute when missing", { - manhattan = function(a, b) sum(abs(a - b)) - sc = sc_slic(v1, step = 8, compactness = 1, dist_fun = manhattan, +test_that("metrics reject invalid dist_fun", { + expect_error(sc_metrics_pixels(v1, sc_full, dist_fun = "not_a_dist"), "does not exist", fixed = TRUE) +}) + +test_that("metrics work after save/read when explicit args are supplied", { + sc = sc_slic(v1, step = 8, compactness = 1, outcomes = c("supercells", "coordinates", "values")) - g_attr = sc_metrics_global(v1, sc) - g_explicit = sc_metrics_global(v1, sc, dist_fun = manhattan) - expect_equal(g_attr$mean_value_dist_scaled, g_explicit$mean_value_dist_scaled, tolerance = 1e-8) - expect_equal(g_attr$mean_spatial_dist_scaled, g_explicit$mean_spatial_dist_scaled, tolerance = 1e-8) - expect_equal(g_attr$mean_combined_dist, g_explicit$mean_combined_dist, tolerance = 1e-8) + gpkg = tempfile(fileext = ".gpkg") + sf::st_write(sc, gpkg, quiet = TRUE) + sc_disk = sf::st_read(gpkg, quiet = TRUE) + unlink(gpkg) + + expect_error( + sc_metrics_global(v1, sc_disk), + "required when it is not stored in 'sc'", + fixed = TRUE + ) + + gl = sc_metrics_global(v1, sc_disk, step = 8, compactness = 1, dist_fun = "euclidean") + expect_s3_class(gl, "data.frame") + expect_equal(nrow(gl), 1) + + pix = sc_metrics_pixels( + v1, sc_disk, + step = 8, compactness = 1, dist_fun = "euclidean", + metrics = "combined" + ) + expect_s4_class(pix, "SpatRaster") + + cl = sc_metrics_supercells( + v1, sc_disk, + step = 8, compactness = 1, dist_fun = "euclidean", + metrics = "combined" + ) + expect_s3_class(cl, "sf") }) diff --git a/tests/testthat/test-sc-slic-convergence.R b/tests/testthat/test-sc-slic-convergence.R new file mode 100644 index 0000000..26846ae --- /dev/null +++ b/tests/testthat/test-sc-slic-convergence.R @@ -0,0 +1,24 @@ +test_that("sc_slic_convergence returns plottable convergence data", { + conv = sc_slic_convergence(v1, step = 8, compactness = 1, iter = 5) + expect_s3_class(conv, "sc_slic_convergence") + expect_s3_class(conv, "data.frame") + expect_true(all(c("iter", "mean_distance") %in% names(conv))) + expect_equal(nrow(conv), 5) + expect_equal(conv$iter, seq_len(5)) + expect_true(all(is.finite(conv$mean_distance))) + tmp = tempfile(fileext = ".pdf") + grDevices::pdf(tmp) + on.exit({ + grDevices::dev.off() + unlink(tmp) + }, add = TRUE) + expect_silent(plot(conv)) +}) + +test_that("sc_slic_convergence validates iter", { + expect_error( + sc_slic_convergence(v1, step = 8, compactness = 1, iter = 0), + "iter must be > 0", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-sc-tune-compactness.R b/tests/testthat/test-sc-tune-compactness.R index 16eee12..8ed7c6e 100644 --- a/tests/testthat/test-sc-tune-compactness.R +++ b/tests/testthat/test-sc-tune-compactness.R @@ -1,19 +1,45 @@ -test_that("sc_tune_compactness returns one-row data frame", { - tune = sc_tune_compactness(v1, step = 8, iter = 1, sample_size = 500) - expect_s3_class(tune, "data.frame") - expect_equal(nrow(tune), 1) - expect_true(all(c("step", "metric", "compactness") %in% names(tune))) - expect_equal(tune$step, 8) - expect_equal(tune$metric, "global") - expect_true(tune$compactness > 0) +test_that("sc_tune_compactness returns one-row output for both metrics", { + for (metric in c("global", "local")) { + tune = sc_tune_compactness(v1, step = 8, iter = 1, sample_size = 500, metric = metric) + expect_s3_class(tune, "data.frame") + expect_equal(nrow(tune), 1) + expect_true(all(c("step", "metric", "dist_fun", "compactness") %in% names(tune))) + expect_equal(tune$step, 8) + expect_equal(tune$metric, metric) + expect_equal(tune$dist_fun, "euclidean") + expect_true(tune$compactness > 0) + } }) -test_that("sc_tune_compactness returns local compactness when requested", { - tune = sc_tune_compactness(v1, step = 8, iter = 1, sample_size = 500, metrics = "local") - expect_s3_class(tune, "data.frame") - expect_equal(nrow(tune), 1) - expect_true(all(c("step", "metric", "compactness") %in% names(tune))) - expect_equal(tune$step, 8) - expect_equal(tune$metric, "local") - expect_true(tune$compactness > 0) +test_that("sc_tune_compactness stores custom dist_fun label", { + manhattan = function(a, b) sum(abs(a - b)) + tune = sc_tune_compactness(v1, step = 8, iter = 1, sample_size = 500, dist_fun = manhattan) + expect_equal(tune$dist_fun, "custom") +}) + +test_that("sc_tune_compactness value_scale rescales compactness and validates input", { + g1 = sc_tune_compactness( + v1, step = 8, iter = 1, metric = "global", + value_scale = 1, sample_size = terra::ncell(v1) + ) + g2 = sc_tune_compactness( + v1, step = 8, iter = 1, metric = "global", + value_scale = 2, sample_size = terra::ncell(v1) + ) + expect_equal(g1$compactness / g2$compactness, 2, tolerance = 1e-6) + + l1 = sc_tune_compactness(v1, step = 8, iter = 1, metric = "local", value_scale = 1) + l2 = sc_tune_compactness(v1, step = 8, iter = 1, metric = "local", value_scale = 2) + expect_equal(l1$compactness / l2$compactness, 2, tolerance = 1e-6) + + expect_error( + sc_tune_compactness(v1, step = 8, value_scale = 0), + "value_scale must be a single positive number or 'auto'", + fixed = TRUE + ) + expect_error( + sc_tune_compactness(v1, step = 8, value_scale = "bad"), + "value_scale must be a single positive number or 'auto'", + fixed = TRUE + ) }) diff --git a/tests/testthat/test-supercells-chunks.R b/tests/testthat/test-supercells-chunks.R deleted file mode 100644 index be67b2b..0000000 --- a/tests/testthat/test-supercells-chunks.R +++ /dev/null @@ -1,6 +0,0 @@ -v3_a = supercells(v3, 100, compactness = 1, chunk = 150) - -test_that("supercells works for many chunks", { - expect_equal(ncol(v3_a), 7) - expect_equal(as.numeric(sf::st_bbox(v3_a)), unname(as.vector(terra::ext(v3)))[c(1, 3, 2, 4)]) -}) diff --git a/tests/testthat/test-supercells-custom-centers.R b/tests/testthat/test-supercells-custom-centers.R deleted file mode 100644 index b0413ac..0000000 --- a/tests/testthat/test-supercells-custom-centers.R +++ /dev/null @@ -1,14 +0,0 @@ -set.seed(2021-11-21) -y1 = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 100, type = "random")) -y2 = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 100, type = "regular")) -y3 = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 100, type = "hexagonal")) - -vol_slic1 = supercells(v1, k = y1, step = 10, compactness = 1, iter = 10, clean = TRUE) -vol_slic2 = supercells(v1, k = y2, step = 10, compactness = 1, iter = 10, clean = TRUE) -vol_slic3 = supercells(v1, k = y3, step = 10, compactness = 1, iter = 10, clean = TRUE) - -test_that("supercells works for custom centers", { - expect_equal(ncol(vol_slic1), 5) - expect_equal(ncol(vol_slic2), 5) - expect_equal(ncol(vol_slic3), 5) -}) diff --git a/tests/testthat/test-supercells-iter0.R b/tests/testthat/test-supercells-iter0.R deleted file mode 100644 index 399177a..0000000 --- a/tests/testthat/test-supercells-iter0.R +++ /dev/null @@ -1,11 +0,0 @@ -set.seed(2021-11-20) -y = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 100, type = "random")) - -vol_slic0a = supercells(v1, k = y, step = 10, compactness = 1, iter = 0) -vol_slic0b = supercells(v1, step = 10, compactness = 1, iter = 0) - -test_that("supercells works for 0 iter", { - expect_equal(ncol(vol_slic0a), 1 + 2 + terra::nlyr(v1) + 1) # supercells + x,y + layers + geometry - expect_equal(nrow(vol_slic0a), 98) - expect_equal(nrow(vol_slic0b), 54) -}) diff --git a/tests/testthat/test-supercells-legacy.R b/tests/testthat/test-supercells-legacy.R new file mode 100644 index 0000000..2eed1b0 --- /dev/null +++ b/tests/testthat/test-supercells-legacy.R @@ -0,0 +1,36 @@ +test_that("legacy supercells wrapper handles basic options", { + sc = supercells(v1, k = 100, compactness = 1) + expect_s3_class(sc, "sf") + expect_true(all(c("supercells", "x", "y") %in% names(sc))) + + sc_no_meta = supercells(v1, k = 30, compactness = 1, metadata = FALSE) + expect_false(any(c("supercells", "x", "y") %in% names(sc_no_meta))) + + expect_error( + supercells(v1, k = 10, compactness = 1, dist_fun = "not_a_dist"), + "does not exist", + fixed = TRUE + ) +}) + +test_that("legacy supercells wrapper supports iter = 0 and custom centers", { + set.seed(2021-11-20) + centers = sf::st_sf(geom = sf::st_sample(sf::st_as_sfc(sf::st_bbox(v1)), 100, type = "random")) + + sc_custom = supercells(v1, k = centers, step = 10, compactness = 1, iter = 0) + sc_default = supercells(v1, step = 10, compactness = 1, iter = 0) + expect_s3_class(sc_custom, "sf") + expect_s3_class(sc_default, "sf") + expect_true(nrow(sc_custom) > 0) + expect_true(nrow(sc_default) > 0) +}) + +test_that("legacy supercells wrapper supports transform and chunked calls", { + sc_lab = supercells(v3, 100, compactness = 1, transform = "to_LAB") + sc_chunk = supercells(v3, 100, compactness = 1, chunk = 150) + + expect_s3_class(sc_lab, "sf") + expect_s3_class(sc_chunk, "sf") + expect_equal(as.numeric(sf::st_bbox(sc_lab)), unname(as.vector(terra::ext(v3)))[c(1, 3, 2, 4)]) + expect_equal(as.numeric(sf::st_bbox(sc_chunk)), unname(as.vector(terra::ext(v3)))[c(1, 3, 2, 4)]) +}) diff --git a/tests/testthat/test-supercells-options.R b/tests/testthat/test-supercells-options.R deleted file mode 100644 index c959440..0000000 --- a/tests/testthat/test-supercells-options.R +++ /dev/null @@ -1,12 +0,0 @@ -test_that("metadata columns can be removed", { - v1_no_meta = supercells(v1, k = 30, compactness = 1, metadata = FALSE) - expect_false(any(c("supercells", "x", "y") %in% names(v1_no_meta))) -}) - -test_that("invalid dist_fun errors", { - expect_error( - supercells(v1, k = 10, compactness = 1, dist_fun = "not_a_dist"), - "does not exist", - fixed = TRUE - ) -}) diff --git a/tests/testthat/test-supercells-v1.R b/tests/testthat/test-supercells-v1.R deleted file mode 100644 index 673681f..0000000 --- a/tests/testthat/test-supercells-v1.R +++ /dev/null @@ -1,34 +0,0 @@ -v1_a = supercells(v1, 100, compactness = 1) -v1_b = supercells(v1, 100, compactness = 1, clean = FALSE) -v1_c = supercells(v1, step = 8, compactness = 1) -my_minarea = 3 -v1_d = supercells(v1, step = 8, compactness = 1, minarea = my_minarea) -v1_e = supercells(v1, step = 8, compactness = 0.1, avg_fun = "median", dist_fun = "jaccard") -v1_f = supercells(v1, 100, compactness = 1, avg_fun = mean) - -test_that("supercells works for 1 var", { - expect_equal(ncol(v1_a), 5) - expect_equal(ncol(v1_a), ncol(v1_b)) - expect_equal(nrow(v1_a), 90) - expect_equal(nrow(v1_b), 88) - expect_equal(nrow(v1_e), 88) - expect_equal(v1_a, v1_c) - expect_true(all(as.numeric(sf::st_area(v1_d)) >= xres(v1) * yres(v1) * my_minarea)) - expect_equal(v1_a, v1_f) -}) - -# test_that("supercells matches reference (no geometry)", { -# ref_path = testthat::test_path("testdata", "v1-supercells-v1.rds") -# testthat::skip_if_not(file.exists(ref_path), "Reference file missing; create with old package version.") - -# current = list( -# v1_a = sf::st_drop_geometry(v1_a), -# v1_b = sf::st_drop_geometry(v1_b), -# v1_c = sf::st_drop_geometry(v1_c), -# v1_d = sf::st_drop_geometry(v1_d), -# v1_e = sf::st_drop_geometry(v1_e), -# v1_f = sf::st_drop_geometry(v1_f) -# ) -# reference = readRDS(ref_path) -# expect_equal(current, reference, tolerance = 1e-6) -# }) diff --git a/tests/testthat/test-supercells-v3.R b/tests/testthat/test-supercells-v3.R deleted file mode 100644 index 18111c2..0000000 --- a/tests/testthat/test-supercells-v3.R +++ /dev/null @@ -1,21 +0,0 @@ -v3_a = supercells(v3, 100, compactness = 1) -v3_b = supercells(v3, 100, compactness = 1, transform = "to_LAB") - -test_that("supercells works for 3 var", { - expect_equal(ncol(v3_a), 7) - # expect_equal(nrow(v3_a), 80) - # expect_equal(nrow(v3_b), 86) - expect_equal(as.numeric(sf::st_bbox(v3_a)), unname(as.vector(terra::ext(v3)))[c(1, 3, 2, 4)]) -}) - -# test_that("supercells matches reference (no geometry)", { -# ref_path = testthat::test_path("testdata", "v3-supercells-v1.rds") -# testthat::skip_if_not(file.exists(ref_path), "Reference file missing; create with old package version.") - -# current = list( -# v3_a = sf::st_drop_geometry(v3_a), -# v3_b = sf::st_drop_geometry(v3_b) -# ) -# reference = readRDS(ref_path) -# expect_equal(current, reference, tolerance = 1e-6) -# }) diff --git a/vignettes/articles/v2-changes-since-v1.Rmd b/vignettes/articles/v2-changes-since-v1.Rmd index c1d469a..02ea380 100644 --- a/vignettes/articles/v2-changes-since-v1.Rmd +++ b/vignettes/articles/v2-changes-since-v1.Rmd @@ -73,7 +73,7 @@ terra::plot(vol_ids) ``` While, `sc_slic()` is the main function, the other two functions are useful for specific tasks. -For example, `sc_slic_points()` is helpful for visualizing initial supercell centers or iteration diagnostics, while `sc_slic_raster()` is useful for large datasets where polygon outputs may be too memory-intensive. +For example, `sc_slic_points()` is helpful for visualizing initial supercell centers, while `sc_slic_raster()` is useful for large datasets where polygon outputs may be too memory-intensive. ## Compactness tuning and iteration diagnostics @@ -92,17 +92,17 @@ plot(sf::st_geometry(vol_sc_tuned), add = TRUE, lwd = 0.6, border = "red") This function also allow to calculate the compactness using second method called `"local"`. -`sc_slic_points(..., iter_diagnostics = TRUE)` attaches iteration diagnostics so you can visualize convergence in mean distance across iterations later on with `sc_plot_iter_diagnostics()`. +`sc_slic_convergence()` provides iteration diagnostics so you can visualize convergence in mean distance across iterations. ```{r} -# Iteration diagnostics plot (only available without chunking) -vol_pts_diag <- sc_slic_points( +# Iteration diagnostics plot +vol_conv <- sc_slic_convergence( vol, step = 8, compactness = 1, - iter_diagnostics = TRUE + iter = 10 ) -sc_plot_iter_diagnostics(vol_pts_diag) +plot(vol_conv) ``` ## Metrics for evaluating results @@ -140,10 +140,10 @@ global_metrics ## SLIC0 adaptive compactness -`compactness = "auto"` enables adaptive compactness (SLIC0). This lets the method adjust compactness across supercells rather than using a single fixed value. +`compactness = use_adaptive()` enables adaptive compactness (SLIC0). This lets the method adjust compactness across supercells rather than using a single fixed value. ```{r} -vol_sc_slic0 <- sc_slic(vol, step = 8, compactness = "auto") +vol_sc_slic0 <- sc_slic(vol, step = 8, compactness = use_adaptive()) # Plot results on top of the volcano raster terra::plot(vol) @@ -152,7 +152,7 @@ plot(sf::st_geometry(vol_sc_slic0), add = TRUE, lwd = 0.6, border = "violet") ## Other changes -- New utilities: Added `step_unit` to `sc_slic()`/`sc_slic_points()`/`sc_slic_raster()` to support map-unit step sizes. +- New utilities: Added `use_meters()` helper for `step` and `use_adaptive()` for adaptive compactness in `sc_slic()`/`sc_slic_points()`/`sc_slic_raster()`. - Behavior: Since version 1.0, the way coordinates are summarized internally has changed, and results in versions after 1.0 may differ slightly from those prior to 1.0. - Performance: Improved speed and memory efficiency. - New utilities: Added experimental `sc_merge_supercells()` for adjacency-constrained greedy merging. diff --git a/vignettes/articles/v2-evaluation.Rmd b/vignettes/articles/v2-evaluation.Rmd index 5d925fb..b2d0bca 100644 --- a/vignettes/articles/v2-evaluation.Rmd +++ b/vignettes/articles/v2-evaluation.Rmd @@ -26,7 +26,7 @@ D = \sqrt{\left(\frac{d_s}{\text{step}}\right)^2 + \left(\frac{d_v}{c}\right)^2} $$ where $d_s$ is the spatial distance in grid-cell units, $d_v$ is the value-space distance defined by `dist_fun`, and $c$ is `compactness`. - + All examples use the `volcano` raster for simplicity, but the same workflow applies to multi-layer rasters. @@ -47,7 +47,7 @@ Pixel metrics are provided with the `sc_metrics_pixels()` function, which accept The pixel metrics include four layers^[Use the `metrics` argument to request a subset of metrics], each with a specific interpretation and a simple definition: -1. `spatial`: $d_s$, the distance from each pixel to its supercell center in grid-cell units (unless the input supercells were created with `step_unit = "map"`, in which case distances are reported in map units). +1. `spatial`: $d_s$, the distance from each pixel to its supercell center in grid-cell units (unless the input supercells were created with `step = use_meters(...)`, in which case distances are reported in map units). Lower spatial values indicate cells that are closer to the center and more compact supercells, while higher values indicate cells that are farther from the center and may indicate irregular shapes or outliers. 2. `value`: $d_v$, the distance from each pixel to its supercell center in the value space defined by your `dist_fun`. Lower value distances indicate more homogeneous supercells, while higher values indicate more heterogeneous supercells or outliers. @@ -126,7 +126,7 @@ metrics_lower <- sc_metrics_global(vol, vol_sc_low) rbind(higher_compactness = metrics_higher, lower_compactness = metrics_lower) ``` -When using `compactness = "auto"`, the value scaling is per-supercell. +When using `compactness = use_adaptive()`, the value scaling is per-supercell. This improves local adaptation, but it also means metrics are not directly comparable to fixed-compactness outputs. In this case, you can still compare the spatial metrics, but the value metrics will reflect the local scaling rather than the original value distances. diff --git a/vignettes/articles/v2-intro.Rmd b/vignettes/articles/v2-intro.Rmd index dfb58f1..8f2c09b 100644 --- a/vignettes/articles/v2-intro.Rmd +++ b/vignettes/articles/v2-intro.Rmd @@ -42,7 +42,7 @@ It can be thought of as the expected spatial scale of the output. Lower values prioritize value similarity and may lead to irregular shapes, while higher values prioritize shape regularity -- supercells may look more like squares -- but may be less homogeneous in terms of values. For compactness selection, you may use `sc_tune_compactness()` to estimate a good value from a short pilot run. -Alternatively, the `"auto"` option for `compactness` enables SLIC0-style adaptive compactness when you want the algorithm to adjust locally -- this does not require setting a specific value, but also takes away direct control. +Alternatively, `compactness = use_adaptive()` enables SLIC0-style adaptive compactness when you want the algorithm to adjust locally -- this does not require setting a specific value, but also takes away direct control. To assess quality of the resulting supercells, use `sc_metrics_pixels()` for pixel-level distances, `sc_metrics_supercells()` for per-supercell summaries, and `sc_metrics_global()` for a general overview. These metrics help compare different parameter settings or input preprocessing choices. @@ -57,7 +57,7 @@ Basic workflows follow the same pattern: choose scale (`step` or `k`), tune or s vol <- terra::rast(system.file("raster/volcano.tif", package = "supercells")) # choose scale and tune compactness -tune <- supercells::sc_tune_compactness(vol, step = 8, metrics = "local") +tune <- supercells::sc_tune_compactness(vol, step = 8, metric = "local") # create supercells vol_sc <- supercells::sc_slic(vol, step = 8, compactness = tune$compactness) @@ -101,7 +101,8 @@ plot(sf::st_geometry(vol_sc), add = TRUE, lwd = 0.6, border = "red") The resulting `sf` object contains one row per supercell. Each row stores summary values of each layer in the original raster, as well as the geometry of the supercell. -By default only summary values are returned, so use `outcomes = c("supercells", "coordinates", "values")` when you also want IDs and center coordinates. +By default, IDs, center coordinates, and summary values are returned (`outcomes = c("supercells", "coordinates", "values")`). +Use `outcomes = "values"` when you want value summaries only. Two related functions provide alternative output formats. Use `sc_slic_points()` to return only supercell centers as points. @@ -114,7 +115,7 @@ Now, let's try to tune the `compactness` parameter using a pilot run. tune <- sc_tune_compactness( vol, step = 8, - metrics = "local" + metric = "local" ) tune ``` diff --git a/vignettes/articles/v2-parameters.Rmd b/vignettes/articles/v2-parameters.Rmd index ac9d977..dc8492a 100644 --- a/vignettes/articles/v2-parameters.Rmd +++ b/vignettes/articles/v2-parameters.Rmd @@ -42,13 +42,13 @@ D = \sqrt{\left(\frac{d_s}{\text{step}}\right)^2 + \left(\frac{d_v}{c}\right)^2} $$ where $d_s$ is the spatial distance in grid-cell units, $d_v$ is the value-space distance (from `dist_fun`), and $c$ is the `compactness` value. -When `step_unit = "map"`, the `step` value is converted to cells before segmentation; distances are still computed in grid-cell units. +When `step` is provided as `use_meters(...)`, it is converted to cells before segmentation; distances are still computed in grid-cell units. Larger `compactness` down-weights the value term, making shapes more regular, while smaller values emphasize value similarity. # Choosing step or k You can control the number and size of supercells using either `step` or `k`. -`step` defines the spacing of initial centers in pixel units (or map units when `step_unit = "map"`). +`step` defines the spacing of initial centers in pixel units (or map units when given as `use_meters(...)`). Smaller `step` values produce more and smaller supercells, while larger values produce fewer and larger supercells. For example, `step = 8` creates centers approximately every 8 pixels, which leads to supercells that are roughly 8 by 8 pixels in size, depending on the compactness. @@ -56,12 +56,12 @@ For example, `step = 8` creates centers approximately every 8 pixels, which lead sc_step <- sc_slic(vol, step = 8, compactness = 5) ``` -By default, `step` is in pixel units, but instead we can also specify it in map units with `step_unit = "map"`. +By default, `step` is in pixel units, but instead we can also specify it in map units with `use_meters()`. This allows us to think about the spatial scales of expected supercells in terms of the actual map units. -For example, if your raster has a resolution of 10 meters, then `step = 200` with `step_unit = "map"` would create centers approximately every 200 meters, which corresponds to every 20 pixels. +For example, if your raster has a resolution of 10 meters, then `step = use_meters(200)` would create centers approximately every 200 meters, which corresponds to every 20 pixels. ```{r} -sc_step_map <- sc_slic(vol, step = 200, step_unit = "map", compactness = 5) +sc_step_map <- sc_slic(vol, step = use_meters(200), compactness = 5) ``` `k` specifies the desired number of supercells and the algorithm chooses an approximate step size. @@ -121,7 +121,7 @@ plot(sc_compact_high[0], add = TRUE, border = "red", lwd = 0.5) The `sc_tune_compactness()` function estimates a reasonable starting value from a short run of the algorithm. -It supports two summaries with `metrics = "global"` and `metrics = "local"`. +It supports two summaries with `metric = "global"` and `metric = "local"`. The global version looks at overall balance between value and spatial distances, while the local version uses a neighborhood-based value scale. More precisely: @@ -133,13 +133,13 @@ More precisely: The local estimate is often more stable for heterogeneous rasters. ```{r} -tune_global <- sc_tune_compactness(vol, step = 8, metrics = "global") -tune_local <- sc_tune_compactness(vol, step = 8, metrics = "local") +tune_global <- sc_tune_compactness(vol, step = 8, metric = "global") +tune_local <- sc_tune_compactness(vol, step = 8, metric = "local") tune_global tune_local ``` -Both results return a one-row data frame with `step`, `metric`, and `compactness`. +Both results return a one-row data frame with `step`, `metric`, `dist_fun`, and `compactness`. You can plug the suggested value into `sc_slic()` by setting `compactness` to the estimated value. ```{r} @@ -148,27 +148,28 @@ sc_tuned <- sc_slic(vol, step = 8, compactness = tune_global$compactness) If your raster has many layers, the value distances can be large. The `value_scale` argument controls the scaling of value distances before the compactness estimate. -The default `"auto"` divides by `sqrt(nlyr(x))`, which is often a good baseline for high-dimensional inputs. -If your values are already standardized or on a common scale, you can set `value_scale = 1`. +Global: `compactness = (median(value) / value_scale) * step / median(spatial)`. +Local: `compactness = median(local_mean_value / value_scale)`. +Use `"auto"` (`sqrt(nlyr(x))`) for Euclidean-like distances; for bounded/angular distances (e.g., cosine), `value_scale = 1` is often better. ## Automatic compactness -For heterogeneous rasters, `compactness = "auto"` enables SLIC0-style adaptive compactness. +For heterogeneous rasters, `compactness = use_adaptive()` enables SLIC0-style adaptive compactness. This adjusts the value scale per supercell and often improves local adaptation. At the same time, it reduces direct control, so use it when a single global compactness is hard to choose. Importantly, it still uses your chosen `dist_fun` for value distances. ```{r} -sc_auto <- sc_slic(vol, step = 8, compactness = "auto") +sc_auto <- sc_slic(vol, step = 8, compactness = use_adaptive()) ``` ```{r} -terra::plot(vol, main = "compactness = auto") +terra::plot(vol, main = "compactness = adaptive") plot(sc_auto[0], add = TRUE, border = "red", lwd = 0.5) ``` -When you compare results, keep in mind that metrics from `"auto"` are not directly comparable to fixed-compactness runs. -You can still compare spatial metrics, but value metrics reflect local scaling when `compactness = "auto"`. +When you compare results, keep in mind that metrics from adaptive compactness are not directly comparable to fixed-compactness runs. +You can still compare spatial metrics, but value metrics reflect local scaling when `compactness = use_adaptive()`. ## Manual tuning @@ -236,12 +237,12 @@ If you need unpostprocessed supercells for speed or debugging, you can set `clea # Iterations -You can inspect how the algorithm converges over iterations by enabling `iter_diagnostics = TRUE`. -This attaches diagnostics to the output and allows plotting with `sc_plot_iter_diagnostics()`.^[It only works when chunking is disabled.] +You can inspect how the algorithm converges over iterations with `sc_slic_convergence()`. +It returns a data frame with per-iteration mean distance and has a dedicated `plot()` method. ```{r} -sc_diag <- sc_slic(vol, step = 8, compactness = 5, iter_diagnostics = TRUE) -sc_plot_iter_diagnostics(sc_diag) +sc_conv <- sc_slic_convergence(vol, step = 8, compactness = 5, iter = 10) +plot(sc_conv) ``` Use the diagnostics to decide whether fewer iterations are sufficient.