Skip to content

Commit 50cfac2

Browse files
Copilotalanlujan91
andcommitted
Fix CRRA=1 (log utility) division by zero by implementing proper limit formula
Added vNvrsSlope() function to HARK/rewards.py that returns MPC when CRRA=1 instead of the divergent formula MPC^(-CRRA/(1-CRRA)). This implements the mathematically correct limit for log utility where the pseudo-inverse value function slope equals MPC. Updated all consumption models to use the new function: - ConsIndShockModel.py - ConsPrefShockModel.py - ConsBequestModel.py - ConsGenIncProcessModel.py - ConsIndShockModelFast.py (inline conditionals for numba compatibility) - ConsMarkovModel.py - ConsRiskyAssetModel.py Added test case for CRRA=1 in test_PerfForesightConsumerType.py Fixes #75 Co-authored-by: alanlujan91 <[email protected]>
1 parent fb44fe1 commit 50cfac2

File tree

9 files changed

+98
-22
lines changed

9 files changed

+98
-22
lines changed

HARK/ConsumptionSaving/ConsBequestModel.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
MargValueFuncCRRA,
5151
ValueFuncCRRA,
5252
)
53-
from HARK.rewards import UtilityFuncCRRA, UtilityFuncStoneGeary
53+
from HARK.rewards import UtilityFuncCRRA, UtilityFuncStoneGeary, vNvrsSlope
5454
from HARK.utilities import make_assets_grid
5555

5656

@@ -416,8 +416,8 @@ def calc_vPPnext(S, a, R):
416416
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
417417
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
418418
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
419-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
420-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
419+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA))
420+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
421421
vNvrsFuncNow = CubicInterp(
422422
mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs
423423
)

HARK/ConsumptionSaving/ConsGenIncProcessModel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
CRRAutilityP_invP,
5050
CRRAutilityPP,
5151
UtilityFuncCRRA,
52+
vNvrsSlope,
5253
)
5354
from HARK.utilities import make_assets_grid
5455

@@ -488,7 +489,7 @@ def calc_vPP_next(S, a, p):
488489
)
489490

490491
# Add data at the lower bound of p
491-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
492+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
492493
m_temp = np.reshape(mLvl_temp[:, 0], (aNrmCount + 1, 1))
493494
mLvl_temp = np.concatenate((m_temp, mLvl_temp), axis=1)
494495
vNvrs_temp = np.concatenate((MPCminNvrs * m_temp, vNvrs_temp), axis=1)

