R/smoother.R

Defines functions plot.dsge_decomposition plot_smoothed_fit plot_smoothed_states plot.dsge_smoothed print.dsge_decomposition print.dsge_smoothed_shocks print.dsge_smoothed kalman_smoother_full shock_decomposition_impl shock_decomposition.dsge_bayes shock_decomposition.dsge_fit shock_decomposition smooth_shocks_impl smooth_shocks.dsge_bayes smooth_shocks.dsge_fit smooth_shocks smooth_states_impl smooth_states.dsge_bayes smooth_states.dsge_fit smooth_states extract_ss_matrices

Documented in plot.dsge_decomposition plot.dsge_smoothed shock_decomposition shock_decomposition.dsge_bayes shock_decomposition.dsge_fit smooth_shocks smooth_shocks.dsge_bayes smooth_shocks.dsge_fit smooth_states smooth_states.dsge_bayes smooth_states.dsge_fit

# Kalman smoother, smoothed shocks, and shock decomposition
#
# Provides post-estimation analysis tools for estimated DSGE models:
# - smooth_states(): smoothed state estimates using all observations
# - smooth_shocks(): structural shock recovery from smoothed states
# - shock_decomposition(): historical decomposition of observables
# - plot methods for decomposition and smoothed results

#' @importFrom graphics rect
#' @importFrom utils head
NULL

# --------------------------------------------------------------------------
# Helper: extract the state-space matrices from a fit or bayes object
# --------------------------------------------------------------------------

#' Extract state-space matrices from an estimated model
#' @param x A `dsge_fit` or `dsge_bayes` object.
#' @param params Optional named numeric parameter vector (for Bayesian, uses
#'   posterior mean if `NULL`).
#' @return A list with G, H, M, D, Z, Q, n_s, n_obs, steady_state, data.
#' @noRd
extract_ss_matrices <- function(x, params = NULL) {
  if (inherits(x, "dsge_fit")) {
    sol <- x$solution
    y <- x$data
  } else if (inherits(x, "dsge_bayes")) {
    # Resolve at posterior mean (or user-supplied params)
    if (is.null(params)) {
      post <- x$posterior
      params_vec <- apply(post, 2, function(col) mean(as.numeric(col)))
    } else {
      params_vec <- params
    }

    # Split into structural + shock_sd
    model <- x$model
    free_params <- x$free_parameters
    shock_names <- x$shock_names
    sd_names <- paste0("sd_e.", shock_names)

    structural_params <- params_vec[free_params]
    shock_sd <- params_vec[sd_names]
    names(shock_sd) <- shock_names

    # Merge with fixed params
    if (inherits(model, "dsgenl_model")) {
      all_params <- c(unlist(model$fixed), structural_params)
    } else {
      all_params <- c(unlist(model$fixed), structural_params)
    }

    sol <- solve_dsge(model, params = all_params, shock_sd = shock_sd)
    y <- x$data
  } else {
    stop("`x` must be a dsge_fit or dsge_bayes object.", call. = FALSE)
  }

  G <- sol$G
  H <- sol$H
  M <- sol$M
  D <- sol$D
  Z <- D %*% G
  Q <- M %*% t(M)

  ss <- NULL
  if (!is.null(sol$steady_state)) ss <- sol$steady_state

  list(G = G, H = H, M = M, D = D, Z = Z, Q = Q,
       n_s = ncol(H), n_obs = ncol(Z),
       steady_state = ss, data = as.matrix(y),
       model = sol$model, solution = sol)
}


# ==========================================================================
# smooth_states()
# ==========================================================================

