From 7b165cf6965e8ee868925bbed1b19de584af0455 Mon Sep 17 00:00:00 2001 From: sb Date: Tue, 27 Jun 2023 18:20:40 -0400 Subject: [PATCH 01/11] trying a way to include explicit equations in HARK models. See #479 #889 --- HARK/ConsumptionSaving/ConsIndShockModel.py | 11 +++++++++-- HARK/core.py | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/HARK/ConsumptionSaving/ConsIndShockModel.py b/HARK/ConsumptionSaving/ConsIndShockModel.py index 9d0c4e0bc..73e4b38c7 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModel.py +++ b/HARK/ConsumptionSaving/ConsIndShockModel.py @@ -1894,7 +1894,7 @@ def get_poststates(self): None """ # should this be "Now", or "Prev"?!? - self.state_now["aNrm"] = self.state_now["mNrm"] - self.controls["cNrm"] + self.state_now["aNrm"] = self.equations['aNrm'](self.state_now, self.controls) # Useful in some cases to precalculate asset level self.state_now["aLvl"] = self.state_now["aNrm"] * self.state_now["pLvl"] @@ -2103,6 +2103,11 @@ def check_conditions(self, verbose=None): } ) +equations = { + 'aNrm' : lambda x, a: x['mNrm'] - a['cNrm'] , + 'thorn' : lambda p : (p['Rfree'] * p['DiscFac']) ** (1 / p['CRRA']) +} + class IndShockConsumerType(PerfForesightConsumerType): """ @@ -2144,6 +2149,8 @@ def __init__(self, verbose=1, quiet=False, **kwds): self.update() # Make assets grid, income process, terminal solution + self.equations.update(equations) + def update_income_process(self): """ Updates this agent's income process based on his own attributes. @@ -3225,7 +3232,7 @@ def check_conditions(self, verbose=None): self.PermGroFac[0] * self.InvEx_PermShkInv ) # [url]/#PGroAdj - self.thorn = (self.Rfree * self.DiscFac) ** (1 / self.CRRA) + self.thorn = self.equations['thorn'](self.parameters) # self.Ex_RNrm = self.Rfree*Ex_PermShkInv/(self.PermGroFac[0]*self.LivPrb[0]) self.GPFRaw = self.thorn / (self.PermGroFac[0]) # [url]/#GPF diff --git a/HARK/core.py b/HARK/core.py index 3149ebf0d..c2104e22c 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -72,9 +72,12 @@ def __eq__(self, other): return NotImplemented - def __init__(self): + def __init__(self, equations = {}, parameters = {}, options = {}): + + self.equations = equations + self.options = options if not hasattr(self, "parameters"): - self.parameters = {} + self.parameters = parameters def __str__(self): type_ = type(self) From d09eb303ebc01d85cd8e5c00d875b4384830a6ee Mon Sep 17 00:00:00 2001 From: sb Date: Fri, 30 Jun 2023 15:08:27 -0400 Subject: [PATCH 02/11] more IndShockConsumerType 'dynamics'. Very lightweight functions --- HARK/ConsumptionSaving/ConsIndShockModel.py | 21 +++++++++++++-------- HARK/model.py | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 HARK/model.py diff --git a/HARK/ConsumptionSaving/ConsIndShockModel.py b/HARK/ConsumptionSaving/ConsIndShockModel.py index 73e4b38c7..90cc33600 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModel.py +++ b/HARK/ConsumptionSaving/ConsIndShockModel.py @@ -52,6 +52,7 @@ MargValueFuncCRRA, ValueFuncCRRA, ) +from HARK.model import Control from HARK.metric import MetricObject from HARK.rewards import ( CRRAutility, @@ -1850,9 +1851,9 @@ def transition(self): PlvlAggNow = self.state_prev["PlvlAgg"] * self.PermShkAggNow # "Effective" interest factor on normalized assets ReffNow = RfreeNow / self.shocks["PermShk"] - bNrmNow = ReffNow * aNrmPrev # Bank balances before labor income + bNrmNow = self.equations['bNrm'](ReffNow, aNrmPrev) # Bank balances before labor income # Market resources after income - mNrmNow = bNrmNow + self.shocks["TranShk"] + mNrmNow = self.equations['mNrm'](bNrmNow, self.shocks["TranShk"]) return pLvlNow, PlvlAggNow, bNrmNow, mNrmNow, None @@ -1894,7 +1895,7 @@ def get_poststates(self): None """ # should this be "Now", or "Prev"?!? - self.state_now["aNrm"] = self.equations['aNrm'](self.state_now, self.controls) + self.state_now["aNrm"] = self.equations['aNrm'](self.state_now['mNrm'], self.controls['cNrm']) # Useful in some cases to precalculate asset level self.state_now["aLvl"] = self.state_now["aNrm"] * self.state_now["pLvl"] @@ -2103,9 +2104,13 @@ def check_conditions(self, verbose=None): } ) -equations = { - 'aNrm' : lambda x, a: x['mNrm'] - a['cNrm'] , - 'thorn' : lambda p : (p['Rfree'] * p['DiscFac']) ** (1 / p['CRRA']) +dynamics = { + 'G' : lambda gamma, psi : gamma * psi, + 'Rnrm' : lambda R, G : R / G, + 'bNrm' : lambda Rnrm, aNrm : Rnrm * aNrm, + 'mNrm' : lambda bNrm, theta : bNrm + theta, + 'cNrm' : Control(['mNrm']), + 'aNrm' : lambda mNrm, cNrm : mNrm - cNrm } @@ -2149,7 +2154,7 @@ def __init__(self, verbose=1, quiet=False, **kwds): self.update() # Make assets grid, income process, terminal solution - self.equations.update(equations) + self.equations.update(dynamics) def update_income_process(self): """ @@ -3232,7 +3237,7 @@ def check_conditions(self, verbose=None): self.PermGroFac[0] * self.InvEx_PermShkInv ) # [url]/#PGroAdj - self.thorn = self.equations['thorn'](self.parameters) + self.thorn = (self.parameters['Rfree'] * self.parameters['DiscFac']) ** (1 / self.parameters['CRRA']) # self.Ex_RNrm = self.Rfree*Ex_PermShkInv/(self.PermGroFac[0]*self.LivPrb[0]) self.GPFRaw = self.thorn / (self.PermGroFac[0]) # [url]/#GPF diff --git a/HARK/model.py b/HARK/model.py new file mode 100644 index 000000000..4eb26d73e --- /dev/null +++ b/HARK/model.py @@ -0,0 +1,6 @@ + + +class Control(): + + def __init__(self, policy_args): + self.policy_args = policy_args From 4e1f4f09e7e384307554c37133fd25bb4876dc27 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 5 Jul 2023 12:23:19 -0400 Subject: [PATCH 03/11] ConsAggShock to call super() init instead of AgentType --- HARK/ConsumptionSaving/ConsAggShockModel.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/HARK/ConsumptionSaving/ConsAggShockModel.py b/HARK/ConsumptionSaving/ConsAggShockModel.py index 89a4f9ae0..b1c024a42 100644 --- a/HARK/ConsumptionSaving/ConsAggShockModel.py +++ b/HARK/ConsumptionSaving/ConsAggShockModel.py @@ -140,12 +140,7 @@ def __init__(self, **kwds): params = init_agg_shocks.copy() params.update(kwds) - AgentType.__init__( - self, - solution_terminal=deepcopy(IndShockConsumerType.solution_terminal_), - pseudo_terminal=False, - **params - ) + super().__init__( **params) # Add consumer-type specific objects, copying to create independent versions self.time_vary = deepcopy(IndShockConsumerType.time_vary_) From e6db4ef2a443c8767362e91f36c6745ad9b37de6 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 12 Jul 2023 10:29:35 -0400 Subject: [PATCH 04/11] new parameters input more consistent with past tests --- HARK/core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index c2104e22c..eb5d51a9c 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -72,12 +72,15 @@ def __eq__(self, other): return NotImplemented - def __init__(self, equations = {}, parameters = {}, options = {}): + def __init__(self, equations = {}, parameters = None, options = {}): self.equations = equations self.options = options if not hasattr(self, "parameters"): - self.parameters = parameters + self.parameters = {} + if parameters is not None: + self.assign_parameters(**parameters) + def __str__(self): type_ = type(self) From 1a6a0dca8b7cbdb0c60845ba949cd341522c6ab7 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 12 Jul 2023 11:27:48 -0400 Subject: [PATCH 05/11] split PF and IndShock dynamics; load PF dynamics in PF AgentType; fix KinkedR inheritance --- HARK/ConsumptionSaving/ConsIndShockModel.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/HARK/ConsumptionSaving/ConsIndShockModel.py b/HARK/ConsumptionSaving/ConsIndShockModel.py index 90cc33600..366036911 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModel.py +++ b/HARK/ConsumptionSaving/ConsIndShockModel.py @@ -1559,6 +1559,14 @@ def prepare_to_calc_EndOfPrdvP(self): # Do Perfect Foresight MIT Shock: Forces Newborns to follow solution path of the agent he/she replaced when True } +PerfForesightConsumerType_dynamics = { + # need dynamic equation for Rnrm here. It's going to get overwritten in downstream models + 'bNrm' : lambda Rnrm, aNrm : Rnrm * aNrm, + 'mNrm' : lambda bNrm, theta : bNrm + theta, + 'cNrm' : Control(['mNrm']), + 'aNrm' : lambda mNrm, cNrm : mNrm - cNrm +} + class PerfForesightConsumerType(AgentType): """ @@ -1612,6 +1620,7 @@ def __init__(self, verbose=1, quiet=False, **kwds): set_verbosity_level((4 - verbose) * 10) self.update_Rfree() # update interest rate if time varying + self.equations.update(PerfForesightConsumerType_dynamics) def pre_solve(self): self.update_solution_terminal() # Solve the terminal period problem @@ -2104,7 +2113,8 @@ def check_conditions(self, verbose=None): } ) -dynamics = { +IndShockConsumerType_dynamics = { + **PerfForesightConsumerType_dynamics, 'G' : lambda gamma, psi : gamma * psi, 'Rnrm' : lambda R, G : R / G, 'bNrm' : lambda Rnrm, aNrm : Rnrm * aNrm, @@ -2113,7 +2123,6 @@ def check_conditions(self, verbose=None): 'aNrm' : lambda mNrm, cNrm : mNrm - cNrm } - class IndShockConsumerType(PerfForesightConsumerType): """ A consumer type with idiosyncratic shocks to permanent and transitory income. @@ -2154,7 +2163,7 @@ def __init__(self, verbose=1, quiet=False, **kwds): self.update() # Make assets grid, income process, terminal solution - self.equations.update(dynamics) + self.equations.update(IndShockConsumerType_dynamics) def update_income_process(self): """ @@ -3628,7 +3637,7 @@ def __init__(self, **kwds): params.update(kwds) # Initialize a basic AgentType - PerfForesightConsumerType.__init__(self, **params) + super().__init__(**params) # Add consumer-type specific objects, copying to create independent versions self.solve_one_period = make_one_period_oo_solver(ConsKinkedRsolver) From b9d6fb652292bf780548a31913e7486642377ded Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 12 Jul 2023 11:28:41 -0400 Subject: [PATCH 06/11] moving Model to model.py --- HARK/core.py | 75 +----------------------------------------------- HARK/model.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 75 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index eb5d51a9c..7f8b43b99 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -23,83 +23,10 @@ TimeVaryingDiscreteDistribution, combine_indep_dstns, ) +from HARK.model import Model from HARK.parallel import multi_thread_commands, multi_thread_commands_fake from HARK.utilities import NullFunc, get_arg_names - -class Model: - """ - A class with special handling of parameters assignment. - """ - - def assign_parameters(self, **kwds): - """ - Assign an arbitrary number of attributes to this agent. - - Parameters - ---------- - **kwds : keyword arguments - Any number of keyword arguments of the form key=value. Each value - will be assigned to the attribute named in self. - - Returns - ------- - none - """ - self.parameters.update(kwds) - for key in kwds: - setattr(self, key, kwds[key]) - - def get_parameter(self, name): - """ - Returns a parameter of this model - - Parameters - ---------- - name : string - The name of the parameter to get - - Returns - ------- - value : - The value of the parameter - """ - return self.parameters[name] - - def __eq__(self, other): - if isinstance(other, type(self)): - return self.parameters == other.parameters - - return NotImplemented - - def __init__(self, equations = {}, parameters = None, options = {}): - - self.equations = equations - self.options = options - if not hasattr(self, "parameters"): - self.parameters = {} - if parameters is not None: - self.assign_parameters(**parameters) - - - def __str__(self): - type_ = type(self) - module = type_.__module__ - qualname = type_.__qualname__ - - s = f"<{module}.{qualname} object at {hex(id(self))}.\n" - s += "Parameters:" - - for p in self.parameters: - s += f"\n{p}: {self.parameters[p]}" - - s += ">" - return s - - def __repr__(self): - return self.__str__() - - class AgentType(Model): """ A superclass for economic agents in the HARK framework. Each model should diff --git a/HARK/model.py b/HARK/model.py index 4eb26d73e..cf695aae5 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -1,6 +1,83 @@ - +""" +Models in the abstract. +""" class Control(): + """ + A class used to indicate that a variable is a control variable. + """ def __init__(self, policy_args): self.policy_args = policy_args + +class Model: + """ + A class with special handling of parameters assignment. + """ + + def assign_parameters(self, **kwds): + """ + Assign an arbitrary number of attributes to this agent. + + Parameters + ---------- + **kwds : keyword arguments + Any number of keyword arguments of the form key=value. Each value + will be assigned to the attribute named in self. + + Returns + ------- + none + """ + self.parameters.update(kwds) + for key in kwds: + setattr(self, key, kwds[key]) + + def get_parameter(self, name): + """ + Returns a parameter of this model + + Parameters + ---------- + name : string + The name of the parameter to get + + Returns + ------- + value : + The value of the parameter + """ + return self.parameters[name] + + def __eq__(self, other): + if isinstance(other, type(self)): + return self.parameters == other.parameters + + return NotImplemented + + def __init__(self, equations = {}, parameters = None, options = {}): + + self.equations = equations + self.options = options + if not hasattr(self, "parameters"): + self.parameters = {} + if parameters is not None: + self.assign_parameters(**parameters) + + + def __str__(self): + type_ = type(self) + module = type_.__module__ + qualname = type_.__qualname__ + + s = f"<{module}.{qualname} object at {hex(id(self))}.\n" + s += "Parameters:" + + for p in self.parameters: + s += f"\n{p}: {self.parameters[p]}" + + s += ">" + return s + + def __repr__(self): + return self.__str__() From 4b0c3ed350016c7244aee7a1e5c294e49ca125e3 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 19 Jul 2023 10:37:41 -0400 Subject: [PATCH 07/11] changelog update for equations in models, PR #1292 --- Documentation/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index b867ec8dc..f51316b5e 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -14,6 +14,7 @@ Release Date: TBD ### Major Changes +- Moves `HARK.core.Model` to new `HARK.model` submodule, and allow `Model` object to be configured with equations, which are dictionaries of callables that take model variables as arguments. [#1292](https://github.com/econ-ark/HARK/pull/1292) - Adds `HARK.core.AgentPopulation` class to represent a population of agents with ex-ante heterogeneous parametrizations as distributions. [#1237](https://github.com/econ-ark/HARK/pull/1237) ### Minor Changes From 32c7cf32ff9eb9abf0876182cb35925215c93466 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 19 Jul 2023 11:34:53 -0400 Subject: [PATCH 08/11] documentatin and automated tests for equations in models --- HARK/model.py | 38 +++++++++++++++++++++++++++----- HARK/tests/test_model.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 HARK/tests/test_model.py diff --git a/HARK/model.py b/HARK/model.py index cf695aae5..9c224ef5b 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -1,18 +1,41 @@ """ Models in the abstract. """ +from collections.abc import Callable, Mapping, Sequence +from typing import Any, Optional class Control(): """ A class used to indicate that a variable is a control variable. + + Parameters + ----------- + policy_args : Sequence[str] + A sequence of variable names, which refer to the values that are the arguments + to decision rules for this control variable. + """ - def __init__(self, policy_args): + def __init__( + self, + policy_args : Sequence[str] + ): self.policy_args = policy_args class Model: """ - A class with special handling of parameters assignment. + An economic model. + This object should contain the information about an environment's dynamics. + + Parameters + ---------- + equations : Mapping(str, Union[Callable, Control]) + A mapping from model variable names (as strings) to transition equations governing + these variables. + parameters : Optional[Mapping(str, Any)] + A mapping from parameters names (strings) to parameter values. + options : Mapping(str, Any) + A mapping from options (str) to option values. """ def assign_parameters(self, **kwds): @@ -33,7 +56,7 @@ def assign_parameters(self, **kwds): for key in kwds: setattr(self, key, kwds[key]) - def get_parameter(self, name): + def get_parameter(self, name: str): """ Returns a parameter of this model @@ -51,11 +74,16 @@ def get_parameter(self, name): def __eq__(self, other): if isinstance(other, type(self)): - return self.parameters == other.parameters + return (self.parameters == other.parameters) and (self.equations == other.equations) return NotImplemented - def __init__(self, equations = {}, parameters = None, options = {}): + def __init__( + self, + equations : Mapping[str, Union[Callable, Control]] = {}, + parameters : Optional[Mapping[str, Any]] = None, + options : Mapping[str, Any] = {} + ): self.equations = equations self.options = options diff --git a/HARK/tests/test_model.py b/HARK/tests/test_model.py new file mode 100644 index 000000000..39ae8d8a8 --- /dev/null +++ b/HARK/tests/test_model.py @@ -0,0 +1,47 @@ +from HARK.model import Control, Model +import unittest + + +class test_Model(unittest.TestCase): + def setUp(self): + + equations_a = { + 'm' : lambda a, r : a * r, + 'c' : Control(['m']), + 'a' : lambda m, c : m - c + } + + parameters_a = { + 'r' : 1.02 + } + + parameters_b = { + 'r' : 1.03 + } + + equations_c = { + 'm' : lambda a, r : a * r, + 'c' : Control(['m']), + 'a' : lambda m, c : m - 2 * c + } + + # similar test to distance_metric + self.model_a = Model( + equations = equations_a, + parameters = parameters_a + ) + + self.model_b = Model( + equations = equations_a, + parameters = parameters_b + ) + + self.model_c = Model( + equations = equations_c, + parameters = parameters_a + ) + + def test_eq(self): + self.assertEqual(self.model_a, self.model_a) + self.assertNotEqual(self.model_a, self.model_b) + self.assertNotEqual(self.model_a, self.model_c) \ No newline at end of file From d7296f75d2f1a10f4017522ab792cef5bae59017 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 19 Jul 2023 11:43:35 -0400 Subject: [PATCH 09/11] adding docs to RST autodocs --- Documentation/reference/index.rst | 1 + HARK/model.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/reference/index.rst b/Documentation/reference/index.rst index 1d9cced2f..de65cad62 100644 --- a/Documentation/reference/index.rst +++ b/Documentation/reference/index.rst @@ -13,6 +13,7 @@ API Reference tools/frame tools/helpers tools/interpolation + tools/model tools/numba_tools tools/parallel tools/rewards diff --git a/HARK/model.py b/HARK/model.py index 9c224ef5b..d1ebc68db 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -2,7 +2,7 @@ Models in the abstract. """ from collections.abc import Callable, Mapping, Sequence -from typing import Any, Optional +from typing import Any, Optional, Union class Control(): """ From 4af0ec2682593e3cdc89d80ea0aad1ee19b9cadb Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 19 Jul 2023 11:52:57 -0400 Subject: [PATCH 10/11] import abstract types from typing to pass Python 3.9 tests --- HARK/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HARK/model.py b/HARK/model.py index d1ebc68db..1c7d4217a 100644 --- a/HARK/model.py +++ b/HARK/model.py @@ -1,8 +1,7 @@ """ Models in the abstract. """ -from collections.abc import Callable, Mapping, Sequence -from typing import Any, Optional, Union +from typing import Any, Callable, Mapping, Optional, Sequence, Union class Control(): """ From 97a17403ab5e033e1b3524655d57b666001c2c60 Mon Sep 17 00:00:00 2001 From: sb Date: Wed, 19 Jul 2023 11:59:46 -0400 Subject: [PATCH 11/11] reference/tools/model.rst for docs --- Documentation/reference/tools/model.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Documentation/reference/tools/model.rst diff --git a/Documentation/reference/tools/model.rst b/Documentation/reference/tools/model.rst new file mode 100644 index 000000000..8006ce09d --- /dev/null +++ b/Documentation/reference/tools/model.rst @@ -0,0 +1,7 @@ +Model +---------- + +.. automodule:: HARK.model + :members: + :undoc-members: + :show-inheritance: