Source code for ogcore.TPI

"""
Time path iteration (TPI) module for OG-Core.

This module contains the following functions:
    get_initial_SS_values()
    firstdoughnutring()
    twist_doughnut()
    inner_loop()
    run_TPI()
"""

# imports
import numpy as np
import pickle
import scipy.optimize as opt
from scipy.sparse import csr_matrix
from ogcore import tax, utils, household, firm, fiscal, pensions, solvers
from ogcore import aggregates as aggr
from ogcore.constants import SHOW_RUNTIME
import os
import warnings
import logging
from distributed import wait

# ``approx_derivative`` and ``group_columns`` live in a private scipy module
# (scipy.optimize._numdiff). Guard the import so a future scipy change disables
# the optional sparse-FOC-Jacobian path -- falling back to dense finite
# differences -- instead of breaking TPI entirely.
try:
    from scipy.optimize._numdiff import approx_derivative, group_columns

    _HAVE_SPARSE_FD = True
except Exception:  # pragma: no cover
    approx_derivative = None
    group_columns = None
    _HAVE_SPARSE_FD = False

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


_BANDED_JAC_CACHE = {}


def _detect_jac_sparsity(fun, x0, args, thr_rel=1e-9):
    """Auto-detect the Jacobian sparsity of `fun` near x0 and return a
    (csr_pattern, groups) coloring for sparse finite differences, or None if
    the Jacobian is too dense to benefit (caller then uses plain dense FD).

    One-time per problem dimension (cached). The pattern is unioned over two
    nearby points so a coupling that happens to vanish at one point is not
    missed. Portable across countries/calibrations/pensions: it reads the
    *actual* structure each run rather than assuming a stencil, so a
    history-dependent pension (dense columns) is detected automatically and
    falls back to dense finite differences.

    Args:
        fun (function): the residual function whose Jacobian is probed
        x0 (array_like): point at which to probe the sparsity
        args (tuple): extra arguments passed to ``fun``
        thr_rel (float): relative threshold below which an entry is treated
            as a structural zero

    Returns:
        result (tuple or None): ``(csr_pattern, groups)`` coloring for sparse
            finite differences, or None if the Jacobian is not sparse enough
            to benefit
    """
    n = len(x0)
    key = (getattr(fun, "__name__", repr(fun)), n)
    cached = _BANDED_JAC_CACHE.get(key, "miss")
    if cached != "miss":
        return cached
    result = None
    try:
        x = np.asarray(x0, dtype=float)
        mask = np.zeros((n, n), dtype=bool)
        for xp in (x, x * 1.05 + 0.01):
            Jd = approx_derivative(fun, xp, args=args, method="2-point")
            sc = max(float(np.max(np.abs(Jd))), 1e-300)
            mask |= np.abs(Jd) > thr_rel * sc
        P = csr_matrix(mask.astype(int))
        groups = group_columns(P)
        if int(groups.max()) + 1 < n // 2:
            result = (P, groups)
        # else: pattern not sparse enough to benefit (expected at small
        # firstdoughnutring dims); silently return None so the caller uses
        # dense FD without surfacing a noisy notice.
    except Exception:
        result = None
    _BANDED_JAC_CACHE[key] = result
    return result


def _banded_jac_for(fun, x0, args, enabled):
    """Return a sparse finite-difference `jac` callable for ``opt.root`` when
    ``enabled`` (``p.use_sparse_FOC_jac``) and the Jacobian is sparse enough to
    benefit; otherwise None, so the caller uses the default dense solver. Used
    at the household full-lifetime and triangle root-finds.

    Args:
        fun (function): the residual function passed to ``opt.root``
        x0 (array_like): initial guess for the root-find
        args (tuple): extra arguments passed to ``fun``
        enabled (bool): value of ``p.use_sparse_FOC_jac``

    Returns:
        _jac (function or None): a callable returning the dense Jacobian built
            by sparse finite differences, or None to use dense finite
            differences
    """
    if not enabled or not _HAVE_SPARSE_FD:
        return None
    bj = _detect_jac_sparsity(fun, x0, args)
    if bj is None:
        return None
    P, groups = bj

    def _jac(_x, *_a):
        return approx_derivative(
            fun, _x, args=_a, method="2-point", sparsity=(P, groups)
        ).toarray()

    return _jac


