Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/data.land/NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export(matchInventoryRings)
export(match_pft)
export(match_species_id)
export(mpot2smoist)
export(ndti_to_sipnet_tillage)
export(netcdf.writer.BADM)
export(om2soc)
export(parse.MatrixNames)
Expand Down
76 changes: 76 additions & 0 deletions modules/data.land/R/ndti_to_sipnet_tillage.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#' Convert fractional NDTI drop to SIPNET tillage effectiveness
#'
#' Maps the fractional NDTI drop produced by the CCMMF NDTI pipeline
#' to \code{tillage_eff_0to1} for use in the PEcAn events JSON schema
#' and written to SIPNET \code{events.in} by
#' \code{\link[PEcAn.SIPNET]{write.events.SIPNET}}.
#'
#' @param delta_ndti numeric vector. Fractional NDTI drop in \[0, 1\],
#' computed as \code{(max_before_min - min_val) / max_before_min}
#' from a smoothed NDTI time series over the fallow season.
#' @param no_till_threshold numeric scalar. \code{delta_ndti} values at
#' or below this threshold are mapped to zero effectiveness (no-till).
#' Default 0.30, based on Dietze & Kanee (pers. comm.).
#' @param slope numeric scalar. Slope of the linear response above
#' \code{no_till_threshold}. Default 2.5, which produces a ramp from
#' 0 at \code{delta_ndti = 0.30} to 1 at \code{delta_ndti = 0.70}
#' (i.e. \code{1 / (0.70 - 0.30)}). Output is clamped to \[0, 1\].
#'
#' @return numeric vector of \code{tillage_eff_0to1} values, same
#' length as \code{delta_ndti}, clamped to \[0, 1\].
#' \code{NA} inputs propagate to \code{NA} outputs.
#'
#' @details
#' NDTI drops after tillage as residue is incorporated into soil.
#' The fractional drop over the fallow season is a proxy for tillage
#' intensity. Default thresholds are from Dietze & Kanee (pers. comm.).
#' The slope parameter can be calibrated once field data (SOC or soil
#' respiration) are available.
#'
#' @references
#' Daughtry, C.S.T., Hunt, E.R., Doraiswamy, P.C., McMurtrey, J.E.
#' (2005). Remote sensing the spatial distribution of crop residues.
#' \emph{Agronomy Journal}, 97(3), 864--871.
#' \doi{10.2134/agronj2004.0291}
#'
#' @examples
#' ndti_to_sipnet_tillage(c(0.25, 0.50, 0.80))
#'
#' # Custom slope for sensitivity analysis
#' ndti_to_sipnet_tillage(c(0.25, 0.50, 0.80), slope = 2.0)
#'
#' @export
ndti_to_sipnet_tillage <- function(
delta_ndti,
no_till_threshold = 0.30,
slope = 2.5
) {
## --- input checks ---
if (!is.numeric(no_till_threshold) || length(no_till_threshold) != 1) {
PEcAn.logger::logger.severe(
"no_till_threshold must be a single numeric value."
)
}

if (!is.numeric(slope) || length(slope) != 1 || slope < 0) {
PEcAn.logger::logger.severe(
"slope must be a single non-negative numeric value; got ", slope, "."
)
}

delta_ndti <- as.numeric(delta_ndti)

if (any(delta_ndti < 0, na.rm = TRUE)) {
PEcAn.logger::logger.warn(
"Negative delta_ndti values detected; these will be treated as no-till."
)
}

## --- mapping ---
tillage_eff <- pmin(pmax((delta_ndti - no_till_threshold) * slope, 0), 1)

# preserve input NAs
tillage_eff[is.na(delta_ndti)] <- NA_real_

tillage_eff
}
53 changes: 53 additions & 0 deletions modules/data.land/man/ndti_to_sipnet_tillage.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions modules/data.land/tests/testthat/test-ndti_to_sipnet_tillage.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
context("ndti_to_sipnet_tillage")

test_that("returns a numeric vector of correct length", {
out <- ndti_to_sipnet_tillage(c(0.2, 0.5, 0.8))
expect_type(out, "double")
expect_length(out, 3)
})

test_that("default slope clamps correctly at boundaries", {
out <- ndti_to_sipnet_tillage(c(0.00, 0.30, 0.70, 1.00))
expect_equal(out, c(0, 0, 1, 1))
})

test_that("midpoint maps to 0.5 with default parameters", {
out <- ndti_to_sipnet_tillage(0.50)
expect_equal(out, 0.5, tolerance = 1e-9)
})

test_that("custom slope scales output correctly", {
# slope = 1 means ramp reaches 1 at delta_ndti = 1.30
out <- ndti_to_sipnet_tillage(c(0.30, 0.80), slope = 1.0)
expect_equal(out[1], 0)
expect_equal(out[2], 0.5, tolerance = 1e-9)
})

test_that("output is always clamped to [0, 1]", {
out <- ndti_to_sipnet_tillage(seq(0, 1, length.out = 50))
expect_true(all(out >= 0))
expect_true(all(out <= 1))
})

test_that("NA propagates and negative delta_ndti clamps to zero", {
out <- ndti_to_sipnet_tillage(c(-0.1, NA, 0.70))
expect_equal(out[1], 0)
expect_true(is.na(out[2]))
expect_equal(out[3], 1)
})

test_that("invalid inputs produce informative errors", {
expect_error(
ndti_to_sipnet_tillage(0.5, no_till_threshold = "a"),
"single numeric"
)
expect_error(
ndti_to_sipnet_tillage(0.5, slope = -1),
"non-negative"
)
})
Loading