Yuxuan / Economics Notebook library

sequence_space_jacobian

Krusell–Smith with Sequence-Space Jacobians

A heterogeneous-agent transition exercise using sequence-space Jacobians and the fake news algorithm.

Julia 1.12.5 notebook · Static rendering · Code was not re-executed

  • Author: Yuxuan Zhao
  • Date: 2026-06-07

This notebook demonstrates how to solve a Krusell-Smith model using sequence-space Jacobians.

1) Model

Time is discrete. The economy is populated by a continuum of households with idiosyncratic productivity and a representative competitive firm.

The aggregate exogenous object is the TFP path

$$ Z=(Z_0,\ldots,Z_{T-1}). $$

The path is known at date 0, so the transition is solved under perfect foresight with respect to aggregate shocks. Households still face idiosyncratic income risk.

Idiosyncratic productivity $e_t$ follows a finite-state Markov process on grid $\mathcal E$ with transition matrix $\Pi$:

$$ \Pr(e_{t+1}=e'\mid e_t=e)=\Pi(e'\mid e). $$

The process is independent across households, so there is no aggregate uncertainty coming from idiosyncratic shocks.

The economy starts from the steady state at date 0. The initial household distribution is $D_0=D_{ss}$ and current aggregate capital is $K_0=K_{ss}$. The finite transition is computed for dates $t=0,\ldots,T-1$. The terminal conditions are

$$ Z_t=Z_{ss}\quad \text{for }t\geq T, \qquad K_t=K_{ss}\quad \text{for }t\geq T+1. $$

Equivalently, the household terminal value at date $T$ is approximated by the steady-state continuation value.

Given a exogeneous path $\{Z_t\}_{t=0}^{T-1}$ and initial & terminal conditions, the perfect-foresight transition equilibrium consists of prices $\{r_t,w_t\}_{t=0}^{T-1}$, household distributions $\{D_t\}_{t=0}^{T-1}$, household policy function and value function $\{g_t,V_t\}_{t=0}^{T-1}$, aggregate quantities $\{C_t,A_{t+1},K_{t+1},Y_t\}_{t=0}^{T-1}$, and the firm's production plan such that:

Household problem

For $t \geq T$, we have $r_t=r_{ss}$ and $w_t=w_{ss}$ for $t\geq T$, so the household problem is stationary: $$ \begin{aligned} V_t(e_t,a_t)=V_{ss}(e_t,a_t) =\max_{c_t,a_{t+1}}\left\{ \frac{c_t^{1-1/\psi}}{1-1/\psi} +\beta\sum_{e_{t+1}}\Pi(e_{t+1}\mid e_t)V_{ss}(e_{t+1},a_{t+1}) \right\} \end{aligned} $$

For $t = 0,\ldots,T-1$, given a price path $\{r_t,w_t\}_{t=0}^{T-1}$ and terminal value $V_T=V_{ss}$, household solves:

$$ \begin{aligned} V_t(e_t,a_t)=\max_{c_t,a_{t+1}}&\left\{ \frac{c_t^{1-1/\psi}}{1-1/\psi} +\beta\sum_{e_{t+1}}\Pi(e_{t+1}\mid e_t)V_{t+1}(e_{t+1},a_{t+1}) \right\} \quad \text{s.t.} \\ c_t+a_{t+1} &=(1+r_t)a_t+w_te_t, \\ \qquad a_{t+1} &\geq \underline a. \end{aligned} $$

Given the policy $a_{t+1}=g_t(e_t,a_t)$ and consumption policy $c_t=c_t(e_t,a_t)$, the distribution evolves forward from $D_0=D_{ss}$. Aggregate asset supply and consumption are

$$ A_{t+1}=\int g_t(e,a)\,dD_t(e,a), \qquad C_t=\int c_t(e,a)\,dD_t(e,a). $$

Firm problem

At each date $t=0,\ldots,T-1$, a representative competitive firm chooses capital and effective labor inputs $(K_t^f,L_t^f)$ to maximize static profit:

$$ \max_{K_t^f,L_t^f}\; Z_t(K_t^f)^\alpha(L_t^f)^{1-\alpha}-(r_t+\delta)K_t^f-w_tL_t^f. $$

The first-order conditions are

$$ r_t+\delta=\alpha Z_t\left(\frac{K_t^f}{L_t^f}\right)^{\alpha-1}, \qquad w_t=(1-\alpha)Z_t\left(\frac{K_t^f}{L_t^f}\right)^\alpha. $$

In equilibrium, the firm rents aggregate capital and aggregate effective labor:

$$ K_t^f=K_t,\qquad L_t^f=L_t. $$

Aggregate effective labor is the cross-sectional integral of idiosyncratic productivity,

$$ L_t=\int e\,dD_t(e,a). $$

Because idiosyncratic shocks are independent across households and the Markov process is stationary, this aggregate is constant along the transition. We normalize this effective labor scale to match the KS1998 calibration notes, where aggregate labor is about $0.2944$ in bad times and $0.3142$ in good times. Here we use

$$ L_t=L_{ss}=0.3043. $$

With this normalization, the firm's equilibrium conditions become

$$ Y_t=Z_tK_t^\alpha L_{ss}^{1-\alpha}, $$

$$ r_t=\alpha Z_t\left(\frac{K_t}{L_{ss}}\right)^{\alpha-1}-\delta, \qquad w_t=(1-\alpha)Z_t\left(\frac{K_t}{L_{ss}}\right)^\alpha. $$

Market clearing

The asset market clears when household aggregate saving equals next-period aggregate capital:

$$ A_{t+1}=K_{t+1},\qquad t=0,\ldots,T-1. $$

The goods market condition is

$$ Y_t=C_t+I_t, \qquad I_t=K_{t+1}-(1-\delta)K_t. $$

In the code below, the SSJ unknown is the next-period capital sequence

$$ K^+=(K_1,\ldots,K_T). $$

The current capital sequence used by the firm is therefore

$$ (K_0,K_1,\ldots,K_{T-1})=(K_{ss},K_1,\ldots,K_{T-1}). $$

Summary of equilibrium conditions

Given $K^+$ and $Z$, the firm block gives prices, the household block gives aggregate asset supply $A^+=(A_1,\ldots,A_T)$, and the target equation is

$$ H_t(K^+,Z)=A_{t+1}(K^+,Z)-K_{t+1}=0, \qquad t=0,\ldots,T-1. $$

The goods-market residual is also computed as a diagnostic, but the SSJ solve uses the asset-market target.

2) Julia Setup

We use helper functions from SSJ_Function.jl. This file contains the generic finite-difference tools, the Rouwenhorst income process, the EGM household solver, distribution iteration, steady-state calibration, and transition-path household block.

using Pkg

function find_project_root(start_dir=pwd())
    dir = abspath(start_dir)
    while true
        if isfile(joinpath(dir, "Project.toml"))
            return dir
        end
        parent = dirname(dir)
        parent == dir && error("Could not find Project.toml from $(start_dir)")
        dir = parent
    end
end

project_root = find_project_root()
Pkg.activate(project_root)

using LinearAlgebra
using Plots

include(joinpath(project_root, "src", "SSJ_Function.jl"))
compute_ks_steady_state

3) Calibration and Steady State

We use the parameter values shown below explicitly rather than relying on hidden defaults.

Imposed structural parameters

We follow the usual macro calibration style: preference and technology parameters are imposed directly, rather than chosen to hit steady-state targets.

