Source code for ogcore.pensions

# Packages
import numpy as np
import numba
from ogcore import utils

# set constants
MONTHS_IN_A_YEAR = 12
THOUSAND = 1000


[docs] def replacement_rate_vals(nssmat, wss, factor_ss, j, p): r""" Calculates replacement rate values for the social security system. .. math:: \theta_{j,R,t+R} = \frac{PIA_{j,R,t+R} \times 12}{factor \times w_{t+R}} Args: nssmat (Numpy array): initial guess at labor supply, size = SxJ new_w (scalar): steady state real wage rate factor_ss (scalar): scaling factor converting model units to dollars j (int): index of lifetime income group p (OG-Core Specifications object): model parameters Returns: theta (Numpy array): social security replacement rate value for lifetime income group j """ if j is not None: e = np.squeeze(p.e[-1, :, j]) # Only computes using SS earnings dim2 = 1 else: e = np.squeeze(p.e[-1, :, :]).reshape( (p.S, p.J) ) # Only computes using SS earnings dim2 = p.J # adjust number of calendar years AIME computed from int model periods equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 earnings = (e * (wss * nssmat * factor_ss)).reshape(p.S, dim2) # get highest earning years for number of years AIME computed from highest_earn = ( -1.0 * np.sort(-1.0 * earnings[: p.retire[-1], :], axis=0) )[:equiv_periods] AIME = highest_earn.sum(0) / ((12.0 * (p.S / 80.0)) * equiv_periods) PIA = np.zeros(dim2) # Compute level of replacement using AIME brackets and PIA rates for jj in range(dim2): if AIME[jj] < p.AIME_bkt_1: PIA[jj] = p.PIA_rate_bkt_1 * AIME[jj] elif AIME[jj] < p.AIME_bkt_2: PIA[jj] = p.PIA_rate_bkt_1 * p.AIME_bkt_1 + p.PIA_rate_bkt_2 * ( AIME[jj] - p.AIME_bkt_1 ) else: PIA[jj] = ( p.PIA_rate_bkt_1 * p.AIME_bkt_1 + p.PIA_rate_bkt_2 * (p.AIME_bkt_2 - p.AIME_bkt_1) + p.PIA_rate_bkt_3 * (AIME[jj] - p.AIME_bkt_2) ) # Set the maximum monthly replacement rate from SS benefits tables PIA[PIA > p.PIA_maxpayment] = p.PIA_maxpayment if p.PIA_minpayment != 0.0: PIA[PIA < p.PIA_minpayment] = p.PIA_minpayment theta = (PIA * (12.0 * p.S / 80.0)) / (factor_ss * wss) if not p.baseline and p.baseline_theta: # use theta from the baseline solution if j is not None: theta = p.SS_theta[j] else: theta = p.SS_theta return theta
[docs] def pension_amount(r, w, n, Y, theta, t, j, shift, method, e, factor, p): """ Calculate public pension benefit amounts for each household. Args: w (array_like): real wage rate n (Numpy array): labor supply theta (Numpy array): social security replacement rate value for lifetime income group j t (int): time period j (int): index of lifetime income group shift (bool): whether computing for periods 0--s or 1--(s+1), =True for 1--(s+1) method (str): adjusts calculation dimensions based on 'SS' or 'TPI' e (Numpy array): effective labor units p (OG-Core Specifications object): model parameters Returns: pension (Numpy array): pension amount for each household """ # TODO: think about how can allow for transition from one # pension system to another along the time path if p.pension_system == "US-Style Social Security": pension = SS_amount(w, n, theta, t, j, shift, method, e, p) elif p.pension_system == "Defined Benefits": pension = DB_amount(w, e, n, j, p) elif p.pension_system == "Notional Defined Contribution": pension = NDC_amount(w, e, n, r, Y, j, p) elif p.pension_system == "Points System": pension = PS_amount(w, e, n, j, factor, p) else: raise ValueError( "pension_system must be one of the following: " "'US-Style Social Security', 'Defined Benefits', " "'Notional Defined Contribution', 'Points System'" ) return pension
[docs] def SS_amount(w, n, theta, t, j, shift, method, e, p): r""" Calculate public pension benefit amounts for each household under a US-style social security system. .. mathL:: pension_{j,s,t} = \theta_j \times w_t \quad \forall s > R Args: w (array_like): real wage rate n (Numpy array): labor supply theta (Numpy array): social security replacement rate value for lifetime income group j t (int): time period j (int): index of lifetime income group shift (bool): whether computing for periods 0--s or 1--(s+1), =True for 1--(s+1) method (str): adjusts calculation dimensions based on 'SS' or 'TPI' e (Numpy array): effective labor units p (OG-Core Specifications object): model parameters Returns: pension (Numpy array): pension amount for each household """ if j is not None: if method == "TPI": if n.ndim == 2: w = w.reshape(w.shape[0], 1) else: if method == "TPI": w = utils.to_timepath_shape(w) pension = np.zeros_like(n) if method == "SS": # Depending on if we are looking at b_s or b_s+1, the # entry for retirement will change (it shifts back one). # The shift boolean makes sure we start replacement rates # at the correct age. if j is None: replace_rate_adjust = p.replacement_rate_adjust[-1, :] else: replace_rate_adjust = p.replacement_rate_adjust[-1, j] if shift is False: pension[p.retire[-1] :] = replace_rate_adjust * theta * w else: pension[p.retire[-1] - 1 :] = replace_rate_adjust * theta * w elif method == "TPI": length = w.shape[0] if not shift: # retireTPI is different from retire, because in TP income # we are counting backwards with different length lists. # This will always be the correct location of retirement, # depending on the shape of the lists. retireTPI = p.retire[t : t + length] - p.S else: retireTPI = p.retire[t : t + length] - 1 - p.S if len(n.shape) == 1: if not shift: retireTPI = p.retire[t] - p.S else: retireTPI = p.retire[t] - 1 - p.S pension[retireTPI:] = ( theta[j] * p.replacement_rate_adjust[t, j] * w[retireTPI:] ) elif len(n.shape) == 2: for tt in range(pension.shape[0]): pension[tt, retireTPI[tt] :] = ( theta * p.replacement_rate_adjust[t + tt, j] * w[tt] ) else: for tt in range(pension.shape[0]): pension[tt, retireTPI[tt] :, :] = ( theta.reshape(1, p.J) * p.replacement_rate_adjust[t + tt, :].reshape(1, p.J) * w[tt] ) elif method == "TPI_scalar": # The above methods won't work if scalars are used. This option # is only called by the SS_TPI_firstdoughnutring function in TPI. pension = theta * p.replacement_rate_adjust[0, j] * w return pension
[docs] def DB_amount(w, e, n, j, p): r""" Calculate public pension from a defined benefits system. .. math:: pension{j,s,t} = \biggl[\frac{\sum_{s=R-ny}^{R-1}w_{t}e_{j,s,t} n_{j,s,t}}{ny}\biggr]\times Cy \times \alpha_{DB} \quad \forall s > R Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply; may be a partial lifetime path (length < S, for cohorts alive when the time path begins), a steady-state vector (S,) or matrix (S, J), or the full time path (T, S, J) j (int): index of lifetime income group p (OG-Core Specifications object): model parameters Returns: DB (Numpy array): pension amount for each household """ L_inc_avg = np.zeros(0) # Adjustment to turn years into model periods # TODO: could add this to parameters.py at some point equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) - 1 L_inc_avg_s = np.zeros(equiv_periods) # Retirement age is potentially time varying (PR #433), but the DB # system supports a single retirement age for now; use the # steady-state value (see Issue #1014). g_y is a scalar parameter, # but the numba loops index it, so pass it as a 1-element array. S_ret = int(np.asarray(p.retire).flat[-1]) g_y_arr = np.atleast_1d(np.asarray(p.g_y, dtype=float)).ravel() # The wage may arrive as a scalar (steady state, or one remaining # period in the TPI); the loops index it as a path, so broadcast it # to the length of the labor-supply vector. if np.ndim(w) == 0: w = np.full(n.shape[0], float(w)) if n.shape[0] < p.S: per_rmn = n.shape[0] # Pre-time-path wages: the recent past is anchored to the # period-0 wage of the current path (trend growth is handled # by the de-trending inside the benefit formula); labor comes # from the model's initial condition n_preTP (see Issue #1014) w_S = np.append((w[0] * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) DB = np.zeros(p.S) DB = DB_1dim_loop( w_S, # TODO: will need to update how the e matrix is handled # here and elsewhere to allow for it to be time varying # (see Issue #1014); use the steady-state profile for now p.e[-1, :, j] if np.ndim(p.e) == 3 else p.e[:, j], n_S, S_ret, p.S, g_y_arr, L_inc_avg_s, L_inc_avg, DB, equiv_periods, p.alpha_db, equiv_yr_contrib, ) DB = DB[-per_rmn:] else: if np.ndim(n) == 1: DB = np.zeros(p.S) DB = DB_1dim_loop( w, e, n, S_ret, p.S, g_y_arr, L_inc_avg_s, L_inc_avg, DB, equiv_periods, p.alpha_db, equiv_yr_contrib, ) elif np.ndim(n) == 2: DB = np.zeros((p.S, p.J)) L_inc_avg_sj = np.zeros((equiv_periods, p.J)) DB = DB_2dim_loop( w, e, n, S_ret, p.S, g_y_arr, L_inc_avg_sj, L_inc_avg, DB, equiv_periods, p.alpha_db, equiv_yr_contrib, ) elif np.ndim(n) == 3: T = n.shape[0] w_path = np.squeeze(np.asarray(w)) if w_path.ndim == 0: w_path = np.full(T, float(w_path)) e_ss = p.e[-1] if np.ndim(p.e) == 3 else p.e DB = DB_3dim_loop( w_path, e_ss, n, p.n_preTP, S_ret, p.S, p.J, float(g_y_arr[-1]), equiv_periods, p.alpha_db, equiv_yr_contrib, ) return DB
[docs] def NDC_amount(w, e, n, r, Y, j, p): r""" Calculate public pension from a notional defined contribution system. .. math:: pension{j,s,t} = \biggl[\sum_{s=E}^{R-1}\tau^{p}_{t}w_{t} e_{j,s,t}n_{j,s,t}(1 + g_{NDC,t})^{R-s-1}\biggr] \delta_{R, t} \quad \forall s > R Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply r (array_like): interest rate Y (array_like): GDP j (int): index of lifetime income group p (OG-Core Specifications object): model parameters Returns: NDC (Numpy array): pension amount for each household """ g_ndc_amount = g_ndc(r, Y, p) delta_ret_amount = delta_ret(r, Y, p) # Single retirement age for now (see Issue #1014); g_y is a scalar # but the numba loops index it; the steady-state wage is a scalar # but the loops index it as a path. S_ret = int(np.asarray(p.retire).flat[-1]) g_y_arr = np.atleast_1d(np.asarray(p.g_y, dtype=float)).ravel() if np.ndim(w) == 0: w = np.full(n.shape[0], float(w)) if n.shape[0] < p.S: per_rmn = n.shape[0] # Pre-time-path wages: the recent past is anchored to the # period-0 wage of the current path (trend growth is handled # by the de-trending inside the benefit formula); labor comes # from the model's initial condition n_preTP (see Issue #1014) w_S = np.append((w[0] * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) NDC_s = np.zeros(S_ret) NDC = np.zeros(p.S) NDC = NDC_1dim_loop( w_S, # steady-state earnings profile for now (see Issue #1014) p.e[-1, :, j] if np.ndim(p.e) == 3 else p.e[:, j], n_S, S_ret, p.S, g_y_arr, p.tau_p, g_ndc_amount, delta_ret_amount, NDC_s, NDC, ) NDC = NDC[-per_rmn:] else: if np.ndim(n) == 1: NDC_s = np.zeros(S_ret) NDC = np.zeros(p.S) NDC = NDC_1dim_loop( w, e, n, S_ret, p.S, g_y_arr, p.tau_p, g_ndc_amount, delta_ret_amount, NDC_s, NDC, ) elif np.ndim(n) == 2: NDC_sj = np.zeros((S_ret, p.J)) NDC = np.zeros((p.S, p.J)) NDC = NDC_2dim_loop( w, e, n, S_ret, p.S, g_y_arr, p.tau_p, g_ndc_amount, delta_ret_amount, NDC_sj, NDC, ) return NDC
[docs] def PS_amount(w, e, n, j, factor, p): r""" Calculate public pension from a points system. .. math:: pension{j,s,t} = \sum_{s=E}^{R-1}w_{t}e_{j,s,t}n_{j,s,t} \times v_{t} \quad \forall s > R Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply j (int): index of lifetime income group factor (scalar): scaling factor converting model units to dollars p (OG-Core Specifications object): model parameters Returns: PS (Numpy array): pension amount for each household """ # Single retirement age for now (see Issue #1014); g_y is # a scalar but the numba loops index it; the steady-state wage is a # scalar but the loops index it as a path. S_ret = int(np.asarray(p.retire).flat[-1]) g_y_arr = np.atleast_1d(np.asarray(p.g_y, dtype=float)).ravel() if np.ndim(w) == 0: w = np.full(n.shape[0], float(w)) if n.shape[0] < p.S: per_rmn = n.shape[0] # Pre-time-path wages: the recent past is anchored to the # period-0 wage of the current path (trend growth is handled # by the de-trending inside the benefit formula); labor comes # from the model's initial condition n_preTP (see Issue #1014) w_S = np.append((w[0] * np.ones(p.S))[:(-per_rmn)], w) n_S = np.append(p.n_preTP[:(-per_rmn), j], n) L_inc_avg_s = np.zeros(S_ret) PS = np.zeros(p.S) PS = PS_1dim_loop( w_S, # steady-state earnings profile for now (see Issue #1014) p.e[-1, :, j] if np.ndim(p.e) == 3 else p.e[:, j], n_S, S_ret, p.S, g_y_arr, p.vpoint, factor, L_inc_avg_s, PS, ) PS = PS[-per_rmn:] else: if np.ndim(n) == 1: L_inc_avg_s = np.zeros(S_ret) PS = np.zeros(p.S) PS = PS_1dim_loop( w, e, n, S_ret, p.S, g_y_arr, p.vpoint, factor, L_inc_avg_s, PS, ) elif np.ndim(n) == 2: L_inc_avg_sj = np.zeros((S_ret, p.J)) PS = np.zeros((p.S, p.J)) PS = PS_2dim_loop( w, e, n, S_ret, p.S, p.J, g_y_arr, p.vpoint, factor, L_inc_avg_sj, PS, ) return PS
[docs] def deriv_theta(r, w, e, Y, per_rmn, factor, p): """ Change in pension benefits for another unit of labor supply for pension system selected Args: r (array_like): interest rate w (array_like): real wage rate e (Numpy array): effective labor units Y (array_like): GDP per_rmn (int): number of periods remaining in the model factor (scalar): scaling factor converting model units to Returns: d_theta (Numpy array): change in pension benefits for another unit of labor supply """ # TODO: Add SS here... if p.pension_system == "Defined Benefits": d_theta = deriv_DB(w, e, per_rmn, p) d_theta = d_theta[-per_rmn:] elif p.pension_system == "Notional Defined Contribution": d_theta = deriv_NDC(r, w, e, Y, per_rmn, p) elif p.pension_system == "Points System": d_theta = deriv_PS(w, e, per_rmn, factor, p) else: raise ValueError( "pension_system must be one of the following: " "'US-style Social Security', 'Defined Benefits', " "'Notional Defined Contribution', 'Points System'" ) return d_theta
[docs] def deriv_NDC(r, w, e, Y, per_rmn, p): r""" Change in NDC pension benefits for another unit of labor supply .. math:: \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = \begin{cases} \tau^{p}_{t}w_{t}e_{j,s}(1+g_{NDC,t})^{u - s}\delta_{R,t}, & \text{if}\ s<R-1 \\ 0, & \text{if}\ s \geq R \\ \end{cases} Args: r (array_like): interest rate w (array_like): real wage rate e (Numpy array): effective labor units Y (array_like): GDP per_rmn (int): number of periods remaining in the model p (OG-Core Specifications object): model parameters Returns: d_theta (Numpy array): change in NDC pension benefits for another unit of labor supply """ # Single retirement age for now (see Issue #1014) S_ret = int(np.asarray(p.retire).flat[-1]) if per_rmn == 1: d_theta = 0 elif per_rmn < (p.S - S_ret + 1): d_theta = np.zeros(per_rmn) else: d_theta_empty = np.zeros(per_rmn) delta_ret_amount = delta_ret(r, Y, p) g_ndc_amount = g_ndc(r, Y, p) d_theta = deriv_NDC_loop( w, e, per_rmn, p.S, S_ret, p.tau_p, g_ndc_amount, delta_ret_amount, d_theta_empty, ) return d_theta
[docs] def deriv_DB(w, e, per_rmn, p): r""" Change in DB pension benefits for another unit of labor supply .. math:: \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = \begin{cases} 0 , & \text{if}\ s < R - Cy \\ w_{t}e_{j,s}\alpha_{DB}\times \frac{Cy}{ny}, & \text{if}\ R - Cy <= s < R \\ 0, & \text{if}\ s \geq R \\ \end{cases} Args: w (array_like): real wage rate e (Numpy array): effective labor units per_rmn (int): number of periods remaining in the model p (OG-Core Specifications object): model parameters Returns: d_theta: change in DB pension benefits for another unit of labor supply """ equiv_periods = int(round((p.S / 80.0) * p.avg_earn_num_years)) - 1 equiv_yr_contrib = int(round((p.S / 80.0) * p.yr_contrib)) - 1 # Single retirement age for now (see Issue #1014) S_ret = int(np.asarray(p.retire).flat[-1]) if per_rmn < (p.S - S_ret + 1): d_theta = np.zeros(p.S) else: d_theta = deriv_DB_loop( w, e, p.S, S_ret, per_rmn, equiv_periods, p.alpha_db, equiv_yr_contrib, ) return d_theta
[docs] def deriv_PS(w, e, per_rmn, factor, p): r""" Change in points system pension benefits for another unit of labor supply .. math:: \frac{\partial \theta_{j,u,t+u-s}}{\partial n_{j,s,t}} = \begin{cases} 0 , & \text{if}\ s < R \\ w_{t}e_{j,s}v_{t}, & \text{if}\ s \geq R \\ \end{cases} Args: w (array_like): real wage rate e (Numpy array): effective labor units per_rmn (int): number of periods remaining in the model factor (scalar): scaling factor converting model units to p (OG-Core Specifications object): model parameters Returns: d_theta (Numpy array): change in points system pension benefits for another unit of labor supply """ # Single retirement age for now (see Issue #1014) S_ret = int(np.asarray(p.retire).flat[-1]) if per_rmn < (p.S - S_ret + 1): d_theta = np.zeros(p.S) else: d_theta_empty = np.zeros(p.S) d_theta = deriv_PS_loop( w, e, p.S, S_ret, per_rmn, d_theta_empty, p.vpoint, factor ) d_theta = d_theta[-per_rmn:] return d_theta
# TODO: can probably assign these growth rates in the if statements in # the pension_amount function # TODO: create a parameter for pension growth rates -- a single param should do
[docs] def delta_point(r, Y, g_n, g_y, p): r""" Compute growth rate used for contributions to points system pension Args: r (array_like): interest rate Y (array_like): GDP g_n (array_like): population growth rate g_y (array_like): GDP growth rate p (OG-Core Specifications object): model parameters Returns: delta_point (Numpy array): growth rate used for contributions to points """ # TODO: Add option to allow use to enter growth rate amount # Also to allow rate to vary by year # Do this for all these growth rates for each system # Might also allow for option to grow at per capital GDP growth rate if p.points_growth_rate == "r": delta_point = r elif p.points_growth_rate == "Curr GDP": delta_point = (Y[1:] - Y[:-1]) / Y[:-1] elif p.points_growth_rate == "LR GDP": delta_point = g_y + g_n else: delta_point = g_y + g_n return delta_point
[docs] def g_ndc(r, Y, p): """ Compute growth rate used for contributions to NDC pension Args: r (array_like): interest rate Y (array_like): GDP p (OG-Core Specifications object): model parameters Returns: g_ndc (Numpy array): growth rate used for contributions to NDC """ if p.ndc_growth_rate == "r": g_ndc = r[-1] elif p.ndc_growth_rate == "Curr GDP": g_ndc = (Y[1:] - Y[:-1]) / Y[:-1] elif p.ndc_growth_rate == "LR GDP": g_ndc = p.g_y[-1] + p.g_n[-1] else: g_ndc = p.g_y[-1] + p.g_n[-1] return g_ndc
[docs] def g_dir(r, Y, g_y, g_n, dir_growth_rate): """ Compute growth rate used for contributions to NDC pension Args: r (array_like): interest rate Y (array_like): GDP g_y (array_like): GDP growth rate g_n (array_like): population growth rate dir_growth_rate (str): growth rate used for contributions to NDC Returns: g_dir (Numpy array): growth rate used for contributions to NDC """ if dir_growth_rate == "r": g_dir = r[-1] elif dir_growth_rate == "Curr GDP": g_dir = (Y[1:] - Y[:-1]) / Y[:-1] elif dir_growth_rate == "LR GDP": g_dir = g_y[-1] + g_n[-1] else: g_dir = g_y[-1] + g_n[-1] return g_dir
[docs] def delta_ret(r, Y, p): r""" Compute conversion coefficient for the NDC pension amount .. math:: \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} Args: r (array_like): interest rate Y (array_like): GDP p (OG-Core Specifications object): model parameters Returns: delta_ret (Numpy array): conversion coefficient for the NDC pension amount """ surv_rates = 1 - p.mort_rates_SS # Single retirement age for now (see Issue #1014) S_ret = int(np.asarray(p.retire).flat[-1]) dir_delta_s_empty = np.zeros(p.S - S_ret + 1) g_dir_value = g_dir(r, Y, p.g_y, p.g_n, p.dir_growth_rate) dir_delta = delta_ret_loop( p.S, S_ret, surv_rates, g_dir_value, dir_delta_s_empty ) delta_ret = 1 / (dir_delta + p.indR - p.k_ret) return delta_ret
[docs] @numba.jit(nopython=True) def deriv_DB_loop( w, e, S, S_ret, per_rmn, avg_earn_num_years, alpha_db, yr_contr ): """ Change in DB pension benefits for another unit of labor supply Args: w (array_like): real wage rate e (Numpy array): effective labor units S (int): number of periods in the model S_ret (int): retirement age per_rmn (int): number of periods remaining in the model avg_earn_num_years (int): number of years AIME is computed from alpha_db (scalar): replacement rate yr_contr (scalar): years of contribution Returns: d_theta (Numpy array): change in DB pension benefits for another unit of labor supply """ d_theta = np.zeros(per_rmn) # print("Year contribution: ", yr_contr) # print("Average earnings years: ", avg_earn_num_years) num_per_retire = S - S_ret for s in range(per_rmn): d_theta[s] = w[s] * e[s] * alpha_db * (yr_contr / avg_earn_num_years) d_theta[-num_per_retire:] = 0.0 return d_theta
[docs] @numba.jit(nopython=True) def deriv_PS_loop(w, e, S, S_ret, per_rmn, d_theta, vpoint, factor): """ Change in points system pension benefits for another unit of labor supply Args: w (array_like): real wage rate e (Numpy array): effective labor units S (int): number of periods in the model S_ret (int): retirement age per_rmn (int): number of periods remaining in the model d_theta (Numpy array): change in points system pension benefits for another unit of labor supply vpoint (scalar): value of points factor (scalar): scaling factor converting model units to local currency Returns: d_theta (Numpy array): change in points system pension benefits for another unit of labor supply """ # TODO: do we need these constants or can we scale vpoint to annual?? for s in range((S - per_rmn), S_ret): d_theta[s] = (w[s] * e[s] * vpoint * MONTHS_IN_A_YEAR) / ( factor * THOUSAND ) return d_theta
[docs] @numba.jit(nopython=True) def deriv_NDC_loop( w, e, per_rmn, S, S_ret, tau_p, g_ndc_value, delta_ret_value, d_theta ): """ Change in NDC pension benefits for another unit of labor supply Args: w (array_like): real wage rate e (Numpy array): effective labor units per_rmn (int): number of periods remaining in the model S (int): number of periods in the model S_ret (int): retirement age tau_p (scalar): tax rate g_ndc_value (scalar): growth rate of NDC pension delta_ret_value (scalar): conversion coefficient for the NDC pension amount d_theta (Numpy array): change in NDC pension benefits for another unit of labor supply Returns: d_theta (Numpy array): change in NDC pension benefits for another unit of labor supply """ for s in range((S - per_rmn), S_ret): d_theta[s - (S - per_rmn)] = ( tau_p * w[s - (S - per_rmn)] * e[s - (S - per_rmn)] * delta_ret_value * (1 + g_ndc_value) ** (S_ret - s - 1) ) return d_theta
[docs] @numba.jit(nopython=True) def delta_ret_loop(S, S_ret, surv_rates, g_dir_value, dir_delta_s): """ Compute conversion coefficient for the NDC pension amount Args: S (int): number of periods in the model S_ret (int): retirement age surv_rates (Numpy array): survival rates g_dir_value (scalar): growth rate of NDC pension dir_delta_s (Numpy array): conversion coefficient for the NDC pension amount Returns: dir_delta (scalar): conversion coefficient for the NDC pension amount """ cumul_surv_rates = np.ones(S - S_ret + 1) for s in range(S - S_ret + 1): surv_rates_vec = surv_rates[S_ret : S_ret + s + 1] surv_rates_vec[0] = 1.0 cumul_surv_rates[s] = np.prod(surv_rates_vec) cumul_g_y = np.ones(S - S_ret + 1) cumul_g_y[s] = (1 / (1 + g_dir_value)) ** s dir_delta_s[s] = cumul_surv_rates[s] * cumul_g_y[s] dir_delta = dir_delta_s.sum() return dir_delta
[docs] @numba.jit(nopython=True) def PS_1dim_loop(w, e, n, S_ret, S, g_y, vpoint, factor, L_inc_avg_s, PS): """ Calculate public pension from a points system. Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model g_y (array_like): GDP growth rate vpoint (scalar): value of points factor (scalar): scaling factor converting model units to local currency L_inc_avg_s (Numpy array): average labor income PS (Numpy array): pension amount for each household Returns: PS (Numpy array): pension amount for each household """ # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): # TODO: allow for g_y to be time varying for s in range(S_ret): L_inc_avg_s[s] = w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] PS[u] = (MONTHS_IN_A_YEAR * vpoint * L_inc_avg_s.sum()) / ( factor * THOUSAND ) return PS
[docs] @numba.jit(nopython=True) def PS_2dim_loop(w, e, n, S_ret, S, J, g_y, vpoint, factor, L_inc_avg_sj, PS): """ Calculate public pension from a points system. Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model J (int): number of lifetime income groups g_y (array_like): GDP growth rate vpoint (scalar): value of points factor (scalar): scaling factor converting model units to local currency L_inc_avg_sj (Numpy array): average labor income PS (Numpy array): pension amount for each household Returns: PS (Numpy array): pension amount for each household """ # TODO: do we need these constants or can we scale vpoint to annual?? for u in range(S_ret, S): for s in range(S_ret): L_inc_avg_sj[s, :] = ( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) PS[u, :] = (MONTHS_IN_A_YEAR * vpoint * L_inc_avg_sj.sum(axis=0)) / ( factor * THOUSAND ) return PS
[docs] @numba.jit(nopython=True) def DB_1dim_loop( w, e, n, S_ret, S, g_y, L_inc_avg_s, L_inc_avg, DB, avg_earn_num_years, alpha_db, yr_contr, ): """ Calculate public pension from a defined benefits system. Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model g_y (array_like): GDP growth rate L_inc_avg_s (Numpy array): average labor income L_inc_avg (scalar): average labor income DB (Numpy array): pension amount for each household avg_earn_num_years (int): number of years AIME is computed from alpha_db (scalar): replacement rate yr_contr (scalar): years of contribution Returns: DB (Numpy array): pension amount for each household """ for u in range(S_ret, S): for s in range(S_ret - avg_earn_num_years, S_ret): # TODO: pass t so that can pull correct g_y value # Just need to make if doing over time path makes sense # or if should just do SS L_inc_avg_s[s - (S_ret - avg_earn_num_years)] = ( w[s] / np.exp(g_y[-1] * (u - s)) * e[s] * n[s] ) L_inc_avg = L_inc_avg_s.sum() / avg_earn_num_years rep_rate = yr_contr * alpha_db DB[u] = rep_rate * L_inc_avg return DB
[docs] @numba.jit(nopython=True) def DB_2dim_loop( w, e, n, S_ret, S, g_y, L_inc_avg_sj, L_inc_avg, DB, avg_earn_num_years, alpha_db, yr_contr, ): """ Calculate public pension from a defined benefits system. Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model g_y (array_like): GDP growth rate L_inc_avg_sj (Numpy array): average labor income L_inc_avg (scalar): average labor income DB (Numpy array): pension amount for each household avg_earn_num_years (int): number of years AIME is computed from alpha_db (scalar): replacement rate yr_contr (scalar): years of contribution Returns: DB (Numpy array): pension amount for each household """ for u in range(S_ret, S): for s in range(S_ret - avg_earn_num_years, S_ret): L_inc_avg_sj[s - (S_ret - avg_earn_num_years), :] = ( w[s] / np.exp(g_y * (u - s)) * e[s, :] * n[s, :] ) L_inc_avg = L_inc_avg_sj.sum(axis=0) / avg_earn_num_years rep_rate = yr_contr * alpha_db DB[u, :] = rep_rate * L_inc_avg return DB
@numba.jit(nopython=True) def DB_3dim_loop( w_path, e_ss, n, n_preTP, S_ret, S, J, g_y, avg_earn_num_years, alpha_db, yr_contr, ): r""" Calculate public pension from a defined benefits system over the full time path. Used when the TPI solution evaluates taxes for all periods at once to compute aggregates. Each retiree's benefit is computed from their own cohort's wage and labor history: the wage entering the average for a household aged u at time t, earned at age s, is the wage at time t - (u - s). Histories that predate the time path use the period-0 wage (trend growth is handled by the de-trending in the benefit formula) and the model's initial labor supply, so this reproduces exactly what the per-cohort household solves compute and household behavior and aggregates are consistent. The inner work is vectorized rather than looped. Args: w_path (Numpy array): real wage rate path, length T e_ss (Numpy array): effective labor units, size SxJ n (Numpy array): labor supply, size TxSxJ n_preTP (Numpy array): pre-time-path labor supply, size SxJ S_ret (int): retirement age S (int): number of periods in the model J (int): number of lifetime income groups g_y (scalar): growth rate of technology avg_earn_num_years (int): number of years earnings are averaged over alpha_db (scalar): replacement rate per year of contribution yr_contr (int): years of contribution Returns: DB (Numpy array): pension amount for each household, size TxSxJ """ T = n.shape[0] s_idx = np.arange(S_ret - avg_earn_num_years, S_ret) DB = np.zeros((T, S, J)) for t in range(T): for u in range(S_ret, S): tau = t - (u - s_idx) w_hist = w_path[np.clip(tau, 0, T - 1)] n_hist = np.where( (tau >= 0)[:, None], n[np.clip(tau, 0, T - 1), s_idx, :], n_preTP[s_idx, :], ) L = (w_hist[:, None] / np.exp(g_y * (u - s_idx))[:, None]) * ( e_ss[s_idx, :] * n_hist ) DB[t, u, :] = ( (L.sum(axis=0) / avg_earn_num_years) * yr_contr * alpha_db ) return DB
[docs] def NDC_1dim_loop(w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_s, NDC): """ Calculate public pension from a notional defined contribution Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model g_y (array_like): GDP growth rate tau_p (scalar): tax rate g_ndc (scalar): growth rate of NDC pension delta_ret (scalar): conversion coefficient for the NDC pension amount NDC_s (Numpy array): average labor income NDC (Numpy array): pension amount for each household Returns: NDC (Numpy array): pension amount for each household """ for u in range(S_ret, S): for s in range(0, S_ret): # TODO: update so can take g_y from period t NDC_s[s] = ( tau_p * (w[s] / np.exp(g_y[-1] * (u - s))) * e[s] * n[s] * ((1 + g_ndc) ** (S_ret - s - 1)) ) NDC[u] = delta_ret * NDC_s.sum() return NDC
[docs] @numba.jit(nopython=True) def NDC_2dim_loop( w, e, n, S_ret, S, g_y, tau_p, g_ndc, delta_ret, NDC_sj, NDC ): """ Calculate public pension from a notional defined contribution Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply S_ret (int): retirement age S (int): number of periods in the model g_y (array_like): GDP growth rate tau_p (scalar): tax rate g_ndc (scalar): growth rate of NDC pension delta_ret (scalar): conversion coefficient for the NDC pension amount NDC_sj (Numpy array): average labor income NDC (Numpy array): pension amount for each household Returns: NDC (Numpy array): pension amount for each household """ for u in range(S_ret, S): for s in range(0, S_ret): NDC_sj[s, :] = ( tau_p * (w[s] / np.exp(g_y * (u - s))) * e[s, :] * n[s, :] * ((1 + g_ndc) ** (S_ret - s - 1)) ) NDC[u, :] = delta_ret * NDC_sj.sum(axis=0) return NDC