#' Smoothed State Estimates from an Estimated DSGE Model
#'
#' Computes the Rauch-Tung-Striebel (RTS) smoother to produce optimal state
#' estimates using all available observations. Compared to the filtered states
#' (which only use past data), smoothed states also incorporate future
#' observations.
#'
#' @param x A `dsge_fit` or `dsge_bayes` object.
#' @param ... Additional arguments (currently unused).
#'
#' @return An object of class `"dsge_smoothed"` containing:
#'   \describe{
#'     \item{smoothed_states}{T x n_s matrix of smoothed state estimates.}
#'     \item{filtered_states}{T x n_s matrix of filtered state estimates.}
#'     \item{smoothed_obs}{T x n_obs matrix of smoothed observable fits.}
#'     \item{residuals}{T x n_obs matrix of observation residuals.}
#'     \item{state_names}{Character vector of state variable names.}
#'     \item{obs_names}{Character vector of observed variable names.}
#'     \item{steady_state}{Steady-state values (if available).}
#'   }
#'
#' @details
#' The smoother uses the state-space representation:
#' \deqn{x_{t+1} = H x_t + M \varepsilon_{t+1}}
#' \deqn{y_t = Z x_t}
#'
#' where \eqn{Z = D \cdot G}. The smoothed states are the expectation of the
#' state vector conditional on all observations: \eqn{x_{t|T} = E[x_t | y_1, \ldots, y_T]}.
#'
#' For Bayesian models, the smoother is evaluated at the posterior mean.
#'
#' @examples
#' \donttest{
#' m <- dsge_model(
#'   obs(y ~ z),
#'   state(z ~ rho * z),
#'   start = list(rho = 0.5)
#' )
#' set.seed(1)
#' e <- rnorm(100)
#' z <- numeric(100); for (i in 2:100) z[i] <- 0.8 * z[i-1] + e[i]
#' fit <- estimate(m, data = data.frame(y = z))
#' sm <- smooth_states(fit)
#' }
#'
#' @export
smooth_states <- function(x, ...) {
  UseMethod("smooth_states")
}

#' @rdname smooth_states
#' @export
smooth_states.dsge_fit <- function(x, ...) {
  smooth_states_impl(x)
}

#' @rdname smooth_states
#' @export
smooth_states.dsge_bayes <- function(x, ...) {
  smooth_states_impl(x)
}

#' Implementation for smooth_states
#' @noRd
smooth_states_impl <- function(x) {
  ss <- extract_ss_matrices(x)
  y <- ss$data
  n_T <- nrow(y)

  # Full smoother with covariance storage
  sm <- kalman_smoother_full(y, ss$G, ss$H, ss$M, ss$D)

  # Smoothed observables: y_hat = Z * x_smooth
  smoothed_obs <- sm$smoothed_states %*% t(ss$Z)
  residuals <- y - smoothed_obs

  # State names
  state_names <- colnames(ss$H)
  if (is.null(state_names)) state_names <- paste0("s", seq_len(ss$n_s))
  obs_names <- colnames(y)
  if (is.null(obs_names)) obs_names <- paste0("y", seq_len(ss$n_obs))

  colnames(sm$smoothed_states) <- state_names
  colnames(sm$filtered_states) <- state_names
  colnames(smoothed_obs) <- obs_names
  colnames(residuals) <- obs_names

  # Smoothed state variances (diagonals of P_smooth at each t)
  smoothed_states_var <- NULL
  if (!is.null(sm$smoothed_P)) {
    smoothed_states_var <- matrix(NA_real_,
                                  nrow = n_T, ncol = ss$n_s,
                                  dimnames = list(NULL, state_names))
    for (t in seq_len(n_T)) {
      pt <- sm$smoothed_P[[t]]
      if (!is.null(pt)) smoothed_states_var[t, ] <- diag(pt)
    }
  }

  structure(
    list(
      smoothed_states     = sm$smoothed_states,
      smoothed_states_var = smoothed_states_var,
      filtered_states     = sm$filtered_states,
      smoothed_obs        = smoothed_obs,
      residuals           = residuals,
      state_names         = state_names,
      obs_names           = obs_names,
      steady_state        = ss$steady_state
    ),
    class = "dsge_smoothed"
  )
}


