Skip to content

Commit 080c21e

Browse files
jstacclaude
andcommitted
Refactor cake_eating_egm lecture: update K operator signature and improve clarity
- Update K operator to take (c_in, x_in) and return (c_out, x_out) for clarity - Rename 'grid' to 's_grid' throughout to emphasize exogenous savings grid - Change shock scale parameter from 's' to 'ν' to avoid confusion with savings - Update solve_model_time_iter to work with new K signature - Fix grammar: change "fixed/calculated" to "fix/calculate" in bullet points - Add missing period after "analytical solutions" - Remove extra space in "is determined" - Expand "EG" abbreviation to "endogenous grid" in comment - Change code-cell from ipython to python3 for consistency - Add note about Python loops and reference to JAX lecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a606a21 commit 080c21e

File tree

1 file changed

+75
-60
lines changed

1 file changed

+75
-60
lines changed

lectures/cake_eating_egm.md

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,19 @@ The original reference is {cite}`Carroll2006`.
4141

4242
Let's start with some standard imports:
4343

44-
```{code-cell} ipython
44+
```{code-cell} python3
4545
import matplotlib.pyplot as plt
4646
import numpy as np
4747
import quantecon as qe
4848
```
4949

5050
## Key Idea
5151

52-
Let's start by reminding ourselves of the theory and then see how the numerics fit in.
52+
First we remind ourselves of the theory and then we turn to numerical methods.
5353

5454
### Theory
5555

56-
Take the model set out in {doc}`Cake Eating IV <cake_eating_time_iter>`, following the same terminology and notation.
56+
We work with the model set out in {doc}`cake_eating_time_iter`, following the same terminology and notation.
5757

5858
The Euler equation is
5959

@@ -79,24 +79,27 @@ u'(c)
7979

8080
### Exogenous Grid
8181

82-
As discussed in {doc}`Cake Eating IV <cake_eating_time_iter>`, to implement the method on a computer, we need a numerical approximation.
82+
As discussed in {doc}`cake_eating_time_iter`, to implement the method on a
83+
computer, we need numerical approximation.
8384

8485
In particular, we represent a policy function by a set of values on a finite grid.
8586

86-
The function itself is reconstructed from this representation when necessary, using interpolation or some other method.
87+
The function itself is reconstructed from this representation when necessary,
88+
using interpolation or some other method.
8789

88-
{doc}`Previously <cake_eating_time_iter>`, to obtain a finite representation of an updated consumption policy, we
90+
Our {doc}`previous strategy <cake_eating_time_iter>` for obtaining a finite representation of an updated consumption policy was to
8991

90-
* fixed a grid of income points $\{x_i\}$
91-
* calculated the consumption value $c_i$ corresponding to each
92-
$x_i$ using {eq}`egm_coledef` and a root-finding routine
92+
* fix a grid of income points $\{x_i\}$
93+
* calculate the consumption value $c_i$ corresponding to each $x_i$ using
94+
{eq}`egm_coledef` and a root-finding routine
9395

9496
Each $c_i$ is then interpreted as the value of the function $K \sigma$ at $x_i$.
9597

96-
Thus, with the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation.
98+
Thus, with the pairs $\{(x_i, c_i)\}$ in hand, we can reconstruct $K \sigma$ via approximation.
9799

98100
Iteration then continues...
99101

102+
100103
### Endogenous Grid
101104

102105
The method discussed above requires a root-finding routine to find the
@@ -105,7 +108,7 @@ $c_i$ corresponding to a given income value $x_i$.
105108
Root-finding is costly because it typically involves a significant number of
106109
function evaluations.
107110

108-
As pointed out by Carroll {cite}`Carroll2006`, we can avoid this if
111+
As pointed out by Carroll {cite}`Carroll2006`, we can avoid this step if
109112
$x_i$ is chosen endogenously.
110113

111114
The only assumption required is that $u'$ is invertible on $(0, \infty)$.
@@ -114,7 +117,7 @@ Let $(u')^{-1}$ be the inverse function of $u'$.
114117

115118
The idea is this:
116119

117-
* First, we fix an *exogenous* grid $\{k_i\}$ for capital ($k = x - c$).
120+
* First, we fix an *exogenous* grid $\{s_i\}$ for savings ($s = x - c$).
118121
* Then we obtain $c_i$ via
119122

120123
```{math}
@@ -123,28 +126,28 @@ The idea is this:
123126
c_i =
124127
(u')^{-1}
125128
\left\{
126-
\beta \int (u' \circ \sigma) (f(k_i) z ) \, f'(k_i) \, z \, \phi(dz)
129+
\beta \int (u' \circ \sigma) (f(s_i) z ) \, f'(s_i) \, z \, \phi(dz)
127130
\right\}
128131
```
129132

130-
* Finally, for each $c_i$ we set $x_i = c_i + k_i$.
133+
* Finally, for each $c_i$ we set $x_i = c_i + s_i$.
131134

132-
It is clear that each $(x_i, c_i)$ pair constructed in this manner satisfies {eq}`egm_coledef`.
135+
Importantly, each $(x_i, c_i)$ pair constructed in this manner satisfies {eq}`egm_coledef`.
133136

134137
With the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation as before.
135138

136-
The name EGM comes from the fact that the grid $\{x_i\}$ is determined **endogenously**.
139+
The name EGM comes from the fact that the grid $\{x_i\}$ is determined **endogenously**.
140+
137141

138142
## Implementation
139143

140-
As in {doc}`Cake Eating IV <cake_eating_time_iter>`, we will start with a simple setting
141-
where
144+
As in {doc}`cake_eating_time_iter`, we will start with a simple setting where
142145

143146
* $u(c) = \ln c$,
144-
* production is Cobb-Douglas, and
147+
* the function $f$ has a Cobb-Douglas specification, and
145148
* the shocks are lognormal.
146149

147-
This will allow us to make comparisons with the analytical solutions
150+
This will allow us to make comparisons with the analytical solutions.
148151

149152
```{code-cell} python3
150153
def v_star(x, α, β, μ):
@@ -164,7 +167,7 @@ def σ_star(x, α, β):
164167
return (1 - α * β) * x
165168
```
166169

167-
We reuse the `Model` structure from {doc}`Cake Eating IV <cake_eating_time_iter>`.
170+
We reuse the `Model` structure from {doc}`cake_eating_time_iter`.
168171

169172
```{code-cell} python3
170173
from typing import NamedTuple, Callable
@@ -174,8 +177,8 @@ class Model(NamedTuple):
174177
f: Callable # production function
175178
β: float # discount factor
176179
μ: float # shock location parameter
177-
s: float # shock scale parameter
178-
grid: np.ndarray # state grid
180+
ν: float # shock scale parameter
181+
s_grid: np.ndarray # exogenous savings grid
179182
shocks: np.ndarray # shock draws
180183
α: float # production function parameter
181184
u_prime: Callable # derivative of utility
@@ -187,7 +190,7 @@ def create_model(u: Callable,
187190
f: Callable,
188191
β: float = 0.96,
189192
μ: float = 0.0,
190-
s: float = 0.1,
193+
ν: float = 0.1,
191194
grid_max: float = 4.0,
192195
grid_size: int = 120,
193196
shock_size: int = 250,
@@ -199,53 +202,59 @@ def create_model(u: Callable,
199202
"""
200203
Creates an instance of the cake eating model.
201204
"""
202-
# Set up grid
203-
grid = np.linspace(1e-4, grid_max, grid_size)
205+
# Set up exogenous savings grid
206+
s_grid = np.linspace(1e-4, grid_max, grid_size)
204207
205208
# Store shocks (with a seed, so results are reproducible)
206209
np.random.seed(seed)
207-
shocks = np.exp(μ + s * np.random.randn(shock_size))
210+
shocks = np.exp(μ + ν * np.random.randn(shock_size))
208211
209-
return Model(u=u, f=f, β=β, μ=μ, s=s, grid=grid, shocks=shocks,
210-
α=α, u_prime=u_prime, f_prime=f_prime, u_prime_inv=u_prime_inv)
212+
return Model(u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv)
211213
```
212214

213215
### The Operator
214216

215217
Here's an implementation of $K$ using EGM as described above.
216218

217219
```{code-cell} python3
218-
def K(σ_array: np.ndarray, model: Model) -> np.ndarray:
220+
def K(
221+
c_in: np.ndarray, # Consumption values on the endogenous grid
222+
x_in: np.ndarray, # Current endogenous grid
223+
model: Model # Model specification
224+
):
219225
"""
220-
The Coleman-Reffett operator using EGM
226+
An implementation of the Coleman-Reffett operator using EGM.
221227
222228
"""
223229
224230
# Simplify names
225-
f, β = model.f, model.β
226-
f_prime, u_prime = model.f_prime, model.u_prime
227-
u_prime_inv = model.u_prime_inv
228-
grid, shocks = model.grid, model.shocks
231+
u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv = model
229232
230-
# Determine endogenous grid
231-
x = grid + σ_array # x_i = k_i + c_i
232-
233-
# Linear interpolation of policy using endogenous grid
234-
σ = lambda x_val: np.interp(x_val, x, σ_array)
233+
# Linear interpolation of policy on the endogenous grid
234+
σ = lambda x: np.interp(x, x_in, c_in)
235235
236236
# Allocate memory for new consumption array
237-
c = np.empty_like(grid)
237+
c_out = np.empty_like(s_grid)
238238
239239
# Solve for updated consumption value
240-
for i, k in enumerate(grid):
241-
vals = u_prime(σ(f(k) * shocks)) * f_prime(k) * shocks
242-
c[i] = u_prime_inv(β * np.mean(vals))
240+
for i, s in enumerate(s_grid):
241+
vals = u_prime(σ(f(s) * shocks)) * f_prime(s) * shocks
242+
c_out[i] = u_prime_inv(β * np.mean(vals))
243+
244+
# Determine corresponding endogenous grid
245+
x_out = s_grid + c_out # x_i = s_i + c_i
243246
244-
return c
247+
return c_out, x_out
245248
```
246249

247250
Note the lack of any root-finding algorithm.
248251

252+
```{note}
253+
The routine is still not particularly fast because we are using pure Python loops.
254+
255+
But in the next lecture ({doc}`cake_eating_egm_jax`) we will use a fully vectorized and efficient solution.
256+
```
257+
249258
### Testing
250259

251260
First we create an instance.
@@ -261,53 +270,53 @@ f_prime = lambda k: α * k**(α - 1)
261270
262271
model = create_model(u=u, f=f, α=α, u_prime=u_prime,
263272
f_prime=f_prime, u_prime_inv=u_prime_inv)
264-
grid = model.grid
273+
s_grid = model.s_grid
265274
```
266275

267276
Here's our solver routine:
268277

269278
```{code-cell} python3
270279
def solve_model_time_iter(model: Model,
271-
σ_init: np.ndarray,
280+
c_init: np.ndarray,
281+
x_init: np.ndarray,
272282
tol: float = 1e-5,
273283
max_iter: int = 1000,
274-
verbose: bool = True) -> np.ndarray:
284+
verbose: bool = True):
275285
"""
276286
Solve the model using time iteration with EGM.
277287
"""
278-
σ = σ_init
288+
c, x = c_init, x_init
279289
error = tol + 1
280290
i = 0
281291
282292
while error > tol and i < max_iter:
283-
σ_new = K(σ, model)
284-
error = np.max(np.abs(σ_new - σ))
285-
σ = σ_new
293+
c_new, x_new = K(c, x, model)
294+
error = np.max(np.abs(c_new - c))
295+
c, x = c_new, x_new
286296
i += 1
287297
if verbose:
288298
print(f"Iteration {i}, error = {error}")
289299
290300
if i == max_iter:
291301
print("Warning: maximum iterations reached")
292302
293-
return σ
303+
return c, x
294304
```
295305

296306
Let's call it:
297307

298308
```{code-cell} python3
299-
σ_init = np.copy(grid)
300-
σ = solve_model_time_iter(model, σ_init)
309+
c_init = np.copy(s_grid)
310+
x_init = s_grid + c_init
311+
c, x = solve_model_time_iter(model, c_init, x_init)
301312
```
302313

303314
Here is a plot of the resulting policy, compared with the true policy:
304315

305316
```{code-cell} python3
306-
x = grid + σ # x_i = k_i + c_i
307-
308317
fig, ax = plt.subplots()
309318
310-
ax.plot(x, σ, lw=2,
319+
ax.plot(x, c, lw=2,
311320
alpha=0.8, label='approximate policy function')
312321
313322
ax.plot(x, σ_star(x, model.α, model.β), 'k--',
@@ -320,16 +329,22 @@ plt.show()
320329
The maximal absolute deviation between the two policies is
321330

322331
```{code-cell} python3
323-
np.max(np.abs(σ - σ_star(x, model.α, model.β)))
332+
np.max(np.abs(c - σ_star(x, model.α, model.β)))
324333
```
325334

326335
Here's the execution time:
327336

328337
```{code-cell} python3
329338
with qe.Timer():
330-
σ = solve_model_time_iter(model, σ_init, verbose=False)
339+
c, x = solve_model_time_iter(model, c_init, x_init, verbose=False)
331340
```
332341

333342
EGM is faster than time iteration because it avoids numerical root-finding.
334343

335344
Instead, we invert the marginal utility function directly, which is much more efficient.
345+
346+
In the {doc}`next lecture <cake_eating_egm_jax>`, we will use a fully vectorized
347+
and efficient version of EGM that is also parallelized using JAX.
348+
349+
This provides an extremely fast way to solve the optimal consumption problem we
350+
have been studying for the last few lectures.

0 commit comments

Comments
 (0)