diff --git a/.buildlibrary b/.buildlibrary index fb0a5e25..e780c60f 100644 --- a/.buildlibrary +++ b/.buildlibrary @@ -1,4 +1,4 @@ -ValidationKey: '22925485' +ValidationKey: '23121280' AutocreateReadme: yes AutocreateCITATION: yes AcceptedWarnings: diff --git a/CITATION.cff b/CITATION.cff index 6f860741..fa51542a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: If you use this software, please cite it using the metadata from this file. type: software title: 'mrmfa: Input data generation for the REMIND MFA' -version: 1.11.1 -date-released: '2026-07-01' +version: 1.12.0 +date-released: '2026-07-10' abstract: The mrmfa packages contains data preprocessing for the REMIND MFA. authors: - family-names: Dürrwächter diff --git a/DESCRIPTION b/DESCRIPTION index 2cd039b3..e50a1149 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: mrmfa Title: Input data generation for the REMIND MFA -Version: 1.11.1 -Date: 2026-07-01 +Version: 1.12.0 +Date: 2026-07-10 Authors@R: c( person("Jakob", "Dürrwächter", , "jakobdu@pik-potsdam.de", role = c("aut", "cre")), person("Bennet", "Weiss", , "bennet.weiss@pik-potsdam.de", role = "aut"), diff --git a/R/calcCeFloorspaceEDGEB.R b/R/calcCeFloorspaceEDGEB.R index 98201aba..b3c32ba8 100644 --- a/R/calcCeFloorspaceEDGEB.R +++ b/R/calcCeFloorspaceEDGEB.R @@ -2,7 +2,8 @@ #' #' @param scenarios EDGE-B scenarios (character vector or string). Available: "SSP1", "SSP2", "SSP3", "SSP4", "SSP5". #' @param collapse Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed. -#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation. +#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation +#' (see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values are identical across scenarios. #' @param dof Integer. Degrees of freedom for spline interpolation. #' @author Bennet Weiss calcCeFloorspaceEDGEB <- function(scenarios = "SSP2", collapse = TRUE, smooth = FALSE, dof = 8) { @@ -12,13 +13,8 @@ calcCeFloorspaceEDGEB <- function(scenarios = "SSP2", collapse = TRUE, smooth = # Unit conversion from million m2 to m2 floorspace <- floorspace * 1e6 - # Remove redundant dimensions (e.g. Scenario, if only one is given) - if (collapse) { - floorspace <- collapseDim(floorspace) - } - # Remove buildings total - floorspace <- floorspace[, , (Variable <- "buildings"), invert = TRUE] + floorspace <- floorspace[, , "buildings", invert = TRUE] # enforce MFA naming convention getNames(floorspace) <- gsub("commercial", "Com", getNames(floorspace)) @@ -27,10 +23,21 @@ calcCeFloorspaceEDGEB <- function(scenarios = "SSP2", collapse = TRUE, smooth = floorspace_years <- getYears(floorspace, as.integer = TRUE) # smooth if requested if (smooth) { - # smooth data and interpolate missing data; ensure pegging of key years - # remove data beyond 2100 from smoothing due to low data quality - years <- floorspace_years[floorspace_years<=2100] - floorspace[, years] <- toolTimeSpline(floorspace[, years], dof = dof) + # smooth data while keeping the historical period identical across scenarios + # exclude data beyond 2100 from smoothing due to low data quality + years <- floorspace_years[floorspace_years <= 2100] + scens <- getItems(floorspace, dim = "scenario") + refScenario <- if ("SSP2" %in% scens) "SSP2" else scens[1] + floorspace[, years] <- toolHistoricallyConsistentSmoothing( + floorspace[, years], + refScenario = refScenario, + dof = dof + ) + } + + # Remove redundant dimensions (e.g. scenario, if only one is given) + if (collapse) { + floorspace <- collapseDim(floorspace) } # interpolate to yearly data diff --git a/R/calcCoGDP.R b/R/calcCoGDP.R index aa25af90..a53869fa 100644 --- a/R/calcCoGDP.R +++ b/R/calcCoGDP.R @@ -16,7 +16,9 @@ #' @param perCapita Logical. If TRUE, GDP is returned as per capita. #' @param scenarios Character vector or string specifying the scenarios to use for the future. #' @param collapse Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed. -#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation. +#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation +#' (see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values +#' are identical across scenarios. #' @param dof Integer. Degrees of freedom for spline interpolation. #' Higher values lead to a closer fit to the original data, while lower values result in smoother curves. #' @return List with Magpie object of GDP (given in 2005 USD) and metadata in calcOutput format. @@ -62,8 +64,13 @@ calcCoGDP <- function(perCapita = FALSE, scenarios = "SSP2", collapse = TRUE, sm if (smooth) { # smooth data and interpolate missing data + # exclude data beyond 2100 from smoothing due to low data quality years <- startyear:2100 - gdp[, years] <- toolTimeSpline(gdp[, years], dof = dof, peggedYears = c(1900, 2023, 2100)) + gdp[, years] <- toolHistoricallyConsistentSmoothing( + gdp[, years], + refScenario = if ("SSP2" %in% getItems(gdp, dim = "scenario")) "SSP2" else getItems(gdp, dim = "scenario")[1], + dof = dof + ) } # finalize for calcOutput diff --git a/R/calcCoPopulation.R b/R/calcCoPopulation.R index 68875a29..888e1173 100644 --- a/R/calcCoPopulation.R +++ b/R/calcCoPopulation.R @@ -12,7 +12,9 @@ #' @author Merlin Jo Hosak, Bennet Weiss #' @param scenarios Character vector or string specifying the scenarios to use for the future. #' @param collapse Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed. -#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation. +#' @param smooth Logical. If TRUE, data is smoothed using spline interpolation +#' (see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values +#' are identical across scenarios. #' @param dof Integer. Degrees of freedom for spline interpolation. #' Higher values lead to a closer fit to the original data, while lower values result in smoother curves. #' @return List with Magpie object of population and metadata in calcOutput @@ -39,10 +41,14 @@ calcCoPopulation <- function(scenarios = "SSP2", collapse = TRUE, smooth = FALSE pop <- toolBackcastByReference(x = pop, ref = worldHist) if (smooth) { - # smooth data and interpolate missing data; ensure pegging of key years - # remove data beyond 2100 from smoothing due to low data quality + # smooth data and interpolate missing data + # exclude data beyond 2100 from smoothing due to low data quality years <- min(getYears(pop, as.integer = TRUE)):2100 - pop[, years] <- toolTimeSpline(pop[, years], dof = dof, peggedYears = c(1900, 2023, 2100)) + pop[, years] <- toolHistoricallyConsistentSmoothing( + pop[, years], + refScenario = if ("SSP2" %in% getItems(pop, dim = "scenario")) "SSP2" else getItems(pop, dim = "scenario")[1], + dof = dof + ) } # build description including scenario and smoothing note diff --git a/R/toolHistoricallyConsistentSmoothing.R b/R/toolHistoricallyConsistentSmoothing.R new file mode 100644 index 00000000..3dd48a68 --- /dev/null +++ b/R/toolHistoricallyConsistentSmoothing.R @@ -0,0 +1,111 @@ +#' Historically consistent time smoothing across scenarios +#' +#' @description +#' Smooths a magpie object over time using \link[madrat]{toolTimeSpline} while +#' ensuring that smoothed values in the historical period are identical across +#' all scenarios. Plain per-scenario spline smoothing lets scenario-specific +#' future data pull the fit inside the historical period, so smoothed +#' historical values would otherwise differ between scenarios. +#' After smoothing, all years up to \code{lastHistYear} are replaced in every +#' scenario by the smoothed values of \code{refScenario}. To avoid a +#' derivative discontinuity at the transition, each non-reference scenario is +#' faded from the reference trajectory to its own trajectory over +#' \code{fadeLength} years using the smootherstep polynomial +#' \eqn{w(t) = 6t^5 - 15t^4 + 10t^3}, whose first and second derivatives +#' vanish at both endpoints. The blended curve therefore matches the slope of +#' the shared history at the start of the fade window and the slope of the +#' scenario-specific spline at its end, yielding a C2-continuous transition. +#' +#' @param x A magpie object with a 'scenario' dimension. +#' @param lastHistYear Integer or NULL. Last year considered historical. If +#' NULL (default), it is detected automatically as the last year up to which +#' all scenarios in \code{x} share identical values. If given, the historical +#' period is forced up to this year and \code{refScenario} values are written +#' there even where scenarios originally differ. Ignored if x contains a +#' single scenario. +#' @param refScenario Character. Name of the scenario whose smoothed values +#' define the shared history (default "SSP2"). Must be located in 'scenario' +#' dimension of \code{x}. Ignored if x contains a single scenario. +#' @param fadeLength Integer > 0. Length of the transition window in years. +#' 1 produces a hard splice, which is generally discontinuous. +#' @param dof Degrees of freedom passed to \link[madrat]{toolTimeSpline}. +#' @param verbose Logical. If TRUE, print informational messages (e.g. the +#' automatically detected last historical year). Default FALSE. +#' @param ... Further arguments (e.g. peggedYears, anchorFactor) passed to +#' \link[madrat]{toolTimeSpline}. +#' @return Smoothed magpie object with scenario-independent history. +#' @author Bennet Weiss +toolHistoricallyConsistentSmoothing <- function( + x, + lastHistYear = NULL, + refScenario = "SSP2", + fadeLength = 10, + dof = 8, + verbose = FALSE, + ... +) { + if (!is.numeric(fadeLength) || length(fadeLength) != 1 || fadeLength <= 0) { + stop("fadeLength must be a single positive number (larger than 0).") + } + + smoothed <- toolTimeSpline(x, dof = dof, ...) + + # without the scenario dimension or with a single scenario there is nothing to harmonize + if (!"scenario" %in% getSets(x)) { + warning("No dimension 'scenario' found in x. Returning plainly smoothed object.") + return(smoothed) + } + scens <- getItems(x, dim = "scenario") + if (length(scens) <= 1) { + return(smoothed) + } + + if (!refScenario %in% scens) { + stop( + "refScenario '", refScenario, "' not found in dimension 'scenario'. Available: ", paste(scens, collapse = ", ") + ) + } + + # selector for a single scenario in the scenario (sub)dimension + sel_refScen <- list(scenario = refScenario) + otherScens <- setdiff(scens, refScenario) + years <- getYears(smoothed, as.integer = TRUE) + + if (is.null(lastHistYear)) { + # detect the historical period as the leading run of years for which all + # scenarios in the (unsmoothed) input share identical values + refArr <- x[, , sel_refScen] + otherArrs <- x[, , list(scenario = otherScens)] + for (i in seq_along(years)) { + sameThisYear <- all(refArr[, i, ] == otherArrs[, i, ]) + if (!sameThisYear) break + lastHistYear <- years[i] + } + if (is.null(lastHistYear)) { + warning( + "Scenarios already differ in the first year; no common historical ", + "period could be detected. Returning plainly smoothed object." + ) + return(smoothed) + } + if (verbose) message("Detected last historical year: ", lastHistYear) + } + + if(min(years) > lastHistYear) { + warning("No years <= lastHistYear (", lastHistYear, ") in data. Returning plainly smoothed object.") + return(smoothed) + } + if (lastHistYear >= max(years)) { + warning("lastHistYear >= last data year. All scenarios are replaced by refScenario values.") + } + + # prepare the fade weights: use C2-continuous smootherstep polynomial (see description) to transition from 0 to 1 + tNorm <- pmax(0, pmin(1, (years - lastHistYear) / fadeLength)) + w <- new.magpie(years = years, fill = (6 * tNorm^5 - 15 * tNorm^4 + 10 * tNorm^3)) + + for (s in otherScens) { + # overwrite hist period with refScen, then fade from reference to scenario-specific spline + smoothed[, , s] <- (1 - w) * smoothed[, , sel_refScen] + w * smoothed[, , s] + } + return(smoothed) +} diff --git a/README.md b/README.md index 01629edc..051ed3f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Input data generation for the REMIND MFA -R package **mrmfa**, version **1.11.1** +R package **mrmfa**, version **1.12.0** [![R build status](https://github.com/pik-piam/mrmfa/workflows/check/badge.svg)](https://github.com/pik-piam/mrmfa/actions) [![codecov](https://codecov.io/gh/pik-piam/mrmfa/branch/master/graph/badge.svg)](https://app.codecov.io/gh/pik-piam/mrmfa) @@ -38,7 +38,7 @@ In case of questions / problems please contact Jakob Dürrwächter . +Dürrwächter J, Weiss B, Schweiger L, Benke F, Hosak M, Zhang Q (2026). "mrmfa: Input data generation for the REMIND MFA." Version: 1.12.0, . A BibTeX entry for LaTeX users is @@ -46,9 +46,9 @@ A BibTeX entry for LaTeX users is @Misc{, title = {mrmfa: Input data generation for the REMIND MFA}, author = {Jakob Dürrwächter and Bennet Weiss and Leonie Schweiger and Falk Benke and Merlin Jo Hosak and Qianzhi Zhang}, - date = {2026-07-01}, + date = {2026-07-10}, year = {2026}, url = {https://github.com/pik-piam/mrmfa}, - note = {Version: 1.11.1}, + note = {Version: 1.12.0}, } ``` diff --git a/man/calcCeFloorspaceEDGEB.Rd b/man/calcCeFloorspaceEDGEB.Rd index a29d4ce8..7bac9b61 100644 --- a/man/calcCeFloorspaceEDGEB.Rd +++ b/man/calcCeFloorspaceEDGEB.Rd @@ -16,7 +16,8 @@ calcCeFloorspaceEDGEB( \item{collapse}{Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed.} -\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation.} +\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation +(see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values are identical across scenarios.} \item{dof}{Integer. Degrees of freedom for spline interpolation.} } diff --git a/man/calcCoGDP.Rd b/man/calcCoGDP.Rd index 3bccbc3d..c323f3ac 100644 --- a/man/calcCoGDP.Rd +++ b/man/calcCoGDP.Rd @@ -19,7 +19,9 @@ calcCoGDP( \item{collapse}{Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed.} -\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation.} +\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation +(see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values +are identical across scenarios.} \item{dof}{Integer. Degrees of freedom for spline interpolation. Higher values lead to a closer fit to the original data, while lower values result in smoother curves.} diff --git a/man/calcCoPopulation.Rd b/man/calcCoPopulation.Rd index 605d6054..4797f429 100644 --- a/man/calcCoPopulation.Rd +++ b/man/calcCoPopulation.Rd @@ -11,7 +11,9 @@ calcCoPopulation(scenarios = "SSP2", collapse = TRUE, smooth = FALSE, dof = 8) \item{collapse}{Logical. If TRUE, redundant dimensions (e.g. scenario if only one requested) are removed.} -\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation.} +\item{smooth}{Logical. If TRUE, data is smoothed using spline interpolation +(see \link{toolHistoricallyConsistentSmoothing}); smoothed historical values +are identical across scenarios.} \item{dof}{Integer. Degrees of freedom for spline interpolation. Higher values lead to a closer fit to the original data, while lower values result in smoother curves.} diff --git a/man/toolHistoricallyConsistentSmoothing.Rd b/man/toolHistoricallyConsistentSmoothing.Rd new file mode 100644 index 00000000..3f6c2b61 --- /dev/null +++ b/man/toolHistoricallyConsistentSmoothing.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/toolHistoricallyConsistentSmoothing.R +\name{toolHistoricallyConsistentSmoothing} +\alias{toolHistoricallyConsistentSmoothing} +\title{Historically consistent time smoothing across scenarios} +\usage{ +toolHistoricallyConsistentSmoothing( + x, + lastHistYear = NULL, + refScenario = "SSP2", + fadeLength = 10, + dof = 8, + verbose = FALSE, + ... +) +} +\arguments{ +\item{x}{A magpie object with a 'scenario' dimension.} + +\item{lastHistYear}{Integer or NULL. Last year considered historical. If +NULL (default), it is detected automatically as the last year up to which +all scenarios in \code{x} share identical values. If given, the historical +period is forced up to this year and \code{refScenario} values are written +there even where scenarios originally differ. Ignored if x contains a +single scenario.} + +\item{refScenario}{Character. Name of the scenario whose smoothed values +define the shared history (default "SSP2"). Must be located in 'scenario' +dimension of \code{x}. Ignored if x contains a single scenario.} + +\item{fadeLength}{Integer > 0. Length of the transition window in years. +1 produces a hard splice, which is generally discontinuous.} + +\item{dof}{Degrees of freedom passed to \link[madrat]{toolTimeSpline}.} + +\item{verbose}{Logical. If TRUE, print informational messages (e.g. the +automatically detected last historical year). Default FALSE.} + +\item{...}{Further arguments (e.g. peggedYears, anchorFactor) passed to +\link[madrat]{toolTimeSpline}.} +} +\value{ +Smoothed magpie object with scenario-independent history. +} +\description{ +Smooths a magpie object over time using \link[madrat]{toolTimeSpline} while +ensuring that smoothed values in the historical period are identical across +all scenarios. Plain per-scenario spline smoothing lets scenario-specific +future data pull the fit inside the historical period, so smoothed +historical values would otherwise differ between scenarios. +After smoothing, all years up to \code{lastHistYear} are replaced in every +scenario by the smoothed values of \code{refScenario}. To avoid a +derivative discontinuity at the transition, each non-reference scenario is +faded from the reference trajectory to its own trajectory over +\code{fadeLength} years using the smootherstep polynomial +\eqn{w(t) = 6t^5 - 15t^4 + 10t^3}, whose first and second derivatives +vanish at both endpoints. The blended curve therefore matches the slope of +the shared history at the start of the fade window and the slope of the +scenario-specific spline at its end, yielding a C2-continuous transition. +} +\author{ +Bennet Weiss +} diff --git a/tests/testthat/test-dummy.R b/tests/testthat/test-dummy.R deleted file mode 100644 index 909f17c6..00000000 --- a/tests/testthat/test-dummy.R +++ /dev/null @@ -1 +0,0 @@ -skip("dummy test") diff --git a/tests/testthat/test-toolHistoricallyConsistentSmoothing.R b/tests/testthat/test-toolHistoricallyConsistentSmoothing.R new file mode 100644 index 00000000..003580d8 --- /dev/null +++ b/tests/testthat/test-toolHistoricallyConsistentSmoothing.R @@ -0,0 +1,151 @@ +# synthetic data: identical noisy history up to divergeYear, scenario-specific growth after +makeTestData <- function(divergeYear = 2020, scenarios = c("SSP1", "SSP2", "SSP3")) { + set.seed(42) + years <- 1950:2050 + x <- magclass::new.magpie( + cells_and_regions = c("DEU", "FRA"), + years = years, + names = scenarios, + fill = 0, + sets = c("region", "year", "scenario") + ) + base <- 100 + (years - 1950)^1.3 + stats::rnorm(length(years), sd = 2) + growthRates <- c(SSP1 = 0.5, SSP2 = 1, SSP3 = 2) + for (r in magclass::getItems(x, dim = 1)) { + for (s in scenarios) { + v <- base + late <- years > divergeYear + v[late] <- v[late] + growthRates[[s]] * (years[late] - divergeYear)^1.5 + x[r, , s] <- v + } + } + x +} + +test_that("historical values are identical across scenarios", { + x <- makeTestData() + res <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020) + refHist <- as.vector(res[, 1950:2020, "SSP2"]) + for (s in c("SSP1", "SSP3")) { + expect_equal(as.vector(res[, 1950:2020, s]), refHist) + } +}) + +test_that("transition is smooth (no kink) and continuous compared to hard splice", { + x <- makeTestData() + res <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020, fadeLength = 10) + hard <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020, fadeLength = 1) + years <- magclass::getYears(res, as.integer = TRUE) + + # kink (second difference) at the transition is smaller for the faded version + transition <- which(years >= 2015 & years <= 2035) + kink <- function(obj) max(abs(diff(as.vector(obj["DEU", , "SSP3"]), differences = 2)[transition])) + expect_gt(kink(hard), kink(res)) + + # continuity: value step at the boundary (lastHistYear -> lastHistYear + 1) + # is much smaller for the faded version than the hard splice + d1Res <- diff(as.vector(res["DEU", , "SSP3"])) + d1Hard <- diff(as.vector(hard["DEU", , "SSP3"])) + transIdx <- which(years == 2020) # d1[i] = step from years[i] to years[i+1] + expect_lt(abs(d1Res[transIdx]), abs(d1Hard[transIdx])) +}) + +test_that("single scenario and missing scenario dimension pass through", { + x <- makeTestData() + x1 <- x[, , "SSP1"] + expected <- toolTimeSpline(x1, dof = 8) + expect_equal( + as.vector(toolHistoricallyConsistentSmoothing(x1)), + as.vector(expected) + ) + # object without a "scenario" set: should warn and return plain smoothed result + x2 <- x1 + magclass::getSets(x2)[3] <- "variable" + expect_warning( + expect_equal( + as.vector(toolHistoricallyConsistentSmoothing(x2)), + as.vector(expected) + ), + "No dimension" + ) +}) + +test_that("reference scenario is unchanged by the harmonization", { + x <- makeTestData() + res <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020) + expected <- toolTimeSpline(x, dof = 8) + expect_equal(as.vector(res[, , "SSP2"]), as.vector(expected[, , "SSP2"])) +}) + +test_that("other third-dimension subdimensions are handled correctly", { + a <- makeTestData() + magclass::getSets(a)[3] <- "scenario" + b <- a * 2 + x <- magclass::mbind( + magclass::add_dimension(a, dim = 3.2, add = "variable", nm = "v1"), + magclass::add_dimension(b, dim = 3.2, add = "variable", nm = "v2") + ) + res <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020) + for (v in c("v1", "v2")) { + refHist <- as.vector(res[, 1950:2020, paste0("SSP2.", v)]) + for (s in c("SSP1", "SSP3")) { + expect_equal(as.vector(res[, 1950:2020, paste0(s, ".", v)]), refHist) + } + } + # variables are not mixed up: v2 history stays about twice v1 history + ratio <- as.vector(res[, 1950:2020, "SSP1.v2"]) / as.vector(res[, 1950:2020, "SSP1.v1"]) + expect_true(all(abs(ratio - 2) < 1e-10)) +}) + +test_that("invalid inputs raise errors and edge cases warn", { + x <- makeTestData() + expect_error( + toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020, refScenario = "SSPX"), + "not found" + ) + expect_error( + toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020, fadeLength = 0), + "fadeLength" + ) + expect_warning( + toolHistoricallyConsistentSmoothing(x, lastHistYear = 2200), + "replaced by refScenario" + ) + expect_warning( + toolHistoricallyConsistentSmoothing(x, lastHistYear = 1900), + "No years" + ) +}) + +test_that("lastHistYear is detected automatically when not provided", { + x <- makeTestData(divergeYear = 2020) + auto <- toolHistoricallyConsistentSmoothing(x) + explicit <- toolHistoricallyConsistentSmoothing(x, lastHistYear = 2020) + expect_equal(as.vector(auto), as.vector(explicit)) +}) + +test_that("verbose reports the detected last historical year", { + x <- makeTestData(divergeYear = 2020) + expect_message( + toolHistoricallyConsistentSmoothing(x, verbose = TRUE), + "Detected last historical year: 2020" + ) +}) + +test_that("no common historical period warns and returns plain smoothing", { + # scenarios already diverge in the first year, so no shared history exists + x <- makeTestData(divergeYear = 1949) + expected <- toolTimeSpline(x, dof = 8) + expect_warning( + res <- toolHistoricallyConsistentSmoothing(x), + "no common historical" + ) + expect_equal(as.vector(res), as.vector(expected)) +}) + +test_that("fade window truncated by end of data works", { + x <- makeTestData() + expect_no_error( + toolHistoricallyConsistentSmoothing(x, lastHistYear = 2045, fadeLength = 20) + ) +})