# ==========================================================================
# smooth_shocks()
# ==========================================================================

#' Extract Smoothed Structural Shocks
#'
#' Recovers the structural shocks from the smoothed states using the state
#' transition equation: \eqn{\hat{\varepsilon}_{t+1} = M^+ (x_{t+1|T} - H x_{t|T})}
#' where \eqn{M^+} is the Moore-Penrose pseudo-inverse of M.
#'
#' @param x A `dsge_fit` or `dsge_bayes` object.
#' @param ... Additional arguments (currently unused).
#'
#' @return An object of class `"dsge_smoothed_shocks"` containing:
#'   \describe{
#'     \item{shocks}{(T-1) x n_shocks matrix of smoothed structural shocks.}
#'     \item{shock_names}{Character vector of shock names.}
#'   }
#'
#' @details
#' From the state transition \eqn{x_{t+1} = H x_t + M \varepsilon_{t+1}},
#' the smoothed innovation is \eqn{x_{t+1|T} - H x_{t|T}}. The structural
#' shocks are recovered by projecting onto M:
#' \eqn{\hat{\varepsilon}_{t+1} = (M'M)^{-1} M' (x_{t+1|T} - H x_{t|T})}.
#'
#' @export
smooth_shocks <- function(x, ...) {
  UseMethod("smooth_shocks")
}

#' @rdname smooth_shocks
#' @export
smooth_shocks.dsge_fit <- function(x, ...) {
  smooth_shocks_impl(x)
}

#' @rdname smooth_shocks
#' @export
smooth_shocks.dsge_bayes <- function(x, ...) {
  smooth_shocks_impl(x)
}

#' Implementation for smooth_shocks
#' @noRd
smooth_shocks_impl <- function(x) {
  info <- extract_ss_matrices(x)

  # Get smoothed states
  sm <- kalman_smoother_full(info$data, info$G, info$H, info$M, info$D)
  xsm <- sm$smoothed_states

  n_T <- nrow(xsm)
  n_shocks <- ncol(info$M)
  H <- info$H
  M <- info$M

  # Moore-Penrose pseudo-inverse of M: M^+ = (M'M)^{-1} M'
  MtM <- t(M) %*% M
  Mplus <- solve(MtM) %*% t(M)

  # Smoothed innovations: x_{t+1|T} - H * x_{t|T}
  innovations <- xsm[2:n_T, , drop = FALSE] -
    t(H %*% t(xsm[1:(n_T - 1), , drop = FALSE]))

  # Structural shocks
  shocks <- t(Mplus %*% t(innovations))

  # Shock names
  shock_names <- colnames(M)
  if (is.null(shock_names)) {
    if (inherits(x, "dsge_bayes")) {
      shock_names <- x$shock_names
    } else {
      shock_names <- paste0("e", seq_len(n_shocks))
    }
  }
  colnames(shocks) <- shock_names

  structure(
    list(shocks = shocks, shock_names = shock_names),
    class = "dsge_smoothed_shocks"
  )
}


# ==========================================================================
# shock_decomposition()
# ==========================================================================

