by Fedor Iskhakov, ANU
Description: Using function interpolation to solve cake eating problem with discretized choice.
Numerically solve
$$ V(W) = \max_{0 \le c \le W} \big[ u(c)+\beta V(W-c) \big ] $$Solve the functional fixed point equation $ T({V})(W) = V(W) $ for $ V(W) $, where
$$ T(V)(W) \equiv \max_{0 \le c \le W} \big[u(c)+\beta V(W-c)\big] $$import numpy as np
class cake_ongrid():
'''Simple class to implement cake eating problem on the grid'''
def __init__(self,beta=.9, Wbar=10, ngrid=50):
'''Initializer'''
self.beta = beta # Discount factor
self.Wbar = Wbar # Upper bound on cake size
self.ngrid = ngrid # Number of grid points
self.epsilon = np.finfo(float).eps # smallest positive float number
self.grid = np.linspace(self.epsilon,Wbar,ngrid) # grid for both state and decision space
def bellman(self,V0):
'''Bellman operator, V0 is one-dim vector of values on grid'''
c = self.grid - self.grid[:,np.newaxis] # current state in columns and choices in rows
c[c==0] = self.epsilon # add small quantity to avoid log(0)
mask = c>0 # mask off infeasible choices
matV1 = np.full((self.ngrid,self.ngrid),-np.inf) # init V with -inf
matV0 = np.repeat(V0.reshape(self.ngrid,1),self.ngrid,1) #current value function repeated in columns
matV1[mask] = np.log(c[mask])+self.beta*matV0[mask] # maximand of the Bellman equation
V1 = np.amax(matV1,axis=0,keepdims=False) # maximum in every column
c1 = self.grid - self.grid[np.argmax(matV1,axis=0)] # consumption (index of maximum in every column)
return V1, c1
def solve(self, maxiter=1000, tol=1e-4, callback=None):
'''Solves the model using VFI (successive approximations)'''
V0=np.log(self.grid) # on first iteration assume consuming everything
for iter in range(maxiter):
V1,c1=self.bellman(V0)
if callback: callback(iter,self.grid,V1,c1) # callback for making plots
if np.all(abs(V1-V0) < tol):
break
V0=V1
else: # when i went up to maxiter
print('No convergence: maximum number of iterations achieved!')
return V1,c1
In video 30 we derived the analytic solution of the cake eating problem
$$ c^{\star}(W) = \frac {W} {1 + \beta B} = \frac {W} {1 + \frac{\beta}{1-\beta}} = (1-\beta)W $$$$ V(W) = \frac{\log(W)}{1-\beta} + \frac{\log(1-\beta)}{1-\beta} + \frac{\beta \log(\beta)}{(1-\beta)^2} $$import matplotlib.pyplot as plt
%matplotlib inline
def check_analytic(model):
'''Check the cake eating numerical solution against the analytic solution'''
# analytic solution
aV = lambda w: np.log(w)/(1 - model.beta) + np.log(1 - model.beta)/(1 - model.beta) + model.beta* np.log(model.beta)/((1 - model.beta)**2)
aP = lambda w: (1 - model.beta) * w
grid = model.grid # grid from the model
xg = np.linspace(model.epsilon,model.Wbar,1000) # dense grid for analytical solution
V,policy = model.solve() # solve the model
# make plots
fig1, (ax1,ax2) = plt.subplots(1,2,figsize=(14,8))
ax1.grid(b=True, which='both', color='0.65', linestyle='-')
ax2.grid(b=True, which='both', color='0.65', linestyle='-')
ax1.set_title('Value functions')
ax2.set_title('Policy functions')
ax1.set_xlabel('Cake size, W')
ax2.set_xlabel('Cake size, W')
ax1.set_ylabel('Value function')
ax2.set_ylabel('Policy function')
ax1.plot(grid[1:],V[1:],linewidth=1.5,label='Numerical')
ax1.plot(xg,aV(xg),linewidth=1.5,label='Analytical')
ax2.plot(grid,policy,linewidth=1.5,label='Numerical')
ax2.plot(grid,aP(grid),linewidth=1.5,label='Analytical')
ax1.legend()
ax2.legend()
plt.show()
m1 = cake_ongrid(beta=0.9,Wbar=10,ngrid=50)
check_analytic(m1)
Rather than trying to avoid interpolation by rewriting the problem in terms of the next period choice, today we will
Control for grid over state space separately from the discretization of the choice variables to increase accuracy
# CODE DEVELOPED IN THE VIDEO
import numpy as np
from scipy import interpolate # Interpolation routines
class cake_discretized():
'''Class to implement the cake eating model with discretized choice'''
def __init__(self,beta=.9, Wbar=10, ngrid=50, nchgrid=100, optim_ch=True):
'''Initializer'''
self.beta = beta # Discount factor
self.Wbar = Wbar # Upper bound on cake size
self.ngrid = ngrid # Number of grid points
self.nchgrid = nchgrid # Number of grid points for choice grid
self.epsilon = np.finfo(float).eps # smallest positive float number
self.grid = np.linspace(self.epsilon,Wbar,ngrid) # grid for state space
self.chgrid = np.linspace(self.epsilon,Wbar,nchgrid) # grid for decision space
self.optim_ch = optim_ch
def bellman(self,V0):
'''Bellman operator, V0 is one-dim vector of values on state grid'''
c = self.chgrid[:,np.newaxis] # column vector
if self.optim_ch:
c = c + np.zeros(self.ngrid) # matrix of consumption values
c *= self.grid/self.Wbar # scale choices to ensure c<W
W = self.grid # one-dim (like row vector)
interp = interpolate.interp1d(self.grid,V0,bounds_error=False,fill_value='extrapolate')
matV1 = np.log(c) + self.beta * interp(W-c)
matV1[c>W] = -np.inf # infeasible choices
V1 = np.amax(matV1,axis=0,keepdims=False) # maximum in every column
if self.optim_ch:
c1 = c[np.argmax(matV1,axis=0),np.arange(self.ngrid)]
else:
c1 = c[np.argmax(matV1,axis=0)] # consumption (index of maximum in every column)
return V1, c1
def solve(self, maxiter=1000, tol=1e-4, callback=None):
'''Solves the model using VFI (successive approximations)'''
V0=np.log(self.grid) # on first iteration assume consuming everything
for iter in range(maxiter):
V1,c1=self.bellman(V0)
if callback: callback(iter,self.grid,V1,c1) # callback for making plots
if np.all(abs(V1-V0) < tol):
break
V0=V1
else: # when i went up to maxiter
print('No convergence: maximum number of iterations achieved!')
return V1,c1
# CODE DEVELOPED IN THE VIDEO
# m1 = cake_ongrid( beta=0.9,Wbar=10,ngrid=50)
m2 = cake_discretized(beta=0.9,Wbar=10,ngrid=100,nchgrid=100,optim_ch=False)
m3 = cake_discretized(beta=0.9,Wbar=10,ngrid=100,nchgrid=100,optim_ch=True)
# check_analytic(m1)
check_analytic(m2)
check_analytic(m3)
# Previously written solution
class cake_discretized():
def __init__(self,beta=.9, Wbar=10, ngrid=50, ngrid_choice=100):
self.beta = beta # Discount factor
self.Wbar = Wbar # Upper bound on cake size
self.ngrid = ngrid # Number of grid points for the size of cake
self.ngrid_choice = ngrid_choice # Number of grid points for how much of cake to consume
self.epsilon = np.finfo(float).eps # smallest positive float number
self.grid = np.linspace(self.epsilon,Wbar,ngrid) # grid for state space
self.grid_choice = np.linspace(self.epsilon,Wbar,ngrid_choice) # grid for decision space
def bellman(self,V0):
#Bellman operator, V0 is one-dim vector of values on grid
matW = np.repeat(np.reshape(self.grid,(1,-1)),self.ngrid_choice,0) # matrix with state space repeated in rows
c = np.repeat(np.reshape(self.grid_choice,(-1,1)),self.ngrid,1) # decisions grid repeated by columns
#c *= np.reshape(self.grid,(1,-1)) /self.Wbar # normalize max choice to current wealth
matWpr = matW-c # size of cake in the next period
matWpr[matWpr==0] = self.epsilon # add small quantity to avoid log(0)
mask = matWpr>0 # mask off infeasible choices
matV1 = np.interp(matWpr,self.grid,V0) # INPERPOLATE values of next period value at next period case sizes
preV1 = np.full((self.ngrid_choice,self.ngrid),-np.inf) # init V with -inf
preV1[mask] = np.log(c[mask]) + self.beta*matV1[mask] # maximand of the Bellman equation
V1 = np.amax(preV1,0,keepdims=False) # maximum in every column
c1 = c[np.argmax(preV1,axis=0),range(self.ngrid)] # choose the max attaining levels of c
return V1, c1
def solve(self, maxiter=1000, tol=1e-4, callback=None):
'''Solves the model using successive approximations'''
V0=np.log(self.grid) # on first iteration assume consuming everything
for iter in range(maxiter):
V1,c1=self.bellman(V0)
if callback: callback(iter,self.grid,V1,c1) # callback for making plots
if np.all(abs(V1-V0) < tol):
break
V0=V1
else: # when i went up to maxiter
print('No convergence: maximum number of iterations achieved!')
return V1,c1
def solve_plot(self, maxiter=1000, tol=1e-4):
'''Illustrate solution'''
fig1, (ax1,ax2) = plt.subplots(1,2,figsize=(14,8))
ax1.grid(b=True, which='both', color='0.65', linestyle='-')
ax2.grid(b=True, which='both', color='0.65', linestyle='-')
ax1.set_title('Value function convergence with VFI')
ax2.set_title('Policy function convergence with VFI')
ax1.set_xlabel('Cake size, W')
ax2.set_xlabel('Cake size, W')
ax1.set_ylabel('Value function')
ax2.set_ylabel('Policy function')
def callback(iter,grid,v,c):
ax1.plot(grid[1:],v[1:],color='k',alpha=0.25)
ax2.plot(grid,c,color='k',alpha=0.25)
V,c = self.solve(maxiter=maxiter,tol=tol,callback=callback)
# add solutions
ax1.plot(self.grid[1:],V[1:],color='r',linewidth=2.5)
ax2.plot(self.grid,c,color='r',linewidth=2.5)
plt.show()
return V,c
m2 = cake_discretized(beta=0.9,Wbar=10,ngrid=50,ngrid_choice=50)
V2,c2 = m2.solve_plot() # make convergence plot
m1 = cake_ongrid(beta=0.9,Wbar=10,ngrid=50)
m2 = cake_discretized(beta=0.9,Wbar=10,ngrid=50)
check_analytic(m1)
check_analytic(m2)