Skip to content

Commit dd88763

Browse files
jstacclaude
andcommitted
Refactor optimal growth lectures into unified cake eating series
This commit restructures and consolidates five lectures into a coherent cake eating series with consistent notation, modern Python patterns, and clearer pedagogical flow. ## File Changes ### Renamed Files - `cake_eating_problem.md` → `cake_eating.md` (Cake Eating I) - `optgrowth.md` → `cake_eating_stochastic.md` (Cake Eating III) - `coleman_policy_iter.md` → `cake_eating_time_iter.md` (Cake Eating IV) - `egm_policy_iter.md` → `cake_eating_egm.md` (Cake Eating V) ### Deleted Files - `optgrowth_fast.md` (content merged into Cake Eating III) ### Updated Files - `_toc.yml` - Updated all file references - `_static/lecture_specific/optgrowth/cd_analytical.py` - Changed variable names ## Major Changes ### 1. Consistent Notation (y → x) Changed state variable from `y` to `x` throughout all lectures to maintain consistency with Cake Eating I and II, which use `x` for cake size. ### 2. Reframed as Cake Eating Problem - Cake Eating III now explains the problem as a stochastic cake that regrows (like a harvest) when seeds are saved - Connected to stochastic growth theory without claiming to be a growth model - Updated all references from "optimal growth" to "cake eating" in text - Changed index entries and section headers accordingly ### 3. Modern Python with Type Hints Converted from traditional classes to typed NamedTuples: - `class Model(NamedTuple)` with full type annotations - `create_model()` factory function - Type hints on all functions using `Callable`, `np.ndarray`, etc. - Changed class methods to standalone functions ### 4. Consistent Naming - `OptimalGrowthModel` → `Model` - `og` → `model` (variable names) - All lectures now use the same model structure ### 5. Pedagogical Improvements **Cake Eating III (Stochastic Dynamics):** - Introduced as continuation of Cake Eating I and II - Uses harvest/regrowth metaphor for stochastic production - Maintained value function iteration approach **Cake Eating IV (Time Iteration):** - Clear introduction explaining time iteration concept - Explains it builds on Cake Eating III - Previews that Cake Eating V will be even more efficient - Defined model inline instead of loading external files **Cake Eating V (EGM):** - Builds naturally from Cake Eating IV - Shows efficiency gains from avoiding root-finding - Consistent model structure throughout ## Technical Details - All Python code uses consistent variable names (x instead of y) - Removed external file dependencies where possible - Inline function definitions for clarity - Updated cross-references between lectures - Preserved mathematical rigor while improving accessibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 43a89ed commit dd88763

File tree

8 files changed

+637
-890
lines changed

8 files changed

+637
-890
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11

2-
def v_star(y, α, β, μ):
2+
def v_star(x, α, β, μ):
33
"""
44
True value function
55
"""
66
c1 = np.log(1 - α * β) / (1 - β)
77
c2 = (μ + α * np.log(α * β)) / (1 - α)
88
c3 = 1 / (1 - β)
99
c4 = 1 / (1 - α * β)
10-
return c1 + c2 * (c3 - c4) + c4 * np.log(y)
10+
return c1 + c2 * (c3 - c4) + c4 * np.log(x)
1111

12-
def σ_star(y, α, β):
12+
def σ_star(x, α, β):
1313
"""
1414
True optimal policy
1515
"""
16-
return (1 - α * β) * y
16+
return (1 - α * β) * x
1717

lectures/_toc.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,11 @@ parts:
8080
- file: cass_fiscal
8181
- file: cass_fiscal_2
8282
- file: ak2
83-
- file: cake_eating_problem
83+
- file: cake_eating
8484
- file: cake_eating_numerical
85-
- file: optgrowth
86-
- file: optgrowth_fast
87-
- file: coleman_policy_iter
88-
- file: egm_policy_iter
85+
- file: cake_eating_stochastic
86+
- file: cake_eating_time_iter
87+
- file: cake_eating_egm
8988
- file: ifp
9089
- file: ifp_advanced
9190
- caption: LQ Control
File renamed without changes.

