From e68079d57cda4d1ccfc7a88548f865dad416df3c Mon Sep 17 00:00:00 2001 From: sb Date: Mon, 20 May 2024 15:06:44 -0400 Subject: [PATCH 01/13] new model dynamics can be expressed as a math string; these get parsed into a function. see #1373 --- HARK/model.py | 6 ++++++ HARK/models/consumer.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HARK/model.py b/HARK/model.py index 46f695523..879b5c14e 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field from HARK.distribution import Distribution +from HARK.parser import math_text_to_lambda from typing import List @@ -50,6 +51,11 @@ class DBlock: dynamics: dict = field(default_factory=dict) reward: dict = field(default_factory=dict) + def __post_init__(self): + for v in self.dynamics: + if isinstance(self.dynamics[v], str): + self.dynamics[v] = math_text_to_lambda(self.dynamics[v]) + def get_shocks(self): return self.shocks diff --git a/HARK/models/consumer.py b/HARK/models/consumer.py index ede5184b2..9a4d2aa0b 100644 --- a/HARK/models/consumer.py +++ b/HARK/models/consumer.py @@ -53,7 +53,7 @@ "b": lambda k, R: k * R / PermGroFac, "m": lambda b, theta: b + theta, "c": Control(["m"]), - "a": lambda m, c: m - c, + "a": 'm - c' }, "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, } From 7e236420cede03c723828d1cb920721f76251502 Mon Sep 17 00:00:00 2001 From: sb Date: Mon, 20 May 2024 16:41:42 -0400 Subject: [PATCH 02/13] ruff fix --- HARK/models/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HARK/models/consumer.py b/HARK/models/consumer.py index 9a4d2aa0b..b9992de91 100644 --- a/HARK/models/consumer.py +++ b/HARK/models/consumer.py @@ -53,7 +53,7 @@ "b": lambda k, R: k * R / PermGroFac, "m": lambda b, theta: b + theta, "c": Control(["m"]), - "a": 'm - c' + "a": "m - c", }, "reward": {"u": lambda c, CRRA: c ** (1 - CRRA) / (1 - CRRA)}, } From ba35d5af740044e143fa26e10ed57a46ebbf0d28 Mon Sep 17 00:00:00 2001 From: sb Date: Mon, 20 May 2024 16:43:06 -0400 Subject: [PATCH 03/13] adding the parser module --- HARK/parser.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 HARK/parser.py diff --git a/HARK/parser.py b/HARK/parser.py new file mode 100644 index 000000000..22072c470 --- /dev/null +++ b/HARK/parser.py @@ -0,0 +1,28 @@ +from sympy.utilities.lambdify import lambdify +from sympy.parsing.sympy_parser import parse_expr + +class Expression(): + + def __init__(self, text): + self.txt + self.expr = parse_expr(text) + self.npf = self.func() + + # first derivatives. + self.grad = { + sym.__str__() : + self.expr.diff(sym) + for sym + in list(self.expr.free_symbols) + } + + def func(self): + return lambdify(list(self.expr.free_symbols), self.expr, "numpy") + +def math_text_to_lambda(text): + """ + Returns a function represented by the given mathematical text. + """ + expr = parse_expr(text) + func = lambdify(list(expr.free_symbols), expr, "numpy") + return func \ No newline at end of file From 217108499b3a58435f02e2b71cfd7d0f79fbf04f Mon Sep 17 00:00:00 2001 From: sb Date: Mon, 20 May 2024 16:50:59 -0400 Subject: [PATCH 04/13] ruff fix --- HARK/parser.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/HARK/parser.py b/HARK/parser.py index 22072c470..61fcba539 100644 --- a/HARK/parser.py +++ b/HARK/parser.py @@ -1,8 +1,8 @@ from sympy.utilities.lambdify import lambdify -from sympy.parsing.sympy_parser import parse_expr +from sympy.parsing.sympy_parser import parse_expr -class Expression(): +class Expression: def __init__(self, text): self.txt self.expr = parse_expr(text) @@ -10,19 +10,17 @@ def __init__(self, text): # first derivatives. self.grad = { - sym.__str__() : - self.expr.diff(sym) - for sym - in list(self.expr.free_symbols) + sym.__str__(): self.expr.diff(sym) for sym in list(self.expr.free_symbols) } def func(self): return lambdify(list(self.expr.free_symbols), self.expr, "numpy") + def math_text_to_lambda(text): """ Returns a function represented by the given mathematical text. """ expr = parse_expr(text) func = lambdify(list(expr.free_symbols), expr, "numpy") - return func \ No newline at end of file + return func From 3e0b93904e67fd6039219e2c7ffea90bc08b969f Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 18 Jun 2024 16:25:47 -0400 Subject: [PATCH 05/13] adds a method to DBlock that returns a discretized version of the block --- HARK/model.py | 21 ++++++++++++++++++++- HARK/tests/test_model.py | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/HARK/model.py b/HARK/model.py index c97a2ad76..99dd7b55d 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -2,7 +2,8 @@ Tools for crafting models. """ -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace +from copy import deepcopy from HARK.distribution import ( Distribution, DiscreteDistributionLabeled, @@ -152,6 +153,24 @@ class DBlock: dynamics: dict = field(default_factory=dict) reward: dict = field(default_factory=dict) + def discretize(self, disc_params): + """ + Returns a new DBlock which is a copy of this one, but with shock discretized. + """ + + disc_shocks = {} + + for shockn in self.shocks: + if shockn in disc_params: + disc_shocks[shockn] = self.shocks[shockn].discretize(**disc_params[shockn]) + else: + disc_shocks[shockn] = deepcopy(self.shocks[shockn]) + + # replace returns a modified copy + new_dblock = replace(self, shocks = disc_shocks) + + return new_dblock + def get_shocks(self): return self.shocks diff --git a/HARK/tests/test_model.py b/HARK/tests/test_model.py index f5b2db434..a5c4feff7 100644 --- a/HARK/tests/test_model.py +++ b/HARK/tests/test_model.py @@ -44,6 +44,11 @@ def setUp(self): def test_init(self): self.assertEqual(self.test_block_A.name, "test block A") + def test_discretize(self): + dbl = self.cblock.discretize({"theta" : {"N" : 5}}) + + self.assertEqual(len(dbl.shocks["theta"].pmv), 5) + def test_transition(self): post = self.cblock.transition(self.dpre, self.dr) From 5e2d74f4e9120dc2e2dc6d7718c119a3d78b48be Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 18 Jun 2024 16:40:27 -0400 Subject: [PATCH 06/13] discretize() method for RBlock --- HARK/model.py | 36 ++++++++++++++++++++++++++++++------ HARK/tests/test_model.py | 10 +++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index 99dd7b55d..122d75174 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass, field, replace -from copy import deepcopy +from copy import copy, deepcopy from HARK.distribution import ( Distribution, DiscreteDistributionLabeled, @@ -135,8 +135,12 @@ def simulate_dynamics( return vals +class Block: + pass + + @dataclass -class DBlock: +class DBlock(Block): """ Represents a 'block' of model behavior. It prioritizes a representation of the dynamics of the block. @@ -162,12 +166,14 @@ def discretize(self, disc_params): for shockn in self.shocks: if shockn in disc_params: - disc_shocks[shockn] = self.shocks[shockn].discretize(**disc_params[shockn]) + disc_shocks[shockn] = self.shocks[shockn].discretize( + **disc_params[shockn] + ) else: disc_shocks[shockn] = deepcopy(self.shocks[shockn]) # replace returns a modified copy - new_dblock = replace(self, shocks = disc_shocks) + new_dblock = replace(self, shocks=disc_shocks) return new_dblock @@ -265,7 +271,7 @@ def mod_dvf(shock_value_array): @dataclass -class RBlock: +class RBlock(Block): """ A recursive block. @@ -276,7 +282,25 @@ class RBlock: name: str = "" description: str = "" - blocks: List[DBlock] = field(default_factory=list) + blocks: List[Block] = field(default_factory=list) + + def discretize(self, disc_params): + """ + Recursively discretizes all the blocks. + It replaces any DBlocks with new blocks with discretized shocks. + """ + # we will be mutating self.blocks so need to iterate through a copy + ib = copy(self.blocks) + + for i, b in enumerate(ib): + if isinstance(b, DBlock): + nb = b.discretize(disc_params) + self.blocks[i] = nb + elif isinstance(b, RBlock): + b.discretize(disc_params) + + # returns the rblock, which is modified, to align the type signatures across subclasses + return self def get_shocks(self): ### TODO: Bug in here is causing AttributeError: 'set' object has no attribute 'draw' diff --git a/HARK/tests/test_model.py b/HARK/tests/test_model.py index a5c4feff7..617fadc4e 100644 --- a/HARK/tests/test_model.py +++ b/HARK/tests/test_model.py @@ -45,7 +45,7 @@ def test_init(self): self.assertEqual(self.test_block_A.name, "test block A") def test_discretize(self): - dbl = self.cblock.discretize({"theta" : {"N" : 5}}) + dbl = self.cblock.discretize({"theta": {"N": 5}}) self.assertEqual(len(dbl.shocks["theta"].pmv), 5) @@ -84,6 +84,8 @@ def setUp(self): self.test_block_C = model.DBlock(**test_block_C_data) self.test_block_D = model.DBlock(**test_block_D_data) + self.cpp = cons.cons_portfolio_problem + def test_init(self): r_block_tree = model.RBlock( blocks=[ @@ -94,3 +96,9 @@ def test_init(self): r_block_tree.get_shocks() self.assertEqual(len(r_block_tree.get_shocks()), 3) + + def test_discretize(self): + cppd = self.cpp.discretize({"theta": {"N": 5}, "risky_return": {"N": 6}}) + + self.assertEqual(len(cppd.get_shocks()["theta"].pmv), 5) + self.assertEqual(len(cppd.get_shocks()["risky_return"].pmv), 6) From cc19df885f44fb9442ec6b2f9e4d7c96e3d596ea Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 18 Jun 2024 16:50:14 -0400 Subject: [PATCH 07/13] change to make RBloc.discretize return copy not mutate original model --- HARK/model.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index 122d75174..b4ad7943a 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -289,18 +289,17 @@ def discretize(self, disc_params): Recursively discretizes all the blocks. It replaces any DBlocks with new blocks with discretized shocks. """ - # we will be mutating self.blocks so need to iterate through a copy - ib = copy(self.blocks) + cbs = copy(self.blocks) - for i, b in enumerate(ib): + for i, b in list(enumerate(cbs)): if isinstance(b, DBlock): nb = b.discretize(disc_params) - self.blocks[i] = nb + cbs[i] = nb elif isinstance(b, RBlock): b.discretize(disc_params) - # returns the rblock, which is modified, to align the type signatures across subclasses - return self + # returns a copy of the RBlock with the blocks replaced + return replace(self, blocks = cbs) def get_shocks(self): ### TODO: Bug in here is causing AttributeError: 'set' object has no attribute 'draw' From 63640e60b62eb3308f4c13e01260811d943f91b1 Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 18 Jun 2024 16:52:37 -0400 Subject: [PATCH 08/13] ruff --- HARK/model.py | 2 +- HARK/tests/test_model.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index b4ad7943a..9b86fab02 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -299,7 +299,7 @@ def discretize(self, disc_params): b.discretize(disc_params) # returns a copy of the RBlock with the blocks replaced - return replace(self, blocks = cbs) + return replace(self, blocks=cbs) def get_shocks(self): ### TODO: Bug in here is causing AttributeError: 'set' object has no attribute 'draw' diff --git a/HARK/tests/test_model.py b/HARK/tests/test_model.py index 617fadc4e..ad14fe27e 100644 --- a/HARK/tests/test_model.py +++ b/HARK/tests/test_model.py @@ -1,6 +1,6 @@ import unittest -from HARK.distribution import Bernoulli +from HARK.distribution import Bernoulli, DiscreteDistribution import HARK.model as model from HARK.model import Control import HARK.models.consumer as cons @@ -102,3 +102,7 @@ def test_discretize(self): self.assertEqual(len(cppd.get_shocks()["theta"].pmv), 5) self.assertEqual(len(cppd.get_shocks()["risky_return"].pmv), 6) + + self.assertFalse( + isinstance(self.cpp.get_shocks()["theta"], DiscreteDistribution) + ) From 96a2185f79a13b839b909fb66d4fcfde19b21c5c Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 18 Jun 2024 16:54:14 -0400 Subject: [PATCH 09/13] CHANGELOG #1460 --- Documentation/CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index ab9d67653..5c3ff22aa 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -8,6 +8,19 @@ For more information on HARK, see [our Github organization](https://github.com/e ## Changes +### 0.16.0 + +Release Date: TBD + +### Major Changes + +- Adds a discretize method to DBlocks and RBlocks (#1460)[https://github.com/econ-ark/HARK/pull/1460] + +### Minor Changes + +none + + ### 0.15.1 Release Date: June 15, 2024 From 75fdb070a6892e6d00e6e99b01ec5d2d17510676 Mon Sep 17 00:00:00 2001 From: sb Date: Sat, 22 Jun 2024 09:33:23 -0400 Subject: [PATCH 10/13] ruff --- HARK/model.py | 2 ++ HARK/tests/test_model.py | 1 + 2 files changed, 3 insertions(+) diff --git a/HARK/model.py b/HARK/model.py index 4cab86141..054ee5e19 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -14,6 +14,7 @@ from HARK.parser import math_text_to_lambda from typing import Any, Callable, Mapping, List, Union + class Aggregate: """ Used to designate a shock as an aggregate shock. @@ -249,6 +250,7 @@ def mod_dvf(shock_value_array): return arrival_value_function + @dataclass class RBlock: """ diff --git a/HARK/tests/test_model.py b/HARK/tests/test_model.py index 728749cf7..f5b2db434 100644 --- a/HARK/tests/test_model.py +++ b/HARK/tests/test_model.py @@ -72,6 +72,7 @@ def test_arrival_value_function(self): av({"k": 1, "R": 1.05, "PermGroFac": 1.1, "theta": 1, "CRRA": 2}) + class test_RBlock(unittest.TestCase): def setUp(self): self.test_block_B = model.DBlock(**test_block_B_data) From 9287eff50bb012613b5a042ec2d5769d6e54189d Mon Sep 17 00:00:00 2001 From: sb Date: Sat, 22 Jun 2024 09:36:41 -0400 Subject: [PATCH 11/13] update changelog for #1427 --- Documentation/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index 284a08933..49b860a50 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -8,13 +8,14 @@ For more information on HARK, see [our Github organization](https://github.com/e ## Changes -### 0.15.2 (in development) +### 0.16.0 (in development) Release Date: TBD #### Major Changes -none +- 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) #### Minor Changes From fd62ccbd55735f9efc9a8e54ddd7aee079327eb7 Mon Sep 17 00:00:00 2001 From: sb Date: Sat, 22 Jun 2024 09:50:44 -0400 Subject: [PATCH 12/13] adding back in PermGroFac into normalized b equation --- HARK/models/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HARK/models/consumer.py b/HARK/models/consumer.py index b9992de91..148c81457 100644 --- a/HARK/models/consumer.py +++ b/HARK/models/consumer.py @@ -50,7 +50,7 @@ "theta": MeanOneLogNormal(sigma=TranShkStd), }, "dynamics": { - "b": lambda k, R: k * R / PermGroFac, + "b": lambda k, R, PermGroFac: k * R / PermGroFac, "m": lambda b, theta: b + theta, "c": Control(["m"]), "a": "m - c", From a9b04072b865f0bc2b3400425479f89933f9a3bf Mon Sep 17 00:00:00 2001 From: sb Date: Sat, 22 Jun 2024 10:02:16 -0400 Subject: [PATCH 13/13] docstring for dynamics param to DBlock, mentioning string option --- HARK/model.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/HARK/model.py b/HARK/model.py index 054ee5e19..1816460fd 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -144,7 +144,16 @@ class DBlock: Parameters ---------- - ... + shocks: Mapping(str, Distribution) + A mapping from variable names to Distribution objects, + representing exogenous shocks. + + dynamics: Mapping(str, str or callable) + A dictionary mapping variable names to mathematical expressions. + These expressions can be simple functions, in which case the + argument names should match the variable inputs. + Or these can be strings, which are parsed into functions. + """ name: str = ""