Uncertain Expansion Example Notebook#
In Chapter 11, we described the small-noise expansion method and the recursive utility framework. This section covers the usage of Expansion Suite
, an open source Python toolbox which uses these methods to solve discrete nonlinear DSGE models. We use an example model taken from Hansen, Khorrami and Tourre (2024).
This notebook provides both written explanations and accompanying code. The simplest way to run the code is to open the Google Colab link at the top of this page. Otherwise, the reader can copy the code snippets onto their local machine.
\(\newcommand{\eqdef}{\stackrel{\text{def}}{=}}\)
1 Model Setup#
We employ a discrete-time approximation of the single-capital model in Section 4.4 of [Hansen et al., 2024], as outlined in Section 11.7.1.
Let \({\widehat K}_t = \log K_t,\), \({\widehat C}_t = \log C_t\), \(\widehat{Z}_t^2 = \log Z_t^2\) and \(D_t = \frac{I_t}{K_t}\). The model uses three states: \(X_t = \begin{bmatrix}{\widehat K}_{t+\epsilon}-{\widehat K}_{t}, Z_t^1, \widehat{Z}_t^2\end{bmatrix}\), as well as two jump variables \(J_t = \begin{bmatrix}{\widehat C}_t, D_t \end{bmatrix}\).
The law of motions for the three states are:
We can solve for the jump variables using the first-order condition for \(D_t\) and the output constraint:
1.2 Rewriting First-Order Conditions #
To input this model into our code, we need to write it in the form introduced in Section 11.9, \(Q_t {\mathbb E} \left( N_{t+\epsilon} H_{t+\epsilon} \mid {\mathfrak A}_t \right) + P_tL_{t} - M_t = 0\), where
By noting that \(MG_t=1\), we can express the model above simply as:
We stack the system above with the state law of motions and solve the system given the explicit approximations for \(Q_t\) and \(N_{t+\epsilon}\) detailed in the appendix of Chapter 11.
2 Inputs #
The Expansion Suite
uses the function uncertain_expansion
to approximate a solution to the above system locally. It takes the following inputs:
The output is of class ModelSolution
, which stores the coefficients for the linear-quadratic approximation for the jump and state variables; continuation values; consumption growth; and log change of measure, as well as the steady-state values of each variables.
We will now walk through the computation using the example above.
2.1 Libraries #
Begin by installing the following libraries and downloading RiskUncertaintyValue
, which contains the functions required to solve the model:
import os
import sys
workdir = os.getcwd()
!git clone https://github.com/lphansen/RiskUncertaintyValue
workdir = os.getcwd() + '/RiskUncertaintyValue'
sys.path.insert(0, workdir+'/src')
import numpy as np
import autograd.numpy as anp
from scipy import optimize
np.set_printoptions(suppress=True)
np.set_printoptions(linewidth=200)
from IPython.display import display, HTML
from BY_example_sol import disp
display(HTML("<style>.container { width:97% !important; }</style>"))
import warnings
warnings.filterwarnings("ignore")
import matplotlib.pyplot as plt
from lin_quad import LinQuadVar
from lin_quad_util import E, concat, next_period, cal_E_ww, lq_sum, N_tilde_measure, E_N_tp1
from uncertain_expansion import uncertain_expansion
np.set_printoptions(suppress=True)
import pickle
from scipy.optimize import fsolve
fatal: destination path 'RiskUncertaintyValue' already exists and is not an empty directory.
2.2 Equilibrium Condition Function #
The first input for uncertain_expansion
requires us to define the equilibrium conditions for the model. Therefore we define a function eq
which takes in a list of jump and state variables as inputs, and outputs the equilibrium equations in the form of equations 1 and 2. The inputs required:
Variable |
Type |
Description |
Corresponds to |
---|---|---|---|
|
array-like |
Vector of jump and state variables in the current period |
\((J_t, X_t)\) |
|
array-like |
Vector of jump and state variables in the next period |
\((J_{t+\epsilon}, X_{t+\epsilon})\) |
|
array-like |
Vector of shock variables in the next period |
\((W_{t+\epsilon})\) |
|
float |
Perturbation parameter |
\(q\) |
|
string |
By default, this function returns the LHS of equation (1) and (2). Set mode to ‘psi1’, ‘psi2’, or ‘phi’ to return the corresponding function in the equilibrium conditions |
|
|
array-like |
Steady-state values of \(N\) and \(Q\) |
|
|
tuple of float/ndarray |
Preference and model parameters |
While writing out eq
, ensure that:
State variables follow jump variables.
The number of jump variables equals the number of forward-looking equilibrium conditions.
The number of state variables equals to the number of state evolution equations.
All values are expressed as type
float
. Otherwise, this may cause type errors.
For the example model, we write:
def eq_onecap_3d(Var_t, Var_tp1, W_tp1, q, mode, recursive_ss, *args):
# Unpack model parameters
γ, β, ρ, a, ϕ_1, ϕ_2, α_k, beta_k, beta_z, sigma_k1, sigma_k2, sigma_k3, sigma_z1, sigma_z2, sigma_z3, sigma_y1, sigma_y2, sigma_y3, beta_2, mu_2, ε = args
w1_tp1, w2_tp1, w3_tp1 = W_tp1.ravel()
# Unpack model variables
log_cmk_t, log_imk_t, gk_t, Z_t, Y_t = Var_t.ravel()
log_cmk_tp1, log_imk_tp1, gk_tp1, Z_tp1, Y_tp1 = Var_tp1.ravel()
imk_t = anp.exp(log_imk_t)
cmk_t = anp.exp(log_cmk_t)
q_t = β*anp.exp((1.-ρ)*recursive_ss[0])
p_t = (1-β)*anp.exp((ρ-1.)*recursive_ss[1])
# State evolution processes
dgk = ε * (ϕ_1 * anp.log(1.+ϕ_2*imk_t)-α_k + beta_k*Z_t - q**2* 0.5 * (sigma_k1**2 + sigma_k2**2 + sigma_k3**2) * anp.exp(Y_t)) + anp.sqrt(ε) *(anp.exp(0.5*Y_t))*(sigma_k1*w1_tp1+sigma_k2*w2_tp1+sigma_k3*w3_tp1)
# Equation for states at time t
phi_1 = gk_tp1 - dgk
phi_2 = Z_tp1 - Z_t + ε * (beta_z*Z_t) - anp.sqrt(ε)*(anp.exp(0.5*Y_t))*(sigma_z1*w1_tp1 + sigma_z2*w2_tp1+ sigma_z3*w3_tp1)
phi_3 = Y_tp1 - Y_t + ε * beta_2 * (1 - mu_2 * anp.exp(-Y_t)) \
+ q**2 * 0.5 * (sigma_y1**2 + sigma_y2**2 + sigma_y3**2) * anp.exp(-Y_t) * ε \
- anp.exp(-0.5 * Y_t) * (sigma_y1*w1_tp1+sigma_y2*w2_tp1+sigma_y3*w3_tp1) * anp.sqrt(ε)
## Output constraint
qnh_1 = 0.
pl_1 = 0.
u_1 = -a + cmk_t + imk_t
## Investment FOC
qnh_2 = ε/(1.+ϕ_2*imk_t)
pl_2 = - anp.exp((1.-ρ)*log_cmk_t) * 1/(a-imk_t)
u_2 = 0.
if mode == 'H':
return anp.array([qnh_1, qnh_2])
if mode == 'L':
return anp.array([pl_1, pl_2])
return anp.array([
q_t*qnh_1 - u_1,
q_t*qnh_2 + p_t* pl_2 - u_2,
phi_1, phi_2, phi_3])
2.3 Steady State Function #
This function takes model parameters as input and outputs the steady states for each variable following the variable order specified in Var_t
and Var_tp1
. To find the steady state, we remove the time subscripts from each variable and solve the system of equations.
def ss_onecap_3d(*args, return_recursive=False):
γ, β, ρ, a, ϕ_1, ϕ_2, α_k, beta_k, beta_z, sigma_k1, sigma_k2, sigma_k3, sigma_z1, sigma_z2, sigma_z3, sigma_y1, sigma_y2, sigma_y3, beta_2, mu_2, ε = args
def f(x):
vmk, imk, log_cmk, log_gk = x
recursive = anp.exp((1 - ρ) * vmk) - (1 - β) * anp.exp((1 - ρ) * log_cmk) - β * (anp.exp((1 - ρ) *vmk) * anp.exp((1 - ρ) * log_gk))
foc = -(1-β)*anp.exp((1 - ρ)*log_cmk) * (1/(a-imk)) + β * ε * anp.exp((1 - ρ)* (log_gk + vmk)) * (1/(1+ ϕ_2 * imk))
capital_growth = log_gk - ε * (ϕ_1 * anp.log(1. + ϕ_2 * imk) - α_k)
output_constraint = log_cmk - anp.log(a - imk)
return anp.array([recursive, foc, capital_growth, output_constraint])
initial_guess = [-5, 0.0664766959220647, -3.86035793, 0.02330655]
root = fsolve(f, initial_guess)
vmk, imk, log_cmk, log_gk = root
log_imk = anp.log(imk)
#Assemble variables for equilibrium conditions
rmv = log_gk
log_y = anp.log(mu_2)
if return_recursive:
return anp.array([rmv, vmk, log_cmk, log_imk, log_gk, 0., log_y])
else:
return anp.array([log_cmk, log_imk, log_gk, 0., log_y])
2.4 Specifying Number of Variables #
We need to specify the number of jump variables, state and shock variables as an array (n_J, n_X, n_W)
.
Show code cell content
var_shape = (2, 3, 3)
2.5 Model Parameters #
Next, we need to specify all the model parameters using a Python tuple.
The first three parameters should be γ, β, ρ, respectively. The package is designed for recursive utlity.
Please use 1.00001 for γ and ρ as approximation for 1.0.
We suggest not using matrix when specifying the equilibrium conditions and parameters.
Show code cell content
#Params
sigma_k1 = 0.92 * anp.sqrt(12)
sigma_k2 = 0.4 * anp.sqrt(12)
sigma_k3 = 0.0
sigma_z1 = 0.0
sigma_z2 = 5.7 * anp.sqrt(12)
sigma_z3 = 0.0
sigma_y1 = 0.0
sigma_y2 = 0.0
sigma_y3 = 0.00031 * anp.sqrt(12)
delta = 0.01
alpha = 0.0922
a = alpha
ϕ_1 = 1/8
ϕ_2 = 8.
beta_k = 0.04
beta_z = 0.056
beta_2 = 0.194
ε = 1.0
mu_2 = 6.3 * (10**(-6))
α_k = 0.04
γ = 8.0
ρ = 1.001
β = anp.exp(-ε * delta)
args = γ, β, ρ, a, ϕ_1, ϕ_2, α_k, beta_k, beta_z, sigma_k1, sigma_k2, sigma_k3, sigma_z1, sigma_z2, sigma_z3, sigma_y1, sigma_y2, sigma_y3, beta_2, mu_2, ε
2.6 Additional Functions #
Finally, we specify how to compute the log-growth of consumption \(\log{C_{t+\epsilon}/C_t}\), log-growth of capital \(\log{G_{t+\epsilon}/G_t}\) and log-consumption-capital ratio \(\log{C_{t}/G_t}\) as a function of the defined variables Var_t
and Var_tp1
, which will be useful in computing the approximations for \(N_{t+\epsilon}\) and \(Q_t\). The log-growth of capital is already a state variable in this particular specification, but we list it here for more general purposes.
To compute log-growth of consumption, we simply use the decomposition:
def gc_onecap_3d(Var_t, Var_tp1, W_tp1, q, *args):
# Unpack model variables
cmk_t, imk_t, gk_t, Z_t, Y_t = Var_t.ravel()
cmk_tp1, imk_tp1, gk_tp1, Z_tp1, Y_tp1 = Var_tp1.ravel()
# Compute log consumption growth
gc_tp1 = cmk_tp1 + gk_tp1 - cmk_t
return gc_tp1
def gk_onecap_3d(Var_t, Var_tp1, W_tp1, q, *args):
# Unpack model variables
cmk_t, imk_t, gk_t, Z_t, Y_t = Var_t.ravel()
cmk_tp1, imk_tp1, gk_tp1, Z_tp1, Y_tp1 = Var_tp1.ravel()
return gk_tp1
def cmk_onecap_3d(Var_t, Var_tp1, W_tp1, q, *args):
# Unpack model variables
cmk_t, imk_t, gk_t, Z_t, Y_t = Var_t.ravel()
cmk_tp1, imk_tp1, gk_tp1, Z_tp1, Y_tp1 = Var_tp1.ravel()
return cmk_t
2.7 Computing ModelSol #
Now we are able to use steps 2.1 to 2.5 as inputs to the uncertain_expansion
function. The solution is stored in a class named ModelSolution
(Please refer to uncertainexpansion.ipynb for details.) under the Linear Quadratic Framework.
See src/uncertain_expansion.py for the source code of the expansion suite.
ModelSol['JX1_t'].coeffs
{'c': array([[-0.00000928],
[ 0.00000359],
[ 0. ],
[ 0. ],
[ 0. ]]),
'x': array([[ 0. , 0.17006158, -0. ],
[-0. , -0.0658057 , -0. ],
[ 1. , 0. , 0. ],
[ 0. , 1. , 0. ],
[ 0. , 0. , 1. ]])}
Show code cell content
# Solve the One-Capital Adjustment Model
eq = eq_onecap_3d
ss = ss_onecap_3d
gc = gc_onecap_3d
gk = gk_onecap_3d
cmk_app = cmk_onecap_3d
approach = '1' # See Exploring Recursive Utility Appendix for difference about approach '1' and '2'
ModelSol = uncertain_expansion(eq, ss, var_shape, args, gc, gk, cmk_app, approach = '1')
Iteration 1: error = 0.170925932
Iteration 2: error = 0.00105362328
Iteration 3: error = 4.51132298e-06
Iteration 4: error = 2.31767169e-08
Iteration 5: error = 1.19069643e-10
3 Outputs#
3.1 List of Outputs #
We now examine the contents of ModelSol
, which contains the attributes listed below. Each approximation is stored in a class LinQuadVar
, which contains the coefficients for \(X_{1,t}, X_{2,t}, X_{1,t}'X_{1,t}, W_{t+\epsilon}, W_{t+\epsilon}'W_{t+\epsilon}, X_{1,t}'W_{t+\epsilon}\) and the constant.
Input |
Type |
Description |
---|---|---|
|
LinQuadVar |
Approximation of jump and state variables at time \(t\). Replace |
|
LinQuadVar |
Same as |
|
LinQuadVar |
Same as |
|
LinQuadVar |
Same as |
|
dict |
Dictionary containing solutions of the continuation values, including \(\mu^0, \Upsilon_0^2, \Upsilon_1^2,\) and \(\Upsilon_2^2\) etc. |
|
LinQuadVar |
Approximation of continuation values \(\widehat{V}^1_{t+\epsilon}-\widehat{R}^1_t\) . Replace |
|
LinQuadVar |
Approximation of consumption growth \(\widehat{C}_{t+\epsilon}-\widehat{C}_t\) . Replace |
|
dict |
Steady states for state and jump variables |
|
LinQuadVar |
Approximation for the log change of measure |
For example, we can obtain the coefficients for the first-order contribution of \(\log{C_t/K_t}\) in the following way, noting that cmk_t
was listed as the first jump variable when we specified the equilibrum conditions.
## Log consumption over capital approximation results, shown in the LinQuadVar coefficients form
ModelSol['JX1_t'][0].coeffs
{'c': array([[-0.00000928]]),
'x': array([[ 0. , 0.17006158, -0. ]])}
We can also display the full second-order approximation of \(\log{C_t/K_t}\) using the disp
function, which renders a LinQuadVar
object in LATEX form.
## Log consumption over capital approximation results, shown in the Latex analytical form
disp(ModelSol['JX_t'][0],'\\log\\frac{C_t}{K_t}')
Another example:
## Log capital growth process second order approximation results
disp(ModelSol['X2_tp1'][0],'\\log\\frac{K_{t+\epsilon}^2}{K_t^2}')
3.3 Simulate Variables #
Given a series of shock processes, we can simulate the path of our state and jump variables using the ModelSolution.simulate
method. Here, we simulate 400 periods of i.i.d standard multivariate normal shocks.
Ws = np.random.multivariate_normal(np.zeros(n_W),np.eye(n_W),size = 400)
JX_sim = ModelSol.simulate(Ws)
4 Using LinQuadVar
in Computation #
In the previous section, we saw how to use uncertain_expansion
to approximate variables and store their coefficients as LinQuadVar
objects. In this section, we explore how to manipulate LinQuadVar
objects for different uses.
To aid our examples, we first extract the steady states for the state evolution processes from the previous model solution:
See src/lin_quad.py for source code of LinQuadVar
definition.
ModelSol['ss']
array([ -3.66035793, -2.71090383, 0.01330655, 0. , -11.97496092])
n_J, n_X, n_W = ModelSol['var_shape']
X0_tp1 = LinQuadVar({'c':np.array([[ModelSol['ss'][0]],[ModelSol['ss'][1]],[ModelSol['ss'][2]]])}, shape = (n_X, n_X, n_W))
4.1 LinQuadVar
Operations #
We can sum multiple LinQuads together in two different ways. Here we demonstrate this with an example by summing the zeroth, first and second order contributions of our approximation for capital growth.
gk_tp1 = X0_tp1[0] + ModelSol['X1_tp1'][0] + 0.5 * ModelSol['X2_tp1'][0]
disp(gk_tp1,'\\log\\frac{K_{t+\epsilon}}{K_t}')
In the next example, we sum together the contributions for both capital growth and technology:
lq_sum([X0_tp1, ModelSol['X1_tp1'], 0.5 * ModelSol['X2_tp1']]).coeffs
{'x2': array([[-0. , 0.0185721, 0. ],
[ 0. , 0.472 , 0. ],
[ 0. , 0. , 0.403 ]]),
'x': array([[-0. , 0.03714422, 0.00000001],
[ 0. , 0.944 , 0. ],
[ 0. , 0. , 0.806 ]]),
'c': array([[-3.66037866],
[-2.71090383],
[-0.07821726]]),
'xw': array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0.00399962, 0.00173897, 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.02478025, 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , -0.21392032]]),
'w': array([[0.00799924, 0.00347793, 0. ],
[0. , 0.04956051, 0. ],
[0. , 0. , 0.42784065]]),
'xx': array([[ 0. , -0. , 0. , -0. , -0.00021926, 0. , 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.097 ]])}
4.2 LinQuadVar
Split and Concat #
split
breaks multiple dimensional LinQuad into one-dimensional LinQuads, while concat
does the inverse.
gk_tp1, Z_tp1, Y_tp1 = ModelSol['X1_tp1'].split()
concat([gk_tp1, Z_tp1, Y_tp1])
<lin_quad.LinQuadVar at 0x314574c50>
4.3 Evaluate a LinQuadVar
#
We can evaluate a LinQuad at specific state \((X_{t},W_{t+\epsilon})\) in time. As an example, we evaluate all 5 variables under steady state with a multivariate random normal shock.
x1 = np.zeros([n_X ,1])
x2 = np.zeros([n_X ,1])
w = np.random.multivariate_normal(np.zeros(n_W),np.eye(n_W),size = 1).T
ModelSol['JX_tp1'](*(x1,x2,w))
array([[ -3.66764679],
[ -2.70809391],
[ 0.00945342],
[ -0.03677232],
[-12.41358991]])
4.4 Next period expression for LinQuadVar
#
ModelSol
allows us to express a jump variable \(J_t\) as a function of \(t\) state and shock variables. Suppose we would like to compute its next period expression \(J_{t+\epsilon}\) with shocks. The function next_period
expresses \(J_{t+\epsilon}\) in terms of \(t\) state variables and \(t+\epsilon\) shock variables. For example, we can express the \(t+\epsilon\) expression for the first-order contribution to consumption over capital as:
cmk1_tp1 = next_period(ModelSol['J1_t'][0], ModelSol['X1_tp1'])
disp(cmk1_tp1, '\\log\\frac{C_{t+\epsilon}^1}{K_{t+\epsilon}^1}')
cmk2_tp1 = next_period(ModelSol['J2_t'][0], ModelSol['X1_tp1'], ModelSol['X2_tp1'])
disp(cmk2_tp1, '\\log\\frac{C_{t+\epsilon}^2}{K_{t+\epsilon}^2}')
4.6 Compute the Expectation of time \(t+\epsilon\) LinQuadVar
#
Suppose the distribution of shocks has a constant mean and variance (not state dependent). Then, we can use the E
function to compute the expectation of a time \(t+\epsilon\) LinQuadVar
as follows:
E_w = ModelSol['util_sol']['μ_0']
cov_w = np.eye(n_W)
E_ww = cal_E_ww(E_w, cov_w)
E_cmk2_tp1 = E(cmk2_tp1, E_w, E_ww)
disp(E_cmk2_tp1, '\mathbb{E}[\\log\\frac{C_{t+\epsilon}^2}{K_{t+\epsilon}^2}|\mathfrak{F_t}]')
Suppose the distribution of shock has a state-dependent mean and variance (implied by \(\tilde{N}_{t+\epsilon}\) shown in the notes), we can use E_N_tp1
and N_tilde_measure
to compute the expectation of time \(t+\epsilon\) LinQuadVar
.
N_cm = N_tilde_measure(ModelSol['util_sol']['log_N_tilde'],(1,n_X,n_W))
E_cmk2_tp1_tilde = E_N_tp1(cmk2_tp1, N_cm)
disp(E_cmk2_tp1_tilde, '\mathbb{\\tilde{E}}[\\log\\frac{C_{t+\epsilon}^2}{K_{t+\epsilon}^2}|\mathfrak{F_t}]')