[docs] def get_initial_SS_values(p): """ Get values of variables for the initial period and the steady state equilibrium values. Args: p (OG-Core Specifications object): model parameters Returns: (tuple): initial period and steady state values: * initial_values (tuple): initial period variable values, (b_sinit, b_splus1init, factor, initial_b, initial_n) * ss_vars (dictionary): dictionary with steady state solution results * theta (Numpy array): steady-state retirement replacement rates, length J * baseline_values (tuple): (Ybaseline, TRbaseline, Gbaseline, D0_baseline), GDP, lump sum transfer, and government spending amounts from the baseline model run """ baseline_ss = os.path.join(p.baseline_dir, "SS", "SS_vars.pkl") ss_baseline_vars = utils.safe_read_pickle(baseline_ss) factor = ss_baseline_vars["factor"] B0 = aggr.get_B(ss_baseline_vars["b_sp1"], p, "SS", True) initial_b = ss_baseline_vars["b_sp1"] * (ss_baseline_vars["B"] / B0) initial_n = ss_baseline_vars["n"] # The DB/NDC/PS pension formulas need the labor supplied before the # time path begins by cohorts alive at t=0. Use the model's initial # labor condition (the same baseline object that initializes wealth); # pre-time-path wages are anchored to the period-0 wage of the # current path inside pensions.py. See Issue #1014 for a fully # history-consistent treatment. p.n_preTP = initial_n Ybaseline = None TRbaseline = None Gbaseline = None Ig_baseline = None if not p.baseline: baseline_tpi = os.path.join(p.baseline_dir, "TPI", "TPI_vars.pkl") tpi_baseline_vars = utils.safe_read_pickle(baseline_tpi) Ybaseline = tpi_baseline_vars["Y"] if p.baseline_spending: baseline_tpi = os.path.join(p.baseline_dir, "TPI", "TPI_vars.pkl") tpi_baseline_vars = utils.safe_read_pickle(baseline_tpi) TRbaseline = tpi_baseline_vars["TR"] Gbaseline = tpi_baseline_vars["G"] Ig_baseline = tpi_baseline_vars["I_g"] if p.baseline: ss_vars = ss_baseline_vars else: reform_ss_path = os.path.join(p.output_base, "SS", "SS_vars.pkl") ss_vars = utils.safe_read_pickle(reform_ss_path) theta = ss_vars["theta"] """ ------------------------------------------------------------------------ Set other parameters and initial values ------------------------------------------------------------------------ """ # Get an initial distribution of wealth with the initial population # distribution. When small_open=True, the value of K0 is used as a # placeholder for first-period wealth B0 = aggr.get_B(initial_b, p, "SS", True) b_sinit = np.array( list(np.zeros(p.J).reshape(1, p.J)) + list(initial_b[:-1]) ) b_splus1init = initial_b # Intial gov't debt and capital stock must match that in the baseline if not p.baseline: baseline_tpi = os.path.join(p.baseline_dir, "TPI", "TPI_vars.pkl") tpi_baseline_vars = utils.safe_read_pickle(baseline_tpi) D0_baseline = tpi_baseline_vars["D"][0] Kg0_baseline = tpi_baseline_vars["K_g"][0] else: RM0_baseline = None D0_baseline = None Kg0_baseline = None initial_values = (B0, b_sinit, b_splus1init, factor, initial_b, initial_n) baseline_values = ( Ybaseline, TRbaseline, Gbaseline, Ig_baseline, D0_baseline, Kg0_baseline, ) return initial_values, ss_vars, theta, baseline_values
[docs] def firstdoughnutring( guesses, r, w, p_tilde, p_i, bq, rm, tr, theta, factor, ubi, j, initial_b, p, ): """ Solves the first entries of the upper triangle of the twist doughnut. This is separate from the main TPI function because the values of b and n are scalars, so it is easier to just have a separate function for these cases. Args: guesses (Numpy array): initial guesses for b and n, length 2 r (scalar): real interest rate w (scalar): real wage rate p_tilde (scalar): composite good price p_i (Numpy array): output goods prices bq (scalar): bequest amounts by age rm (scalar): remittance amounts by age tr (scalar): government transfer amount theta (Numpy array): retirement replacement rates, length J factor (scalar): scaling factor converting model units to dollars ubi (scalar): individual UBI credit to household s=E+S of type j in period 0 j (int): index of ability type initial_b (Numpy array): SxJ matrix, savings of agents alive at T=0 p (OG-Core Specifications object): model parameters Returns: euler errors (Numpy array): errors from first order conditions, length 2 """ b_splus1 = float(guesses[0]) n = float(guesses[1]) b_s = float(initial_b[-2, j]) # Find errors from FOC for savings and FOC for labor supply error1 = household.FOC_savings( np.array([r]), np.array([w]), np.array([p_tilde]), np.array([p_i]), b_s, np.array([b_splus1]), np.array([n]), np.array([bq]), np.array([rm]), factor, np.array([tr]), np.array([ubi]), theta[j], p.rho[0, -1, j], p.etr_params[0][-1], p.mtry_params[0][-1], None, j, p, "TPI_scalar", ) error2 = household.FOC_labor( np.array([r]), np.array([w]), np.array([p_tilde]), np.array([p_i]), b_s, b_splus1, np.array([n]), np.array([bq]), np.array([rm]), factor, np.array([tr]), np.array([ubi]), theta[j], p.chi_n[0, -1], p.etr_params[0][-1], p.mtrx_params[0][-1], None, j, p, "TPI_scalar", ) if n <= 0 or n >= 1: error2 += 1e12 if b_splus1 <= 0: error1 += 1e12 return [np.squeeze(error1)] + [np.squeeze(error2)]
[docs] def twist_doughnut( guesses, r, w, p_tilde, p_i, bq, rm, tr, theta, factor, ubi, j, s, t, etr_params, mtrx_params, mtry_params, initial_b, p, ): """ Solves the upper triangle of time path iterations. These are the agents who are alive at time T=0 so that we do not solve for their full lifetime (so of their life was before the model begins). Args: guesses (list): initial guesses for b and n, length 2s r (Numpy array): real interest rate w (Numpy array): real wage rate p_tilde (Numpy array): composite good price p_i (Numpy array): output goods prices bq (Numpy array): bequest amounts by age, length s rm (Numpy array): remittance amounts by age, length s tr (Numpy array): government transfer amount theta (Numpy array): retirement replacement rates, length J factor (scalar): scaling factor converting model units to dollars ubi (Numpy array): length remaining periods of life UBI payout to household j (int): index of ability type s (int): years of life remaining t (int): model period etr_params (list): ETR function parameters, list of lists with size = sxsxnum_params mtrx_params (list): labor income MTR function parameters, list of lists with size = sxsxnum_params mtry_params (list): capital income MTR function parameters, lists of lists with size = sxsxnum_params initial_b (Numpy array): savings of agents alive at T=0, size = SxJ p (OG-Core Specifications object): model parameters Returns: euler errors (Numpy array): errors from first order conditions, length 2s """ length = int(len(guesses) / 2) b_guess = np.array(guesses[:length]) n_guess = np.array(guesses[length:]) if length == p.S: b_s = np.array([0] + list(b_guess[:-1])) else: b_s = np.array([initial_b[-(s + 3), j]] + list(b_guess[:-1])) b_splus1 = b_guess w_s = w[t : t + length] r_s = r[t : t + length] p_tilde_s = p_tilde[t : t + length] p_i_s = p_i[t : t + length, :] n_s = n_guess chi_n_s = np.diag(p.chi_n[t : t + p.S, :], max(p.S - length, 0)) rho_s = np.diag(p.rho[t : t + p.S, :, j], max(p.S - length, 0)) error1 = household.FOC_savings( r_s, w_s, p_tilde_s, p_i_s, b_s, b_splus1, n_s, bq, rm, factor, tr, ubi, theta, rho_s, etr_params, mtry_params, t, j, p, "TPI", ) error2 = household.FOC_labor( r_s, w_s, p_tilde_s, p_i_s, b_s, b_splus1, n_s, bq, rm, factor, tr, ubi, theta, chi_n_s, etr_params, mtrx_params, t, j, p, "TPI", ) # Check and punish constraint violations mask1 = n_guess < 0 error2[mask1] += 1e12 mask2 = n_guess > p.ltilde error2[mask2] += 1e12 mask4 = b_guess <= 0 error2[mask4] += 1e12 mask5 = b_splus1 < 0 error2[mask5] += 1e12 return list(error1.flatten()) + list(error2.flatten())
def _params_to_array(nested, tax_func_type): """Convert a nested tax-parameter slice to a numpy array. For numeric tax functions this lets ``get_tax_rates`` skip a costly per-call list-to-array conversion. ``mono`` and ``mono2D`` store callables rather than numbers, so their nested-list form is returned unchanged. """ if tax_func_type in ("mono", "mono2D"): return nested return np.array(nested)
[docs] def inner_loop(guesses, outer_loop_vars, initial_values, ubi, j, ind, p): """ Given path of economic aggregates and factor prices, solves household problem. This has been termed the inner-loop (in contrast to the outer fixed point loop that solves for GE factor prices and economic aggregates). Args: guesses (tuple): initial guesses for b and n, (guesses_b, guesses_n) outer_loop_vars (tuple): values for factor prices and economic aggregates used in household problem (r_p, r, w, p_m, BQ, RM, TR, theta) r_p (Numpy array): real interest rate on household portfolio r (Numpy array): real interest rate on private capital w (Numpy array): real wage rate p_m (Numpy array): output goods prices BQ (array_like): aggregate bequest amounts TR (Numpy array): lump sum transfer amount theta (Numpy array): retirement replacement rates, length J initial_values (tuple): initial period variable values, (b_sinit, b_splus1init, factor, initial_b, initial_n, D0_baseline) ubi (array_like): T+S x S x J array time series of UBI transfers in model units for each type-j age-s household in every period t j (int): index of ability type ind (Numpy array): integers from 0 to S-1 p (OG-Core Specifications object): model parameters Returns: (tuple): household solution results: * euler_errors (Numpy array): errors from FOCs, size = Tx2S * b_mat (Numpy array): savings amounts, size = TxS * n_mat (Numpy array): labor supply amounts, size = TxS """ K0, b_sinit, b_splus1init, factor, initial_b, initial_n = initial_values guesses_b, guesses_n = guesses r_p, r, w, p_m, BQ, RM, TR, theta = outer_loop_vars # compute composite good price p_i = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T + p.S, 1, 1)) * np.tile(p_m.reshape(p.T + p.S, 1, p.M), (1, p.I, 1)) ).sum(axis=2) p_tilde = aggr.get_ptilde(p_i[:, :], p.tau_c[:, :], p.alpha_c, "TPI") # compute bq bq = household.get_bq(BQ, None, p, "TPI") # compute tr tr = household.get_tr(TR, None, p, "TPI") # compute rm rm = household.get_rm(RM, None, p, "TPI") # initialize arrays b_mat = np.zeros((p.T + p.S, p.S)) n_mat = np.zeros((p.T + p.S, p.S)) euler_errors = np.zeros((p.T, 2 * p.S)) solutions = opt.root( firstdoughnutring, [guesses_b[0, -1], guesses_n[0, -1]], args=( r_p[0], w[0], p_tilde[0], p_i[0, :], bq[0, -1, j], rm[0, -1, j], tr[0, -1, j], theta * p.replacement_rate_adjust[0, j], factor, ubi[0, -1, j], j, initial_b, p, ), method=p.FOC_root_method, tol=MINIMIZER_TOL, ) b_mat[0, -1], n_mat[0, -1] = solutions.x[0], solutions.x[1] for s in range(p.S - 2): # Upper triangle ind2 = np.arange(s + 2) b_guesses_to_use = np.diag(guesses_b[: p.S, :], p.S - (s + 2)) n_guesses_to_use = np.diag(guesses_n[: p.S, :], p.S - (s + 2)) theta_to_use = theta[j] * p.replacement_rate_adjust[: p.S, j] bq_to_use = np.diag(bq[: p.S, :, j], p.S - (s + 2)) rm_to_use = np.diag(rm[: p.S, :, j], p.S - (s + 2)) tr_to_use = np.diag(tr[: p.S, :, j], p.S - (s + 2)) ubi_to_use = np.diag(ubi[: p.S, :, j], p.S - (s + 2)) num_params = len(p.etr_params[0][0]) # Convert the per-age tax-parameter slices to arrays here, once, # rather than letting get_tax_rates re-convert them from nested # lists on each of its many calls. mono/mono2D store callables, # not numbers, so _params_to_array leaves them as nested lists. temp_etr = [ [p.etr_params[t][p.S - s - 2 + t][i] for i in range(num_params)] for t in range(s + 2) ] etr_params_to_use = _params_to_array(temp_etr, p.tax_func_type) temp_mtrx = [ [p.mtrx_params[t][p.S - s - 2 + t][i] for i in range(num_params)] for t in range(s + 2) ] mtrx_params_to_use = _params_to_array(temp_mtrx, p.tax_func_type) temp_mtry = [ [p.mtry_params[t][p.S - s - 2 + t][i] for i in range(num_params)] for t in range(s + 2) ] mtry_params_to_use = _params_to_array(temp_mtry, p.tax_func_type) _tri_x0 = list(b_guesses_to_use) + list(n_guesses_to_use) _tri_args = ( r_p, w, p_tilde, p_i, bq_to_use, rm_to_use, tr_to_use, theta_to_use, factor, ubi_to_use, j, s, 0, etr_params_to_use, mtrx_params_to_use, mtry_params_to_use, initial_b, p, ) _tri_jac = _banded_jac_for( twist_doughnut, _tri_x0, _tri_args, p.use_sparse_FOC_jac ) try: solutions = opt.root( twist_doughnut, _tri_x0, args=_tri_args, method=p.FOC_root_method, tol=MINIMIZER_TOL, jac=_tri_jac, ) except Exception: solutions = opt.root( twist_doughnut, _tri_x0, args=_tri_args, method=p.FOC_root_method, tol=MINIMIZER_TOL, ) b_vec = solutions.x[: int(len(solutions.x) / 2)] b_mat[ind2, p.S - (s + 2) + ind2] = b_vec n_vec = solutions.x[int(len(solutions.x) / 2) :] n_mat[ind2, p.S - (s + 2) + ind2] = n_vec for t in range(0, p.T): b_guesses_to_use = 0.75 * np.diag(guesses_b[t : t + p.S, :]) n_guesses_to_use = np.diag(guesses_n[t : t + p.S, :]) theta_to_use = theta[j] * p.replacement_rate_adjust[t : t + p.S, j] bq_to_use = np.diag(bq[t : t + p.S, :, j]) rm_to_use = np.diag(rm[t : t + p.S, :, j]) tr_to_use = np.diag(tr[t : t + p.S, :, j]) ubi_to_use = np.diag(ubi[t : t + p.S, :, j]) # initialize array of diagonal elements num_params = len(p.etr_params[t][0]) etr_params_to_use = _params_to_array( [ [p.etr_params[t + s][s][i] for i in range(num_params)] for s in range(p.S) ], p.tax_func_type, ) mtrx_params_to_use = _params_to_array( [ [p.mtrx_params[t + s][s][i] for i in range(num_params)] for s in range(p.S) ], p.tax_func_type, ) mtry_params_to_use = _params_to_array( [ [p.mtry_params[t + s][s][i] for i in range(num_params)] for s in range(p.S) ], p.tax_func_type, ) _td_args = ( r_p, w, p_tilde, p_i, bq_to_use, rm_to_use, tr_to_use, theta_to_use, factor, ubi_to_use, j, None, t, etr_params_to_use, mtrx_params_to_use, mtry_params_to_use, initial_b, p, ) _td_x0 = list(b_guesses_to_use) + list(n_guesses_to_use) # When p.use_sparse_FOC_jac is set, supply a banded finite-difference # Jacobian (far fewer evaluations per build); otherwise _bj_jac is None # and opt.root uses its default dense finite differences. The except # clause is a safety net: any solver failure retries with the dense # Jacobian, so the result is unchanged. _bj_jac = _banded_jac_for( twist_doughnut, _td_x0, _td_args, p.use_sparse_FOC_jac ) try: solutions = opt.root( twist_doughnut, _td_x0, args=_td_args, method=p.FOC_root_method, tol=MINIMIZER_TOL, jac=_bj_jac, ) except Exception: solutions = opt.root( twist_doughnut, _td_x0, args=_td_args, method=p.FOC_root_method, tol=MINIMIZER_TOL, ) euler_errors[t, :] = solutions.fun b_vec = solutions.x[: p.S] b_mat[t + ind, ind] = b_vec n_vec = solutions.x[p.S :] n_mat[t + ind, ind] = n_vec return euler_errors, b_mat, n_mat
[docs] def run_TPI(p, client=None): """ Solve for transition path equilibrium of OG-Core. Args: p (OG-Core Specifications object): model parameters client (Dask client object): client Returns: output (dictionary): dictionary with transition path solution results """ if p.use_sparse_FOC_jac and not _HAVE_SPARSE_FD: print("[TPI] sparse FOC jacobian unavailable; using dense") # unpack tuples of parameters initial_values, ss_vars, theta, baseline_values = get_initial_SS_values(p) B0, b_sinit, b_splus1init, factor, initial_b, initial_n = initial_values ( Ybaseline, TRbaseline, Gbaseline, Ig_baseline, D0_baseline, Kg0_baseline, ) = baseline_values # Create time path of UBI household benefits and aggregate UBI outlays ubi = p.ubi_nom_array / factor UBI = aggr.get_L(ubi[: p.T], p, "TPI") logger.info( f"Government spending breakpoints are tG1: {p.tG1}; and tG2: {p.tG2}" ) # Initialize guesses at time paths # Make array of initial guesses for labor supply and savings guesses_b = utils.get_initial_path(initial_b, ss_vars["b_sp1"], p, "ratio") guesses_n = utils.get_initial_path(initial_n, ss_vars["n"], p, "ratio") b_mat = guesses_b n_mat = guesses_n ind = np.arange(p.S) # Get path for aggregate savings and labor supply L_init = np.ones((p.T + p.S,)) * ss_vars["L"] B_init = np.ones((p.T + p.S,)) * ss_vars["B"] L_init[: p.T] = aggr.get_L(n_mat[: p.T], p, "TPI") B_init[1 : p.T] = aggr.get_B(b_mat[: p.T], p, "TPI", False)[: p.T - 1] B_init[0] = B0 K_init = B_init * ss_vars["K"] / ss_vars["B"] K = K_init K_d = K_init * ss_vars["K_d"] / ss_vars["K"] K_f = K_init * ss_vars["K_f"] / ss_vars["K"] L = L_init B = B_init K_g = np.ones_like(K) * ss_vars["K_g"] Y = np.zeros_like(K) Y[: p.T] = firm.get_Y(K[: p.T], K_g[: p.T], L[: p.T], p, "TPI") Y[p.T :] = ss_vars["Y"] # path for industry specific aggregates K_vec_init = np.ones((p.T + p.S, p.M)) * ss_vars["K_m"].reshape(1, p.M) L_vec_init = np.ones((p.T + p.S, p.M)) * ss_vars["L_m"].reshape(1, p.M) Y_vec_init = np.ones((p.T + p.S, p.M)) * ss_vars["Y_m"].reshape(1, p.M) # compute w w = np.ones_like(K) * ss_vars["w"] # compute goods prices p_m = np.ones((p.T + p.S, p.M)) * ss_vars["p_m"].reshape(1, p.M) p_m[: p.T, :] = firm.get_pm( w[: p.T], Y_vec_init[: p.T, :], L_vec_init[: p.T, :], p, "TPI" ) p_m = p_m / p_m[:, -1].reshape( p.T + p.S, 1 ) # normalize prices by industry M p_i = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T + p.S, 1, 1)) * np.tile(p_m.reshape(p.T + p.S, 1, p.M), (1, p.I, 1)) ).sum(axis=2) p_tilde = aggr.get_ptilde(p_i[:, :], p.tau_c[:, :], p.alpha_c, "TPI") if not any(p.zeta_K == 1): w[: p.T] = np.squeeze( firm.get_w(Y[: p.T], L[: p.T], p_m[: p.T, :], p, "TPI") ) # repeat with updated w p_m[: p.T, :] = firm.get_pm( w[: p.T], Y_vec_init[: p.T, :], L_vec_init[: p.T, :], p, "TPI" ) p_m = p_m / p_m[:, -1].reshape( p.T + p.S, 1 ) # normalize prices by industry M p_i = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T + p.S, 1, 1)) * np.tile(p_m.reshape(p.T + p.S, 1, p.M), (1, p.I, 1)) ).sum(axis=2) p_tilde = aggr.get_ptilde(p_i[:, :], p.tau_c[:, :], p.alpha_c, "TPI") # path for interest rates r = np.zeros_like(Y) r[: p.T] = np.squeeze( firm.get_r(Y[: p.T], K[: p.T], p_m[: p.T, :], p, "TPI") ) r[p.T :] = ss_vars["r"] # For case where economy is small open econ r[p.zeta_K == 1] = p.world_int_rate[p.zeta_K == 1] # initial guesses at fiscal vars if p.budget_balance: if np.abs(ss_vars["TR"]) < 1e-13: TR_ss2 = 0.0 # sometimes SS is very small but not zero, # even if taxes are zero, this get's rid of the # approximation error, which affects the pct changes below else: TR_ss2 = ss_vars["TR"] TR = np.ones(p.T + p.S) * TR_ss2 total_tax_revenue = TR - ss_vars["agg_pension_outlays"] G = np.zeros(p.T + p.S) D = np.zeros(p.T + p.S) D_d = np.zeros(p.T + p.S) D_f = np.zeros(p.T + p.S) I_g = fiscal.get_I_g(Y[: p.T], None, p, "TPI") else: if p.baseline_spending: # Will set to TRbaseline here, but will be updated in TPI loop # with call to fiscal.get_TR TR = np.concatenate( (TRbaseline[: p.T], np.ones(p.S) * ss_vars["TR"]) ) # Will set to Ig_baseline here, but will be updated in TPI loop # with call to fiscal.get_I_g I_g = np.concatenate( (Ig_baseline[: p.T], np.ones(p.S) * ss_vars["I_g"]) ) # Will set to Gbaseline here, but will be updated in TPI loop # with call to fiscal.D_G_path, which also does closure rule G = np.concatenate((Gbaseline[: p.T], np.ones(p.S) * ss_vars["G"])) else: TR = p.alpha_T * Y G = np.ones(p.T + p.S) * ss_vars["G"] I_g = np.ones(p.T + p.S) * ss_vars["I_g"] D = np.ones(p.T + p.S) * ss_vars["D"] D_d = D * ss_vars["D_d"] / ss_vars["D"] D_f = D * ss_vars["D_f"] / ss_vars["D"] if p.baseline: K_g0 = p.initial_Kg_ratio * Y[0] else: K_g0 = Kg0_baseline K_g = fiscal.get_K_g(K_g0, I_g, p, "TPI") total_tax_revenue = np.ones(p.T + p.S) * ss_vars["total_tax_revenue"] # Compute other interest rates r_gov = np.ones_like(r) * ss_vars["r_gov"] r_gov[: p.T] = fiscal.get_r_gov(r[: p.T], D[: p.T] / Y[: p.T], p, "TPI") r_p = np.ones_like(r) * ss_vars["r_p"] MPKg = np.zeros((p.T, p.M)) for m in range(p.M): MPKg[:, m] = np.squeeze( firm.get_MPx( Y_vec_init[: p.T, m], K_g[: p.T], p.gamma_g[m], p, "TPI", m ) ) r_p[: p.T] = aggr.get_r_p( r[: p.T], r_gov[: p.T], p_m[: p.T, :], K_vec_init[: p.T, :], K_g[: p.T], D[: p.T], MPKg, p, "TPI", ) # Initialize bequests BQ0 = aggr.get_BQ(r_p[0], initial_b, None, p, "SS", True) if not p.use_zeta: BQ = np.zeros((p.T + p.S, p.J)) for j in range(p.J): BQ[:, j] = ( list(np.linspace(BQ0[j], ss_vars["BQ"][j], p.T)) + [ss_vars["BQ"][j]] * p.S ) BQ = np.array(BQ) else: BQ = list(np.linspace(BQ0, ss_vars["BQ"], p.T)) + [ss_vars["BQ"]] * p.S BQ = np.array(BQ) # Initialize aggregate remittances if p.baseline: RM = aggr.get_RM(Y, p, "TPI") else: # This is the reform case and is based off of Ybaseline, but allows for # remittance parameters to change in a reform and update the RM series Ybaseline_ext = np.concatenate( [Ybaseline, np.ones(p.S) * Ybaseline[-1]] ) RM = aggr.get_RM(Ybaseline_ext, p, "TPI") # Start transition path iteration (TPI) TPIiter = 0 TPIdist = 10 euler_errors = np.zeros((p.T, 2 * p.S, p.J)) TPIdist_vec = np.zeros(p.maxiter) stall = None stall_reported = None # Pluggable outer-loop update rule. Default "picard" -> None -> the native # damped functional-iteration path below (unchanged, so golden outputs # are preserved); "anderson" accelerates using the residual history. See # ogcore.solvers. outer_updater = solvers.make_outer_updater( getattr(p, "TPI_outer_method", "picard"), p ) # Optional trust region for the accelerated step ("anchored" Anderson): # clamp each step to within trust_radius x the damped-step length of the # always-feasible damped point, growing the radius after an improving step # and shrinking it (with a reset) after a worsening one. This keeps the # accelerated iterate near a feasible point -- the standard cure for the # overshoot-into-infeasible-region divergence of unguarded accelerators. # Defaults to 1.0 (anchored) when accelerating; a non-positive radius # disables the trust region (unguarded accelerator, for testing only). trust_radius = getattr(p, "TPI_trust_radius", 1.0) if trust_radius is not None and trust_radius <= 0: trust_radius = None trust_radius_min = getattr(p, "TPI_trust_radius_min", 0.1) trust_radius_max = getattr(p, "TPI_trust_radius_max", 10.0) prev_accel_dist = np.inf # 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 # TPI loop while (TPIiter < p.maxiter) and (TPIdist >= p.mindist_TPI): outer_loop_vars = (r_p, r, w, p_m, BQ, RM, TR, theta) # compute composite good price p_i = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T + p.S, 1, 1)) * np.tile(p_m.reshape(p.T + p.S, 1, p.M), (1, p.I, 1)) ).sum(axis=2) p_tilde = aggr.get_ptilde(p_i[:, :], p.tau_c[:, :], p.alpha_c, "TPI") # Initialize Euler errors euler_errors = np.zeros((p.T, 2 * p.S, p.J)) # Solve for household decisions results = [] if client: futures = [] for j in range(p.J): guesses = (guesses_b[:, :, j], guesses_n[:, :, j]) f = client.submit( inner_loop, guesses, outer_loop_vars, initial_values, ubi, j, ind, scattered_p_future, ) futures.append(f) try: # Wait for futures with timeout, then gather results done, not_done = wait(futures, timeout=600) if not_done: # Some futures didn't complete in time raise TimeoutError( f"{len(not_done)} futures did not complete" " within 600 seconds" ) 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 this iteration." ) for future in futures: future.cancel() results = [] for j in range(p.J): guesses = (guesses_b[:, :, j], guesses_n[:, :, j]) res = inner_loop( guesses, outer_loop_vars, initial_values, ubi, j, ind, p, ) results.append(res) else: # Serial fallback (no dask client) for j in range(p.J): guesses = (guesses_b[:, :, j], guesses_n[:, :, j]) res = inner_loop( guesses, outer_loop_vars, initial_values, ubi, j, ind, p, # pass the raw object directly ) results.append(res) for j, result in enumerate(results): euler_errors[:, :, j], b_mat[:, :, j], n_mat[:, :, j] = result bmat_s = np.zeros((p.T, p.S, p.J)) bmat_s[0, 1:, :] = initial_b[:-1, :] bmat_s[1:, 1:, :] = b_mat[: p.T - 1, :-1, :] bmat_splus1 = np.zeros((p.T, p.S, p.J)) bmat_splus1[:, :, :] = b_mat[: p.T, :, :] num_params = len(p.etr_params[0][0]) etr_params_4D = [ [ [ [p.etr_params[t][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] for t in range(p.T) ] bqmat = household.get_bq(BQ, None, p, "TPI") rmmat = household.get_rm(RM, None, p, "TPI") trmat = household.get_tr(TR, None, p, "TPI") tax_mat = tax.net_taxes( r_p[: p.T], w[: p.T], bmat_s, n_mat[: p.T, :, :], bqmat[: p.T, :, :], factor, trmat[: p.T, :, :], ubi[: p.T, :, :], theta, 0, None, False, "TPI", p.e, etr_params_4D, p, ) income_tax_mat = tax.income_tax_liab( r_p[: p.T], w[: p.T], bmat_s, n_mat[: p.T, :, :], factor, 0, None, "TPI", p.e, etr_params_4D, p, ) pension_mat = pensions.pension_amount( r_p[: p.T], w[: p.T], n_mat[: p.T, :, :], 1, theta, 0, None, False, "TPI", p.e, factor, p, ) bq_tax_mat = tax.bequest_tax_liab( r_p[: p.T], bmat_s, bqmat[: p.T, :, :], 0, None, "TPI", p ) wealth_tax_mat = tax.wealth_tax_liab( r_p[: p.T], bmat_s, 0, None, "TPI", p ) r_p_path = utils.to_timepath_shape(r_p) p_tilde_path = utils.to_timepath_shape(p_tilde) wpath = utils.to_timepath_shape(w) c_mat = household.get_cons( r_p_path[: p.T, :, :], wpath[: p.T, :, :], p_tilde_path[: p.T, :, :], p_i[: p.T, :], bmat_s, bmat_splus1, n_mat[: p.T, :, :], bqmat[: p.T, :, :], rmmat[: p.T, :, :], tax_mat, p.e, p.tau_c[: p.T, :], p, method="TPI", ) c_i = household.get_ci( c_mat[: p.T, :, :], p_i[: p.T, :], p_tilde[: p.T], p.tau_c[: p.T, :], p.alpha_c, p.c_min, "TPI", ) sales_tax_mat = tax.cons_tax_liab(c_i, p_i, p, "TPI") C = aggr.get_C(c_mat, p, "TPI") c_i = household.get_ci( c_mat[: p.T, :, :], p_i[: p.T, :], p_tilde[: p.T], p.tau_c[: p.T, :], p.alpha_c, p.c_min, "TPI", ) y_before_tax_mat = household.get_y( r_p_path[: p.T, :, :], wpath[: p.T, :, :], bmat_s[: p.T, :, :], n_mat[: p.T, :, :], p, "TPI", ) labor_income_mat = ( w[: p.T].reshape(p.T, 1, 1) * n_mat[: p.T, :, :] * p.e ) capital_income_mat = ( r_p[: p.T].reshape(p.T, 1, 1) * bmat_s[: p.T, :, :] ) # Update aggregate variables L[: p.T] = aggr.get_L(n_mat[: p.T], p, "TPI") B[1 : p.T] = aggr.get_B(bmat_splus1[: p.T], p, "TPI", False)[: p.T - 1] w_open = firm.get_w_from_r(p.world_int_rate[: p.T], p, "TPI") # Find output, labor demand, capital demand for M-1 industries L_vec = np.zeros((p.T, p.M)) K_vec = np.zeros((p.T, p.M)) C_vec = np.zeros((p.T, p.I)) K_demand_open_vec = np.zeros((p.T, p.M)) for i_ind in range(p.I): C_vec[:, i_ind] = aggr.get_C(c_i[: p.T, i_ind, :, :], p, "TPI") Y_vec = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T, 1, 1)) * np.tile(C_vec[: p.T, :].reshape(p.T, p.I, 1), (1, 1, p.M)) ).sum(axis=1) for m_ind in range(p.M - 1): KYrat_m = firm.get_KY_ratio( r[: p.T], p_m[: p.T, :], p, "TPI", 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, "TPI", m_ind ) K_demand_open_vec[:, m_ind] = firm.get_K( p.world_int_rate[: p.T], w_open[: p.T], L_vec[: p.T, m_ind], p, "TPI", m_ind, ) # Find output, labor demand, capital demand for last industry L_M = np.maximum( np.ones(p.T) * 0.001, L[: p.T] - L_vec[: p.T, :].sum(-1) ) # make sure L_M > 0 K_demand_open_vec[:, -1] = firm.get_K( p.world_int_rate[: p.T], w_open[: p.T], L_M[: p.T], p, "TPI", -1 ) K[: p.T], K_d[: p.T], K_f[: p.T] = aggr.get_K_splits( B[: p.T], K_demand_open_vec[: p.T, :].sum(-1), D_d[: p.T], p.zeta_K[: p.T], ) K_M = np.maximum( np.ones(p.T) * 0.001, K[: p.T] - K_vec[: p.T, :].sum(-1) ) # make sure K_M > 0 L_vec[:, -1] = L_M K_vec[:, -1] = K_M Y_vec[:, -1] = firm.get_Y( K_vec[: p.T, -1], K_g[: p.T], L_vec[: p.T, -1], p, "TPI", -1 ) Y = (p_m[: p.T, :] * Y_vec[: p.T, :]).sum(-1) ( total_tax_rev, 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[: p.T], w[: p.T], bmat_s, n_mat[: p.T, :, :], bqmat[: p.T, :, :], c_i[: p.T, :, :, :], Y_vec[: p.T, :], L_vec[: p.T, :], K_vec[: p.T, :], p_m[: p.T, :], factor, ubi[: p.T, :, :], theta, etr_params_4D, p.e, p, None, "TPI", ) total_tax_revenue[: p.T] = total_tax_rev if not p.baseline_spending: I_g = fiscal.get_I_g(Y[: p.T], None, p, "TPI") if p.baseline: K_g0 = p.initial_Kg_ratio * Y[0] K_g = fiscal.get_K_g(K_g0, I_g, p, "TPI") dg_fixed_values = ( Y, total_tax_revenue, agg_pension_outlays, UBI_outlays, TR, I_g, Gbaseline, D0_baseline, ) ( Dnew, G[: p.T], D_d[: p.T], D_f[: p.T], r_gov_new, new_borrowing, debt_service, new_borrowing_f, ) = fiscal.D_G_path(r, dg_fixed_values, p) rnew = r.copy() rnew[: p.T] = np.squeeze( firm.get_r( Y_vec[: p.T, -1], K_vec[: p.T, -1], p_m[: p.T, :], p, "TPI", -1 ) ) # For case where economy is small open econ rnew[p.zeta_K == 1] = p.world_int_rate[p.zeta_K == 1] MPKg_vec = np.zeros((p.T, p.M)) for m in range(p.M): MPKg_vec[:, m] = np.squeeze( firm.get_MPx( Y_vec[: p.T, m], K_g[: p.T], p.gamma_g[m], p, "TPI", m ) ) K[: p.T], K_d[: p.T], K_f[: p.T] = aggr.get_K_splits( B[: p.T], K_demand_open_vec.sum(-1), D_d[: p.T], p.zeta_K[: p.T] ) r_gov_new = fiscal.get_r_gov( rnew[: p.T], Dnew[: p.T] / Y[: p.T], p, "TPI" ) r_p_new = aggr.get_r_p( rnew[: p.T], r_gov_new[: p.T], p_m[: p.T, :], K_vec[: p.T, :], K_g[: p.T], Dnew[: p.T], MPKg_vec, p, "TPI", ) # compute w wnew = np.squeeze( firm.get_w( Y_vec[: p.T, -1], L_vec[: p.T, -1], p_m[: p.T, :], p, "TPI", -1 ) ) # compute new prices new_p_m = firm.get_pm(wnew, Y_vec, L_vec, p, "TPI") new_p_m = new_p_m / new_p_m[:, -1].reshape( p.T, 1 ) # normalize prices by industry M b_mat_shift = np.append( np.reshape(initial_b, (1, p.S, p.J)), b_mat[: p.T - 1, :, :], axis=0, ) BQnew = aggr.get_BQ(r_p_new[: p.T], b_mat_shift, None, p, "TPI", False) bqmat_new = household.get_bq(BQnew, None, p, "TPI") ( total_tax_rev, 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_new[: p.T], wnew[: p.T], bmat_s, n_mat[: p.T, :, :], bqmat_new[: p.T, :, :], c_i[: p.T, :, :, :], Y_vec[: p.T, :], L_vec[: p.T, :], K_vec[: p.T, :], new_p_m[: p.T, :], factor, ubi[: p.T, :, :], theta, etr_params_4D, p.e, p, None, "TPI", ) total_tax_revenue[: p.T] = total_tax_rev TR_new = fiscal.get_TR( Y[: p.T], TR[: p.T], G[: p.T], total_tax_revenue[: p.T], agg_pension_outlays[: p.T], UBI_outlays[: p.T], I_g[: p.T], p, "TPI", ) RM = aggr.get_RM(Y[: p.T], p, "TPI") RM = np.concatenate([RM, np.ones(p.S) * RM[-1]]) # update vars for next iteration if outer_updater is None: # "picard": the historical damped functional-iteration step, # unchanged, so the default behavior (and golden outputs) is # preserved exactly. w[: p.T] = utils.convex_combo(wnew[: p.T], w[: p.T], p.nu) r[: p.T] = utils.convex_combo(rnew[: p.T], r[: p.T], p.nu) r_p[: p.T] = utils.convex_combo(r_p_new[: p.T], r_p[: p.T], p.nu) p_m[: p.T, :] = utils.convex_combo( new_p_m[: p.T, :], p_m[: p.T, :], p.nu ) BQ[: p.T] = utils.convex_combo(BQnew[: p.T], BQ[: p.T], p.nu) if not p.baseline_spending: TR[: p.T] = utils.convex_combo(TR_new[: p.T], TR[: p.T], p.nu) else: # Accelerated outer step on the packed macro/price vector # {r_p, r, w, p_m, BQ[, TR]}; the update rule (Anderson) uses the # recent residual history. Auxiliaries stay damped below. blocks = [ (r_p, r_p_new), (r, rnew), (w, wnew), (p_m, new_p_m), (BQ, BQnew), ] if not p.baseline_spending: blocks.append((TR, TR_new)) x, gx = solvers.pack_outer_vars(blocks, p.T) # True fixed-point residual (implied vs the PRE-update guess). The # post-update TPIdist below is spuriously ~0 for steps that set # x_next ~= gx (e.g. Anderson's undamped first step), so it is # overridden with this to avoid declaring false convergence. accel_dist = float(np.max(utils.pct_diff_func(gx, x))) # Anchored/trust-region control: grow the radius after an improving # accelerated step and shrink it (resetting the memory) after a # worsening one, using the residual trend as the accept/reject # signal. None => unclamped. if trust_radius is not None and TPIiter > 0: if accel_dist <= prev_accel_dist: trust_radius = min(trust_radius_max, trust_radius * 1.5) else: trust_radius = max(trust_radius_min, trust_radius * 0.5) outer_updater.reset() logger.info( f"accel step worsened; trust radius -> {trust_radius}" ) prev_accel_dist = accel_dist # Accelerate the DAMPED map (1-nu)x + nu*G(x), which is contractive # at the calibrated nu, rather than the raw map G -- G is # non-contractive on the stiff case (why Picard needs a low nu), so # accelerating it directly overshoots and diverges. Same fixed # point; convergence is still measured on the raw residual above. gx_damped = (1.0 - p.nu) * x + p.nu * gx x_next = outer_updater.update(x, gx_damped) if not np.all(np.isfinite(x_next)): # accelerated step blew up -> damped Picard fallback + reset x_next = p.nu * gx + (1.0 - p.nu) * x outer_updater.reset() logger.info( "accelerated step non-finite; Picard fallback + reset" ) elif trust_radius is not None: # Clamp the accelerated step to the trust region around the # always-feasible damped point gx_damped, sized relative to the # damped step length so it tightens as the solve converges. dev = x_next - gx_damped dev_norm = float(np.linalg.norm(dev)) max_dev = trust_radius * float(np.linalg.norm(gx_damped - x)) if dev_norm > max_dev > 0.0: x_next = gx_damped + dev * (max_dev / dev_norm) solvers.unpack_outer_vars(x_next, blocks, p.T) # Auxiliaries (unchanged): government rate, debt, and the household # policy warm-starts stay on the damped update. r_gov[: p.T] = utils.convex_combo(r_gov_new[: p.T], r_gov[: p.T], p.nu) D[: p.T] = Dnew[: p.T] guesses_b = utils.convex_combo(b_mat, guesses_b, p.nu) guesses_n = utils.convex_combo(n_mat, guesses_n, p.nu) logger.info( f"w diff: {(wnew[: p.T] - w[: p.T]).max()}, " + f"{(wnew[: p.T] - w[: p.T]).min()}" ) logger.info( f"r diff: {(rnew[: p.T] - r[: p.T]).max()}, " + f"{(rnew[: p.T] - r[: p.T]).min()}" ) logger.info( f"r_p diff: {(r_p_new[: p.T] - r_p[: p.T]).max()}, " + f"{(r_p_new[: p.T] - r_p[: p.T]).min()}" ) logger.info( f"p_m diff: {(new_p_m[: p.T, :] - p_m[: p.T, :]).max()}, " + f"{(new_p_m[: p.T, :] - p_m[: p.T, :]).min()}" ) logger.info( f"BQ diff: {(BQnew[: p.T] - BQ[: p.T]).max()}, " + f"{(BQnew[: p.T] - BQ[: p.T]).min()}" ) logger.info( f"TR diff: {(TR_new[: p.T] - TR[: p.T]).max()}, " + f"{(TR_new[: p.T] - TR[: p.T]).min()}" ) TPIdist = np.array( list(utils.pct_diff_func(r_p_new[: p.T], r_p[: p.T])) + list(utils.pct_diff_func(rnew[: p.T], r[: p.T])) + list(utils.pct_diff_func(wnew[: p.T], w[: p.T])) + list( utils.pct_diff_func(new_p_m[: p.T, :], p_m[: p.T, :]).flatten() ) + list(utils.pct_diff_func(BQnew[: p.T], BQ[: p.T]).flatten()) + list(utils.pct_diff_func(TR_new[: p.T], TR[: p.T])) ).max() if outer_updater is not None: # accelerated methods: use the true residual accel_dist, computed # above from the pre-update guess, because the post-update # distance can be spuriously ~0 for accelerated steps. TPIdist = accel_dist TPIdist_vec[TPIiter] = TPIdist # Accelerated safety net: if a step raised the distance sharply or went # non-finite, reset the accelerator so the next step is a fresh damped # restart (prevents runaway on the stiff case). Never fires for picard. if outer_updater is not None and TPIiter > 0: if ( not np.isfinite(TPIdist) or TPIdist > 10.0 * TPIdist_vec[TPIiter - 1] ): outer_updater.reset() logger.info("accelerated step diverged; reset accelerator") TPIiter += 1 logger.info(f"Iteration: {TPIiter}") logger.info(f"Distance: {TPIdist}") # Stall detection: when the best distance has stopped improving # over a window of iterations, further iterations repeat the same # pattern and cannot reach the tolerance -- diagnose the cause and, # if TPI_stall_action="stop", end the loop early (the solution # checks after the loop then fail the run as any non-convergence). # In the default warn mode the diagnosis is logged once per stall, # and again only if it changes (e.g. escalates to diverging). stall = solvers.diagnose_stall( TPIdist_vec, TPIiter, p.TPI_stall_window ) if stall != stall_reported: stall_reported = stall if stall == "diverging": logger.error( "TPI stalled and diverging: the best distance over " f"the last {p.TPI_stall_window} iterations is far " "above the earlier best. This usually signals an " "inconsistent fiscal block (spending, revenue, and " "debt_ratio_ss), not a solver problem." ) elif stall == "oscillating": logger.error( "TPI stalled: the best distance has not improved " f"over the last {p.TPI_stall_window} iterations " f"(current {TPIdist:.2e}, tolerance " f"{p.mindist_TPI}). The outer loop is cycling; try " "a lower nu, or TPI_outer_method='anderson' if not " "already enabled." ) if stall is not None and p.TPI_stall_action == "stop": break # Compute effective and marginal tax rates for all agents num_params = len(p.mtrx_params[0][0]) etr_params_4D = [ [ [ [p.etr_params[t][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] for t in range(p.T) ] mtrx_params_4D = [ [ [ [p.mtrx_params[t][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] for t in range(p.T) ] mtry_params_4D = [ [ [ [p.mtry_params[t][s][i] for i in range(num_params)] for j in range(p.J) ] for s in range(p.S) ] for t in range(p.T) ] labor_noncompliance_rate_3D = np.tile( np.reshape( p.labor_income_tax_noncompliance_rate[: p.T, :], (p.T, 1, p.J) ), (1, p.S, 1), ) capital_noncompliance_rate_3D = np.tile( np.reshape( p.capital_income_tax_noncompliance_rate[: p.T, :], (p.T, 1, p.J) ), (1, p.S, 1), ) income_tax_filer_3D = np.tile( np.reshape(p.income_tax_filer[: p.T, :], (p.T, 1, p.J)), (1, p.S, 1), ) e_3D = p.e mtry_path = tax.MTR_income( r_p_path[: p.T], wpath[: p.T], bmat_s[: p.T, :, :], n_mat[: p.T, :, :], factor, True, e_3D, etr_params_4D, mtry_params_4D, capital_noncompliance_rate_3D, income_tax_filer_3D, p, ) mtrx_path = tax.MTR_income( r_p_path[: p.T], wpath[: p.T], bmat_s[: p.T, :, :], n_mat[: p.T, :, :], factor, False, e_3D, etr_params_4D, mtrx_params_4D, labor_noncompliance_rate_3D, income_tax_filer_3D, p, ) etr_path = tax.ETR_income( r_p_path[: p.T], wpath[: p.T], bmat_s[: p.T, :, :], n_mat[: p.T, :, :], factor, e_3D, etr_params_4D, labor_noncompliance_rate_3D, capital_noncompliance_rate_3D, income_tax_filer_3D, p, ) # Note that implicitly in this computation is that immigrants' # wealth is all in the form of private capital I_d = aggr.get_I( bmat_splus1[: p.T], K_d[1 : p.T + 1], K_d[: p.T], p, "TPI" ) I = aggr.get_I(bmat_splus1[: p.T], K[1 : p.T + 1], K[: p.T], p, "TPI") # noqa: E741 # solve resource constraint # foreign debt service costs debt_service_f = fiscal.get_debt_service_f(r_p, D_f) net_capital_outflows = aggr.get_capital_outflows( r_p[: p.T], K_f[: p.T], new_borrowing_f[: p.T], debt_service_f[: p.T], p, ) # Fill in arrays, noting that M-1 industries only produce consumption goods G_vec = np.zeros((p.T, p.M)) G_vec[:, -1] = G[: p.T] # Map consumption goods back to demands for production goods C_m_vec = ( np.tile(p.io_matrix.reshape(1, p.I, p.M), (p.T, 1, 1)) * np.tile(C_vec[: p.T, :].reshape(p.T, p.I, 1), (1, 1, p.M)) ).sum(axis=1) I_d_vec = np.zeros((p.T, p.M)) I_d_vec[:, -1] = I_d[: p.T] I_g_vec = np.zeros((p.T, p.M)) I_g_vec[:, -1] = I_g[: p.T] net_capital_outflows_vec = np.zeros((p.T, p.M)) net_capital_outflows_vec[:, -1] = net_capital_outflows[: p.T] RM_vec = np.zeros((p.T, p.M)) RM_vec[:, -1] = RM[: p.T] foreign_aid_vec = np.zeros((p.T, p.M)) foreign_aid_vec[:, -1] = p.alpha_FA[: p.T] * Y[: p.T] RC_error = aggr.resource_constraint( Y_vec, C_m_vec, G_vec, I_d_vec, I_g_vec, net_capital_outflows_vec, RM_vec, foreign_aid_vec, ) # Compute total investment (not just domestic) I_total = aggr.get_I(None, K[1 : p.T + 1], K[: p.T], p, "total_tpi") # Compute resource constraint error rce_max = np.amax(np.abs(RC_error)) logger.info(f"Max absolute value resource constraint error: {rce_max}") logger.info("Checking time path for violations of constraints.") for t in range(p.T): household.constraint_checker_TPI( b_mat[t], n_mat[t], c_mat[t], t, p.ltilde ) eul_savings = euler_errors[:, : p.S, :] eul_laborleisure = euler_errors[:, p.S :, :] logger.info(f"Max Euler error, savings: {np.abs(eul_savings).max()}") logger.info( f"Max Euler error labor supply: {np.abs(eul_laborleisure).max()}" ) """ ------------------------------------------------------------------------ Save variables/values so they can be used in other modules ------------------------------------------------------------------------ """ output = { "Y": Y[: p.T, ...], "B": B[: p.T, ...], "K": K[: p.T, ...], "K_f": K_f[: p.T], "K_d": K_d[: p.T, ...], "L": L[: p.T, ...], "C": C[: p.T, ...], "I": I[: p.T, ...], "I_total": I_total[: p.T, ...], "I_d": I_d[: p.T, ...], "K_g": K_g[: p.T, ...], "I_g": I_g[: p.T, ...], "BQ": BQ[: p.T, ...], "RM": RM[: p.T, ...], "Y_m": Y_vec[: p.T, ...], "K_m": K_vec[: p.T, ...], "L_m": L_vec[: p.T, ...], "C_i": C_vec[: p.T, ...], "TR": TR[: p.T, ...], "agg_pension_outlays": agg_pension_outlays[: p.T, ...], "G": G[: p.T, ...], "UBI": UBI[: p.T, ...], "total_government_outlays": ( TR[: p.T, ...] + UBI[: p.T, ...] + G[: p.T, ...] + I_g[: p.T, ...] + debt_service[: p.T, ...] + agg_pension_outlays[: p.T, ...] ), "total_primary_government_outlays": ( agg_pension_outlays[: p.T, ...] + TR[: p.T, ...] + UBI[: p.T, ...] + G[: p.T, ...] + I_g[: p.T, ...] ), "total_tax_revenue": total_tax_revenue[: p.T, ...], "business_tax_revenue": business_tax_revenue[: p.T, ...], "iit_payroll_tax_revenue": iit_payroll_tax_revenue[: p.T, ...], "iit_revenue": iit_revenue[: p.T, ...], "payroll_tax_revenue": payroll_tax_revenue[: p.T, ...], "bequest_tax_revenue": bequest_tax_revenue[: p.T, ...], "wealth_tax_revenue": wealth_tax_revenue[: p.T, ...], "cons_tax_revenue": cons_tax_revenue[: p.T, ...], "D": D[: p.T, ...], "D_f": D_f[: p.T, ...], "D_d": D_d[: p.T, ...], "new_borrowing": new_borrowing[: p.T, ...], "debt_service": debt_service[: p.T, ...], "new_borrowing_f": new_borrowing_f[: p.T, ...], "debt_service_f": debt_service_f[: p.T, ...], "r": r[: p.T, ...], "r_gov": r_gov[: p.T, ...], "r_p": r_p[: p.T, ...], "w": w[: p.T, ...], "p_m": p_m[: p.T, ...], "p_i": p_i[: p.T, ...], "p_tilde": p_tilde[: p.T, ...], "b_sp1": bmat_splus1[: p.T, ...], "b_s": bmat_s[: p.T, ...], "n": n_mat[: p.T, ...], "c": c_mat[: p.T, ...], "c_i": c_i[: p.T, ...], "bq": bqmat[: p.T, ...], "rm": rmmat[: p.T, ...], "tr": trmat[: p.T, ...], "ubi": ubi[: p.T, ...], "before_tax_income": y_before_tax_mat[: p.T, ...], "labor_income": labor_income_mat[: p.T, ...], "capital_income": capital_income_mat[: p.T, ...], "hh_net_taxes": tax_mat[: p.T, ...], "income_payroll_taxes": income_tax_mat[: p.T, ...], "sales_tax": sales_tax_mat[: p.T, ...], "wealth_tax": wealth_tax_mat[: p.T, ...], "bequest_tax": bq_tax_mat[: p.T, ...], "pension_benefits": pension_mat[: p.T, ...], "etr": etr_path[: p.T, ...], "mtrx": mtrx_path[: p.T, ...], "mtry": mtry_path[: p.T, ...], "theta": theta, "factor": factor, "euler_savings": eul_savings[: p.T, ...], "euler_labor_leisure": eul_laborleisure[: p.T, ...], "resource_constraint_error": RC_error[: p.T, ...], } tpi_dir = os.path.join(p.output_base, "TPI") utils.mkdirs(tpi_dir) tpi_vars = os.path.join(tpi_dir, "TPI_vars.pkl") with open(tpi_vars, "wb") as f: pickle.dump(output, f) if np.any(G) < 0: logger.warning( "Government spending is negative along transition path" + " to satisfy budget" ) if ( (TPIiter >= p.maxiter) or (np.absolute(TPIdist) > p.mindist_TPI) ) and ENFORCE_SOLUTION_CHECKS: msg = "Transition path equlibrium not found (TPIdist)" if stall == "oscillating": msg += ( "; the outer loop stalled cycling -- try a lower nu, or " "TPI_outer_method='anderson'" ) elif stall == "diverging": msg += ( "; the outer loop stalled diverging -- check the fiscal " "block (spending, revenue, debt_ratio_ss)" ) raise RuntimeError(msg) if (np.any(np.absolute(RC_error) >= p.RC_TPI)) and ENFORCE_SOLUTION_CHECKS: raise RuntimeError( "Transition path equlibrium not found " + "(RC_error)" ) if ( np.any(np.absolute(eul_savings) >= p.mindist_TPI) or (np.any(np.absolute(eul_laborleisure) > p.mindist_TPI)) ) and ENFORCE_SOLUTION_CHECKS: raise RuntimeError( "Transition path equlibrium not found " + "(eulers)" ) return output