#' Historical Shock Decomposition
#'
#' Decomposes the observed variables into the contributions of each
#' structural shock. At each time t, the observed deviation from steady
#' state is written as a sum of contributions from current and past
#' shocks plus the initial condition contribution.
#'
#' @param x A `dsge_fit` or `dsge_bayes` object.
#' @param ... Additional arguments (currently unused).
#'
#' @return An object of class `"dsge_decomposition"` containing:
#'   \describe{
#'     \item{decomposition}{A 3D array with dimensions
#'       \code{[T, n_obs, n_shocks + 1]}. The last slice contains the initial
#'       condition contribution.}
#'     \item{obs_names}{Character vector of observed variable names.}
#'     \item{shock_names}{Character vector of shock names (plus "initial").}
#'     \item{observed}{T x n_obs matrix of observed data (deviations).}
#'   }
#'
#' @details
#' The state-space solution gives:
#' \deqn{x_t = H^t x_0 + \sum_{j=1}^{t} H^{t-j} M \varepsilon_j}
#'
#' The historical decomposition partitions the observed variables
#' \eqn{y_t = Z x_t} into the contribution of each structural shock
#' \eqn{\varepsilon_j^{(k)}} accumulated through the propagation mechanism.
#' The sum of all contributions (including the initial condition term)
#' reproduces the smoothed observables exactly.
#'
#' @examples
#' \donttest{
#' m <- dsge_model(
#'   obs(y ~ z),
#'   state(z ~ rho * z),
#'   start = list(rho = 0.5)
#' )
#' set.seed(1)
#' e <- rnorm(100)
#' z <- numeric(100); for (i in 2:100) z[i] <- 0.8*z[i-1]+e[i]
#' fit <- estimate(m, data = data.frame(y = z))
#' hd <- shock_decomposition(fit)
#' plot(hd)
#' }
#'
#' @export
shock_decomposition <- function(x, ...) {
  UseMethod("shock_decomposition")
}

#' @rdname shock_decomposition
#' @export
shock_decomposition.dsge_fit <- function(x, ...) {
  shock_decomposition_impl(x)
}

#' @rdname shock_decomposition
#' @export
shock_decomposition.dsge_bayes <- function(x, ...) {
  shock_decomposition_impl(x)
}

#' Implementation for shock_decomposition
#' @noRd
shock_decomposition_impl <- function(x) {
  info <- extract_ss_matrices(x)

  # Get smoothed states and shocks
  sm <- kalman_smoother_full(info$data, info$G, info$H, info$M, info$D)
  xsm <- sm$smoothed_states

  n_T <- nrow(xsm)
  H <- info$H
  M <- info$M
  Z <- info$Z
  n_s <- info$n_s
  n_obs <- info$n_obs
  n_shocks <- ncol(M)

  # Recover smoothed shocks (T-1 x n_shocks)
  MtM <- t(M) %*% M
  Mplus <- solve(MtM) %*% t(M)
  innovations <- xsm[2:n_T, , drop = FALSE] -
    t(H %*% t(xsm[1:(n_T - 1), , drop = FALSE]))
  eps <- t(Mplus %*% t(innovations))  # (T-1) x n_shocks

  # Shock names
  shock_names <- colnames(M)
  if (is.null(shock_names)) {
    if (inherits(x, "dsge_bayes")) {
      shock_names <- x$shock_names
    } else {
      shock_names <- paste0("e", seq_len(n_shocks))
    }
  }

  all_names <- c(shock_names, "initial")

  # Decomposition: accumulate contributions
  # x_t = H^t x_0 + sum_{j=1}^{t} H^{t-j} M eps_j
  # Contribution of shock k at time t:
  #   c_t^k = sum_{j=1}^{t} H^{t-j} M[,k] eps_j[k]
  # Initial condition contribution:
  #   c_t^0 = H^t x_0

  # 3D array: [time, observable, shock+initial]
  decomp <- array(0, dim = c(n_T, n_obs, n_shocks + 1))

  # State-level contributions: [n_s] per shock per time
  state_contrib <- array(0, dim = c(n_T, n_s, n_shocks + 1))

  # Initial condition contribution
  state_contrib[1, , n_shocks + 1] <- xsm[1, ]  # = H^0 * x_0 + ... ≈ x_0

  for (t in 2:n_T) {
    # Propagate all previous contributions through H
    for (k in seq_len(n_shocks + 1)) {
      state_contrib[t, , k] <- as.numeric(H %*% state_contrib[t - 1, , k])
    }
    # Add current shock contributions
    for (k in seq_len(n_shocks)) {
      state_contrib[t, , k] <- state_contrib[t, , k] +
        as.numeric(M[, k] * eps[t - 1, k])
    }
  }

  # Map state contributions to observables via Z
  for (k in seq_len(n_shocks + 1)) {
    decomp[, , k] <- state_contrib[, , k] %*% t(Z)
  }

  # Obs names
  obs_names <- colnames(info$data)
  if (is.null(obs_names)) obs_names <- paste0("y", seq_len(n_obs))

  structure(
    list(
      decomposition = decomp,
      obs_names = obs_names,
      shock_names = all_names,
      observed = info$data
    ),
    class = "dsge_decomposition"
  )
}


# ==========================================================================
# Enhanced Kalman smoother (stores filtered states for return)
# ==========================================================================

#' Full Kalman smoother returning both filtered and smoothed states
#' @noRd
kalman_smoother_full <- function(y, G, H, M, D) {
  # Forward filter
  fwd <- kalman_filter(y, G, H, M, D)

  if (!is.finite(fwd$loglik)) {
    return(list(smoothed_states = fwd$filtered_states,
                filtered_states = fwd$filtered_states))
  }

  n_T <- nrow(y)
  n_s <- ncol(H)
  Q <- M %*% t(M)

  smoothed_states <- fwd$filtered_states
  smoothed_P <- vector("list", n_T)
  x_smooth <- fwd$filtered_states[n_T, ]
  P_smooth <- fwd$filtered_P[[n_T]]
  smoothed_P[[n_T]] <- P_smooth

  for (t in (n_T - 1):1) {
    P_filt_t <- fwd$filtered_P[[t]]
    x_filt_t <- fwd$filtered_states[t, ]
    x_pred_tp1 <- fwd$predicted_states[t + 1, ]

    P_pred_tp1 <- H %*% P_filt_t %*% t(H) + Q
    P_pred_tp1 <- (P_pred_tp1 + t(P_pred_tp1)) / 2

    # Smoother gain
    J_t <- P_filt_t %*% t(H) %*% solve(P_pred_tp1)

    # Smoothed state
    x_smooth <- x_filt_t + as.numeric(J_t %*% (x_smooth - x_pred_tp1))
    smoothed_states[t, ] <- x_smooth

    P_smooth <- P_filt_t + J_t %*% (P_smooth - P_pred_tp1) %*% t(J_t)
    smoothed_P[[t]] <- P_smooth
  }

  list(smoothed_states  = smoothed_states,
       filtered_states  = fwd$filtered_states,
       smoothed_P       = smoothed_P)
}


# ==========================================================================
# Print methods
# ==========================================================================

#' @export
print.dsge_smoothed <- function(x, ...) {
  n_T <- nrow(x$smoothed_states)
  n_s <- ncol(x$smoothed_states)
  n_obs <- ncol(x$smoothed_obs)
  cat("DSGE Smoothed States\n")
  cat(sprintf("  Observations: %d\n", n_T))
  state_str <- paste(head(x$state_names, 5), collapse = ", ")
  if (n_s > 5) state_str <- paste0(state_str, ", ...")
  cat(sprintf("  States:       %d (%s)\n", n_s, state_str))
  cat(sprintf("  Observables:  %d (%s)\n", n_obs,
              paste(x$obs_names, collapse = ", ")))
  cat(sprintf("  Mean |residual|: %s\n",
              paste(round(colMeans(abs(x$residuals)), 6), collapse = ", ")))
  invisible(x)
}

#' @export
print.dsge_smoothed_shocks <- function(x, ...) {
  cat("DSGE Smoothed Structural Shocks\n")
  cat(sprintf("  Periods:  %d\n", nrow(x$shocks)))
  cat(sprintf("  Shocks:   %d (%s)\n", length(x$shock_names),
              paste(x$shock_names, collapse = ", ")))
  cat(sprintf("  Shock SDs: %s\n",
              paste(round(apply(x$shocks, 2, stats::sd), 6), collapse = ", ")))
  invisible(x)
}

