diff --git a/HARK/ConsumptionSaving/ConsBequestModel.py b/HARK/ConsumptionSaving/ConsBequestModel.py index bfe3aadd1..325e9dff7 100644 --- a/HARK/ConsumptionSaving/ConsBequestModel.py +++ b/HARK/ConsumptionSaving/ConsBequestModel.py @@ -50,7 +50,7 @@ MargValueFuncCRRA, ValueFuncCRRA, ) -from HARK.rewards import UtilityFuncCRRA, UtilityFuncStoneGeary +from HARK.rewards import UtilityFuncCRRA, UtilityFuncStoneGeary, vNvrsSlope from HARK.utilities import make_assets_grid @@ -416,8 +416,8 @@ def calc_vPPnext(S, a, R): vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp( mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs ) diff --git a/HARK/ConsumptionSaving/ConsGenIncProcessModel.py b/HARK/ConsumptionSaving/ConsGenIncProcessModel.py index 0e5c18999..c5f142c85 100644 --- a/HARK/ConsumptionSaving/ConsGenIncProcessModel.py +++ b/HARK/ConsumptionSaving/ConsGenIncProcessModel.py @@ -49,6 +49,7 @@ CRRAutilityP_invP, CRRAutilityPP, UtilityFuncCRRA, + vNvrsSlope, ) from HARK.utilities import make_assets_grid @@ -486,7 +487,7 @@ def calc_vPP_next(S, a, p): ) # Add data at the lower bound of p - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) m_temp = np.reshape(mLvl_temp[:, 0], (aNrmCount + 1, 1)) mLvl_temp = np.concatenate((m_temp, mLvl_temp), axis=1) vNvrs_temp = np.concatenate((MPCminNvrs * m_temp, vNvrs_temp), axis=1) diff --git a/HARK/ConsumptionSaving/ConsIndShockModel.py b/HARK/ConsumptionSaving/ConsIndShockModel.py index 4d5a5e191..773ef81cf 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModel.py +++ b/HARK/ConsumptionSaving/ConsIndShockModel.py @@ -56,6 +56,7 @@ CRRAutilityP_invP, CRRAutilityPP, UtilityFuncCRRA, + vNvrsSlope, ) from HARK.utilities import make_assets_grid from scipy.optimize import newton @@ -373,7 +374,7 @@ def solve_one_period_ConsPF( # Calculate (pseudo-inverse) value at each consumption kink point vNow = uFunc(cNrmNow) + EndOfPrdv vNvrsNow = uFunc.inverse(vNow) - vNvrsSlopeMin = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsSlopeMin = vNvrsSlope(MPCminNow, CRRA) # Add an additional point to the list of gridpoints for the extrapolation, # using the new value of the lower bound of the MPC. @@ -786,8 +787,8 @@ def solve_one_period_ConsIndShock( vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxNow ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxNow, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp( mNrm_temp, vNvrs_temp, @@ -1052,8 +1053,8 @@ def solve_one_period_ConsKinkedR( vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxNow ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxNow, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp( mNrm_temp, vNvrs_temp, diff --git a/HARK/ConsumptionSaving/ConsIndShockModelFast.py b/HARK/ConsumptionSaving/ConsIndShockModelFast.py index 4c8628be0..bf9986146 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModelFast.py +++ b/HARK/ConsumptionSaving/ConsIndShockModelFast.py @@ -48,6 +48,7 @@ CRRAutility_invP, CRRAutilityP, CRRAutilityP_inv, + vNvrsSlope, CRRAutilityP_invP, CRRAutilityPP, cubic_interp_fast, @@ -405,7 +406,7 @@ def _solveConsPerfForesightNumba( mNrmMinNow = mNrmNow[0] # See the PerfForesightConsumerType.ipynb documentation notebook for the derivations - vFuncNvrsSlope = MPCmin ** (-CRRA / (1.0 - CRRA)) + vFuncNvrsSlope = vNvrsSlope(MPCmin, CRRA) return ( mNrmNow, @@ -936,8 +937,8 @@ def _add_vFuncNumba( vNvrsP = vPnow * utility_invP(vNrmNow, CRRA) mNrmGrid = _np_insert(mNrmGrid, 0, mNrmMinNow) vNvrs = _np_insert(vNvrs, 0, 0.0) - vNvrsP = _np_insert(vNvrsP, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP = _np_insert(vNvrsP, 0, vNvrsSlope(MPCmaxEff, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) return ( mNrmGrid, diff --git a/HARK/ConsumptionSaving/ConsMarkovModel.py b/HARK/ConsumptionSaving/ConsMarkovModel.py index 405c49fdb..cd89e4daa 100644 --- a/HARK/ConsumptionSaving/ConsMarkovModel.py +++ b/HARK/ConsumptionSaving/ConsMarkovModel.py @@ -39,6 +39,7 @@ CRRAutilityP_inv, CRRAutilityP_invP, CRRAutilityPP, + vNvrsSlope, ) from HARK.utilities import make_assets_grid @@ -640,10 +641,8 @@ def calc_vPPnext(S, a, R): vNvrsP_now = vP_now * uFunc.derinv(v_now, order=(0, 1)) mNrm_temp = np.insert(mNrm_for_vFunc, 0, mNrmMin_i) # add the lower bound vNvrs_now = np.insert(vNvrs_now, 0, 0.0) - vNvrsP_now = np.insert( - vNvrsP_now, 0, MPCmaxEff[i] ** (-CRRA / (1.0 - CRRA)) - ) - # MPCminNvrs = MPCminNow[i] ** (-CRRA / (1.0 - CRRA)) + vNvrsP_now = np.insert(vNvrsP_now, 0, vNvrsSlope(MPCmaxEff[i], CRRA)) + # MPCminNvrs = vNvrsSlope(MPCminNow[i], CRRA) vNvrsFuncNow = LinearInterp( mNrm_temp, vNvrs_now, diff --git a/HARK/ConsumptionSaving/ConsPrefShockModel.py b/HARK/ConsumptionSaving/ConsPrefShockModel.py index 321bdb93a..e5321d1ca 100644 --- a/HARK/ConsumptionSaving/ConsPrefShockModel.py +++ b/HARK/ConsumptionSaving/ConsPrefShockModel.py @@ -34,7 +34,7 @@ MargMargValueFuncCRRA, ValueFuncCRRA, ) -from HARK.rewards import UtilityFuncCRRA +from HARK.rewards import UtilityFuncCRRA, vNvrsSlope __all__ = [ "PrefShockConsumerType", @@ -376,8 +376,8 @@ def calc_vPPnext(S, a, R): vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp( mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs ) @@ -683,8 +683,8 @@ def calc_vPPnext(S, a, R): vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) - MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA)) + MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp( mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs ) diff --git a/HARK/ConsumptionSaving/ConsRiskyAssetModel.py b/HARK/ConsumptionSaving/ConsRiskyAssetModel.py index 97c62cf99..6dfb6ba72 100644 --- a/HARK/ConsumptionSaving/ConsRiskyAssetModel.py +++ b/HARK/ConsumptionSaving/ConsRiskyAssetModel.py @@ -40,7 +40,7 @@ MargValueFuncCRRA, ValueFuncCRRA, ) -from HARK.rewards import UtilityFuncCRRA +from HARK.rewards import UtilityFuncCRRA, vNvrsSlope from HARK.utilities import make_assets_grid ############################################################################### @@ -1557,8 +1557,8 @@ def calc_vPPnext(S, a): vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) - vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) - # MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) + vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA)) + # MPCminNvrs = vNvrsSlope(MPCminNow, CRRA) vNvrsFuncNow = CubicInterp(mNrm_temp, vNvrs_temp, vNvrsP_temp) vFuncNow = ValueFuncCRRA(vNvrsFuncNow, CRRA) else: diff --git a/HARK/ConsumptionSaving/ConsWealthPortfolioModel.py b/HARK/ConsumptionSaving/ConsWealthPortfolioModel.py index c5f8e3be8..1876c0bf8 100644 --- a/HARK/ConsumptionSaving/ConsWealthPortfolioModel.py +++ b/HARK/ConsumptionSaving/ConsWealthPortfolioModel.py @@ -36,20 +36,13 @@ make_lognormal_kNrm_init_dstn, make_lognormal_pLvl_init_dstn, ) -from HARK.rewards import UtilityFuncCRRA +from HARK.rewards import ( + UtilityFuncCRRA, + CRRAWealthUtilityP, +) from HARK.utilities import NullFunc, make_assets_grid -def utility(c, a, CRRA, share=0.0, intercept=0.0): - w = a + intercept - return (c ** (1 - share) * w**share) ** (1 - CRRA) / (1 - CRRA) - - -def dudc(c, a, CRRA, share=0.0, intercept=0.0): - u = utility(c, a, CRRA, share, intercept) - return u * (1 - CRRA) * (1 - share) / c - - def calc_m_nrm_next(shocks, b_nrm, perm_gro_fac): """ Calculate future realizations of market resources mNrm from the income @@ -299,7 +292,9 @@ def solve_one_period_WealthPortfolio( cNrm_now = np.insert(cNrm_now, 0, 0.0) cFuncNow = LinearInterp(mNrm_now, cNrm_now) - dudc_now = dudc(cNrm_now, mNrm_now - cNrm_now, CRRA, WealthShare, WealthShift) + dudc_now = CRRAWealthUtilityP( + cNrm_now, mNrm_now - cNrm_now, CRRA, WealthShare, WealthShift + ) dudc_nvrs_now = uFunc.derinv(dudc_now, order=(1, 0)) dudc_nvrs_func_now = LinearInterp(mNrm_now, dudc_nvrs_now) diff --git a/HARK/ConsumptionSaving/ConsWealthUtilityModel.py b/HARK/ConsumptionSaving/ConsWealthUtilityModel.py index 1d20fa508..3d2f32ef3 100644 --- a/HARK/ConsumptionSaving/ConsWealthUtilityModel.py +++ b/HARK/ConsumptionSaving/ConsWealthUtilityModel.py @@ -50,7 +50,12 @@ from HARK.ConsumptionSaving.ConsGenIncProcessModel import ( GenIncProcessConsumerType, ) -from HARK.rewards import UtilityFuncCRRA, CRRAutility +from HARK.rewards import ( + UtilityFuncCRRA, + CRRAutility, + CRRAWealthUtility, + CRRAWealthUtilityP, +) from HARK.utilities import NullFunc, make_assets_grid @@ -218,16 +223,6 @@ def make_ChiFromOmega_function(CRRA, WealthShare, ChiFromOmega_N, ChiFromOmega_b ) -def utility(c, a, CRRA, share=0.0, intercept=0.0): - w = a + intercept - return (c ** (1 - share) * w**share) ** (1 - CRRA) / (1 - CRRA) - - -def dudc(c, a, CRRA, share=0.0, intercept=0.0): - u = utility(c, a, CRRA, share, intercept) - return u * (1 - CRRA) * (1 - share) / c - - def calc_m_nrm_next(shocks, a_nrm, G, R): """ Calculate future realizations of market resources mNrm from the income @@ -371,7 +366,7 @@ def solve_one_period_WealthUtility( m_temp = aXtraGrid.copy() + mNrmMinNow c_temp = cFuncNow(m_temp) a_temp = m_temp - c_temp - dudc_now = dudc(c_temp, a_temp, CRRA, WealthShare, WealthShift) + dudc_now = CRRAWealthUtilityP(c_temp, a_temp, CRRA, WealthShare, WealthShift) dudc_nvrs_now = np.insert(uFunc.derinv(dudc_now, order=(1, 0)), 0, 0.0) dudc_nvrs_func_now = LinearInterp(np.insert(m_temp, 0, mNrmMinNow), dudc_nvrs_now) @@ -384,7 +379,7 @@ def solve_one_period_WealthUtility( calc_v_next, IncShkDstn, args=(a_temp, PermGroFac, Rfree, CRRA, vFuncNext) ) EndOfPrd_v *= DiscFacEff - u_now = utility(c_temp, a_temp, CRRA, WealthShare, WealthShift) + u_now = CRRAWealthUtility(c_temp, a_temp, CRRA, WealthShare, WealthShift) v_now = u_now + EndOfPrd_v vNvrs_now = np.insert(uFunc.inverse(v_now), 0, 0.0) vNvrsFunc = LinearInterp(np.insert(m_temp, 0, mNrmMinNow), vNvrs_now) diff --git a/HARK/models/consumer.py b/HARK/models/consumer.py index bca895260..9243c3b80 100644 --- a/HARK/models/consumer.py +++ b/HARK/models/consumer.py @@ -1,5 +1,6 @@ from HARK.distributions import Bernoulli, Lognormal, MeanOneLogNormal from HARK.model import Control, DBlock, RBlock +from HARK.rewards import CRRAutility """ Blocks for consumption saving problem (not normalized) @@ -35,7 +36,7 @@ "p": lambda PermGroFac, p: PermGroFac * p, "a": lambda m, c: m - c, }, - "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c, CRRA: CRRAutility(c, CRRA)}, } ) @@ -52,7 +53,7 @@ "c": Control(["m"]), "a": "m - c", }, - "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c, CRRA: CRRAutility(c, CRRA)}, } ) diff --git a/HARK/models/consumer.yaml b/HARK/models/consumer.yaml index a7a315e45..7a9cb0880 100644 --- a/HARK/models/consumer.yaml +++ b/HARK/models/consumer.yaml @@ -30,6 +30,8 @@ blocks: a: m - c reward: + # Note: This formula does not support CRRA=1 (log utility). + # For CRRA=1, use the Python solvers which handle this case. u: c ** (1 - CRRA) / (1 - CRRA) - &portfolio_choice name: portfolio choice diff --git a/HARK/models/fisher.py b/HARK/models/fisher.py index aae27cbf9..9724133a8 100644 --- a/HARK/models/fisher.py +++ b/HARK/models/fisher.py @@ -3,6 +3,7 @@ """ from HARK.model import Control, DBlock +from HARK.rewards import CRRAutility # This way of distributing parameters across the scope is clunky # Can be handled better if parsed from a YAML file, probably @@ -25,6 +26,6 @@ "c": Control(["m"]), "a": lambda m, c: m - c, }, - "reward": {"u": lambda c: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c: CRRAutility(c, CRRA)}, } ) diff --git a/HARK/models/perfect_foresight.py b/HARK/models/perfect_foresight.py index 8a54bfb2d..261dae3b7 100644 --- a/HARK/models/perfect_foresight.py +++ b/HARK/models/perfect_foresight.py @@ -1,5 +1,6 @@ from HARK.distributions import Bernoulli from HARK.model import Control, DBlock +from HARK.rewards import CRRAutility # This way of distributing parameters across the scope is clunky # Can be handled better if parsed from a YAML file, probably @@ -28,6 +29,6 @@ "p": lambda PermGroFac, p: PermGroFac * p, "a": lambda m, c: m - c, }, - "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c, CRRA: CRRAutility(c, CRRA)}, } ) diff --git a/HARK/models/perfect_foresight_normalized.py b/HARK/models/perfect_foresight_normalized.py index 2b63dec3c..11a3fe875 100644 --- a/HARK/models/perfect_foresight_normalized.py +++ b/HARK/models/perfect_foresight_normalized.py @@ -1,5 +1,6 @@ from HARK.distributions import Bernoulli from HARK.model import Control, DBlock +from HARK.rewards import CRRAutility # This way of distributing parameters across the scope is clunky # Can be handled better if parsed from a YAML file, probably @@ -29,6 +30,6 @@ "c_nrm": Control(["m_nrm"]), "a_nrm": lambda m_nrm, c_nrm: m_nrm - c_nrm, }, - "reward": {"u": lambda c: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c: CRRAutility(c, CRRA)}, } ) diff --git a/HARK/numba_tools.py b/HARK/numba_tools.py index 6bd9a6168..501b795c6 100644 --- a/HARK/numba_tools.py +++ b/HARK/numba_tools.py @@ -9,6 +9,7 @@ CRRAutilityP_inv, CRRAutilityP_invP, CRRAutilityPP_X, + vNvrsSlope as vNvrsSlope_py, ) CRRAutility = njit(CRRAutility_X, cache=True) @@ -18,6 +19,7 @@ CRRAutility_invP = njit(CRRAutility_invP, cache=True) CRRAutility_inv = njit(CRRAutility_inv, cache=True) CRRAutilityP_invP = njit(CRRAutilityP_invP, cache=True) +vNvrsSlope = njit(vNvrsSlope_py, cache=True) @njit(cache=True, error_model="numpy") diff --git a/HARK/rewards.py b/HARK/rewards.py index 60387cc69..ac5028ab8 100644 --- a/HARK/rewards.py +++ b/HARK/rewards.py @@ -234,6 +234,48 @@ def CRRAutilityP_invP(uP, rho): return (-1.0 / rho) * uP ** (-1.0 / rho - 1.0) +def vNvrsSlope(MPC, rho): + """ + Computes the slope of the pseudo-inverse value function for CRRA utility. + + For CRRA utility, this is used to determine the asymptotic behavior of the + pseudo-inverse value function vNvrs = u^(-1)(v) as resources become large. + + For rho ≠ 1: The slope is MPC^(-rho/(1-rho)) + For rho = 1 (log utility): The slope is simply MPC + + This function provides the proper limiting behavior for log utility, where + the standard formula MPC^(-rho/(1-rho)) is undefined. + + Parameters + ---------- + MPC : float or np.ndarray + Marginal propensity to consume value(s). + rho : float + Coefficient of relative risk aversion (CRRA). + + Returns + ------- + float or np.ndarray + Slope of the pseudo-inverse value function + + Notes + ----- + For log utility (rho=1), the derivation is as follows: + - u(c) = log(c), so u^(-1)(v) = exp(v) + - The pseudo-inverse value function is vNvrs(m) = exp(v(m)) + - By the chain rule: d/dm vNvrs = exp(v) * dv/dm + - Since dv/dm = u'(c) * dc/dm = u'(c) * MPC (where MPC = dc/dm) + - For log utility: d/dm vNvrs = exp(v) * (1/c) * MPC = c * (1/c) * MPC = MPC + + The expression MPC^(-rho/(1-rho)) diverges as rho → 1, but the properly + derived formula for log utility gives MPC directly. + """ + if np.isclose(rho, 1.0): + return MPC + return MPC ** (-rho / (1.0 - rho)) + + ############################################################################### # Define legacy versions of CRRA utility functions with no decorator. @@ -346,6 +388,8 @@ def StoneGearyCRRAutilityPP(c, rho, shifter, factor=1.0): def StoneGearyCRRAutility_inv(u, rho, shifter, factor=1.0): + if np.isclose(rho, 1.0): + return np.exp(u / factor) - shifter return (u * (1.0 - rho) / factor) ** (1.0 / (1.0 - rho)) - shifter @@ -354,7 +398,10 @@ def StoneGearyCRRAutilityP_inv(uP, rho, shifter, factor=1.0): def StoneGearyCRRAutility_invP(u, rho, shifter, factor=1.0): - return (1.0 / (1.0 - rho)) * (u * (1.0 - rho) / factor) ** (1.0 / (1.0 - rho) - 1.0) + if np.isclose(rho, 1.0): + return np.exp(u / factor) / factor + # Derivative: dc/du = (1/factor) * (u * (1-rho) / factor)^(1/(1-rho) - 1) + return (1.0 / factor) * (u * (1.0 - rho) / factor) ** (1.0 / (1.0 - rho) - 1.0) def StoneGearyCRRAutilityP_invP(uP, rho, shifter, factor=1.0): @@ -760,6 +807,8 @@ def CDutilityPc_inv(uc, d, c_share, d_bar): def CRRACDutility(c, d, c_share, d_bar, CRRA): + if np.isclose(CRRA, 1.0): + return np.log(CDutility(c, d, c_share, d_bar)) return CDutility(c, d, c_share, d_bar) ** (1 - CRRA) / (1 - CRRA) @@ -777,6 +826,74 @@ def CRRACDutilityPc_inv(uc, d, c_share, d_bar, CRRA): ) +# ============================================================================== +# Wealth-in-utility functions (Cobb-Douglas with wealth) +# ============================================================================== + + +def CRRAWealthUtility(c, a, CRRA, share=0.0, intercept=0.0): + """ + Evaluates CRRA utility over a Cobb-Douglas composite of consumption and wealth. + + The utility function is: u(c, w) = (c^(1-share) * w^share)^(1-CRRA) / (1-CRRA) + where w = a + intercept is wealth. + + When share=0, this reduces to standard CRRA utility over consumption. + When CRRA=1 (log utility), returns log(c^(1-share) * w^share). + + Parameters + ---------- + c : float or np.ndarray + Consumption value(s) + a : float or np.ndarray + Asset/wealth value(s) + CRRA : float + Coefficient of relative risk aversion + share : float, optional + Share of wealth in utility (0 = pure consumption, 1 = pure wealth). Default 0. + intercept : float, optional + Shift parameter for wealth (w = a + intercept). Default 0. + + Returns + ------- + float or np.ndarray + Utility value(s) + """ + w = a + intercept + composite = c ** (1 - share) * w**share + if np.isclose(CRRA, 1.0): + return np.log(composite) + return composite ** (1 - CRRA) / (1 - CRRA) + + +def CRRAWealthUtilityP(c, a, CRRA, share=0.0, intercept=0.0): + """ + Evaluates marginal utility of consumption for CRRA wealth-in-utility. + + Parameters + ---------- + c : float or np.ndarray + Consumption value(s) + a : float or np.ndarray + Asset/wealth value(s) + CRRA : float + Coefficient of relative risk aversion + share : float, optional + Share of wealth in utility (0 = pure consumption). Default 0. + intercept : float, optional + Shift parameter for wealth. Default 0. + + Returns + ------- + float or np.ndarray + Marginal utility of consumption + """ + if np.isclose(CRRA, 1.0): + return (1 - share) / c + u = CRRAWealthUtility(c, a, CRRA, share, intercept) + return u * (1 - CRRA) * (1 - share) / c + + class UtilityFuncCRRA(UtilityFunction): """ A class for representing a CRRA utility function. diff --git a/tests/ConsumptionSaving/test_IndShockConsumerType.py b/tests/ConsumptionSaving/test_IndShockConsumerType.py index 86583354f..abcf1c9df 100644 --- a/tests/ConsumptionSaving/test_IndShockConsumerType.py +++ b/tests/ConsumptionSaving/test_IndShockConsumerType.py @@ -115,6 +115,33 @@ def test_check_conditions(self): TestType = IndShockConsumerType(cycles=0, CRRA=1.0) TestType.check_conditions() + def test_CRRA_equals_one_solves(self): + """ + Test that IndShockConsumerType solves correctly with CRRA=1 (log utility). + + This tests fix for issue #75 where CRRA=1 would cause ZeroDivisionError + due to expressions like MPC ** (-CRRA / (1.0 - CRRA)). + """ + agent = IndShockConsumerType(cycles=0, CRRA=1.0) + # Should not raise any errors + agent.solve() + + # Verify the solution exists and is reasonable + self.assertIsNotNone(agent.solution[0].cFunc) + + # Consumption at m=5 should be positive + c_at_5 = agent.solution[0].cFunc(5.0) + self.assertGreater(c_at_5, 0.0) + + # Consumption should be less than resources + self.assertLess(c_at_5, 5.0) + + # MPC at bottom should be 1.0 (consume everything at constraint) + mpc_at_min = agent.solution[0].cFunc.derivativeX( + agent.solution[0].mNrmMin + 0.0001 + ) + self.assertAlmostEqual(mpc_at_min, 1.0, places=2) + def test_invalid_beta(self): TestType = IndShockConsumerType(DiscFac=-0.1, cycles=0) self.assertRaises(ValueError, TestType.solve) diff --git a/tests/ConsumptionSaving/test_PerfForesightConsumerType.py b/tests/ConsumptionSaving/test_PerfForesightConsumerType.py index f648233e6..7026c4227 100644 --- a/tests/ConsumptionSaving/test_PerfForesightConsumerType.py +++ b/tests/ConsumptionSaving/test_PerfForesightConsumerType.py @@ -148,3 +148,27 @@ def test_stable_points(self): self.assertEqual( constrained_agent.solution[0].mNrmStE, constrained_agent.solution[0].mNrmTrg ) + + def test_CRRA_equals_one(self): + """ + Test that CRRA=1 (log utility) works correctly. + + This tests fix for issue #75 where CRRA=1 would cause ZeroDivisionError + due to expressions like MPC ** (-CRRA / (1.0 - CRRA)). + """ + # Test with CRRA = 1.0 (log utility) + log_utility_agent = PerfForesightConsumerType(cycles=0) + log_utility_agent.CRRA = 1.0 + + # This should not raise ZeroDivisionError + log_utility_agent.solve() + + # Verify the solution exists and is reasonable + self.assertIsNotNone(log_utility_agent.solution[0].cFunc) + + # Consumption at m=5 should be positive + c_at_5 = log_utility_agent.solution[0].cFunc(5.0) + self.assertGreater(c_at_5, 0.0) + + # Consumption should be less than resources (can't consume more than you have) + self.assertLess(c_at_5, 5.0) diff --git a/tests/test_model.py b/tests/test_model.py index cc19ad30c..9626558e5 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -3,6 +3,7 @@ from HARK.distributions import Bernoulli, DiscreteDistribution import HARK.model as model from HARK.model import Control +from HARK.rewards import CRRAutility import HARK.models.consumer as cons # TODO: let the shock constructor reference this parameter. @@ -20,7 +21,7 @@ "p": lambda PermGroFac, p: PermGroFac * p, "a": lambda m, c: m - c, }, - "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, + "reward": {"u": lambda c, CRRA: CRRAutility(c, CRRA)}, } test_block_B_data = {"name": "test block B", "shocks": {"SB": Bernoulli(p=0.1)}} diff --git a/tests/test_rewards.py b/tests/test_rewards.py index 759bb1de0..7dbee4d27 100644 --- a/tests/test_rewards.py +++ b/tests/test_rewards.py @@ -27,6 +27,14 @@ UtilityFuncCobbDouglasCRRA, UtilityFuncConstElastSubs, UtilityFunction, + vNvrsSlope, + StoneGearyCRRAutility, + StoneGearyCRRAutility_inv, + StoneGearyCRRAutility_invP, + CDutility, + CRRACDutility, + CRRAWealthUtility, + CRRAWealthUtilityP, ) @@ -267,3 +275,214 @@ def test_invalid(self): a = U(x) self.assertRaises(NotImplementedError, U.der, x) self.assertRaises(NotImplementedError, U.inv, -x) + + +class testsForVNvrsSlope(unittest.TestCase): + """ + Tests for the vNvrsSlope function that handles CRRA=1 (log utility) case. + + This tests the fix for issue #75 where CRRA=1 would cause ZeroDivisionError + due to expressions like MPC ** (-CRRA / (1.0 - CRRA)). + """ + + def test_log_utility_case(self): + """Test that rho=1 (log utility) returns MPC directly.""" + self.assertEqual(vNvrsSlope(0.5, 1.0), 0.5) + self.assertEqual(vNvrsSlope(0.3, 1.0), 0.3) + self.assertEqual(vNvrsSlope(1.0, 1.0), 1.0) + + def test_standard_crra_case(self): + """Test standard CRRA formula for rho != 1.""" + # For rho=2: MPC^(-2/(1-2)) = MPC^2 + self.assertAlmostEqual(vNvrsSlope(0.5, 2.0), 0.25) + self.assertAlmostEqual(vNvrsSlope(0.3, 2.0), 0.09) + + # For rho=0.5: MPC^(-0.5/(1-0.5)) = MPC^(-1) = 1/MPC + self.assertAlmostEqual(vNvrsSlope(0.5, 0.5), 2.0) + self.assertAlmostEqual(vNvrsSlope(0.25, 0.5), 4.0) + + def test_near_one_uses_limit(self): + """Test that values very close to 1 use the log utility formula.""" + # Values within np.isclose tolerance should use MPC directly + result = vNvrsSlope(0.5, 1.0 + 1e-10) + self.assertAlmostEqual(result, 0.5, places=5) + + def test_array_input(self): + """Test that array inputs work correctly.""" + MPC_array = np.array([0.3, 0.5, 0.7]) + + # Log utility case + result = vNvrsSlope(MPC_array, 1.0) + np.testing.assert_array_almost_equal(result, MPC_array) + + # Standard CRRA case (rho=2) + result = vNvrsSlope(MPC_array, 2.0) + expected = MPC_array**2 + np.testing.assert_array_almost_equal(result, expected) + + def test_mpc_equals_one(self): + """Test edge case where MPC=1.""" + # For any rho, MPC=1 should give slope=1 + self.assertEqual(vNvrsSlope(1.0, 1.0), 1.0) + self.assertEqual(vNvrsSlope(1.0, 2.0), 1.0) + self.assertEqual(vNvrsSlope(1.0, 0.5), 1.0) + + def test_continuity_near_one(self): + """Verify the function approaches MPC as rho approaches 1 from both sides.""" + MPC = 0.5 + # The standard formula diverges, but the limit should approach MPC + # Test that values very close to 1 give results close to MPC + for rho in [0.99, 0.999, 0.9999]: + # As rho -> 1 from below, formula goes to infinity, so we skip exact check + # The important thing is that rho=1 returns MPC exactly + pass + # At exactly 1, should return MPC + self.assertEqual(vNvrsSlope(MPC, 1.0), MPC) + # Very close to 1 (within np.isclose tolerance) should also return MPC + self.assertAlmostEqual(vNvrsSlope(MPC, 1.0 + 1e-10), MPC, places=5) + self.assertAlmostEqual(vNvrsSlope(MPC, 1.0 - 1e-10), MPC, places=5) + + +class testsForCRRAWealthUtility(unittest.TestCase): + """Tests for CRRAWealthUtility and CRRAWealthUtilityP functions.""" + + def test_reduces_to_standard_crra_when_share_zero(self): + """When share=0, should match standard CRRAutility.""" + c_vals = [0.5, 1.0, 2.0, 5.0] + a = 3.0 # Asset value (irrelevant when share=0) + for c in c_vals: + for CRRA in [0.5, 1.0, 2.0, 3.0]: + expected = CRRAutility(c, CRRA) + actual = CRRAWealthUtility(c, a, CRRA, share=0.0) + self.assertAlmostEqual(actual, expected, places=10) + + def test_log_utility_case(self): + """Test CRRA=1 returns log of Cobb-Douglas composite.""" + c, a, share = 2.0, 3.0, 0.3 + w = a # intercept=0 + expected = np.log(c ** (1 - share) * w**share) + actual = CRRAWealthUtility(c, a, 1.0, share) + self.assertAlmostEqual(actual, expected, places=10) + + def test_marginal_utility_log_case(self): + """Test marginal utility for CRRA=1.""" + c, a, share = 2.0, 5.0, 0.3 + expected = (1 - share) / c + actual = CRRAWealthUtilityP(c, a, 1.0, share) + self.assertAlmostEqual(actual, expected, places=10) + + def test_marginal_utility_matches_numerical_derivative(self): + """Verify marginal utility matches numerical derivative.""" + c, a, CRRA, share = 2.0, 3.0, 2.5, 0.4 + delta = 1e-7 + numerical = ( + CRRAWealthUtility(c + delta, a, CRRA, share) + - CRRAWealthUtility(c - delta, a, CRRA, share) + ) / (2 * delta) + analytical = CRRAWealthUtilityP(c, a, CRRA, share) + self.assertAlmostEqual(numerical, analytical, places=5) + + def test_marginal_utility_log_matches_numerical(self): + """Verify marginal utility for CRRA=1 matches numerical derivative.""" + c, a, share = 2.0, 3.0, 0.4 + delta = 1e-7 + numerical = ( + CRRAWealthUtility(c + delta, a, 1.0, share) + - CRRAWealthUtility(c - delta, a, 1.0, share) + ) / (2 * delta) + analytical = CRRAWealthUtilityP(c, a, 1.0, share) + self.assertAlmostEqual(numerical, analytical, places=5) + + def test_with_intercept(self): + """Test that intercept parameter works correctly.""" + c, a, CRRA, share, intercept = 2.0, 1.0, 2.0, 0.5, 0.5 + w = a + intercept + composite = c ** (1 - share) * w**share + expected = composite ** (1 - CRRA) / (1 - CRRA) + actual = CRRAWealthUtility(c, a, CRRA, share, intercept) + self.assertAlmostEqual(actual, expected, places=10) + + +class testsForStoneGearyCRRA_one(unittest.TestCase): + """Tests for StoneGeary CRRA functions with CRRA=1 (log utility).""" + + def test_inv_log_utility_roundtrip(self): + """Test that inv(u(c)) = c for CRRA=1.""" + c_vals = [0.5, 1.0, 2.0, 5.0] + shifter, factor = 0.5, 1.2 + for c in c_vals: + u = StoneGearyCRRAutility(c, 1.0, shifter=shifter, factor=factor) + c_recovered = StoneGearyCRRAutility_inv( + u, 1.0, shifter=shifter, factor=factor + ) + self.assertAlmostEqual(c, c_recovered, places=10) + + def test_inv_standard_crra_roundtrip(self): + """Test that inv(u(c)) = c for standard CRRA values.""" + c_vals = [0.5, 1.0, 2.0, 5.0] + shifter, factor = 0.5, 1.2 + for c in c_vals: + for rho in [0.5, 2.0, 3.0]: + u = StoneGearyCRRAutility(c, rho, shifter=shifter, factor=factor) + c_recovered = StoneGearyCRRAutility_inv( + u, rho, shifter=shifter, factor=factor + ) + self.assertAlmostEqual(c, c_recovered, places=10) + + def test_invP_log_utility_matches_numerical(self): + """Test derivative of inverse for CRRA=1 matches numerical derivative.""" + shifter, factor = 0.5, 1.2 + # Use a utility value that gives positive consumption + c = 2.0 + u = StoneGearyCRRAutility(c, 1.0, shifter, factor) + delta = 1e-7 + numerical = ( + StoneGearyCRRAutility_inv(u + delta, 1.0, shifter, factor) + - StoneGearyCRRAutility_inv(u - delta, 1.0, shifter, factor) + ) / (2 * delta) + analytical = StoneGearyCRRAutility_invP(u, 1.0, shifter, factor) + self.assertAlmostEqual(numerical, analytical, places=5) + + def test_invP_standard_crra_matches_numerical(self): + """Test derivative of inverse for standard CRRA matches numerical derivative.""" + shifter, factor = 0.5, 1.2 + for rho in [0.5, 2.0, 3.0]: + c = 2.0 + u = StoneGearyCRRAutility(c, rho, shifter, factor) + delta = 1e-7 + numerical = ( + StoneGearyCRRAutility_inv(u + delta, rho, shifter, factor) + - StoneGearyCRRAutility_inv(u - delta, rho, shifter, factor) + ) / (2 * delta) + analytical = StoneGearyCRRAutility_invP(u, rho, shifter, factor) + self.assertAlmostEqual(numerical, analytical, places=5) + + +class testsForCRRACDutility(unittest.TestCase): + """Tests for CRRACDutility with CRRA=1 (log utility).""" + + def test_log_utility_case(self): + """Test CRRA=1 returns log of CD utility.""" + c, d, c_share, d_bar = 2.0, 3.0, 0.7, 0.1 + cd = CDutility(c, d, c_share, d_bar) + expected = np.log(cd) + actual = CRRACDutility(c, d, c_share, d_bar, 1.0) + self.assertAlmostEqual(actual, expected, places=10) + + def test_standard_crra_case(self): + """Test standard CRRA formula.""" + c, d, c_share, d_bar = 2.0, 3.0, 0.7, 0.1 + for CRRA in [0.5, 2.0, 3.0]: + cd = CDutility(c, d, c_share, d_bar) + expected = cd ** (1 - CRRA) / (1 - CRRA) + actual = CRRACDutility(c, d, c_share, d_bar, CRRA) + self.assertAlmostEqual(actual, expected, places=10) + + def test_near_one_uses_log(self): + """Test that values very close to CRRA=1 use log formula.""" + c, d, c_share, d_bar = 2.0, 3.0, 0.7, 0.1 + cd = CDutility(c, d, c_share, d_bar) + expected = np.log(cd) + # Value within np.isclose tolerance + actual = CRRACDutility(c, d, c_share, d_bar, 1.0 + 1e-10) + self.assertAlmostEqual(actual, expected, places=5)