Source code for ogcore.SS

# imports
import numpy as np
import scipy.optimize as opt
from ogcore import tax, pensions, household, firm, utils, fiscal
from ogcore import aggregates as aggr
from ogcore.constants import SHOW_RUNTIME, DEV_FACTOR_LIST
import os
import warnings
import logging

logger = logging.getLogger(__name__)

if not SHOW_RUNTIME:
    warnings.simplefilter("ignore", RuntimeWarning)

"""
Set minimizer tolerance
"""
MINIMIZER_TOL = 1e-13

"""
Set flag for enforcement of solution check
"""
ENFORCE_SOLUTION_CHECKS = True


"""
------------------------------------------------------------------------
    Define Functions
------------------------------------------------------------------------
"""


[docs] def euler_equation_solver(guesses, *args): """ Finds the euler errors for certain b and n, one ability type at a time. Args: guesses (Numpy array): initial guesses for b and n, length 2S args (tuple): tuple of arguments (r, w, p_tilde, p_i, bq, TR, factor, j, p) r (scalar): real interest rate w (scalar): real wage rate p_tilde (scalar): composite good price bq (Numpy array): bequest amounts by age, length S rm (scalar): remittance amounts by age, length S tr (scalar): government transfer amount by age, length S ubi (vector): universal basic income (UBI) payment, length S factor (scalar): scaling factor converting model units to dollars p (OG-Core Specifications object): model parameters Returns: errros (Numpy array): errors from FOCs, length 2S """ r, w, p_tilde, p_i, bq, rm, tr, ubi, factor, j, p = args b_guess = np.array(guesses[: p.S]) n_guess = np.array(guesses[p.S :]) b_s = np.array([0] + list(b_guess[:-1])) b_splus1 = b_guess theta = pensions.replacement_rate_vals(n_guess, w, factor, j, p) error1 = household.FOC_savings( r, w, p_tilde, p_i, b_s, b_splus1, n_guess, bq, rm, factor, tr, ubi, theta, p.rho[-1, :, j], p.etr_params[-1], p.mtry_params[-1], None, j, p, "SS", ) error2 = household.FOC_labor( r, w, p_tilde, p_i, b_s, b_splus1, n_guess, bq, rm, factor, tr, ubi, theta, p.chi_n[-1, :], p.etr_params[-1], p.mtrx_params[-1], None, j, p, "SS", ) # Put in constraints for consumption and savings. # According to the euler equations, they can be negative. When # Chi_b is large, they will be. This prevents that from happening. # I'm not sure if the constraints are needed for labor. # But we might as well put them in for now. mask1 = n_guess < 0 mask2 = n_guess > p.ltilde mask3 = b_guess <= 0 mask4 = np.isnan(n_guess) mask5 = np.isnan(b_guess) error2[mask1] = 1e14 error2[mask2] = 1e14 error1[mask3] = 1e14 error1[mask5] = 1e14 error2[mask4] = 1e14 taxes = tax.net_taxes( r, w, b_s, n_guess, bq, factor, tr, ubi, theta, None, j, False, "SS", p.e[-1, :, j], p.etr_params[-1], p, ) cons = household.get_cons( r, w, p_tilde, p_i, b_s, b_splus1, n_guess, bq, rm, taxes, p.e[-1, :, j], p.tau_c[-1, :], p, ) mask6 = cons < 0 error1[mask6] = 1e14 errors = np.hstack((error1, error2)) return errors
[docs] def solve_for_j( guesses, r_p, w, p_tilde, p_i, bq_j, rm_j, tr_j, ubi_j, factor, j, p_future, ): """ Solves the household's optimization problem for a given type j. Args: guesses (Numpy array): initial guesses for b and n, length 2S r_p (scalar): return on household investment portfolio w (scalar): real wage rate p_tilde (scalar): composite good price p_i (Numpy array): prices for consumption good i bq_j (Numpy array): bequest amounts by age, length S rm_j (Numpy array): remittance amounts by age, length S tr_j (Numpy array): government transfer amount by age, length S ubi_j (vector): universal basic income (UBI) payment, length S factor (scalar): scaling factor converting model units to dollars j (int): household type index p_future (OG-Core Specifications object): future model parameters Returns: root (OptimizeResult): the optimization result """ # scattered_p is either the original object (serial case) # or a Future pointing to it (distributed case) return opt.root( euler_equation_solver, guesses * 0.9, args=( r_p, w, p_tilde, p_i, bq_j, rm_j, tr_j, ubi_j, factor, j, p_future, ), method=p_future.FOC_root_method, tol=MINIMIZER_TOL, )
[docs] def inner_loop(outer_loop_vars, p, client): """ This function solves for the inner loop of the SS. That is, given the guesses of the outer loop variables (r, w, TR, factor) this function solves the households' problems in the SS. Args: outer_loop_vars (tuple): tuple of outer loop variables, (bssmat, nssmat, r_p, r, w, p_m, BQ, RM, TR, factor) or (bssmat, nssmat, r_p, r, w, p_m, BQ, RM, Y, TR, factor) bssmat (Numpy array): initial guess at savings, size = SxJ nssmat (Numpy array): initial guess at labor supply, size = SxJ r_p (scalar): return on household investment portfolio r (scalar): real interest rate w (scalar): real wage rate p_m (array_like): production goods prices BQ (array_like): aggregate bequest amount(s) TR (scalar): lump sum transfer amount Y (scalar): real GDP factor (scalar): scaling factor converting model units to dollars p (OG-Core Specifications object): model parameters client (Dask client object): client Returns: (tuple): results from household solution: * euler_errors (Numpy array): errors terms from FOCs, size = 2SxJ * bssmat (Numpy array): savings, size = SxJ * nssmat (Numpy array): labor supply, size = SxJ * new_r (scalar): real interest rate on firm capital * new_r_gov (scalar): real interest rate on government debt * new_r_p (scalar): real interest rate on household portfolio * new_w (scalar): real wage rate * new_p_i (array_like): good prices * K_vec (array_like): capital demand for each industry * L_vec (array_like): labor demand for each industry * Y_vec (array_like): output from each industry * new_TR (scalar): lump sum transfer amount * new_Y (scalar): real GDP * new_factor (scalar): scaling factor converting model units to dollars * new_BQ (array_like): aggregate bequest amount(s) * average_income_model (scalar): average income in model units """ # unpack variables to pass to function bssmat, nssmat, r_p, r, w, p_m, Y, BQ, TR, Ig_baseline, factor = ( outer_loop_vars ) p_m = np.array(p_m) # TODO: why is this a list otherwise? p_i = np.dot(p.io_matrix, p_m) BQ = np.array(BQ) RM = np.array(aggr.get_RM(Y, p, "SS")) # initialize array for euler errors euler_errors = np.zeros((2 * p.S, p.J)) p_tilde = aggr.get_ptilde(p_i, p.tau_c[-1, :], p.alpha_c) bq = household.get_bq(BQ, None, p, "SS") rm = household.get_rm(RM, None, p, "SS") tr = household.get_tr(TR, None, p, "SS") ubi = p.ubi_nom_array[-1, :, :] / factor results = [] # from dask.base import dask_sizeof if client: # Before scattering, temporarily remove unpicklable schema objects schema_backup = {} for attr in ["_defaults_schema", "_validator_schema", "sel"]: if hasattr(p, attr): schema_backup[attr] = getattr(p, attr) try: delattr(p, attr) except Exception: pass # Scatter the parameters scattered_p_future = client.scatter(p, broadcast=True) # Restore the schema objects (they're not needed by workers anyway) for attr, value in schema_backup.items(): try: setattr(p, attr, value) except Exception: pass # Launch in parallel with submit (or map) futures = [] for j in range(p.J): guesses = np.append(bssmat[:, j], nssmat[:, j]) f = client.submit( solve_for_j, guesses, r_p, w, p_tilde, p_i, bq[:, j], rm[:, j], tr[:, j], ubi[:, j], factor, j, scattered_p_future, ) futures.append(f) try: results = client.gather(futures) except Exception as e: # Cancel remaining futures and fall back to serial computation logger.warning( f"Dask computation failed with error: {e}. " "Falling back to serial computation." ) for f in futures: f.cancel() # Re-run serially results = [] for j in range(p.J): guesses = np.append(bssmat[:, j], nssmat[:, j]) res = solve_for_j( guesses, r_p, w, p_tilde, p_i, bq[:, j], rm[:, j], tr[:, j], ubi[:, j], factor, j, p, # pass the raw object directly ) results.append(res) else: # Serial fallback (no dask client) for j in range(p.J): guesses = np.append(bssmat[:, j], nssmat[:, j]) res = solve_for_j( guesses, r_p, w, p_tilde, p_i, bq[:, j], rm[:, j], tr[:, j], ubi[:, j], factor, j, p, # pass the raw object directly ) results.append(res) for j, result in enumerate(results): euler_errors[:, j] = result.fun bssmat[:, j] = result.x[: p.S] nssmat[:, j] = result.x[p.S :] b_splus1 = bssmat b_s = np.array(list(np.zeros(p.J).reshape(1, p.J)) + list(bssmat[:-1, :])) theta = pensions.replacement_rate_vals(nssmat, w, factor, None, p) num_params = len(p.etr_params[-1][0]) etr_params_3D = [ [ [p.etr_params[-1][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] net_tax = tax.net_taxes( r_p, w, b_s, nssmat, bq, factor, tr, ubi, theta, None, None, False, "SS", np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, p, ) c_s = household.get_cons( r_p, w, p_tilde, p_i, b_s, b_splus1, nssmat, bq, rm, net_tax, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), p.tau_c[-1, :], p, ) c_i = household.get_ci( c_s, p_i, p_tilde, p.tau_c[-1, :], p.alpha_c, p.c_min ) L = aggr.get_L(nssmat, p, "SS") B = aggr.get_B(bssmat, p, "SS", False) # Find gov't debt r_gov = fiscal.get_r_gov(r, p.debt_ratio_ss, p, "scalar", t=-1) D, D_d, D_f, new_borrowing, _, new_borrowing_f = fiscal.get_D_ss( r_gov, Y, p ) I_g = fiscal.get_I_g(Y, Ig_baseline, p, "SS") K_g = fiscal.get_K_g(0, I_g, p, "SS") # Find wage rate consistent with open economy interest rate # this is an approximation - assumes only KL in rest of world # production function w_open = firm.get_w_from_r(p.world_int_rate[-1], p, "SS") # Find output, labor demand, capital demand for M-1 industries L_vec = np.zeros(p.M) K_vec = np.zeros(p.M) C_vec = np.zeros(p.I) K_demand_open_vec = np.zeros(p.M) for i_ind in range(p.I): C_vec[i_ind] = aggr.get_C(c_i[i_ind, :, :], p, "SS").item() Y_vec = np.dot(p.io_matrix.T, C_vec) for m_ind in range(p.M - 1): KYrat_m = firm.get_KY_ratio(r, p_m, p, "SS", m_ind) K_vec[m_ind] = KYrat_m * Y_vec[m_ind] L_vec[m_ind] = firm.solve_L( Y_vec[m_ind], K_vec[m_ind], K_g, p, "SS", m_ind ) K_demand_open_vec[m_ind] = firm.get_K( p.world_int_rate[-1], w_open, L_vec[m_ind], p, "SS", m_ind ) # Find output, labor demand, capital demand for industry M L_M = max(0.001, L - L_vec.sum()) # make sure L_M > 0 K_demand_open_vec[-1] = firm.get_K( p.world_int_rate[-1], w_open, L_M, p, "SS", -1 ) K, K_d, K_f = aggr.get_K_splits( B, K_demand_open_vec.sum(), D_d, p.zeta_K[-1] ) K_M = max(0.001, K - K_vec.sum()) # make sure K_M > 0 L_vec[-1] = L_M K_vec[-1] = K_M Y_vec[-1] = firm.get_Y(K_vec[-1], K_g, L_vec[-1], p, "SS", -1) # Find GDP Y = (p_m * Y_vec).sum() I_g = fiscal.get_I_g(Y, Ig_baseline, p, "SS") K_g = fiscal.get_K_g(0, I_g, p, "SS") if p.zeta_K[-1] == 1.0: new_r = p.world_int_rate[-1] else: new_r = firm.get_r(Y_vec[-1], K_vec[-1], p_m, p, "SS", -1) new_w = firm.get_w(Y_vec[-1], L_vec[-1], p_m, p, "SS") new_r_gov = fiscal.get_r_gov(new_r, p.debt_ratio_ss, p, "scalar", t=-1) # now get accurate measure of debt service cost ( D, D_d, D_f, new_borrowing, debt_service, new_borrowing_f, ) = fiscal.get_D_ss(new_r_gov, Y, p) MPKg_vec = np.zeros(p.M) for m in range(p.M): MPKg_vec[m] = firm.get_MPx(Y_vec[m], K_g, p.gamma_g[m], p, "SS", m) new_r_p = aggr.get_r_p( new_r, new_r_gov, p_m, K_vec, K_g, D, MPKg_vec, p, "SS" ) # NOTE: avoid np.squeeze on p.e[-1, :, :] — for J=1 it collapses the J # axis to a 1-D array, and the subsequent multiplication against the # (S, J) nssmat broadcasts into an (S, S) outer product, scaling the # income sum by S. p.e[-1, :, :] is already (S, J). average_income_model = ( (new_r_p * b_s + new_w * p.e[-1, :, :] * nssmat) * p.omega_SS ).sum() if p.baseline: new_factor = p.mean_income_data / average_income_model else: new_factor = factor new_BQ = aggr.get_BQ(new_r_p, bssmat, None, p, "SS", False) new_bq = household.get_bq(new_BQ, None, p, "SS") new_RM = aggr.get_RM(Y, p, "SS") new_rm = household.get_rm(new_RM, None, p, "SS") tr = household.get_tr(TR, None, p, "SS") theta = pensions.replacement_rate_vals(nssmat, new_w, new_factor, None, p) # Find updated goods prices new_p_m = firm.get_pm(new_w, Y_vec, L_vec, p, "SS") new_p_m = new_p_m / new_p_m[-1] # normalize prices by industry M new_p_i = np.dot(p.io_matrix, new_p_m) new_p_tilde = aggr.get_ptilde(new_p_i, p.tau_c[-1, :], p.alpha_c) num_params = len(p.etr_params[-1][0]) etr_params_3D = [ [ [p.etr_params[-1][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] taxss = tax.net_taxes( new_r_p, new_w, b_s, nssmat, new_bq, factor, tr, ubi, theta, None, None, False, "SS", np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, p, ) cssmat = household.get_cons( new_r_p, new_w, new_p_tilde, new_p_i, b_s, bssmat, nssmat, new_bq, new_rm, taxss, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), p.tau_c[-1, :], p, ) ( total_tax_revenue, _, agg_pension_outlays, UBI_outlays, _, _, _, _, _, _, ) = aggr.revenue( new_r_p, new_w, b_s, nssmat, new_bq, c_i, Y_vec, L_vec, K_vec, new_p_m, factor, ubi, theta, etr_params_3D, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), p, None, "SS", ) G = fiscal.get_G_ss( Y, total_tax_revenue, agg_pension_outlays, TR, UBI_outlays, I_g, new_borrowing, debt_service, p, ) new_TR = fiscal.get_TR( Y, TR, G, total_tax_revenue, agg_pension_outlays, UBI_outlays, I_g, p, "SS", ) G_vec = np.zeros(p.M) G_vec[-1] = G C_m_vec = np.dot(p.io_matrix.T, C_vec) I_d_vec = np.zeros(p.M) I_d = aggr.get_I(b_splus1, K_d, K_d, p, "SS") I_d_vec[-1] = I_d I_g_vec = np.zeros(p.M) I_g_vec[-1] = I_g debt_service_f = fiscal.get_debt_service_f(r_p, D_f) net_capital_outflows = aggr.get_capital_outflows( r_p, K_f, new_borrowing_f, debt_service_f, p ) net_capital_outflows_vec = np.zeros(p.M) net_capital_outflows_vec[-1] = net_capital_outflows rc_error = ( Y_vec - C_m_vec - G_vec - I_d_vec - I_g_vec - net_capital_outflows_vec ) return ( euler_errors, bssmat, nssmat, new_r, new_r_gov, new_r_p, new_w, new_p_m, K_vec, L_vec, Y_vec, new_RM, new_TR, Y, new_factor, new_BQ, average_income_model, )
[docs] def SS_solver( bmat, nmat, r_p, r, w, p_m, Y, BQ, TR, Ig_baseline, factor, p, client, fsolve_flag=False, ): """ Solves for the steady state distribution of capital, labor, as well as w, r, TR and the scaling factor, using functional iteration. Args: bmat (Numpy array): initial guess at savings, size = SxJ nmat (Numpy array): initial guess at labor supply, size = SxJ r_p (scalar): return on household investment portfolio r (scalar): real interest rate w (scalar): real wage rate p_m (array_like): good prices Y (scalar): real GDP BQ (array_like): aggregate bequest amount(s) TR (scalar): lump sum transfer amount factor (scalar): scaling factor converting model units to dollars p (OG-Core Specifications object): model parameters client (Dask client object): client Returns: output (dictionary): dictionary with steady state solution results """ dist = 10 iteration = 0 dist_vec = np.zeros(p.maxiter) maxiter_ss = p.maxiter nu_ss = p.nu if fsolve_flag: # case where already solved via SS_fsolve maxiter_ss = 1 if p.baseline_spending: TR_baseline = p.alpha_bs_T[-1] * TR if not p.budget_balance and not p.baseline_spending: Y = TR / p.alpha_T[-1] while (dist > p.mindist_SS) and (iteration < maxiter_ss): # Solve for the steady state levels of b and n, given w, r, # Y, BQ, TR, and factor if p.baseline_spending: TR = p.alpha_bs_T[-1] * TR_baseline if not p.budget_balance and not p.baseline_spending: Y = TR / p.alpha_T[-1] outer_loop_vars = ( bmat, nmat, r_p, r, w, p_m, Y, BQ, TR, Ig_baseline, factor, ) ( euler_errors, new_bmat, new_nmat, new_r, new_r_gov, new_r_p, new_w, new_p_m, new_K_vec, new_L_vec, new_Y_vec, new_RM, new_TR, new_Y, new_factor, new_BQ, average_income_model, ) = inner_loop(outer_loop_vars, p, client) # update guesses for next iteration bmat = utils.convex_combo(new_bmat, bmat, nu_ss) nmat = utils.convex_combo(new_nmat, nmat, nu_ss) r_p = utils.convex_combo(new_r_p, r_p, nu_ss) r = utils.convex_combo(new_r, r, nu_ss) w = utils.convex_combo(new_w, w, nu_ss) p_m = utils.convex_combo(new_p_m, p_m, nu_ss) factor = utils.convex_combo(new_factor, factor, nu_ss) BQ = utils.convex_combo(new_BQ, BQ, nu_ss) if p.baseline_spending: Y = utils.convex_combo(new_Y, Y, nu_ss) if Y != 0: dist = np.array( [utils.pct_diff_func(new_r, r)] + [utils.pct_diff_func(new_r_p, r_p)] + [utils.pct_diff_func(new_w, w)] + list(utils.pct_diff_func(new_p_m, p_m)) + list(utils.pct_diff_func(new_BQ, BQ)) + [utils.pct_diff_func(new_Y, Y)] + [utils.pct_diff_func(new_factor, factor)] ).max() else: # If Y is zero (if there is no output), a percent difference # will throw NaN's, so we use an absolute difference dist = np.array( [utils.pct_diff_func(new_r, r)] + [utils.pct_diff_func(new_r_p, r_p)] + [utils.pct_diff_func(new_w, w)] + list(utils.pct_diff_func(new_p_m, p_m)) + list(utils.pct_diff_func(new_BQ, BQ)) + [abs(new_Y - Y)] + [utils.pct_diff_func(new_factor, factor)] ).max() else: if p.baseline_spending: TR = p.alpha_bs_T[-1] * TR_baseline else: TR = utils.convex_combo(new_TR, TR, nu_ss) dist = np.array( [float(utils.pct_diff_func(new_r, r))] + [float(utils.pct_diff_func(new_r_p, r_p))] + [float(utils.pct_diff_func(new_w, w))] + list(utils.pct_diff_func(new_p_m, p_m)) + list(utils.pct_diff_func(new_BQ, BQ)) + [float(utils.pct_diff_func(new_TR, TR))] + [float(utils.pct_diff_func(new_factor, factor))] ).max() dist_vec[iteration] = dist # Similar to TPI: if the distance between iterations increases, then # decrease the value of nu to prevent cycling if iteration > 10: if dist_vec[iteration] - dist_vec[iteration - 1] > 0: nu_ss /= 2.0 logger.info(f"New value of nu: {nu_ss}") iteration += 1 logger.info(f"Iteration: {iteration} Distance: {dist}") # Generate the SS values of variables, including euler errors bssmat_s = np.append(np.zeros((1, p.J)), bmat[:-1, :], axis=0) bssmat_splus1 = bmat nssmat = nmat rss = new_r wss = new_w K_vec_ss = new_K_vec L_vec_ss = new_L_vec Y_vec_ss = new_Y_vec r_gov_ss = fiscal.get_r_gov(rss, p.debt_ratio_ss, p, "scalar", t=-1) p_m_ss = new_p_m p_i_ss = np.dot(p.io_matrix, p_m_ss) p_tilde_ss = aggr.get_ptilde(p_i_ss, p.tau_c[-1, :], p.alpha_c) RM_ss = new_RM TR_ss = new_TR Yss = new_Y I_g_ss = fiscal.get_I_g(Yss, Ig_baseline, p, "SS") K_g_ss = fiscal.get_K_g(0, I_g_ss, p, "SS") Lss = aggr.get_L(nssmat, p, "SS") Bss = aggr.get_B(bssmat_splus1, p, "SS", False) ( Dss, D_d_ss, D_f_ss, new_borrowing, debt_service, new_borrowing_f, ) = fiscal.get_D_ss(r_gov_ss, Yss, p) logger.info(f"SS debt = {Dss}, {new_borrowing_f}") w_open = firm.get_w_from_r(p.world_int_rate[-1], p, "SS") K_demand_open_ss = np.zeros(p.M) for m in range(p.M): K_demand_open_ss[m] = firm.get_K( p.world_int_rate[-1], w_open, L_vec_ss[m], p, "SS", m ) Kss, K_d_ss, K_f_ss = aggr.get_K_splits( Bss, K_demand_open_ss.sum(), D_d_ss, p.zeta_K[-1] ) # Yss = firm.get_Y(Kss, K_g_ss, Lss, p, 'SS') I_g_ss = fiscal.get_I_g(Yss, Ig_baseline, p, "SS") K_g_ss = fiscal.get_K_g(0, I_g_ss, p, "SS") MPKg_vec = np.zeros(p.M) for m in range(p.M): MPKg_vec[m] = firm.get_MPx( Y_vec_ss[m], K_g_ss, p.gamma_g[m], p, "SS", m ) # r_p_ss = aggr.get_r_p( # rss, r_gov_ss, p_m_ss, K_vec_ss, K_g_ss, Dss, MPKg_vec, p, "SS" # ) r_p_ss = new_r_p # Note that implicitly in this computation is that immigrants' # wealth is all in the form of private capital I_d_ss = aggr.get_I(bssmat_splus1, K_d_ss, K_d_ss, p, "SS") Iss = aggr.get_I(bssmat_splus1, Kss, Kss, p, "SS") BQss = new_BQ factor_ss = factor bqssmat = household.get_bq(BQss, None, p, "SS") trssmat = household.get_tr(TR_ss, None, p, "SS") rmssmat = household.get_rm(RM_ss, None, p, "SS") ubissmat = p.ubi_nom_array[-1, :, :] / factor_ss theta = pensions.replacement_rate_vals(nssmat, wss, factor_ss, None, p) # Compute effective and marginal tax rates for all agents num_params = len(p.etr_params[-1][0]) etr_params_3D = [ [ [p.etr_params[-1][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] mtrx_params_3D = [ [ [p.mtrx_params[-1][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] mtry_params_3D = [ [ [p.mtry_params[-1][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] labor_noncompliance_rate_2D = np.tile( np.reshape(p.labor_income_tax_noncompliance_rate[-1, :], (1, p.J)), (p.S, 1), ) capital_noncompliance_rate_2D = np.tile( np.reshape(p.capital_income_tax_noncompliance_rate[-1, :], (1, p.J)), (p.S, 1), ) income_tax_filer_2D = np.tile( np.reshape(p.income_tax_filer[-1, :], (1, p.J)), (p.S, 1), ) mtry_ss = tax.MTR_income( r_p_ss, wss, bssmat_s, nssmat, factor, True, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, mtry_params_3D, capital_noncompliance_rate_2D, income_tax_filer_2D, p, ) mtrx_ss = tax.MTR_income( r_p_ss, wss, bssmat_s, nssmat, factor, False, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, mtrx_params_3D, labor_noncompliance_rate_2D, income_tax_filer_2D, p, ) etr_ss = tax.ETR_income( r_p_ss, wss, bssmat_s, nssmat, factor, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, labor_noncompliance_rate_2D, capital_noncompliance_rate_2D, income_tax_filer_2D, p, ) taxss = tax.net_taxes( r_p_ss, wss, bssmat_s, nssmat, bqssmat, factor_ss, trssmat, ubissmat, theta, None, None, False, "SS", np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, p, ) income_tax_ss = tax.income_tax_liab( r_p_ss, wss, bssmat_s, nssmat, factor_ss, 0, None, "SS", np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), etr_params_3D, p, ) pension_ss = pensions.pension_amount( r_p_ss, wss, nssmat, 1, theta, 0, None, False, "SS", np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), factor_ss, p, ) bq_tax_ss = tax.bequest_tax_liab( r_p_ss, bssmat_s, bqssmat, 0, None, "SS", p ) wealth_tax_ss = tax.wealth_tax_liab(r_p_ss, bssmat_s, 0, None, "SS", p) cssmat = household.get_cons( r_p_ss, wss, p_tilde_ss, p_i_ss, bssmat_s, bssmat_splus1, nssmat, bqssmat, rmssmat, taxss, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), p.tau_c[-1, :], p, ) c_i = household.get_ci( cssmat, p_i_ss, p_tilde_ss, p.tau_c[-1, :], p.alpha_c, p.c_min, "SS", ) sales_tax_ss = tax.cons_tax_liab(c_i, p_i_ss, p, "SS") yss_before_tax_mat = household.get_y( r_p_ss, wss, bssmat_s, nssmat, p, "SS" ) labor_income_ss = ( wss * nssmat * np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)) ) capital_income_ss = r_p_ss * bssmat_s Css = aggr.get_C(cssmat, p, "SS") c_i_ss_mat = household.get_ci( cssmat, p_i_ss, p_tilde_ss, p.tau_c[-1, :], p.alpha_c, p.c_min ) C_vec_ss = np.zeros(p.I) for i_ind in range( p.I ): # TODO: update aggr.get_C to take full IxSxJ array C_vec_ss[i_ind] = aggr.get_C( c_i_ss_mat[i_ind, :, :], p, "SS", ).item() ( total_tax_revenue, iit_payroll_tax_revenue, agg_pension_outlays, UBI_outlays, bequest_tax_revenue, wealth_tax_revenue, cons_tax_revenue, business_tax_revenue, payroll_tax_revenue, iit_revenue, ) = aggr.revenue( r_p_ss, wss, bssmat_s, nssmat, bqssmat, c_i_ss_mat, Y_vec_ss, L_vec_ss, K_vec_ss, p_m, factor, ubissmat, theta, etr_params_3D, np.squeeze(p.e[-1, :, :]).reshape((p.S, p.J)), p, None, "SS", ) Gss = fiscal.get_G_ss( Yss, total_tax_revenue, agg_pension_outlays, TR_ss, UBI_outlays, I_g_ss, new_borrowing, debt_service, p, ) # Compute total investment (not just domestic) Iss_total = aggr.get_I(None, Kss, Kss, p, "total_ss") # solve resource constraint # net foreign borrowing debt_service_f = fiscal.get_debt_service_f(r_p_ss, D_f_ss) net_capital_outflows = aggr.get_capital_outflows( r_p_ss, K_f_ss, new_borrowing_f, debt_service_f, p ) # Fill in arrays, noting that M-1 industries only produce consumption goods G_vec_ss = np.zeros(p.M) # Map consumption goods back to demands for production goods logger.info(f"IO: {p.io_matrix.T.shape}, C: {C_vec_ss.shape}") C_m_vec_ss = np.dot(p.io_matrix.T, C_vec_ss) G_vec_ss[-1] = Gss I_d_vec_ss = np.zeros(p.M) I_d_vec_ss[-1] = I_d_ss I_g_vec_ss = np.zeros(p.M) I_g_vec_ss[-1] = I_g_ss net_capital_outflows_vec = np.zeros(p.M) net_capital_outflows_vec[-1] = net_capital_outflows RM_vec_ss = np.zeros(p.M) RM_vec_ss[-1] = RM_ss foreign_aid_vec_ss = np.zeros(p.M) foreign_aid_vec_ss[-1] = p.alpha_FA[-1] * Yss RC = aggr.resource_constraint( Y_vec_ss, C_m_vec_ss, G_vec_ss, I_d_vec_ss, I_g_vec_ss, net_capital_outflows_vec, RM_vec_ss, foreign_aid_vec_ss, ) logger.info(f"Foreign debt holdings = {D_f_ss}") logger.info(f"Foreign capital holdings = {K_f_ss}") logger.info(f"resource constraint: {RC}") if Gss < 0: logger.warning( "Steady state government spending is negative to satisfy" + " budget" ) if ENFORCE_SOLUTION_CHECKS and (max(np.absolute(RC)) > p.RC_SS): logger.warning(f"Resource Constraint Difference: {RC}") err = "Steady state aggregate resource constraint not satisfied" raise RuntimeError(err) # check constraints household.constraint_checker_SS(bssmat_splus1, nssmat, cssmat, p.ltilde) euler_savings = euler_errors[: p.S, :] euler_labor_leisure = euler_errors[p.S :, :] logger.info( "Maximum error in labor FOC = " f"{np.absolute(euler_labor_leisure).max()}" ) logger.info( f"Maximum error in savings FOC = {np.absolute(euler_savings).max()}" ) # Return dictionary of SS results output = { "Y": Yss, "B": Bss, "K": Kss, "K_f": K_f_ss, "K_d": K_d_ss, "L": Lss, "C": Css, "I": Iss, "I_total": Iss_total, "I_d": I_d_ss, "K_g": K_g_ss, "I_g": I_g_ss, "BQ": BQss, "RM": RM_ss, "Y_m": Y_vec_ss, "K_m": K_vec_ss, "L_m": L_vec_ss, "C_i": C_vec_ss, "TR": TR_ss, "agg_pension_outlays": agg_pension_outlays, "total_government_outlays": ( TR_ss + UBI_outlays + Gss + I_g_ss + debt_service + agg_pension_outlays ), "total_primary_government_outlays": ( agg_pension_outlays + TR_ss + UBI_outlays + Gss + I_g_ss ), "G": Gss, "UBI": UBI_outlays, "total_tax_revenue": total_tax_revenue, "business_tax_revenue": business_tax_revenue, "iit_payroll_tax_revenue": iit_payroll_tax_revenue, "iit_revenue": iit_revenue, "payroll_tax_revenue": payroll_tax_revenue, "bequest_tax_revenue": bequest_tax_revenue, "wealth_tax_revenue": wealth_tax_revenue, "cons_tax_revenue": cons_tax_revenue, "D": Dss, "D_f": D_f_ss, "D_d": D_d_ss, "new_borrowing": new_borrowing, "debt_service": debt_service, "new_borrowing_f": new_borrowing_f, "debt_service_f": debt_service_f, "r": rss, "r_gov": r_gov_ss, "r_p": r_p_ss, "w": wss, "p_m": p_m_ss, "p_i": p_i_ss, "p_tilde": p_tilde_ss, "b_sp1": bssmat_splus1, "b_s": bssmat_s, "n": nssmat, "c": cssmat, "c_i": c_i_ss_mat, "bq": bqssmat, "rm": rmssmat, "tr": trssmat, "ubi": ubissmat, "before_tax_income": yss_before_tax_mat, "labor_income": labor_income_ss, "capital_income": capital_income_ss, "hh_net_taxes": taxss, "income_payroll_taxes": income_tax_ss, "sales_tax": sales_tax_ss[: p.T, ...], "wealth_tax": wealth_tax_ss[: p.T, ...], "bequest_tax": bq_tax_ss[: p.T, ...], "pension_benefits": pension_ss[: p.T, ...], "etr": etr_ss, "mtrx": mtrx_ss, "mtry": mtry_ss, "theta": theta, "factor": factor_ss, "euler_savings": euler_savings, "euler_labor_leisure": euler_labor_leisure, "resource_constraint_error": RC, } return output
[docs] def SS_fsolve(guesses, *args): """ Solves for the steady state distribution of capital, labor, as well as w, r, TR and the scaling factor, using a root finder. Args: guesses (list): initial guesses outer loop variables (r_p, r, w, p_m, BQ, TR (or Y), factor) args (tuple): tuple of arguments (bssmat, nssmat, TR_ss, factor_ss, p, client) bssmat (Numpy array): initial guess at savings, size = SxJ nssmat (Numpy array): initial guess at labor supply, size = SxJ TR_ss (scalar): lump sum transfer amount factor_ss (scalar): scaling factor converting model units to dollars p (OG-Core Specifications object): model parameters client (Dask client object): client Returns: errors (list): errors from differences between guessed and implied outer loop variables """ bssmat, nssmat, TR_ss, Ig_baseline, factor_ss, p, client = args # Rename the inputs r_p = guesses[0] r = guesses[1] w = guesses[2] p_m = guesses[3 : 3 + p.M] Y = guesses[3 + p.M] if p.baseline: BQ = guesses[3 + p.M + 1 : -2] TR = guesses[-2] factor = guesses[-1] else: BQ = guesses[3 + p.M + 1 : -1] TR = guesses[-1] factor = factor_ss if p.baseline_spending: TR = TR_ss if not p.budget_balance and not p.baseline_spending: Y = TR / p.alpha_T[-1] outer_loop_vars = ( bssmat, nssmat, r_p, r, w, p_m, Y, BQ, TR, Ig_baseline, factor, ) # Solve for the steady state levels of b and n, given w, r, TR and # factor ( euler_errors, bssmat, nssmat, new_r, new_r_gov, new_r_p, new_w, new_p_m, new_K_vec, new_L_vec, new_Y_vec, new_RM, new_TR, new_Y, new_factor, new_BQ, average_income_model, ) = inner_loop(outer_loop_vars, p, client) # Create list of errors in general equilibrium variables error_r_p = float(new_r_p - r_p) # Check and punish violations of the bounds on the interest rate error_r = float(new_r - r) error_w = float(new_w - w) if new_r + p.delta <= 0: error_r_p = 1e9 error_r = 1e9 if new_w < 0: error_w = 1e9 error_p_m = new_p_m - p_m error_p_m[new_p_m < 0] = 1e9 error_Y = float(new_Y - Y) error_BQ = new_BQ - BQ error_TR = float(new_TR - TR) # divide factor by 1000000 to put on similar scale error_factor = float(new_factor / 1000000 - factor / 1000000) # Check and punish violations of the factor if new_factor <= 0: error_factor = 1e9 if p.baseline: errors = ( [error_r_p, error_r, error_w] + list(error_p_m) + [error_Y] + list(error_BQ) + [error_TR, error_factor] ) else: errors = ( [error_r_p, error_r, error_w] + list(error_p_m) + [error_Y] + list(error_BQ) + [error_TR] ) error_string = [f"{error:.3e}" for error in errors] logger.info(f"GE loop errors = {error_string}") return errors
[docs] def SS_initial_guesses(p, b_val=0.0055, n_val=0.4, r_tr_scalars=[1.0, 1.0]): """ Finds the initial guesses for b, n and for the steady state outer loop variables. Args: p (OG-Core Specifications object): model parameters b_val (float): initial guess value for savings n_val (float): initial guess value for labor supply r_tr_scalars (list): scalars to adjust initial guesses for r and TR Returns: guesses (list): initial guesses for outer loop variables b_guess (ndarray): initial guess for savings n_guess (ndarray): initial guess for labor supply """ r_p_guess = r_tr_scalars[0] * p.initial_guess_r_SS rguess = r_tr_scalars[0] * p.initial_guess_r_SS wguess = firm.get_w_from_r(rguess, p, "SS") p_m_guess = np.ones(p.M) TRguess = r_tr_scalars[1] * p.initial_guess_TR_SS Yguess = TRguess / p.alpha_T[-1] # create guesses list # Note that BQ is an vector of lenght J if use_zeta=False if p.use_zeta: b_guess = np.ones((p.S, p.J)) * b_val n_guess = np.ones((p.S, p.J)) * n_val * p.ltilde BQguess = 0.12231465279007188 else: b_guess = ( np.ones((p.S, p.J)) * 0.07 ) # TODO: remove hardcode here and next line n_guess = np.ones((p.S, p.J)) * 0.35 * p.ltilde BQguess = aggr.get_BQ(rguess, b_guess, None, p, "SS", False) # append factor guess if baseline BQ_items = [BQguess] if p.use_zeta else list(BQguess) guesses = ( [r_p_guess, rguess, wguess] + list(p_m_guess) + [Yguess] + BQ_items + [TRguess] ) if p.baseline: guesses.append(p.initial_guess_factor_SS) return guesses, b_guess, n_guess
[docs] def run_SS(p, client=None): """ Solve for steady-state equilibrium of OG-Core. Args: p (OG-Core Specifications object): model parameters client (Dask client object): client Returns: output (dictionary): dictionary with steady-state solution results """ # For initial guesses of w, r, TR, and factor, we use values that # are close to some steady state values. # Use the baseline solution to get starting values for the reform use_new_guesses = False # initialize this flag, switches to true # if baseline solution not work for reform if p.baseline is False: baseline_ss_path = os.path.join(p.baseline_dir, "SS", "SS_vars.pkl") ss_solutions = utils.safe_read_pickle(baseline_ss_path) if p.baseline is False and p.reform_use_baseline_solution: # use baseline solution as starting values if dimensions match try: if ss_solutions["b_sp1"].shape == ( p.S, p.J, ) and np.squeeze(ss_solutions["Y_m"].shape) == (p.M): logger.info("Using previous solutions for SS") ( b_guess, n_guess, r_p_guess, rguess, wguess, p_m_guess, BQguess, TRguess, Yguess, factor_ss, ) = ( ss_solutions["b_sp1"], ss_solutions["n"], float(ss_solutions["r_p"]), float(ss_solutions["r"]), float(ss_solutions["w"]), ss_solutions["p_m"], ss_solutions["BQ"], float(ss_solutions["TR"]), float(ss_solutions["Y"]), ss_solutions["factor"], ) # set new attribute of parameters for SS theta p.SS_theta = ss_solutions["theta"] use_new_guesses = False if p.baseline_spending: TR_baseline = TRguess Ig_baseline = ss_solutions["I_g"] else: TR_baseline = None Ig_baseline = None BQ_items = [BQguess] if p.use_zeta else list(BQguess) guesses = ( [r_p_guess, rguess, wguess] + list(p_m_guess) + [Yguess] + BQ_items + [TRguess] ) # Now solve for the steady state of the reform ss_params = ( b_guess, n_guess, TR_baseline, Ig_baseline, factor_ss, p, client, ) # Solve for steady state using root finder sol = opt.root( SS_fsolve, guesses, args=ss_params, method=p.SS_root_method, tol=p.mindist_SS, ) else: logger.warning( "Dimensions of previous solutions for SS do not match" ) use_new_guesses = True except KeyError: logger.warning("KeyError: previous solutions for SS not found") use_new_guesses = True if p.baseline or not p.reform_use_baseline_solution or use_new_guesses: # Loop over initial guesses of r and TR until find a solution # or until have gone through all guesses. This should usually # solve in the first guess SS_solved = False k = 0 while not SS_solved and k < len(DEV_FACTOR_LIST) - 1: for k, v in enumerate(DEV_FACTOR_LIST): logger.info( "SS using initial guess factors for r and TR of " + f"{v[0]} and {v[1]} respectively." ) guesses, b_guess, n_guess = SS_initial_guesses( p, r_tr_scalars=v ) ss_params = ( b_guess, n_guess, None, None, None, p, client, ) if p.baseline: factor_ss = None else: factor_ss = ss_solutions[ "factor" ] # don't guess factor, use baseline # set new attribute of parameters for SS theta p.SS_theta = ss_solutions["theta"] if p.baseline_spending: TR_baseline = ss_solutions["TR"] Ig_baseline = ss_solutions["I_g"] else: TR_baseline = None Ig_baseline = None ss_params = ( b_guess, n_guess, TR_baseline, Ig_baseline, factor_ss, p, client, ) # Solve for steady state using root finder sol = opt.root( SS_fsolve, guesses, args=ss_params, method=p.SS_root_method, tol=p.mindist_SS, ) if sol.success: SS_solved = True break r_p_ss = sol.x[0] rss = sol.x[1] wss = sol.x[2] p_m_ss = sol.x[3 : 3 + p.M] if p.baseline: BQss = sol.x[3 + p.M + 1 : -2] TR_ss = sol.x[-2] factor_ss = sol.x[-1] Yss = TR_ss / p.alpha_T[-1] # may not be right - if # budget_balance = True, but that's ok - will be fixed in # SS_solver else: Yss = sol.x[3 + p.M] BQss = sol.x[3 + p.M + 1 : -1] TR_ss = sol.x[-1] if not p.baseline_spending: Yss = TR_ss / p.alpha_T[-1] # may not be right - if # budget_balance = True, but that's ok - will be fixed in # SS_solver if ENFORCE_SOLUTION_CHECKS and not sol.success: raise RuntimeError("Steady state equilibrium not found") # Trigger flag that model has been solved fsolve_flag = True # Return SS values of variables if p.baseline or not p.baseline_spending: Ig_baseline = None output = SS_solver( b_guess, n_guess, r_p_ss, rss, wss, p_m_ss, Yss, BQss, TR_ss, Ig_baseline, factor_ss, p, client, fsolve_flag, ) if output["G"] < 0.0: warnings.warn( "Warning: The combination of the tax policy " + "you specified and your target debt-to-GDP " + "ratio results in an infeasible amount of " + "government spending in order to close the " + "budget (i.e., G < 0)" ) return output