$$ \beta=0.96, \qquad \psi=1, \qquad \alpha=0.36, \qquad \delta=0.025. $$

The idiosyncratic productivity process is approximated by a 7-state Rouwenhorst chain:

$$ \rho_e=0.966, \qquad \sigma_e=0.05. $$

The aggregate technology level is normalized to $Z_{ss}=1$. For effective labor, we use the same scale as in the KS1998 calibration notes. In those notes, hours when employed are $\tilde l=0.3271$, unemployment is $u_b=0.10$ in bad times and $u_g=0.04$ in good times, so effective aggregate labor is

$$ L_b=\tilde l(1-u_b)=0.2944, \qquad L_g=\tilde l(1-u_g)=0.3142. $$

Here we use the middle of this range as the steady-state labor normalization:

$$ L_{ss}=0.3043. $$

The asset grid is a numerical choice:

$$ n_A=500, \qquad a_{min}=0, \qquad a_{max}=200. $$

Steady-state computation

Given a candidate aggregate capital level $K$, firm prices are

$$ r(K)=\alpha Z_{ss}\left(\frac{K}{L_{ss}}\right)^{\alpha-1}-\delta, \qquad w(K)=(1-\alpha)Z_{ss}\left(\frac{K}{L_{ss}}\right)^\alpha. $$

At these prices, the household steady-state problem implies aggregate asset supply $A(K)$. The steady-state capital stock is the fixed point

$$ A(K_{ss})=K_{ss}. $$

After solving this scalar equation, output and consumption are recovered from

$$ Y_{ss}=Z_{ss}K_{ss}^{\alpha}L_{ss}^{1-\alpha}, \qquad C_{ss}=Y_{ss}-\delta K_{ss}. $$

Thus in this version, $\beta$, $\alpha$, $\delta$, $Z_{ss}$, and the labor scale $L_{ss}$ are imposed as structural parameters. The object solved from equilibrium is $K_{ss}$, not a parameter chosen to match a target interest rate or target output level.

params = KSParams(
    beta = 0.96,
    eis = 1.0,
    delta = 0.025,
    alpha = 0.36,
    Z = 1.0,
    rho_e = 0.966,
    sd_e = 0.05,
    L = 0.3043,   # effective labor scale from KS1998: about 0.2944 to 0.3142
    nE = 7,
    nA = 500,
    amin = 0.0,
    amax = 200.0,
)

ss = compute_ks_steady_state(params)

println("beta = ", ss.beta)
println("K = ", ss.K)
println("Z = ", ss.Z)
println("r = ", ss.r)
println("w = ", ss.w)
println("Y = ", ss.Y)
println("A = ", ss.A)
println("C = ", ss.C)
println("asset_mkt = ", ss.asset_mkt)
println("goods_mkt = ", ss.goods_mkt)
beta = 0.96
K = 4.328892381727622
Z = 1.0
r = 0.04081647296453147
w = 1.6645119416268583
Y = 0.7914234122453953
A = 4.328892381909209
C = 0.6832011028777369
asset_mkt = 1.815871897292709e-10
goods_mkt = -1.7553224151978242e-10

The household block also produces policy functions and a stationary distribution. The plot below shows the steady-state consumption policy as a function of assets for each productivity state.

plot(
    ss.a_grid,
    ss.c_policy',
    xlabel="assets",
    ylabel="consumption",
    title="Steady-state consumption policy",
    label=false,
    linewidth=1.5
)
No description has been provided for this image

4) Blocks and DAG

The DAG uses one unknown sequence and one exogenous sequence:

$$ K^+=(K_1,\ldots,K_T),\qquad Z=(Z_0,\ldots,Z_{T-1}). $$

  1. Firm block

    Inputs: the current capital sequence and the TFP sequence,

    $$ K^{current}=(K_0,K_1,\ldots,K_{T-1}),\qquad Z=(Z_0,\ldots,Z_{T-1}). $$

    Since the unknown is $K^+=(K_1,\ldots,K_T)$ and $K_0=K_{ss}$, the code constructs

    $$ K^{current}=lag(K^+)=(K_{ss},K_1,\ldots,K_{T-1}). $$

    Outputs:

    $$ r=(r_0,\ldots,r_{T-1}),\qquad w=(w_0,\ldots,w_{T-1}),\qquad Y=(Y_0,\ldots,Y_{T-1}). $$

  2. Household transition block

    Inputs:

    $$ r=(r_0,\ldots,r_{T-1}),\qquad w=(w_0,\ldots,w_{T-1}). $$

    The block solves household policies backward from the terminal steady state, then pushes the distribution forward from $D_0=D_{ss}$. Outputs:

    $$ A^+=(A_1,\ldots,A_T),\qquad C=(C_0,\ldots,C_{T-1}). $$

  3. Asset-market block

    The SSJ target residual is

    $$ asset\_mkt_t=A_{t+1}-K_{t+1}, \qquad t=0,\ldots,T-1. $$

The reduced sequence-space system is therefore

$$ H(K^+,Z)=A^+(K^+,Z)-K^+=0. $$

InlineSVG(ks_dag_svg())
No description has been provided for this image

Implementing the DAG blocks

The code below implements the three blocks shown in the DAG. These are model-specific functions, so they are kept in the notebook rather than hidden inside SSJ_Function.jl.

Firm block: from capital and TFP to prices

Input paths are the unknown next-period capital sequence $K^+=(K_1,\ldots,K_T)$ and the exogenous TFP sequence $Z=(Z_0,\ldots,Z_{T-1})$. Since firms at date $t$ use current capital, the first step is the timing conversion

$$ K^{current}=(K_0,K_1,\ldots,K_{T-1})=(K_{ss},K_1,\ldots,K_{T-1}). $$

For each date $t=0,\ldots,T-1$, the firm block applies the static FOCs

$$ r_t=\alpha Z_t\left(\frac{K_t}{L}\right)^{\alpha-1}-\delta, \qquad w_t=(1-\alpha)Z_t\left(\frac{K_t}{L}\right)^\alpha, $$

and production

$$ Y_t=Z_tK_t^\alpha L^{1-\alpha}. $$

Output: paths $(r,w,Y)$.

# Timing helper: convert next-period capital choices K^+ = (K_1,...,K_T)
# into current capital used by firms, (K_0,K_1,...,K_{T-1}).
function lag_path(x, x0)
    return vcat(x0, x[1:end-1])
end

# Firm block
# Inputs:
#   Kplus: next-period capital sequence (K_1,...,K_T)
#   Z:     TFP sequence (Z_0,...,Z_{T-1})
#   ss:    steady-state object, used for initial capital K_0
#   params: model parameters
# Outputs:
#   r, w, Y: price and output sequences for dates 0,...,T-1
function firm_paths(Kplus, Z, ss, params)
    K_current = lag_path(Kplus, ss.K)
    L = ss.L

    r = params.alpha .* Z .* (K_current ./ L).^(params.alpha - 1) .- params.delta
    w = (1 - params.alpha) .* Z .* (K_current ./ L).^params.alpha
    Y = Z .* K_current.^params.alpha .* L.^(1 - params.alpha)

    return r, w, Y
end
firm_paths (generic function with 1 method)

Household transition block: from prices to aggregate saving

Input paths are prices $(r_0,\ldots,r_{T-1})$ and $(w_0,\ldots,w_{T-1})$. The block solves the household problem backward using terminal value $V_T=V_{ss}$:

