Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Transition matrices (general, life-cycle) #1286

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6c87cf5
Shock distribution maker
Mv77 Jun 15, 2023
23f46bd
Make args work
Mv77 Jun 15, 2023
a3a74a6
Create transition matrix method sketch
Mv77 Jun 15, 2023
0430b26
Construct transition matrix from conditional distribution
Mv77 Jun 15, 2023
a73f6dc
Support masking states
Mv77 Jun 15, 2023
b8e6fda
Shock engine for consportfolio
Mv77 Jun 15, 2023
9310d25
State grid for consportfolio
Mv77 Jun 15, 2023
88b71cd
Transition equation for ConsPortfolio
Mv77 Jun 15, 2023
ff42bdf
Test for frictionless and calvo agent
Mv77 Jun 15, 2023
c4c5064
Black
Mv77 Jun 15, 2023
11d7059
Add a life-cycle example
Mv77 Jul 5, 2023
bf705bb
Merge branch 'plumbing/labeled-dist-of-fun' into lc-transition
Mv77 Jul 18, 2023
eed2d01
Start working xrrays in
Mv77 Jul 18, 2023
c7a816f
Use xarray
Mv77 Jul 19, 2023
d3539d3
Create conditional policyfuncs
Mv77 Jul 19, 2023
0f1f453
Merge branch 'lc-transition-xarray' into lc-transition
Mv77 Jul 19, 2023
d3238a8
Compare with current methods in ConsIndShock
Mv77 Jul 20, 2023
6b502aa
Implement deaths (but results don't match)
Mv77 Jul 21, 2023
f155fe6
Fix treatment of newborns in infinite horizon models
Mv77 Jul 21, 2023
516f4a0
Add transition matrix class
Mv77 Jul 25, 2023
f2d73ad
Merge branch 'master' into lc-transition
Mv77 Jul 25, 2023
cb583c8
Add a bruteforce LC transition matrix
Mv77 Jul 25, 2023
4678d60
Proof of concept of simulating LC distributions
Mv77 Jul 26, 2023
d4e37de
Fix and test infinite horizon simulator
Mv77 Jul 26, 2023
fea5c2c
Add tool to find the steady state
Mv77 Jul 26, 2023
126e8c5
Use tool for evaluating outcomes of interest as functions
Mv77 Aug 1, 2023
10210f1
Compare with Will in more detail
Mv77 Aug 1, 2023
6aa5f34
Typo
Mv77 Aug 2, 2023
41f6ee2
Merge branch 'master' into lc-transition
Mv77 Aug 15, 2023
9555210
Implement pre- and post-multiplication by transition mats
Mv77 Aug 23, 2023
d96fe71
Fix pre-multiplication bug caught with 3x3 test
Mv77 Aug 23, 2023
497a7f2
Update distribution iterator
Mv77 Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions HARK/ConsumptionSaving/ConsPortfolioModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
MargValueFuncCRRA,
ValueFuncCRRA,
)
from HARK.distribution import (
combine_indep_dstns,
DiscreteDistribution,
DiscreteDistributionLabeled,
)
from HARK.metric import MetricObject
import xarray as xr