#' @export
print.dsge_decomposition <- function(x, ...) {
  dims <- dim(x$decomposition)
  cat("DSGE Historical Shock Decomposition\n")
  cat(sprintf("  Periods:     %d\n", dims[1]))
  cat(sprintf("  Observables: %d (%s)\n", dims[2],
              paste(x$obs_names, collapse = ", ")))
  cat(sprintf("  Components:  %d (%s)\n", dims[3],
              paste(x$shock_names, collapse = ", ")))

  # Verification: sum of contributions vs observed
  recon <- apply(x$decomposition, c(1, 2), sum)
  max_err <- max(abs(recon - x$observed))
  cat(sprintf("  Reconstruction error: %.2e\n", max_err))
  invisible(x)
}


# ==========================================================================
# Plot methods
# ==========================================================================

#' Plot Smoothed States
#'
#' @param x A `dsge_smoothed` object.
#' @param which Which states to plot. Integer vector, character vector of
#'   state names, or `NULL` (all states).
#' @param type Either `"states"` or `"fit"`. `"states"` plots the smoothed
#'   state variables; `"fit"` plots the smoothed observables against data.
#' @param ... Additional arguments passed to [plot()].
#'
#' @return No return value, called for the side effect of producing
#'   smoothed state or fit plots on the active graphics device.
#'
#' @export
plot.dsge_smoothed <- function(x, which = NULL, type = c("states", "fit"),
                                ...) {
  type <- match.arg(type)

  if (type == "states") {
    plot_smoothed_states(x, which, ...)
  } else {
    plot_smoothed_fit(x, ...)
  }
}

#' @noRd
plot_smoothed_states <- function(x, which = NULL, ...) {
  states <- x$smoothed_states
  vars   <- x$smoothed_states_var
  has_var <- !is.null(vars)
  n_s <- ncol(states)
  snames <- x$state_names

  if (is.null(which)) which <- seq_len(min(n_s, 9))
  if (is.character(which)) which <- match(which, snames)

  n_plot <- length(which)
  ncols <- ceiling(sqrt(n_plot))
  nrows <- ceiling(n_plot / ncols)

  old_par <- graphics::par(no.readonly = TRUE)
  on.exit(graphics::par(old_par))
  .dsge_par_grid(nrows, ncols)

  t_idx <- seq_len(nrow(states))

  for (i in which) {
    st <- states[, i]
    if (has_var) {
      sd_i  <- sqrt(pmax(vars[, i], 0))
      lower <- st - 2 * sd_i
      upper <- st + 2 * sd_i
      ylim  <- range(c(lower, upper), na.rm = TRUE)
    } else {
      ylim  <- range(st, na.rm = TRUE)
    }
    graphics::plot(t_idx, st, type = "n",
                   main = snames[i], xlab = "Period",
                   ylab = "Deviation from SS",
                   ylim = ylim, ...)
    .dsge_grid()
    .dsge_zero_line()
    if (has_var) {
      .dsge_band(t_idx, lower, upper)
    }
    graphics::lines(t_idx, st, col = .DSGE_INK_PRIMARY, lwd = 1.6)
  }
}

#' @noRd
plot_smoothed_fit <- function(x, ...) {
  n_obs <- ncol(x$smoothed_obs)
  ncols <- ceiling(sqrt(n_obs))
  nrows <- ceiling(n_obs / ncols)

  obs_data <- x$residuals + x$smoothed_obs  # = original data

  old_par <- graphics::par(no.readonly = TRUE)
  on.exit(graphics::par(old_par))
  .dsge_par_grid(nrows, ncols)

  for (j in seq_len(n_obs)) {
    ylim <- range(c(obs_data[, j], x$smoothed_obs[, j]))
    graphics::plot(obs_data[, j], type = "n",
                   main = x$obs_names[j], xlab = "Period", ylab = "",
                   ylim = ylim, ...)
    .dsge_grid()
    graphics::lines(obs_data[, j], col = .DSGE_INK_NEUTRAL, lwd = 1.0)
    graphics::lines(x$smoothed_obs[, j],
                    col = .DSGE_INK_PRIMARY, lwd = 1.8)
    .dsge_legend("topright",
                 legend = c("Data", "Smoothed"),
                 col    = c(.DSGE_INK_NEUTRAL, .DSGE_INK_PRIMARY),
                 lwd    = c(1.0, 1.8))
  }
}


