Step 5 - Exogenous Transition

1 Shared Setup

Load minimal packages and global options only.

Check that required packages are installed. Stop early with a clear error if anything is missing. This keeps the tutorial run deterministic and minimal.

required_packages <- c("jsonlite", "ggplot2")
missing_packages <- required_packages[!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing_packages) > 0) {
  stop("Missing package(s): ", paste(missing_packages, collapse = ", "), call. = FALSE)
}

1.1 Dependency Context

This step is standalone: it reuses IO loading helpers and introduces exogenous transition closure logic.

helper_dir <- if (dir.exists('R/helpers')) 'R/helpers' else '../R/helpers'
source(file.path(helper_dir, 'cache_io.R'))
source(file.path(helper_dir, 'jsonstat_parse.R'))
source(file.path(helper_dir, 'iot_load.R'))
source(file.path(helper_dir, 'config_wealth.R'))

1.2 Function and Variable Location Map

This step uses IO/data helpers plus closure solver utilities.

Helper functions (file paths): - make_core_config() in R/helpers/config_wealth.R. - download_or_load_iot(), find_energy_indices() in R/helpers/iot_load.R. - Closure solver functions (solve_io_consistency and related options) are sourced from closure_utils.R in chunk step5_functions_exogenous_transition.

Local function in this step file: - simulate_iot_exogenous_transition() is defined in chunk step5_functions_exogenous_transition.

Where key variables are set in this step: - core_iot (and core_config if missing) are prepared in step5_run_load_iot. - Baseline run step5_baseline is created in step5_run_baseline. - Core play variable closure_play is set in step5_play_core_closure. - Optional play variable eps_green_play (and implied eps_brown) is set in step5_play_optional_eps.

2 Step 5: IOT Load + Exogenous Transition (Advanced/Optional)

2.1 Objective

Run an exogenous energy transition experiment where green/brown targets are imposed and closure rules determine how the rest of the economy adjusts.

2.2 Why this step matters

This step teaches closure logic explicitly: different assumptions can produce different sector-adjustment paths even under the same headline growth targets.

2.3 What changed from Step 4

  • Transition is now imposed exogenously (targets), not generated endogenously.
  • Closure options are made explicit and compared.
  • This is advanced/optional and intended as a methodological deep dive.

2.4 Equations

Economic question: with fixed aggregate and sector targets, which additional closure assumptions are needed to produce a consistent IO path?

Core yearly consistency problem: - Technology update from base transactions and output guess: A_t = Z_base / x_t - IO consistency: x_t = (I - A_t)^(-1) f_t - Exogenous targets: g_green = eps_green, g_brown = eps_brown, aggregate g_target - Closure decides how remaining sectors/final demand absorb residual adjustment.

Underdetermination (equation counting intuition): - Imposing two energy targets plus one aggregate growth target usually leaves more unknown sector adjustments than independent constraints. - Therefore extra closure assumptions are required to select one feasible path.

Closure options (economic interpretation): - residual-others: non-target sectors absorb remaining adjustment. - fixed-others: non-target sectors are held close to baseline and demand adjusts elsewhere. - uniform-demand: adjustment spreads broadly across demand components. - eps-only: follow only sector target rates with minimal additional balancing restrictions.

Symbol glossary: - eps_green, eps_brown: imposed sector growth rates. - g_target: aggregate growth target. - g_other: implied adjustment for non-target sectors. - rel_io_resid: numerical consistency residual from iterative solve.

Model status in Step 5: - Exogenous: target growth rates and closure choice. - Endogenous: sector outputs and implied residual growth allocation. - Calibrated: base IO structure from loaded IOT.

Reference note: closure sensitivity emphasis follows structural-transition SFC-IO work such as Pettena & Raberto (2025).

2.5 TFM (Step 5)

Flow Households Government Production (IOT aggregate) Sum
Income Y +Y 0 -Y 0
Taxes T -T +T 0 0
Consumption C -C 0 +C 0
Government demand G 0 -G +G 0
Change in wealth/debt Delta_V (= Delta_B) -Delta_V +Delta_B 0 0
Column sum 0 0 0 0

From column sums: - Delta_V = YD - C. - Delta_B = G - T. - Y = C + G. - Delta_V = Delta_B.

Step-5 interpretation: - Exogenous transition targets and closure options change the sector allocation behind production, while these compact institutional stock-flow identities stay the same. - A fully sector-expanded TFM would show the target sectors and residual sectors explicitly.

2.6 BSM (Step 5)

Stock Households Government Production (IOT aggregate) Sum
Wealth/debt stock V (=B) +V -B 0 0
Net worth NW +NW_h +NW_g +NW_p 0

