GitHub

@@ -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

```

113113114114

Here's the Coleman-Reffett operator using EGM.

115115116116

The 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

```

146150147151

We define utility and production functions globally.

@@ -160,58 +164,56 @@ f_prime = lambda k, α: α * k**(α - 1)

160164

Now 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

```

168170169171

The solver uses JAX's `jax.lax.while_loop` for the iteration and is JIT-compiled for speed.

170172171173

```{code-cell} python3

172174

@jax.jit

173175

def 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

```

199202200203

We 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

```

206210207211

Let's plot the resulting policy against the analytical solution.

208212209213

```{code-cell} python3

210-

x = grid + σ # x_i = k_i + c_i

211-212214

fig, ax = plt.subplots()

213215214-

ax.plot(x, σ, lw=2,

216+

ax.plot(x, c, lw=2,

215217

alpha=0.8, label='approximate policy function')

216218217219

ax.plot(x, σ_star(x, model.α, model.β), 'k--',

@@ -224,15 +226,16 @@ plt.show()

224226

The 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.β)))

228230

print(f"Maximum absolute deviation: {max_dev:.7}")

229231

```

230232231233

The JAX implementation is very fast thanks to JIT compilation and vectorization.

232234233235

```{code-cell} python3

234236

with 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

```

237240238241

This speed comes from:

@@ -282,76 +285,86 @@ def u_prime_inv_crra(x, γ):

282285

Now 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

```

310318311319

We also need a solver that uses this operator.

312320313321

```{code-cell} python3

314322

@jax.jit

315323

def 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

```

342351343352

Now 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]

347356

policies = {}

357+

endogenous_grids = {}

348358349-

model_crra = create_model(α=α)

359+

model_crra = create_model()

350360351361

for γ 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.

361374

fig, ax = plt.subplots()

362375363376

for γ 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()

377390378391

Note 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$.

381394382395

Let's check the maximum deviation between the log utility case ($\gamma = 1.0$) and values approaching from above.

383396

Read the original on github.com ↗