Source code for ogcore.pensions

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

# set constants
MONTHS_IN_A_YEAR = 12
THOUSAND = 1000


[docs] @numba.jit(nopython=True) def earnings_band_at(x, lo, hi, period): """ Part of labor income ``x`` (w * e * n, in model units) that lies between the cutoffs in effect in the model period in which it was earned, min(max(x, lo), hi) - lo. With ``lo = 0`` and ``hi = inf`` this returns ``x`` unchanged. Args: x (scalar or Numpy array): labor income, w * e * n lo (Numpy array): calendar path of the lower cutoff, in model units of labor income hi (Numpy array): calendar path of the upper cutoff, in model units of labor income period (int): model period in which ``x`` was earned; periods before or after a path read its first or last element Returns: band (scalar or Numpy array): part of ``x`` between the cutoffs in effect in ``period`` """ lo_t = lo[min(max(period, 0), lo.size - 1)] hi_t = hi[min(max(period, 0), hi.size - 1)] return np.minimum(np.maximum(x, lo_t), hi_t) - lo_t
def _band_paths(earn_lo, earn_hi, method): """ Calendar paths of the earnings band cutoffs used by ``DB_amount`` and ``NDC_amount``. Args: earn_lo (scalar, array_like, or None): lower cutoff, in model units of labor income, a single value or a path by period earn_hi (scalar, array_like, or None): upper cutoff method (str): 'SS', 'TPI', or 'TPI_scalar' Returns: lo, hi (Numpy arrays or None): None, None when no band is set. Otherwise 1-D float arrays: the full paths for 'TPI', and the final (steady-state) or first value for 'SS' or 'TPI_scalar', matching the rows the other pension parameters are read from. """ if earn_lo is None and earn_hi is None: return None, None lo = np.atleast_1d( np.asarray(0.0 if earn_lo is None else earn_lo, dtype=float) ).ravel() hi = np.atleast_1d( np.asarray(np.inf if earn_hi is None else earn_hi, dtype=float) ).ravel() if method == "SS": lo, hi = lo[-1:], hi[-1:] elif method == "TPI_scalar": lo, hi = lo[:1], hi[:1] return np.ascontiguousarray(lo), np.ascontiguousarray(hi) def _path_from(path, t): """ A calendar path starting in period t (its last element if t is past its end), or None if ``path`` is None. """ if path is None: return None return np.ascontiguousarray(path[min(t, path.size - 1) :]) def cohort_array(name, p, by_j=False): """ Return the pension parameter ``name`` as an array by period and age. After ``Specifications.compute_default_params`` the cohort pension parameters (``retire``, ``avg_earn_num_years``, ``yr_contrib``, ``tau_p``, ``k_ret``, ``indR``) are (T + S) x S arrays holding, for the household of age s in period t, the value that applies to its birth cohort, and ``alpha_db`` is (T + S) x S x J. This helper also accepts the forms a caller may set by hand: a single number is broadcast to the full shape, and an array without the age dimension (a path by period, or by period and lifetime income group) is taken to hold the same value at every age in a period. Args: name (str): attribute name on ``p`` p (OG-Core Specifications object): model parameters by_j (bool): whether the parameter also varies by lifetime income group, in which case the array is (T + S) x S x J Returns: arr (Numpy array): the parameter by period and age Raises: ValueError: if the attribute cannot be broadcast to that shape """ arr = np.asarray(getattr(p, name), dtype=float) shape = (p.T + p.S, p.S, p.J) if by_j else (p.T + p.S, p.S) if arr.ndim == len(shape) and arr.shape == shape: return arr if arr.ndim == 0 or (arr.size > 0 and np.all(arr == arr.flat[0])): # a single value (possibly an array built for other dimensions, # as when a test changes S after the object is created) return np.broadcast_to(arr.flat[0], shape) if arr.ndim == len(shape) - 1 and arr.shape[0] == shape[0]: return np.broadcast_to(np.expand_dims(arr, 1), shape) raise ValueError( f"{name} must be a number or an array of shape {shape} (or the " f"same without the age dimension), got shape {arr.shape}" ) def cohort_path(arr, t, per_rmn, p): """ Lifetime path of a period-by-age array for one household. The household has ``per_rmn`` periods of life remaining in period t, so it is age S - per_rmn in period t and lives age u in period t - (S - per_rmn) + u: its path runs down a diagonal of ``arr``. Ages lived before the time path begins read the first row and ages after it ends read the last row, matching how the DB and NDC formulas treat wages and labor supply outside the path. Args: arr (Numpy array): (T + S) x S or (T + S) x S x J array t (int): model period of the first of the remaining periods per_rmn (int): number of periods of life remaining p (OG-Core Specifications object): model parameters Returns: path (Numpy array): length S (or S x J) path by age """ age0 = p.S - per_rmn rows = np.clip(t - age0 + np.arange(p.S), 0, arr.shape[0] - 1) return arr[rows, np.arange(p.S)] def pension_periods(p): """ Convert the pension parameters measured in calendar years into integer model periods, by period and age. Args: p (OG-Core Specifications object): model parameters Returns: equiv_periods (Numpy array): (T + S) x S number of model periods over which earnings are averaged (``avg_earn_num_years``) equiv_yr_contrib (Numpy array): (T + S) x S number of model periods of contributions credited (``yr_contrib``) S_ret (Numpy array): (T + S) x S model period of retirement Raises: ValueError: if the averaging window is shorter than one model period or longer than the working life for any household, in which case the window would index before the start of the lifetime The result depends only on ``S``, ``T`` and the three parameters above, which do not change while the model solves, yet the household problem asks for it several times per evaluation. It is therefore cached on ``p`` and recomputed only when one of them is reassigned (each parameter is compared by identity, so changing an array's elements in place is not detected). The returned arrays are read-only, as they are shared between calls. """ key = (p.S, p.T, p.avg_earn_num_years, p.yr_contrib, p.retire) cached = getattr(p, "_pension_periods_cache", None) if ( cached is not None and cached[0][:2] == key[:2] and all(a is b for a, b in zip(cached[0][2:], key[2:])) ): return cached[1] per_year = p.S / 80.0 avg_years = cohort_array("avg_earn_num_years", p) equiv_periods = np.rint(per_year * avg_years).astype(int) equiv_yr_contrib = np.rint( per_year * cohort_array("yr_contrib", p) ).astype(int) S_ret = cohort_array("retire", p).astype(int) if np.any(equiv_periods < 1): raise ValueError( "avg_earn_num_years must cover at least one model period: " f"avg_earn_num_years={avg_years.min()} with S={p.S} " f"gives {equiv_periods.min()} periods." ) if np.any(equiv_periods > S_ret): bad = np.argwhere(equiv_periods > S_ret)[0] raise ValueError( "avg_earn_num_years cannot exceed the working life: " f"avg_earn_num_years={avg_years[tuple(bad)]} is " f"{equiv_periods[tuple(bad)]} model periods, but the " f"household of age {bad[1]} in period {bad[0]} retires after " f"{S_ret[tuple(bad)]} periods." ) result = (equiv_periods, equiv_yr_contrib, S_ret) for arr in result: arr.setflags(write=False) p._pension_periods_cache = (key, result) return result
[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. # The Social Security window keeps the one-period shift it has always # used (so pinned steady-state and time-path results are unchanged); # see pension_periods for the unshifted conversion the DB system uses. # The shift is floored at one period so that a one-year window cannot # produce a 0/0 replacement rate, and the window is bounds-checked. equiv_periods, _, S_ret = pension_periods(p) equiv_periods, S_ret = int(equiv_periods[-1, -1]), int(S_ret[-1, -1]) equiv_periods = max(equiv_periods - 1, 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[:S_ret, :], 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
def replacement_rate_adjustment(pension, t, j, method, p): """ Return the replacement rate adjustment, shaped to broadcast against pension. US-Style Social Security applies this itself inside SS_amount. The other three systems do not, so pension_amount applies it for them. Along the time path the multiplier varies with each cohort's own year, so it is indexed at t + tt for row tt. A 1D pension array is one cohort's remaining lifetime starting in period t, so entry tt is paid in period t + tt and gets that year's multiplier. The steady state uses the final value of the parameter, so a permanent adjustment carries into the steady state that the time path converges to. The multiplier scales the whole pension array, while SS_amount scales only the entries from retirement on. These are equivalent only because DB, NDC, and PS benefits are zero before retirement; a system that pays benefits before retirement would need the SS_amount treatment. Args: pension (Numpy array): pension amount for each household t (int): model period j (int): index of lifetime income group, None if all groups method (str): 'SS', 'TPI', or 'TPI_scalar' p (OG-Core Specifications object): model parameters Returns: adjustment (array_like): multiplier on the replacement rate """ adjust = p.replacement_rate_adjust if method == "SS": return adjust[-1, :] if j is None else adjust[-1, j] if method == "TPI_scalar": return adjust[0, j] length = pension.shape[0] if pension.ndim == 1: return adjust[t : t + length, j] if pension.ndim == 2: return adjust[t : t + length, j].reshape(length, 1) return adjust[t : t + length, :].reshape(length, 1, p.J)
[docs] def pension_amount( r, w, n, ndc_rates, theta, t, j, shift, method, e, factor, p ): """ Calculate public pension benefit amounts for each household. Args: r (array_like): real interest rate w (array_like): real wage rate n (Numpy array): labor supply ndc_rates (NDCRates): calendar paths of the NDC credited and discount rates (``ndc_rates_path``), used only by the notional defined contribution system; None for the steady-state rates 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, t, j, method, p) pension = pension * replacement_rate_adjustment( pension, t, j, method, p ) elif p.pension_system == "Notional Defined Contribution": pension = NDC_amount(w, e, n, r, ndc_rates, t, j, method, p) pension = pension * replacement_rate_adjustment( pension, t, j, method, p ) elif p.pension_system == "Points System": pension = PS_amount(w, e, n, j, factor, p) pension = pension * replacement_rate_adjustment( pension, t, j, method, p ) elif p.pension_system == "Hybrid DB-NDC": pension = DB_NDC_hybrid_amount(w, e, n, r, ndc_rates, t, j, method, p) pension = pension * replacement_rate_adjustment( pension, t, j, method, p ) else: raise ValueError( "pension_system must be one of the following: " "'US-Style Social Security', 'Defined Benefits', " "'Notional Defined Contribution', 'Points System', " "'Hybrid DB-NDC'" ) return pension
def retirement_mask(t, length, shift, method, ndim, p): """ Boolean array marking the households that are at or past their own retirement age. ``p.retire`` is a (T + S) x S integer array giving, for the household of age s in period t, the model period at which it becomes eligible for the public pension. A cohort's path runs down a diagonal of that array, so the household path reads the cohort's own retirement age rather than the one in force in a single calendar year. Args: t (int): first model period covered by the arrays (ignored for 'SS' and 'TPI_scalar') length (int): number of periods covered by the arrays along the time path; for a 1D cohort path this is the number of periods of life remaining shift (bool): whether computing for periods 0--s or 1--(s+1), =True for 1--(s+1), which moves eligibility one age earlier method (str): 'SS', 'TPI', or 'TPI_scalar' ndim (int): number of dimensions of the labor supply array the mask must broadcast against: 1 for a single cohort path or an S vector, 2 for S x J or (length) x S, 3 for (length) x S x J p (OG-Core Specifications object): model parameters Returns: mask (Numpy array): boolean, shaped to broadcast against the pension array """ retire = np.asarray(p.retire) S = p.S ages = np.arange(S) offset = 1 if shift else 0 if method == "SS": # the steady state uses the final period's retirement age mask = ages >= retire[-1, :] - offset if ndim == 2: mask = mask.reshape(S, 1) elif method == "TPI_scalar": # the oldest household in period 0 mask = np.array(S - 1 >= retire[0, S - 1] - offset) elif method == "TPI": if ndim == 1: # One cohort with ``length`` periods of life remaining, # starting in period t at age S - length: its own retirement # ages run down the diagonal of retire with offset # S - length, the same extraction the household problem # uses for chi_n and rho. cohort_retire = np.diagonal( retire[t : t + S, :], offset=S - length ) mask = ages[S - length :] >= cohort_retire - offset else: # cross sections for periods t, ..., t + length - 1 mask = ages.reshape(1, S) >= retire[t : t + length, :] - offset if ndim == 3: mask = mask[:, :, np.newaxis] else: raise ValueError( "method must be one of 'SS', 'TPI', or 'TPI_scalar', got " f"{method!r}" ) return mask
[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. .. math:: pension_{j,s,t} = \theta_j \times w_t \quad \forall s \geq R_{t-s} where $R_{t-s}$ is the retirement age of the cohort born in period $t - s$, read from the (T + S) x S array ``p.retire``. Args: w (array_like): real wage rate n (Numpy array): labor supply theta (array_like): social security replacement rate before replacement_rate_adjust scaling. A length J vector when j is None; a scalar (or length 1 array) for group j when n is 1D or method is 'TPI_scalar' 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 Notes: replacement_rate_adjust is applied here, and only here, for the US-style system, so callers pass the unadjusted theta. Along the time path the factor is indexed by the year the benefit is paid and the retirement age by the household's own cohort, so the single-cohort path agrees with the (T, S, J) array that the aggregates use. The steady state uses the final values of both. """ n = np.asarray(n) w = np.asarray(w) theta = np.asarray(theta) adjust = p.replacement_rate_adjust 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: # a scalar when J = 1 and n is an S vector, else (1, J) benefit = np.squeeze(adjust[-1, :] * theta * w) if n.ndim == 2: benefit = benefit.reshape(1, -1) else: benefit = adjust[-1, j] * theta * w benefit = np.broadcast_to(benefit, n.shape) mask = retirement_mask(None, None, shift, method, n.ndim, p) elif method == "TPI": length = w.shape[0] if n.ndim == 1: # One cohort's remaining lifetime: entry tt is paid in # period t + tt, so the adjustment is indexed by that year. benefit = theta * adjust[t : t + length, j] * w elif n.ndim == 2: benefit = ( theta * adjust[t : t + length, j].reshape(length, 1) * w.reshape(length, 1) ) else: benefit = ( theta.reshape(1, 1, p.J) * adjust[t : t + length, :].reshape(length, 1, p.J) * utils.to_timepath_shape(w) ) mask = retirement_mask(t, length, shift, method, n.ndim, p) 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. benefit = theta * adjust[0, j] * w mask = retirement_mask(None, None, shift, method, 1, p) else: raise ValueError( "method must be one of 'SS', 'TPI', or 'TPI_scalar', got " f"{method!r}" ) pension = np.where(mask, benefit, 0.0) return pension
[docs] def DB_amount(w, e, n, t, j, method, p, earn_lo=None, earn_hi=None): r""" Calculate public pension from a defined benefits system. .. math:: pension_{j,s,t} = \biggl[\frac{\sum_{u=R-ny}^{R-1}w_{t-s+u} e_{j,u,t-s+u}n_{j,u,t-s+u}}{ny}\biggr]\times Cy \times \alpha^{DB}_{j,s,t} \quad \forall s \geq R where the retirement age $R$, the averaging window $ny$ (``avg_earn_num_years``), the years of contribution $Cy$ (``yr_contrib``), and the replacement rate $\alpha^{DB}$ (``alpha_db``) are those of the household's own birth cohort at the age and period the benefit is paid, read from the period-by-age arrays that ``pension_regimes`` builds. With no regimes, $R$, $ny$, and $Cy$ are the same for everyone and $\alpha^{DB}_{j,t}$ varies only by lifetime income group and the calendar year of payment. 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) t (int): model period of the first element of ``n`` (for a partial or full lifetime path) or of the first row of ``n`` (for the full time path) j (int): index of lifetime income group, None if all groups method (str): adjusts calculation dimensions based on 'SS', 'TPI', or 'TPI_scalar' p (OG-Core Specifications object): model parameters earn_lo (scalar, array_like, or None): only labor income w * e * n above earn_lo enters the benefit (used by the hybrid DB-NDC system). In model units of labor income, a single value or a path by model period of length T + S; the cutoff in effect in the period each income was earned applies. None (the default) for no lower cutoff. earn_hi (scalar, array_like, or None): only labor income below earn_hi enters the benefit, as for earn_lo. None (the default) for no upper cutoff. Returns: DB (Numpy array): pension amount for each household """ ny, cy, S_ret = pension_periods(p) alpha_db = cohort_array("alpha_db", p, by_j=True) # g_y is a scalar parameter, but the numba loops index it, so pass # it as a 1-element array. 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)) earn_lo, earn_hi = _band_paths(earn_lo, earn_hi, method) 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) # The first doughnut ring (method 'TPI_scalar') solves the # household in its final period of life at t = 0 and passes # t = None, as every other TPI_scalar branch indexes period 0 if method == "TPI_scalar": t = 0 # This household is age S - per_rmn at time t, so its benefit # at age u is paid in period t + u - (S - per_rmn); its own # cohort's pension parameters run down that diagonal S_ret_u = cohort_path(S_ret, t, per_rmn, p) ny_u = cohort_path(ny, t, per_rmn, p) cy_u = cohort_path(cy, t, per_rmn, p) alpha_db_u = cohort_path(alpha_db, t, per_rmn, p)[:, j] # Effective labor units over this household's lifetime: the # profile in effect in the period each age is lived in. Ages # before the time path begins use the period-0 profile (as the # wage above) and ages beyond the end of the path use the # final (steady-state) profile. if np.ndim(p.e) == 3: e_S = cohort_path(np.asarray(p.e), t, per_rmn, p)[:, j] else: e_S = p.e[:, j] DB = DB_1dim_loop( w_S, e_S, n_S, S_ret_u, p.S, g_y_arr, np.zeros(p.S), ny_u, alpha_db_u, cy_u, earn_lo, earn_hi, t - (p.S - per_rmn), ) DB = DB[-per_rmn:] else: if np.ndim(n) == 1: if method == "SS": row = -1 elif method == "TPI_scalar": row = 0 else: row = None if row is None: # full lifetime of a cohort that is age 0 at time t S_ret_u = cohort_path(S_ret, t, p.S, p) ny_u = cohort_path(ny, t, p.S, p) cy_u = cohort_path(cy, t, p.S, p) alpha_db_u = cohort_path(alpha_db, t, p.S, p)[:, j] else: S_ret_u, ny_u, cy_u = S_ret[row], ny[row], cy[row] alpha_db_u = alpha_db[row, :, j] DB = DB_1dim_loop( w, e, n, S_ret_u, p.S, g_y_arr, np.zeros(p.S), ny_u, alpha_db_u, cy_u, earn_lo, earn_hi, # period of age 0 (the band paths of 'SS' and # 'TPI_scalar' have a single value) t if row is None else 0, ) elif np.ndim(n) == 2: # cross-section of all S x J households in a single period if method == "SS": row = -1 elif method == "TPI_scalar": row = 0 else: row = t DB = DB_2dim_loop( w, e, n, S_ret[row], p.S, g_y_arr, np.zeros((p.S, p.J)), ny[row], alpha_db[row], cy[row], earn_lo, earn_hi, max(row, 0), ) 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 # parameters in effect for each (period, age) of the path rows = np.clip(t + np.arange(T), 0, S_ret.shape[0] - 1) DB = DB_3dim_loop( w_path, e_ss, n, p.n_preTP, S_ret[rows], p.S, p.J, float(g_y_arr[-1]), ny[rows], np.ascontiguousarray(alpha_db[rows]), cy[rows], # the loop counts periods from the first row of n _path_from(earn_lo, t), _path_from(earn_hi, t), ) return DB
[docs] def NDC_amount( w, e, n, r, ndc_rates, t, j, method, p, earn_lo=None, earn_hi=None ): r""" Calculate public pension from a notional defined contribution system. .. math:: pension_{j,s,t} = \biggl[\sum_{u=E}^{R-1}\tau^{p}_{u,t-s+u} w_{t-s+u}e_{j,u,t-s+u}n_{j,u,t-s+u} \prod_{a=u+1}^{R-1}(1 + g_{NDC,t-s+a}) \biggr]\delta_{R,s,t} \quad \forall s \geq R where the retirement age $R$ and the conversion coefficient $\delta_{R,s,t}$ (through ``k_ret`` and ``indR``) are those of the household's own birth cohort at the age and period the benefit is paid, and the contribution rate $\tau^{p}_{u,t-s+u}$ is the one the cohort faced at each working age u, all read from the period-by-age arrays that ``pension_regimes`` builds. Each contribution earns the rate credited in each period between the contribution and retirement, $g_{NDC,t}$, and the conversion coefficient discounts payments at the rate in the period the household retires. 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) r (array_like): interest rate, used for the steady-state rates when ``ndc_rates`` is not given ndc_rates (NDCRates): calendar paths of the credited and discount rates from ``ndc_rates_path``; anything else (such as None) uses the steady-state rates, with the 'r' options reading the last element of ``r`` t (int): model period of the first element of ``n`` (for a partial or full lifetime path) or of the first row of ``n`` (for the full time path) j (int): index of lifetime income group, None if all groups method (str): adjusts calculation dimensions based on 'SS', 'TPI', or 'TPI_scalar' p (OG-Core Specifications object): model parameters earn_lo (scalar, array_like, or None): only labor income w * e * n above earn_lo enters the benefit (used by the hybrid DB-NDC system). In model units of labor income, a single value or a path by model period of length T + S; the cutoff in effect in the period each income was earned applies. None (the default) for no lower cutoff. earn_hi (scalar, array_like, or None): only labor income below earn_hi enters the benefit, as for earn_lo. None (the default) for no upper cutoff. Returns: NDC (Numpy array): pension amount for each household """ if not isinstance(ndc_rates, NDCRates): ndc_rates = NDCRates( np.array([g_ndc(r, None, p)]), np.array([g_dir(r, None, p)]) ) credit = np.ascontiguousarray(ndc_rates.credit, dtype=float) discount = ndc_rates.discount S_ret = cohort_array("retire", p).astype(int) tau_p = cohort_array("tau_p", p) indR = cohort_array("indR", p) k_ret = cohort_array("k_ret", p) # 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. 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)) earn_lo, earn_hi = _band_paths(earn_lo, earn_hi, method) 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) # The first doughnut ring (method 'TPI_scalar') solves the # household in its final period of life at t = 0 and passes # t = None, as every other TPI_scalar branch indexes period 0 if method == "TPI_scalar": t = 0 # this household's own cohort parameters, by age S_ret_u = cohort_path(S_ret, t, per_rmn, p) tau_p_u = cohort_path(tau_p, t, per_rmn, p) # the household is age S - per_rmn in period t, so it reaches # retirement age R in period t - (S - per_rmn) + R t_birth = t - (p.S - per_rmn) delta_ret_u = delta_ret( r, None, p, S_ret_u, cohort_path(indR, t, per_rmn, p), cohort_path(k_ret, t, per_rmn, p), t_ret=t_birth + S_ret_u, g_dir_path=discount, ) 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_u, p.S, g_y_arr, tau_p_u, credit, t_birth, delta_ret_u, np.zeros(p.S), earn_lo, earn_hi, ) NDC = NDC[-per_rmn:] else: if np.ndim(n) == 1: if method == "SS": row = -1 elif method == "TPI_scalar": row = 0 else: row = None if row is None: # full lifetime of a cohort that is age 0 at time t S_ret_u = cohort_path(S_ret, t, p.S, p) tau_p_u = cohort_path(tau_p, t, p.S, p) indR_u = cohort_path(indR, t, p.S, p) k_ret_u = cohort_path(k_ret, t, p.S, p) t_birth = t else: S_ret_u, tau_p_u = S_ret[row], tau_p[row] indR_u, k_ret_u = indR[row], k_ret[row] # the steady state (rates do not vary), or a household # born in period 0 t_birth = 0 t_ret_u = None if row == -1 else t_birth + S_ret_u NDC = NDC_1dim_loop( w, e, n, S_ret_u, p.S, g_y_arr, tau_p_u, credit, t_birth, delta_ret( r, None, p, S_ret_u, indR_u, k_ret_u, t_ret_u, g_dir_path=discount, ), np.zeros(p.S), earn_lo, earn_hi, ) elif np.ndim(n) == 2: # cross-section of all S x J households in a single period if method == "SS": row = -1 elif method == "TPI_scalar": row = 0 else: row = t # the household of age u in period `row` retired in period # row - u + R (None in the steady state) t_ret_u = None if row == -1 else row - np.arange(p.S) + S_ret[row] NDC = NDC_2dim_loop( w, e, n, S_ret[row], p.S, g_y_arr, tau_p[row], credit, 0 if row == -1 else row, delta_ret( r, None, p, S_ret[row], indR[row], k_ret[row], t_ret_u, g_dir_path=discount, ), np.zeros((p.S, p.J)), earn_lo, earn_hi, ) 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 # parameters in effect for each (period, age) of the path rows = np.clip(t + np.arange(T), 0, S_ret.shape[0] - 1) # the household of age u in period t + i retired in period # t + i - u + R t_ret_path = ( (t + np.arange(T))[:, None] - np.arange(p.S)[None, :] ) + S_ret[rows] NDC = NDC_3dim_loop( w_path, e_ss, n, p.n_preTP, S_ret[rows], p.S, p.J, float(g_y_arr[-1]), np.ascontiguousarray(tau_p[rows]), # the loop counts periods from the first row of n credit[min(t, credit.size - 1) :], delta_ret( r, None, p, S_ret[rows], indR[rows], k_ret[rows], t_ret_path, g_dir_path=discount, ), _path_from(earn_lo, t), _path_from(earn_hi, t), ) return NDC
[docs] def DB_NDC_hybrid_amount(w, e, n, r, ndc_rates, t, j, method, p): r""" Calculate public pension from a hybrid defined benefit and notional defined contribution system. Labor income $w_t e_{j,s,t} n_{j,s,t}$ is split into two bands at the cutoffs $f_{1,t} \bar{y}$ and $f_{2,t} \bar{y}$, where $f_{1,t}$ and $f_{2,t}$ are ``factor1_db_ndc_hybrid`` and ``factor2_db_ndc_hybrid`` in period t and $\bar{y}$ is ``avg_labor_inc_t1_model_units``, the average labor income in the first model period in model units, taken from outside the model. The cutoffs therefore do not depend on any endogenous variable. Both factors may vary over time; the cutoffs that apply to any income are those of the period in which it was earned. Income up to $f_{1,t} \bar{y}$ is subject to the payroll tax ``tau_payroll`` and earns a defined benefit computed with the ``DB_amount`` formula and parameters on that capped income. Income between $f_{1,t} \bar{y}$ and $f_{2,t} \bar{y}$ is subject to the NDC contribution rate ``tau_p`` and earns a notional defined contribution benefit computed with the ``NDC_amount`` formula and parameters. Income above $f_{2,t} \bar{y}$ earns no benefit. The pension is the sum: .. math:: pension_{j,s,t} = pension^{DB}_{j,s,t}\bigl(\min\{w e n, f_1 \bar{y}\}\bigr) + pension^{NDC}_{j,s,t}\bigl(\max\{ \min\{w e n, f_2 \bar{y}\} - f_1 \bar{y}, 0\}\bigr) \quad \forall s \geq R where the argument of each term is the labor income that enters that system, with the cutoffs of the period of each earning. Args: w (array_like): real wage rate e (Numpy array): effective labor units n (Numpy array): labor supply; same shapes as accepted by ``DB_amount`` and ``NDC_amount`` r (array_like): interest rate, used for the steady-state NDC rates when ``ndc_rates`` is not given ndc_rates (NDCRates): calendar paths of the NDC credited and discount rates (``ndc_rates_path``), or None for the steady-state rates; see ``NDC_amount`` t (int): model period of the first element of ``n`` j (int): index of lifetime income group, None if all groups method (str): adjusts calculation dimensions based on 'SS', 'TPI', or 'TPI_scalar' p (OG-Core Specifications object): model parameters Returns: pension (Numpy array): pension amount for each household Raises: ValueError: if ``factor2_db_ndc_hybrid`` is less than ``factor1_db_ndc_hybrid`` in any period """ # paths by model period (length T + S once Specifications extends # them); a single value is also accepted factor1 = np.atleast_1d( np.asarray(p.factor1_db_ndc_hybrid, dtype=float) ).ravel() factor2 = np.atleast_1d( np.asarray(p.factor2_db_ndc_hybrid, dtype=float) ).ravel() n_per = max(factor1.size, factor2.size) f1_full = factor1[np.minimum(np.arange(n_per), factor1.size - 1)] f2_full = factor2[np.minimum(np.arange(n_per), factor2.size - 1)] if np.any(f2_full < f1_full): bad = int(np.argmax(f2_full < f1_full)) raise ValueError( "factor2_db_ndc_hybrid must be at least factor1_db_ndc_hybrid " f"in every period: in period {bad}, factor1_db_ndc_hybrid=" f"{f1_full[bad]} and factor2_db_ndc_hybrid={f2_full[bad]}" ) # cutoffs in model units of labor income cut1 = factor1 * p.avg_labor_inc_t1_model_units cut2 = factor2 * p.avg_labor_inc_t1_model_units DB = DB_amount(w, e, n, t, j, method, p, earn_lo=0.0, earn_hi=cut1) NDC = NDC_amount( w, e, n, r, ndc_rates, t, j, method, p, earn_lo=cut1, earn_hi=cut2, ) return DB + 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, t, j, 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 t (int): model period j (int): index of lifetime income group p (OG-Core Specifications object): model parameters Returns: d_theta (Numpy array): change in pension benefits for another unit of labor supply """ # TODO: Add Social Security here... if p.pension_system == "Defined Benefits": d_theta = deriv_DB(w, e, per_rmn, t, j, 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 """ # steady-state retirement age and contribution rate S_ret = int(cohort_array("retire", p)[-1, -1]) tau_p = float(cohort_array("tau_p", p)[-1, -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, tau_p, g_ndc_amount, delta_ret_amount, d_theta_empty, ) return d_theta
[docs] def deriv_DB(w, e, per_rmn, t, j, 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}_{j,t+R-s}\times \frac{Cy}{ny}, & \text{if}\ R - Cy <= s < R \\ 0, & \text{if}\ s \geq R \\ \end{cases} With a replacement rate that varies over time, the change in the benefit paid at each age u depends on the rate in effect at that time. This function returns a single derivative for each working age s, evaluated at the rate in effect when the household retires (period t + R - s), as an approximation. Args: w (array_like): real wage rate e (Numpy array): effective labor units per_rmn (int): number of periods remaining in the model t (int): model period j (int): index of lifetime income group p (OG-Core Specifications object): model parameters Returns: d_theta: change in DB pension benefits for another unit of labor supply """ # steady-state values of the pension periods equiv_periods, equiv_yr_contrib, S_ret = ( int(a[-1, -1]) for a in pension_periods(p) ) alpha_db = cohort_array("alpha_db", p, by_j=True) if per_rmn < (p.S - S_ret + 1): d_theta = np.zeros(p.S) else: # element s of the loop is a household of age S - per_rmn + s # at time t, which retires in period t + S_ret - age age = p.S - per_rmn + np.arange(per_rmn) idx = np.clip(t + S_ret - age, 0, alpha_db.shape[0] - 1) alpha_db_s = alpha_db[idx, S_ret, j] d_theta = deriv_DB_loop( w, e, p.S, S_ret, per_rmn, equiv_periods, alpha_db_s, 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
NDC_GROWTH_CHOICES = ("r", "Curr GDP", "LR GDP", "wage bill", "avg wage")
[docs] class NDCRates(NamedTuple): """ Calendar paths of the rates used in a notional defined contribution system, built by ``ndc_rates_path``. Both fields are length T + S on the time path, or length 1 for a steady state. Periods before the path read the first element and periods after it the last. """ #: Numpy array: rate credited to notional balances in each model #: period, the growth realized from the previous period (after any #: averaging set by ``ndc_growth_avg_years``) credit: np.ndarray #: Numpy array: rate used to discount payments in the conversion #: coefficient of a household retiring in each model period discount: np.ndarray
def _ss_growth_rate(choice, r, p): """ Steady-state value of an NDC growth rate. Stationarized aggregates are constant in the steady state, so GDP and the wage bill grow at the rate of productivity and population growth, exp(g_y)(1 + g_n) - 1, and the average wage at the rate of productivity growth, exp(g_y) - 1. Args: choice (str): one of ``NDC_GROWTH_CHOICES`` r (array_like): interest rate; its last element is used p (OG-Core Specifications object): model parameters Returns: rate (scalar): steady-state growth rate per model period """ g_y = float(np.asarray(p.g_y).flat[-1]) g_n_ss = float(getattr(p, "g_n_ss", np.asarray(p.g_n).flat[-1])) if choice == "r": return float(np.asarray(r).flat[-1]) elif choice in ("LR GDP", "Curr GDP", "wage bill"): return np.exp(g_y) * (1.0 + g_n_ss) - 1.0 elif choice == "avg wage": return np.exp(g_y) - 1.0 raise ValueError( f"NDC growth rate must be one of {NDC_GROWTH_CHOICES}, got {choice!r}" )
[docs] def growth_rate_path(choice, r, Y, w, L, p, method): r""" Calendar path of an NDC growth rate. Element t is the growth realized from period t - 1 to period t, in real, non-stationarized terms. Stationarized aggregates are divided by $e^{g_y t}$ and by population, and ``p.g_n[t]`` is population growth from t to t + 1, so for an aggregate X .. math:: g_{t} = \frac{\hat{X}_{t}}{\hat{X}_{t-1}}e^{g_y} (1 + g_{n,t-1}) - 1 where X is GDP for 'Curr GDP' and the wage bill wL for 'wage bill'. 'avg wage' is the growth of earnings per person, the same expression without population growth. 'r' is the interest rate path and 'LR GDP' the constant steady-state growth of GDP. Growth into period 0 uses the period-0 level as the level of the period before. Args: choice (str): one of ``NDC_GROWTH_CHOICES`` r (array_like): interest rate path Y (array_like): GDP path, used for 'Curr GDP' w (array_like): real wage path, used for 'wage bill' and 'avg wage' L (array_like): aggregate labor path, used for 'wage bill' and 'avg wage' p (OG-Core Specifications object): model parameters method (str): 'SS' for the steady-state value or 'TPI' for the time path Returns: rate (Numpy array): length 1 in the steady state, else T + S """ if method == "SS" or choice == "LR GDP": return np.array([_ss_growth_rate(choice, r, p)]) n_per = p.T + p.S def _extend(x): # paths may stop at T; hold the last value x = np.atleast_1d(np.asarray(x, dtype=float)).ravel()[:n_per] return np.append(x, np.full(n_per - x.size, x[-1])) if choice == "r": return _extend(r) if choice == "Curr GDP": X = _extend(Y) elif choice in ("wage bill", "avg wage"): X = _extend(w) * _extend(L) else: raise ValueError( f"NDC growth rate must be one of {NDC_GROWTH_CHOICES}, got " f"{choice!r}" ) g_y = float(np.asarray(p.g_y).flat[-1]) X_prev = np.append(X[0], X[:-1]) rate = X / X_prev * np.exp(g_y) if choice == "wage bill" or choice == "Curr GDP": g_n_ss = float(getattr(p, "g_n_ss", np.asarray(p.g_n).flat[-1])) g_n_preTP = float(getattr(p, "g_n_preTP", g_n_ss)) # population growth from t - 1 to t g_n_prev = np.append(g_n_preTP, _extend_pop(p.g_n, n_per, g_n_ss)) rate = rate * (1.0 + g_n_prev[:n_per]) return rate - 1.0
def _extend_pop(g_n, n_per, g_n_ss): """ Population growth path g_n[t] (growth from t to t + 1) for t = 0, ..., n_per - 2, padded with the steady-state rate. """ g_n = np.atleast_1d(np.asarray(g_n, dtype=float)).ravel()[: n_per - 1] return np.append(g_n, np.full(n_per - 1 - g_n.size, g_n_ss))
[docs] def ndc_rates_path(r, Y, w, L, p, method): """ Credited return and annuity discount rate paths for an NDC system. The credited return is ``ndc_growth_rate`` averaged over the ``ndc_growth_avg_years`` years up to and including each period (periods before the path take the period-0 rate). The discount rate is ``dir_growth_rate``; a household's conversion coefficient uses its value in the period the household retires. Args: r (array_like): interest rate path (scalar in the steady state) Y (array_like): GDP path w (array_like): real wage path L (array_like): aggregate labor path p (OG-Core Specifications object): model parameters method (str): 'SS' or 'TPI' Returns: rates (NDCRates): credit and discount rate paths """ credit = growth_rate_path(p.ndc_growth_rate, r, Y, w, L, p, method) years = float(getattr(p, "ndc_growth_avg_years", 1)) window = max(1, int(np.rint(years * p.S / 80.0))) if window > 1 and credit.size > 1: padded = np.append(np.full(window - 1, credit[0]), credit) credit = np.convolve(padded, np.ones(window) / window, "valid") discount = growth_rate_path(p.dir_growth_rate, r, Y, w, L, p, method) return NDCRates(credit, discount)
[docs] def g_ndc(r, Y, p): """ Steady-state growth rate credited to NDC balances. Args: r (array_like): interest rate; its last element is used Y (array_like): GDP (unused: GDP growth in the steady state follows from g_y and g_n) p (OG-Core Specifications object): model parameters Returns: g_ndc (scalar): growth rate credited to NDC balances """ return _ss_growth_rate(p.ndc_growth_rate, r, p)
[docs] def g_dir(r, Y, p): """ Steady-state rate used to discount payments in the NDC conversion coefficient. Args: r (array_like): interest rate; its last element is used Y (array_like): GDP (unused: GDP growth in the steady state follows from g_y and g_n) p (OG-Core Specifications object): model parameters Returns: g_dir (scalar): discount rate in the conversion coefficient """ return _ss_growth_rate(p.dir_growth_rate, r, p)
[docs] def annuity_mort_rates(p): """ Mortality rates by period and age used to annuitize NDC balances. These are the model's mortality rates ``rho``, averaged over lifetime income groups with their population weights ``lambdas``, so that a single conversion coefficient applies to every income group. A ``mort_rates_SS`` attribute set on ``p`` by hand (a vector by age) overrides ``rho`` and is used in every period. Args: p (OG-Core Specifications object): model parameters Returns: mort_rates (Numpy array): (T + S) x S mortality rates, where row t, column s is the probability that a household of age s in period t dies before period t + 1 """ mort_rates_SS = getattr(p, "mort_rates_SS", None) if mort_rates_SS is not None: mort_rates_SS = np.asarray(mort_rates_SS, dtype=float).ravel() return np.broadcast_to( mort_rates_SS, (p.T + p.S, mort_rates_SS.shape[0]) ) rho = np.asarray(p.rho, dtype=float) if rho.ndim == 3: rho = rho @ np.asarray(p.lambdas, dtype=float).ravel() return np.broadcast_to(rho, (p.T + p.S, rho.shape[-1]))
[docs] def delta_ret( r, Y, p, S_ret=None, indR=None, k_ret=None, t_ret=None, g_dir_path=None ): r""" Compute conversion coefficient for the NDC pension amount .. math:: \delta_{R} = (dir_{R} + ind_{R} - k)^{-1} The mortality rates entering $dir_{R}$ are set by the parameter ``ndc_mortality``: * 'steady state': the steady-state mortality rates, for every cohort * 'period': the mortality rates by age in the period in which the household retires (a period life table) * 'cohort': the mortality rates the household's own birth cohort faces at each age after retirement (a projected cohort life table), the rates that also enter the household's problem Payments are discounted at ``dir_growth_rate``. Given a calendar path of that rate (``g_dir_path``), each household uses its value in the period it retires, held fixed over the payments; otherwise the steady-state rate is used. Args: r (array_like): interest rate Y (array_like): GDP p (OG-Core Specifications object): model parameters S_ret (array_like): model period of retirement; any shape, or None for the steady-state value (the final row of ``retire``) indR (array_like): survivor benefit adjustment, broadcastable against ``S_ret``, or None for the steady-state value k_ret (array_like): payment frequency adjustment, broadcastable against ``S_ret``, or None for the steady-state value t_ret (array_like): model period in which the household reaches age ``S_ret``, broadcastable against ``S_ret``; may lie before 0 or after T + S - 1, in which case the first or last row of the mortality rates is used. None for the steady state. g_dir_path (array_like): calendar path of the discount rate (``NDCRates.discount``), read at ``t_ret`` (clipped to the path); None to use the steady-state rate Returns: delta_ret (scalar or Numpy array): conversion coefficient for the NDC pension amount, shaped like ``S_ret`` """ mort_rates = annuity_mort_rates(p) n_rows, n_ages = mort_rates.shape if S_ret is None: S_ret = cohort_array("retire", p)[-1, -1] if indR is None: indR = cohort_array("indR", p)[-1, -1] if k_ret is None: k_ret = cohort_array("k_ret", p)[-1, -1] S_ret = np.asarray(S_ret, dtype=int) # discount rate of each household: the rate in its retirement period if g_dir_path is None or t_ret is None: g_dir_value = np.full(S_ret.shape, g_dir(r, Y, p)) else: g_dir_path = np.atleast_1d(np.asarray(g_dir_path, dtype=float)) idx = np.clip(np.asarray(t_ret, dtype=int), 0, g_dir_path.size - 1) g_dir_value = np.broadcast_to(g_dir_path[idx], S_ret.shape) method = getattr(p, "ndc_mortality", "steady state") if t_ret is None or method == "steady state": t_ret = np.full(S_ret.shape, n_rows - 1) elif method == "period": t_ret = np.clip(np.asarray(t_ret, dtype=int), 0, n_rows - 1) elif method == "cohort": # a cohort retiring S periods or more before the path begins # reads the first row at every age, and one retiring after it # ends reads the last row t_ret = np.clip(np.asarray(t_ret, dtype=int), -p.S, n_rows - 1) else: raise ValueError( "ndc_mortality must be one of 'steady state', 'period', " f"or 'cohort', got {method!r}" ) t_ret = np.broadcast_to(t_ret, S_ret.shape) # the annuity factor depends only on the retirement age, the # mortality rates' period of retirement, and the discount rate, so # compute it once per distinct combination keys, inverse = np.unique( np.stack( [S_ret.ravel(), t_ret.ravel(), np.ravel(g_dir_value)], axis=1 ), axis=0, return_inverse=True, ) ages = np.arange(n_ages) dir_delta_pairs = np.zeros(keys.shape[0]) for i, (R, tR, g) in enumerate(keys): R, tR = int(R), int(tR) if method == "cohort": # age a is reached in period tR + a - R rows = np.clip(tR - R + ages, 0, n_rows - 1) else: rows = np.full(n_ages, tR) surv_rates = np.ascontiguousarray(1.0 - mort_rates[rows, ages]) dir_delta_pairs[i] = delta_ret_loop(p.S, R, surv_rates, g) dir_delta = dir_delta_pairs[np.ravel(inverse)].reshape(S_ret.shape) delta_ret = 1 / (dir_delta + np.asarray(indR) - np.asarray(k_ret)) if delta_ret.ndim == 0: return float(delta_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 (Numpy array): replacement rate for each of the per_rmn remaining periods 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[s] * (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): r""" Compute the annuity factor in the NDC conversion coefficient .. math:: dir_{R} = \sum_{k=0}^{S-R-1}\left[\prod_{i=0}^{k-1} (1 - \hat{\rho}_{R+i})\right](1 + g_{dir})^{-k} The pension is paid at ages R, ..., S - 1. Following the household mortality convention, ``1 - rho[s]`` is the probability of surviving from age s to age s + 1, so the payment at age R + k is received with probability prod_{i=0}^{k-1} (1 - rho[R + i]) by a household alive at retirement age R. Args: S (int): number of periods in the model S_ret (int): retirement age surv_rates (Numpy array): length S survival rates by age, 1 - rho g_dir_value (scalar): discount rate used in the conversion coefficient Returns: dir_delta (scalar): expected discounted number of pension payments per unit of annual benefit """ dir_delta = 0.0 cumul_surv = 1.0 discount = 1.0 for k in range(S - S_ret): dir_delta += cumul_surv * discount cumul_surv *= surv_rates[S_ret + k] discount /= 1 + g_dir_value 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, DB, avg_earn_num_years, alpha_db, yr_contr, earn_lo=None, earn_hi=None, t_birth=0, ): """ Calculate public pension from a defined benefits system for one household over its lifetime. Args: w (array_like): real wage rate at each age, length S e (Numpy array): effective labor units at each age, length S n (Numpy array): labor supply at each age, length S S_ret (Numpy array): retirement period that applies to the household at each age u, length S S (int): number of periods in the model g_y (array_like): GDP growth rate DB (Numpy array): pension amount for each household (filled in and returned), length S avg_earn_num_years (Numpy array): number of periods earnings are averaged over, by age u, length S alpha_db (Numpy array): replacement rate in effect at each age u, length S yr_contr (Numpy array): periods of contribution credited, by age u, length S earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts t_birth (int): model period in which the household is age 0, so earnings at age s were made in period t_birth + s Returns: DB (Numpy array): pension amount for each household """ for u in range(S): R = S_ret[u] if u < R: continue ny = avg_earn_num_years[u] L_inc_avg = 0.0 for s in range(R - ny, R): if earn_lo is None: L_inc_avg += w[s] * e[s] * n[s] / np.exp(g_y[-1] * (u - s)) else: x = earnings_band_at( w[s] * e[s] * n[s], earn_lo, earn_hi, t_birth + s ) L_inc_avg += x / np.exp(g_y[-1] * (u - s)) DB[u] = yr_contr[u] * alpha_db[u] * L_inc_avg / ny return DB
[docs] @numba.jit(nopython=True) def DB_2dim_loop( w, e, n, S_ret, S, g_y, DB, avg_earn_num_years, alpha_db, yr_contr, earn_lo=None, earn_hi=None, t_cross=0, ): """ Calculate public pension from a defined benefits system for a cross section of S x J households in one period. Args: w (array_like): real wage rate, length S e (Numpy array): effective labor units, size S x J n (Numpy array): labor supply, size S x J S_ret (Numpy array): retirement period of the household of each age u, length S S (int): number of periods in the model g_y (array_like): GDP growth rate DB (Numpy array): pension amount for each household (filled in and returned), size S x J avg_earn_num_years (Numpy array): number of periods earnings are averaged over, by age u, length S alpha_db (Numpy array): replacement rate in effect at each age u for each group j, size S x J yr_contr (Numpy array): periods of contribution credited, by age u, length S earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts t_cross (int): model period of the cross section; the household of age u earned at age s in period t_cross - u + s Returns: DB (Numpy array): pension amount for each household """ for u in range(S): R = S_ret[u] if u < R: continue ny = avg_earn_num_years[u] L_inc_avg = np.zeros(DB.shape[1]) for s in range(R - ny, R): if earn_lo is None: L_inc_avg += ( w[s] * e[s, :] * n[s, :] / np.exp(g_y[-1] * (u - s)) ) else: x = earnings_band_at( w[s] * e[s, :] * n[s, :], earn_lo, earn_hi, t_cross - u + s, ) L_inc_avg += x / np.exp(g_y[-1] * (u - s)) DB[u, :] = yr_contr[u] * alpha_db[u, :] * L_inc_avg / ny return DB
[docs] @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, earn_lo=None, earn_hi=None, ): 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 pension parameters are read at (t, u), the cell of the period-by-age arrays that belongs to the retiree's own cohort. 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 (Numpy array): retirement period by period and age, size T x S 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 (Numpy array): number of periods earnings are averaged over, by period and age, size T x S alpha_db (Numpy array): replacement rate per year of contribution by period, age, and group, size T x S x J yr_contr (Numpy array): periods of contribution credited, by period and age, size T x S earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts Both paths start in the period of the first row of n. Returns: DB (Numpy array): pension amount for each household, size TxSxJ """ T = n.shape[0] DB = np.zeros((T, S, J)) for t in range(T): for u in range(S): R = S_ret[t, u] if u < R: continue ny = avg_earn_num_years[t, u] s_idx = np.arange(R - ny, R) 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, :], ) x = e_ss[s_idx, :] * n_hist if earn_lo is None: L = (w_hist[:, None] / np.exp(g_y * (u - s_idx))[:, None]) * x else: # band of labor income, with the cutoffs in effect in # the period of each earning x = w_hist[:, None] * x for i in range(ny): x[i, :] = earnings_band_at( x[i, :], earn_lo, earn_hi, tau[i] ) L = x / np.exp(g_y * (u - s_idx))[:, None] DB[t, u, :] = ( (L.sum(axis=0) / ny) * yr_contr[t, u] * alpha_db[t, u, :] ) return DB
[docs] @numba.jit(nopython=True) def NDC_1dim_loop( w, e, n, S_ret, S, g_y, tau_p, g_ndc, t_birth, delta_ret, NDC, earn_lo=None, earn_hi=None, ): r""" Calculate public pension from a notional defined contribution system for one household over its lifetime. The contribution made at working age s earns the rate credited in each later period up to the period before retirement: .. math:: \prod_{a=s+1}^{R-1}(1 + g_{NDC,t_0 + a}) where $t_0$ is the household's period of birth (age 0). Args: w (array_like): real wage rate at each age, length S e (Numpy array): effective labor units at each age, length S n (Numpy array): labor supply at each age, length S S_ret (Numpy array): retirement period that applies to the household at each age u, length S S (int): number of periods in the model g_y (array_like): GDP growth rate tau_p (Numpy array): contribution rate the household faced at each working age s, length S g_ndc (Numpy array): calendar path of the credited rate (``NDCRates.credit``), a 1-D float array; periods outside it read the first or last element t_birth (int): model period in which the household is age 0 delta_ret (Numpy array): conversion coefficient for the NDC pension amount that applies at each age u, length S NDC (Numpy array): pension amount for each household (filled in and returned), length S earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts The cutoffs are read in period t_birth + s for age s. Returns: NDC (Numpy array): pension amount for each household """ # credited rate at each age of the household's life g_age = g_ndc[np.clip(t_birth + np.arange(S), 0, g_ndc.size - 1)] for u in range(S): R = S_ret[u] if u < R: continue NDC_s = 0.0 growth = 1.0 for s in range(R - 1, -1, -1): # TODO: update so can take g_y from period t if earn_lo is None: x = e[s] * n[s] NDC_s += ( tau_p[s] * (w[s] / np.exp(g_y[-1] * (u - s))) * x * growth ) else: x = earnings_band_at( w[s] * e[s] * n[s], earn_lo, earn_hi, t_birth + s ) NDC_s += tau_p[s] * (x / np.exp(g_y[-1] * (u - s))) * growth growth *= 1.0 + g_age[s] NDC[u] = delta_ret[u] * NDC_s return NDC
[docs] @numba.jit(nopython=True) def NDC_2dim_loop( w, e, n, S_ret, S, g_y, tau_p, g_ndc, t_cross, delta_ret, NDC, earn_lo=None, earn_hi=None, ): """ Calculate public pension from a notional defined contribution system for a cross section of S x J households in one period. As in ``NDC_1dim_loop``, contributions compound at the rates credited in the periods the household lived through: the household of age u in period ``t_cross`` was age s in period t_cross - u + s. Args: w (array_like): real wage rate, length S e (Numpy array): effective labor units, size S x J n (Numpy array): labor supply, size S x J S_ret (Numpy array): retirement period of the household of each age u, length S S (int): number of periods in the model g_y (array_like): GDP growth rate tau_p (Numpy array): contribution rate of the household of each age s, length S g_ndc (Numpy array): calendar path of the credited rate t_cross (int): model period of the cross section delta_ret (Numpy array): conversion coefficient for the NDC pension amount of the household of each age u, length S NDC (Numpy array): pension amount for each household (filled in and returned), size S x J earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts The cutoffs are read in period t_cross - u + s. Returns: NDC (Numpy array): pension amount for each household """ n_g = g_ndc.shape[0] for u in range(S): R = S_ret[u] if u < R: continue NDC_s = np.zeros(NDC.shape[1]) growth = 1.0 for s in range(R - 1, -1, -1): if earn_lo is None: x = e[s, :] * n[s, :] NDC_s += ( tau_p[s] * (w[s] / np.exp(g_y[-1] * (u - s))) * x * growth ) else: x = earnings_band_at( w[s] * e[s, :] * n[s, :], earn_lo, earn_hi, t_cross - u + s, ) NDC_s += tau_p[s] * (x / np.exp(g_y[-1] * (u - s))) * growth idx = min(max(t_cross - u + s, 0), n_g - 1) growth *= 1.0 + g_ndc[idx] NDC[u, :] = delta_ret[u] * NDC_s return NDC
@numba.jit(nopython=True) def NDC_3dim_loop( w_path, e_ss, n, n_preTP, S_ret, S, J, g_y, tau_p, g_ndc, delta_ret, earn_lo=None, earn_hi=None, ): r""" Calculate public pension from a notional defined contribution system over the full time path. Used when the TPI solution evaluates taxes for all periods at once to compute aggregates. As in ``DB_3dim_loop``, each retiree's benefit is built from their own cohort's history: the contribution at age s of a household aged u at time t was made in period t - (u - s), at that period's wage, labor supply, and contribution rate, and earns the rates credited in periods t - (u - s) + 1 through t - (u - R) - 1. Histories that predate the time path use the period-0 wage, contribution rate, and credited rate and the model's initial labor supply, so this reproduces what the per-cohort household solves compute. 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 (Numpy array): retirement period by period and age, size T x S S (int): number of periods in the model J (int): number of lifetime income groups g_y (scalar): growth rate of technology tau_p (Numpy array): contribution rate by period and age, size T x S g_ndc (Numpy array): calendar path of the credited rate delta_ret (Numpy array): conversion coefficient for the NDC pension amount by period and age, size T x S earn_lo (Numpy array): calendar path of the lower cutoff of the earnings band, in model units of labor income; only labor income w * e * n above it counts. None (the default) applies no band. earn_hi (Numpy array): calendar path of the upper cutoff of the earnings band; only labor income below it counts Both paths start in the period of the first row of n. Returns: NDC (Numpy array): pension amount for each household, size TxSxJ """ T = n.shape[0] n_g = g_ndc.shape[0] NDC = np.zeros((T, S, J)) for t in range(T): for u in range(S): R = S_ret[t, u] if u < R: continue s_idx = np.arange(R) tau = t - (u - s_idx) rows = np.clip(tau, 0, T - 1) w_hist = w_path[rows] n_hist = np.where( (tau >= 0)[:, None], n[rows, s_idx, :], n_preTP[s_idx, :], ) contrib = np.zeros(R) growth = 1.0 for i in range(R - 1, -1, -1): s = s_idx[i] if earn_lo is None: contrib[i] = ( tau_p[rows[i], s] * w_hist[i] / np.exp(g_y * (u - s)) * growth ) else: # the wage enters through the band of labor income contrib[i] = ( tau_p[rows[i], s] / np.exp(g_y * (u - s)) * growth ) idx = min(max(tau[i], 0), n_g - 1) growth *= 1.0 + g_ndc[idx] x = e_ss[s_idx, :] * n_hist if earn_lo is not None: # band of labor income, with the cutoffs in effect in # the period of each contribution x = w_hist[:, None] * x for i in range(R): x[i, :] = earnings_band_at( x[i, :], earn_lo, earn_hi, tau[i] ) L = contrib[:, None] * x NDC[t, u, :] = delta_ret[t, u] * L.sum(axis=0) return NDC