diff --git a/lectures/_toc.yml b/lectures/_toc.yml
index 164a4181b..877bf938b 100644
--- a/lectures/_toc.yml
+++ b/lectures/_toc.yml
@@ -107,6 +107,7 @@ parts:
- file: markov_perf
- file: uncertainty_traps
- file: aiyagari
+ - file: aiyagari_egm
- file: ak_aiyagari
- caption: Asset Pricing and Finance
numbered: true
diff --git a/lectures/aiyagari_egm.md b/lectures/aiyagari_egm.md
new file mode 100644
index 000000000..c1e3050eb
--- /dev/null
+++ b/lectures/aiyagari_egm.md
@@ -0,0 +1,965 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 0.13
+ jupytext_version: 1.17.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
+(aiyagari_egm)=
+```{raw} jupyter
+
+```
+
+# The Aiyagari Model with Endogenous Grid Method
+
+```{contents} Contents
+:depth: 2
+```
+
+In addition to what's included in base Anaconda, we need to install QuantEcon's Python library and JAX.
+
+```{code-cell} ipython3
+:tags: [hide-output]
+
+!pip install quantecon jax
+```
+
+## Overview
+
+This lecture combines two important computational methods in macroeconomics:
+
+1. The **Aiyagari model** {cite}`Aiyagari1994` - a heterogeneous agent model with incomplete markets
+2. The **endogenous grid method** (EGM) {cite}`Carroll2006` - an efficient algorithm for solving dynamic programming problems
+
+In the {doc}`standard Aiyagari lecture `, we solved the household problem using discretization and value function iteration.
+
+We then computed aggregate capital at a given set of prices using the stationary distribution of the finite Markov chain.
+
+In this lecture, we take a different approach:
+
+1. We use the **endogenous grid method** to solve the household problem via the Euler equation and linear interpolation.
+2. We compute aggregate capital by **simulation** rather than an algebraic technique (which only works for the finite case).
+
+These modifications make the solution method faster and more flexible, especially when dealing with more complex models.
+
+### References
+
+The primary references for this lecture are:
+
+* our {doc}`previous Aiyagari lecture ` for the key ideas
+* {cite}`Aiyagari1994` for the economic model
+* {cite}`Carroll2006` for the endogenous grid method
+* Chapter 18 of {cite}`Ljungqvist2012` for textbook treatment
+
+
+### Preliminaries
+
+We use the following imports:
+
+```{code-cell} ipython3
+import quantecon as qe
+import matplotlib.pyplot as plt
+import jax
+import jax.numpy as jnp
+from typing import NamedTuple
+from scipy.optimize import bisect
+import numpy as np
+from numba import jit
+```
+
+We will use 64-bit floats with JAX in order to increase precision.
+
+```{code-cell} ipython3
+jax.config.update("jax_enable_x64", True)
+```
+
+## The Economy
+
+The economy consists of households and a representative firm.
+
+### Households
+
+Infinitely lived households face idiosyncratic income shocks and a borrowing constraint.
+
+The savings problem faced by a typical household is
+
+$$
+ \max \mathbb E \sum_{t=0}^{\infty} \beta^t u(c_t)
+$$
+
+subject to
+
+$$
+ a_{t+1} + c_t \leq w z_t + (1 + r) a_t
+ \quad
+ c_t \geq 0,
+ \quad \text{and} \quad
+ a_t \geq -B
+$$
+
+where
+
+* $c_t$ is current consumption
+* $a_t$ is assets
+* $z_t$ is an exogenous component of labor income (stochastic employment status)
+* $w$ is the wage rate
+* $r$ is the interest rate
+* $B$ is the maximum amount that the agent is allowed to borrow
+
+The exogenous process $\{z_t\}$ follows a finite state Markov chain with stochastic matrix $\Pi$.
+
+Optimal interior consumption choices satisfy the Euler equation
+
+$$
+ u'(c) = \beta \mathbb{E_z} [(1 + r) u'(c')]
+$$
+
+(We use $'$ symbols for both derivatives and future values, which is not ideal but convenient and common.)
+
+In terms of assets, this is
+
+$$
+ u'(w z + (1 + r) a - a')
+ = \beta (1 + r) \sum_{z'} u'(w z' + (1 + r) a' - s(a', z')) \Pi(z, z')
+$$
+
+where $s$ is the optimal savings policy function.
+
+
+### Firms
+
+Firms produce output by hiring capital and labor under constant returns to scale.
+
+The representative firm's output is
+
+$$
+Y = A K^{\alpha} N^{1 - \alpha}
+$$
+
+where
+
+* $A$ and $\alpha$ are parameters with $A > 0$ and $\alpha \in (0, 1)$
+* $K$ is aggregate capital
+* $N$ is total labor supply (normalized to 1)
+
+These parameters are stored in the following namedtuple:
+
+```{code-cell} ipython3
+class Firm(NamedTuple):
+ A: float = 1.0 # Total factor productivity
+ N: float = 1.0 # Total labor supply
+ α: float = 0.33 # Capital share
+ δ: float = 0.05 # Depreciation rate
+```
+
+From the firm's first-order condition, the inverse demand for capital is
+
+```{math}
+:label: aiy_egm_rgk
+
+r = A \alpha \left( \frac{N}{K} \right)^{1 - \alpha} - \delta
+```
+
+```{code-cell} ipython3
+def r_given_k(K, firm):
+ """
+ Inverse demand curve for capital.
+ """
+ A, N, α, δ = firm
+ return A * α * (N / K)**(1 - α) - δ
+```
+
+The equilibrium wage rate as a function of $r$ is
+
+```{math}
+:label: aiy_egm_wgr
+
+w(r) = A (1 - \alpha) (A \alpha / (r + \delta))^{\alpha / (1 - \alpha)}
+```
+
+```{code-cell} ipython3
+def r_to_w(r, firm):
+ """
+ Equilibrium wages associated with a given interest rate r.
+ """
+ A, N, α, δ = firm
+ return A * (1 - α) * (A * α / (r + δ))**(α / (1 - α))
+```
+
+### Equilibrium
+
+A **stationary rational expectations equilibrium (SREE)** consists of prices and policies such that:
+
+* Households optimize given prices
+* Firms maximize profits given prices
+* Markets clear: aggregate capital supply equals aggregate capital demand
+* Aggregate quantities are constant over time
+
+## Implementation with EGM
+
+### Household primitives
+
+First we set up the household parameters and grids:
+
+```{code-cell} ipython3
+class Household(NamedTuple):
+ β: float # Discount factor
+ a_grid: jnp.ndarray # Asset grid
+ z_grid: jnp.ndarray # Exogenous states
+ Π: jnp.ndarray # Transition matrix
+
+def create_household(β=0.96, # Discount factor
+ Π=[[0.9, 0.1], [0.1, 0.9]], # Markov chain
+ z_grid=[0.1, 1.0], # Exogenous states
+ a_min=1e-10, a_max=50, # Asset grid
+ a_size=200):
+ """
+ Create a Household namedtuple with custom grids.
+ """
+ a_grid = jnp.linspace(a_min, a_max, a_size)
+ z_grid, Π = map(jnp.array, (z_grid, Π))
+ return Household(β=β, a_grid=a_grid, z_grid=z_grid, Π=Π)
+```
+
+For utility, we assume $u(c) = \log(c)$, which gives us $u'(c) = 1/c$ and $(u')^{-1}(x) = 1/x$.
+
+```{code-cell} ipython3
+@jax.jit
+def u(c):
+ return jnp.log(c)
+
+@jax.jit
+def u_prime(c):
+ return 1 / c
+
+@jax.jit
+def u_prime_inv(x):
+ return 1 / x
+```
+
+Here's a namedtuple for prices:
+
+```{code-cell} ipython3
+class Prices(NamedTuple):
+ r: float = 0.01 # Interest rate
+ w: float = 1.0 # Wages
+```
+
+### The EGM operator
+
+The key insight of EGM is to avoid root-finding by choosing the asset grid exogenously and computing the consumption values directly from the Euler equation.
+
+The Coleman-Reffett operator using EGM works as follows:
+
+1. Start with a consumption policy function $\sigma$ represented on an exogenous grid of assets $\{a_i\}$
+2. For each asset level $a_i$ and employment state $z_j$:
+ - Compute the right-hand side of the Euler equation:
+ $$\text{RHS} = \beta (1 + r) \sum_{z'} \Pi(z_j, z') u'(\sigma(a_i, z'))$$
+ - Use the inverse marginal utility to get current consumption:
+ $$c_{ij} = (u')^{-1}(\text{RHS})$$
+ - Compute the endogenous income level:
+ $$y_{ij} = c_{ij} + a_i$$
+3. Reconstruct the new policy $K\sigma$ on the original asset grid using interpolation
+
+```{code-cell} ipython3
+@jax.jit
+def K_egm(σ, household, prices):
+ """
+ The Coleman-Reffett operator using EGM for the Aiyagari model.
+
+ Parameters
+ ----------
+ σ : array_like(float, ndim=2)
+ Current consumption policy, where σ[i, j] is consumption
+ when assets are a_grid[i] and employment state is z_grid[j]
+ household : Household
+ Household parameters and grids
+ prices : Prices
+ Interest rate and wage
+
+ Returns
+ -------
+ σ_new : array_like(float, ndim=2)
+ Updated consumption policy on the same grid
+ """
+ # Unpack
+ β, a_grid, z_grid, Π = household
+ a_size, z_size = len(a_grid), len(z_grid)
+ r, w = prices
+
+ # Allocate memory for new consumption
+ σ_new = jnp.zeros((a_size, z_size))
+
+ # For each current employment state
+ for j in range(z_size):
+ # Step 1: Use a_grid as exogenous grid for tomorrow's assets (a')
+ # Compute expectation: E[u'(c(a', z')) | z=z_j]
+ Eu_prime = jnp.zeros(a_size)
+ for jp in range(z_size):
+ Eu_prime += Π[j, jp] * u_prime(σ[:, jp])
+
+ # Step 2: Get consumption on endogenous grid using Euler equation
+ c_endo = u_prime_inv(β * (1 + r) * Eu_prime)
+
+ # Step 3: Compute endogenous asset grid for today
+ # From budget constraint: a' = (1+r)a + wz - c
+ # Solving for a: a = (c + a' - wz) / (1+r)
+ a_endo = (c_endo + a_grid - w * z_grid[j]) / (1 + r)
+
+ # Step 4: Interpolate back to exogenous asset grid
+ # Handle borrowing constraint
+ for i, a in enumerate(a_grid):
+ if a < a_endo[0]:
+ # Below minimum of endogenous grid - consume all income
+ σ_new = σ_new.at[i, j].set(w * z_grid[j] + (1 + r) * a - a_grid[0])
+ else:
+ # Interpolate
+ σ_new = σ_new.at[i, j].set(jnp.interp(a, a_endo, c_endo))
+
+ return σ_new
+```
+
+Let's also create a more efficient JIT-compiled version:
+
+```{code-cell} ipython3
+@jax.jit
+def K_egm_jit(σ, household, prices):
+ """
+ Vectorized JIT-compiled version of the EGM operator.
+ """
+ # Unpack
+ β, a_grid, z_grid, Π = household
+ a_size, z_size = len(a_grid), len(z_grid)
+ r, w = prices
+
+ # Compute expectation: E[u'(c(a', z')) | z]
+ # Eu_prime[i, j] = sum_jp Π[j, jp] * u'(σ[i, jp])
+ Eu_prime = (Π @ u_prime(σ).T).T # (a_size, z_size)
+
+ # Apply Euler equation to get consumption on endogenous grid
+ c_endo = u_prime_inv(β * (1 + r) * Eu_prime) # (a_size, z_size)
+
+ # Compute endogenous asset grid: a = (c + a' - wz) / (1+r)
+ # a_endo[i, j] is today's assets when tomorrow's assets are a_grid[i]
+ # and today's employment is z_grid[j]
+ a_endo = (c_endo + a_grid[:, None] - w * z_grid[None, :]) / (1 + r)
+
+ # Interpolate back to exogenous grid
+ # For each employment state j, interpolate from (a_endo[:, j], c_endo[:, j])
+ # to get consumption at exogenous grid points a_grid
+
+ def interpolate_policy(j):
+ # Handle borrowing constraint
+ # If a < min(a_endo), consume everything except minimum savings
+ return jnp.where(
+ a_grid < a_endo[0, j],
+ w * z_grid[j] + (1 + r) * a_grid - a_grid[0],
+ jnp.interp(a_grid, a_endo[:, j], c_endo[:, j])
+ )
+
+ σ_new = jax.vmap(interpolate_policy)(jnp.arange(z_size))
+
+ return σ_new.T # (a_size, z_size)
+```
+
+### Solving the household problem
+
+We solve for the optimal policy by iterating the EGM operator to convergence:
+
+```{code-cell} ipython3
+def solve_household_egm(household, prices,
+ tol=1e-5, max_iter=1000, verbose=False):
+ """
+ Solve the household problem using EGM iteration.
+
+ Returns the optimal consumption policy σ[i,j] where i indexes
+ assets and j indexes employment states.
+ """
+ β, a_grid, z_grid, Π = household
+ a_size, z_size = len(a_grid), len(z_grid)
+
+ # Initial guess: consume a fraction of income
+ r, w = prices
+ a_mesh = a_grid[:, None]
+ z_mesh = z_grid[None, :]
+ income = w * z_mesh + (1 + r) * a_mesh
+ σ = 0.5 * income
+
+ # Iterate until convergence
+ for i in range(max_iter):
+ σ_new = K_egm_jit(σ, household, prices)
+ error = jnp.max(jnp.abs(σ_new - σ))
+ σ = σ_new
+
+ if verbose and i % 50 == 0:
+ print(f"Iteration {i}, error = {error:.6f}")
+
+ if error < tol:
+ if verbose:
+ print(f"Converged in {i} iterations")
+ break
+
+ return σ
+```
+
+Let's test this on an example:
+
+```{code-cell} ipython3
+# Create household and prices
+household = create_household()
+prices = Prices(r=0.01, w=1.0)
+
+print(f"Solving household problem with r={prices.r}, w={prices.w}")
+
+# Solve
+with qe.Timer():
+ σ_star = solve_household_egm(household, prices, verbose=True)
+```
+
+Let's plot the resulting policy functions:
+
+```{code-cell} ipython3
+β, a_grid, z_grid, Π = household
+r, w = prices
+
+# Convert consumption policy to savings policy
+a_mesh = a_grid[:, None]
+z_mesh = z_grid[None, :]
+income = w * z_mesh + (1 + r) * a_mesh
+savings = income - σ_star
+
+fig, axes = plt.subplots(1, 2, figsize=(14, 5))
+
+# Plot consumption policy
+ax = axes[0]
+for j, z in enumerate(z_grid):
+ ax.plot(a_grid, σ_star[:, j], label=f'$z={z:.2f}$', lw=2, alpha=0.7)
+ax.set_xlabel('Assets $a$')
+ax.set_ylabel('Consumption $c$')
+ax.set_title('Consumption Policy')
+ax.legend()
+ax.grid(alpha=0.3)
+
+# Plot savings policy
+ax = axes[1]
+ax.plot(a_grid, a_grid, 'k--', lw=1, alpha=0.5, label='45° line')
+for j, z in enumerate(z_grid):
+ ax.plot(a_grid, savings[:, j], label=f'$z={z:.2f}$', lw=2, alpha=0.7)
+ax.set_xlabel('Current Assets $a$')
+ax.set_ylabel('Next Period Assets $a\'$')
+ax.set_title('Savings Policy')
+ax.legend()
+ax.grid(alpha=0.3)
+
+plt.tight_layout()
+plt.show()
+```
+
+## Computing Aggregate Capital by Simulation
+
+Instead of computing the stationary distribution of the Markov chain analytically, we compute aggregate capital by simulating a large number of households.
+
+This approach:
+* Is more flexible (works with continuous shocks, non-linear policies, etc.)
+* Avoids storing and manipulating large transition matrices
+* Is conceptually simpler
+
+```{code-cell} ipython3
+@jax.jit
+def simulate_households(σ, household, prices,
+ num_households=10_000,
+ num_periods=1000,
+ seed=42):
+ """
+ Simulate a cross-section of households and compute average assets.
+
+ Parameters
+ ----------
+ σ : array_like(float)
+ Consumption policy function
+ household : Household
+ Household parameters
+ prices : Prices
+ Interest rate and wage
+ num_households : int
+ Number of households to simulate
+ num_periods : int
+ Number of periods to simulate (we use the last period)
+ seed : int
+ Random seed
+
+ Returns
+ -------
+ mean_assets : float
+ Average asset holdings across households
+ """
+ β, a_grid, z_grid, Π = household
+ r, w = prices
+ z_size = len(z_grid)
+
+ # Initialize random states
+ key = jax.random.PRNGKey(seed)
+
+ # Initial asset distribution (start everyone at the middle of the grid)
+ a_init_idx = len(a_grid) // 2
+ assets = jnp.ones(num_households) * a_grid[a_init_idx]
+
+ # Initial employment state distribution (draw from stationary dist)
+ # For simplicity, start everyone in state 0
+ z_indices = jnp.zeros(num_households, dtype=int)
+
+ # Simulate forward
+ for t in range(num_periods):
+ # Generate random shocks for employment transitions
+ key, subkey = jax.random.split(key)
+ unif = jax.random.uniform(subkey, shape=(num_households,))
+
+ # Update employment states based on transition matrix
+ z_indices_new = jnp.zeros(num_households, dtype=int)
+ for i in range(num_households):
+ z_idx = z_indices[i]
+ # Cumulative probabilities
+ cum_probs = jnp.cumsum(Π[z_idx, :])
+ # Find which state to transition to
+ z_indices_new = z_indices_new.at[i].set(
+ jnp.searchsorted(cum_probs, unif[i])
+ )
+ z_indices = z_indices_new
+
+ # Get current income
+ z_current = z_grid[z_indices]
+ income = w * z_current + (1 + r) * assets
+
+ # Interpolate consumption policy
+ consumption = jnp.zeros(num_households)
+ for i in range(num_households):
+ # Use 2D interpolation: interp over assets for each z
+ consumption = consumption.at[i].set(
+ jnp.interp(assets[i], a_grid, σ[:, z_indices[i]])
+ )
+
+ # Update assets
+ assets = income - consumption
+ assets = jnp.maximum(assets, a_grid[0]) # Enforce borrowing constraint
+
+ return jnp.mean(assets)
+```
+
+The function above is fully JIT-compiled but might be slow for the loops. Let's create a more efficient version using `jax.lax.fori_loop`:
+
+```{code-cell} ipython3
+def simulate_assets_efficient(σ, household, prices,
+ num_households=50_000,
+ num_periods=1000,
+ burn_in=500,
+ seed=42):
+ """
+ Efficient simulation using numpy (numba would be even better but
+ we'll keep it simple with numpy for now).
+ """
+ β, a_grid, z_grid, Π = household
+ r, w = prices
+ z_size = len(z_grid)
+
+ # Convert to numpy for simulation
+ a_grid_np = np.array(a_grid)
+ z_grid_np = np.array(z_grid)
+ Π_np = np.array(Π)
+ σ_np = np.array(σ)
+
+ # Set random seed
+ np.random.seed(seed)
+
+ # Initialize
+ a_init_idx = len(a_grid_np) // 2
+ assets = np.ones(num_households) * a_grid_np[a_init_idx]
+ z_indices = np.zeros(num_households, dtype=int)
+
+ # Simulate
+ for t in range(num_periods):
+ # Draw employment shocks
+ unif = np.random.uniform(size=num_households)
+
+ # Update employment states
+ for i in range(num_households):
+ cum_probs = np.cumsum(Π_np[z_indices[i], :])
+ z_indices[i] = np.searchsorted(cum_probs, unif[i])
+
+ # Compute income
+ z_current = z_grid_np[z_indices]
+ income = w * z_current + (1 + r) * assets
+
+ # Interpolate consumption policy
+ # Note: np.interp extrapolates using boundary values, which can cause
+ # issues if assets go far outside the grid. The grid should be large
+ # enough to cover the range of assets in equilibrium.
+ consumption = np.array([
+ np.interp(assets[i], a_grid_np, σ_np[:, z_indices[i]])
+ for i in range(num_households)
+ ])
+
+ # Update assets
+ assets = income - consumption
+ assets = np.maximum(assets, a_grid_np[0]) # Enforce borrowing constraint
+
+ # Clip assets to grid maximum to prevent extrapolation issues
+ # In equilibrium, assets should stay within the grid if a_max is large enough
+ assets = np.minimum(assets, a_grid_np[-1])
+
+ return np.mean(assets)
+```
+
+Now we can compute capital supply for given prices:
+
+```{code-cell} ipython3
+def capital_supply(household, prices,
+ num_households=50_000,
+ num_periods=1000):
+ """
+ Compute aggregate capital supply by simulation.
+ """
+ # Solve household problem
+ σ = solve_household_egm(household, prices)
+
+ # Simulate to get mean assets
+ mean_assets = simulate_assets_efficient(
+ σ, household, prices,
+ num_households=num_households,
+ num_periods=num_periods
+ )
+
+ return mean_assets
+```
+
+Let's test it:
+
+```{code-cell} ipython3
+household = create_household()
+prices = Prices(r=0.01, w=1.0)
+
+print("Computing capital supply via simulation...")
+with qe.Timer():
+ K_supply = capital_supply(household, prices,
+ num_households=10_000,
+ num_periods=500)
+
+print(f"Capital supply: {K_supply:.4f}")
+```
+
+## Computing Equilibrium
+
+Now we can compute the equilibrium by finding the interest rate where capital supply equals capital demand.
+
+The equilibrium mapping is:
+
+$$
+K_{n+1} = G(K_n)
+$$
+
+where $G(K)$ computes:
+1. Prices $(r, w)$ from firm FOCs given $K$
+2. Household optimal policy given prices
+3. Aggregate capital via simulation
+
+```{code-cell} ipython3
+def G(K, firm, household,
+ num_households=10_000,
+ num_periods=500):
+ """
+ The equilibrium mapping K -> K'.
+ """
+ # Get prices from firm problem
+ r = r_given_k(K, firm)
+ w = r_to_w(r, firm)
+ prices = Prices(r=r, w=w)
+
+ # Compute capital supply
+ K_supply = capital_supply(household, prices,
+ num_households=num_households,
+ num_periods=num_periods)
+
+ return K_supply
+```
+
+We can compute equilibrium using bisection:
+
+```{code-cell} ipython3
+def compute_equilibrium(firm, household,
+ K_min=1.0, K_max=20.0,
+ num_households=10_000,
+ num_periods=500,
+ xtol=1e-3):
+ """
+ Compute equilibrium capital stock using bisection.
+ """
+ def excess_demand(K):
+ K_supply = G(K, firm, household,
+ num_households=num_households,
+ num_periods=num_periods)
+ return K - K_supply
+
+ K_star = bisect(excess_demand, K_min, K_max, xtol=xtol)
+ return K_star
+```
+
+Let's compute the equilibrium:
+
+```{code-cell} ipython3
+firm = Firm()
+household = create_household()
+
+print("Computing equilibrium capital stock...")
+print("(This may take a few minutes due to simulation)")
+
+with qe.Timer():
+ K_star = compute_equilibrium(
+ firm, household,
+ K_min=4.0, K_max=12.0,
+ num_households=5_000,
+ num_periods=400,
+ xtol=5e-2
+ )
+
+print(f"\nEquilibrium capital: {K_star:.4f}")
+
+# Get equilibrium prices
+r_star = r_given_k(K_star, firm)
+w_star = r_to_w(r_star, firm)
+print(f"Equilibrium interest rate: {r_star:.4f}")
+print(f"Equilibrium wage: {w_star:.4f}")
+```
+
+### Visualizing equilibrium
+
+Let's plot the supply and demand curves:
+
+```{code-cell} ipython3
+# Compute supply curve (capital as function of r)
+r_vals = np.linspace(0.005, 0.04, 10)
+K_supply_vals = []
+
+print("Computing supply curve...")
+for r in r_vals:
+ w = r_to_w(r, firm)
+ prices = Prices(r=r, w=w)
+ K_s = capital_supply(household, prices,
+ num_households=5_000,
+ num_periods=300)
+ K_supply_vals.append(K_s)
+ print(f" r={r:.4f}: K={K_s:.3f}")
+
+# Demand curve
+K_vals = np.linspace(4, 12, 50)
+r_demand_vals = r_given_k(K_vals, firm)
+
+# Plot
+fig, ax = plt.subplots(figsize=(10, 6))
+
+ax.plot(K_supply_vals, r_vals, 'o-', lw=2,
+ alpha=0.7, label='Capital Supply (households)',
+ markersize=6)
+ax.plot(K_vals, r_demand_vals, lw=2,
+ alpha=0.7, label='Capital Demand (firms)')
+
+# Mark equilibrium
+ax.plot(K_star, r_star, 'r*', markersize=15,
+ label=f'Equilibrium (K={K_star:.3f})', zorder=5)
+
+ax.set_xlabel('Capital $K$', fontsize=12)
+ax.set_ylabel('Interest Rate $r$', fontsize=12)
+ax.set_title('Aiyagari Model Equilibrium', fontsize=14)
+ax.legend(fontsize=10)
+ax.grid(alpha=0.3)
+
+plt.tight_layout()
+plt.show()
+```
+
+## Wealth Distribution
+
+One advantage of the simulation approach is that we can easily examine the wealth distribution:
+
+```{code-cell} ipython3
+def simulate_wealth_distribution(household, prices,
+ num_households=50_000,
+ num_periods=1000):
+ """
+ Simulate and return the cross-sectional wealth distribution.
+ """
+ # Solve household problem
+ σ = solve_household_egm(household, prices)
+
+ β, a_grid, z_grid, Π = household
+ r, w = prices
+
+ # Convert to numpy
+ a_grid_np = np.array(a_grid)
+ z_grid_np = np.array(z_grid)
+ Π_np = np.array(Π)
+ σ_np = np.array(σ)
+
+ np.random.seed(42)
+
+ # Initialize
+ a_init_idx = len(a_grid_np) // 2
+ assets = np.ones(num_households) * a_grid_np[a_init_idx]
+ z_indices = np.zeros(num_households, dtype=int)
+
+ # Simulate
+ for t in range(num_periods):
+ unif = np.random.uniform(size=num_households)
+
+ for i in range(num_households):
+ cum_probs = np.cumsum(Π_np[z_indices[i], :])
+ z_indices[i] = np.searchsorted(cum_probs, unif[i])
+
+ z_current = z_grid_np[z_indices]
+ income = w * z_current + (1 + r) * assets
+
+ consumption = np.array([
+ np.interp(assets[i], a_grid_np, σ_np[:, z_indices[i]])
+ for i in range(num_households)
+ ])
+
+ assets = income - consumption
+ assets = np.maximum(assets, a_grid_np[0])
+
+ return assets, z_indices
+
+# Simulate wealth distribution at equilibrium
+prices_star = Prices(r=r_star, w=w_star)
+assets_dist, z_dist = simulate_wealth_distribution(
+ household, prices_star,
+ num_households=10_000,
+ num_periods=800
+)
+
+# Plot
+fig, axes = plt.subplots(1, 2, figsize=(14, 5))
+
+# Histogram
+ax = axes[0]
+ax.hist(assets_dist, bins=50, density=True, alpha=0.7, edgecolor='black')
+ax.axvline(np.mean(assets_dist), color='red', linestyle='--',
+ linewidth=2, label=f'Mean = {np.mean(assets_dist):.3f}')
+ax.axvline(np.median(assets_dist), color='orange', linestyle='--',
+ linewidth=2, label=f'Median = {np.median(assets_dist):.3f}')
+ax.set_xlabel('Assets', fontsize=12)
+ax.set_ylabel('Density', fontsize=12)
+ax.set_title('Wealth Distribution', fontsize=14)
+ax.legend()
+ax.grid(alpha=0.3)
+
+# Lorenz curve
+ax = axes[1]
+sorted_assets = np.sort(assets_dist)
+cum_wealth = np.cumsum(sorted_assets) / np.sum(sorted_assets)
+cum_pop = np.linspace(0, 1, len(sorted_assets))
+
+ax.plot(cum_pop, cum_wealth, lw=2, label='Lorenz Curve')
+ax.plot([0, 1], [0, 1], 'k--', lw=1, alpha=0.5, label='Perfect Equality')
+ax.set_xlabel('Cumulative Population Share', fontsize=12)
+ax.set_ylabel('Cumulative Wealth Share', fontsize=12)
+ax.set_title('Lorenz Curve', fontsize=14)
+ax.legend()
+ax.grid(alpha=0.3)
+
+plt.tight_layout()
+plt.show()
+
+# Compute Gini coefficient
+gini = 1 - 2 * np.trapz(cum_wealth, cum_pop)
+print(f"\nGini coefficient: {gini:.4f}")
+```
+
+## Summary and Comparison
+
+This lecture demonstrated how to solve the Aiyagari model using:
+
+1. **Endogenous Grid Method (EGM)** for the household problem
+ - Avoids costly root-finding by working backwards from the Euler equation
+ - Directly computes consumption from marginal utility
+ - More efficient than value function iteration
+
+2. **Simulation** for computing aggregate capital
+ - Simulates a large cross-section of households
+ - More flexible than analytical stationary distributions
+ - Allows easy computation of wealth inequality measures
+
+### Comparison with standard approach
+
+Compared to the {doc}`standard Aiyagari lecture `:
+
+**Advantages:**
+* EGM is faster than Howard iteration (no root-finding needed)
+* Simulation is more flexible (works with continuous shocks, non-linear policies)
+* Easy to compute distributional statistics (Gini, percentiles, etc.)
+* Simpler to extend to more complex models
+
+**Disadvantages:**
+* Simulation requires large number of households for accuracy
+* Equilibrium computation is slower due to simulation
+* Less precise than analytical stationary distribution
+
+### Extensions
+
+This framework can be easily extended to:
+* Continuous income shocks (e.g., lognormal)
+* More complex preference specifications
+* Aggregate shocks and heterogeneous agent New Keynesian (HANK) models
+* Life-cycle models with age-dependent policies
+
+## Exercises
+
+```{exercise}
+:label: aiyagari_egm_ex1
+
+Compare the speed and accuracy of EGM vs. Howard iteration for solving the household problem.
+
+1. Implement a version that uses Howard iteration (you can adapt code from the {doc}`standard Aiyagari lecture `)
+2. Time both methods and compare the resulting policies
+3. Which method is faster? Are the policies identical?
+```
+
+```{exercise}
+:label: aiyagari_egm_ex2
+
+Study how the wealth distribution changes with the discount factor $\beta$.
+
+1. Compute equilibria for $\beta \in \{0.94, 0.95, 0.96, 0.97\}$
+2. For each $\beta$, compute and plot the wealth distribution
+3. How does the Gini coefficient change with $\beta$?
+4. Explain the economic intuition
+```
+
+```{exercise}
+:label: aiyagari_egm_ex3
+
+The simulation method introduced in this lecture uses a fixed number of periods. Investigate the impact of this choice:
+
+1. Vary `num_periods` from 200 to 2000
+2. For each value, compute the mean assets multiple times with different random seeds
+3. Plot the standard deviation of the capital estimate as a function of `num_periods`
+4. What is the trade-off between accuracy and computational cost?
+```
+
+```{exercise}
+:label: aiyagari_egm_ex4
+
+Extend the model to include a third employment state (e.g., unemployed, part-time, full-time):
+
+1. Set up a 3-state Markov chain with appropriate transition probabilities
+2. Define income levels for each state
+3. Re-compute the equilibrium
+4. How does the additional heterogeneity affect the wealth distribution?
+```