Skip to content

Commit 901dd7c

Browse files
committed
Reproduce PR #1479
Alan rejiggered the contents of distribution.py, so I have recreated the changes from my prior PR into this one. Committing and then will run tests.
1 parent ea4931f commit 901dd7c

11 files changed

+213
-55
lines changed

Documentation/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Release Date: TBD
3434
- Fixes notation errors in IndShockConsumerType.make_euler_error_func from prior changes. [1495](https://github.com/econ-ark/HARK/pull/1495)
3535
- Fixes typos in IdentityFunction interpolator class. [1492](https://github.com/econ-ark/HARK/pull/1492)
3636
- Expands functionality of Cobb-Douglas aggregator for CRRA utility. [1363](https://github.com/econ-ark/HARK/pull/1363)
37+
- Improved tracking of the bounds of support for distributions, and (some) solvers now respect those bounds when computing the "worst outcome". [XXXX](https://github.com/econ-ark/HARK/pull/1479)
3738

3839
### 0.15.1
3940

HARK/Calibration/Income/IncomeProcesses.py

+31-5
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,27 @@ class LognormPermIncShk(DiscreteDistribution):
7474

7575
def __init__(self, sigma, n_approx, neutral_measure=False, seed=0):
7676
# Construct an auxiliary discretized normal
77-
logn_approx = MeanOneLogNormal(sigma).discretize(
77+
lognormal_dstn = MeanOneLogNormal(sigma)
78+
logn_approx = lognormal_dstn.discretize(
7879
n_approx if sigma > 0.0 else 1, method="equiprobable", tail_N=0
7980
)
81+
82+
limit = {
83+
"dist": lognormal_dstn,
84+
"method": "equiprobable",
85+
"N": n_approx,
86+
"endpoints": False,
87+
"infimum": logn_approx.limit["infimum"],
88+
"supremum": logn_approx.limit["supremum"],
89+
}
90+
8091
# Change the pmv if necessary
8192
if neutral_measure:
8293
logn_approx.pmv = (logn_approx.atoms * logn_approx.pmv).flatten()
8394

84-
super().__init__(pmv=logn_approx.pmv, atoms=logn_approx.atoms, seed=seed)
95+
super().__init__(
96+
pmv=logn_approx.pmv, atoms=logn_approx.atoms, limit=limit, seed=seed
97+
)
8598

8699

87100
class MixtureTranIncShk(DiscreteDistribution):
@@ -110,15 +123,22 @@ class MixtureTranIncShk(DiscreteDistribution):
110123
"""
111124

112125
def __init__(self, sigma, UnempPrb, IncUnemp, n_approx, seed=0):
113-
dstn_approx = MeanOneLogNormal(sigma).discretize(
126+
dstn_orig = MeanOneLogNormal(sigma)
127+
dstn_approx = dstn_orig.discretize(
114128
n_approx if sigma > 0.0 else 1, method="equiprobable", tail_N=0
115129
)
130+
116131
if UnempPrb > 0.0:
117132
dstn_approx = add_discrete_outcome_constant_mean(
118133
dstn_approx, p=UnempPrb, x=IncUnemp
119134
)
120135

121-
super().__init__(pmv=dstn_approx.pmv, atoms=dstn_approx.atoms, seed=seed)
136+
super().__init__(
137+
pmv=dstn_approx.pmv,
138+
atoms=dstn_approx.atoms,
139+
limit=dstn_approx.limit,
140+
seed=seed,
141+
)
122142

123143

124144
class MixtureTranIncShk_HANK(DiscreteDistribution):
@@ -173,7 +193,12 @@ def __init__(
173193
# Rescale the transitory shock values to account for new features
174194
TranShkMean_temp = (1.0 - tax_rate) * labor * wage
175195
dstn_approx.atoms *= TranShkMean_temp
176-
super().__init__(pmv=dstn_approx.pmv, atoms=dstn_approx.atoms, seed=seed)
196+
super().__init__(
197+
pmv=dstn_approx.pmv,
198+
atoms=dstn_approx.atoms,
199+
limit=dstn_approx.limit,
200+
seed=seed,
201+
)
177202

178203

179204
class BufferStockIncShkDstn(DiscreteDistributionLabeled):
@@ -239,6 +264,7 @@ def __init__(
239264
var_names=["PermShk", "TranShk"],
240265
pmv=joint_dstn.pmv,
241266
atoms=joint_dstn.atoms,
267+
limit=joint_dstn.limit,
242268
seed=seed,
243269
)
244270

HARK/ConsumptionSaving/ConsIndShockModel.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -395,31 +395,45 @@ def solve_one_period_ConsPF(
395395
return solution_now
396396

397397

398-
def calc_worst_inc_prob(inc_shk_dstn):
398+
def calc_worst_inc_prob(inc_shk_dstn, use_infimum=True):
399399
"""Calculate the probability of the worst income shock.
400400
401401
Args:
402402
inc_shk_dstn (DiscreteDistribution): Distribution of shocks to income.
403+
use_infimum (bool): Indicator for whether to try to use the infimum of the limiting (true) income distribution.
403404
"""
404405
probs = inc_shk_dstn.pmv
405406
perm, tran = inc_shk_dstn.atoms
406407
income = perm * tran
407-
worst_inc = np.min(income)
408+
if use_infimum:
409+
worst_inc = np.prod(inc_shk_dstn.limit["infimum"])
410+
else:
411+
worst_inc = np.min(income)
408412
return np.sum(probs[income == worst_inc])
409413

410414

411-
def calc_boro_const_nat(m_nrm_min_next, inc_shk_dstn, rfree, perm_gro_fac):
415+
def calc_boro_const_nat(
416+
m_nrm_min_next, inc_shk_dstn, rfree, perm_gro_fac, use_infimum=True
417+
):
412418
"""Calculate the natural borrowing constraint.
413419
414420
Args:
415421
m_nrm_min_next (float): Minimum normalized market resources next period.
416422
inc_shk_dstn (DiscreteDstn): Distribution of shocks to income.
417423
rfree (float): Risk free interest factor.
418424
perm_gro_fac (float): Permanent income growth factor.
425+
use_infimum (bool): Indicator for whether to use the infimum of the limiting (true) income distribution
419426
"""
420-
perm, tran = inc_shk_dstn.atoms
421-
temp_fac = (perm_gro_fac * np.min(perm)) / rfree
422-
return (m_nrm_min_next - np.min(tran)) * temp_fac
427+
if use_infimum:
428+
perm_min, tran_min = inc_shk_dstn.limit["infimum"]
429+
else:
430+
perm, tran = inc_shk_dstn.atoms
431+
perm_min = np.min(perm)
432+
tran_min = np.min(tran)
433+
434+
temp_fac = (perm_gro_fac * perm_min) / rfree
435+
boro_cnst_nat = (m_nrm_min_next - tran_min) * temp_fac
436+
return boro_cnst_nat
423437

424438

425439
def calc_m_nrm_min(boro_const_art, boro_const_nat):
@@ -823,7 +837,7 @@ def solve_one_period_ConsKinkedR(
823837
DiscFacEff = DiscFac * LivPrb # "effective" discount factor
824838

825839
# Calculate the probability that we get the worst possible income draw
826-
WorstIncPrb = calc_worst_inc_prob(IncShkDstn)
840+
WorstIncPrb = calc_worst_inc_prob(IncShkDstn, use_infimum=False)
827841
# WorstIncPrb is the "Weierstrass p" concept: the odds we get the WORST thing
828842
Ex_IncNext = expected(lambda x: x["PermShk"] * x["TranShk"], IncShkDstn)
829843
hNrmNow = calc_human_wealth(solution_next.hNrm, PermGroFac, Rsave, Ex_IncNext)
@@ -835,7 +849,11 @@ def solve_one_period_ConsKinkedR(
835849

836850
# Calculate the minimum allowable value of money resources in this period
837851
BoroCnstNat = calc_boro_const_nat(
838-
solution_next.mNrmMin, IncShkDstn, Rboro, PermGroFac
852+
solution_next.mNrmMin,
853+
IncShkDstn,
854+
Rboro,
855+
PermGroFac,
856+
use_infimum=False,
839857
)
840858
# Set the minimum allowable (normalized) market resources based on the natural
841859
# and artificial borrowing constraints

HARK/ConsumptionSaving/tests/test_ConsNewKeynesianModel.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_calc_tran_matrix(self):
3939
AggC = np.dot(gridc.flatten(), vecDstn) # Aggregate Consumption
4040
AggA = np.dot(grida.flatten(), vecDstn) # Aggregate Assets
4141

42-
self.assertAlmostEqual(AggA[0], 0.82984, places=4)
42+
self.assertAlmostEqual(AggA[0], 0.82960, places=4)
4343
self.assertAlmostEqual(AggC[0], 1.00780, places=4)
4444

4545

@@ -52,6 +52,6 @@ def test_calc_jacobian(self):
5252
Agent.compute_steady_state()
5353
CJAC_Perm, AJAC_Perm = Agent.calc_jacobian("PermShkStd", 50)
5454

55-
self.assertAlmostEqual(CJAC_Perm.T[30][29], -0.10503, places=HARK_PRECISION)
55+
self.assertAlmostEqual(CJAC_Perm.T[30][29], -0.10508, places=HARK_PRECISION)
5656
self.assertAlmostEqual(CJAC_Perm.T[30][30], 0.10316, places=HARK_PRECISION)
5757
self.assertAlmostEqual(CJAC_Perm.T[30][31], 0.09059, places=HARK_PRECISION)

HARK/ConsumptionSaving/tests/test_IndShockConsumerType.py

+14-12
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,17 @@ def test_ConsIndShockSolverBasic(self):
5050

5151
self.assertAlmostEqual(
5252
LifecycleExample.solution[0].cFunc(1).tolist(),
53-
0.75062,
53+
0.75074,
5454
places=HARK_PRECISION,
5555
)
5656
self.assertAlmostEqual(
5757
LifecycleExample.solution[1].cFunc(1).tolist(),
58-
0.75864,
58+
0.75876,
5959
places=HARK_PRECISION,
6060
)
6161
self.assertAlmostEqual(
6262
LifecycleExample.solution[2].cFunc(1).tolist(),
63-
0.76812,
63+
0.76824,
6464
places=HARK_PRECISION,
6565
)
6666

@@ -227,13 +227,15 @@ def test_infinite_horizon(self):
227227
IndShockExample.solve()
228228

229229
self.assertAlmostEqual(
230-
IndShockExample.solution[0].mNrmStE, 1.54882, places=HARK_PRECISION
231-
)
232-
self.assertAlmostEqual(
233-
IndShockExample.solution[0].cFunc.functions[0].x_list[0],
234-
-0.25018,
235-
places=HARK_PRECISION,
230+
IndShockExample.solution[0].mNrmStE, 1.54765, places=HARK_PRECISION
236231
)
232+
# self.assertAlmostEqual(
233+
# IndShockExample.solution[0].cFunc.functions[0].x_list[0],
234+
# -0.25018,
235+
# places=HARK_PRECISION,
236+
# )
237+
# This test is commented out because it was trivialized by revisions to the "worst income shock" code.
238+
# The bottom x value of the unconstrained consumption function will definitely be zero, so this is pointless.
237239

238240
IndShockExample.track_vars = ["aNrm", "mNrm", "cNrm", "pLvl"]
239241
IndShockExample.initialize_sim()
@@ -309,7 +311,7 @@ def test_lifecyle(self):
309311

310312
self.assertAlmostEqual(
311313
LifecycleExample.solution[5].cFunc(3).tolist(),
312-
2.12998,
314+
2.13004,
313315
places=HARK_PRECISION,
314316
)
315317

@@ -383,15 +385,15 @@ def test_cyclical(self):
383385

384386
self.assertAlmostEqual(
385387
CyclicalExample.solution[3].cFunc(3).tolist(),
386-
1.59584,
388+
1.59597,
387389
places=HARK_PRECISION,
388390
)
389391

390392
CyclicalExample.initialize_sim()
391393
CyclicalExample.simulate()
392394

393395
self.assertAlmostEqual(
394-
CyclicalExample.state_now["aLvl"][1], 3.32431, places=HARK_PRECISION
396+
CyclicalExample.state_now["aLvl"][1], 3.31950, places=HARK_PRECISION
395397
)
396398

397399

HARK/ConsumptionSaving/tests/test_modelcomparisons.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def setUp(self):
4747
test_dictionary["UnempPrb"] = 0.0
4848
test_dictionary["T_cycle"] = 1
4949
test_dictionary["T_retire"] = 0
50-
test_dictionary["BoroCnstArt"] = None
50+
test_dictionary["BoroCnstArt"] = 0.0
5151

5252
InfiniteType = IndShockConsumerType(**test_dictionary)
5353
InfiniteType.cycles = 0
@@ -79,7 +79,7 @@ def diffFunc(m):
7979
m
8080
) - self.InfiniteType.cFunc[0](m)
8181

82-
points = np.arange(0.5, mNrmMinInf + aXtraMin, mNrmMinInf + aXtraMax)
82+
points = np.arange(0.5, mNrmMinInf + aXtraMax, mNrmMinInf + aXtraMin)
8383
difference = diffFunc(points)
8484
max_difference = np.max(np.abs(difference))
8585

HARK/distributions/base.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def __init__(self, seed: Optional[int] = 0) -> None:
4343

4444
self._seed: int = _seed
4545
self._rng: random.Generator = random.default_rng(self._seed)
46+
47+
# Bounds of distribution support should be overwritten by subclasses
48+
self.infimum = np.array([])
49+
self.supremum = np.array([])
4650

4751
@property
4852
def seed(self) -> int:
@@ -130,7 +134,7 @@ def discretize(
130134
131135
Returns
132136
-------
133-
DiscreteDistribution
137+
discretized_dstn : DiscreteDistribution
134138
Discretized distribution.
135139
136140
Raises
@@ -149,7 +153,10 @@ def discretize(
149153
)
150154

151155
approx = getattr(self, approx_method)
152-
return approx(N, endpoints, **kwds)
156+
discretized_dstn = approx(N, endpoints, **kwds)
157+
discretized_dstn.limit["infimum"] = self.infimum.copy()
158+
discretized_dstn.limit["supremum"] = self.supremum.copy()
159+
return discretized_dstn
153160

154161

155162
class MarkovProcess(Distribution):

HARK/distributions/continuous.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def __init__(self, mu=0.0, sigma=1.0, seed=0):
6161
)
6262

6363
super().__init__(stats.norm, loc=mu, scale=sigma, seed=seed)
64+
self.infimum = -np.inf * np.ones(self.mu.size)
65+
self.supremum = np.inf * np.ones(self.mu.size)
6466

6567
def discretize(self, N, method="hermite", endpoints=False):
6668
"""
@@ -75,8 +77,6 @@ def _approx_hermite(self, N, endpoints=False):
7577
Returns a discrete approximation of this distribution
7678
using the Gauss-Hermite quadrature rule.
7779
78-
TODO: add endpoints option
79-
8080
Parameters
8181
----------
8282
N : int
@@ -107,8 +107,6 @@ def _approx_equiprobable(self, N, endpoints=False):
107107
"""
108108
Returns a discrete equiprobable approximation of this distribution.
109109
110-
TODO: add endpoints option
111-
112110
Parameters
113111
----------
114112
N : int
@@ -223,6 +221,8 @@ def __init__(
223221
super().__init__(
224222
stats.lognorm, s=self.sigma, scale=np.exp(self.mu), loc=0, seed=seed
225223
)
224+
self.infimum = np.array([0.0])
225+
self.supremum = np.array([np.inf])
226226

227227
def _approx_equiprobable(
228228
self, N, endpoints=False, tail_N=0, tail_bound=None, tail_order=np.e
@@ -349,8 +349,6 @@ def _approx_hermite(self, N, endpoints=False):
349349
Returns a discrete approximation of this distribution
350350
using the Gauss-Hermite quadrature rule.
351351
352-
TODO: add endpoints option
353-
354352
Parameters
355353
----------
356354
N : int
@@ -446,6 +444,8 @@ def __init__(self, bot=0.0, top=1.0, seed=0):
446444
super().__init__(
447445
stats.uniform, loc=self.bot, scale=self.top - self.bot, seed=seed
448446
)
447+
self.infimum = np.array([0.0])
448+
self.supremum = np.array([np.inf])
449449

450450
def _approx_equiprobable(self, N, endpoints=False):
451451
"""
@@ -476,7 +476,12 @@ def _approx_equiprobable(self, N, endpoints=False):
476476
atoms = np.concatenate(([self.bot], atoms, [self.top]))
477477
pmv = np.concatenate(([0.0], pmv, [0.0]))
478478

479-
limit = {"dist": self, "method": "equiprobable", "N": N, "endpoints": endpoints}
479+
limit = {
480+
"dist": self,
481+
"method": "equiprobable",
482+
"N": N,
483+
"endpoints": endpoints,
484+
}
480485

481486
return DiscreteDistribution(
482487
pmv,
@@ -506,5 +511,8 @@ class Weibull(ContinuousFrozenDistribution):
506511
def __init__(self, scale=1.0, shape=1.0, seed=0):
507512
self.scale = np.asarray(scale)
508513
self.shape = np.asarray(shape)
514+
509515
# Set up the RNG
510516
super().__init__(stats.weibull_min, c=shape, scale=scale, seed=seed)
517+
self.infimum = np.array([0.0])
518+
self.supremum = np.array([np.inf])

0 commit comments

Comments
 (0)