Agents face both idiosyncratic and aggregate risk.
Markets are incomplete:
The only asset available is physical capital.
Negative capital positions are ruled out by a borrowing constraint.
For comparison:
Bewley/Huggett models:
No aggregate risk.
Only asset is a one-period bond in zero net supply.
Aiyagari model:
No aggregate risk.
Only asset is capital.
Aggregate stock of capital always remains at steady-state.
Key challenge:
How do we solve a model featuring both idiosyncratic and aggregate risk, where the distribution of wealth is itself an endogenous state variable?
Labor income: each employed household supplies \(\tilde{l}\) efficiency units of labor, so income is \(w_t \tilde{l}\varepsilon_{jt}\) with \(\varepsilon_{jt} \in \{0,1\}\).
If \(\min \{ w_t \tilde{l}\varepsilon_{jt} \} = 0\), then \(\underline{k} = 0\) is a natural borrowing limit: there exists a path along which an indebted household receives zero labor income and therefore can never repay any positive amount of debt.
Correlated transitions: the employment transition probabilities depend on the aggregate state transition. Let \(u_g\) and \(u_b\) denote the unemployment rates in good and bad times.
Joint transition matrix: rows correspond to current states \[(z,\varepsilon) \in \{(g,1),(g,0),(b,1),(b,0)\},\] and columns correspond to next-period states \((z',\varepsilon')\) in the same order.
How is the joint matrix constructed?
Each row must sum to one.
Marginalizing over employment must recover the aggregate transition: \[
\sum_{\varepsilon' \in \{0,1\}} \Pr(z',\varepsilon' \mid z,\varepsilon) = \Pr(z' \mid z) = \pi_{zz'}.
\]
The stationary employment shares within each aggregate state must match the target unemployment rates: \[
\Pr(\varepsilon = 0 \mid z = g) = u_g, \qquad \Pr(\varepsilon = 0 \mid z = b) = u_b.
\]
In applications, the remaining free parameters are chosen to match job-finding and job-separation durations, so bad times feature both higher unemployment and more persistent non-employment.
Aggregate employment: depends only on the aggregate state:
The aggregate state is \(s_t = (z_t, \mu_t)\), where \(\mu_t\) is the distribution of households over \((k, \varepsilon)\) and aggregate capital is \[
K_t = \int k \, d\mu_t(k,\varepsilon).
\]
Timing And Market Clearing
Within period \(t\):
The aggregate state \((z_t,\mu_t)\) is observed.
Firms rent capital \(K_t\) and labor \(L_t\), and competitive prices \((r_t,w_t)\) are determined.
Households receive income, consume \(c_t\), and choose savings \(k_{t+1}\).
Next period shocks are realized, inducing the new distribution \(\mu_{t+1}\).
Capital market clearing:\[
K_t = \int k \, d\mu_t(k,\varepsilon).
\]
Law of motion for distribution: For every realized aggregate transition \(z \to z'\) and every measurable set \(\Delta_k\), \[
H(\mu, z, z')(\varepsilon', \Delta_k) = \sum_{\varepsilon \in \{0,1\}} \int 1\{k'(k, \varepsilon; z, \mu) \in \Delta_k\} \Pr(\varepsilon' \mid \varepsilon, z, z') \, d\mu(k, \varepsilon)
\]
Computation
Computational Difficulty
The endogenous wealth distribution, \(\mu\), is a state variable.
Why? Since consumers have different propensities to save, we need to know the distribution to forecast next period’s capital stock \(K'\). This, in turn, determines the return on capital \(r'\). Hence, \(\mu\) is part of the value function.
The problem is in fact more severe:
To forecast \(r''\) (and further into the future), we also need to forecast \(K''\), since consumption/savings decisions are forward looking. Consequently, \(\mu'\) (the next period distribution) is part of the value function in the next period.
The challenge:
\(\mu\) is an infinite-dimensional object (think: moment generating function).
Furthermore, the transition equation \(H(\cdot)\) (which governs the evolution of \(\mu\)) is also infinite dimensional.
From Exact To Approximate Aggregation
Exact recursive problem: households should solve \[
v(k,\varepsilon; z,\mu),
\] because future prices depend on future aggregates, and future aggregates depend on the entire distribution \(\mu\).
Krusell-Smith idea: replace the true infinite-dimensional forecasting problem with a perceived law of motion for aggregate capital: \[
K' = G(K,z).
\]
If this perceived law is accurate enough, households can solve the much simpler problem \[
v(k,\varepsilon; z,K)
\] instead of carrying the whole distribution as a state variable.
The approximation is not exact. It is a numerical claim that, for forecasting \(K'\), the additional information in \(\mu\) beyond current \(K\) is small.
Approximate Aggregation KS Solution
In an approximate equilibrium, agents will use a forecasting rule for aggregate capital next period:
\[
\begin{aligned}
&z = z^g: \quad \log K' = a_0 + a_1 \log K \\[1em]
&z = z^b: \quad \log K' = b_0 + b_1 \log K
\end{aligned}
\]
This rule is recursive, so it also helps forecast \(K''\), \(K'''\), etc.
Interpretation: Either as bounded rationality, or as the best linear forecast for capital next period.
\[
\begin{aligned}
&c + k' = \left[1 + r(z, K) - \delta\right]k + w(z, K)\tilde{l}\varepsilon \\[1em]
&\log K' =
\begin{cases}
a_0 + a_1 \log K & \text{if } z = z^g \\
b_0 + b_1 \log K & \text{if } z = z^b \\
\end{cases} \\[1em]
&k' \geq 0
\end{aligned}
\]
Idea:
Find coefficients \(\{a_0, a_1, b_0, b_1\}\) as a fixed point to this problem:
Start with initial guesses for the coefficients.
Solve the value function with these guesses.
Update the guesses based on the solution.
Repeat until convergence.
Why This Reduction Can Work
The wealth distribution matters for individual savings decisions.
KS makes a narrower claim: for forecasting aggregate capital one period ahead, most of the relevant information in \(\mu\) is often summarized by current aggregate capital \(K\) and the aggregate shock \(z\).
So the hard mapping \[
\mu_t \mapsto K_{t+1}
\] is approximated by the simpler mapping \[
(K_t,z_t) \mapsto K_{t+1}.
\]
This is why the forecasting regressions and their errors are central to the method.
Policy Function Iteration
Start with an initial guess:\(k'(k, \varepsilon; z, K)\)
Compute next period’s capital for all possible future states: For each possible realization of \((\varepsilon', z')\), calculate \(k'' = k'(k', \varepsilon'; z', K')\)
We begin with a few imports and two tiny utility functions.
import timeimport jaximport jax.numpy as jnpimport numpy as npimport matplotlib.pyplot as pltfrom jax import laxfrom typing import NamedTuplejax.config.update("jax_enable_x64", True)def print_summary(title, rows):print(title)print("-"*len(title))for label, value in rows:print(f"{label:<24}{value}")def block_tree(tree):"""Force all JAX arrays in a pytree to finish computation."""for leaf in jax.tree_util.tree_leaves(tree):ifhasattr(leaf, "block_until_ready"): leaf.block_until_ready()return treedef time_call(fn, *args, warmup=False, **kwargs):"""Time a call, optionally using one untimed warmup pass first."""if warmup: block_tree(fn(*args, **kwargs)) start = time.perf_counter() out = fn(*args, **kwargs) block_tree(out) elapsed = time.perf_counter() - startreturn out, elapsed
Warning
As in the RBC lecture, we enable 64-bit precision because iterative dynamic-programming problems can become numerically fragile under JAX’s default 32-bit mode.
A Small Amount Of Structure
Compared with standard incomplete markets models, this model has more moving parts. We still want the code to read line by line, but it helps to bundle related objects into NamedTuples so that JAX can pass them around cleanly.
Model Setup Summary
-------------------
Steady-state capital 37.9893
Benefit rate 0.00
Simulation length 1100
Number of agents 10000
Build The Grids
We use a dense grid near the borrowing constraint for individual capital, because that is where the savings policy is most curved. Aggregate capital moves over a much narrower range, so a short uniform grid is enough.
We now generate one aggregate history and one panel of idiosyncratic employment histories. We will reuse these same shocks inside the outer KS loop so that the forecasting rule is updated against a fixed simulation environment.
@jax.jit(static_argnums=(1,))def simulate_agshock(key, T, shocks):"""Simulate a T-period aggregate state sequence, coded as 0=bad and 1=good."""# Step function: generate next state from previous using transition probabilitiesdef step(carry, _): key, current_state = carry key, subkey = jax.random.split(key)# Draw next aggregate state (0=bad, 1=good) next_state = jnp.where( jax.random.uniform(subkey) < shocks.P_agg[current_state, 0], jnp.int32(0), jnp.int32(1), )return (key, next_state), next_state# Start in "bad" state (0), run T periods (key, _), agshock = lax.scan(step, (key, jnp.int32(0)), None, length=T)return key, agshock@jax.jit(static_argnums=(2,))def simulate_idshock(key, agshock, N, shocks, params):"""Simulate an employment panel of shape (T, N), coded as 0=unemp and 1=emp."""# Initial employment status at t=0, random by cross-sectional unemployment rate key, subkey = jax.random.split(key) id0 = (jax.random.uniform(subkey, (N,)) > params.ur_vals[0]).astype(jnp.int32)# Step function: generate next period's employment state for each agent # using conditional probabilities given agg state today & tomorrowdef step(carry, xs): key, current_id = carry agg_today, agg_tomorrow = xs key, subkey = jax.random.split(key) draws = jax.random.uniform(subkey, (N,)) p_unemp = shocks.P_cond_unemp[agg_today, agg_tomorrow][current_id] # Pr(unemp' | agg, id)# 0=unemp, 1=emp next_id = jnp.where(draws < p_unemp, jnp.int32(0), jnp.int32(1))return (key, next_id), next_id# Simulate over all periods, using realized aggregate shocks (_, _), id_rest = lax.scan(step, (key, id0), (agshock[:-1], agshock[1:]))# Concatenate initial and subsequent periodsreturn jnp.concatenate([id0[None], id_rest], axis=0)
prices as functions of aggregate capital and the aggregate state
labour income as a function of wages and employment
current resources at each grid point
@jax.jitdef prices_from_capital(K, agg_state, params):"""Return the rental rate and wage for aggregate capital K in state agg_state.""" z = params.z_vals[agg_state] employment_rate = params.er_vals[agg_state] capital_per_eff_labor = K / (employment_rate * params.l_bar) r = params.alpha * z * capital_per_eff_labor ** (params.alpha -1.0) w = (1.0- params.alpha) * z * capital_per_eff_labor ** params.alphareturn r, w@jax.jitdef net_labor_income(wage, agg_state, emp_state, params):"""Labour income net of unemployment insurance transfers and taxes.""" ur = params.ur_vals[agg_state] er = params.er_vals[agg_state] employed_income = wage * params.l_bar - params.benefit_rate * wage * (ur / er) unemployed_income = params.benefit_rate * wagereturn jnp.where(emp_state ==1, employed_income, unemployed_income)
We now build the current-period resource tensor. Its shape is the same as the policy tensor.
def build_household_objects(grids, params):"""Precompute labour income and total resources on the full state grid.""" k_mesh = grids.k_grid[:, None, None, None] km_mesh = grids.km_grid[None, :, None, None] agg_mesh = jnp.arange(params.z_vals.shape[0], dtype=jnp.int32)[None, None, :, None] emp_mesh = jnp.arange(2, dtype=jnp.int32)[None, None, None, :] r, w = prices_from_capital(km_mesh, agg_mesh, params) labor_income = net_labor_income(w, agg_mesh, emp_mesh, params) resources = (1.0+ r - params.delta) * k_mesh + labor_incomereturn HouseholdObjects( labor_income=labor_income, resources=resources, )household = build_household_objects(grids, params)print_summary("Household Objects", [ ("labor_income shape", f"{household.labor_income.shape}"), ("resources shape", f"{household.resources.shape}"), ("resource range",f"[{household.resources.min():.4f}, {household.resources.max():.4f}]", ), ],)
When we update the Euler equation, today’s policy choice \(k'\) usually lies between grid points. So we need to interpolate the future policy function at off-grid points.
@jax.jitdef interp_2d_policy(values, kq, kmq, grids):""" Perform bilinear interpolation over (k, K) grid to estimate the policy value at arbitrary query points. Args: values: 2D array of policy function, shaped (k_grid, km_grid). kq: array-like, query values for individual capital (k). kmq: array-like, query values for aggregate capital (K). grids: object containing discretized k_grid and km_grid. Returns: Interpolated values at (kq, kmq) locations. """# Clip queries so they're within the grid bounds kq = jnp.clip(kq, grids.k_grid[0], grids.k_grid[-1]) kmq = jnp.clip(kmq, grids.km_grid[0], grids.km_grid[-1]) nk = grids.k_grid.shape[0] # number of individual capital grid points nK = grids.km_grid.shape[0] # number of aggregate capital grid points# Find the upper and lower indices for interpolation in k_grid ik_hi = jnp.clip(jnp.searchsorted(grids.k_grid, kq, side="right"), 1, nk -1) ik_lo = ik_hi -1# Find the upper and lower indices for interpolation in km_grid iK_hi = jnp.clip(jnp.searchsorted(grids.km_grid, kmq, side="right"), 1, nK -1) iK_lo = iK_hi -1# Gather grid values at those indices k_lo = grids.k_grid[ik_lo] k_hi = grids.k_grid[ik_hi] K_lo = grids.km_grid[iK_lo] K_hi = grids.km_grid[iK_hi]# Compute normalized distances between grid points (weights) wk = (kq - k_lo) / (k_hi - k_lo) wK = (kmq - K_lo) / (K_hi - K_lo)# Obtain function values at the four corners f00 = values[ik_lo, iK_lo] # bottom-left f10 = values[ik_hi, iK_lo] # bottom-right f01 = values[ik_lo, iK_hi] # top-left f11 = values[ik_hi, iK_hi] # top-right# Compute weighted sum for bilinear interpolationreturn ( (1.0- wk) * (1.0- wK) * f00+ wk * (1.0- wK) * f10+ (1.0- wk) * wK * f01+ wk * wK * f11 )
Step 3: Forecast Next Period Aggregate Capital
The KS approximation replaces the full law of motion of the distribution by a simple forecasting rule:
\[
\log K' = B_{0,z} + B_{1,z} \log K.
\]
We store the coefficients in \[
B =
\begin{bmatrix}
b_0 & b_1 \\
a_0 & a_1
\end{bmatrix},
\] where row 0 is the bad state and row 1 is the good state.
This is the core numerical step. Conceptually we do four things:
take a current guess for \(k'\)
use it to evaluate future savings \(k''\)
compute expected marginal utility tomorrow
back out current consumption and current savings
The only difficult part is bookkeeping over future states, so we make that bookkeeping explicit.
@jax.jitdef reshape_policy_by_future_state(policy):"""Reorder policy to shape (future_state, k, K).""" nk, nK = policy.shape[:2]return policy.transpose(2, 3, 0, 1).reshape(-1, nk, nK)@jax.jitdef reshape_joint_probabilities(shocks):"""Reorder P(z', e' | z, e) to shape (future_state, z, e)."""return shocks.P_joint.reshape(2, 2, -1).transpose(2, 0, 1)
For one specific future state \((z',\varepsilon')\), we can compute the Euler-equation contribution state by state.
@jax.jitdef marginal_value_in_future_state( future_index, policy, predicted_K_next, future_policy_slices, params, grids, solver_config,):"""Return the Euler RHS contribution for one future state (z', eps')."""# Get aggregate (z') and employment (eps') state indices for this future state next_agg = solver_config.future_states[future_index, 0] next_emp = solver_config.future_states[future_index, 1]# Interpolate future savings policy k'' at this future state kpp = interp_2d_policy( future_policy_slices[future_index], policy, predicted_K_next, grids, )# Compute next period interest rate and wage given K' r_next, w_next = prices_from_capital(predicted_K_next, next_agg, params)# Compute labor income for next period labor_income_next = net_labor_income(w_next, next_agg, next_emp, params)# Compute next period consumption using budget constraint c_next = jnp.maximum( (1.0+ r_next - params.delta) * policy + labor_income_next - kpp,1e-12, # avoid negative or zero consumption )# Compute and return discounted marginal utility term, as Euler equation RHSreturn (1.0+ r_next - params.delta) * c_next ** (-params.sigma)
Now we aggregate over the four future states with the correct transition probabilities.
@jax.jitdef expected_euler_rhs( policy, B, params, grids, shocks, solver_config,):""" Compute the expected RHS of the Euler equation at each current state. Returns E[(1 + r' - delta) * u_c(c')]. - policy: current savings policy. - B: aggregate law coefficients. - params: model parameters. - grids: state grids. - shocks: idiosyncratic and aggregate shock structure. - solver_config: solver settings and state info. """# Forecast next-period aggregate capital from law of motion K_next = forecast_next_aggregate_capital(B, grids)# Broadcast K_next for shape compatibility with policy grid predicted_K_next = jnp.broadcast_to(K_next[None, :, :, None], policy.shape)# Reorganize policy by future states (z', eps') for vectorized calls future_policy_slices = reshape_policy_by_future_state(policy)# Reshape transition probabilities by future state future_probabilities = reshape_joint_probabilities(shocks)# Compute the RHS for each possible future state rhs_terms = jax.vmap(lambda future_index: marginal_value_in_future_state( future_index, policy, predicted_K_next, future_policy_slices, params, grids, solver_config, ) )(jnp.arange(solver_config.future_states.shape[0]))# Weighted sum over future states using joint transition probabilitiesreturn jnp.sum(rhs_terms * future_probabilities[:, None, None, :, :], axis=0)
Finally, the Euler equation gives current consumption, and the budget constraint gives updated savings.
Individual Problem Summary
--------------------------
Elapsed time 0.2347 s
Iterations 1887
Final sup norm 9.979e-09
Policy shape (200, 4, 2, 2)
Consumption range [0.0000, 20.3613]
Once we have a policy, the next KS step is mechanical: simulate many households, average their capital, and recover the implied aggregate capital path.
@jax.jitdef simulate_aggregate_path(policy, agshock, idshock, kcross0, grids):""" Simulate the evolution of aggregate capital over time given a household policy and realized shocks. Args: policy: Household policy function (capital choice). agshock: Sequence of aggregate shocks (indices). idshock: Sequence of idiosyncratic employment status shocks (0=unemployed, 1=employed). kcross0: Initial capital levels for all households. grids: Grid information for interpolation and bounds. Returns: kmts: Sequence of aggregate capital (mean) over time (truncated to grid min/max), shape (T,). kcross_T: Final cross-sectional capital levels, shape (N,). """def step(kcross, shocks_t):# Unpack current shocks for this period: agg_t (aggregate state), id_t (idiosyncratic employment) agg_t, id_t = shocks_t# Compute aggregate capital, clipped to allowed range km_t = jnp.clip(jnp.mean(kcross), grids.km_min, grids.km_max)# Interpolate next-period capital according to individual status (u: unemployed, e: employed) kprime_u = interp_2d_policy(policy[:, :, agg_t, 0], kcross, km_t, grids) kprime_e = interp_2d_policy(policy[:, :, agg_t, 1], kcross, km_t, grids)# Select next-period capital based on employment status kcross_next = jnp.where(id_t ==0, kprime_u, kprime_e)# Apply bounds to capital choices kcross_next = jnp.clip(kcross_next, grids.k_min, grids.k_max)return kcross_next, km_t# Run the simulation forward in time for each period's shocks kcross_T, kmts = lax.scan(step, kcross0, (agshock, idshock))# Return path of mean capital and final distribution of capitalreturn kmts, kcross_T
Regression Update Summary
-------------------------
B (bad state) [0.44174589 0.87942783]
B (good state) [0.45837376 0.87654118]
R^2 bad 0.999722
R^2 good 0.999830
Obs. bad 506
Obs. good 493
One Complete KS Update
At this point the full logic is easy to read: solve, simulate, regress.
def ks_update_step( B, policy_init, kcross0, agshock, idshock, params, grids, shocks, household, simulation, solver_config,):""" Perform one step of the KS (Krusell–Smith) outer update. Solves the individual household problem given guess B, simulates the aggregate path, then runs the forecasting regression update, returning all key objects. Returns: policy: updated individual policy function c_policy: updated consumption policy function kmts: simulated aggregate capital path kcross_T: simulated individual-level capital distribution (final) B_hat: updated forecasting coefficients (as jnp array) regression: full regression result object """# Solve individual optimization given current forecasting rule coefficients B policy, c_policy, _, _ = solve_individual_problem( B, policy_init, params, grids, shocks, household, solver_config )# Simulate the capital path and individual cross-sectional distribution kmts, kcross_T = simulate_aggregate_path(policy, agshock, idshock, kcross0, grids)# Update forecasting rule via regression on simulated data regression = update_forecasting_rule(kmts, agshock, simulation.ndiscard)# Return all relevant objects for outer update loopreturn ( policy, # new individual policy function c_policy, # new consumption policy function kmts, # simulated aggregate capital over time kcross_T, # cross-sectional capital distribution at end jnp.asarray(regression.B_hat), # updated B coefficients (as JAX array) regression, # full regression object (with R^2, n, etc.) )
In the KS algorithm, the forecasting coefficients \(B\) usually change only gradually across outer iterations.
Because of that, the converged savings policy from iteration \(n\) is already a very good initial guess for iteration \(n+1\).
So instead of restarting every inner solve from the same crude policy_init, we warm start from the previous converged policy.
This does not change the fixed point we are targeting. It only makes the inner policy iteration start much closer to its new solution.
Warm Start In The Outer Loop
The implementation is simple:
set current_policy = policy_init before the loop
solve the household problem using current_policy
after the solve, update current_policy = policy
use this updated policy as the initial guess in the next outer iteration
In practice, this substantially reduces the number of inner policy iterations in later KS rounds, so it is a pure algorithmic speedup with no change in the target solution.
We now define whole soltion function for the KS model.
def solve_ks_model( B_init, policy_init, kcross0, agshock, idshock, params, grids, shocks, household, simulation, solver_config, outer_config,):""" Solves the KS (Krusell–Smith) model using an outer fixed-point iteration on the forecasting rule coefficients. The routine updates the perceived law of motion until convergence. """ B = np.asarray(B_init) # Initial perceived law of motion coefficients current_kcross = jnp.asarray( kcross0 ) # Initial cross-sectional capital distribution current_policy = jnp.asarray(policy_init) # Initial policy guess history = [] # To store convergence historyfor it inrange( outer_config.max_iter_B ): # Main outer loop over forecasting rule updates# Solve the individual (household) dynamic programming problem policy, c_policy, _, _ = solve_individual_problem( jnp.asarray(B), current_policy, params, grids, shocks, household, solver_config, )# Simulate the aggregate capital path and update cross-sectional distribution kmts, current_kcross = simulate_aggregate_path( policy, agshock, idshock, current_kcross, grids )# Fit/update the forecasting rule via regression regression = update_forecasting_rule(kmts, agshock, simulation.ndiscard) B_hat = regression.B_hat # Updated law of motion coefficients diff_B =float(np.linalg.norm(B_hat - B)) # Difference in coefficients history.append(dict( iteration=it +1, diff_B=diff_B, R2_bad=regression.R2_bad, R2_good=regression.R2_good, ) )# Use converged policy for next iteration (warm start) current_policy = policy# Relaxation update of coefficient vector B = outer_config.update_B * B_hat + (1.0- outer_config.update_B) * Bif diff_B <= outer_config.tol_B: # Check for convergencebreakreturndict( B=jnp.asarray(B), # Converged coefficients (JAX array) policy=policy, # Final policy function consumption=c_policy, # Corresponding consumption policy kmts=kmts, # Simulated aggregate capital path kcross=current_kcross, # Final cross-sectional capital distribution history=history, # Iteration history: convergence stats regression=regression, # Final regression object )
Full KS Solve Summary
---------------------
Elapsed time 7.3077 s
Outer iterations 28
Final diff_B 6.390e-07
R^2 bad 0.999927
R^2 good 0.999964
B (bad state) [0.14500784 0.96016969]
B (good state) [0.15565201 0.95866002]
Speedup: 3.46x
The original Maliar, Maliar, and Valli (2010) MATLAB code took 25.3 seconds to solve the model, so the JAX implementation is much faster.
Visualize the Convergence
plt.figure(figsize=(8, 4))plt.semilogy( [row["iteration"] for row in ks_history], [row["diff_B"] for row in ks_history], marker="o",)plt.title("Outer KS Convergence")plt.xlabel("Iteration")plt.ylabel(r"$||B^{new} - B^{old}||$")plt.tight_layout()plt.show()
Visualize the Convergence
Forecast Accuracy Plot
As a final diagnostic, we compare the aggregate capital path generated by the household policy with the path implied by the converged perceived law of motion itself.
@jax.jitdef simulate_alm_path(B, km0, agshock, grids):"""Simulate the path implied directly by the aggregate law of motion."""def step(km_t, agg_t): log_km_next = B[agg_t, 0] + B[agg_t, 1] * jnp.log(km_t) km_next = jnp.clip(jnp.exp(log_km_next), grids.km_min, grids.km_max)return km_next, km_next _, km_next = lax.scan(step, km0, agshock[:-1])return jnp.concatenate([jnp.array([km0]), km_next])
kmalm_final = simulate_alm_path( ks_solution["B"], ks_solution["kmts"][0], agshock, grids)diff_kmt =100* (kmalm_final[-1] - ks_solution["kmts"][-1]) / ks_solution["kmts"][-1]print(f"Difference in final aggregate capital: {diff_kmt:.4f}%")
Difference in final aggregate capital: -0.0613%
plt.figure(figsize=(10, 4))plt.plot(ks_solution["kmts"], label="Simulated from policy rule")plt.plot(kmalm_final, "--", label="Implied by aggregate law")plt.title("Converged KS Forecast Accuracy")plt.xlabel("Time")plt.ylabel("Aggregate capital")plt.legend()plt.tight_layout()plt.show()
Forecast Accuracy Plot
References
Chipeniuk, Karsten, Nets Hawk Katz, and Todd Walker. 2016. “Approximate Aggregation in the Neoclassical Growth Model with Idiosyncratic Shocks.”
Haan, Wouter J. Den, Kenneth L. Judd, and Michel Juillard. 2010. “Computational Suite of Models with Heterogeneous Agents: Incomplete Markets and Aggregate Uncertainty.”Journal of Economic Dynamics and Control 34 (1): 1–3.
Maliar, Lilia, Serguei Maliar, and Fernando Valli. 2010. “Solving the Incomplete Markets Model with Aggregate Uncertainty Using the Krusell–Smith Algorithm.”Journal of Economic Dynamics and Control 34 (1): 42–49.
Werning, Iván. 2015. “Incomplete Markets and Aggregate Demand.” w. National Bureau of Economic Research.
Young, Eric R. 2010. “Solving the Incomplete Markets Model with Aggregate Uncertainty Using the Krusell–Smith Algorithm and Non-Stochastic Simulations.”Journal of Economic Dynamics and Control 34 (1): 36–41.