Skip to content

Commit 07aac8e

Browse files
committed
Add tests & documentation, improve format
Object classes (etc) are now displayed in line.
1 parent bc614bf commit 07aac8e

File tree

3 files changed

+204
-25
lines changed

3 files changed

+204
-25
lines changed

HARK/metric.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,30 +137,34 @@ def describe_metric(thing, n=0, label=None, D=1e10):
137137
if label is None:
138138
desc = ""
139139
else:
140-
desc = pad * n * " " + "-" + label + ": "
140+
desc = pad * n * " " + "- " + label + " "
141141

142142
# If both inputs are numbers, distance is their difference
143143
if isinstance(thing, (int, float)):
144-
desc += "absolute difference of values\n"
144+
desc += "(scalar): absolute difference of values\n"
145145

146146
elif isinstance(thing, list):
147147
J = len(thing)
148-
desc += "largest distance in this list:\n"
148+
desc += "(list) largest distance among:\n"
149149
if n == D:
150150
desc += pad * (n + 1) * " " + "SUPPRESSED OUTPUT\n"
151151
else:
152152
for j in range(J):
153-
desc += describe_metric(thing[j], n + 1, label=str(j), D=D)
153+
desc += describe_metric(thing[j], n + 1, label="[" + str(j) + "]", D=D)
154154

155155
elif isinstance(thing, np.ndarray):
156-
desc += "greatest absolute difference among array elements\n"
156+
desc += (
157+
"(array"
158+
+ str(thing.shape)
159+
+ "): greatest absolute difference among elements\n"
160+
)
157161

158162
elif isinstance(thing, dict):
159163
if "distance_criteria" in thing.keys():
160164
my_keys = thing["distance_criteria"]
161165
else:
162166
my_keys = thing.keys()
163-
desc += "largest distance among these keys:\n"
167+
desc += "(dict): largest distance among these keys:\n"
164168
if n == D:
165169
desc += pad * (n + 1) * " " + "SUPPRESSED OUTPUT\n"
166170
else:
@@ -169,7 +173,11 @@ def describe_metric(thing, n=0, label=None, D=1e10):
169173