lectures/cake_eating_egm.md

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
kernelspec:
7+
display_name: Python 3
8+
language: python
9+
name: python3
10+
---
11+
12+
```{raw} jupyter
13+
<div id="qe-notebook-header" align="right" style="text-align:right;">
14+
<a href="https://quantecon.org/" title="quantecon.org">
15+
<img style="width:250px;display:inline;" width="250px" src="https://assets.quantecon.org/img/qe-menubar-logo.svg" alt="QuantEcon">
16+
</a>
17+
</div>
18+
```
19+
20+
# {index}`Cake Eating V: The Endogenous Grid Method <single: Cake Eating V: The Endogenous Grid Method>`
21+
22+
```{contents} Contents
23+
:depth: 2
24+
```
25+
26+
27+
## Overview
28+
29+
Previously, we solved the stochastic cake eating problem using
30+
31+
1. {doc}`value function iteration <cake_eating_stochastic>`
32+
1. {doc}`Euler equation based time iteration <cake_eating_time_iter>`
33+
34+
We found time iteration to be significantly more accurate and efficient.
35+
36+
In this lecture, we'll look at a clever twist on time iteration called the **endogenous grid method** (EGM).
37+
38+
EGM is a numerical method for implementing policy iteration invented by [Chris Carroll](https://econ.jhu.edu/directory/christopher-carroll/).
39+
40+
The original reference is {cite}`Carroll2006`.
41+
42+
Let's start with some standard imports:
43+
44+
```{code-cell} ipython
45+
import matplotlib.pyplot as plt
46+
import numpy as np
47+
from numba import jit
48+
```
49+
50+
## Key Idea
51+
52+
Let's start by reminding ourselves of the theory and then see how the numerics fit in.
53+
54+
### Theory
55+
56+
Take the model set out in {doc}`Cake Eating IV <cake_eating_time_iter>`, following the same terminology and notation.
57+
58+
The Euler equation is
59+
60+
```{math}
61+
:label: egm_euler
62+
63+
(u'\circ \sigma^*)(x)
64+
= \beta \int (u'\circ \sigma^*)(f(x - \sigma^*(x)) z) f'(x - \sigma^*(x)) z \phi(dz)
65+
```
66+
67+
As we saw, the Coleman-Reffett operator is a nonlinear operator $K$ engineered so that $\sigma^*$ is a fixed point of $K$.
68+
69+
It takes as its argument a continuous strictly increasing consumption policy $\sigma \in \Sigma$.
70+
71+
It returns a new function $K \sigma$, where $(K \sigma)(x)$ is the $c \in (0, \infty)$ that solves
72+
73+
```{math}
74+
:label: egm_coledef
75+
76+
u'(c)
77+
= \beta \int (u' \circ \sigma) (f(x - c) z ) f'(x - c) z \phi(dz)
78+
```
79+
80+
### Exogenous Grid
81+
82+
As discussed in {doc}`Cake Eating IV <cake_eating_time_iter>`, to implement the method on a computer, we need a numerical approximation.
83+
84+
In particular, we represent a policy function by a set of values on a finite grid.
85+
86+
The function itself is reconstructed from this representation when necessary, using interpolation or some other method.
87+
88+
{doc}`Previously <cake_eating_time_iter>`, to obtain a finite representation of an updated consumption policy, we
89+
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
93+
94+
Each $c_i$ is then interpreted as the value of the function $K \sigma$ at $x_i$.
95+
96+
Thus, with the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation.
97+
98+
Iteration then continues...
99+
100+
### Endogenous Grid
101+
102+
The method discussed above requires a root-finding routine to find the
103+
$c_i$ corresponding to a given income value $x_i$.
104+
105+
Root-finding is costly because it typically involves a significant number of
106+
function evaluations.
107+
108+
As pointed out by Carroll {cite}`Carroll2006`, we can avoid this if
109+
$x_i$ is chosen endogenously.
110+
111+
The only assumption required is that $u'$ is invertible on $(0, \infty)$.
112+
113+
Let $(u')^{-1}$ be the inverse function of $u'$.
114+
115+
The idea is this:
116+
117+
* First, we fix an *exogenous* grid $\{k_i\}$ for capital ($k = x - c$).
118+
* Then we obtain $c_i$ via
119+
120+
```{math}
121+
:label: egm_getc
122+
123+
c_i =
124+
(u')^{-1}
125+
\left\{
126+
\beta \int (u' \circ \sigma) (f(k_i) z ) \, f'(k_i) \, z \, \phi(dz)
127+
\right\}
128+
```
129+
130+
* Finally, for each $c_i$ we set $x_i = c_i + k_i$.
131+
132+
It is clear that each $(x_i, c_i)$ pair constructed in this manner satisfies {eq}`egm_coledef`.
133+
134+
With the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation as before.
135+
136+
The name EGM comes from the fact that the grid $\{x_i\}$ is determined **endogenously**.
137+
138+
## Implementation
139+
140+
As in {doc}`Cake Eating IV <cake_eating_time_iter>`, we will start with a simple setting
141+
where
142+
143+
* $u(c) = \ln c$,
144+
* production is Cobb-Douglas, and
145+
* the shocks are lognormal.
146+
147+
This will allow us to make comparisons with the analytical solutions
148+
149+
```{code-cell} python3
150+
:load: _static/lecture_specific/optgrowth/cd_analytical.py
151+
```
152+
153+
We reuse the `Model` structure from {doc}`Cake Eating IV <cake_eating_time_iter>`.
154+
155+
```{code-cell} python3
156+
from typing import NamedTuple, Callable
157+
158+
class Model(NamedTuple):
159+
u: Callable # utility function
160+
f: Callable # production function
161+
β: float # discount factor
162+
μ: float # shock location parameter
163+
s: float # shock scale parameter
164+
grid: np.ndarray # state grid
165+
shocks: np.ndarray # shock draws
166+
α: float = 0.4 # production function parameter
167+
u_prime: Callable = None # derivative of utility
168+
f_prime: Callable = None # derivative of production
169+
u_prime_inv: Callable = None # inverse of u_prime
170+
171+
172+
def create_model(u: Callable,
173+
f: Callable,
174+
β: float = 0.96,
175+
μ: float = 0.0,
176+
s: float = 0.1,
177+
grid_max: float = 4.0,
178+
grid_size: int = 120,
179+
shock_size: int = 250,
180+
seed: int = 1234,
181+
α: float = 0.4,
182+
u_prime: Callable = None,
183+
f_prime: Callable = None,
184+
u_prime_inv: Callable = None) -> Model:
185+
"""
186+
Creates an instance of the cake eating model.
187+
"""
188+
# Set up grid
189+
grid = np.linspace(1e-4, grid_max, grid_size)
190+
191+
# Store shocks (with a seed, so results are reproducible)
192+
np.random.seed(seed)
193+
shocks = np.exp(μ + s * np.random.randn(shock_size))
194+
195+
return Model(u=u, f=f, β=β, μ=μ, s=s, grid=grid, shocks=shocks,
196+
α=α, u_prime=u_prime, f_prime=f_prime, u_prime_inv=u_prime_inv)
197+
```
198+
199+
### The Operator
200+
201+
Here's an implementation of $K$ using EGM as described above.
202+
203+
```{code-cell} python3
204+
@jit
205+
def K(σ_array: np.ndarray, model: Model) -> np.ndarray:
206+
"""
207+
The Coleman-Reffett operator using EGM
208+
209+
"""
210+
211+
# Simplify names
212+
f, β = model.f, model.β
213+
f_prime, u_prime = model.f_prime, model.u_prime
214+
u_prime_inv = model.u_prime_inv
215+
grid, shocks = model.grid, model.shocks
216+
217+
# Determine endogenous grid
218+
x = grid + σ_array # x_i = k_i + c_i
219+
220+
# Linear interpolation of policy using endogenous grid
221+
σ = lambda x_val: np.interp(x_val, x, σ_array)
222+
223+
# Allocate memory for new consumption array
224+
c = np.empty_like(grid)
225+
226+
# Solve for updated consumption value
227+
for i, k in enumerate(grid):
228+
vals = u_prime(σ(f(k) * shocks)) * f_prime(k) * shocks
229+
c[i] = u_prime_inv(β * np.mean(vals))
230+
231+
return c
232+
```
233+
234+
Note the lack of any root-finding algorithm.
235+
236+
### Testing
237+
238+
First we create an instance.
239+
240+
```{code-cell} python3
241+
# Define utility and production functions with derivatives
242+
α = 0.4
243+
u = lambda c: np.log(c)
244+
u_prime = lambda c: 1 / c
245+
u_prime_inv = lambda x: 1 / x
246+
f = lambda k: k**α
247+
f_prime = lambda k: α * k**(α - 1)
248+
249+
model = create_model(u=u, f=f, α=α, u_prime=u_prime,
250+
f_prime=f_prime, u_prime_inv=u_prime_inv)
251+
grid = model.grid
252+
```
253+
254+
Here's our solver routine:
255+
256+
```{code-cell} python3
257+
def solve_model_time_iter(model: Model,
258+
σ_init: np.ndarray,
259+
tol: float = 1e-5,
260+
max_iter: int = 1000,
261+
verbose: bool = True) -> np.ndarray:
262+
"""
263+
Solve the model using time iteration with EGM.
264+
"""
265+
σ = σ_init
266+
error = tol + 1
267+
i = 0
268+
269+
while error > tol and i < max_iter:
270+
σ_new = K(σ, model)
271+
error = np.max(np.abs(σ_new - σ))
272+
σ = σ_new
273+
i += 1
274+
if verbose:
275+
print(f"Iteration {i}, error = {error}")
276+
277+
if i == max_iter:
278+
print("Warning: maximum iterations reached")
279+
280+
return σ
281+
```
282+
283+
Let's call it:
284+
285+
```{code-cell} python3
286+
σ_init = np.copy(grid)
287+
σ = solve_model_time_iter(model, σ_init)
288+
```
289+
290+
Here is a plot of the resulting policy, compared with the true policy:
291+
292+
```{code-cell} python3
293+
x = grid + σ # x_i = k_i + c_i
294+
295+
fig, ax = plt.subplots()
296+
297+
ax.plot(x, σ, lw=2,
298+
alpha=0.8, label='approximate policy function')
299+
300+
ax.plot(x, σ_star(x, model.α, model.β), 'k--',
301+
lw=2, alpha=0.8, label='true policy function')
302+
303+
ax.legend()
304+
plt.show()
305+
```
306+
307+
The maximal absolute deviation between the two policies is
308+
309+
```{code-cell} python3
310+
np.max(np.abs(σ - σ_star(x, model.α, model.β)))
311+
```
312+
313+
How long does it take to converge?
314+
315+
```{code-cell} python3
316+
%%timeit -n 3 -r 1
317+
σ = solve_model_time_iter(model, σ_init, verbose=False)
318+
```
319+
320+
Relative to time iteration, which was already found to be highly efficient, EGM
321+
has managed to shave off still more run time without compromising accuracy.
322+
323+
This is due to the lack of a numerical root-finding step.
324+
325+
We can now solve the stochastic cake eating problem at given parameters extremely fast.

0 commit comments

Comments
 (0)