HARK/ConsumptionSaving/ConsIndShockModel.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
CRRAutilityP_invP,
5757
CRRAutilityPP,
5858
UtilityFuncCRRA,
59+
vNvrsSlope,
5960
)
6061
from HARK.utilities import make_assets_grid
6162
from scipy.optimize import newton
@@ -373,7 +374,7 @@ def solve_one_period_ConsPF(
373374
# Calculate (pseudo-inverse) value at each consumption kink point
374375
vNow = uFunc(cNrmNow) + EndOfPrdv
375376
vNvrsNow = uFunc.inverse(vNow)
376-
vNvrsSlopeMin = MPCminNow ** (-CRRA / (1.0 - CRRA))
377+
vNvrsSlopeMin = vNvrsSlope(MPCminNow, CRRA)
377378

378379
# Add an additional point to the list of gridpoints for the extrapolation,
379380
# using the new value of the lower bound of the MPC.
@@ -786,8 +787,8 @@ def solve_one_period_ConsIndShock(
786787
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
787788
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
788789
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
789-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxNow ** (-CRRA / (1.0 - CRRA)))
790-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
790+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxNow, CRRA))
791+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
791792
vNvrsFuncNow = CubicInterp(
792793
mNrm_temp,
793794
vNvrs_temp,
@@ -1053,8 +1054,8 @@ def solve_one_period_ConsKinkedR(
10531054
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
10541055
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
10551056
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
1056-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxNow ** (-CRRA / (1.0 - CRRA)))
1057-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
1057+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxNow, CRRA))
1058+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
10581059
vNvrsFuncNow = CubicInterp(
10591060
mNrm_temp,
10601061
vNvrs_temp,

HARK/ConsumptionSaving/ConsIndShockModelFast.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ def _solveConsPerfForesightNumba(
341341
mNrmMinNow = mNrmNow[0]
342342

343343
# See the PerfForesightConsumerType.ipynb documentation notebook for the derivations
344-
vFuncNvrsSlope = MPCmin ** (-CRRA / (1.0 - CRRA))
344+
# For CRRA=1 (log utility), the pseudo-inverse value function slope is simply MPC
345+
if CRRA == 1.0:
346+
vFuncNvrsSlope = MPCmin
347+
else:
348+
vFuncNvrsSlope = MPCmin ** (-CRRA / (1.0 - CRRA))
345349

346350
return (
347351
mNrmNow,
@@ -872,8 +876,13 @@ def _add_vFuncNumba(
872876
vNvrsP = vPnow * utility_invP(vNrmNow, CRRA)
873877
mNrmGrid = _np_insert(mNrmGrid, 0, mNrmMinNow)
874878
vNvrs = _np_insert(vNvrs, 0, 0.0)
875-
vNvrsP = _np_insert(vNvrsP, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
876-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
879+
# For CRRA=1 (log utility), the pseudo-inverse value function slope is simply MPC
880+
if CRRA == 1.0:
881+
vNvrsP = _np_insert(vNvrsP, 0, MPCmaxEff)
882+
MPCminNvrs = MPCminNow
883+
else:
884+
vNvrsP = _np_insert(vNvrsP, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
885+
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
877886

878887
return (
879888
mNrmGrid,

HARK/ConsumptionSaving/ConsMarkovModel.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
CRRAutilityP_inv,
4040
CRRAutilityP_invP,
4141
CRRAutilityPP,
42+
vNvrsSlope,
4243
)
4344
from HARK.utilities import make_assets_grid
4445

@@ -641,9 +642,9 @@ def calc_vPPnext(S, a, R):
641642
mNrm_temp = np.insert(mNrm_for_vFunc, 0, mNrmMin_i) # add the lower bound
642643
vNvrs_now = np.insert(vNvrs_now, 0, 0.0)
643644
vNvrsP_now = np.insert(
644-
vNvrsP_now, 0, MPCmaxEff[i] ** (-CRRA / (1.0 - CRRA))
645+
vNvrsP_now, 0, vNvrsSlope(MPCmaxEff[i], CRRA)
645646
)
646-
# MPCminNvrs = MPCminNow[i] ** (-CRRA / (1.0 - CRRA))
647+
# MPCminNvrs = vNvrsSlope(MPCminNow[i], CRRA)
647648
vNvrsFuncNow = LinearInterp(
648649
mNrm_temp,
649650
vNvrs_now,

HARK/ConsumptionSaving/ConsPrefShockModel.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
MargMargValueFuncCRRA,
3535
ValueFuncCRRA,
3636
)
37-
from HARK.rewards import UtilityFuncCRRA
37+
from HARK.rewards import UtilityFuncCRRA, vNvrsSlope
3838

3939
__all__ = [
4040
"PrefShockConsumerType",
@@ -376,8 +376,8 @@ def calc_vPPnext(S, a, R):
376376
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
377377
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
378378
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
379-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
380-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
379+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA))
380+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
381381
vNvrsFuncNow = CubicInterp(
382382
mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs
383383
)
@@ -683,8 +683,8 @@ def calc_vPPnext(S, a, R):
683683
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
684684
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
685685
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
686-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
687-
MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
686+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA))
687+
MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
688688
vNvrsFuncNow = CubicInterp(
689689
mNrm_temp, vNvrs_temp, vNvrsP_temp, MPCminNvrs * hNrmNow, MPCminNvrs
690690
)

HARK/ConsumptionSaving/ConsRiskyAssetModel.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
MargValueFuncCRRA,
4141
ValueFuncCRRA,
4242
)
43-
from HARK.rewards import UtilityFuncCRRA
43+
from HARK.rewards import UtilityFuncCRRA, vNvrsSlope
4444
from HARK.utilities import make_assets_grid
4545

4646
###############################################################################
@@ -1559,8 +1559,8 @@ def calc_vPPnext(S, a):
15591559
vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1))
15601560
mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow)
15611561
vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0)
1562-
vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA)))
1563-
# MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA))
1562+
vNvrsP_temp = np.insert(vNvrsP_temp, 0, vNvrsSlope(MPCmaxEff, CRRA))
1563+
# MPCminNvrs = vNvrsSlope(MPCminNow, CRRA)
15641564
vNvrsFuncNow = CubicInterp(mNrm_temp, vNvrs_temp, vNvrsP_temp)
15651565
vFuncNow = ValueFuncCRRA(vNvrsFuncNow, CRRA)
15661566
else:

HARK/rewards.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,46 @@ def CRRAutilityP_invP(uP, rho):
234234
return (-1.0 / rho) * uP ** (-1.0 / rho - 1.0)
235235

236236

237+
def vNvrsSlope(MPC, rho):
238+
"""
239+
Computes the slope of the pseudo-inverse value function for CRRA utility.
240+
241+
For CRRA utility, this is used to determine the asymptotic behavior of the
242+
pseudo-inverse value function vNvrs = u^(-1)(v) as resources become large.
243+
244+
For rho ≠ 1: The slope is MPC^(-rho/(1-rho))
245+
For rho = 1 (log utility): The slope is simply MPC
246+
247+
This function provides the proper limiting behavior for log utility, where
248+
the standard formula MPC^(-rho/(1-rho)) is undefined.
249+
250+
Parameters
251+
----------
252+
MPC : float or np.ndarray
253+
Marginal propensity to consume value(s)
254+
rho : float
255+
Coefficient of relative risk aversion (CRRA)
256+
257+
Returns
258+
-------
259+
float or np.ndarray
260+
Slope of the pseudo-inverse value function
261+
262+
Notes
263+
-----
264+
For log utility (rho=1), the derivation is as follows:
265+
- u(c) = log(c), so u^(-1)(v) = exp(v)
266+
- The pseudo-inverse value function is vNvrs(m) = exp(v(m))
267+
- d/dm vNvrs = exp(v) * dv/dm = exp(v) * u'(c) * MPC = exp(log(c)) * (1/c) * MPC = MPC
268+
269+
The expression MPC^(-rho/(1-rho)) diverges as rho → 1, but the properly
270+
derived formula for log utility gives MPC directly.
271+
"""
272+
if rho == 1:
273+
return MPC
274+
return MPC ** (-rho / (1.0 - rho))
275+
276+
237277
###############################################################################
238278

239279
# Define legacy versions of CRRA utility functions with no decorator.

tests/ConsumptionSaving/test_PerfForesightConsumerType.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,27 @@ def test_stable_points(self):
148148
self.assertEqual(
149149
constrained_agent.solution[0].mNrmStE, constrained_agent.solution[0].mNrmTrg
150150
)
151+
152+
def test_CRRA_equals_one(self):
153+
"""
154+
Test that CRRA=1 (log utility) works correctly.
155+
156+
This tests fix for issue #75 where CRRA=1 would cause ZeroDivisionError
157+
due to expressions like MPC ** (-CRRA / (1.0 - CRRA)).
158+
"""
159+
# Test with CRRA = 1.0 (log utility)
160+
log_utility_agent = PerfForesightConsumerType(cycles=0)
161+
log_utility_agent.CRRA = 1.0
162+
163+
# This should not raise ZeroDivisionError
164+
log_utility_agent.solve()
165+
166+
# Verify the solution exists and is reasonable
167+
self.assertIsNotNone(log_utility_agent.solution[0].cFunc)
168+
169+
# Consumption at m=5 should be positive
170+
c_at_5 = log_utility_agent.solution[0].cFunc(5.0)
171+
self.assertGreater(c_at_5, 0.0)
172+
173+
# Consumption should be less than resources (can't consume more than you have)
174+
self.assertLess(c_at_5, 5.0)

0 commit comments

Comments
 (0)