Net-worth identities: - NW_h = V - NW_g = -B - NW_p = 0 (in this minimal closure, production has no autonomous financial stock) - with V = B, NW_h + NW_g + NW_p = 0.

Interpretation for Step 5: - Closure rules change how output adjusts across production sectors. - They do not add new institutional financial assets/liabilities in this minimal setup. - So production appears explicitly as a sector in BSM, with net worth set to zero by construction.

Define Step 5 exogenous-transition function.

2.7 Algorithm (pseudo-code)

  1. Choose closure option and sector targets (eps_green, eps_brown, g_target).
  2. For each year, solve IO consistency under that closure.
  3. Record sector outputs and implied residual adjustments.
  4. Compare alternative closures under identical targets.

Pseudo-code: each year call the closure solver, enforce exogenous brown/green rates, and store GDP plus energy-sector outputs.

closure_file <- if (file.exists("closure_utils.R")) "closure_utils.R" else "../closure_utils.R"
source(closure_file)
simulate_iot_exogenous_transition <- function(iot,
                                              T = 20,
                                              closure_option = "residual-others",
                                              g_target = 0.01,
                                              eps_green = 0.04,
                                              eps_brown = -0.04,
                                              max_iter = 20) {
  idx <- find_energy_indices(iot$sector_codes)

  x_prev <- as.numeric(iot$x0)
  F_prev <- as.numeric(iot$F0)
  years <- iot$base_year + seq_len(T) - 1

  out <- data.frame(
    year = years,
    closure = closure_option,
    GDP = NA_real_,
    X_green = NA_real_,
    X_brown = NA_real_,
    g_green = NA_real_,
    g_brown = NA_real_,
    g_other = NA_real_,
    io_resid = NA_real_
  )

  for (tt in seq_len(T)) {
    sol <- solve_io_consistency(
      Z_base = iot$Z0,
      F_prev = F_prev,
      x_init = x_prev,
      diag_mat = diag(iot$n),
      option = closure_option,
      eps_R = eps_green,
      eps_N = eps_brown,
      g = g_target,
      idx_ren = idx$idx_green,
      idx_nren = idx$idx_brown,
      p_out_ren = 1,
      p_out_nren = 1,
      va_coeff = iot$va_coeff,
      target = "output",
      max_iter = max_iter,
      rel_io_tol = 1e-8
    )

    x_now <- pmax(as.numeric(sol$X), 0)
    F_now <- pmax(as.numeric(sol$F), 0)

    out[tt, c("GDP", "X_green", "X_brown", "g_green", "g_brown", "g_other", "io_resid")] <- c(
      sum(iot$va_coeff * x_now),
      x_now[idx$idx_green],
      x_now[idx$idx_brown],
      sol$g_R,
      sol$g_N,
      sol$g_O,
      sol$rel_io_resid
    )

    x_prev <- x_now
    F_prev <- F_now
  }

  out
}

Load IOT for this advanced step.

Use the same core configuration machinery and cached data path as Step 2.

step5_T <- if (interactive()) 12 else 20
step5_max_iter <- if (interactive()) 12 else 20

if (!exists("core_iot", inherits = FALSE)) {
  core_config <- make_core_config()
  core_iot <- download_or_load_iot(core_config)
}
iot <- core_iot
c(country = iot$country, year = iot$base_year, n_sectors = iot$n)
  country      year n_sectors 
     "AT"    "2020"      "64" 

Run baseline closure path.

Use baseline closure and fixed aggregate growth target. Store GDP and energy-sector paths as the comparison baseline.

base_closure <- "residual-others"
base_g_target <- 0.01
base_eps_green <- 0.04
base_eps_brown <- -0.04
step5_baseline <- simulate_iot_exogenous_transition(
  iot,
  T = step5_T,
  closure_option = base_closure,
  g_target = base_g_target,
  eps_green = base_eps_green,
  eps_brown = base_eps_brown,
  max_iter = step5_max_iter
)

Core play: switch closure option.

Run identical targets under a different closure rule and compare GDP path response.

closure_options <- c("residual-others", "fixed-others", "uniform-demand", "eps-only")
closure_play <- closure_options[3]  # @exercise[id=step5_closure;kind=core;question_expr=closure_options[3];prompt="Switch closure option and inspect GDP and green/brown paths";hint="Compare fixed-others vs uniform-demand"]

step5_closure_play <- simulate_iot_exogenous_transition(
  iot,
  T = step5_T,
  closure_option = closure_play,
  g_target = base_g_target,
  eps_green = base_eps_green,
  eps_brown = base_eps_brown,
  max_iter = step5_max_iter
)