$$ V_t(e,a)=\max_{c,a'}\left\{u(c)+\beta\sum_{e'}\Pi(e'\mid e)V_{t+1}(e',a')\right\} $$

subject to

$$ c+a'=(1+r_t)a+w_te, \qquad a'\geq \underline a. $$

This gives policy functions $a'=g_t(e,a)$ and $c=c_t(e,a)$. Then the distribution is pushed forward from $D_0=D_{ss}$, and we aggregate

$$ A_{t+1}=\int g_t(e,a)dD_t(e,a), \qquad C_t=\int c_t(e,a)dD_t(e,a). $$

Output: paths $(A^+,C)$.

# Household transition block
# Inputs:
#   r_path, w_path: price sequences for dates 0,...,T-1
#   ss: terminal steady state and initial distribution D_0
#   params: model parameters
# Outputs:
#   A: next-period aggregate asset supply sequence (A_1,...,A_T)
#   C: aggregate consumption sequence (C_0,...,C_{T-1})
#   D_path: distribution sequence (D_0,...,D_T)
function household_transition(r_path, w_path, ss, params)
    T = length(r_path)

    # Backward pass: solve household policies from T-1 back to 0.
    # The terminal object is the steady-state marginal value V_A,T = V_A,ss.
    Va_next = ss.Va
    a_policies = Vector{Matrix{Float64}}(undef, T)
    c_policies = Vector{Matrix{Float64}}(undef, T)

    for t in T:-1:1
        Va, a_policy, c_policy = egm_step(
            Va_next, ss.a_grid, ss.e_grid, ss.Pi,
            r_path[t], w_path[t], ss.beta, params.eis
        )
        a_policies[t] = a_policy
        c_policies[t] = c_policy
        Va_next = Va
    end

    # Forward pass: start from D_0 = D_ss and aggregate decisions.
    D = ss.D
    A = zeros(T)
    C = zeros(T)
    D_path = Vector{Matrix{Float64}}(undef, T + 1)
    D_path[1] = copy(D)

    for t in 1:T
        A[t] = aggregate_assets(D, a_policies[t])
        C[t] = aggregate_consumption(D, c_policies[t])
        D = forward_distribution(D, a_policies[t], ss.a_grid, ss.Pi)
        D_path[t + 1] = copy(D)
    end

    return (; A, C, D_path)
end
household_transition (generic function with 1 method)

Asset-market block: from household saving to target residual

Given $K^+$ and $Z$, the firm and household blocks imply household aggregate asset supply

$$ A^+(K^+,Z)=(A_1,\ldots,A_T). $$

The equilibrium target is asset-market clearing at each date:

$$ H_t(K^+,Z)=A_{t+1}(K^+,Z)-K_{t+1}=0, \qquad t=0,\ldots,T-1. $$

The code also computes the goods-market residual

$$ goods\_mkt_t=Y_t-C_t-I_t, \qquad I_t=K_{t+1}-(1-\delta)K_t, $$

but this is a diagnostic, not the SSJ target.

# Reduced sequence-space residual map
# Inputs:
#   Kplus: unknown path (K_1,...,K_T)
#   Z:     exogenous path (Z_0,...,Z_{T-1})
# Output:
#   named tuple containing the target residual and useful model variables
function ks_residual(Kplus, Z, ss, params)
    r, w, Y = firm_paths(Kplus, Z, ss, params)
    hh = household_transition(r, w, ss, params)
    A, C = hh.A, hh.C

    # Target equation: household asset supply must equal next-period capital.
    asset_mkt = A .- Kplus

    # Goods-market residual is only a diagnostic.
    K_current = lag_path(Kplus, ss.K)
    I = Kplus .- (1 - params.delta) .* K_current
    goods_mkt = Y .- C .- I

    return (; asset_mkt, goods_mkt, A, C, D_path = hh.D_path, I, r, w, Y)
end

# Convenience map for later Jacobian calculations.
# Given Kplus and Z in levels, return one model variable in levels.
function ks_variable_levels(varname, Kplus, Z, ss, params)
    out = ks_residual(Kplus, Z, ss, params)

    if varname == :K
        return Kplus
    elseif varname == :Z
        return Z
    else
        return getproperty(out, varname)
    end
end
ks_variable_levels (generic function with 1 method)
T = 80
Kplus_ss_path = fill(ss.K, T)
Z_ss_path = fill(ss.Z, T)

out_ss = ks_residual(Kplus_ss_path, Z_ss_path, ss, params)
println("Maximum steady-state asset-market residual: ", maximum(abs.(out_ss.asset_mkt)))
println("Maximum steady-state goods-market residual: ", maximum(abs.(out_ss.goods_mkt)))
Maximum steady-state asset-market residual: 1.5759467508758007e-8
Maximum steady-state goods-market residual: 5.074560771589631e-10

5) Jacobians and Linearized Dynamics

Around the steady state, the residual map is written in terms of the next-period capital path $K^+=(K_1,\ldots,K_T)$:

$$ H(K^+,Z)=A(r(K^+,Z),w(K^+,Z))-K^+. $$

$$ H(K^+_{ss}+dK^+,Z_{ss}+dZ)\approx H(K^+_{ss},Z_{ss})+H_KdK^++H_ZdZ. $$

Since $H(K^+_{ss},Z_{ss})=0$, the linearized equilibrium condition is

$$ H_KdK^++H_ZdZ=0. $$

Thus the general-equilibrium Jacobian for the next-period capital path is

$$ G^{K^+,Z}=\frac{dK^+}{dZ}=-H_K^{-1}H_Z. $$

In block form,

$$ \begin{aligned} H_K&=J^{A,r}J^{r,K}+J^{A,w}J^{w,K}-I,\\ H_Z&=J^{A,r}J^{r,Z}+J^{A,w}J^{w,Z}. \end{aligned} $$

The code below follows the same DAG logic as the Python SSJ package. First compute the firm-block Jacobians and the household partial-equilibrium Jacobians, then compose them into the asset-market residual Jacobians $H_K$ and $H_Z$.

function household_pe_jacobians(ss, params; T=80, h=1e-5)
    r_ss_path = fill(ss.r, T)
    w_ss_path = fill(ss.w, T)
    base = household_transition(r_ss_path, w_ss_path, ss, params)

    function jacobians_for_price(price::Symbol)
        J_A = zeros(T, T)
        J_C = zeros(T, T)
        for s in 1:T
            r_path = copy(r_ss_path)
            w_path = copy(w_ss_path)
            if price == :r
                r_path[s] += h
            elseif price == :w
                w_path[s] += h
            else
                error("price must be :r or :w")
            end
            out = household_transition(r_path, w_path, ss, params)
            J_A[:, s] = (out.A .- base.A) ./ h
            J_C[:, s] = (out.C .- base.C) ./ h
        end
        return J_A, J_C
    end

    A_r, C_r = jacobians_for_price(:r)
    A_w, C_w = jacobians_for_price(:w)
    return (; A_r, C_r, A_w, C_w)
end

