tests/testthat/test-smoother.R

# Tests for smooth_states(), smooth_shocks(), shock_decomposition()

test_that("smooth_states works on ML fit", {
  # Simple AR(1) model
  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  set.seed(42)
  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)

  expect_s3_class(sm, "dsge_smoothed")
  expect_equal(nrow(sm$smoothed_states), 100)
  expect_equal(ncol(sm$smoothed_states), 1)  # 1 state
  expect_equal(nrow(sm$smoothed_obs), 100)
  expect_equal(ncol(sm$smoothed_obs), 1)  # 1 observable
  expect_equal(nrow(sm$residuals), 100)

  # Smoothed obs should approximate the data
  expect_true(cor(sm$smoothed_obs[, 1], z) > 0.95)
})

test_that("smooth_states dimensions match for multi-state model", {
  m <- dsge_model(
    obs(y1 ~ a),
    obs(y2 ~ b),
    state(a ~ rho_a * a),
    state(b ~ rho_b * b),
    start = list(rho_a = 0.5, rho_b = 0.3)
  )

  set.seed(42)
  e1 <- rnorm(80)
  e2 <- rnorm(80)
  a <- numeric(80); b <- numeric(80)
  for (i in 2:80) {
    a[i] <- 0.8 * a[i-1] + e1[i]
    b[i] <- 0.5 * b[i-1] + e2[i]
  }
  fit <- estimate(m, data = data.frame(y1 = a, y2 = b))

  sm <- smooth_states(fit)

  expect_equal(nrow(sm$smoothed_states), 80)
  expect_equal(ncol(sm$smoothed_states), 2)  # 2 states
  expect_equal(ncol(sm$smoothed_obs), 2)     # 2 observables
})

test_that("smooth_shocks works on ML fit", {
  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  set.seed(42)
  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))

  sh <- smooth_shocks(fit)

  expect_s3_class(sh, "dsge_smoothed_shocks")
  expect_equal(nrow(sh$shocks), 99)  # T-1 shocks
  expect_equal(ncol(sh$shocks), 1)   # 1 shock
  expect_equal(length(sh$shock_names), 1)

  # Shock SD should be approximately 1 (or close to estimated sd)
  expect_true(sd(sh$shocks[, 1]) > 0.5 && sd(sh$shocks[, 1]) < 2.0)
})

test_that("shock_decomposition sums to observed data", {
  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  set.seed(42)
  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)

  expect_s3_class(hd, "dsge_decomposition")
  expect_equal(dim(hd$decomposition)[1], 100)  # T
  expect_equal(dim(hd$decomposition)[2], 1)    # n_obs
  expect_equal(dim(hd$decomposition)[3], 2)    # 1 shock + initial

  # Key test: sum of contributions = smoothed observables
  recon <- apply(hd$decomposition, c(1, 2), sum)
  # Should match the smoothed observables (from smooth_states)
  sm <- smooth_states(fit)
  # Decomposition sums to smoothed obs
  expect_true(max(abs(recon[, 1] - sm$smoothed_obs[, 1])) < 1e-6)
})

test_that("shock_decomposition with multiple shocks", {
  m <- dsge_model(
    obs(y1 ~ a),
    obs(y2 ~ b),
    state(a ~ rho_a * a),
    state(b ~ rho_b * b),
    start = list(rho_a = 0.5, rho_b = 0.3)
  )

  set.seed(42)
  e1 <- rnorm(80)
  e2 <- rnorm(80)
  a <- numeric(80); b <- numeric(80)
  for (i in 2:80) {
    a[i] <- 0.8 * a[i-1] + e1[i]
    b[i] <- 0.5 * b[i-1] + e2[i]
  }
  fit <- estimate(m, data = data.frame(y1 = a, y2 = b))

  hd <- shock_decomposition(fit)

  expect_equal(dim(hd$decomposition)[3], 3)  # 2 shocks + initial

  # Reconstruction test for both observables
  recon <- apply(hd$decomposition, c(1, 2), sum)
  sm <- smooth_states(fit)
  expect_true(max(abs(recon - sm$smoothed_obs)) < 1e-6)
})

test_that("print methods work", {
  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  set.seed(42)
  e <- rnorm(50)
  z <- numeric(50)
  for (i in 2:50) z[i] <- 0.8 * z[i - 1] + e[i]
  fit <- estimate(m, data = data.frame(y = z))

  expect_output(print(smooth_states(fit)), "DSGE Smoothed States")
  expect_output(print(smooth_shocks(fit)), "DSGE Smoothed Structural Shocks")
  expect_output(print(shock_decomposition(fit)), "DSGE Historical Shock Decomposition")
})

test_that("plot methods execute without error", {
  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  set.seed(42)
  e <- rnorm(50)
  z <- numeric(50)
  for (i in 2:50) z[i] <- 0.8 * z[i - 1] + e[i]
  fit <- estimate(m, data = data.frame(y = z))

  sm <- smooth_states(fit)
  hd <- shock_decomposition(fit)

  # Plot should not error
  pdf(NULL)
  expect_no_error(plot(sm, type = "states"))
  expect_no_error(plot(sm, type = "fit"))
  expect_no_error(plot(hd))
  dev.off()
})

test_that("analytical check: AR(1) smoothed states match true states", {
  # For a simple AR(1) with known rho, the Kalman smoother should
  # recover the true states well
  set.seed(123)
  rho_true <- 0.9
  sd_true <- 1.0
  n <- 200

  z_true <- numeric(n)
  for (i in 2:n) z_true[i] <- rho_true * z_true[i-1] + rnorm(1, sd = sd_true)

  m <- dsge_model(
    obs(y ~ z),
    state(z ~ rho * z),
    start = list(rho = 0.5)
  )

  fit <- estimate(m, data = data.frame(y = z_true))
  sm <- smooth_states(fit)

  # Smoothed states should correlate strongly with true states
  r <- cor(sm$smoothed_states[, 1], z_true)
  expect_true(r > 0.95)
})

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.