@@ -39,21 +39,26 @@ EGM is a numerical method for implementing policy iteration invented by [Chris C
39394040The original reference is {cite}`Carroll2006`.
414142+For now we will focus on a clean and simple implementation of EGM that stays
43+close to the underlying mathematics.
44+45+Then, in {doc}`the next lecture <cake_eating_egm_jax>`, we will construct a fully vectorized and parallelized version of EGM based on JAX.
46+4247Let's start with some standard imports:
434844-```{code-cell} ipython
49+```{code-cell} python3
4550import matplotlib.pyplot as plt
4651import numpy as np
4752import quantecon as qe
4853```
49545055## Key Idea
515652-Let's start by reminding ourselves of the theory and then see how the numerics fit in.
57+First we remind ourselves of the theory and then we turn to numerical methods.
53585459### Theory
556056-Take the model set out in {doc}`Cake Eating IV <cake_eating_time_iter>`, following the same terminology and notation.
61+We work with the model set out in {doc}`cake_eating_time_iter`, following the same terminology and notation.
57625863The Euler equation is
5964@@ -79,24 +84,27 @@ u'(c)
79848085### Exogenous Grid
818682-As discussed in {doc}`Cake Eating IV <cake_eating_time_iter>`, to implement the method on a computer, we need a numerical approximation.
87+As discussed in {doc}`cake_eating_time_iter`, to implement the method on a
88+computer, we need numerical approximation.
83898490In particular, we represent a policy function by a set of values on a finite grid.
859186-The function itself is reconstructed from this representation when necessary, using interpolation or some other method.
92+The function itself is reconstructed from this representation when necessary,
93+using interpolation or some other method.
879488-{doc}`Previously <cake_eating_time_iter>`, to obtain a finite representation of an updated consumption policy, we
95+Our {doc}`previous strategy <cake_eating_time_iter>` for obtaining a finite representation of an updated consumption policy was to
899690-* 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
97+* fix a grid of income points $\{x_i\}$
98+* calculate the consumption value $c_i$ corresponding to each $x_i$ using
99+ {eq}`egm_coledef` and a root-finding routine
9310094101Each $c_i$ is then interpreted as the value of the function $K \sigma$ at $x_i$.
9510296-Thus, with the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation.
103+Thus, with the pairs $\{(x_i, c_i)\}$ in hand, we can reconstruct $K \sigma$ via approximation.
9710498105Iteration then continues...
99106107+100108### Endogenous Grid
101109102110The method discussed above requires a root-finding routine to find the
@@ -105,7 +113,7 @@ $c_i$ corresponding to a given income value $x_i$.
105113Root-finding is costly because it typically involves a significant number of
106114function evaluations.
107115108-As pointed out by Carroll {cite}`Carroll2006`, we can avoid this if
116+As pointed out by Carroll {cite}`Carroll2006`, we can avoid this step if
109117$x_i$ is chosen endogenously.
110118111119The only assumption required is that $u'$ is invertible on $(0, \infty)$.
@@ -114,7 +122,7 @@ Let $(u')^{-1}$ be the inverse function of $u'$.
114122115123The idea is this:
116124117-* First, we fix an *exogenous* grid $\{k_i\}$ for capital ($k = x - c$).
125+* First, we fix an *exogenous* grid $\{s_i\}$ for savings ($s = x - c$).
118126* Then we obtain $c_i$ via
119127120128```{math}
@@ -123,28 +131,28 @@ The idea is this:
123131c_i =
124132(u')^{-1}
125133\left\{
126- \beta \int (u' \circ \sigma) (f(k_i) z ) \, f'(k_i) \, z \, \phi(dz)
134+ \beta \int (u' \circ \sigma) (f(s_i) z ) \, f'(s_i) \, z \, \phi(dz)
127135\right\}
128136```
129137130-* Finally, for each $c_i$ we set $x_i = c_i + k_i$.
138+* Finally, for each $c_i$ we set $x_i = c_i + s_i$.
131139132-It is clear that each $(x_i, c_i)$ pair constructed in this manner satisfies {eq}`egm_coledef`.
140+Importantly, each $(x_i, c_i)$ pair constructed in this manner satisfies {eq}`egm_coledef`.
133141134142With the points $\{x_i, c_i\}$ in hand, we can reconstruct $K \sigma$ via approximation as before.
135143136-The name EGM comes from the fact that the grid $\{x_i\}$ is determined **endogenously**.
144+The name EGM comes from the fact that the grid $\{x_i\}$ is determined **endogenously**.
145+137146138147## Implementation
139148140-As in {doc}`Cake Eating IV <cake_eating_time_iter>`, we will start with a simple setting
141-where
149+As in {doc}`cake_eating_time_iter`, we will start with a simple setting where
142150143151* $u(c) = \ln c$,
144-* production is Cobb-Douglas, and
152+* the function $f$ has a Cobb-Douglas specification, and
145153* the shocks are lognormal.
146154147-This will allow us to make comparisons with the analytical solutions
155+This will allow us to make comparisons with the analytical solutions.
148156149157```{code-cell} python3
150158def v_star(x, α, β, μ):
@@ -164,7 +172,7 @@ def σ_star(x, α, β):
164172 return (1 - α * β) * x
165173```
166174167-We reuse the `Model` structure from {doc}`Cake Eating IV <cake_eating_time_iter>`.
175+We reuse the `Model` structure from {doc}`cake_eating_time_iter`.
168176169177```{code-cell} python3
170178from typing import NamedTuple, Callable
@@ -174,8 +182,8 @@ class Model(NamedTuple):
174182 f: Callable # production function
175183 β: float # discount factor
176184 μ: float # shock location parameter
177- s: float # shock scale parameter
178- grid: np.ndarray # state grid
185+ ν: float # shock scale parameter
186+ s_grid: np.ndarray # exogenous savings grid
179187 shocks: np.ndarray # shock draws
180188 α: float # production function parameter
181189 u_prime: Callable # derivative of utility
@@ -187,7 +195,7 @@ def create_model(u: Callable,
187195 f: Callable,
188196 β: float = 0.96,
189197 μ: float = 0.0,
190- s: float = 0.1,
198+ ν: float = 0.1,
191199 grid_max: float = 4.0,
192200 grid_size: int = 120,
193201 shock_size: int = 250,
@@ -199,53 +207,59 @@ def create_model(u: Callable,
199207 """
200208 Creates an instance of the cake eating model.
201209 """
202- # Set up grid
203- grid = np.linspace(1e-4, grid_max, grid_size)
210+ # Set up exogenous savings grid
211+ s_grid = np.linspace(1e-4, grid_max, grid_size)
204212205213 # Store shocks (with a seed, so results are reproducible)
206214 np.random.seed(seed)
207- shocks = np.exp(μ + s * np.random.randn(shock_size))
215+ shocks = np.exp(μ + ν * np.random.randn(shock_size))
208216209- 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)
217+ return Model(u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv)
211218```
212219213220### The Operator
214221215222Here's an implementation of $K$ using EGM as described above.
216223217224```{code-cell} python3
218-def K(σ_array: np.ndarray, model: Model) -> np.ndarray:
225+def K(
226+ c_in: np.ndarray, # Consumption values on the endogenous grid
227+ x_in: np.ndarray, # Current endogenous grid
228+ model: Model # Model specification
229+ ):
219230 """
220- The Coleman-Reffett operator using EGM
231+ An implementation of the Coleman-Reffett operator using EGM.
221232222233 """
223234224235 # 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
229-230- # Determine endogenous grid
231- x = grid + σ_array # x_i = k_i + c_i
236+ u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv = model
232237233- # Linear interpolation of policy using endogenous grid
234- σ = lambda x_val: np.interp(x_val, x, σ_array)
238+ # Linear interpolation of policy on the endogenous grid
239+ σ = lambda x: np.interp(x, x_in, c_in)
235240236241 # Allocate memory for new consumption array
237- c = np.empty_like(grid)
242+ c_out = np.empty_like(s_grid)
238243239244 # 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))
245+ for i, s in enumerate(s_grid):
246+ vals = u_prime(σ(f(s) * shocks)) * f_prime(s) * shocks
247+ c_out[i] = u_prime_inv(β * np.mean(vals))
248+249+ # Determine corresponding endogenous grid
250+ x_out = s_grid + c_out # x_i = s_i + c_i
243251244- return c
252+ return c_out, x_out
245253```
246254247255Note the lack of any root-finding algorithm.
248256257+```{note}
258+The routine is still not particularly fast because we are using pure Python loops.
259+260+But in the next lecture ({doc}`cake_eating_egm_jax`) we will use a fully vectorized and efficient solution.
261+```
262+249263### Testing
250264251265First we create an instance.
@@ -261,53 +275,53 @@ f_prime = lambda k: α * k**(α - 1)
261275262276model = create_model(u=u, f=f, α=α, u_prime=u_prime,
263277 f_prime=f_prime, u_prime_inv=u_prime_inv)
264-grid = model.grid
278+s_grid = model.s_grid
265279```
266280267281Here's our solver routine:
268282269283```{code-cell} python3
270284def solve_model_time_iter(model: Model,
271- σ_init: np.ndarray,
285+ c_init: np.ndarray,
286+ x_init: np.ndarray,
272287 tol: float = 1e-5,
273288 max_iter: int = 1000,
274- verbose: bool = True) -> np.ndarray:
289+ verbose: bool = True):
275290 """
276291 Solve the model using time iteration with EGM.
277292 """
278- σ = σ_init
293+ c, x = c_init, x_init
279294 error = tol + 1
280295 i = 0
281296282297 while error > tol and i < max_iter:
283- σ_new = K(σ, model)
284- error = np.max(np.abs(σ_new - σ))
285- σ = σ_new
298+ c_new, x_new = K(c, x, model)
299+ error = np.max(np.abs(c_new - c))
300+ c, x = c_new, x_new
286301 i += 1
287302 if verbose:
288303 print(f"Iteration {i}, error = {error}")
289304290305 if i == max_iter:
291306 print("Warning: maximum iterations reached")
292307293- return σ
308+ return c, x
294309```
295310296311Let's call it:
297312298313```{code-cell} python3
299-σ_init = np.copy(grid)
300-σ = solve_model_time_iter(model, σ_init)
314+c_init = np.copy(s_grid)
315+x_init = s_grid + c_init
316+c, x = solve_model_time_iter(model, c_init, x_init)
301317```
302318303319Here is a plot of the resulting policy, compared with the true policy:
304320305321```{code-cell} python3
306-x = grid + σ # x_i = k_i + c_i
307-308322fig, ax = plt.subplots()
309323310-ax.plot(x, σ, lw=2,
324+ax.plot(x, c, lw=2,
311325 alpha=0.8, label='approximate policy function')
312326313327ax.plot(x, σ_star(x, model.α, model.β), 'k--',
@@ -320,16 +334,22 @@ plt.show()
320334The maximal absolute deviation between the two policies is
321335322336```{code-cell} python3
323-np.max(np.abs(σ - σ_star(x, model.α, model.β)))
337+np.max(np.abs(c - σ_star(x, model.α, model.β)))
324338```
325339326340Here's the execution time:
327341328342```{code-cell} python3
329343with qe.Timer():
330- σ = solve_model_time_iter(model, σ_init, verbose=False)
344+ c, x = solve_model_time_iter(model, c_init, x_init, verbose=False)
331345```
332346333347EGM is faster than time iteration because it avoids numerical root-finding.
334348335349Instead, we invert the marginal utility function directly, which is much more efficient.
350+351+In the {doc}`next lecture <cake_eating_egm_jax>`, we will use a fully vectorized
352+and efficient version of EGM that is also parallelized using JAX.
353+354+This provides an extremely fast way to solve the optimal consumption problem we
355+have been studying for the last few lectures.