function firm_jacobians(ss, params; T=80, h=1e-5)
    Kss = fill(ss.K, T)
    Zss = fill(ss.Z, T)
    r_K = finite_difference_jacobian_at(K -> firm_paths(K, Zss, ss, params)[1], Kss; h=h)
    w_K = finite_difference_jacobian_at(K -> firm_paths(K, Zss, ss, params)[2], Kss; h=h)
    Y_K = finite_difference_jacobian_at(K -> firm_paths(K, Zss, ss, params)[3], Kss; h=h)
    r_Z = finite_difference_jacobian_at(Z -> firm_paths(Kss, Z, ss, params)[1], Zss; h=h)
    w_Z = finite_difference_jacobian_at(Z -> firm_paths(Kss, Z, ss, params)[2], Zss; h=h)
    Y_Z = finite_difference_jacobian_at(Z -> firm_paths(Kss, Z, ss, params)[3], Zss; h=h)
    return (; r_K, w_K, Y_K, r_Z, w_Z, Y_Z)
end

hh_J = household_pe_jacobians(ss, params; T=T, h=1e-5)
firm_J = firm_jacobians(ss, params; T=T, h=1e-5)

H_K = hh_J.A_r * firm_J.r_K + hh_J.A_w * firm_J.w_K - Matrix{Float64}(I, T, T)
H_Z = hh_J.A_r * firm_J.r_Z + hh_J.A_w * firm_J.w_Z

G_KZ = -H_K \ H_Z

println("size(H_K) = ", size(H_K))
println("size(H_Z) = ", size(H_Z))
println("size(G_KZ) = ", size(G_KZ))
size(H_K) = (80, 80)
size(H_Z) = (80, 80)
size(G_KZ) = (80, 80)

Once $G^{K^+,Z}$ is known, the remaining aggregate responses follow from the DAG chain rule. The firm block maps $(K^+,Z)$ into prices and output,

$$ \begin{aligned} G^{r,Z}&=J^{r,K}G^{K^+,Z}+J^{r,Z},\\ G^{w,Z}&=J^{w,K}G^{K^+,Z}+J^{w,Z},\\ G^{Y,Z}&=J^{Y,K}G^{K^+,Z}+J^{Y,Z}. \end{aligned} $$

The household block then maps prices into aggregate assets and consumption,

$$ \begin{aligned} G^{A,Z}&=J^{A,r}G^{r,Z}+J^{A,w}G^{w,Z},\\ G^{C,Z}&=J^{C,r}G^{r,Z}+J^{C,w}G^{w,Z}. \end{aligned} $$

Thus $X_K$ and $X_Z$ are not computed as one black-box map for every variable. They come from the corresponding block: firm Jacobians for $r,w,Y$, and household partial-equilibrium Jacobians for $A,C$.

G_rZ = firm_J.r_K * G_KZ + firm_J.r_Z
G_wZ = firm_J.w_K * G_KZ + firm_J.w_Z
G_YZ = firm_J.Y_K * G_KZ + firm_J.Y_Z

G_CZ = hh_J.C_r * G_rZ + hh_J.C_w * G_wZ
G_AZ = hh_J.A_r * G_rZ + hh_J.A_w * G_wZ

println("size(G_CZ) = ", size(G_CZ))
size(G_CZ) = (80, 80)

6) Impulse Responses

After the Jacobians are computed, any small TFP shock path is handled by matrix multiplication. For example,

$$ dK^+=G^{K^+,Z}dZ,\qquad dC=G^{C,Z}dZ. $$

We use a 1 percent AR(1)-style TFP shock,

$$ dZ_t=0.01 Z_{ss}\rho^t, $$

and compare responses for several values of $\rho$.

rhos = [0.2, 0.4, 0.6, 0.8, 0.9]
dZ = hcat([0.01 .* ss.Z .* (rho .^ (0:T-1)) for rho in rhos]...)

# Responses implied by the general-equilibrium Jacobians.
dr = 10_000 .* (G_rZ * dZ)
dK = 100 .* (G_KZ * dZ) ./ ss.K
dC = 100 .* (G_CZ * dZ) ./ ss.C

plot_horizon = 80
quarters = 0:plot_horizon-1
labels = ["rho=$(rho)" for rho in rhos]

dZ_percent = 100 .* dZ ./ ss.Z