# Define a class to represent the single period solution of the portfolio choice problem
Expand Down Expand Up @@ -145,6 +151,20 @@ def __init__(
self.EndOfPrddvds_fxd = EndOfPrddvds_fxd
self.AdjPrb = AdjPrb

def cFunc(self, mNrm, Share, Adjust):
cNrm = xr.full_like(mNrm, np.nan)
cNrm[Adjust] = self.cFuncAdj(mNrm[Adjust])
no_adj = ~Adjust
cNrm[no_adj] = self.cFuncFxd(mNrm[no_adj], Share[no_adj])
return cNrm

def ShareFunc(self, mNrm, Share, Adjust):
ShareNext = xr.full_like(mNrm, np.nan)
ShareNext[Adjust] = self.ShareFuncAdj(mNrm[Adjust])
no_adj = ~Adjust
ShareNext[no_adj] = self.ShareFuncFxd(mNrm[no_adj], Share[no_adj])
return ShareNext


class PortfolioConsumerType(RiskyAssetConsumerType):
"""
Expand Down Expand Up @@ -316,6 +336,114 @@ def get_controls(self):
self.controls["cNrm"] = cNrmNow
self.controls["Share"] = ShareNow

def shock_dstn_engine(self, ShockDstn, AdjustPrb):
"""
Creates a joint labeled distribution of all the shocks in the model
"""

# ShockDstn is created by RiskyAssetConsumerType and it contains, in order:
# PermShk, TranShk, and Risky

# Create a distribution for the Adjust shock.
# TODO: self.AdjustDstn already exists, but it is a FrozenDist type of object
# that does not work with combine_indep_dstns. This should be fixed.
if AdjustPrb < 1.0:
AdjustDstn = DiscreteDistribution(
np.array([1.0 - AdjustPrb, AdjustPrb]), [False, True]
)
else:
AdjustDstn = DiscreteDistribution(np.array([1.0]), [True])

LabeledShkDstn = DiscreteDistributionLabeled.from_unlabeled(
dist=combine_indep_dstns(ShockDstn, AdjustDstn),
name="Full shock distribution",
var_names=["PermShk", "TranShk", "Risky", "Adjust"],
)

return LabeledShkDstn

def make_state_grid(
self,
PLvlGrid=None,
mNrmGrid=None,
ShareGrid=None,
AdjustGrid=None,
):
if PLvlGrid is None:
PLvlGrid = np.array([1.0])
if mNrmGrid is None:
mNrmGrid = np.array([1.0])
if ShareGrid is None:
ShareGrid = np.array([1.0])
if AdjustGrid is None:
AdjustGrid = np.array([True])

# Create a mesh
points = np.meshgrid(PLvlGrid, mNrmGrid, ShareGrid, AdjustGrid, indexing="ij")
points = np.stack([x.flatten() for x in points], axis=0)

mesh = xr.DataArray(
points,
dims=["var", "mesh"],
coords={"var": ["PLvl", "mNrm", "Share", "Adjust"]},
)

self.state_grid = xr.Dataset(
data_vars={
"PLvl": ("mesh", points[0]),
"mNrm": ("mesh", points[1]),
"Share": ("mesh", points[2]),
"Adjust": ("mesh", points[3].astype(bool)),
},
coords={"mesh": np.arange(points.shape[1])},
attrs={
"grids": {
"PLvl": PLvlGrid,
"mNrm": mNrmGrid,
"Share": ShareGrid,
"Adjust": AdjustGrid,
},
"mesh_order": ["PLvl", "mNrm", "Share", "Adjust"],
},
)

def state_to_state_trans(self, shocks_next, solution, state, PermGroFac, Rfree):
# Consumption
cNrm = solution.cFunc(state["mNrm"], state["Share"], state["Adjust"])
# Savings
aNrm = state["mNrm"] - cNrm
# Share
Share_next = solution.ShareFunc(state["mNrm"], state["Share"], state["Adjust"])
# Shock transition
state_next = post_state_transition(
shocks_next,
state["PLvl"],
aNrm,
Share_next,
PermGroFac,
Rfree,
)

return state_next
Comment on lines +410 to +427
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just updated the methods to use xarrays and labeled distributions. They now take more expressive transition equations like these.



def post_state_transition(shocks_next, PLvl, aNrm, Share_next, PermGroFac, Rfree):
PermGroShk = shocks_next["PermShk"] * PermGroFac
PLvl_next = PLvl * PermGroShk
Rport = Rfree + Share_next * (shocks_next["Risky"] - Rfree)
mNrm_next = aNrm * Rport / PermGroShk + shocks_next["TranShk"]

# Augment dimensions if needed
Share_next = Share_next * xr.ones_like(PLvl_next)
Adjust_next = shocks_next["Adjust"] * xr.ones_like(PLvl_next, dtype=bool)

return {
"PLvl": PLvl_next,
"mNrm": mNrm_next,
"Share": Share_next,
"Adjust": Adjust_next,
}


class SequentialPortfolioConsumerType(PortfolioConsumerType):
def __init__(self, verbose=False, quiet=False, **kwds):
Expand Down
68 changes: 68 additions & 0 deletions HARK/ConsumptionSaving/tests/test_ConsPortfolioModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import HARK.ConsumptionSaving.ConsPortfolioModel as cpm
from HARK import make_one_period_oo_solver
from HARK.tests import HARK_PRECISION
from copy import copy


class PortfolioConsumerTypeTestCase(unittest.TestCase):
Expand Down Expand Up @@ -304,3 +305,70 @@ def test_draws(self):
# Adjust
self.assertTrue(np.all(Adjust_draws[t_age == 1] == 0))
self.assertTrue(np.all(Adjust_draws[t_age == 2] == 1))


from HARK.ConsumptionSaving.ConsIndShockModel import init_lifecycle
from HARK.ConsumptionSaving.ConsRiskyAssetModel import risky_asset_parms


class test_transition_mat(unittest.TestCase):
def setUp(self):
pass

def test_LC(self):
# Create an lc agent
lc_pars = copy(init_lifecycle)
lc_pars.update(risky_asset_parms)
agent = cpm.PortfolioConsumerType(**lc_pars)
agent.solve()

# Make shock distribution and grid
agent.make_shock_distributions()
agent.make_state_grid(
PLvlGrid=None,
# Low number of points, else RAM reqs are high
mNrmGrid=np.linspace(0, 10, 5),
ShareGrid=None,
AdjustGrid=None,
)
# Solve
agent.solve()
# Check that it is indeed an LC model
assert len(agent.solution) > 10

# Get transition matrices
agent.find_transition_matrices()
assert len(agent.solution) - 1 == len(agent.trans_mats)

def test_adjust(self):
# Create agent
agent = cpm.PortfolioConsumerType(**cpm.init_portfolio)
agent.make_shock_distributions()
agent.make_state_grid(
PLvlGrid=None,
mNrmGrid=np.linspace(0, 30, 50),
ShareGrid=None,
AdjustGrid=None,
)
agent.solve()
agent.find_transition_matrices()
self.assertTrue(agent.trans_mats[0].size == np.power(50, 2))

def test_calvo(self):
# Create agent that has some chance of not being able to
# adjust
params = copy(cpm.init_portfolio)
params["AdjustPrb"] = 0.5

agent = cpm.PortfolioConsumerType(**params)
agent.make_shock_distributions()
# Share and adjust become states, so we need grids for them
agent.make_state_grid(
PLvlGrid=None,
mNrmGrid=np.linspace(0, 30, 50),
ShareGrid=np.linspace(0, 1, 10),
AdjustGrid=np.array([False, True]),
)
agent.solve()
agent.find_transition_matrices()
self.assertTrue(agent.trans_mats[0].size == np.power(50 * 10 * 2, 2))
117 changes: 117 additions & 0 deletions HARK/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from HARK.parallel import multi_thread_commands, multi_thread_commands_fake
from HARK.utilities import NullFunc, get_arg_names

from HARK.mat_methods import mass_to_grid


class Model:
"""
Expand Down Expand Up @@ -850,6 +852,121 @@ def clear_history(self):
self.history[var_name] = np.empty((self.T_sim, self.AgentCount))
self.history[var_name].fill(np.nan)

def make_shock_distributions(self):
# Calculate number of periods per cycle, defaults to 1 if all variables are time invariant
if len(self.time_vary) > 0:
# name = agent.time_vary[0]
# T = len(eval('agent.' + name))
T = len(self.__dict__[self.time_vary[0]])
else:
T = 1

dstn_dict = {parameter: self.__dict__[parameter] for parameter in self.time_inv}
dstn_dict.update({parameter: None for parameter in self.time_vary})

if hasattr(self.shock_dstn_engine, "dstn_args"):
these_args = self.shock_dstn_engine.dstn_args
else:
these_args = get_arg_names(self.shock_dstn_engine)

these_args = tuple(filter(lambda x: x != "self", these_args))

# Initialize the list of shock distributions for this cycle,
# then iterate on periods
shock_dstns = []

cycles_range = [0] + list(range(T - 1, 0, -1))
for k in range(T - 1, -1, -1) if self.cycles == 1 else cycles_range:
# Update time-varying single period inputs
for name in self.time_vary:
if name in these_args:
dstn_dict[name] = self.__dict__[name][k]

# Make a temporary dictionary for this period
temp_dict = {name: dstn_dict[name] for name in these_args}

# Construct this period's shock distribution one period
# Add it to the solution, and move to the next period
dstn_t = self.shock_dstn_engine(**temp_dict)
shock_dstns.insert(0, dstn_t)

# Save list of shock distributions
self.full_shock_dstns = shock_dstns

def find_transition_matrices(self):
# Calculate number of periods per cycle, defaults to 1 if all variables are time invariant
if len(self.time_vary) > 0:
# name = agent.time_vary[0]
# T = len(eval('agent.' + name))
T = len(self.__dict__[self.time_vary[0]])
else:
T = 1

trans_dict = {
parameter: self.__dict__[parameter] for parameter in self.time_inv
}
trans_dict.update({parameter: None for parameter in self.time_vary})

if hasattr(self.state_to_state_trans, "trans_args"):
these_args = self.state_to_state_trans.trans_args
else:
these_args = get_arg_names(self.state_to_state_trans)

exclude_args = ["self", "shocks_next", "state", "solution"]
these_args = tuple(filter(lambda x: x not in exclude_args, these_args))

# Extract state grid data
grids = [
self.state_grid.attrs["grids"][x].astype(float)
for x in self.state_grid.attrs["mesh_order"]
]
# Find values and indices of non-trivial grids
nt_inds, nt_grids = zip(*[[i, x] for i, x in enumerate(grids) if len(x) > 1])

# Number of points in full grid
mesh_size = self.state_grid.coords["mesh"].size

# Initialize the list transition matrices
trans_mats = []

cycles_range = [0] + list(range(T - 1, 0, -1))
for k in range(T - 1, -1, -1) if self.cycles == 1 else cycles_range:
# Update time-varying single period inputs
for name in self.time_vary:
if name in these_args:
trans_dict[name] = self.__dict__[name][k]

# Make a temporary dictionary for this period
temp_dict = {name: trans_dict[name] for name in these_args}

shock_dstn = self.full_shock_dstns[k]

def trans_wrapper(shocks_next, solution, state_points):
return self.state_to_state_trans(
shocks_next, solution, state_points, **temp_dict
)

state_dstn = shock_dstn.dist_of_func(
trans_wrapper, solution=self.solution[k], state_points=self.state_grid
)

state_points = state_dstn.dataset.to_array().values[nt_inds, :, :]
pmv = state_dstn.probability.values

# Construct transition matrix from the object above
tmat = np.zeros((mesh_size, mesh_size))
for i in range(mesh_size):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Vectorize

tmat[i, :] = mass_to_grid(
points=state_points[:, i, :].T,
mass=pmv,
grids=nt_grids,
)
# Prepend
trans_mats.insert(0, tmat)

# Save matrices
self.trans_mats = trans_mats


def solve_agent(agent, verbose):
"""
Expand Down