Plot closure comparison.

Overlay GDP paths to isolate closure sensitivity.

par(mfrow = c(1, 2), mar = c(4, 4, 3, 1), cex.main = 0.9)
y_gdp <- range(c(step5_baseline$GDP, step5_closure_play$GDP), na.rm = TRUE)
ratio_base <- step5_baseline$X_brown / pmax(step5_baseline$X_green, 1e-9)
ratio_play <- step5_closure_play$X_brown / pmax(step5_closure_play$X_green, 1e-9)
y_ratio <- range(c(ratio_base, ratio_play), na.rm = TRUE)
if (!all(is.finite(y_gdp))) stop("Non-finite GDP values in Step 5 core plot.", call. = FALSE)
if (!all(is.finite(y_ratio))) stop("Non-finite brown/green ratio in Step 5 core plot.", call. = FALSE)
if (y_gdp[1] == y_gdp[2]) y_gdp <- y_gdp + c(-1, 1) * max(1, 0.05 * abs(y_gdp[1]))
if (y_ratio[1] == y_ratio[2]) y_ratio <- y_ratio + c(-1, 1) * max(1, 0.05 * abs(y_ratio[1]))
plot(step5_baseline$year, step5_baseline$GDP, type = "l", lwd = 2, col = "steelblue",
     xlab = "year", ylab = "GDP (million EUR)", ylim = y_gdp, main = "Step 5 Core: GDP by closure")
lines(step5_closure_play$year, step5_closure_play$GDP, lwd = 2, col = "firebrick")
legend("topleft", legend = c(paste0("base (", base_closure, ")"), paste0("play (", closure_play, ")")),
       col = c("steelblue", "firebrick"), lty = 1, bty = "n", cex = 0.8)
plot(step5_baseline$year, ratio_base,
     type = "l", lwd = 2, col = "steelblue", ylim = y_ratio,
     xlab = "year", ylab = "X_brown / X_green", main = paste0("Step 5 Core: mix (base ", base_closure, ")"))
lines(step5_closure_play$year, ratio_play, lwd = 2, col = "firebrick")

par(mfrow = c(1, 1))

Optional play: strengthen exogenous transition rates.

Change green/brown target rates and inspect the brown/green output ratio path.

eps_green_play <- 0.02  # @exercise[id=step5_eps;kind=optional;question_expr=0.02;prompt="Change exogenous green growth target and compare brown/green ratio";hint="Use eps_brown = -eps_green"]
eps_brown_play <- -eps_green_play

step5_eps_play <- simulate_iot_exogenous_transition(
  iot,
  T = step5_T,
  closure_option = base_closure,
  g_target = base_g_target,
  eps_green = eps_green_play,
  eps_brown = eps_brown_play,
  max_iter = step5_max_iter
)

ratio_base <- step5_baseline$X_brown / pmax(step5_baseline$X_green, 1e-9)
ratio_play <- step5_eps_play$X_brown / pmax(step5_eps_play$X_green, 1e-9)
y_ratio <- range(c(ratio_base, ratio_play), na.rm = TRUE)
if (!all(is.finite(y_ratio))) stop("Non-finite brown/green ratio in Step 5 optional plot.", call. = FALSE)
if (y_ratio[1] == y_ratio[2]) y_ratio <- y_ratio + c(-1, 1) * max(1, 0.05 * abs(y_ratio[1]))
plot(step5_baseline$year, ratio_base, type = "l", lwd = 2, col = "steelblue",
     xlab = "year", ylab = "X_brown / X_green", ylim = y_ratio,
     main = paste0("Step 5 Optional: base eps_g=", base_eps_green))
lines(step5_eps_play$year, ratio_play, lwd = 2, col = "darkgreen")
legend("topright", legend = c(paste0("base (eps_g=", base_eps_green, ")"), paste0("play (eps_g=", eps_green_play, ")")),
       col = c("steelblue", "darkgreen"), lty = 1, bty = "n", cex = 0.85)

2.8 Insight (Step 5)

  • Under the same headline targets, closure choices redistribute adjustment differently across sectors.
  • Report both GDP and sector-mix outcomes; aggregate growth alone hides structural burden sharing.

2.9 What can go wrong / interpretation caveat

  • Some closure/target combinations may be numerically feasible but economically implausible.
  • Always inspect residuals and sector trajectories, not only headline GDP.

2.10 Interpretation

Closure choice and exogenous energy targets change the sectoral adjustment path even when aggregate growth is fixed.