@@ -85,7 +85,7 @@ class Model(NamedTuple):
8585 β: float # discount factor
8686 μ: float # shock location parameter
8787 s: float # shock scale parameter
88- grid: jnp.ndarray # state grid
88+ s_grid: jnp.ndarray # exogenous savings grid
8989 shocks: jnp.ndarray # shock draws
9090 α: float # production function parameter
9191@@ -101,47 +101,51 @@ def create_model(β: float = 0.96,
101101 """
102102 Creates an instance of the cake eating model.
103103 """
104- # Set up grid
105- grid = jnp.linspace(1e-4, grid_max, grid_size)
104+ # Set up exogenous savings grid
105+ s_grid = jnp.linspace(1e-4, grid_max, grid_size)
106106107107 # Store shocks (with a seed, so results are reproducible)
108108 key = jax.random.PRNGKey(seed)
109109 shocks = jnp.exp(μ + s * jax.random.normal(key, shape=(shock_size,)))
110110111- return Model(β=β, μ=μ, s=s, grid=grid, shocks=shocks, α=α)
111+ return Model(β=β, μ=μ, s=s, s_grid=s_grid, shocks=shocks, α=α)
112112```
113113114114Here's the Coleman-Reffett operator using EGM.
115115116116The key JAX feature here is `vmap`, which vectorizes the computation over the grid points.
117117118118```{code-cell} python3
119-def K(σ_array: jnp.ndarray, model: Model) -> jnp.ndarray:
119+def K(
120+ c_in: jnp.ndarray, # Consumption values on the endogenous grid
121+ x_in: jnp.ndarray, # Current endogenous grid
122+ model: Model # Model specification
123+ ):
120124 """
121125 The Coleman-Reffett operator using EGM
122126123127 """
124128125129 # Simplify names
126130 β, α = model.β, model.α
127- grid, shocks = model.grid, model.shocks
128-129- # Determine endogenous grid
130- x = grid + σ_array # x_i = k_i + c_i
131+ s_grid, shocks = model.s_grid, model.shocks
131132132133 # Linear interpolation of policy using endogenous grid
133- σ = lambda x_val: jnp.interp(x_val, x, σ_array)
134+ σ = lambda x_val: jnp.interp(x_val, x_in, c_in)
134135135136 # Define function to compute consumption at a single grid point
136- def compute_c(k):
137- vals = u_prime(σ(f(k, α) * shocks)) * f_prime(k, α) * shocks
137+ def compute_c(s):
138+ vals = u_prime(σ(f(s, α) * shocks)) * f_prime(s, α) * shocks
138139 return u_prime_inv(β * jnp.mean(vals))
139140140141 # Vectorize over grid using vmap
141142 compute_c_vectorized = jax.vmap(compute_c)
142- c = compute_c_vectorized(grid)
143+ c_out = compute_c_vectorized(s_grid)
144+145+ # Determine corresponding endogenous grid
146+ x_out = s_grid + c_out # x_i = s_i + c_i
143147144- return c
148+ return c_out, x_out
145149```
146150147151We define utility and production functions globally.
@@ -160,58 +164,56 @@ f_prime = lambda k, α: α * k**(α - 1)
160164Now we create a model instance.
161165162166```{code-cell} python3
163-α = 0.4
164-165-model = create_model(α=α)
166-grid = model.grid
167+model = create_model()
168+s_grid = model.s_grid
167169```
168170169171The solver uses JAX's `jax.lax.while_loop` for the iteration and is JIT-compiled for speed.
170172171173```{code-cell} python3
172174@jax.jit
173175def solve_model_time_iter(model: Model,
174- σ_init: jnp.ndarray,
176+ c_init: jnp.ndarray,
177+ x_init: jnp.ndarray,
175178 tol: float = 1e-5,
176- max_iter: int = 1000) -> jnp.ndarray:
179+ max_iter: int = 1000):
177180 """
178181 Solve the model using time iteration with EGM.
179182 """
180183181184 def condition(loop_state):
182- i, σ, error = loop_state
185+ i, c, x, error = loop_state
183186 return (error > tol) & (i < max_iter)
184187185188 def body(loop_state):
186- i, σ, error = loop_state
187- σ_new = K(σ, model)
188- error = jnp.max(jnp.abs(σ_new - σ))
189- return i + 1, σ_new, error
189+ i, c, x, error = loop_state
190+ c_new, x_new = K(c, x, model)
191+ error = jnp.max(jnp.abs(c_new - c))
192+ return i + 1, c_new, x_new, error
190193191194 # Initialize loop state
192- initial_state = (0, σ_init, tol + 1)
195+ initial_state = (0, c_init, x_init, tol + 1)
193196194197 # Run the loop
195- i, σ, error = jax.lax.while_loop(condition, body, initial_state)
198+ i, c, x, error = jax.lax.while_loop(condition, body, initial_state)
196199197- return σ
200+ return c, x
198201```
199202200203We solve the model starting from an initial guess.
201204202205```{code-cell} python3
203-σ_init = jnp.copy(grid)
204-σ = solve_model_time_iter(model, σ_init)
206+c_init = jnp.copy(s_grid)
207+x_init = s_grid + c_init
208+c, x = solve_model_time_iter(model, c_init, x_init)
205209```
206210207211Let's plot the resulting policy against the analytical solution.
208212209213```{code-cell} python3
210-x = grid + σ # x_i = k_i + c_i
211-212214fig, ax = plt.subplots()
213215214-ax.plot(x, σ, lw=2,
216+ax.plot(x, c, lw=2,
215217 alpha=0.8, label='approximate policy function')
216218217219ax.plot(x, σ_star(x, model.α, model.β), 'k--',
@@ -224,15 +226,16 @@ plt.show()
224226The fit is very good.
225227226228```{code-cell} python3
227-max_dev = jnp.max(jnp.abs(σ - σ_star(x, model.α, model.β)))
229+max_dev = jnp.max(jnp.abs(c - σ_star(x, model.α, model.β)))
228230print(f"Maximum absolute deviation: {max_dev:.7}")
229231```
230232231233The JAX implementation is very fast thanks to JIT compilation and vectorization.
232234233235```{code-cell} python3
234236with qe.Timer(precision=8):
235- σ = solve_model_time_iter(model, σ_init).block_until_ready()
237+ c, x = solve_model_time_iter(model, c_init, x_init)
238+ jax.block_until_ready(c)
236239```
237240238241This speed comes from:
@@ -282,76 +285,86 @@ def u_prime_inv_crra(x, γ):
282285Now we create a version of the Coleman-Reffett operator that takes $\gamma$ as a parameter.
283286284287```{code-cell} python3
285-def K_crra(σ_array: jnp.ndarray, model: Model, γ: float) -> jnp.ndarray:
288+def K_crra(
289+ c_in: jnp.ndarray, # Consumption values on the endogenous grid
290+ x_in: jnp.ndarray, # Current endogenous grid
291+ model: Model, # Model specification
292+ γ: float # CRRA parameter
293+ ):
286294 """
287295 The Coleman-Reffett operator using EGM with CRRA utility
288296 """
289297 # Simplify names
290298 β, α = model.β, model.α
291- grid, shocks = model.grid, model.shocks
292-293- # Determine endogenous grid
294- x = grid + σ_array
299+ s_grid, shocks = model.s_grid, model.shocks
295300296301 # Linear interpolation of policy using endogenous grid
297- σ = lambda x_val: jnp.interp(x_val, x, σ_array)
302+ σ = lambda x_val: jnp.interp(x_val, x_in, c_in)
298303299304 # Define function to compute consumption at a single grid point
300- def compute_c(k):
301- vals = u_prime_crra(σ(f(k, α) * shocks), γ) * f_prime(k, α) * shocks
305+ def compute_c(s):
306+ vals = u_prime_crra(σ(f(s, α) * shocks), γ) * f_prime(s, α) * shocks
302307 return u_prime_inv_crra(β * jnp.mean(vals), γ)
303308304309 # Vectorize over grid using vmap
305310 compute_c_vectorized = jax.vmap(compute_c)
306- c = compute_c_vectorized(grid)
311+ c_out = compute_c_vectorized(s_grid)
312+313+ # Determine corresponding endogenous grid
314+ x_out = s_grid + c_out # x_i = s_i + c_i
307315308- return c
316+ return c_out, x_out
309317```
310318311319We also need a solver that uses this operator.
312320313321```{code-cell} python3
314322@jax.jit
315323def solve_model_crra(model: Model,
316- σ_init: jnp.ndarray,
324+ c_init: jnp.ndarray,
325+ x_init: jnp.ndarray,
317326 γ: float,
318327 tol: float = 1e-5,
319- max_iter: int = 1000) -> jnp.ndarray:
328+ max_iter: int = 1000):
320329 """
321330 Solve the model using time iteration with EGM and CRRA utility.
322331 """
323332324333 def condition(loop_state):
325- i, σ, error = loop_state
334+ i, c, x, error = loop_state
326335 return (error > tol) & (i < max_iter)
327336328337 def body(loop_state):
329- i, σ, error = loop_state
330- σ_new = K_crra(σ, model, γ)
331- error = jnp.max(jnp.abs(σ_new - σ))
332- return i + 1, σ_new, error
338+ i, c, x, error = loop_state
339+ c_new, x_new = K_crra(c, x, model, γ)
340+ error = jnp.max(jnp.abs(c_new - c))
341+ return i + 1, c_new, x_new, error
333342334343 # Initialize loop state
335- initial_state = (0, σ_init, tol + 1)
344+ initial_state = (0, c_init, x_init, tol + 1)
336345337346 # Run the loop
338- i, σ, error = jax.lax.while_loop(condition, body, initial_state)
347+ i, c, x, error = jax.lax.while_loop(condition, body, initial_state)
339348340- return σ
349+ return c, x
341350```
342351343352Now we solve for $\gamma = 1$ (log utility) and values approaching 1 from above.
344353345354```{code-cell} python3
346355γ_values = [1.0, 1.05, 1.1, 1.2]
347356policies = {}
357+endogenous_grids = {}
348358349-model_crra = create_model(α=α)
359+model_crra = create_model()
350360351361for γ in γ_values:
352- σ_init = jnp.copy(model_crra.grid)
353- σ_gamma = solve_model_crra(model_crra, σ_init, γ).block_until_ready()
354- policies[γ] = σ_gamma
362+ c_init = jnp.copy(model_crra.s_grid)
363+ x_init = model_crra.s_grid + c_init
364+ c_gamma, x_gamma = solve_model_crra(model_crra, c_init, x_init, γ)
365+ jax.block_until_ready(c_gamma)
366+ policies[γ] = c_gamma
367+ endogenous_grids[γ] = x_gamma
355368 print(f"Solved for γ = {γ}")
356369```
357370@@ -361,7 +374,7 @@ Plot the policies on their endogenous grids.
361374fig, ax = plt.subplots()
362375363376for γ in γ_values:
364- x = model_crra.grid + policies[γ]
377+ x = endogenous_grids[γ]
365378 if γ == 1.0:
366379 ax.plot(x, policies[γ], 'k-', linewidth=2,
367380 label=f'γ = {γ:.2f} (log utility)', alpha=0.8)
@@ -377,7 +390,7 @@ plt.show()
377390378391Note that the plots for $\gamma > 1$ do not cover the entire x-axis range shown.
379392380-This is because the endogenous grid $x = k + \sigma(k)$ depends on the consumption policy, which varies with $\gamma$.
393+This is because the endogenous grid $x = s + \sigma(s)$ depends on the consumption policy, which varies with $\gamma$.
381394382395Let's check the maximum deviation between the log utility case ($\gamma = 1.0$) and values approaching from above.
383396