p0 = plot(
    quarters,
    dZ_percent[1:plot_horizon, :],
    label=reshape(labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="percent deviation",
    title="TFP shock paths"
)

p1 = plot(
    quarters,
    dr[1:plot_horizon, :],
    label=reshape(labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="basis points",
    title="Interest rate responses"
)

p2 = plot(
    quarters,
    dK[1:plot_horizon, :],
    label=reshape(labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="percent deviation",
    title="Capital responses"
)

plot(p0, p1, p2, layout=(1, 3), size=(1200, 350))
No description has been provided for this image

The impulse-response plots above show that, for persistent shocks, especially high $\rho$, aggregate variables are still visibly away from steady state near the truncation horizon. If prices and capital have not fully converged, the distribution updating operator is also not exactly the steady-state operator. It is therefore unlikely that the cross-sectional distribution has fully returned to $D_{ss}$ by date $T$.

It means the finite-horizon perfect-foresight approximation may be affected by truncation error. We need to check whether the transition path is close enough to steady state by date $T$ for the terminal condition to be a good approximation. If not, we may need to increase $T$ or use a more accurate terminal condition.

7) Terminal Distribution Diagnostic

The finite-horizon transition uses the steady-state household marginal value as the terminal condition,

$$ V_{a,T}=V_{a,ss}. $$

This pins down the continuation problem after date $T$, but it does not impose

$$ D_T=D_{ss}. $$

We use a 1 percent AR(1)-style TFP shock,

$$ dZ_t=0.01 Z_{ss}\rho^t, $$

Set different values of $\rho$ to see how the distribution convergence depends on shock persistence.

Take each linear general-equilibrium response computed above and form the level paths

$$ Z^{(j)}=Z_{ss}+dZ^{(j)}, \qquad K^{+,(j)}=K^+_{ss}+G^{K^+,Z}dZ^{(j)}. $$

Then feed these paths back into the full nonlinear transition block. Starting from $D_0=D_{ss}$, the household block generates

$$ \{A_{t+1}, C_t, D_t\}_{t=0}^{T}. $$

We plot the pathwise distribution distance

$$ \Delta_t^{D,(j)} = \|D_t^{(j)}-D_{ss}\|_\infty = \sup_{e,a}|D_t^{(j)}(e,a)-D_{ss}(e,a)|, \qquad t=0,\ldots,T. $$

On the discrete grid this is computed as maximum(abs.(D_t .- ss.D)). The initial value is exactly zero because $D_0=D_{ss}$, so the log-scale plot omits that zero point.

function distribution_sup_norm_path(Kplus_path, Z_path, ss, params)
    out = ks_residual(Kplus_path, Z_path, ss, params)
    return [maximum(abs.(D .- ss.D)) for D in out.D_path]
end

D_sup_paths = hcat([
    distribution_sup_norm_path(
        Kplus_ss_path .+ G_KZ * dZ[:, j],
        Z_ss_path .+ dZ[:, j],
        ss,
        params,
    )
    for j in eachindex(rhos)
]...)

# D_0 equals D_ss exactly, so it is zero and cannot be shown on a log scale.
D_sup_paths_plot = copy(D_sup_paths)
D_sup_paths_plot[D_sup_paths_plot .== 0.0] .= NaN

periods_D = 0:T
log_ticks = 10.0 .^ (-5:-1)
log_tick_labels = ["10^-5", "10^-4", "10^-3", "10^-2", "10^-1"]

plot(
    periods_D,
    D_sup_paths_plot,
    label=reshape(labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="||D_t - D_ss||_inf",
    title="Distribution distance from steady state",
    yaxis=:log10,
    ylims=(1e-5, 1e-1),
    yticks=(log_ticks, log_tick_labels),
    size=(850, 350),
)
No description has been provided for this image

It means we will have truncation error in the distribution, especially for persistent shocks or those shocks happened near the truncation horizon. For example, if we set $T = 80$ and we want to study the news shocks effect happened at date $79$, the distribution at date $T$ is still visibly away from steady state, so the terminal condition is not a good approximation.

We need to extend the truncation horizon to a larger $T$ to reduce this error.

8) News Shocks

A news shock is simply a TFP path whose nonzero entry starts in the future. The linear response is obtained using the same Jacobian matrix. The columns of $G^{K^+,Z}$ can be interpreted as responses of the next-period capital path to one-period TFP news arriving at different dates.

news_dates = [5, 10, 15, 20, 25]
dZ_news = zeros(T, length(news_dates))
for (j, date) in enumerate(news_dates)
    dZ_news[date + 1, j] = 0.01 * ss.Z
end

# Responses to one-period TFP news shocks arriving at different future dates.
dr_news = 10_000 .* (G_rZ * dZ_news)
dK_news = 100 .* (G_KZ * dZ_news) ./ ss.K
dZ_news_percent = 100 .* dZ_news ./ ss.Z
news_labels = ["news at t=$d" for d in news_dates]

p_news_z = plot(
    quarters,
    dZ_news_percent[1:plot_horizon, :],
    label=reshape(news_labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="percent deviation",
    title="TFP news paths"
)

p_news_r = plot(
    quarters,
    dr_news[1:plot_horizon, :],
    label=reshape(news_labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="basis points",
    title="Interest rate responses"
)

p_news_k = plot(
    quarters,
    dK_news[1:plot_horizon, :],
    label=reshape(news_labels, 1, :),
    linewidth=2,
    xlabel="quarters",
    ylabel="percent deviation",
    title="Capital responses"
)

plot(p_news_z, p_news_r, p_news_k, layout=(1, 3), size=(1200, 350))
No description has been provided for this image

9) Nonlinear Perfect-Foresight Response

Everything above used a first-order approximation around the steady state. For a small TFP path $dZ$, we used the Jacobian formula

$$ dK^+=G^{K^+,Z}dZ. $$

This is fast, but it is only a local linear approximation. In this section we instead solve the full nonlinear perfect-foresight transition problem for a larger TFP shock.

Given a level path

$$ Z=(Z_0,\ldots,Z_{T-1}), $$

the unknown is still the next-period capital path

$$ K^+=(K_1,\ldots,K_T). $$

For any candidate $K^+$, the residual map does the full nonlinear calculation:

  1. firm block: $(K^+,Z)\rightarrow (r,w,Y)$,
  2. household transition block: $(r,w)\rightarrow (A^+,C)$,
  3. asset-market residual:

$$ H(K^+,Z)=A^+(K^+,Z)-K^+. $$

The nonlinear equilibrium condition is

$$ H(K^+,Z)=0. $$

The code below solves this equation by a quasi-Newton method. Starting from the steady-state path $K^+_0=(K_{ss},\ldots,K_{ss})$, each iteration evaluates the nonlinear residual exactly, but uses the steady-state Jacobian $H_K$ as an approximate derivative:

$$ K^+_{n+1}=K^+_n-H_K^{-1}H(K^+_n,Z). $$

So this is not just matrix-multiplying the linear response. The residual $H(K^+_n,Z)$ is recomputed from the full nonlinear firm and household blocks at every iteration. Finally, we compare the nonlinear response with the linear approximation from the Jacobian:

$$ K^+_{linear}=K^+_{ss}+G^{K^+,Z}dZ. $$

If the shock is small, the two responses should be close. If the shock is large, the difference shows the importance of nonlinearities.

function solve_ks_nonlinear(Z_level, ss, params, H_K; tol=1e-8, max_iter=30)
    T = length(Z_level)
    K = fill(ss.K, T)
    out = ks_residual(K, Z_level, ss, params)

    for it in 1:max_iter
        err = maximum(abs.(out.asset_mkt))
        if err < tol
            return K, out, it
        end
        K += -(H_K \ out.asset_mkt)
        out = ks_residual(K, Z_level, ss, params)
    end
    error("Nonlinear solver did not converge.")
end

big_dZ = 0.10 .* ss.Z .* (0.8 .^ (0:T-1))
Z_big = fill(ss.Z, T) + big_dZ

K_nonlin, td_nonlin, iters = solve_ks_nonlinear(Z_big, ss, params, H_K)
K_lin = fill(ss.K, T) + G_KZ * big_dZ
r_lin = ss.r .+ G_rZ * big_dZ

println("Nonlinear iterations: ", iters)
println("Maximum nonlinear asset-market residual: ", maximum(abs.(td_nonlin.asset_mkt)))

p_nonlin = plot(
    quarters,
    10_000 .* (td_nonlin.r[1:plot_horizon] .- ss.r),
    label="nonlinear",
    linewidth=2.5,
    xlabel="quarters",
    ylabel="basis points",
    title="Interest rate response to a 10% TFP shock"
)
plot!(
    p_nonlin,
    quarters,
    10_000 .* (r_lin[1:plot_horizon] .- ss.r),
    label="linear",
    linewidth=2.5,
    linestyle=:dash
)
Nonlinear iterations: 6
Maximum nonlinear asset-market residual: 1.6121770585186823e-10
No description has been provided for this image

10) Fake News Algorithm

10.1) Naive Algorithm

Previously, we computed the household sequence-space Jacobians using direct finite differences.

First, define the asset-market clearing residual

$$ H(K^+,Z)=A(r(K^+,Z),w(K^+,Z))-K^+. $$

Around the steady state,

$$ dH = H_K dK^+ + H_Z dZ. $$

Market clearing requires $dH=0$, so

$$ dK^+ = G^{K^+,Z}dZ, \qquad G^{K^+,Z}=-H_K^{-1}H_Z. $$

Once $G^{K^+,Z}$ is known, the Jacobians of the other aggregate variables with respect to TFP follow from the DAG chain rule. For example,

$$ \begin{aligned} G^{r,Z}&=J^{r,K}G^{K^+,Z}+J^{r,Z}, \\ G^{w,Z}&=J^{w,K}G^{K^+,Z}+J^{w,Z}, \\ G^{C,Z}&=J^{C,r}G^{r,Z}+J^{C,w}G^{w,Z}. \end{aligned} $$

The key expensive objects are the household partial-equilibrium Jacobians. For example,

$$ J^{C,r} = \begin{bmatrix} \dfrac{\partial C_0}{\partial r_0} & \dfrac{\partial C_0}{\partial r_1} & \cdots & \dfrac{\partial C_0}{\partial r_{T-1}} \\ \dfrac{\partial C_1}{\partial r_0} & \dfrac{\partial C_1}{\partial r_1} & \cdots & \dfrac{\partial C_1}{\partial r_{T-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \dfrac{\partial C_{T-1}}{\partial r_0} & \dfrac{\partial C_{T-1}}{\partial r_1} & \cdots & \dfrac{\partial C_{T-1}}{\partial r_{T-1}} \end{bmatrix}. $$

The direct finite-difference method computes this matrix column by column. We want to know how each $r_s$ affects each $C_t$. The most straightforward method is:

  1. First, perturb $r_0$ and compute the responses of all $C_0,\ldots,C_{T-1}$, which gives the first column.
  2. Second, perturb $r_1$ and compute the responses of all $C_0,\ldots,C_{T-1}$, which gives the second column.
  3. Continue this process until perturbing $r_{T-1}$ to get the last column.

Thus there are $T$ perturbation experiments, and each experiment contains a length-$T$ backward-forward transition. The expensive household computations therefore scale like $O(T^2)$.

10.2) Fake News Algorithm

Now consider how to compute the Jacobian more efficiently. We use $J^{C,r}$ as an example:

$$ J^{C,r}_{t,s} = \frac{\partial C_t}{\partial r_s}, \qquad t,s=0,\ldots,T-1. $$

Household Block

Input: $(r_0,\ldots,r_{T-1},w_0,\ldots,w_{T-1})$.

Output: $(A_1,\ldots,A_T,C_0,\ldots,C_{T-1})$.

The block solves the household problem backward using terminal value $V_T=V_{ss}$:

$$ V_t(e,a)=\max_{c,a'}\left\{u(c)+\beta\sum_{e'}\Pi(e'\mid e)V_{t+1}(e',a')\right\} $$

subject to

$$ c+a'=(1+r_t)a+w_te, \qquad a'\geq \underline a. $$

This gives policy functions $a'=g_t(e,a)$ and $c=c_t(e,a)$. Then the distribution is pushed forward from $D_0=D_{ss}$, and we aggregate

$$ A_{t+1}=\int g_t(e,a)dD_t(e,a), \qquad C_t=\int c_t(e,a)dD_t(e,a). $$

We want to compute the Jacobian $J^{C,r}$, for example:

$$ J^{C,r}_{t,s} = \frac{\partial C_t}{\partial r_s} $$

Taking a first-order derivative with respect to $r_s$ gives

$$ J^{C,r}_{t,s} = \sum_{e,a} \frac{\partial c_t(e,a)}{\partial r_s} D_{ss}(e,a) + \sum_{e,a} c_{ss}(e,a) \frac{\partial D_t(e,a)}{\partial r_s}. $$

Define

$$ dc^{r,s}_t(e,a) = \frac{\partial c_t(e,a)}{\partial r_s}, \qquad dD^{r,s}_t(e,a) = \frac{\partial D_t(e,a)}{\partial r_s}. $$

Then

$$ J^{C,r}_{t,s} = \underbrace{\sum_{e,a} dc^{r,s}_t(e,a) D_{ss}(e,a)}_{\text{direct policy response}} + \underbrace{\sum_{e,a} c_{ss}(e,a) dD^{r,s}_t(e,a)}_{\text{distribution response}}. $$

The first term is the direct policy response. The second term is the distribution response.

First Row

First, we consider how to compute the first row:

$$ \left( J^{C,r}_{0,0}, J^{C,r}_{0,1}, \ldots, J^{C,r}_{0,T-1} \right), $$

where

$$ J^{C,r}_{0,s} = \frac{\partial C_0}{\partial r_s}. $$

This object measures how news about a one-period interest-rate perturbation at date $s$ changes aggregate consumption at date $0$.

At date $0$, households observe the entire future price path. Therefore, even if the perturbation occurs in the future, the date-0 consumption policy changes:

$$ c_0(e,a) \quad \Rightarrow \quad c^{r,s}_0(e,a). $$

However, the date-0 distribution is fixed at the steady state, so there is no distribution response: $$ D_0=D_{ss}. $$

Define:

$$ dc^{r,s}_0(e,a) = \frac{c^{r,s}_0(e,a)-c_{ss}(e,a)}{\varepsilon}. \\ dD^{r,s}_0(e,a) = \frac{D^{r,s}_0(e,a)-D_{ss}(e,a)}{\varepsilon} = 0. $$

We have aggregate consumption as

$$ \begin{aligned} C_0 = \sum_{e,a} c_0(e,a)D_0(e,a) \end{aligned} $$

Since the initial distribution is fixed at the steady state,

$$ D_0=D_{ss}, $$

the first-row Jacobian entry is

$$ \begin{aligned} J^{C,r}_{0,s} &= \underbrace{\sum_{e,a} d c^{r,s}_0(e,a)D_{ss}(e,a)}_{\text{direct policy response}} + \underbrace{\sum_{e,a} c_{ss}(e,a) d D^{r,s}_0(e,a)}_{\text{distribution response} = 0} \\ &= \sum_{e,a} dc^{r,s}_0(e,a)D_{ss}(e,a). \end{aligned} $$

The key observation is that, around the steady state, the household problem is time invariant.

$$ (r_0,\ldots,r_{t-1},r_t,\ldots,r_{T-1}), \qquad (w_0,\ldots,w_{t-1},w_t,\ldots,w_{T-1}) $$ where $w_t = w_{ss}$ for any $t$, $r_t = r_{ss}$ for any $t$, except for at date $s$, $r_s = r_{ss} + \varepsilon$.

Therefore, the policy response depends only on the time distance between the current date $t$ and the shock date $s$.

If $s<t$, the shock has already passed, then from date $t$ onward the price path is back at the steady state. Hence the direct policy response is zero:

$$ c_t^{r, s}(e,a)=c_{ss}(e,a), \qquad s<t. $$

If $s\ge t$, then the household problem only depends on the horizon $s-t$ until the shock arrives. $$ c_t^{r, s}(e,a)=c^{r, T-1}_{T-1 -(s-t)}(e,a). $$

Thus:

$$ \begin{aligned} c^{r, s}_t(e,a) &= \begin{cases} c_{ss}(e,a), & s<t, \\ c^{r, T-1}_{T-1 -(s-t)}(e,a), & s\ge t. \end{cases} \\ dc^{r, s}_t(e,a) &= \begin{cases} 0, & s<t, \\ dc^{r, T-1}_{T-1 -(s-t)}(e,a), & s\ge t. \end{cases} \end{aligned} $$

To compute the first row, we only need one backward experiment with the perturbation placed at date $T-1$: $$ d c^{r, s}_0(e,a) = d c^{r, T-1}_{T-1 -s}(e,a), \qquad s=0,\ldots,T-1. $$

Thus, $$ J^{C,r}_{0,s} = \sum_{e,a} dc^{r, T-1}_{T-1 -s}(e,a)D_{ss}(e,a), \qquad s=0,\ldots,T-1. $$

First Column

We know:

$$ J^{C,r}_{t,s} = \underbrace{\sum_{e,a} dc^{r,s}_t(e,a)D_{ss}(e,a)}_{\text{direct policy response}} + \underbrace{\sum_{e,a} c_{ss}(e,a)dD^{r,s}_t(e,a)}_{\text{distribution response}}. $$

Now we consider how to compute the first column: $$ \left( J^{C,r}_{0,0}, J^{C,r}_{1,0}, \ldots, J^{C,r}_{T-1,0} \right), $$

where

$$ J^{C,r}_{\tau,0} = \frac{\partial C_\tau}{\partial r_0}. $$

For $\tau = 0$, we have: $$ d D^{r,0}_0(e,a) = 0, \qquad dc^{r,0}_0(e,a) = dc^{r,T-1}_{T-1}(e,a). $$ which is calculated in the first row. Thus: $$ J^{C,r}_{0,0} = \sum_{e,a} dc^{r,T-1}_{T-1}(e,a)D_{ss}(e,a). $$

For $\tau \ge 1$, we know the shock happened at date $0$, so the direct policy response is zero for all $\tau \ge 1$: $$ c^{r,0}_\tau(e,a) = c_{ss}(e,a), \qquad dc^{r,0}_\tau(e,a) = 0. $$

Then for $\tau \ge 1$, the first column is computed as $$ \begin{aligned} J^{C,r}_{\tau,0} &= \underbrace{\sum_{e,a} dc^{r,0}_\tau(e,a)D_{ss}(e,a)}_{\text{direct policy response} = 0} + \underbrace{\sum_{e,a} c_{ss}(e,a)dD^{r,0}_\tau(e,a)}_{\text{distribution response}} \\ &= \underbrace{\sum_{e,a} c_{ss}(e,a)dD^{r,0}_\tau(e,a)}_{\text{distribution response}} \end{aligned} $$

The key problem is how to compute the distribution response $dD^{r,0}_\tau(e,a)$.

The distribution is pushed forward from the initial steady-state distribution $D_0=D_{ss}$, household have date-0 saving policy $a^{r,0}_0(e,a)$: $$ D_1^{r,0}(e',a') = \sum_{e,a} \Pi(e'|e) \mathbf{1}\{a' = a^{r,0}_0(e,a)\} D_{ss}(e,a). $$

Then $$ dD^{r,0}_1 = \frac{D_1^{r,0} - D_{ss}}{\varepsilon}. $$

Starting from $D_1^{r,0}$, price path and policy return to the steady state, so the distribution is pushed forward using the steady-state policy $a_{ss}(e,a)$: $$ D_\tau^{r,0}(e',a') = \Lambda_{ss}^{\tau-1}(D_1^{r,0})(e',a'), \qquad d D_\tau^{r,0} = \Lambda_{ss}^{\tau-1} d D_1^{r,0}. \qquad \tau \ge 1 $$

Then $$ J^{C,r}_{\tau,0} = \sum_{e,a} c_{ss}(e,a) (\Lambda_{ss}^{\tau-1} d D_1^{r,0})(e,a), \qquad \tau \ge 1. $$

But we do not actually need to compute the full distribution path. We only need the aggregate consumption response.

Using vector notation, the last expression is

$$ J^{C,r}_{\tau,0} = c_{ss}'\Lambda_{ss}^{\tau-1}dD^{r,0}_1. $$

Instead of pushing the distribution deviation forward, we can move the steady-state transition operator to the consumption object:

$$ c_{ss}'\Lambda_{ss}^{\tau-1}dD^{r,0}_1 = \left((\Lambda_{ss}')^{\tau-1}c_{ss}\right)'dD^{r,0}_1. $$

Define the expectation objects

$$ E^C_0(e,a)=c_{ss}(e,a), $$

and recursively

$$ E^C_{\tau+1}(e,a) = \mathbb E_{ss} \left[ E^C_\tau(e',a') \mid e,a \right]. $$

Equivalently,

$$ E^C_\tau = (\Lambda_{ss}')^\tau c_{ss}. $$

Therefore, for $\tau\ge 1$,

$$ J^{C,r}_{\tau,0} = \sum_{e,a} E^C_{\tau-1}(e,a)dD^{r,0}_1(e,a). $$

The interpretation is that $dD^{r,0}_1(e,a)$ tells us where the date-0 shock moves probability mass at date 1, while $E^C_{\tau-1}(e,a)$ tells us how much consumption that mass is expected to generate $\tau-1$ periods later under the steady-state transition. Thus the distribution propagation is captured by the expectation objects, without explicitly computing every future distribution.

Derive the full Jacobian

We already know how to compute the first row and first column of the Jacobian. The rest of the Jacobian can be computed by combining these two results.

We know: $$ J^{C,r}_{t,s} = \underbrace{\sum_{e,a} dc^{r,s}_t(e,a)D_{ss}(e,a)}_{\text{direct policy response}} + \underbrace{\sum_{e,a} c_{ss}(e,a)dD^{r,s}_t(e,a)}_{\text{distribution response}}. $$

For the first term, we have: $$ d c^{r,s}_t = d c^{r,s-1}_{t-1} \\ \Longrightarrow \sum_{e,a} dc^{r,s}_t(e,a)D_{ss}(e,a) = \sum_{e,a} dc^{r,s-1}_{t-1}(e,a)D_{ss}(e,a) $$

For the second term, we have: $$ d D^{r,s}_t = d D^{r,s-1}_{t-1} + \Lambda_{ss}^{t-1} d D^{r,s}_1 \\ $$

To understand the recursion, compare two experiments.

Experiment A: the interest-rate shock happens at date $s$, and we look at the response at date $t$.

Experiment B: the interest-rate shock happens at date $s-1$, and we look at the response at date $t-1$.

From date $1$ onward, Experiment A looks like Experiment B: the shock is one period closer, and the outcome date is also one period closer. This gives the shifted policy-response term,

$$ dc^{r,s}_t(e,a) = dc^{r,s-1}_{t-1}(e,a). $$

However, the distribution is not simply shifted. In Experiment A, households already know at date $0$ that the future shock at date $s$ will occur. Therefore, the date-0 saving policy changes, which moves the date-1 distribution:

$$ D^{r,s}_1 = D_{ss} + dD^{r,s}_1. $$

In Experiment B, the initial distribution is still

$$ D^{r,s-1}_0 = D_{ss}. $$

Therefore, from date $1$ onward, Experiment A has the same future perturbation structure as Experiment B, but it starts from a different distribution.

The distribution response in Experiment A can be decomposed into two parts. First, if the date-1 distribution were still $D_{ss}$, then the response from date $1$ to date $t$ would simply be the shifted response from Experiment B:

$$ dD^{r,s-1}_{t-1}. $$

Second, Experiment A has an extra date-1 distribution deviation created by date-0 news:

$$ dD^{r,s}_1. $$

To first order, this extra distribution deviation is propagated forward by the steady-state transition operator:

$$ \Lambda_{ss}^{t-1}dD^{r,s}_1. $$

Hence,

$$ dD^{r,s}_t = dD^{r,s-1}_{t-1} + \Lambda_{ss}^{t-1}dD^{r,s}_1. $$

We have: $$ \begin{aligned} J^{C,r}_{t,s} &= \underbrace{\sum_{e,a} dc^{r,s}_t(e,a)D_{ss}(e,a)}_{\text{direct policy response}} + \underbrace{\sum_{e,a} c_{ss}(e,a)dD^{r,s}_t(e,a)}_{\text{distribution response}} \\ &= \underbrace{ \sum_{e,a} dc^{r,s-1}_{t-1}(e,a)D_{ss}(e,a) + \sum_{e,a} c_{ss}(e,a)dD^{r,s-1}_{t-1}(e,a) }_{J^{C,r}_{t-1,s-1}} + \underbrace{ \sum_{e,a} c_{ss}(e,a) \left(\Lambda_{ss}^{t-1}dD^{r,s}_1\right)(e,a) }_{F^{C,r}_{t-1,s-1}} \\ &= J^{C,r}_{t-1,s-1} + F^{C,r}_{t-1,s-1}. \end{aligned} $$

Following the PDF's shifted indexing convention, $F^{C,r}_{t-1,s-1}$ is the correction used when recovering $J^{C,r}_{t,s}$ from $J^{C,r}_{t-1,s-1}$. We compute it using the expectation objects defined above: $$ F^{C,r}_{t-1,s-1} = \sum_{e,a} E^C_{t-1}(e,a)dD^{r,s}_1(e,a). $$

We know

$$ J^{C,r}_{t,s} = \begin{cases} J^{C,r}_{t,0}, & s=0, \\ J^{C,r}_{0,s}, & t=0, \\ J^{C,r}_{t-1,s-1}+F^{C,r}_{t-1,s-1}, & t\ge 1,\ s\ge 1. \end{cases} $$

Equivalently, the Jacobian has the structure

$$ J^{C,r} = \begin{bmatrix} J_{0,0} & J_{0,1} & J_{0,2} & \cdots \\ J_{1,0} & J_{0,0}+F_{0,0} & J_{0,1}+F_{0,1} & \cdots \\ J_{2,0} & J_{1,0}+F_{1,0} & J_{1,1}+F_{1,1} & \cdots \\ \vdots & \vdots & \vdots & \ddots \end{bmatrix}. $$

Thus, after computing the first row, the first column, and the fake-news matrix, every interior element of the Jacobian is obtained by moving one step diagonally up-left and adding the corresponding fake-news correction.

10.3) Julia Implementation

The code below implements the fake news algorithm for the household partial-equilibrium Jacobians. We reuse the firm-block and naive household Jacobian helpers from Section 5, then compose the household and firm blocks to recover the same general-equilibrium objects.

function compose_ks_ge_jacobians(hh, ss, params; T=80, h=1e-5)
    firm_J = firm_jacobians(ss, params; T=T, h=h)

    H_K = hh.A_r * firm_J.r_K + hh.A_w * firm_J.w_K - Matrix{Float64}(I, T, T)
    H_Z = hh.A_r * firm_J.r_Z + hh.A_w * firm_J.w_Z
    G_KZ = -H_K \ H_Z

    G_rZ = firm_J.r_K * G_KZ + firm_J.r_Z
    G_wZ = firm_J.w_K * G_KZ + firm_J.w_Z
    G_YZ = firm_J.Y_K * G_KZ + firm_J.Y_Z
    G_AZ = hh.A_r * G_rZ + hh.A_w * G_wZ
    G_CZ = hh.C_r * G_rZ + hh.C_w * G_wZ

    return (; hh, firm_J, H_K, H_Z, G_KZ, G_rZ, G_wZ, G_YZ, G_AZ, G_CZ)
end

function ks_naive_ge_jacobians(ss, params; T=80, h=1e-5)
    hh = household_pe_jacobians(ss, params; T=T, h=h)
    return compose_ks_ge_jacobians(hh, ss, params; T=T, h=h)
end

function household_fake_news_jacobians(ss, params; T=80, h=1e-5)
    r_ss_path = fill(ss.r, T)
    w_ss_path = fill(ss.w, T)
    D1_ss = forward_distribution(ss.D, ss.a_policy, ss.a_grid, ss.Pi)

    function jacobians_for_price(price::Symbol)
        r_path = copy(r_ss_path)
        w_path = copy(w_ss_path)

        if price == :r
            r_path[end] += h
        elseif price == :w
            w_path[end] += h
        else
            error("price must be :r or :w")
        end

        policies = backward_policy_path(r_path, w_path, ss, params)

        first_row_A = zeros(T)
        first_row_C = zeros(T)
        dD1 = Vector{Matrix{Float64}}(undef, T)

        # Horizon s=0 means the shock is today. In the terminal-shock experiment,
        # this corresponds to the last backward policy, index T.
        for s in 0:T-1
            policy_index = T - s
            da = (policies.a_policies[policy_index] .- ss.a_policy) ./ h
            dc = (policies.c_policies[policy_index] .- ss.c_policy) ./ h

            first_row_A[s + 1] = sum(da .* ss.D)
            first_row_C[s + 1] = sum(dc .* ss.D)

            D1_perturbed = forward_distribution(ss.D, policies.a_policies[policy_index], ss.a_grid, ss.Pi)
            dD1[s + 1] = (D1_perturbed .- D1_ss) ./ h
        end

        # E_A[t] and E_C[t] store E^A_{t-1} and E^C_{t-1} in 1-based indexing.
        E_A = Vector{Matrix{Float64}}(undef, T)
        E_C = Vector{Matrix{Float64}}(undef, T)
        E_A[1] = copy(ss.a_policy)
        E_C[1] = copy(ss.c_policy)
        for t in 2:T
            E_A[t] = expectation_step(E_A[t - 1], ss.a_policy, ss.a_grid, ss.Pi)
            E_C[t] = expectation_step(E_C[t - 1], ss.a_policy, ss.a_grid, ss.Pi)
        end

        first_col_A = zeros(T)
        first_col_C = zeros(T)
        first_col_A[1] = first_row_A[1]
        first_col_C[1] = first_row_C[1]
        for t in 2:T
            first_col_A[t] = sum(E_A[t - 1] .* dD1[1])
            first_col_C[t] = sum(E_C[t - 1] .* dD1[1])
        end

        # PDF shifted indexing: F[t-1,s-1] is used to recover J[t,s].
        # Therefore column s-1 of F uses the one-step distribution response dD1^s.
        F_A = zeros(T, T)
        F_C = zeros(T, T)
        for t in 1:T-1, s_shifted in 1:T-1
            F_A[t, s_shifted] = sum(E_A[t] .* dD1[s_shifted + 1])
            F_C[t, s_shifted] = sum(E_C[t] .* dD1[s_shifted + 1])
        end

        J_A = recover_fake_news_jacobian(first_row_A, first_col_A, F_A)
        J_C = recover_fake_news_jacobian(first_row_C, first_col_C, F_C)

        return J_A, J_C
    end

    A_r, C_r = jacobians_for_price(:r)
    A_w, C_w = jacobians_for_price(:w)

    return (; A_r, C_r, A_w, C_w)
end

function ks_fake_news_ge_jacobians(ss, params; T=80, h=1e-5)
    hh = household_fake_news_jacobians(ss, params; T=T, h=h)
    return compose_ks_ge_jacobians(hh, ss, params; T=T, h=h)
end
ks_fake_news_ge_jacobians (generic function with 1 method)
# Warm up Julia compilation on a smaller horizon before timing.
T_warm = 200
_ = ks_naive_ge_jacobians(ss, params; T=T_warm, h=1e-5)
_ = ks_fake_news_ge_jacobians(ss, params; T=T_warm, h=1e-5)

naive_time = @elapsed begin
    naive = ks_naive_ge_jacobians(ss, params; T=T, h=1e-5)
end

fake_news_time = @elapsed begin
    fn = ks_fake_news_ge_jacobians(ss, params; T=T, h=1e-5)
end

println("naive block-level time  = ", round(naive_time, digits=3), " seconds")
println("fake news time          = ", round(fake_news_time, digits=3), " seconds")
println("speedup                 = ", round(naive_time / fake_news_time, digits=2), "x")
println()
println("max |H_K_fn - H_K_naive|   = ", maximum(abs.(fn.H_K .- naive.H_K)))
println("max |H_Z_fn - H_Z_naive|   = ", maximum(abs.(fn.H_Z .- naive.H_Z)))
println("max |G_KZ_fn - G_KZ_naive| = ", maximum(abs.(fn.G_KZ .- naive.G_KZ)))
naive block-level time  = 5.66 seconds
fake news time          = 0.532 seconds
speedup                 = 10.64x

max |H_K_fn - H_K_naive|   = 5.43769378683473e-5
max |H_Z_fn - H_Z_naive|   = 0.0007402157543558996
max |G_KZ_fn - G_KZ_naive| = 0.0001259378745397735