From 50922e779debf00f9b63b59e74b737c907f155c6 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Thu, 9 Mar 2023 13:54:58 -0500 Subject: [PATCH 01/18] create Parameters --- HARK/core.py | 125 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 106 insertions(+), 19 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index b15a99425..98bfb25db 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -6,23 +6,17 @@ model adds an additional layer, endogenizing some of the inputs to the micro problem by finding a general equilibrium dynamic rule. """ -import os import sys +from collections import namedtuple from copy import copy, deepcopy -from distutils.dir_util import copy_tree from time import time from warnings import warn import numpy as np -from HARK.distribution import ( - Distribution, - IndexDistribution, - TimeVaryingDiscreteDistribution, -) - -from .parallel import multi_thread_commands, multi_thread_commands_fake -from .utilities import NullFunc, get_arg_names +from HARK.distribution import IndexDistribution, TimeVaryingDiscreteDistribution +from HARK.parallel import multi_thread_commands, multi_thread_commands_fake +from HARK.utilities import NullFunc, get_arg_names def distance_metric(thing_a, thing_b): @@ -150,11 +144,109 @@ def distance(self, other): return max(distance_list) +class Parameters: + def __init__(self, parameters): + self._parameters = parameters.copy() + self._term_age = parameters.get("T_cycle", None) + self._time_inv = [] + self._time_var = [] + + # Infer which parameters are time varying + for key, value in parameters.items(): + self._parameters[key] = self.__infer_dims__(key, value) + + def __infer_dims__(self, key, value): + if isinstance(value, (int, float, np.ndarray, type(None))): + self.__add_to_time_inv(key) + return value + if isinstance(value, (list, tuple)): + if len(value) == 1: + self.__add_to_time_inv(key) + return value[0] + if self._term_age is None: + self._term_age = len(value) + if len(value) == self._term_age: + self.__add_to_time_vary(key) + return np.asarray(value) + raise ValueError(f"Parameter {key} must be of length 1 or {self._term_age}") + raise ValueError(f"Parameter {key} has type {type(value)}") + + def __add_to_time_inv(self, key): + if key in self._time_var: + self._time_var.remove(key) + if key not in self._time_inv: + self._time_inv.append(key) + + def __add_to_time_vary(self, key): + if key in self._time_inv: + self._time_inv.remove(key) + if key not in self._time_var: + self._time_var.append(key) + + def __getitem__(self, key): + return self._parameters[key] + + def __getattr__(self, key): + return self._parameters[key] + + def __setitem__(self, key, value): + self._parameters[key] = value + + def __setattr__(self, key, value): + if key.startswith("_"): + # Handle setting internal attributes normally + super().__setattr__(key, value) + else: + # Handle setting parameters as dictionary items + self._parameters[key] = value + + def keys(self): + return self._parameters.keys() + + def values(self): + return self._parameters.values() + + def items(self): + return self._parameters.items() + + def __iter__(self): + return iter(self._parameters) + + def __deepcopy__(self, memo): + return deepcopy(self._parameters, memo) + + def to_dict(self, keys=None): + if keys is None: + keys = self._parameters.keys() + return {key: self[key] for key in keys} + + def to_namedtuple(self, keys=None): + if keys is None: + keys = self._parameters.keys() + return namedtuple("Parameters", keys)(**{key: self[key] for key in keys}) + + def update(self, other): + self._parameters.update(other) + self.__init__(self._parameters) + + def __repr__(self): + return ( + f"Parameters:\n" + f"term_age={self._term_age}\n" + f"time_inv={self._time_inv}\n" + f"time_var={self._time_var}\n" + f"\n{self._parameters}" + ) + + class Model: """ A class with special handling of parameters assignment. """ + def __init__(self, **kwds): + self.parameters = Parameters(kwds) + def assign_parameters(self, **kwds): """ Assign an arbitrary number of attributes to this agent. @@ -170,8 +262,8 @@ def assign_parameters(self, **kwds): none """ self.parameters.update(kwds) - for key in kwds: - setattr(self, key, kwds[key]) + for key, value in kwds.items(): + setattr(self, key, value) def get_parameter(self, name): """ @@ -191,13 +283,9 @@ def get_parameter(self, name): def __eq__(self, other): if isinstance(other, type(self)): - return self.parameters == other.parameters - - return notImplemented + return self.parameters.to_dict() == other.parameters.to_dict() - def __init__(self): - if not hasattr(self, "parameters"): - self.parameters = {} + return NotImplemented def __str__(self): type_ = type(self) @@ -1511,7 +1599,6 @@ def update_dynamics(self): Should have attributes named in dyn_vars. """ # Make a dictionary of inputs for the dynamics calculator - history_vars_string = "" arg_names = list(get_arg_names(self.calc_dynamics)) if "self" in arg_names: arg_names.remove("self") From fed117fd939bc099acbb5bb9dd0e2af2847e3091 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Thu, 9 Mar 2023 13:55:06 -0500 Subject: [PATCH 02/18] fix bugs --- HARK/ConsumptionSaving/ConsGenIncProcessModel.py | 2 +- HARK/ConsumptionSaving/ConsLaborModel.py | 6 ++---- .../tests/test_ConsGenIncProcessModel.py | 6 ++---- .../ConsumptionSaving/tests/test_IndShockConsumerType.py | 5 +++-- HARK/utilities.py | 9 +++++---- examples/GenIncProcessModel/GenIncProcessModel.py | 6 ++---- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/HARK/ConsumptionSaving/ConsGenIncProcessModel.py b/HARK/ConsumptionSaving/ConsGenIncProcessModel.py index fb12b97cc..737785189 100644 --- a/HARK/ConsumptionSaving/ConsGenIncProcessModel.py +++ b/HARK/ConsumptionSaving/ConsGenIncProcessModel.py @@ -861,7 +861,7 @@ def solve(self): # long run permanent income growth doesn't work yet init_explicit_perm_inc["PermGroFac"] = [1.0] init_explicit_perm_inc["aXtraMax"] = 30 -init_explicit_perm_inc["aXtraExtra"] = [0.005, 0.01] +init_explicit_perm_inc["aXtraExtra"] = np.array([0.005, 0.01]) class GenIncProcessConsumerType(IndShockConsumerType): diff --git a/HARK/ConsumptionSaving/ConsLaborModel.py b/HARK/ConsumptionSaving/ConsLaborModel.py index 71c4c2d62..42a18a01f 100644 --- a/HARK/ConsumptionSaving/ConsLaborModel.py +++ b/HARK/ConsumptionSaving/ConsLaborModel.py @@ -787,10 +787,8 @@ def plot_LbrFunc(self, t, bMin=None, bMax=None, ShkSet=None): 1.0, 1.0, ] # Wage rate in a lifecycle -init_labor_lifecycle["LbrCostCoeffs"] = [ - -2.0, - 0.4, -] # Assume labor cost coeffs is a polynomial of degree 1 +# Assume labor cost coeffs is a polynomial of degree 1 +init_labor_lifecycle["LbrCostCoeffs"] = np.array([-2.0, 0.4]) init_labor_lifecycle["T_cycle"] = 10 # init_labor_lifecycle['T_retire'] = 7 # IndexError at line 774 in interpolation.py. init_labor_lifecycle[ diff --git a/HARK/ConsumptionSaving/tests/test_ConsGenIncProcessModel.py b/HARK/ConsumptionSaving/tests/test_ConsGenIncProcessModel.py index d9b03145f..8a040a88e 100644 --- a/HARK/ConsumptionSaving/tests/test_ConsGenIncProcessModel.py +++ b/HARK/ConsumptionSaving/tests/test_ConsGenIncProcessModel.py @@ -25,10 +25,8 @@ # Parameters for constructing the "assets above minimum" grid "aXtraMin": 0.001, # Minimum end-of-period "assets above minimum" value "aXtraMax": 30, # Maximum end-of-period "assets above minimum" value - "aXtraExtra": [ - 0.005, - 0.01, - ], # Some other value of "assets above minimum" to add to the grid + # Some other value of "assets above minimum" to add to the grid + "aXtraExtra": np.array([0.005, 0.01]), "aXtraNestFac": 3, # Exponential nesting factor when constructing "assets above minimum" grid "aXtraCount": 48, # Number of points in the grid of "assets above minimum" # Parameters describing the income process diff --git a/HARK/ConsumptionSaving/tests/test_IndShockConsumerType.py b/HARK/ConsumptionSaving/tests/test_IndShockConsumerType.py index 92923bd1d..f1881a275 100644 --- a/HARK/ConsumptionSaving/tests/test_IndShockConsumerType.py +++ b/HARK/ConsumptionSaving/tests/test_IndShockConsumerType.py @@ -762,13 +762,13 @@ def setUp(self): "LivPrb": LivPrb, "PermGroFac": [PermGroFac], "Rfree": Rfree, - "track_vars": ["bNrm", "t_age"], } ) def test_NewbornStatesAndShocks(self): # Make agent, shock and initial condition histories agent = IndShockConsumerType(**self.base_params) + agent.track_vars = ["bNrm", "t_age"] agent.make_shock_history() # Find indices of agents and time periods that correspond to deaths @@ -814,13 +814,13 @@ def setUp(self): { "AgentCount": agent_count, "T_sim": t_sim, - "track_vars": ["t_age", "t_cycle"], } ) def test_compare_t_age_t_cycle(self): # Make agent, shock and initial condition histories agent = IndShockConsumerType(**self.base_params) + agent.track_vars = ["t_age", "t_cycle"] agent.make_shock_history() # Solve and simulate the agent @@ -855,6 +855,7 @@ def test_compare_t_age_t_cycle_premature_death(self): par["T_age"] = par["T_age"] - 8 # Make agent, shock and initial condition histories agent = IndShockConsumerType(**par) + agent.track_vars = ["t_age", "t_cycle"] agent.make_shock_history() # Solve and simulate the agent diff --git a/HARK/utilities.py b/HARK/utilities.py index 2bed83e06..a97e4a4aa 100644 --- a/HARK/utilities.py +++ b/HARK/utilities.py @@ -165,10 +165,11 @@ def construct_assets_grid(parameters): ) # Add in additional points for the grid: - for a in aXtraExtra: - if a is not None and a not in aXtraGrid: - j = aXtraGrid.searchsorted(a) - aXtraGrid = np.insert(aXtraGrid, j, a) + if aXtraExtra is not None: + for a in aXtraExtra: + if a is not None and a not in aXtraGrid: + j = aXtraGrid.searchsorted(a) + aXtraGrid = np.insert(aXtraGrid, j, a) return aXtraGrid diff --git a/examples/GenIncProcessModel/GenIncProcessModel.py b/examples/GenIncProcessModel/GenIncProcessModel.py index e08fa66dc..5934d9264 100644 --- a/examples/GenIncProcessModel/GenIncProcessModel.py +++ b/examples/GenIncProcessModel/GenIncProcessModel.py @@ -200,10 +200,8 @@ # Parameters for constructing the "assets above minimum" grid "aXtraMin": 0.001, # Minimum end-of-period "assets above minimum" value "aXtraMax": 30, # Maximum end-of-period "assets above minimum" value - "aXtraExtra": [ - 0.005, - 0.01, - ], # Some other value of "assets above minimum" to add to the grid + # Some other value of "assets above minimum" to add to the grid + "aXtraExtra": np.array([0.005, 0.01]), "aXtraNestFac": 3, # Exponential nesting factor when constructing "assets above minimum" grid "aXtraCount": 48, # Number of points in the grid of "assets above minimum" # Parameters describing the income process From 68f48e075d0121603556cb4df7a2c92e151d09c3 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Thu, 9 Mar 2023 14:02:23 -0500 Subject: [PATCH 03/18] sync notebook --- examples/GenIncProcessModel/GenIncProcessModel.ipynb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/GenIncProcessModel/GenIncProcessModel.ipynb b/examples/GenIncProcessModel/GenIncProcessModel.ipynb index 4570b10d2..07dbc24ac 100644 --- a/examples/GenIncProcessModel/GenIncProcessModel.ipynb +++ b/examples/GenIncProcessModel/GenIncProcessModel.ipynb @@ -206,10 +206,8 @@ " # Parameters for constructing the \"assets above minimum\" grid\n", " \"aXtraMin\": 0.001, # Minimum end-of-period \"assets above minimum\" value\n", " \"aXtraMax\": 30, # Maximum end-of-period \"assets above minimum\" value\n", - " \"aXtraExtra\": [\n", - " 0.005,\n", - " 0.01,\n", - " ], # Some other value of \"assets above minimum\" to add to the grid\n", + " # Some other value of \"assets above minimum\" to add to the grid\n", + " \"aXtraExtra\": np.array([0.005, 0.01]),\n", " \"aXtraNestFac\": 3, # Exponential nesting factor when constructing \"assets above minimum\" grid\n", " \"aXtraCount\": 48, # Number of points in the grid of \"assets above minimum\"\n", " # Parameters describing the income process\n", From b47f0a98fae5b13677f79c38cd3d79f0a11490fa Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Thu, 9 Mar 2023 19:22:27 -0500 Subject: [PATCH 04/18] fix bugs --- HARK/core.py | 2 +- .../Quick_start_with_solution.ipynb | 12 +- .../Quick_start_with_solution.py | 12 +- .../LifecycleModel/EstimationParameters.py | 554 ++++++++++++++---- 4 files changed, 450 insertions(+), 130 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index 98bfb25db..e352d320b 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -163,7 +163,7 @@ def __infer_dims__(self, key, value): if len(value) == 1: self.__add_to_time_inv(key) return value[0] - if self._term_age is None: + if self._term_age is None or self._term_age == 1: self._term_age = len(value) if len(value) == self._term_age: self.__add_to_time_vary(key) diff --git a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.ipynb b/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.ipynb index 094b0d85f..e917b0cd9 100644 --- a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.ipynb +++ b/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.ipynb @@ -3366,10 +3366,10 @@ "prob_surv = 1 - prob_dead\n", "\n", "# The HARK argument need to be a list, thus convert it from numpy array\n", - "prob_surv_list = np.ndarray.tolist(prob_surv[:80])\n", + "prob_surv_list = np.ndarray.tolist(prob_surv[:79])\n", "\n", "income_profile = np.genfromtxt(\"productivity_profile.csv\", delimiter=\",\", skip_header=1)\n", - "income_profile_list = np.ndarray.tolist(income_profile[:80])\n", + "income_profile_list = np.ndarray.tolist(income_profile[:79])\n", "\n", "# Continue your solution" ] @@ -3445,10 +3445,10 @@ "\n", "prob_dead = np.genfromtxt(\"life_table.csv\", delimiter=\",\", skip_header=1)\n", "prob_surv = 1 - prob_dead\n", - "prob_surv_list = np.ndarray.tolist(prob_surv[:80])\n", + "prob_surv_list = np.ndarray.tolist(prob_surv[:79])\n", "\n", "income_profile = np.genfromtxt(\"productivity_profile.csv\", delimiter=\",\", skip_header=1)\n", - "income_profile_list = np.ndarray.tolist(income_profile[:80])\n", + "income_profile_list = np.ndarray.tolist(income_profile[:79])\n", "\n", "Ex_dictionary = {\n", " \"CRRA\": 2.0,\n", @@ -3457,7 +3457,7 @@ " \"LivPrb\": prob_surv_list,\n", " \"PermGroFac\": income_profile_list,\n", " \"cycles\": 1,\n", - " \"T_cycle\": 1,\n", + " \"T_cycle\": 79,\n", "}\n", "\n", "Ex_agent = PerfForesightConsumerType(**Ex_dictionary)\n", @@ -3478,7 +3478,7 @@ " \"pLvlInitMean\": 0.0,\n", " \"pLvlInitStd\": 0.05,\n", " \"PermGroFacAgg\": 1.0,\n", - " \"T_cycle\": 1,\n", + " \"T_cycle\": 79,\n", " \"T_sim\": 2000,\n", " \"T_age\": 80,\n", " \"BoroCnstArt\": 0.0,\n", diff --git a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py b/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py index 42e209614..d621e9ff6 100644 --- a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py +++ b/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py @@ -713,10 +713,10 @@ prob_surv = 1 - prob_dead # The HARK argument need to be a list, thus convert it from numpy array -prob_surv_list = np.ndarray.tolist(prob_surv[:80]) +prob_surv_list = np.ndarray.tolist(prob_surv[:79]) income_profile = np.genfromtxt("productivity_profile.csv", delimiter=",", skip_header=1) -income_profile_list = np.ndarray.tolist(income_profile[:80]) +income_profile_list = np.ndarray.tolist(income_profile[:79]) # Continue your solution @@ -733,10 +733,10 @@ prob_dead = np.genfromtxt("life_table.csv", delimiter=",", skip_header=1) prob_surv = 1 - prob_dead -prob_surv_list = np.ndarray.tolist(prob_surv[:80]) +prob_surv_list = np.ndarray.tolist(prob_surv[:79]) income_profile = np.genfromtxt("productivity_profile.csv", delimiter=",", skip_header=1) -income_profile_list = np.ndarray.tolist(income_profile[:80]) +income_profile_list = np.ndarray.tolist(income_profile[:79]) Ex_dictionary = { "CRRA": 2.0, @@ -745,7 +745,7 @@ "LivPrb": prob_surv_list, "PermGroFac": income_profile_list, "cycles": 1, - "T_cycle": 1, + "T_cycle": 79, } Ex_agent = PerfForesightConsumerType(**Ex_dictionary) @@ -766,7 +766,7 @@ "pLvlInitMean": 0.0, "pLvlInitStd": 0.05, "PermGroFacAgg": 1.0, - "T_cycle": 1, + "T_cycle": 79, "T_sim": 2000, "T_age": 80, "BoroCnstArt": 0.0, diff --git a/examples/LifecycleModel/EstimationParameters.py b/examples/LifecycleModel/EstimationParameters.py index 01c5454fc..d3bb158fd 100644 --- a/examples/LifecycleModel/EstimationParameters.py +++ b/examples/LifecycleModel/EstimationParameters.py @@ -1,148 +1,468 @@ -''' +""" Specifies the full set of calibrated values required to estimate the SolvingMicroDSOPs model. The empirical data is stored in a separate csv file and is loaded in SetupSCFdata. -''' +""" + +import numpy as np + # --------------------------------------------------------------------------------- # - Define all of the model parameters for SolvingMicroDSOPs and ConsumerExamples - # --------------------------------------------------------------------------------- -exp_nest = 3 # Number of times to "exponentially nest" when constructing a_grid -aXtraMin = 0.001 # Minimum end-of-period "assets above minimum" value -aXtraMax = 20 # Maximum end-of-period "assets above minimum" value -aXtraHuge = None # A very large value of assets to add to the grid, not used -aXtraExtra = None # Some other value of assets to add to the grid, not used -aXtraCount = 8 # Number of points in the grid of "assets above minimum" - -BoroCnstArt = 0.0 # Artificial borrowing constraint; imposed minimum level of end-of period assets -CubicBool = True # Use cubic spline interpolation when True, linear interpolation when False -vFuncBool = False # Whether to calculate the value function during solution - -Rfree = 1.03 # Interest factor on assets -PermShkCount = 7 # Number of points in discrete approximation to permanent income shocks -TranShkCount = 7 # Number of points in discrete approximation to transitory income shocks -UnempPrb = 0.005 # Probability of unemployment while working -UnempPrbRet = 0.000 # Probability of "unemployment" while retired -IncUnemp = 0.0 # Unemployment benefits replacement rate -IncUnempRet = 0.0 # "Unemployment" benefits when retired - -final_age = 90 # Age at which the problem ends (die with certainty) -retirement_age = 65 # Age at which the consumer retires -initial_age = 25 # Age at which the consumer enters the model -TT = final_age - initial_age # Total number of periods in the model +exp_nest = 3 # Number of times to "exponentially nest" when constructing a_grid +aXtraMin = 0.001 # Minimum end-of-period "assets above minimum" value +aXtraMax = 20 # Maximum end-of-period "assets above minimum" value +aXtraHuge = None # A very large value of assets to add to the grid, not used +aXtraExtra = None # Some other value of assets to add to the grid, not used +aXtraCount = 8 # Number of points in the grid of "assets above minimum" + +# Artificial borrowing constraint; imposed minimum level of end-of period assets +BoroCnstArt = 0.0 +CubicBool = ( + True # Use cubic spline interpolation when True, linear interpolation when False +) +vFuncBool = False # Whether to calculate the value function during solution + +Rfree = 1.03 # Interest factor on assets +PermShkCount = ( + 7 # Number of points in discrete approximation to permanent income shocks +) +TranShkCount = ( + 7 # Number of points in discrete approximation to transitory income shocks +) +UnempPrb = 0.005 # Probability of unemployment while working +UnempPrbRet = 0.000 # Probability of "unemployment" while retired +IncUnemp = 0.0 # Unemployment benefits replacement rate +IncUnempRet = 0.0 # "Unemployment" benefits when retired + +final_age = 90 # Age at which the problem ends (die with certainty) +retirement_age = 65 # Age at which the consumer retires +initial_age = 25 # Age at which the consumer enters the model +TT = final_age - initial_age # Total number of periods in the model retirement_t = retirement_age - initial_age - 1 -CRRA_start = 4.0 # Initial guess of the coefficient of relative risk aversion during estimation (rho) -DiscFacAdj_start = 0.99 # Initial guess of the adjustment to the discount factor during estimation (beth) -DiscFacAdj_bound = [0.0001,15.0] # Bounds for beth; if violated, objective function returns "penalty value" -CRRA_bound = [0.0001,15.0] # Bounds for rho; if violated, objective function returns "penalty value" +# Initial guess of the coefficient of relative risk aversion during estimation (rho) +CRRA_start = 4.0 +# Initial guess of the adjustment to the discount factor during estimation (beth) +DiscFacAdj_start = 0.99 +DiscFacAdj_bound = [ + 0.0001, + 15.0, +] # Bounds for beth; if violated, objective function returns "penalty value" +CRRA_bound = [ + 0.0001, + 15.0, +] # Bounds for rho; if violated, objective function returns "penalty value" # Expected growth rates of permanent income over the lifecycle, starting from age 25 -PermGroFac = [ 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, - 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, - 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, 1.025, - 1.025, 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , - 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , 1.01 , 0.7 , # <-- This represents retirement - 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ] +PermGroFac = [ + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.025, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 0.7, # <-- This represents retirement + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, +] # Age-varying discount factors over the lifecycle, lifted from Cagetti (2003) -DiscFac_timevary = [1.064914 , 1.057997 , 1.051422 , 1.045179 , 1.039259 , - 1.033653 , 1.028352 , 1.023348 , 1.018632 , 1.014198 , - 1.010037 , 1.006143 , 1.002509 , 0.9991282, 0.9959943, - 0.9931012, 0.9904431, 0.9880143, 0.9858095, 0.9838233, - 0.9820506, 0.9804866, 0.9791264, 0.9779656, 0.9769995, - 0.9762239, 0.9756346, 0.9752274, 0.9749984, 0.9749437, - 0.9750595, 0.9753422, 0.9757881, 0.9763936, 0.9771553, - 0.9780698, 0.9791338, 0.9803439, 0.981697 , 0.8287214, - 0.9902111, 0.9902111, 0.9902111, 0.9902111, 0.9902111, - 0.9902111, 0.9902111, 0.9902111, 0.9902111, 0.9902111, - 0.9902111, 0.9902111, 0.9902111, 0.9902111, 0.9902111, - 0.9902111, 0.9902111, 0.9902111, 0.9902111, 0.9902111, - 0.9902111, 0.9902111, 0.9902111, 0.9902111, 0.9902111] +DiscFac_timevary = [ + 1.064914, + 1.057997, + 1.051422, + 1.045179, + 1.039259, + 1.033653, + 1.028352, + 1.023348, + 1.018632, + 1.014198, + 1.010037, + 1.006143, + 1.002509, + 0.9991282, + 0.9959943, + 0.9931012, + 0.9904431, + 0.9880143, + 0.9858095, + 0.9838233, + 0.9820506, + 0.9804866, + 0.9791264, + 0.9779656, + 0.9769995, + 0.9762239, + 0.9756346, + 0.9752274, + 0.9749984, + 0.9749437, + 0.9750595, + 0.9753422, + 0.9757881, + 0.9763936, + 0.9771553, + 0.9780698, + 0.9791338, + 0.9803439, + 0.981697, + 0.8287214, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, + 0.9902111, +] # Survival probabilities over the lifecycle, starting from age 25 -LivPrb = [ 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , - 1. , 1. , 1. , 1. , 1. , # <-- automatic survival to age 65 - 0.98438596, 0.98438596, 0.98438596, 0.98438596, 0.98438596, - 0.97567062, 0.97567062, 0.97567062, 0.97567062, 0.97567062, - 0.96207901, 0.96207901, 0.96207901, 0.96207901, 0.96207901, - 0.93721595, 0.93721595, 0.93721595, 0.93721595, 0.93721595, - 0.63095734, 0.63095734, 0.63095734, 0.63095734, 0.63095734] +LivPrb = [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, # <-- automatic survival to age 65 + 0.98438596, + 0.98438596, + 0.98438596, + 0.98438596, + 0.98438596, + 0.97567062, + 0.97567062, + 0.97567062, + 0.97567062, + 0.97567062, + 0.96207901, + 0.96207901, + 0.96207901, + 0.96207901, + 0.96207901, + 0.93721595, + 0.93721595, + 0.93721595, + 0.93721595, + 0.93721595, + 0.63095734, + 0.63095734, + 0.63095734, + 0.63095734, + 0.63095734, +] # Standard deviations of permanent income shocks by age, starting from age 25 -PermShkStd = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, -0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, -0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, # <-- no permanent income shocks after retirement -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +PermShkStd = [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.0, + 0.0, + 0.0, # <-- no permanent income shocks after retirement + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, +] # Standard deviations of transitory income shocks by age, starting from age 25 -TranShkStd = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, -0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, -0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, # <-- no transitory income shocs after retirement -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +TranShkStd = [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.0, + 0.0, + 0.0, # <-- no transitory income shocs after retirement + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, +] # Age groups for the estimation: calculate average wealth-to-permanent income ratio # for consumers within each of these age groups, compare actual to simulated data -empirical_cohort_age_groups = [[ 26,27,28,29,30 ], - [ 31,32,33,34,35 ], - [ 36,37,38,39,40 ], - [ 41,42,43,44,45 ], - [ 46,47,48,49,50 ], - [ 51,52,53,54,55 ], - [ 56,57,58,59,60 ]] - -initial_wealth_income_ratio_vals = [0.17, 0.5, 0.83] # Three point discrete distribution of initial w -initial_wealth_income_ratio_probs = [0.33333, 0.33333, 0.33334] # Equiprobable discrete distribution of initial w -num_agents = 10000 # Number of agents to simulate -bootstrap_size = 50 # Number of re-estimations to do during bootstrap -seed = 31382 # Just an integer to seed the estimation +empirical_cohort_age_groups = [ + [26, 27, 28, 29, 30], + [31, 32, 33, 34, 35], + [36, 37, 38, 39, 40], + [41, 42, 43, 44, 45], + [46, 47, 48, 49, 50], + [51, 52, 53, 54, 55], + [56, 57, 58, 59, 60], +] + +initial_wealth_income_ratio_vals = [ + 0.17, + 0.5, + 0.83, +] # Three point discrete distribution of initial w +initial_wealth_income_ratio_probs = [ + 0.33333, + 0.33333, + 0.33334, +] # Equiprobable discrete distribution of initial w +num_agents = 10000 # Number of agents to simulate +bootstrap_size = 50 # Number of re-estimations to do during bootstrap +seed = 31382 # Just an integer to seed the estimation # ----------------------------------------------------------------------------- # -- Set up the dictionary "container" for making a basic lifecycle type ------ # ----------------------------------------------------------------------------- # Dictionary that can be passed to ConsumerType to instantiate -init_consumer_objects = {"CRRA":CRRA_start, - "Rfree":Rfree, - "PermGroFac":PermGroFac, - "BoroCnstArt":BoroCnstArt, - "PermShkStd":PermShkStd, - "PermShkCount":PermShkCount, - "TranShkStd":TranShkStd, - "TranShkCount":TranShkCount, - "T_cycle":TT, - "UnempPrb":UnempPrb, - "UnempPrbRet":UnempPrbRet, - "T_retire":retirement_t, - "T_age":TT+1, - "IncUnemp":IncUnemp, - "IncUnempRet":IncUnempRet, - "aXtraMin":aXtraMin, - "aXtraMax":aXtraMax, - "aXtraCount":aXtraCount, - "aXtraExtra":[aXtraExtra,aXtraHuge], - "aXtraNestFac":exp_nest, - "LivPrb":LivPrb, - "DiscFac":DiscFac_timevary, - 'AgentCount':num_agents, - 'seed':seed, - 'tax_rate':0.0, - 'vFuncBool':vFuncBool, - 'CubicBool':CubicBool - } - - -if __name__ == '__main__': +init_consumer_objects = { + "CRRA": CRRA_start, + "Rfree": Rfree, + "PermGroFac": PermGroFac, + "BoroCnstArt": BoroCnstArt, + "PermShkStd": PermShkStd, + "PermShkCount": PermShkCount, + "TranShkStd": TranShkStd, + "TranShkCount": TranShkCount, + "T_cycle": TT, + "UnempPrb": UnempPrb, + "UnempPrbRet": UnempPrbRet, + "T_retire": retirement_t, + "T_age": TT + 1, + "IncUnemp": IncUnemp, + "IncUnempRet": IncUnempRet, + "aXtraMin": aXtraMin, + "aXtraMax": aXtraMax, + "aXtraCount": aXtraCount, + "aXtraExtra": np.array([aXtraExtra, aXtraHuge]), + "aXtraNestFac": exp_nest, + "LivPrb": LivPrb, + "DiscFac": DiscFac_timevary, + "AgentCount": num_agents, + "seed": seed, + "tax_rate": 0.0, + "vFuncBool": vFuncBool, + "CubicBool": CubicBool, +} + + +if __name__ == "__main__": print("Sorry, EstimationParameters doesn't actually do anything on its own.") print("This module is imported by StructEstimation, providing calibrated ") print("parameters for the example estimation. Please see that module if you ") From 843b6985a7e61b365da2aaadb11f4a265394d05b Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Fri, 7 Apr 2023 15:25:01 -0400 Subject: [PATCH 05/18] fix bugs --- .pre-commit-config.yaml | 8 +-- HARK/core.py | 111 +++++++++++++++++++++------------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4496cafb2..cc85f6788 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: Documentation/example_notebooks/ repos: - repo: https://github.com/mwouts/jupytext - rev: v1.14.4 + rev: v1.14.5 hooks: - id: jupytext args: @@ -11,7 +11,7 @@ repos: files: ^examples/.*\.ipynb$ - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black exclude: ^examples/ @@ -30,7 +30,7 @@ repos: exclude: ^examples/ - repo: https://github.com/pycqa/isort - rev: 5.11.5 + rev: 5.12.0 hooks: - id: isort name: isort (python) @@ -38,7 +38,7 @@ repos: exclude: ^examples/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.4 + rev: v3.0.0-alpha.6 hooks: - id: prettier exclude: ^examples/ diff --git a/HARK/core.py b/HARK/core.py index 6272da5c5..6f0eed6f0 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -20,15 +20,13 @@ class Parameters: - def __init__(self, parameters): - self._parameters = parameters.copy() + def __init__(self, **parameters): self._term_age = parameters.get("T_cycle", None) - self._time_inv = [] - self._time_var = [] + self._age_inv = [] + self._age_var = [] - # Infer which parameters are time varying for key, value in parameters.items(): - self._parameters[key] = self.__infer_dims__(key, value) + setattr(self, key, value) def __infer_dims__(self, key, value): if isinstance(value, (int, float, np.ndarray, type(None))): @@ -42,30 +40,41 @@ def __infer_dims__(self, key, value): self._term_age = len(value) if len(value) == self._term_age: self.__add_to_time_vary(key) - return np.asarray(value) + return value raise ValueError(f"Parameter {key} must be of length 1 or {self._term_age}") raise ValueError(f"Parameter {key} has type {type(value)}") def __add_to_time_inv(self, key): - if key in self._time_var: - self._time_var.remove(key) - if key not in self._time_inv: - self._time_inv.append(key) + if key in self._age_var: + self._age_var.remove(key) + if key not in self._age_inv: + self._age_inv.append(key) def __add_to_time_vary(self, key): - if key in self._time_inv: - self._time_inv.remove(key) - if key not in self._time_var: - self._time_var.append(key) - - def __getitem__(self, key): - return self._parameters[key] - - def __getattr__(self, key): - return self._parameters[key] + if key in self._age_inv: + self._age_inv.remove(key) + if key not in self._age_var: + self._age_var.append(key) + + def __getitem__(self, age): + if isinstance(age, int): + # return parameters at age + assert age < self._term_age + + params = {} + for key in self._age_inv: + params[key] = getattr(self, key) + for key in self._age_var: + params[key] = getattr(self, key)[age] + return Parameters(**params) + + elif isinstance(age, str): + return getattr(self, age) def __setitem__(self, key, value): - self._parameters[key] = value + if not isinstance(key, str): + raise ValueError("Parameters must be set with a string key") + self.__setattr__(key, value) def __setattr__(self, key, value): if key.startswith("_"): @@ -73,45 +82,39 @@ def __setattr__(self, key, value): super().__setattr__(key, value) else: # Handle setting parameters as dictionary items - self._parameters[key] = value + new_value = self.__infer_dims__(key, value) + super().__setattr__(key, new_value) def keys(self): - return self._parameters.keys() + return self._age_inv + self._age_var def values(self): - return self._parameters.values() + return [getattr(self, key) for key in self.keys()] def items(self): - return self._parameters.items() + return [(key, getattr(self, key)) for key in self.keys()] def __iter__(self): - return iter(self._parameters) + return iter(self.keys()) def __deepcopy__(self, memo): - return deepcopy(self._parameters, memo) - - def to_dict(self, keys=None): - if keys is None: - keys = self._parameters.keys() - return {key: self[key] for key in keys} + return Parameters(**deepcopy(self.to_dict(), memo)) - def to_namedtuple(self, keys=None): - if keys is None: - keys = self._parameters.keys() - return namedtuple("Parameters", keys)(**{key: self[key] for key in keys}) + def to_dict(self): + return {key: getattr(self, key) for key in self.keys()} - def update(self, other): - self._parameters.update(other) - self.__init__(self._parameters) + def to_namedtuple(self): + return namedtuple("Parameters", self.keys())(**self.to_dict()) - def __repr__(self): - return ( - f"Parameters:\n" - f"term_age={self._term_age}\n" - f"time_inv={self._time_inv}\n" - f"time_var={self._time_var}\n" - f"\n{self._parameters}" - ) + def update(self, other_params): + if isinstance(other_params, Parameters): + for key, value in other_params: + setattr(self, key, value) + elif isinstance(other_params, dict): + for key, value in other_params.items(): + setattr(self, key, value) + else: + raise ValueError("Parameters must be a dict or a Parameters object") class Model: @@ -120,7 +123,7 @@ class Model: """ def __init__(self, **kwds): - self.parameters = Parameters(kwds) + self.parameters = Parameters(**kwds) def assign_parameters(self, **kwds): """ @@ -167,14 +170,14 @@ def __str__(self): module = type_.__module__ qualname = type_.__qualname__ - s = f"<{module}.{qualname} object at {hex(id(self))}.\n" - s += "Parameters:" + string = f"<{module}.{qualname} object at {hex(id(self))}.\n" + string += "Parameters:" for p in self.parameters: - s += f"\n{p}: {self.parameters[p]}" + string += f"\n{p}: {self.parameters[p]}" - s += ">" - return s + string += ">" + return string def __repr__(self): return self.__str__() From c5d47190ec32ae8afab9ccf57e96f9713d7ff3d8 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Fri, 7 Apr 2023 15:39:59 -0400 Subject: [PATCH 06/18] fix codecov? --- .github/workflows/coverage.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7d336c7c7..44a73fbc2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -28,6 +28,8 @@ jobs: - name: Generate coverage report run: | pip install pytest pytest-cov - NUMBA_DISABLE_JIT=1 pytest --cov=./ --cov-report=xml + pytest --cov=./ --cov-report=xml - name: upload coverage report uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: false From 81937dffaf7d4bfaf4bcc61d2e3f0ce3a2fcd012 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Fri, 7 Apr 2023 16:40:59 -0400 Subject: [PATCH 07/18] add documentation --- HARK/core.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index 6f0eed6f0..db0ef09f4 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -20,7 +20,34 @@ class Parameters: + """ + This class defines an object that stores all of the parameters for a model + as attributes of itself. It is designed to also handle the age-varying + dynamics of parameters. + + Attributes + ---------- + + _term_age : int + The terminal age of the agents in the model. + _age_inv : list + A list of the names of the parameters that are invariant over time. + _age_var : list + A list of the names of the parameters that vary over time. + """ + def __init__(self, **parameters): + """ + Initializes a Parameters object and parses the age-varying + dynamics of the parameters. + + Parameters + ---------- + + parameters : keyword arguments + Any number of keyword arguments of the form key=value. + To parse a dictionary of parameters, use the ** operator. + """ self._term_age = parameters.get("T_cycle", None) self._age_inv = [] self._age_var = [] @@ -29,6 +56,23 @@ def __init__(self, **parameters): setattr(self, key, value) def __infer_dims__(self, key, value): + """ + Infers the age-varying dimensions of a parameter. + + If the parameter is a scalar, numpy array, or None, it is assumed to be + invariant over time. If the parameter is a list or tuple, it is assumed + to be varying over time. If the parameter is a list or tuple of length + greater than 1, the length of the list or tuple must match the + `_term_age` attribute of the Parameters object. + + Parameters + ---------- + key : str + name of parameter + value : Any + value of parameter + + """ if isinstance(value, (int, float, np.ndarray, type(None))): self.__add_to_time_inv(key) return value @@ -45,33 +89,78 @@ def __infer_dims__(self, key, value): raise ValueError(f"Parameter {key} has type {type(value)}") def __add_to_time_inv(self, key): + """ + Adds parameter name to invariant list and removes from varying list. + + Parameters + ---------- + key : str + parameter name + """ if key in self._age_var: self._age_var.remove(key) if key not in self._age_inv: self._age_inv.append(key) def __add_to_time_vary(self, key): + """ + Adds parameter name to varying list and removes from invariant list. + + Parameters + ---------- + key : str + parameter name + """ if key in self._age_inv: self._age_inv.remove(key) if key not in self._age_var: self._age_var.append(key) - def __getitem__(self, age): - if isinstance(age, int): + def __getitem__(self, age_or_key): + """ + If age_or_key is an integer, returns a Parameters object with the parameters + that apply to that age. This includes all invariant parameters and the + `age_or_key`th element of all age-varying parameters. If age_or_key is a string, + it returns the value of the parameter with that name. + + Parameters + ---------- + age_or_key : int or str + Age or key of parameter(s) + + + Returns + ------- + Parameters or value + Parameters object with parameters that apply to age `age_or_key` or value of + parameter with name `age_or_key`. + """ + if isinstance(age_or_key, int): # return parameters at age - assert age < self._term_age + assert age_or_key < self._term_age params = {} for key in self._age_inv: params[key] = getattr(self, key) for key in self._age_var: - params[key] = getattr(self, key)[age] + params[key] = getattr(self, key)[age_or_key] return Parameters(**params) - elif isinstance(age, str): - return getattr(self, age) + elif isinstance(age_or_key, str): + return getattr(self, age_or_key) def __setitem__(self, key, value): + """ + Sets the value of a parameter. + + Parameters + ---------- + key : str + name of parameter + value : Any + value of parameter + + """ if not isinstance(key, str): raise ValueError("Parameters must be set with a string key") self.__setattr__(key, value) From 96d56538d633160141e50921dd1197d675d66a97 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Wed, 19 Apr 2023 11:07:45 -0400 Subject: [PATCH 08/18] seb's suggestions --- HARK/core.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/HARK/core.py b/HARK/core.py index 4475911d3..5c74f7711 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -137,7 +137,8 @@ def __getitem__(self, age_or_key): """ if isinstance(age_or_key, int): # return parameters at age - assert age_or_key < self._term_age + if age_or_key >= self._term_age: + raise ValueError("Age or key is greater than or equal to term age.") params = {} for key in self._age_inv: @@ -205,6 +206,12 @@ def update(self, other_params): else: raise ValueError("Parameters must be a dict or a Parameters object") + def __str__(self): + return f"Parameters({str(self.to_dict())})" + + def __repr__(self): + return f"Parameters( _age_inv = {str(self._age_inv)}, _age_var = {str(self._age_var)}, | {str(self.to_dict())})" + class Model: """ From 42dde44a081a48c8a24382eb4fc74835f2f55b14 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Wed, 3 May 2023 10:16:54 -0400 Subject: [PATCH 09/18] add documentation --- HARK/core.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/HARK/core.py b/HARK/core.py index eb994caaf..9496a17bf 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -176,6 +176,18 @@ def __setitem__(self, key, value): self.__setattr__(key, value) def __setattr__(self, key, value): + """ + Sets attribute depending on key. If key starts with an underscore, it is + assumed to be an internal attribute and is set normally. Otherwise, we + infer its age-varying dimensions and set it as an attribute. + + Parameters + ---------- + key : str + Name of parameter + value : Any + Value of parameter + """ if key.startswith("_"): # Handle setting internal attributes normally super().__setattr__(key, value) @@ -185,27 +197,57 @@ def __setattr__(self, key, value): super().__setattr__(key, new_value) def keys(self): + """ + Returns a list of the names of the parameters. + """ return self._age_inv + self._age_var def values(self): + """ + Returns a list of the values of the parameters. + """ return [getattr(self, key) for key in self.keys()] def items(self): + """ + Returns a list of tuples of the form (name, value) for each parameter. + """ return [(key, getattr(self, key)) for key in self.keys()] def __iter__(self): + """ + Allows for iterating over the parameter names. + """ return iter(self.keys()) def __deepcopy__(self, memo): + """ + Returns a deep copy of the Parameters object. + """ return Parameters(**deepcopy(self.to_dict(), memo)) def to_dict(self): + """ + Returns a dictionary of the parameters. + """ return {key: getattr(self, key) for key in self.keys()} def to_namedtuple(self): + """ + Returns a namedtuple of the parameters. + """ return namedtuple("Parameters", self.keys())(**self.to_dict()) def update(self, other_params): + """ + Updates the parameters with the values from another + Parameters object or a dictionary. + + Parameters + ---------- + other_params : Parameters or dict + Parameters object or dictionary of parameters to update with. + """ if isinstance(other_params, Parameters): for key, value in other_params: setattr(self, key, value) @@ -216,9 +258,15 @@ def update(self, other_params): raise ValueError("Parameters must be a dict or a Parameters object") def __str__(self): + """ + Returns a simple string representation of the Parameters object. + """ return f"Parameters({str(self.to_dict())})" def __repr__(self): + """ + Returns a detailed string representation of the Parameters object. + """ return f"Parameters( _age_inv = {str(self._age_inv)}, _age_var = {str(self._age_var)}, | {str(self.to_dict())})" From b9f4a9e1b5c4f41ba6f3dc83f84145a4a681c154 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Wed, 3 May 2023 10:44:59 -0400 Subject: [PATCH 10/18] fix bug --- HARK/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index 9496a17bf..867343448 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -1680,9 +1680,6 @@ def distribute_params(agent, param_name, param_count, distribution): return agent_set -Parameters = NewType("ParameterDict", dict) - - @dataclass class AgentPopulation: """ @@ -1690,7 +1687,7 @@ class AgentPopulation: """ agent_type: AgentType # type of agent in the population - parameters: Parameters # dictionary of parameters + parameters: dict # dictionary of parameters seed: int = 0 # random seed time_var: List[str] = field(init=False) time_inv: List[str] = field(init=False) From b655e30082378cf93d011bea206d20a6c380304d Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 24 Jul 2023 11:43:36 -0400 Subject: [PATCH 11/18] change to internal dictionary --- HARK/core.py | 100 ++++++++++++++------------------------------------- 1 file changed, 26 insertions(+), 74 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index 867343448..7ce1d21df 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -31,7 +31,7 @@ class Parameters: """ This class defines an object that stores all of the parameters for a model - as attributes of itself. It is designed to also handle the age-varying + as an internal dictionary. It is designed to also handle the age-varying dynamics of parameters. Attributes @@ -57,12 +57,13 @@ def __init__(self, **parameters): Any number of keyword arguments of the form key=value. To parse a dictionary of parameters, use the ** operator. """ - self._term_age = parameters.get("T_cycle", None) - self._age_inv = [] - self._age_var = [] + self._term_age = parameters.pop("T_cycle", None) + self._age_inv = set() + self._age_var = set() + self._parameters = {} for key, value in parameters.items(): - setattr(self, key, value) + self._parameters[key] = self.__infer_dims__(key, value) def __infer_dims__(self, key, value): """ @@ -99,31 +100,17 @@ def __infer_dims__(self, key, value): def __add_to_time_inv(self, key): """ - Adds parameter name to invariant list and removes from varying list. - - Parameters - ---------- - key : str - parameter name + Adds parameter name to invariant set and removes from varying set. """ - if key in self._age_var: - self._age_var.remove(key) - if key not in self._age_inv: - self._age_inv.append(key) + self._age_var.discard(key) + self._age_inv.add(key) def __add_to_time_vary(self, key): """ - Adds parameter name to varying list and removes from invariant list. - - Parameters - ---------- - key : str - parameter name + Adds parameter name to varying set and removes from invariant set. """ - if key in self._age_inv: - self._age_inv.remove(key) - if key not in self._age_var: - self._age_var.append(key) + self._age_inv.discard(key) + self._age_var.add(key) def __getitem__(self, age_or_key): """ @@ -131,33 +118,19 @@ def __getitem__(self, age_or_key): that apply to that age. This includes all invariant parameters and the `age_or_key`th element of all age-varying parameters. If age_or_key is a string, it returns the value of the parameter with that name. - - Parameters - ---------- - age_or_key : int or str - Age or key of parameter(s) - - - Returns - ------- - Parameters or value - Parameters object with parameters that apply to age `age_or_key` or value of - parameter with name `age_or_key`. """ if isinstance(age_or_key, int): - # return parameters at age if age_or_key >= self._term_age: - raise ValueError("Age or key is greater than or equal to term age.") + raise ValueError("Age is greater than or equal to terminal age.") - params = {} - for key in self._age_inv: - params[key] = getattr(self, key) - for key in self._age_var: - params[key] = getattr(self, key)[age_or_key] + params = {key: self._parameters[key] for key in self._age_inv} + params.update( + {key: self._parameters[key][age_or_key] for key in self._age_var} + ) return Parameters(**params) elif isinstance(age_or_key, str): - return getattr(self, age_or_key) + return self._parameters[age_or_key] def __setitem__(self, key, value): """ @@ -173,28 +146,7 @@ def __setitem__(self, key, value): """ if not isinstance(key, str): raise ValueError("Parameters must be set with a string key") - self.__setattr__(key, value) - - def __setattr__(self, key, value): - """ - Sets attribute depending on key. If key starts with an underscore, it is - assumed to be an internal attribute and is set normally. Otherwise, we - infer its age-varying dimensions and set it as an attribute. - - Parameters - ---------- - key : str - Name of parameter - value : Any - Value of parameter - """ - if key.startswith("_"): - # Handle setting internal attributes normally - super().__setattr__(key, value) - else: - # Handle setting parameters as dictionary items - new_value = self.__infer_dims__(key, value) - super().__setattr__(key, new_value) + self._parameters[key] = value def keys(self): """ @@ -206,13 +158,13 @@ def values(self): """ Returns a list of the values of the parameters. """ - return [getattr(self, key) for key in self.keys()] + return [self._parameters[key] for key in self.keys()] def items(self): """ Returns a list of tuples of the form (name, value) for each parameter. """ - return [(key, getattr(self, key)) for key in self.keys()] + return [(key, self._parameters[key]) for key in self.keys()] def __iter__(self): """ @@ -230,7 +182,7 @@ def to_dict(self): """ Returns a dictionary of the parameters. """ - return {key: getattr(self, key) for key in self.keys()} + return {key: self._parameters[key] for key in self.keys()} def to_namedtuple(self): """ @@ -250,10 +202,10 @@ def update(self, other_params): """ if isinstance(other_params, Parameters): for key, value in other_params: - setattr(self, key, value) + self._parameters[key] = value elif isinstance(other_params, dict): for key, value in other_params.items(): - setattr(self, key, value) + self._parameters[key] = value else: raise ValueError("Parameters must be a dict or a Parameters object") @@ -267,7 +219,7 @@ def __repr__(self): """ Returns a detailed string representation of the Parameters object. """ - return f"Parameters( _age_inv = {str(self._age_inv)}, _age_var = {str(self._age_var)}, | {str(self.to_dict())})" + return f"Parameters( _age_inv = {self._age_inv}, _age_var = {self._age_var}, | {self.to_dict()})" class Model: @@ -294,7 +246,7 @@ def assign_parameters(self, **kwds): """ self.parameters.update(kwds) for key, value in kwds.items(): - setattr(self, key, value) + self._parameters[key] = value def get_parameter(self, name): """ From 0cfc729d405d3638e8ae92ad824bd26c9e41672f Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 24 Jul 2023 11:43:46 -0400 Subject: [PATCH 12/18] add basic tests --- HARK/tests/test_core.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/HARK/tests/test_core.py b/HARK/tests/test_core.py index 6b61a80f8..87d91919d 100644 --- a/HARK/tests/test_core.py +++ b/HARK/tests/test_core.py @@ -9,7 +9,7 @@ IndShockConsumerType, init_idiosyncratic_shocks, ) -from HARK.core import AgentPopulation, AgentType, distribute_params +from HARK.core import AgentPopulation, AgentType, Parameters, distribute_params from HARK.distribution import Uniform from HARK.metric import MetricObject, distance_metric @@ -168,3 +168,27 @@ def test_create_agents(self): self.agent_pop.create_distributed_agents() self.assertEqual(len(self.agent_pop.agents), 12) + + +class test_parameters(unittest.TestCase): + def setUp(self): + self.params = Parameters(T_cycle=3, a=1, b=[2, 3, 4], c=np.array([5, 6, 7])) + + def test_init(self): + self.assertEqual(self.params._term_age, 3) + self.assertEqual(self.params._age_inv, {"a", "c"}) + self.assertEqual(self.params._age_var, {"b"}) + + def test_getitem(self): + self.assertEqual(self.params["a"], 1) + self.assertEqual(self.params[0]["b"], 2) + self.assertEqual(self.params["c"][1], 6) + + def test_setitem(self): + self.params["d"] = 8 + self.assertEqual(self.params["d"], 8) + + def test_update(self): + self.params.update({"a": 9, "b": [10, 11, 12]}) + self.assertEqual(self.params["a"], 9) + self.assertEqual(self.params[0]["b"], 10) From 42377c6052765645d769dc1caccbf383028f91b8 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 24 Jul 2023 13:00:33 -0400 Subject: [PATCH 13/18] reverse model --- HARK/core.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/HARK/core.py b/HARK/core.py index 7ce1d21df..dc316c0bb 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -227,8 +227,9 @@ class Model: A class with special handling of parameters assignment. """ - def __init__(self, **kwds): - self.parameters = Parameters(**kwds) + def __init__(self): + if not hasattr(self, "parameters"): + self.parameters = {} def assign_parameters(self, **kwds): """ @@ -245,8 +246,8 @@ def assign_parameters(self, **kwds): none """ self.parameters.update(kwds) - for key, value in kwds.items(): - self._parameters[key] = value + for key in kwds: + setattr(self, key, kwds[key]) def get_parameter(self, name): """ @@ -266,7 +267,7 @@ def get_parameter(self, name): def __eq__(self, other): if isinstance(other, type(self)): - return self.parameters.to_dict() == other.parameters.to_dict() + return self.parameters == other.parameters return NotImplemented @@ -275,14 +276,14 @@ def __str__(self): module = type_.__module__ qualname = type_.__qualname__ - string = f"<{module}.{qualname} object at {hex(id(self))}.\n" - string += "Parameters:" + s = f"<{module}.{qualname} object at {hex(id(self))}.\n" + s += "Parameters:" for p in self.parameters: - string += f"\n{p}: {self.parameters[p]}" + s += f"\n{p}: {self.parameters[p]}" - string += ">" - return string + s += ">" + return s def __repr__(self): return self.__str__() From 256fe9af3780963e4391316133df7fd9f3266f1a Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 24 Jul 2023 13:15:34 -0400 Subject: [PATCH 14/18] make dict copy --- HARK/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HARK/core.py b/HARK/core.py index dc316c0bb..0bc749908 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -57,7 +57,8 @@ def __init__(self, **parameters): Any number of keyword arguments of the form key=value. To parse a dictionary of parameters, use the ** operator. """ - self._term_age = parameters.pop("T_cycle", None) + params = parameters.copy() + self._term_age = params.pop("T_cycle", None) self._age_inv = set() self._age_var = set() self._parameters = {} From c7ce56210054d11ca1234d2b00fefd897aea14e6 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 24 Jul 2023 13:26:11 -0400 Subject: [PATCH 15/18] fix failing test --- HARK/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HARK/core.py b/HARK/core.py index 0bc749908..2c13d22fc 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -63,7 +63,7 @@ def __init__(self, **parameters): self._age_var = set() self._parameters = {} - for key, value in parameters.items(): + for key, value in params.items(): self._parameters[key] = self.__infer_dims__(key, value) def __infer_dims__(self, key, value): From 03c49fdae4cbcdfddeca021ac5c49e083ff7e399 Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Mon, 7 Aug 2023 16:39:22 -0400 Subject: [PATCH 16/18] Delete Quick_start_with_solution.py --- .../Quick_start_with_solution.py | 799 ------------------ 1 file changed, 799 deletions(-) delete mode 100644 examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py diff --git a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py b/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py deleted file mode 100644 index d621e9ff6..000000000 --- a/examples/Journeys/Quickstart_tutorial/Quick_start_with_solution.py +++ /dev/null @@ -1,799 +0,0 @@ -# --- -# jupyter: -# jupytext: -# formats: ipynb,py:percent -# notebook_metadata_filter: all -# text_representation: -# extension: .py -# format_name: percent -# format_version: '1.3' -# jupytext_version: 1.14.4 -# kernelspec: -# display_name: Python 3 (ipykernel) -# language: python -# name: python3 -# language_info: -# codemirror_mode: -# name: ipython -# version: 3 -# file_extension: .py -# mimetype: text/x-python -# name: python -# nbconvert_exporter: python -# pygments_lexer: ipython3 -# version: 3.10.8 -# --- - -# %% [markdown] solution="shown" -# Quickstart tutorial -# ====== -# -# ## Summary -# -# This notebook provides the basics of the microeconomic, agent-type class which is fundamental for HARK. -# -# ____ -# ### Structure: -# -# - **Part 1**: basics of the perfect-foresight agent model -# - **Part 2**: more advanced methods for the perfect-foresight agent model -# -# ### Learning outcomes: -# - **Part 1**: -# - Learn how to declare basic agent-type objects -# - Learn solution methods for the agent-type objects -# - Plot value function and consumption function -# - Learn how to simulate the agent-type objects -# - Plot value function -# - **Part 2**: -# - Learn how to build life-cycle models -# - Learn more advanced simulation techniques -# - Learn advanced plots -# ____ -# ## Introduction to the consumer problem -# -# HARK AgentType classes were designed to solve the consumer problem. -# -# In the most basic formulation, the consumer problem is given as follows. The consumer lives T+1 periods (T $\leq \infty$) and during her lifetime receives the same income $Y$. In each period t (0$\leq$ t$\leq$ T) she can spend it on the consumption $C_t$ or invest in an asset $A_t$ with risk free interest rate R. She maximize the lifetime utility, by solving the following Bellman equation defined on the "cash in hand" state space $M_t = C_t +A_t$: -# -# For $t -# -# Obviously, HARK was designed to solve much more complicated consumer problems. However, it was written in the object programming paradigma (OPP). Thus, the class designed to solve such basic problem: $\texttt{PerfForesightConsumerType}$ is then a foundation (parent/subclass in the OPP language) for the more advanced classes with the heterogeneous agents. In the diagram you can observe the inheritance between some of the HARK Agent-type classes: -# -# -# As you can observe, the $\texttt{AgentType}$ superclass is the most general type of framework for the microeconomic models implemented in HARK. The child/subclass of $\texttt{AgentType}$ is $\texttt{PerfForesightConsumerType}$. There, you will need to define parameters, or **attributes** in OPP (such as $T$, $\beta$, and so on). Next, there are classes which inherit those attributes, and further incorporate the heterogeneity of agents. -# -# In these classes, you will need to *additionally* define parameters of the heterogeneity you wish to model (idiosyncratic shocks to income and aggregate productivity shocks are two common examples). Moreover, **methods** (which define how the object is created, how the solution is presented, etc.) of the subclasses are the same or modified methods of the parent class. -# -# Therefore, to master the basics of HARK microclass you will first need to learn the $\texttt{PerfForesightConsumerType}$ class. Consequently, this tutorial aims to teach you this. However, the majority of the presented methods are general for the HARK agent-type objects (though it may involve assigning more parameters). -# -# In the next notebooks, the class $\texttt{IndShockConsumerType}$ with idiosyncratic income shocks is a more specific example of using the HARK microclass. -# - -# %% [markdown] -# # Part I: Basics of the perfect foresight model -# -# In this part, you learn basics of the perfect foresight model. We will solve the example of the consumer problem presented in the introduction. - -# %% [markdown] -# ## Getting started -# First, you need to import HARK and a few additional libraries. Importantly, to use $\texttt{PerfForesightConsumerType}$ you also need to import HARK.ConsumptionSaving.ConsIndShockModel sublibrary. - -# %% - -# import sys -# import os -# sys.path.insert(0, os.path.abspath('../../../.')) -import matplotlib.pyplot as plt -import numpy as np -import HARK - -from copy import deepcopy -from HARK.ConsumptionSaving.ConsIndShockModel import * -from HARK.utilities import plot_funcs_der, plot_funcs - - -# %% [markdown] -# ## Agent-type object creation -# The most basic way of creating HARK object is to call its constructor (in OPP method which create the object, called by the class name). -# -# For $\texttt{PerfForesightConsumerType}$ we need to set: -# - $T+1$: a consumer's lifespan, called $\texttt{cycles}$ in the code, if $T= \infty$, set $\texttt{cycles}$=0. -# - $R$: risk free intrest rate, called $\texttt{Rfree}$ in the code. -# - $\beta$: a discount factor, $\texttt{DiscFac}$ in the code. -# - $\rho$: CRRA utility function parameter, $\texttt{CRRA}$ in the code. -# -# Additionally, you need to define two parameters which do not occur in the presented example, but nevertheless can be useful: -# -# - Probability of surviving to the next period, called $\texttt{LivPrb}$ in the code. -# - Income $Y$ growth factor, $\texttt{PermGroFac}$ in the code. -# -# We call our first HARK object **Example_agent_1** and set the example values of the parameters. - -# %% -Example_agent_1 = PerfForesightConsumerType( - cycles=0, CRRA=2.0, Rfree=1.03, DiscFac=0.99, LivPrb=1.0, PermGroFac=1.0 -) - - -# %% [markdown] -# Because we did not assume growth in 𝑌 or survival uncertainty , we set these values to 1. - -# %% [markdown] -# The second method involves creating a **dictionary**: a list of parameters' names and values. Here we define the dictionary with the same values as in the first example. - -# %% -First_dictionary = { - "CRRA": 2.0, - "DiscFac": 0.99, - "Rfree": 1.03, - "cycles": 0, - "LivPrb": [1.00], - "PermGroFac": [1.00], -} - -# %% [markdown] -# To create an object with a dictionary, use the constructor with the previously defined dictionary as an argument: -# - -# %% -Example_agent_2 = PerfForesightConsumerType(**First_dictionary) - -# %% [markdown] -# Although the first method is easier, we recommend defining a dictionary whenever you create a HARK object. First, it makes your code cleaner. Second, it enables you to create multiple objects with the same dictionary (the importantance of which will become apparent as we move on to creating macro classes). -# - -# %% [markdown] -# The presented here methods work also for the more sophisticated HARK object (however you will need to specify more parameters). - -# %% [markdown] -# ### Creating an agent-type object by copy -# -# Once creating an agent-type object, you can use its set of parameters to create another. To do so you need to use **deepcopy** method from copy package. - -# %% -Example_agent_3 = deepcopy(Example_agent_2) - -# %% [markdown] -# Note: **Do not** only use an assignment operator (=) because it does not create new object. For example, a command such as: - -# %% -Example_agent_4 = Example_agent_2 - -# %% [markdown] -# does not create a new object. It will only gives a new name to the object Example_agent_2 (this gives a single agent object both names Example_agent_2 and Example_agent_4). - -# %% [markdown] -# ### Modifying parameter values -# -# You can easily change the parameter value of the object by "." operator. -# -# For example, to change the discount factor value of the object created in the previous subsection: -# - -# %% -Example_agent_3.DiscFac = 0.95 - -# %% [markdown] -# ## Solving an agent-type problems -# -# To solve agent type problems such as the on presented in the example, you need to find a **value function** from the Bellman equations and **the policy functions**. In our case, the only policy function is a consumption function: a function that for each age t and cash-in-hand $M_t$, specify the optimal consumption level: $c_t(M_t)$. -# -# To solve a model in HARK, you need to use $\texttt{solve}$ method. For example, if we want to solve the model with parameters of the object Example_agent_2: - -# %% -Example_agent_2.solve() - -# %% [markdown] -# ### Solution elements -# -# `Solve` method finds the value function and consumption function for each period t of the consumer's life (in case of the infinite T, it specifies only one set of functions; because all the parameters are stable and lifespan is always infinite, the functions are the same for each $t$). -# -# Besides consumption and value functions, `solve` method create also a few attributes, the most important is minimal cash-in-hand value for which the problem has a solution. -# -# The exact name of these attributes in HARK are: -# -# - vFunc: value function -# - cFunc: consumption function -# - mNrmMin: Minimum value of $M_t$ such that cFunc and vFunc are defined. -# -# To get access to the value/consumption function you need to specify the period t and the object name, using two times operator. So to get access to the value function, consumption function and mNrmMin for the solved example: -# -# - -# %% -Example_agent_2.solution[0].vFunc -Example_agent_2.solution[0].cFunc -Example_agent_2.solution[0].mNrmMin - - -# %% [markdown] -# As you can see, only mNrmMin can be printed as a value. However, the value and consumption functions can be plotted. -# - -# %% [markdown] -# ### Plotting the solution -# -# After $\texttt{solve}$ method is used, the value and consumption functions can be plotted. HARK dedicated function for doing so is `plot_funcs`. As arguments, you need to give a function from the solution (possible a few functions) and the limits of interval for which you want to make a plot. -# -# For example, we can plot consumption and value functions on the interval from mNrmMin to -mNrmMin. -# - -# %% -min_v = Example_agent_2.solution[0].mNrmMin -max_v = -Example_agent_2.solution[0].mNrmMin -print("Consumption function") -plot_funcs([Example_agent_2.solution[0].cFunc], min_v, max_v) -print("Value function") -plot_funcs([Example_agent_2.solution[0].vFunc], min_v, max_v) - -# %% [markdown] -# ## Simulation -# -# Next step is to simulate the agent behavior. To do so, you first need to set a few parameters for the sake of the simulation: -# -# - $\texttt{AgentCount}$: number of simulated agents -# - $\texttt{T_cycle}$: logical parameter which governs the time flow during the simulation (if it is moving forward or backward) -# - $\texttt{T_sim}$: number of simulation periods -# - $\texttt{T_age}$: Age after which simulated agents die with certainty -# -# Moreover, HARK enables simulation of the model with the log-normal distributions of the initial assets and incomes. You need to set the parameters: -# -# - $\texttt{aNrmInitMean}$: Mean of log initial assets -# - $\texttt{aNrmInitStd}$: Standard deviation of log initial assets -# - $\texttt{pLvlInitMean}$: Mean of log initial permanent income -# - $\texttt{pLvlInitStd}$: Standard deviation of log initial permanent income -# -# Lastly, using HARK agent type class, you can also set the aggregate income increase (so that the rate of the income increase is common to all agents). You may then set a parameter: -# -# - $\texttt{PermGroFacAgg}$: Aggregate permanent income growth factor -# -# In our example, we simulate 1 agent, as it is a representative agent model. Time flow is chronological and there is no initial heterogeneity. Thus, std of the initial assets and income distributions are set to 0. The initial assets and income are set to 1.0. There is no aggregate income increase, so we set the income growth factor to 1. We simulate 1000 periods and assume an infinitely lived agent. -# -# To declare the values of these parameters, we create a new dictionary: - -# %% -Simulation_dictionary = { - "AgentCount": 1, - "aNrmInitMean": 0.0, - "aNrmInitStd": 0.0, - "pLvlInitMean": 0.0, - "pLvlInitStd": 0.0, - "PermGroFacAgg": 1.0, - "T_cycle": 1, - "T_sim": 1000, - "T_age": None, -} - - -# %% [markdown] -# Next, you need to update the object. To do so we use **setattr** function, which adds the parameters' values to the defined agent object. - -# %% -for key, value in Simulation_dictionary.items(): - setattr(Example_agent_2, key, value) - -# %% [markdown] -# Finally, you can start our simulation. First, you need to decide which variables you want to track, we choose an assets level and consumption level, in the code they are called: $\texttt{aNrmNow}$ and $\texttt{cNrmNow}$. Next, you need to initialize the simulation by $\texttt{initialize_sim}$ method. Lastly, run the simulation with the $\texttt{simulate}$ method. - -# %% -Example_agent_2.track_vars = [ - "aNrm", - "cNrm", -] # should these be 'aLvl, cLvl' since the normalized versions of these variables isn't introduced until the next section? -Example_agent_2.initialize_sim() -Example_agent_2.simulate() - -# %% [markdown] -# ## Plotting the simulation -# -# Plotting the simulation is a little bit more complicated than plotting the solution, as you cannot use a dedicated function. Instead, we will use the **matplot** library in the following way. -# -# To see the consumption and asset history, we can use objects created by the simulation which contain the history of every agent in each of the simulation periods. These objects have the same naming as the tracked variables with a **\_hist** ending. Thus, from the previous example, the history of assets and consumption are called $\texttt{aNrmNow_hist}$ and $\texttt{cNrmNow_hist}$. -# -# Let's make a plot of the assets level and consumption level during the simulated periods. First, define the vectors of mean assets and consumption. Here, there is only one consumer, so we do not need to use a mean function (although it is done so here). However, if you want to plot the mean asset/consumption level for many agents, you will need to use this method. -# - -# %% -periods = np.linspace(0, 1000, 1000) -asset_level = np.mean(Example_agent_2.history["aNrm"][0:1000], axis=1) -cons_level = np.mean(Example_agent_2.history["cNrm"][0:1000], axis=1) - -plt.figure(figsize=(5, 5)) -plt.plot(periods, asset_level, label="Assets level") -plt.plot(periods, cons_level, label="Consumption level") -plt.legend(loc=2) -plt.show() - -# %% [markdown] -# Now, let's plot the mean asset and consumption increase: - -# %% -increase_assets = asset_level[1:1000] / asset_level[0:999] -increase_cons = cons_level[1:1000] / cons_level[0:999] -plt.figure(figsize=(5, 5)) -plt.plot(periods[1:1000], increase_assets, label="Assets increase") -plt.plot(periods[1:1000], increase_cons, label="Consumption increase") -plt.legend(loc=2) -plt.show() - -# %% [markdown] -# ## Exercise -# -# Congratulations! You've just learned the basics of the agent-type class in HARK. It is time for some exercises: -# -# - -# %% [markdown] -# ### Exercise 1: create the agent-type object -# -# Define a dictionary and then use it to create the agent-type object with the parameters: -# -# - $\beta = 0.96$ -# - $\rho = 2.0$ -# - $T = \infty$ -# - Risk free interest rate $R= 1.05$ -# Assume no survival uncertainty and income growth factor 1.01 -# - -# %% -# Write your solution here - -# fill the dictionary and then use it to create the object - -# First_dictionary = { -# 'CRRA' : , -# 'DiscFac' : , -# 'Rfree' : , -# 'cycles' : , -# 'LivPrb' : [], -# 'PermGroFac' : [], -# } -# Exercise_agent = - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -# Solution -First_dictionary = { - "CRRA": 2.0, - "DiscFac": 0.96, - "Rfree": 1.05, - "cycles": 0, - "LivPrb": [1.0], - "PermGroFac": [1.0], -} -Exercise_agent = PerfForesightConsumerType(**First_dictionary) - -# %% [markdown] -# ### Exercise 2: Solve the model and plot the value function -# -# - -# %% -# Write your solution here, use methods from "solving the model" subsection - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -# Solution -Exercise_agent.solve() - -min_v = Exercise_agent.solution[0].mNrmMin -max_v = -Exercise_agent.solution[0].mNrmMin -print("Value function") -plot_funcs([Exercise_agent.solution[0].vFunc], min_v, max_v) - -# %% [markdown] -# ### Exercise 3: Prepare the simulation -# -# Next prepare the simulation. Assume that **there exsists the initial assets and income heterogenity**. Assume, the initial income and assets distributions are log-normal, have mean 1 and std 1. Simulate 1000 agents for 1000 periods. -# -# Add the new parameters to the object: - -# %% -# Write your solution here. - -# Fill the dictionary -# Simulation_dictionary = { 'AgentCount': , -# 'aNrmInitMean' : , -# 'aNrmInitStd' : , -# 'pLvlInitMean' : , -# 'pLvlInitStd' : , -# 'PermGroFacAgg' : 1.0, #assume no income aggregate growth -# 'T_cycle' : 1, #assume forward time flow -# 'T_sim' : , -# 'T_age' : None #assume immortal agents -# } - -# for key,value in Simulation_dictionary.items(): -# setattr(Exercise_agent,key,value) - - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -# Solution -Simulation_dictionary = { - "AgentCount": 1000, - "aNrmInitMean": 1.0, - "aNrmInitStd": 1.0, - "pLvlInitMean": 1.0, - "pLvlInitStd": 1.0, - "PermGroFacAgg": 1.0, - "T_cycle": 1, - "T_sim": 1000, - "T_age": None, -} - -for key, value in Simulation_dictionary.items(): - setattr(Exercise_agent, key, value) - -# %% [markdown] -# ### Exercise 4: Simulate -# -# - -# %% -# Write your solution here. Use the commands from "simulation" subsection, track consumption values - - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -# Solution -Exercise_agent.track_vars = ["aNrm", "cNrm"] -Exercise_agent.initialize_sim() -Exercise_agent.simulate() - -# %% [markdown] solution="hidden" -# ### Exercise 5: Plot the simulations -# -# Plot mean consumption level and consumption increase: - -# %% -# Write your solution here. - -# Firstly prepare the vectors which you would like to plot: -# periods= np.linspace(0,1000,1000) -# cons_level = np.mean(Exercise_agent.cNrmNow_hist[0:1000], axis = 1) -# increase_cons = cons_level[1:1000]/cons_level[0:999] - -# next plot your solution - - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -# Solution -periods = np.linspace(0, 1000, 1000) -cons_level = np.mean(Exercise_agent.history["cNrm"][0:1000], axis=1) -increase_cons = cons_level[1:1000] / cons_level[0:999] - -plt.figure(figsize=(5, 5)) -plt.plot(periods, cons_level, label="Consumption level") -plt.legend(loc=2) -plt.show() - -plt.figure(figsize=(5, 5)) -plt.plot(periods[1:1000], increase_cons, label="Consumption increase") -plt.legend(loc=2) -plt.show() - - -# %% [markdown] -# # PART II: advanced methods for the perfect foresight agent -# -# In this part we focus on more complicated cases of the deterministic agent model. -# -# In the previous example survival probability (in the code **LivPrb**) and income increase factor (in the code **PermGroFac**) were stable and set to 1. However, if you want to build deterministic life-cycle model you need to add a age-dependent survival probability or income growth. -# -# Consumer problem in this setting is: -# -# \begin{eqnarray*} -# V_t(M_t,Y_t) &=& \max_{C_t}~U(C_t) + \beta \pi_t V_{t+1}(M_{t+1},Y_{t+1}), \\ -# & s.t. & \\ -# %A_t &=& M_t - C_t, \\ -# M_{t+1} &=& R (M_{t}-C_{t}) + Y_{t+1}, \\ -# Y_{t+1} &=& \Gamma_{t+1} Y_t, \\ -# \end{eqnarray*} -# -# Where $Y_t$ is an age-dependent income, $\pi_t$ is a survival probability and $\Gamma_{t+1}$ is an income growth rate. Also $\pi_{T+1} =0$ -# -# While it does not reduce the computational complexity of the problem (as permanent income is deterministic, given its initial condition $Y_0$), HARK represents this problem with normalized variables (represented in lower case), dividing all real variables by permanent income $Y_t$ and utility levels by $Y_t^{1-\rho}$. The Bellman form of the model thus reduces to: -# -# \begin{eqnarray*} -# v_t(m_t) &=& \max_{c_t}~U(c_t) ~+ \beta_{t+1}\pi_{t+1} \Gamma_{t+1}^{1-\rho} v_{t+1}(m_{t+1}), \\ -# & s.t. & \\ -# a_t &=& m_t - c_t, \\ -# m_{t+1} &=& R / \Gamma_{t+1} a_t + 1. -# \end{eqnarray*} -# -# To solve this problem we need to study the **cycles** parameter more carefully. There is a notebook dedicated to solving and simulating life-cycle models which can be found here: [Cycles_tutorial](https://github.com/econ-ark/HARK/blob/master/examples/LifecycleModel/Cycles_tutorial.ipynb). -# - -# %% [markdown] -# ### Methods of plotting the solution -# -# $\texttt{plot_funcs()}$ enables to plot many functions at the same graph. You need to declare them as vector of functions. -# -# To see this, just follow an example. We plot the consumption functions for each age $t$ of the consumer. -# -# To get better access to the consumption functions, you can use $\texttt{unpack('cFunc')}$ method, which will create the attribute $\texttt{cFunc}$ of the object (so you do not have to use it is as a solution attribute). -# -# We illustrate this with the solution for "Exercise_agent_3". Recall that this agent was given a different time preference value ($\beta = .95$). Here, we also changed the length of the life-cycle of this agent to $10$ periods. - -# %% -Example_agent_3.cycles = 1 -Example_agent_3.LivPrb = [0.99, 0.98, 0.97, 0.96, 0.95, 0.94, 0.93, 0.92, 0.91, 0.90] -Example_agent_3.PermGroFac = [1.01, 1.01, 1.01, 1.02, 1.00, 0.99, 0.5, 1.0, 1.0, 1.0] - - -Example_agent_3.solve() -Example_agent_3.unpack("cFunc") - -# %% [markdown] -# Next, we set the minimal value of the gird such that at least one of the consumption functions is defined. - -# %% -min_v = min(Example_agent_3.solution[t].mNrmMin for t in range(11)) -max_v = -min_v -print("Consumption functions") -plot_funcs(Example_agent_3.cFunc[:], min_v, max_v) - -# %% [markdown] -# If you want to compare a few functions (eg. value functions), you can also construct the vector by yourself, for example: -# - -# %% -print("Value functions") -plot_funcs( - [ - Example_agent_3.solution[0].vFunc, - Example_agent_3.solution[5].vFunc, - Example_agent_3.solution[9].vFunc, - ], - min_v, - max_v, -) - - -# %% [markdown] -# ## Advanced simulation techniques -# Here we present more advanced simulation techniques with the mortal agents and income dynamics. -# -# We will also present how to plot the distribution of assets among the agents. -# -# First, as in the part 1 of the tutorial, you need to define the simulation dictionary. However, you need to be careful with T_age parameter: because a maximal lifespan is 11 (T=10), T_age is set to 10, to ensure that all agents die after this age. -# -# For the rest of the parameters, we set the number of consumers alive in each period to 1000. Initial asset level is near 0 (log of -10). The initial income level is given by the log-normal distribution with mean 0 and std 1. We set the rest of parameters as in the previous example. -# - -# %% -Simulation_dictionary = { - "AgentCount": 1000, - "aNrmInitMean": -10.0, - "aNrmInitStd": 0.0, - "pLvlInitMean": 0.0, - "pLvlInitStd": 1.0, - "PermGroFacAgg": 1.0, - "T_cycle": 1, - "T_sim": 200, - "T_age": 10, -} - -for key, value in Simulation_dictionary.items(): - setattr(Example_agent_3, key, value) - -# %% [markdown] -# Next, we simulate the economy and plot the mean asset level. However, be careful! $\texttt{aNrmNow}$ gives the asset levels normalized by the income. To get the original asset level we need to use $\texttt{aLvlNow}$ (unfortunately, cLvlNow is not implemented). - -# %% -Example_agent_3.track_vars = ["aNrm", "cNrm", "aLvl"] -Example_agent_3.initialize_sim() -Example_agent_3.simulate() - - -periods = np.linspace(0, 200, 200) -assets_level = np.mean(Example_agent_3.history["aLvl"][0:200], axis=1) - -plt.figure(figsize=(5, 5)) -plt.plot(periods, assets_level, label="assets level") -plt.legend(loc=2) -plt.show() - -# %% [markdown] -# As you can see, for the first 10 periods the asset level much more fluctuate. It is because in the first periods the agents which were born in period 0 strictly dominate the population (as only a small fraction die in the first periods of life). -# -# You can simply cut the first observations, to get asset levels for more balanced population. - -# %% -after_burnout = np.mean(Example_agent_3.history["aLvl"][10:200], axis=1) - -plt.figure(figsize=(5, 5)) -plt.plot(periods[10:200], after_burnout, label="assets level") -plt.legend(loc=2) -plt.show() - -# %% [markdown] -# ### Plotting the distribution of assets -# -# When you plot similar simulations, often the main interest is not to get exact assets/consumption levels during the simulation but rather a general distribution of assets. -# -# In our case, we plot the asset distribution. -# -# First, get one vector of the asset levels: -# - -# %% -sim_wealth = np.reshape(Example_agent_3.history["aLvl"], -1) - -# %% [markdown] -# Next, we plot simple histogram of assets level using a standard **hist** function from matplotlib library - -# %% -print("Wealth distribution histogram") -n, bins, patches = plt.hist(sim_wealth, 100, density=True, range=[0.0, 10.0]) - -# %% [markdown] -# With HARK, you can also easily plot the Lorenz curve. To do so import some HARK utilities which help us plot Lorenz curve: - -# %% - -from HARK.utilities import get_lorenz_shares, get_percentiles - - -# %% [markdown] -# Then, use $\texttt{get_lorenz_shares}$ to plot the Lornez curve. - -# %% -pctiles = np.linspace(0.001, 0.999, 15) -# SCF_Lorenz_points = get_lorenz_shares(SCF_wealth,weights=SCF_weights,percentiles=pctiles) -sim_Lorenz_points = get_lorenz_shares(sim_wealth, percentiles=pctiles) - -plt.figure(figsize=(5, 5)) -plt.title("Lorenz curve") -plt.plot(pctiles, sim_Lorenz_points, "-b", label="Lorenz curve") -plt.plot(pctiles, pctiles, "g-.", label="45 Degree") -plt.xlabel("Percentile of net worth") -plt.ylabel("Cumulative share of wealth") -plt.legend(loc=2) -plt.ylim([0, 1]) -plt.show() - -# %% [markdown] -# ## Exercise -# -# Let's make a model with a little more realistic assumptions. -# -# In files 'life_table.csv' you find the death-probablities for Americans in age 25-105 in 2017 from Human Mortality Database. The age-dependent income for American males in file 'productivity_profile.csv' are deduced from Heathcote et al. (2010). Try to build a model with this data, assuming additionaly CRRA parameter to be 2.0, discount rate set to 0.99 and interest rate set to 1.05. Moreover, assume that initial income is given by log-normal distribution with mean 0 and std 0.05. Assume that initial asset is near 0 for all agents. -# -# Do the following tasks: -# - Build a dictionary and create an object with a given data and parameters -# - Solve model and plot a consumption functions for each age -# - Simulate 1000 agents for 2000 periods -# - Plot a histogram of the assets distribution and the Lorenz curve - -# %% solution="hidden" -# Write your solution here - -# Firstly import data, you can use this part of code (however, there are other ways to do this) -import sys -import os - -sys.path.insert(0, os.path.abspath("..")) - - -prob_dead = np.genfromtxt("life_table.csv", delimiter=",", skip_header=1) -prob_surv = 1 - prob_dead - -# The HARK argument need to be a list, thus convert it from numpy array -prob_surv_list = np.ndarray.tolist(prob_surv[:79]) - -income_profile = np.genfromtxt("productivity_profile.csv", delimiter=",", skip_header=1) -income_profile_list = np.ndarray.tolist(income_profile[:79]) - -# Continue your solution - - -# %% [markdown] solution="hidden" solution_first=true -# **Solution**: click on the box on the left to expand the solution - -# %% solution="hidden" -import sys -import os - -sys.path.insert(0, os.path.abspath("..")) - - -prob_dead = np.genfromtxt("life_table.csv", delimiter=",", skip_header=1) -prob_surv = 1 - prob_dead -prob_surv_list = np.ndarray.tolist(prob_surv[:79]) - -income_profile = np.genfromtxt("productivity_profile.csv", delimiter=",", skip_header=1) -income_profile_list = np.ndarray.tolist(income_profile[:79]) - -Ex_dictionary = { - "CRRA": 2.0, - "Rfree": 1.05, - "DiscFac": 0.99, - "LivPrb": prob_surv_list, - "PermGroFac": income_profile_list, - "cycles": 1, - "T_cycle": 79, -} - -Ex_agent = PerfForesightConsumerType(**Ex_dictionary) -Ex_agent.solve() - -Ex_agent.unpack("cFunc") - -min_v = min(Ex_agent.solution[t].mNrmMin for t in range(11)) -max_v = -min_v -print("Consumption functions") -plot_funcs(Ex_agent.cFunc[:], min_v, max_v) - - -Simulation_dictionary = { - "AgentCount": 1000, - "aNrmInitMean": -10.0, - "aNrmInitStd": 0.0, - "pLvlInitMean": 0.0, - "pLvlInitStd": 0.05, - "PermGroFacAgg": 1.0, - "T_cycle": 79, - "T_sim": 2000, - "T_age": 80, - "BoroCnstArt": 0.0, -} - -for key, value in Simulation_dictionary.items(): - setattr(Ex_agent, key, value) - -Ex_agent.track_vars = ["aNrm", "cNrm", "aLvl"] -Ex_agent.initialize_sim() -Ex_agent.simulate() - - -sim_wealth = np.reshape(Ex_agent.history["aLvl"], -1) -print("Wealth distribution histogram") -n, bins, patches = plt.hist(sim_wealth, 50, density=True, range=[-1.0, 2.0]) - -pctiles = np.linspace(0.001, 0.999, 15) -# SCF_Lorenz_points = get_lorenz_shares(SCF_wealth,weights=SCF_weights,percentiles=pctiles) -sim_Lorenz_points = get_lorenz_shares(sim_wealth, percentiles=pctiles) - -plt.figure(figsize=(5, 5)) -plt.title("Lorenz curve") -plt.plot(pctiles, sim_Lorenz_points, "-b", label="Lorenz curve") -plt.plot(pctiles, pctiles, "g-.", label="45 Degree") -plt.xlabel("Percentile of net worth") -plt.ylabel("Cumulative share of wealth") -plt.legend(loc=2) -plt.ylim([0, 1]) -plt.show() From 38ccc75d3d93d91221a9532bc505169ca1fda4ce Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Tue, 15 Aug 2023 16:08:37 -0400 Subject: [PATCH 17/18] update tests --- .pre-commit-config.yaml | 10 ++--- HARK/core.py | 96 ++++++++++++++++++++++------------------- HARK/tests/test_core.py | 77 +++++++++++++++++++++++++++++++-- 3 files changed, 130 insertions(+), 53 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edb768ec5..1acc0a54e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: Documentation/example_notebooks/ repos: - repo: https://github.com/mwouts/jupytext - rev: v1.14.5 + rev: v1.15.0 hooks: - id: jupytext args: [--sync, --set-formats, "ipynb", --pipe, black, --execute] @@ -10,20 +10,20 @@ repos: files: ^examples/.*\.ipynb$ - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black exclude: ^examples/ - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.10.1 hooks: - id: pyupgrade args: ["--py38-plus"] exclude: ^examples/ - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.15.0 hooks: - id: blacken-docs exclude: ^examples/ @@ -37,7 +37,7 @@ repos: exclude: ^examples/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.9-for-vscode + rev: v3.0.1 hooks: - id: prettier exclude: ^examples/ diff --git a/HARK/core.py b/HARK/core.py index 2c13d22fc..6ae9623da 100644 --- a/HARK/core.py +++ b/HARK/core.py @@ -7,7 +7,7 @@ problem by finding a general equilibrium dynamic rule. """ import sys -from collections import namedtuple +from collections import defaultdict, namedtuple from copy import copy, deepcopy from dataclasses import dataclass, field from time import time @@ -37,15 +37,15 @@ class Parameters: Attributes ---------- - _term_age : int + _length : int The terminal age of the agents in the model. - _age_inv : list + _invariant_params : list A list of the names of the parameters that are invariant over time. - _age_var : list + _varying_params : list A list of the names of the parameters that vary over time. """ - def __init__(self, **parameters): + def __init__(self, **parameters: Any): """ Initializes a Parameters object and parses the age-varying dynamics of the parameters. @@ -58,15 +58,17 @@ def __init__(self, **parameters): To parse a dictionary of parameters, use the ** operator. """ params = parameters.copy() - self._term_age = params.pop("T_cycle", None) - self._age_inv = set() - self._age_var = set() - self._parameters = {} + self._length = params.pop("T_cycle", None) + self._invariant_params = set() + self._varying_params = set() + self._parameters: Dict[str, Union[int, float, np.ndarray, list, tuple]] = {} for key, value in params.items(): self._parameters[key] = self.__infer_dims__(key, value) - def __infer_dims__(self, key, value): + def __infer_dims__( + self, key: str, value: Union[int, float, np.ndarray, list, tuple, None] + ) -> Union[int, float, np.ndarray, list, tuple]: """ Infers the age-varying dimensions of a parameter. @@ -85,55 +87,61 @@ def __infer_dims__(self, key, value): """ if isinstance(value, (int, float, np.ndarray, type(None))): - self.__add_to_time_inv(key) + self.__add_to_invariant__(key) return value if isinstance(value, (list, tuple)): if len(value) == 1: - self.__add_to_time_inv(key) + self.__add_to_invariant__(key) return value[0] - if self._term_age is None or self._term_age == 1: - self._term_age = len(value) - if len(value) == self._term_age: - self.__add_to_time_vary(key) + if self._length is None or self._length == 1: + self._length = len(value) + if len(value) == self._length: + self.__add_to_varying__(key) return value - raise ValueError(f"Parameter {key} must be of length 1 or {self._term_age}") - raise ValueError(f"Parameter {key} has type {type(value)}") + raise ValueError( + f"Parameter {key} must be of length 1 or {self._length}, not {len(value)}" + ) + raise ValueError(f"Parameter {key} has unsupported type {type(value)}") - def __add_to_time_inv(self, key): + def __add_to_invariant__(self, key: str): """ Adds parameter name to invariant set and removes from varying set. """ - self._age_var.discard(key) - self._age_inv.add(key) + self._varying_params.discard(key) + self._invariant_params.add(key) - def __add_to_time_vary(self, key): + def __add_to_varying__(self, key: str): """ Adds parameter name to varying set and removes from invariant set. """ - self._age_inv.discard(key) - self._age_var.add(key) + self._invariant_params.discard(key) + self._varying_params.add(key) - def __getitem__(self, age_or_key): + def __getitem__(self, item_or_key: Union[int, str]): """ - If age_or_key is an integer, returns a Parameters object with the parameters + If item_or_key is an integer, returns a Parameters object with the parameters that apply to that age. This includes all invariant parameters and the - `age_or_key`th element of all age-varying parameters. If age_or_key is a string, + `item_or_key`th element of all age-varying parameters. If item_or_key is a string, it returns the value of the parameter with that name. """ - if isinstance(age_or_key, int): - if age_or_key >= self._term_age: - raise ValueError("Age is greater than or equal to terminal age.") + if isinstance(item_or_key, int): + if item_or_key >= self._length: + raise ValueError( + f"Age {item_or_key} is greater than or equal to terminal age {self._length}." + ) - params = {key: self._parameters[key] for key in self._age_inv} + params = {key: self._parameters[key] for key in self._invariant_params} params.update( - {key: self._parameters[key][age_or_key] for key in self._age_var} + { + key: self._parameters[key][item_or_key] + for key in self._varying_params + } ) return Parameters(**params) + elif isinstance(item_or_key, str): + return self._parameters[item_or_key] - elif isinstance(age_or_key, str): - return self._parameters[age_or_key] - - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any): """ Sets the value of a parameter. @@ -147,25 +155,25 @@ def __setitem__(self, key, value): """ if not isinstance(key, str): raise ValueError("Parameters must be set with a string key") - self._parameters[key] = value + self._parameters[key] = self.__infer_dims__(key, value) def keys(self): """ Returns a list of the names of the parameters. """ - return self._age_inv + self._age_var + return self._invariant_params | self._varying_params def values(self): """ Returns a list of the values of the parameters. """ - return [self._parameters[key] for key in self.keys()] + return list(self._parameters.values()) def items(self): """ Returns a list of tuples of the form (name, value) for each parameter. """ - return [(key, self._parameters[key]) for key in self.keys()] + return list(self._parameters.items()) def __iter__(self): """ @@ -202,11 +210,9 @@ def update(self, other_params): Parameters object or dictionary of parameters to update with. """ if isinstance(other_params, Parameters): - for key, value in other_params: - self._parameters[key] = value + self._parameters.update(other_params.to_dict()) elif isinstance(other_params, dict): - for key, value in other_params.items(): - self._parameters[key] = value + self._parameters.update(other_params) else: raise ValueError("Parameters must be a dict or a Parameters object") @@ -220,7 +226,7 @@ def __repr__(self): """ Returns a detailed string representation of the Parameters object. """ - return f"Parameters( _age_inv = {self._age_inv}, _age_var = {self._age_var}, | {self.to_dict()})" + return f"Parameters( _age_inv = {self._invariant_params}, _age_var = {self._varying_params}, | {self.to_dict()})" class Model: diff --git a/HARK/tests/test_core.py b/HARK/tests/test_core.py index 87d91919d..0cda93381 100644 --- a/HARK/tests/test_core.py +++ b/HARK/tests/test_core.py @@ -4,6 +4,7 @@ import unittest import numpy as np +import pytest from HARK.ConsumptionSaving.ConsIndShockModel import ( IndShockConsumerType, @@ -175,9 +176,9 @@ def setUp(self): self.params = Parameters(T_cycle=3, a=1, b=[2, 3, 4], c=np.array([5, 6, 7])) def test_init(self): - self.assertEqual(self.params._term_age, 3) - self.assertEqual(self.params._age_inv, {"a", "c"}) - self.assertEqual(self.params._age_var, {"b"}) + self.assertEqual(self.params._length, 3) + self.assertEqual(self.params._invariant_params, {"a", "c"}) + self.assertEqual(self.params._varying_params, {"b"}) def test_getitem(self): self.assertEqual(self.params["a"], 1) @@ -192,3 +193,73 @@ def test_update(self): self.params.update({"a": 9, "b": [10, 11, 12]}) self.assertEqual(self.params["a"], 9) self.assertEqual(self.params[0]["b"], 10) + + def test_initialization(self): + params = Parameters(a=1, b=[1, 2], T_cycle=2) + assert params._length == 2 + assert params._invariant_params == {"a"} + assert params._varying_params == {"b"} + + def test_infer_dims_scalar(self): + params = Parameters(a=1) + assert params["a"] == 1 + + def test_infer_dims_array(self): + params = Parameters(b=np.array([1, 2])) + assert all(params["b"] == np.array([1, 2])) + + def test_infer_dims_list_varying(self): + params = Parameters(b=[1, 2], T_cycle=2) + assert params["b"] == [1, 2] + + def test_infer_dims_list_invariant(self): + params = Parameters(b=[1]) + assert params["b"] == 1 + + def test_setitem(self): + params = Parameters(a=1) + params["b"] = 2 + assert params["b"] == 2 + + def test_keys_values_items(self): + params = Parameters(a=1, b=2) + assert set(params.keys()) == {"a", "b"} + assert set(params.values()) == {1, 2} + assert set(params.items()) == {("a", 1), ("b", 2)} + + def test_to_dict(self): + params = Parameters(a=1, b=2) + assert params.to_dict() == {"a": 1, "b": 2} + + def test_to_namedtuple(self): + params = Parameters(a=1, b=2) + named_tuple = params.to_namedtuple() + assert named_tuple.a == 1 + assert named_tuple.b == 2 + + def test_update_params(self): + params1 = Parameters(a=1, b=2) + params2 = Parameters(a=3, c=4) + params1.update(params2) + assert params1["a"] == 3 + assert params1["c"] == 4 + + def test_unsupported_type_error(self): + with pytest.raises(ValueError): + Parameters(b={1, 2}) + + def test_get_item_dimension_error(self): + params = Parameters(b=[1, 2], T_cycle=2) + with pytest.raises(ValueError): + params[2] + + def test_getitem_with_key(self): + params = Parameters(a=1, b=[2, 3], T_cycle=2) + assert params["a"] == 1 + assert params["b"] == [2, 3] + + def test_getitem_with_item(self): + params = Parameters(a=1, b=[2, 3], T_cycle=2) + age_params = params[1] + assert age_params["a"] == 1 + assert age_params["b"] == 3 From ceeb757d4fdf12e5dda899006f590f435373ef9e Mon Sep 17 00:00:00 2001 From: Alan Lujan Date: Wed, 13 Sep 2023 12:14:04 -0400 Subject: [PATCH 18/18] Update CHANGELOG.md --- Documentation/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index e1c009e65..65cc2fcbc 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -15,6 +15,7 @@ Release Date: TBD ### Major Changes - 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) +- Adds `HARK.core.Parameters` class to represent a collection of time varying and time invariant parameters in a model. [#1240](https://github.com/econ-ark/HARK/pull/1240) ### Minor Changes @@ -23,6 +24,7 @@ Release Date: TBD - Fixes bug in the metric that compares dictionaries with the same keys. [#1260](https://github.com/econ-ark/HARK/pull/1260) - Fixes bug in the calc_jacobian method. [#1342](https://github.com/econ-ark/HARK/pull/1342) - Fixes bug that prevented risky-asset consumer types from working with time-varying interest rates `Rfree`. [1343](https://github.com/econ-ark/HARK/pull/1343) + ### 0.13.0 Release Date: February, 16, 2023