# Tests for second-order perturbation (v0.8.0) # --- Helper: simple nonlinear model for testing --- build_nl_ar1 <- function(rhoA = 0.90) { dsgenl_model( "Y = A", "A(+1) = A^rhoA", observed = "Y", unobserved = character(0), exo_state = "A", fixed = list(), start = list(rhoA = rhoA), ss_guess = c(Y = 1, A = 1) ) } # Helper: model with genuine nonlinearity (curvature matters) build_nl_consumption <- function() { dsgenl_model( "Y = A * H", "1 / C = beta * R / C(+1)", "Y = C", "R = 1 / beta", "A(+1) = A^rhoA", observed = "Y", unobserved = c("C", "H", "R"), exo_state = "A", fixed = list(beta = 0.99), start = list(rhoA = 0.90), ss_guess = c(Y = 1, C = 1, H = 1, R = 1.0101, A = 1) ) } test_that("solve_dsge with order=2 returns second-order solution", { mod <- build_nl_ar1() sol <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) expect_s3_class(sol, "dsge_solution") expect_equal(sol$order, 2L) expect_false(is.null(sol$g_xx)) expect_false(is.null(sol$h_xx)) expect_false(is.null(sol$g_ss)) expect_false(is.null(sol$h_ss)) }) test_that("first-order solution preserved in second-order object", { mod <- build_nl_ar1() sol1 <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 1) sol2 <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) # G and H should be identical expect_equal(sol2$G, sol1$G, tolerance = 1e-6) expect_equal(sol2$H, sol1$H, tolerance = 1e-6) expect_equal(sol2$M, sol1$M, tolerance = 1e-6) expect_true(sol2$stable) }) test_that("g_xx and h_xx have correct dimensions", { mod <- build_nl_ar1() sol <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) n_c <- nrow(sol$G) n_s <- ncol(sol$G) expect_equal(dim(sol$g_xx), c(n_c, n_s, n_s)) expect_equal(dim(sol$h_xx), c(n_s, n_s, n_s)) expect_equal(length(sol$g_ss), n_c) expect_equal(length(sol$h_ss), n_s) }) test_that("order=2 with linear model gives error", { m <- dsge_model( obs(y ~ z), state(z ~ rho * z), start = list(rho = 0.9) ) expect_error( solve_dsge(m, params = c(rho = 0.9), order = 2), "dsgenl_model" ) }) test_that("order=1 is default and backward compatible", { mod <- build_nl_ar1() sol_default <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01)) sol_explicit <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 1) expect_equal(sol_default$G, sol_explicit$G) expect_equal(sol_default$H, sol_explicit$H) expect_null(sol_default$order) # first-order doesn't set order field }) test_that("simulate_2nd_order produces output", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) sim <- simulate_2nd_order(sol, n = 50, n_burn = 10, seed = 42) expect_equal(nrow(sim$states), 50L) expect_equal(nrow(sim$controls), 50L) expect_false(is.null(sim$state_levels)) expect_false(is.null(sim$control_levels)) expect_equal(sim$order, 2L) }) test_that("simulate_2nd_order errors on first-order solution", { mod <- build_nl_ar1() sol <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 1) expect_error(simulate_2nd_order(sol), "order = 2") }) test_that("irf_2nd_order produces output", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) irf <- irf_2nd_order(sol, shock = "A", size = 1, periods = 20) expect_s3_class(irf, "dsge_irf_2nd") expect_true("period" %in% names(irf)) expect_true("variable" %in% names(irf)) expect_true("response" %in% names(irf)) expect_equal(irf$order[1], 2L) }) test_that("irf_2nd_order errors on unknown shock", { mod <- build_nl_ar1() sol <- solve_dsge(mod, params = c(rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) expect_error(irf_2nd_order(sol, shock = "bad_shock"), "Unknown shock") }) test_that("second-order IRF is asymmetric for positive vs negative shocks", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) irf_pos <- irf_2nd_order(sol, shock = "A", size = 3, periods = 10) irf_neg <- irf_2nd_order(sol, shock = "A", size = -3, periods = 10) # Extract Y responses y_pos <- irf_pos$response[irf_pos$variable == "Y"] y_neg <- irf_neg$response[irf_neg$variable == "Y"] # Second-order: positive and negative should NOT be mirror images # (they would be mirror images under first-order) # Test that the asymmetry is nonzero (even if small) asymmetry <- y_pos + y_neg # would be zero for first-order # With small models the asymmetry may be tiny, so just check structure expect_equal(length(y_pos), length(y_neg)) expect_equal(length(y_pos), 10L) }) test_that("second-order solution with multi-variable model works", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) expect_equal(sol$order, 2L) expect_true(nrow(sol$G) >= 2) # multiple controls expect_true(ncol(sol$G) >= 1) # at least one state # g_ss should be named expect_false(is.null(names(sol$g_ss))) expect_false(is.null(names(sol$h_ss))) }) test_that("invalid order value gives error", { mod <- build_nl_ar1() expect_error(solve_dsge(mod, params = c(rhoA = 0.9), order = 3), "order must be 1 or 2") expect_error(solve_dsge(mod, params = c(rhoA = 0.9), order = 0), "order must be 1 or 2") }) test_that("second-order constant correction (g_ss) is finite", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) expect_true(all(is.finite(sol$g_ss))) expect_true(all(is.finite(sol$h_ss))) }) test_that("second-order simulation is reproducible with seed", { mod <- build_nl_consumption() sol <- suppressWarnings( solve_dsge(mod, params = c(beta = 0.99, rhoA = 0.90), shock_sd = c(A = 0.01), order = 2) ) sim1 <- simulate_2nd_order(sol, n = 20, n_burn = 5, seed = 123) sim2 <- simulate_2nd_order(sol, n = 20, n_burn = 5, seed = 123) expect_equal(sim1$states, sim2$states) expect_equal(sim1$controls, sim2$controls) })