From b4b2bd91c8882fc1c6d370a3c0b18409db54b5a6 Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 2 Jul 2024 16:58:51 -0400 Subject: [PATCH 1/7] consumer.yaml config file; yaml loader in HARK.parser; smoke test --- HARK/parser.py | 41 +++++++++++++++++++++++++++++++ HARK/tests/test_parser.py | 51 +++++++++++++++++++++++++++++++++++++++ requirements/base.txt | 1 + 3 files changed, 93 insertions(+) create mode 100644 HARK/tests/test_parser.py diff --git a/HARK/parser.py b/HARK/parser.py index 61fcba539..404c7b6f4 100644 --- a/HARK/parser.py +++ b/HARK/parser.py @@ -1,5 +1,16 @@ +from HARK.distribution import Bernoulli, Lognormal, MeanOneLogNormal from sympy.utilities.lambdify import lambdify from sympy.parsing.sympy_parser import parse_expr +import yaml + + +class ControlToken: + """ + Represents a parsed Control variable. + """ + + def __init__(self, args): + pass class Expression: @@ -17,6 +28,14 @@ def func(self): return lambdify(list(self.expr.free_symbols), self.expr, "numpy") +def tuple_constructor_from_class(cls): + def constructor(loader, node): + value = loader.construct_mapping(node) + return (cls, value) + + return constructor + + def math_text_to_lambda(text): """ Returns a function represented by the given mathematical text. @@ -24,3 +43,25 @@ def math_text_to_lambda(text): expr = parse_expr(text) func = lambdify(list(expr.free_symbols), expr, "numpy") return func + + +def harklang_loader(): + """Add constructors to PyYAML loader.""" + loader = yaml.SafeLoader + yaml.SafeLoader.add_constructor( + "!Bernoulli", tuple_constructor_from_class(Bernoulli) + ) + yaml.SafeLoader.add_constructor( + "!MeanOneLogNormal", tuple_constructor_from_class(MeanOneLogNormal) + ) + yaml.SafeLoader.add_constructor( + "!Lognormal", tuple_constructor_from_class(Lognormal) + ) + yaml.SafeLoader.add_constructor( + "!Control", tuple_constructor_from_class(ControlToken) + ) + + return loader + + +######################################################################## diff --git a/HARK/tests/test_parser.py b/HARK/tests/test_parser.py new file mode 100644 index 000000000..770b6e486 --- /dev/null +++ b/HARK/tests/test_parser.py @@ -0,0 +1,51 @@ +import os +import unittest + + +import HARK.parser as parser +import yaml + + +class test_consumption_parsing(unittest.TestCase): + def setUp(self): + this_file_path = os.path.dirname(__file__) + consumer_yaml_path = os.path.join(this_file_path, "../models/consumer.yaml") + + self.consumer_yaml_file = open(consumer_yaml_path, "r") + + def test_parse(self): + self.consumer_yaml_file + + config = yaml.load(self.consumer_yaml_file, Loader=parser.harklang_loader()) + pass + + """ + try: + config = yaml.load(open('perfect_foresight_full_experimental.yaml', 'r'), Loader= get_loader()) + + # data is copied + assert config['model']['blocks']['consumption'] == config['model']['strategies'][1]['block'] + # data is maintained once in memory and referenced in both places + assert config['model']['blocks']['consumption'] is config['model']['strategies'][1]['block'] + + # object created by parser + c1 = config['model']['blocks']['consumption']['dynamics']['c'] + c2 = config['model']['strategies'][1]['block']['dynamics']['c'] + + # objects are equal + assert c1 == c2 + # objects are identical in memory; the reference is shared. + assert c1 is c2 + + a_str = config['model']['blocks']['consumption']['dynamics']['a'] + a_expr = parse_expr(a_str) + a_func = lambdify(list(a_expr.free_symbols), a_expr, "numpy") + + m = np.random.random(100) + c = np.random.random(100) + + #import pdb; pdb.set_trace() + + except yaml.YAMLError as exc: + print("Error in configuration file:", exc) + """ diff --git a/requirements/base.txt b/requirements/base.txt index 05ac8eb76..7bcfe9f62 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -5,6 +5,7 @@ networkx>=3 numba<0.60.0 numpy>=1.23 pandas>=1.5 +pyyaml>=6.0 quantecon scipy>=1.10 seaborn>=0.12 From f52f327b8cba382bb3d20be6ba3efbc6fcca9b9d Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 2 Jul 2024 17:03:12 -0400 Subject: [PATCH 2/7] adding the consumer.yaml file --- HARK/models/consumer.yaml | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 HARK/models/consumer.yaml diff --git a/HARK/models/consumer.yaml b/HARK/models/consumer.yaml new file mode 100644 index 000000000..a36be8b29 --- /dev/null +++ b/HARK/models/consumer.yaml @@ -0,0 +1,60 @@ +# +# A YAML configuration file for the consumption portfolio problem blocks. +# + +calibration: + DiscFac: 0.96 + CRRA: 2.0 + R: 1.03 # can be overriden by portfolio dynamics + Rfree: 1.03 + EqP: 0.02 + LivPrb: 0.98 + PermGroFac: 1.01 + BoroCnstArt: None + TranShkStd: 0.1 + RiskyStd: 0.1 + +blocks: + - &cons_normalized + name: consumption normalized + shocks: + live: !Bernoulli + p: LivPrb + theta: !MeanOneLogNormal + sigma: TranShkStd + dynamics: + b: k * R / PermGroFac + m: b + theta + c: !Control + info: m + a: m - c + + reward: + u: c ** (1 - CRRA) / (1 - CRRA) + + +#portfolio_block = DBlock( +# **{ +# "name": "portfolio", +# "shocks": { +# "risky_return": (Lognormal, {"mean": "Rfree + EqP", "std": "RiskyStd"}) +# }, +# "dynamics": { +# "stigma": Control(["a"]), +# "R": lambda stigma, Rfree, risky_return: Rfree +# + (risky_return - Rfree) * stigma, +# }, +# } +#) + +#tick_block = DBlock( +# **{ +# "name": "tick", +# "dynamics": { +# "k": lambda a: a, +# }, +# } +#) + +#cons_problem = RBlock(blocks=[consumption_block, tick_block]) +#cons_portfolio_problem = RBlock(blocks=[consumption_block, portfolio_block, tick_block]) From 2beb4c69ac82eb6779917270d3a207adb05926d2 Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 2 Jul 2024 17:03:35 -0400 Subject: [PATCH 3/7] ruff fixes --- HARK/models/consumer.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HARK/models/consumer.yaml b/HARK/models/consumer.yaml index a36be8b29..c09075992 100644 --- a/HARK/models/consumer.yaml +++ b/HARK/models/consumer.yaml @@ -29,7 +29,7 @@ blocks: info: m a: m - c - reward: + reward: u: c ** (1 - CRRA) / (1 - CRRA) From 2db65276463175478d68cdda2e69151788ad76d3 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 3 Jul 2024 09:47:55 -0400 Subject: [PATCH 4/7] passing tests for parsing block configuration and building working DBlock, normalized consumption --- HARK/model.py | 4 ++++ HARK/tests/test_parser.py | 39 ++++++++------------------------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index 1fdf35bb4..8eb256124 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -260,6 +260,10 @@ def __post_init__(self): if isinstance(self.dynamics[v], str): self.dynamics[v] = math_text_to_lambda(self.dynamics[v]) + for r in self.reward: + if isinstance(self.reward[r], str): + self.reward[r] = math_text_to_lambda(self.reward[r]) + def get_shocks(self): return self.shocks diff --git a/HARK/tests/test_parser.py b/HARK/tests/test_parser.py index 770b6e486..d662dcb3a 100644 --- a/HARK/tests/test_parser.py +++ b/HARK/tests/test_parser.py @@ -1,7 +1,7 @@ import os import unittest - +import HARK.model as model import HARK.parser as parser import yaml @@ -17,35 +17,12 @@ def test_parse(self): self.consumer_yaml_file config = yaml.load(self.consumer_yaml_file, Loader=parser.harklang_loader()) - pass - - """ - try: - config = yaml.load(open('perfect_foresight_full_experimental.yaml', 'r'), Loader= get_loader()) - - # data is copied - assert config['model']['blocks']['consumption'] == config['model']['strategies'][1]['block'] - # data is maintained once in memory and referenced in both places - assert config['model']['blocks']['consumption'] is config['model']['strategies'][1]['block'] - - # object created by parser - c1 = config['model']['blocks']['consumption']['dynamics']['c'] - c2 = config['model']['strategies'][1]['block']['dynamics']['c'] - - # objects are equal - assert c1 == c2 - # objects are identical in memory; the reference is shared. - assert c1 is c2 - - a_str = config['model']['blocks']['consumption']['dynamics']['a'] - a_expr = parse_expr(a_str) - a_func = lambdify(list(a_expr.free_symbols), a_expr, "numpy") - - m = np.random.random(100) - c = np.random.random(100) - #import pdb; pdb.set_trace() + self.assertEqual(config['calibration']['DiscFac'], 0.96) + self.assertEqual(config['blocks'][0]['name'], 'consumption normalized') - except yaml.YAMLError as exc: - print("Error in configuration file:", exc) - """ + ## construct and test the block + cons_norm_block = model.DBlock(**config['blocks'][0]) + cons_norm_block.construct_shocks(config['calibration']) + cons_norm_block.discretize({"theta": {"N": 5}}) + self.assertEqual(cons_norm_block.calc_reward({"c": 1, "CRRA": 2})["u"], -1.0) From 2a09a2760b330fbe6e24d946588fe9b528aa925b Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 3 Jul 2024 09:54:04 -0400 Subject: [PATCH 5/7] adding YAML config for portfolio block and tests --- HARK/models/consumer.yaml | 37 ++++++++++--------------------------- HARK/tests/test_parser.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/HARK/models/consumer.yaml b/HARK/models/consumer.yaml index c09075992..a7a315e45 100644 --- a/HARK/models/consumer.yaml +++ b/HARK/models/consumer.yaml @@ -31,30 +31,13 @@ blocks: reward: u: c ** (1 - CRRA) / (1 - CRRA) - - -#portfolio_block = DBlock( -# **{ -# "name": "portfolio", -# "shocks": { -# "risky_return": (Lognormal, {"mean": "Rfree + EqP", "std": "RiskyStd"}) -# }, -# "dynamics": { -# "stigma": Control(["a"]), -# "R": lambda stigma, Rfree, risky_return: Rfree -# + (risky_return - Rfree) * stigma, -# }, -# } -#) - -#tick_block = DBlock( -# **{ -# "name": "tick", -# "dynamics": { -# "k": lambda a: a, -# }, -# } -#) - -#cons_problem = RBlock(blocks=[consumption_block, tick_block]) -#cons_portfolio_problem = RBlock(blocks=[consumption_block, portfolio_block, tick_block]) + - &portfolio_choice + name: portfolio choice + shocks: + risky_return: !Lognormal + mean: Rfree + EqP + std: RiskyStd + dynamics: + stigma: !Control + info: a + R: Rfree + (risky_return - Rfree) * stigma diff --git a/HARK/tests/test_parser.py b/HARK/tests/test_parser.py index d662dcb3a..0a49e4171 100644 --- a/HARK/tests/test_parser.py +++ b/HARK/tests/test_parser.py @@ -18,11 +18,16 @@ def test_parse(self): config = yaml.load(self.consumer_yaml_file, Loader=parser.harklang_loader()) - self.assertEqual(config['calibration']['DiscFac'], 0.96) - self.assertEqual(config['blocks'][0]['name'], 'consumption normalized') + self.assertEqual(config["calibration"]["DiscFac"], 0.96) + self.assertEqual(config["blocks"][0]["name"], "consumption normalized") - ## construct and test the block - cons_norm_block = model.DBlock(**config['blocks'][0]) - cons_norm_block.construct_shocks(config['calibration']) + ## construct and test the consumption block + cons_norm_block = model.DBlock(**config["blocks"][0]) + cons_norm_block.construct_shocks(config["calibration"]) cons_norm_block.discretize({"theta": {"N": 5}}) self.assertEqual(cons_norm_block.calc_reward({"c": 1, "CRRA": 2})["u"], -1.0) + + ## construct and test the portfolio block + portfolio_block = model.DBlock(**config["blocks"][1]) + portfolio_block.construct_shocks(config["calibration"]) + portfolio_block.discretize({"risky_return": {"N": 5}}) From 4f0f0e3e8c7988662239dccec80c5eca110ee8d7 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 3 Jul 2024 10:04:15 -0400 Subject: [PATCH 6/7] docs, changelog for #1465 --- Documentation/CHANGELOG.md | 1 + HARK/parser.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index a9d7f959f..a25858258 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -18,6 +18,7 @@ Release Date: TBD - Allows structural equations in model files to be provided in string form [#1427](https://github.com/econ-ark/HARK/pull/1427) - Introduces `HARK.parser' module for parsing configuration files into models [#1427](https://github.com/econ-ark/HARK/pull/1427) - Allows construction of shocks with arguments based on mathematical expressions [#1464](https://github.com/econ-ark/HARK/pull/1464) +- YAML configuration file for the normalized consumption and portolio choice [#1465](https://github.com/econ-ark/HARK/pull/1465) #### Minor Changes diff --git a/HARK/parser.py b/HARK/parser.py index 404c7b6f4..3f21ff7f9 100644 --- a/HARK/parser.py +++ b/HARK/parser.py @@ -46,7 +46,10 @@ def math_text_to_lambda(text): def harklang_loader(): - """Add constructors to PyYAML loader.""" + """ + A PyYAML loader that supports tags for HARKLang, + such as random variables and model tags. + """ loader = yaml.SafeLoader yaml.SafeLoader.add_constructor( "!Bernoulli", tuple_constructor_from_class(Bernoulli) @@ -62,6 +65,3 @@ def harklang_loader(): ) return loader - - -######################################################################## From feacec7c3f53c0502e1765f04fbf2a47a40e4b45 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 3 Jul 2024 11:59:13 -0400 Subject: [PATCH 7/7] removing stray print statements --- HARK/model.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index 8eb256124..d20e3341a 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -122,9 +122,6 @@ def construct_shocks(shock_data, scope): dist_args[a] = arg_value - print(v) - print(dist_class) - print(dist_args) dist = dist_class(**dist_args) sd[v] = dist