shade-econ / sequence-jacobian

A unified framework to solve and analyze heterogeneous-agent macro models.
MIT License
251 stars 148 forks source link

Problem with `solve_impulse_nonlinear` with permanent type #23

Open raphaelhuleux opened 5 months ago

raphaelhuleux commented 5 months ago

Hello,

When solving a model with two types of heterogenous households (in this case, two different levels of permanent income), it seems that the solve_impulse_nonlinear doesn't compute the proper consumption path for the two types of households.

For example, in this simple one-asset Krusell-Smith model, the variable C_low following a TFP shock is not equal to distribution * policy function. It doesn't converge back to zero and is very different from the same variable computed through solve_impulse_linear, whereas the model should be approximately linear for a small shock. I suspect it's an issue with the initial distribution used to compute the transition. I tried imposing manually ss.internals['hh_low']['Dbeg'] = ss.internals['hh_low']['D'], etc, but it didn't solve the issue.

It also seems that the aggregate variables td_nonlin['C'] and td_lin['C'] match and are accurate, so I'm unsure where the issue comes from.

I also noted that there is no issue when setting Z_low = Z_high = 1.

import numpy as np
from sequence_jacobian import simple, create_model
from sequence_jacobian.blocks.het_block import het
from sequence_jacobian import grids, interpolate, misc
import matplotlib.pyplot as plt

def make_grid_one_asset(amax, nA, nE, rho_e, sig_e):

    e_grid, pi_e, Pi = grids.markov_rouwenhorst(rho=rho_e, sigma=sig_e, N=nE)
    a_grid = grids.agrid(amax=amax, n=nA)

    Dbeg = np.zeros((nE, nA))
    Dbeg[:,0] = pi_e

    Dbeg_low = Dbeg
    Dbeg_high = Dbeg

    Pi_low = Pi
    Pi_high = Pi
    return a_grid, e_grid, Pi, Pi_low, Pi_high

def hh_init_oa(a_grid, Z, z_grid, r, sigma):
    coh = (1 + r) * a_grid[np.newaxis,:] + Z * z_grid[:,np.newaxis] 
    Va = (1 + r) * (0.1 * coh) ** (-sigma)
    return Va

@het(exogenous=['Pi'], policy='a', backward='Va', backward_init=hh_init_oa)
def hh_one_asset(Va_p, a_grid, Z, z_grid, r, beta, sigma):

    uc_nextgrid = beta * Va_p 
    c_nextgrid = uc_nextgrid ** (-1/sigma)
    coh = (1 + r) * a_grid[np.newaxis, :] + Z * z_grid[:,np.newaxis]
    a = interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid)
    misc.setmin(a, a_grid[0])
    c = coh - a
    Va = (1 + r) * c ** (-sigma)

    return Va, a, c

'''Part 1: Blocks'''

@simple 
def firm(K, delta, alpha, tfp):
    Y = tfp * K(-1)**alpha
    r = alpha * Y / K(-1) - delta
    w = (1-alpha) * Y
    I = K - (1-delta) * K(-1)
    return Y, r, w, I

@simple
def mkt_clearing(A, K, C, Y, I, verbose, beta):

    asset_mkt = A - K
    goods_mkt = Y - C - I

    if verbose == True:
        print('asset_mkt = ', float(asset_mkt))
        print('A         = ', float(A))
        print('beta      = ', float(beta))
        print(' ')

    return asset_mkt, goods_mkt

@simple  
def aggregate(A_low, A_high, C_low, C_high, share_low, share_high):

    A = A_low * share_low + A_high * share_high 
    C = C_low * share_low + C_high * share_high 
    return A, C 

def income(e_grid, w):
    z_grid = w * e_grid 
    return z_grid

# Combine Blocks
hh_one_asset = hh_one_asset.add_hetinputs([income, make_grid_one_asset])

to_map = ['Z', *hh_one_asset.outputs]
hh_low = hh_one_asset.remap({k: k + '_low' for k in to_map}).rename('hh_low')
hh_high = hh_one_asset.remap({k: k + '_high' for k in to_map}).rename('hh_high')
blocks = [hh_low, hh_high, firm, mkt_clearing, aggregate]
one_asset_model = create_model(blocks, name='One-Asset HANK')

calibration = {'tfp': 1,
               'nE': 11, 'nA': 500, 'sigma':1,
               'amax': 5_000, 'rho_e': 0.91, 'sig_e': 0.9, 
               'alpha':1/3, "delta":0.09,
               'verbose': True, 'share_low':0.9, 'share_high':0.1}

calibration['K'] = ((0.05+calibration['delta']) / (calibration['alpha']))**(1/(calibration['alpha']-1))
calibration['Z_high'] = 1.8
# Normalize total endowment in permanent productivity to 1 
calibration['Z_low'] = (1 - calibration['Z_high'] * calibration['share_high']) / calibration['share_low']

# Steady state

ss =  one_asset_model.solve_steady_state(calibration, {'beta':(0.8,0.95)}, {'asset_mkt':0})
print(ss['goods_mkt']) # Check Walras law
ss['verbose']=False

# Transition
unknowns = ['K']
targets = ['asset_mkt']

T = 300
sig = 0.0005
rho = 0.15
dtfp = sig * rho ** np.arange(T)

td_nonlin = one_asset_model.solve_impulse_nonlinear(ss, unknowns, targets, {"tfp": dtfp}, internals=['hh_low', 'hh_high'])
td_lin = one_asset_model.solve_impulse_linear(ss, unknowns, targets, {"tfp": dtfp})

# This should be the same as td_nonlin['C_low'] : distribution * policy function
dC_low = np.sum(td_nonlin.internals['hh_low']['c']*(ss.internals['hh_low']['D'] + td_nonlin.internals['hh_low']['D']), axis = (1,2)) 
error = np.max(np.abs(dC_low - td_nonlin['C_low']))
print(error)

# Difference between solve_impulse_nonlinear and policy function * distribution
plt.plot(td_nonlin['C_low'], label = 'solve_impulse_nonlinear')
plt.plot(dC_low, linestyle = ':', label = 'policy function * distribution')
plt.legend()
plt.show()

# Difference beteen nonlinear and linear: should be approximately the same for a small shock
plt.plot(td_nonlin['C_low'],label =  'nonlinear')
plt.plot(td_lin['C_low'], linestyle = ':', label = 'linear')
plt.legend()
plt.show()

# Aggregate consumption match for linear and nonlinear
plt.plot(td_nonlin['C'],label =  'nonlinear')
plt.plot(td_lin['C'], linestyle = ':', label = 'linear')
plt.legend()
plt.show()
azinoma commented 1 week ago

Hi Raphael (infering), I also had some problems with remap, did you try to not use remap but instead just define two independent Hh blocks (in case it's still a problem)?