#' Plot Historical Shock Decomposition
#'
#' Creates a stacked bar chart showing the contribution of each structural
#' shock to the observed variables over time.
#'
#' @param x A `dsge_decomposition` object.
#' @param which Which observable(s) to plot. Integer or character. Default
#'   is all.
#' @param ... Additional arguments (currently unused).
#'
#' @return No return value, called for the side effect of producing
#'   stacked bar charts of the historical shock decomposition on the
#'   active graphics device.
#'
#' @export
plot.dsge_decomposition <- function(x, which = NULL, ...) {
  decomp <- x$decomposition
  n_T <- dim(decomp)[1]
  n_obs <- dim(decomp)[2]
  n_comp <- dim(decomp)[3]
  obs_names <- x$obs_names
  shock_names <- x$shock_names

  if (is.null(which)) which <- seq_len(n_obs)
  if (is.character(which)) which <- match(which, obs_names)

  n_plot <- length(which)

  # Colour palette: structural shocks + initial conditions (last slot)
  n_shocks <- n_comp - 1
  cols <- c(.dsge_palette(n_shocks), .DSGE_INK_NEUTRAL)

  old_par <- graphics::par(no.readonly = TRUE)
  on.exit(graphics::par(old_par))
  .dsge_par_grid(n_plot, 1L)
  graphics::par(mar = c(3.2, 3.6, 2.0, 8.5), xpd = TRUE)

  for (j_idx in seq_along(which)) {
    j <- which[j_idx]
    contrib <- decomp[, j, ]  # T x n_comp

    # Separate positive and negative
    pos <- pmax(contrib, 0)
    neg <- pmin(contrib, 0)

    ylim <- c(min(colSums(t(neg))), max(colSums(t(pos)))) * 1.1

    graphics::plot(NULL, xlim = c(1, n_T), ylim = ylim,
                   main = obs_names[j], xlab = "Period",
                   ylab = "Contribution")
    .dsge_grid()
    .dsge_zero_line()

    # Stacked positive bars
    cum_pos <- rep(0, n_T)
    for (k in seq_len(n_comp)) {
      top <- cum_pos + pos[, k]
      for (t in seq_len(n_T)) {
        if (pos[t, k] > 1e-10) {
          graphics::rect(t - 0.4, cum_pos[t], t + 0.4, top[t],
                         col = cols[k],
                         border = "white", lwd = 0.3)
        }
      }
      cum_pos <- top
    }

    # Stacked negative bars
    cum_neg <- rep(0, n_T)
    for (k in seq_len(n_comp)) {
      bottom <- cum_neg + neg[, k]
      for (t in seq_len(n_T)) {
        if (neg[t, k] < -1e-10) {
          graphics::rect(t - 0.4, bottom[t], t + 0.4, cum_neg[t],
                         col = cols[k],
                         border = "white", lwd = 0.3)
        }
      }
      cum_neg <- bottom
    }

    # Overlay actual reconstructed series
    recon <- rowSums(contrib)
    graphics::lines(recon, col = .DSGE_INK_REF, lwd = 1.6)

    # Legend (outside plot, in right margin)
    if (j_idx == 1) {
      .dsge_legend("topright", inset = c(-0.24, 0),
                   legend = shock_names, fill = cols,
                   title = "Shocks")
    }
  }
}

Try the dsge package in your browser

Any scripts or data that you put into this service are public.

dsge documentation built on Sept. 25, 2026, 5:08 p.m.