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)
}Step 3 - Endogenous Transition
2 Step 3: Endogenous Transition
2.1 Objective
Activate endogenous brown-to-green demand-share reallocation on top of the Step 2 calibrated baseline and compare macro/sector outcomes.
2.2 Why this step matters
Participants learn the difference between changing model structure (endogenous transition mechanism) and changing exogenous scenario assumptions. This is central for ecological macro modeling.
2.3 What changed from Step 2
- Added endogenous update rule for demand shares (
beta_C) across brown and green sectors. - Kept all Step 2 accounting identities and production mapping unchanged.
- Introduced transition speed and signal sensitivity as new behavioral levers.
2.4 Equations
Economic question: if household demand shares shift endogenously from brown to green sectors, how much do aggregate GDP and sector composition change relative to the no-transition baseline?
Transition equations: - Signal from previous outputs: signal_t = (x_green,t-1 - x_brown,t-1) / sum_i |x_i,t-1| - Share transfer: shift_t = max(0, transition_speed * (1 + transition_signal_weight * signal_t)) - Share updates: beta_C,brown,t = beta_C,brown,t-1 - shift_t, beta_C,green,t = beta_C,green,t-1 + shift_t - Feasibility: transfer is bounded so shares stay non-negative and sum-preserving.
Macro-production block (unchanged): - f_t = beta_C,t * C_t + beta_G * G_t + I - x_t = L f_t, Y_t = sum_i(v_i * x_i,t) - C_t = c_y * YD_t + c_v * V_{t-1}, V_t = V_{t-1} + YD_t - C_t, B_t = B_{t-1} + (G_t - T_t)
Symbol glossary: - transition_speed: baseline yearly pace of brown-to-green reallocation. - transition_signal_weight: amplifies/dampens transfer based on prior output structure. - beta_C,t: time-varying household demand composition vector. - x_green, x_brown: selected energy-sector outputs used for endogenous signal.
Why two transition parameters (and not one): - transition_speed sets the policy/behavioral baseline pace of reallocation (how much would shift if there were no endogenous signal effect). - transition_signal_weight sets feedback strength from current structure (signal_t) to next shift (path dependence/amplification). - Keeping them separate lets you ask two distinct questions: “faster planned transition” versus “stronger endogenous reinforcement”.
Model status in Step 3: - Exogenous: fiscal parameters and baseline assumptions inherited from Step 2. - Endogenous: transition share updates and all macro/sector paths. - Calibrated: still the Step 2 base-year calibration object.
Scope of the transition mechanism in this step (important): - Only household demand composition (beta_C) is transitioned. Government composition (beta_G) is fixed on purpose, to isolate one mechanism at a time. - Why not transition government demand here? To keep identification clean: Step 3 asks what household-side composition change alone does. You can add a beta_G shift in an extension step. - This is a demand-side energy-transition proxy, not a full structural/technological transition model. - Shares are reallocated with sum_i beta_C,i = 1 each period, so we move demand across sectors rather than creating extra demand by construction. - Aggregate consumption level C_t is still determined by the behavioral equation (c_y, c_v), while beta_C only controls sector allocation of that C_t. - A and Z are fixed at base-year values in Step 3, so production technology/intermediate structure does not transition endogenously here. - Implication: this step estimates reallocation effects through fixed technology; a fuller transition model would allow time-varying A_t/Z_t (and likely endogenous investment/learning).
Two-parameter transition equation (same as code, written to separate roles): - shift_t = transition_speed * (1 + transition_signal_weight * signal_t) clipped to feasible range. - transition_speed controls baseline pace. - transition_signal_weight controls endogenous amplification/damping around that baseline pace.
2.5 TFM (Step 3)
| 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.
Sector-expansion interpretation: - This compact table still comes from sector-level production x = Lf and aggregation Y = sum_i v_i x_i. - What changes in Step 3 is demand composition (beta_C,t), not the accounting closure.
2.6 BSM (Step 3)
| 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 - with V = B, NW_h + NW_g + NW_p = 0.
2.7 Stability conditions (practical)
- Very high
transition_speedor extremetransition_signal_weightcan generate unrealistic jumps in shares. - Keep shares bounded and inspect whether sector trajectories remain economically interpretable.
Rebuild Step 2 baseline objects in this standalone step.
Before rebuilding, print the active core configuration so participants can verify country/year/table settings.
Economic reason for this check: - Transition outcomes are sensitive to IO structure, so country/year/table choice must be explicit. - This printed configuration is the data contract for the full Step 3 run.
core_config <- make_core_config()
core_config_view <- list(
country = core_config$country,
year = core_config$year,
scope = core_config$scope,
table_type = core_config$table_type,
iot_dataset = iot_schema(core_config$table_type)$dataset_id,
cache_dir = core_config$cache_dir
)
core_config_view$country
[1] "AT"
$year
[1] "2020"
$scope
[1] "TOTAL"
$table_type
[1] "product_by_product"
$iot_dataset
[1] "naio_10_cp1700"
$cache_dir
[1] "data"
For standalone execution, the hidden block below reconstructs Step 2 objects exactly once: 1. Re-declare the Step 2 baseline simulator (same equations, no transition). 2. Load IOT and wealth with the same core_config. 3. Rebuild calib3 with build_sim_iot_from_data(). 4. Simulate step2_baseline so Step 3 comparison is always available.
2.8 Algorithm (pseudo-code)
- Recreate Step 2 baseline calibration and no-transition path.
- Initialize demand shares and previous-year sector output.
- Each year: compute transition signal, update shares with bounds, run macro-IO recursion.
- Store both macro indicators and brown/green share series for comparison.
Define Step 3 endogenous transition dynamics.
Local equations implemented in the next code block: - signal_t = (x_green,t-1 - x_brown,t-1) / sum_i |x_i,t-1| - shift_t = max(0, transition_speed * (1 + transition_signal_weight * signal_t)) - shift_t <= beta_C,brown,t-1 (bounded transfer constraint) - beta_C,brown,t = beta_C,brown,t-1 - shift_t, beta_C,green,t = beta_C,green,t-1 + shift_t - f_t = beta_C,t * C_t + beta_G * G_t + I, x_t = L f_t, then usual SFC updates for Y, T, V, B
Code-local symbol reminders: - transition_speed: baseline yearly transfer intensity. - transition_signal_weight: amplification/damping of transfer through previous output structure. - x_prev: previous-year sector output vector used to build signal_t.
Detailed walkthrough of the endogenous algorithm: 1. Initialize with Step 2 calibration and baseline sector output anchor x_prev. 2. At each period, compute a transition signal from previous brown vs green output. 3. Convert that signal into a bounded share transfer shift from brown to green demand share. 4. Recompute macro reduced-form terms (k, b) with the updated demand-share vector. 5. Solve C_t, map to sector output, aggregate to GDP, and update stocks (V, B) exactly as in Step 2. 6. Store brown and green shares each period so structural change can be visualized explicitly.
Why this design is useful: - Only one mechanism is added relative to Step 2 (share dynamics), so causal interpretation is clean. - All accounting identities remain the same, reducing ambiguity about what drives differences.
Transition anchor sectors used in this step (where idx_brown and idx_green come from).
These indices are detected by find_energy_indices() during calibration, then used here for endogenous share transfer. Detection rule in helpers: - brown index = first sector code matching (^|_)C19 (coke/refined petroleum products) - green index = first sector code matching (^|_)D35|(^|_)D (electricity/gas/steam aggregate) - if either is missing, the model stops with an explicit error
Print the actual sector mapping for the current dataset/configuration.
idx_b <- calib3$idx$idx_brown
idx_g <- calib3$idx$idx_green
transition_sector_map <- data.frame(
role = c('brown', 'green'),
index = c(idx_b, idx_g),
sector_code = calib3$sector_codes[c(idx_b, idx_g)],
sector_label = calib3$sector_labels[c(idx_b, idx_g)],
stringsAsFactors = FALSE
)
transition_sector_map role index sector_code sector_label
1 brown 10 CPA_C19 Coke and refined petroleum products
2 green 24 CPA_D Electricity, gas, steam and air conditioning
simulate_sim_iot_endogenous_transition <- function(calib,
T = 20,
c_y = 0.82,
c_v = 0.03,
g_G = 0.01,
transition_speed = 0.0015,
transition_signal_weight = 1) {
years <- calib$base_year + seq_len(T) - 1
out <- data.frame(
year = years,
GDP = NA_real_, TAX = NA_real_, YD = NA_real_, C = NA_real_, G = NA_real_,
V = NA_real_, DEF = NA_real_, B = NA_real_, betaC_brown = NA_real_, betaC_green = NA_real_
)
V_prev <- calib$V0
B_prev <- calib$B0
G_prev <- calib$G0
x_prev <- as.numeric(calib$L %*% (calib$beta_C * sum(calib$I_i0) + calib$beta_G * calib$G0 + calib$I_i0))
beta_C <- calib$beta_C
for (tt in seq_len(T)) {
G_t <- if (tt == 1) calib$G0 else G_prev * (1 + g_G)
if (is.finite(calib$idx$idx_green) && is.finite(calib$idx$idx_brown)) {
signal <- (x_prev[calib$idx$idx_green] - x_prev[calib$idx$idx_brown]) / pmax(sum(abs(x_prev)), 1)
shift <- max(0, transition_speed * (1 + transition_signal_weight * signal))
shift <- min(shift, max(beta_C[calib$idx$idx_brown] - 1e-8, 0))
beta_C[calib$idx$idx_brown] <- beta_C[calib$idx$idx_brown] - shift
beta_C[calib$idx$idx_green] <- beta_C[calib$idx$idx_green] + shift
}
k <- as.numeric(t(calib$va_coeff) %*% (calib$L %*% beta_C))
b <- as.numeric(t(calib$va_coeff) %*% (calib$L %*% (calib$beta_G * G_t + calib$I_i0)))
den <- 1 - c_y * (1 - calib$tau_y) * k
C_t <- max((c_y * (1 - calib$tau_y) * b + c_v * V_prev) / den, 0)
fd_t <- beta_C * C_t + calib$beta_G * G_t + calib$I_i0
x_t <- pmax(as.numeric(calib$L %*% fd_t), 0)
Y_t <- sum(calib$va_coeff * x_t)
TAX_t <- calib$tau_y * Y_t
YD_t <- Y_t - TAX_t
V_t <- V_prev + YD_t - C_t
DEF_t <- G_t - TAX_t
B_t <- B_prev + DEF_t
out[tt, c("GDP", "TAX", "YD", "C", "G", "V", "DEF", "B", "betaC_brown", "betaC_green")] <- c(
Y_t, TAX_t, YD_t, C_t, G_t, V_t, DEF_t, B_t,
if (is.finite(calib$idx$idx_brown)) beta_C[calib$idx$idx_brown] else NA_real_,
if (is.finite(calib$idx$idx_green)) beta_C[calib$idx$idx_green] else NA_real_
)
x_prev <- x_t
V_prev <- V_t
B_prev <- B_t
G_prev <- G_t
}
out
}Core play: transition speed.
This play changes only transition_speed, holding all other coefficients fixed. Economic meaning: you are changing the pace of endogenous demand reallocation, not fiscal stance or technology. After running, compare the new path against Step 2 baseline to isolate structural-composition effects on GDP and shares.
transition_speed_base <- 0
transition_signal_weight_base <- 1
transition_speed_play <- 0.001 # TODO [core:step3_speed] Change endogenous transition speed and compare GDP/energy-share paths Hint: Try 0.001 then 0.004
step3_base <- simulate_sim_iot_endogenous_transition(
calib3, T = 20,
transition_speed = transition_speed_base,
transition_signal_weight = transition_signal_weight_base
)
step3_play <- simulate_sim_iot_endogenous_transition(
calib3, T = 20,
transition_speed = transition_speed_play,
transition_signal_weight = transition_signal_weight_base
)Print quick structural diagnostics for the transition run.
Purpose of diagnostics: - Feasibility check: shares should stay in sensible ranges. - Direction check: brown share should typically fall and green share rise if transition is active. - Macro check: final GDP level provides quick context before plotting full paths.
transition_anchor_info <- data.frame(
role = c('brown', 'green'),
sector_code = calib3$sector_codes[c(calib3$idx$idx_brown, calib3$idx$idx_green)],
sector_label = calib3$sector_labels[c(calib3$idx$idx_brown, calib3$idx$idx_green)],
betaC_initial = c(step3_base$betaC_brown[1], step3_base$betaC_green[1]),
stringsAsFactors = FALSE
)
transition_anchor_info role sector_code sector_label betaC_initial
1 brown CPA_C19 Coke and refined petroleum products 0.007532253
2 green CPA_D Electricity, gas, steam and air conditioning 0.025367721
share_diag <- data.frame(
metric = c('min_betaC_brown', 'max_betaC_brown', 'min_betaC_green', 'max_betaC_green', 'final_GDP_mEUR'),
value = c(
min(step3_play$betaC_brown, na.rm = TRUE),
max(step3_play$betaC_brown, na.rm = TRUE),
min(step3_play$betaC_green, na.rm = TRUE),
max(step3_play$betaC_green, na.rm = TRUE),
tail(step3_play$GDP, 1)
)
)
share_diag metric value
1 min_betaC_brown 1.000000e-08
2 max_betaC_brown 6.502731e-03
3 min_betaC_green 2.639724e-02
4 max_betaC_green 3.289996e-02
5 final_GDP_mEUR 4.306834e+05
Why brown share can start very low (this is not a bug): - beta_C comes directly from base-year household final demand shares in the chosen IOT classification. - In many recent EU tables, direct household demand share for the selected brown anchor (C19: refined petroleum/coke products) is small, so betaC_brown starts low. - The printed transition_anchor_info table above shows the exact sector codes/labels and their initial shares for your active dataset.
Plot Step 3 core comparison.
How to interpret panel 1 (GDP): difference from baseline shows macro consequence of composition reallocation. How to interpret panel 2 (shares): confirms whether and how fast the structural shift actually occurred. Reading both together avoids misinterpretation: GDP can move even with modest share shifts if affected sectors have high multiplier/value-added content.
par(mfrow = c(1, 2), mar = c(4, 4, 3, 1), cex.main = 0.9)
y_gdp <- range(c(step3_base$GDP, step3_play$GDP), na.rm = TRUE)
if (!all(is.finite(y_gdp))) stop("Non-finite GDP values in Step 3 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]))
plot(step3_base$year, step3_base$GDP, type = "l", lwd = 2, col = "steelblue",
xlab = "year", ylab = "GDP (million EUR)", ylim = y_gdp, main = "Step 3 Core: GDP")
lines(step3_play$year, step3_play$GDP, lwd = 2, col = "firebrick")
legend("topleft",
legend = c(paste0("base (s=", transition_speed_base, ")"), paste0("play (s=", transition_speed_play, ")")),
col = c("steelblue", "firebrick"), lty = 1, bty = "n", cex = 0.85, xpd = NA)
share_rng <- range(c(step3_base$betaC_brown, step3_base$betaC_green, step3_play$betaC_brown, step3_play$betaC_green), na.rm = TRUE)
share_pad <- 0.05 * diff(share_rng)
if (!is.finite(share_pad) || share_pad == 0) share_pad <- 0.002
plot(step3_base$year, step3_base$betaC_brown, type = "l", lwd = 2, col = "firebrick", lty = 2,
xlab = "year", ylab = "share",
ylim = c(max(0, share_rng[1] - share_pad), min(1, share_rng[2] + share_pad)),
main = paste0("Step 3 Core: demand shares (base s=", transition_speed_base, ")"))
lines(step3_play$year, step3_play$betaC_brown, lwd = 2, col = "firebrick", lty = 1)
lines(step3_base$year, step3_base$betaC_green, lwd = 2, col = "darkgreen", lty = 2)
lines(step3_play$year, step3_play$betaC_green, lwd = 2, col = "darkgreen", lty = 1)
points(step3_play$year, step3_play$betaC_green, pch = 16, cex = 0.35, col = "darkgreen")
legend("topright",
legend = c("brown base", "brown play", "green base", "green play"),
col = c("firebrick", "firebrick", "darkgreen", "darkgreen"),
lty = c(2, 1, 2, 1), bty = "n", cex = 0.8, xpd = NA)par(mfrow = c(1, 1))Optional play: transition asymmetry / signal sensitivity.
This play changes transition_signal_weight, which governs how strongly current structure feeds back into next period transfer. Higher values make the transition more path-dependent: successful green expansion can accelerate further shifts. Compare dashed vs solid lines to see whether feedback amplifies or dampens structural change.
transition_signal_weight_play <- 0.5 # TODO [optional:step3_signal] Change transition signal sensitivity and compare brown/green share dynamics Hint: Try 0.5, 1.0, 1.8
step3_opt <- simulate_sim_iot_endogenous_transition(calib3, T = 20, transition_speed = transition_speed_play, transition_signal_weight = transition_signal_weight_play)
share_rng_opt <- range(c(step3_play$betaC_brown, step3_opt$betaC_brown, step3_play$betaC_green, step3_opt$betaC_green), na.rm = TRUE)
share_pad_opt <- 0.05 * diff(share_rng_opt)
if (!is.finite(share_pad_opt) || share_pad_opt == 0) share_pad_opt <- 0.002
plot(step3_play$year, step3_play$betaC_brown, type = "l", lwd = 2, col = "firebrick",
xlab = "year", ylab = "beta_C shares",
ylim = c(max(0, share_rng_opt[1] - share_pad_opt), min(1, share_rng_opt[2] + share_pad_opt)),
main = paste0("Step 3 Optional: signal w (s=", transition_speed_play, ")"))
lines(step3_opt$year, step3_opt$betaC_brown, lwd = 2, col = "firebrick", lty = 2)
lines(step3_play$year, step3_play$betaC_green, lwd = 2, col = "darkgreen")
lines(step3_opt$year, step3_opt$betaC_green, lwd = 2, col = "darkgreen", lty = 2)
legend("topright",
legend = c(paste0("brown base (w=", transition_signal_weight_base, ")"), paste0("brown play (w=", transition_signal_weight_play, ")"),
paste0("green base (w=", transition_signal_weight_base, ")"), paste0("green play (w=", transition_signal_weight_play, ")")),
col = c("firebrick", "firebrick", "darkgreen", "darkgreen"), lty = c(1, 2, 1, 2), bty = "n", cex = 0.78, xpd = NA)2.9 Insight (Step 3)
- Endogenous transition can reallocate sectoral demand even when aggregate fiscal settings are unchanged.
- GDP response depends on the IO structure and value-added content of sectors gaining/losing demand shares.
2.10 What can go wrong / interpretation caveat
- If transition parameters are too aggressive, share paths may be mechanically feasible but economically implausible.
- This mechanism captures demand-side structural change only; technology and price dynamics are still fixed.
2.11 Interpretation
Step 3 introduces a separate endogenous-transition algorithm on top of the Step 2 baseline dynamics.