170174
elif isinstance(thing, MetricObject):
171175
my_keys = thing.distance_criteria
172-
desc += "largest distance among these attributes:\n"
176+
desc += (
177+
"(" + type(thing).__name__ + "): largest distance among these attributes:\n"
178+
)
179+
if len(my_keys) == 0:
180+
desc += pad * (n + 1) * " " + "NO distance_critera SPECIFIED"
173181
if n == D:
174182
desc += pad * (n + 1) * " " + "SUPPRESSED OUTPUT\n"
175183
else:
@@ -239,7 +247,7 @@ def describe_distance(self, display=True, max_depth=None):
239247
Description of how this object's distance metric is computed, if
240248
display=False.
241249
"""
242-
max_depth = max_depth or np.inf
250+
max_depth = max_depth if max_depth is not None else np.inf
243251

244252
keys = self.distance_criteria
245253
if len(keys) == 0:

examples/Gentle-Intro/Advanced-Intro.ipynb

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,26 @@
1818
},
1919
{
2020
"cell_type": "markdown",
21-
"id": "9e27a5d6-12d4-4ad5-83fa-465ca2a0beef",
21+
"id": "37bf4f7e-347a-42d9-bfd7-fb4294c01606",
2222
"metadata": {},
2323
"source": [
2424
"## Measuring Up: Distance in HARK\n",
2525
"\n",
26-
"In heterogeneous agents macroeconomics, we are often interested in infinite horizon models. Such models are usually solved by \"finite horizon approximation\": iteratively solving backward one period at a time until consecutive solutions are sufficiently close together that we conclude the process has converged. When using Bellman value functions, this logic relies on the fact that the Bellman operator is a contraction mapping.\n",
27-
"\n",
28-
"But what does it mean to be \"sufficiently close\"? What are we even comparing when we talk about the \"distance\" between two candidate solutions? It depends on the context, and so HARK has a system for easily identifying \"what matters\" when calculating the distance between two abstract objects."
26+
"In heterogeneous agents macroeconomics, we are often interested in infinite horizon models. Such models are usually solved by \"finite horizon approximation\": iteratively solving backward one period at a time until consecutive solutions are sufficiently close together that we conclude the process has converged. When using Bellman value functions, this logic relies on the fact that the Bellman operator is a contraction mapping."
2927
]
3028
},
3129
{
3230
"cell_type": "markdown",
33-
"id": "6ea0a819-3ac3-40d4-aecc-136460e1fe24",
31+
"id": "89e630e9-c545-47e3-8817-ee927da34cfa",
3432
"metadata": {},
3533
"source": [
34+
"### The Universal Distance Metric\n",
35+
"\n",
36+
"But what does it mean to be \"sufficiently close\"? What are we even comparing when we talk about the \"distance\" between two candidate solutions? It depends on the context, and so HARK has a system for easily identifying \"what matters\" when calculating the distance between two abstract objects.\n",
37+
"\n",
3638
"Particularly, all classes in HARK that could reasonably be part of the representation of a model solution inherit from the superclass `HARK.metric.MetricObject`. Essentially all this class does is provide a \"universal distance metric\" with simple customization.\n",
3739
"\n",
38-
"The *only* think that a subclass of `MetricObject` needs to specify is a class attribute called `distance_criteria`, which must be a list of strings. Each element of `distance_criteria` names an attribute of that class that should be compared if HARK is ever asked to compare two instances of that class."
40+
"The *only* thing that a subclass of `MetricObject` needs to specify is a class attribute called `distance_criteria`, which must be a list of strings. Each element of `distance_criteria` names an attribute of that class that should be compared if HARK is ever asked to compare two instances of that class."
3941
]
4042
},
4143
{
@@ -68,7 +70,7 @@
6870
"source": [
6971
"As long as you are coding with `MetricObject` subclasses (like the interpolators in `HARK.interpolation`) and standard numeric Python objects, the distance metric will always succeed in comparing objects.\n",
7072
"\n",
71-
"Note that comparing \"incomparable\" objects, like arrays of different shapes, will return a somewhat arbitrary \"large\" number (not near zero). This is because the sole purpose of measuring \"distance\" in HARK is to evaluate whether two solutions are sufficiently close. The tolerance level for such operations is usually on the order of $10^{-4}$ to $10^{-8}$, not $10$ or $1000$. That is, those \"error code distances\" mostly serve to ensure that a convergence criteria will *definitely* not be met when the comparitors are incomparable."
73+
"Note that comparing \"incomparable\" objects, like arrays of different shapes, will return a somewhat arbitrary \"large\" number (not near zero). This is because the sole purpose of measuring \"distance\" in HARK is to evaluate whether two solutions are sufficiently close. The tolerance level for such operations is usually on the order of $10^{-4}$ to $10^{-8}$, not $10$ or $1000$. Thus those \"error code distances\" mostly serve to ensure that a convergence criteria will *definitely* not be met when the comparitors are incomparable."
7274
]
7375
},
7476
{
@@ -83,32 +85,122 @@
8385
},
8486
{
8587
"cell_type": "markdown",
86-
"id": "3f83756d-4e68-4d20-89e2-690c33aa7f0e",
88+
"id": "152804a3-55ec-4512-9576-5773760160e3",
8789
"metadata": {},
8890
"source": [
89-
"## Uncommon Options When Solving Models\n",
91+
"### Getting a Description of the Distance Metric\n",
9092
"\n",
91-
"The `solve()` method is usually called without any arguments, but there are a few options you can specify."
93+
"We're not going to sugarcoat it: solution objects in HARK can be *very* complicated, with several layers of nesting. The objects that are actually being *numerically* compared by the distance metric might be three to six layers deep, buried in attributes, lists, and/or dictionaries.\n",
94+
"\n",
95+
"To make it a little bit easier to understand what's actually being compared, `MetricObject` (and its many descendant classes) have a `describe_distance` method that produces a nested description of how an object's distance metric would be computed. Let's see a quick example:"
96+
]
97+
},
98+
{
99+
"cell_type": "code",
100+
"execution_count": 2,
101+
"id": "d30b649d-04a6-4686-9e12-6cff4cfc6116",
102+
"metadata": {},
103+
"outputs": [
104+
{
105+
"name": "stdout",
106+
"output_type": "stream",
107+
"text": [
108+
"largest distance among these attributes:\n",
109+
" - vPfunc: largest distance among these attributes:\n",
110+
" - cFunc: largest distance among these attributes:\n",
111+
" - functions: largest distance in this list:\n",
112+
" - 0: largest distance among these attributes:\n",
113+
" - x_list: greatest absolute difference among array elements\n",
114+
" - y_list: greatest absolute difference among array elements\n",
115+
" - 1: largest distance among these attributes:\n",
116+
" - x_list: greatest absolute difference among array elements\n",
117+
" - y_list: greatest absolute difference among array elements\n",
118+
" - CRRA: absolute difference of values\n"
119+
]
120+
}
121+
],
122+
"source": [
123+
"from HARK.models import IndShockConsumerType\n",
124+
"\n",
125+
"MyType = IndShockConsumerType() # default parameters, only one non-terminal period\n",
126+
"MyType.solve()\n",
127+
"MyType.solution[0].describe_distance()"
92128
]
93129
},
94130
{
95131
"cell_type": "markdown",
96-
"id": "0fb621bc-0a3e-4da8-a067-21ace101d41a",
132+
"id": "3b7b26dc-f96f-4910-9fa9-162ceaf4738f",
97133
"metadata": {},
98134
"source": [
99-
"### Tell Me More, Tell Me More: `verbose`\n",
135+
"First, notice that the distance metric *wasn't actually used* in this case-- it's a finite horizon model with only one non-terminal period! The `describe_distance` method is independent of `distance_metric`, and simply produces what *would be done* if the object were compared to itself.\n",
100136
"\n",
101-
"First, passing `verbose=True` (or just `True`, because it is the first argument) when solving an infinite horizon model (`cycles=0`) will print solution progress to screen. This can be useful when developing a new model, if you want to know how long iterations take and how the solver is doing with respect to convergence."
137+
"By default, `describe_distance` prints its description to screen; if you would rather have it returned as a string, pass the argument `display=False`.\n",
138+
"\n",
139+
"The first line indicates that the `solution` itself has a distance metric that depends *only* on its `vPfunc` attribute, the only item at the first level of indentation. The marginal value function `vPfunc` has `distance criteria` of `cFunc` (the pseudo-inverse marginal value function, which is the same thing as the consumption function) and risk aversion `CRRA` (very last line).\n",
140+
"\n",
141+
"Why would `CRRA` be compared if it *never* changes during the model? Because we want `distance_metric` to properly compare two objects to see if they're the same (or close to the same), and *generically* two marginal value functions would be *different functions* if they had the same pseudo-inverse marginal value function but *different* values of $\\rho$."
142+
]
143+
},
144+
{
145+
"cell_type": "markdown",
146+
"id": "600e71af-42c2-4431-bd75-602408388837",
147+
"metadata": {},
148+
"source": [
149+
"The upshot is that for an `IndShockConsumerType`, convergence of the solution depends on the consumption function, which is what you probably guessed in the first place. But what does that actually mean numerically?\n",
150+
"\n",
151+
"The consumption function in this model is an instance of `LowerEnvelope`, an interpolation wrapper class that evaluates to the *lowest* function value (pointwise). The two component `functions` are the unconstrained consumption function (if the artificial borrowing constraint were not imposed *that period*) and the borrowing constraint itself; whenever the unconstrained consumption function would exceed the constraint, the agent actually consumes on the constraint.\n",
152+
"\n",
153+
"Each of those component functions (indexed by `0` and `1`) is an instance of `LinearInterp`, and the `distance_metric` compares their interpoling grid and function values-- the `x_list` and `y_list` arrays."
154+
]
155+
},
156+
{
157+
"cell_type": "markdown",
158+
"id": "1ad73b3f-fd8f-4b3f-aac3-7f89112d1853",
159+
"metadata": {},
160+
"source": [
161+
"In some cases, the output from `describe_distance` is so large that it's difficult to read. To handle this, you can specify a `max_depth` to indicate how many layers deep the description should go. For example, we can suppress the description of the consumption function like so:"
102162
]
103163
},
104164
{
105165
"cell_type": "code",
106-
"execution_count": 1,
107-
"id": "a8b73d97-f92d-4514-9f53-5367de6719e6",
166+
"execution_count": 4,
167+
"id": "4aecc5de-20f7-423d-9368-fd2a85b9f412",
168+
"metadata": {},
169+
"outputs": [
170+
{
171+
"name": "stdout",
172+
"output_type": "stream",
173+
"text": [
174+
"largest distance among these attributes:\n",
175+
" - vPfunc: largest distance among these attributes:\n",
176+
" - cFunc: largest distance among these attributes:\n",
177+
" SUPPRESSED OUTPUT\n",
178+
" - CRRA: absolute difference of values\n"
179+
]
180+
}
181+
],
182+
"source": [
183+
"MyType.solution[0].describe_distance(max_depth=2)"
184+
]
185+
},
186+
{
187+
"cell_type": "markdown",
188+
"id": "3f83756d-4e68-4d20-89e2-690c33aa7f0e",
189+
"metadata": {},
190+
"source": [
191+
"## Uncommon Options When Solving Models\n",
192+
"\n",
193+
"The `solve()` method is usually called without any arguments, but there are a few options you can specify."
194+
]
195+
},
196+
{
197+
"cell_type": "markdown",
198+
"id": "0fb621bc-0a3e-4da8-a067-21ace101d41a",
108199
"metadata": {},
109-
"outputs": [],
110200
"source": [
111-
"from HARK.ConsumptionSaving.ConsIndShockModel import IndShockConsumerType"
201+
"### Tell Me More, Tell Me More: `verbose`\n",
202+
"\n",
203+
"First, passing `verbose=True` (or just `True`, because it is the first argument) when solving an infinite horizon model (`cycles=0`) will print solution progress to screen. This can be useful when developing a new model, if you want to know how long iterations take and how the solver is doing with respect to convergence."
112204
]
113205
},
114206
{

tests/test_metric.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Unit tests for HARK.metric. Very little is tested here; the tests for describing
3+
distance metric are mostly to ensure test coverage.
4+
"""
5+
6+
# Bring in modules we need
7+
import unittest
8+
9+
from HARK.metric import distance_metric, describe_metric, MetricObject
10+
from HARK.models import IndShockConsumerType
11+
12+
13+
class testsForDictionaryMetric(unittest.TestCase):
14+
def setUp(self):
15+
dict_A = {
16+
"height": 5.0,
17+
"width": 3.0,
18+
"depth": 2.0,
19+
}
20+
dict_B = dict_A.copy()
21+
dict_B["distance_criteria"] = ["height"]
22+
dict_C = dict_B.copy()
23+
dict_C["width"] += 1.0
24+
self.A = dict_A
25+
self.B = dict_B
26+
self.C = dict_C
27+
28+
def test_same(self):
29+
dist = distance_metric(self.A, self.A)
30+
self.assertAlmostEqual(dist, 0.0)
31+
dist = distance_metric(self.B, self.B)
32+
self.assertAlmostEqual(dist, 0.0)
33+
dist = distance_metric(self.C, self.C)
34+
self.assertAlmostEqual(dist, 0.0)
35+
36+
def test_diff(self):
37+
dist = distance_metric(self.A, self.B)
38+
self.assertAlmostEqual(dist, 1000.0)
39+
dist = distance_metric(self.B, self.C)
40+
self.assertAlmostEqual(dist, 0.0)
41+
dist = distance_metric(self.A, self.C)
42+
self.assertAlmostEqual(dist, 1000.0)
43+
del self.C["distance_criteria"]
44+
dist = distance_metric(self.A, self.C)
45+
self.assertAlmostEqual(dist, 1.0)
46+
47+
48+
class testsForDescribeDistance(unittest.TestCase):
49+
def setUp(self):
50+
agent = IndShockConsumerType()
51+
agent.solve()
52+
self.agent = agent
53+
54+
dict_A = {
55+
"height": 5.0,
56+
"width": 3.0,
57+
"depth": 2.0,
58+
}
59+
dict_B = dict_A.copy()
60+
dict_B["distance_criteria"] = ["height"]
61+
self.A = dict_A
62+
self.B = dict_B
63+
64+
def test_solution(self):
65+
self.agent.solution[0].describe_distance()
66+
out = self.agent.solution[0].describe_distance(display=False)
67+
self.agent.solution[0].describe_distance(max_depth=0)
68+
self.agent.solution[0].describe_distance(max_depth=1)
69+
self.agent.solution[0].describe_distance(max_depth=2)
70+
self.agent.solution[0].describe_distance(max_depth=3)
71+
self.agent.solution[0].describe_distance(max_depth=4)
72+
73+
def test_dictionary(self):
74+
describe_metric(self.A)
75+
describe_metric(self.B)
76+
77+
def test_null(self):
78+
X = MetricObject()
79+
X.describe_distance()

0 commit comments

Comments
 (0)