Perfect foresight consumption-saving#

The PerfForesightConsumerType class

# Initial imports and notebook setup, click arrow to show

from copy import copy

import matplotlib.pyplot as plt
import numpy as np

from HARK.ConsumptionSaving.ConsIndShockModel import PerfForesightConsumerType
from HARK.utilities import plot_funcs

mystr = lambda number: "{:.4f}".format(number)

The module HARK.ConsumptionSaving.ConsIndShockModel concerns consumption-saving models with idiosyncratic shocks to (non-capital) income. All of the models assume CRRA utility with geometric discounting, no bequest motive, and income shocks that are either fully transitory or fully permanent.

ConsIndShockModel currently includes three models: 1. A very basic “perfect foresight” model with no uncertainty. 2. A model with risk over transitory and permanent income shocks. 3. The model described in (2), with an interest rate for debt that differs from the interest rate for savings.

This notebook provides documentation for the first of these three models. \(\newcommand{\CRRA}{\rho}\) \(\newcommand{\DiePrb}{\mathsf{D}}\) \(\newcommand{\PermGroFac}{\Gamma}\) \(\newcommand{\Rfree}{\mathsf{R}}\) \(\newcommand{\DiscFac}{\beta}\)

Statement of perfect foresight consumption-saving model#

The PerfForesightConsumerType class models the problem of a consumer with Constant Relative Risk Aversion utility specified by \begin{align*} U(C) = \frac{C^{1-\CRRA}}{1-\rho}, \end{align*} who has perfect foresight about everything except whether he will die between the end of period \(t\) and the beginning of period \(t+1\), which occurs with probability \(\DiePrb_{t+1}\). Permanent labor income \(P_t\) grows from period \(t\) to period \(t+1\) by factor \(\PermGroFac_{t+1}\).

At the beginning of period \(t\), the consumer has an amount of market resources \(M_t\) (which includes both market wealth and current income) and must choose how much of those resources to consume \(C_t\), while retaining the rest in a riskless asset \(A_t\), which will earn return factor \(\Rfree\). The consumer cannot necessarily borrow arbitarily; instead, he might be constrained to have a wealth-to-income ratio at least as great as some “artificial borrowing constraint” \(\underline{a} \leq 0\).

The agent’s flow of future utility \(U(C_{t+n})\) from consumption is geometrically discounted by factor \(\DiscFac^n\). If the consumer dies, he receives zero utility flow for the rest of time.

The agent’s problem can be written in Bellman form as:

\begin{align*} V_t(M_t,P_t) &= \max_{C_t}U(C_t) + \DiscFac (1 - \DiePrb_{t+1}) V_{t+1}(M_{t+1},P_{t+1}), \\ & \text{s.t.} \\ A_t &= M_t - C_t, \\ A_t/P_t &\geq \underline{a}, \\ M_{t+1} &= \Rfree A_t + Y_{t+1}, \\ Y_{t+1} &= P_{t+1}, \\ P_{t+1} &= \PermGroFac_{t+1} P_t. \end{align*}

The consumer’s problem is characterized by the coefficient of relative risk aversion \(\CRRA\), the intertemporal discount factor \(\DiscFac\), the interest factor \(\Rfree\), and age-varying sequences of the permanent income growth factor \(\PermGroFac_t\) and survival probability \((1 - \DiePrb_t)\).

While it does not reduce the computational complexity of the problem (as permanent income is deterministic, given its initial condition \(P_0\)), HARK represents this problem with normalized variables (represented in lower case), dividing all real variables by permanent income \(P_t\) and utility levels by \(P_t^{1-\CRRA}\). The Bellman form of the model thus reduces to:

\begin{align*} v_t(m_t) &= \max_{c_t}u(c_t) + \DiscFac (1 - \DiePrb_{t+1}) \PermGroFac_{t+1}^{1-\CRRA} v_{t+1}(m_{t+1}), \\ & \text{s.t.} \\ a_t &= m_t - c_t, \\ a_t &\geq \underline{a}, \\ m_{t+1} &= \Rfree a_t/\PermGroFac_{t+1} + 1. \end{align*}

Solution method for PerfForesightConsumerType#

Because of the assumptions of CRRA utility and no risk other than mortality, the problem has a closed form solution when there is no artificial borrowing constraint. In fact, the consumption function is perfectly linear, and the value function composed with the inverse utility function is also linear. The mathematical solution of this model is described in detail in the lecture notes PerfForesightCRRA.

The one period problem for this model is solved by the function solveConsPerfForesight, which creates an instance of the class ConsPerfForesightSolver. To construct an instance of the class PerfForesightConsumerType, several parameters must be passed to its constructor as shown in the table below.

Example parameter values to construct an instance of PerfForesightConsumerType#




Example value



Intertemporal discount factor




Coefficient of relative risk aversion




Risk free interest factor



\(1 - \DiePrb_{t+1}\)

Survival probability





Permanent income growth factor





Artificial borrowing constraint




Maximum number of gridpoints in consumption function




Number of periods in this type’s “cycle”




Number of times the “cycle” occurs



Note that the survival probability and income growth factor have time subscripts; likewise, the example values for these parameters are lists rather than simply single floats. This is because those parameters are time-varying: their values can depend on which period of the problem the agent is in. All time-varying parameters must be specified as lists, even if the same value occurs in each period for this type.

The artificial borrowing constraint can be any non-positive float, or it can be None to indicate no artificial borrowing constraint. The maximum number of gridpoints in the consumption function is only relevant if the borrowing constraint is not None; without an upper bound on the number of gridpoints, kinks in the consumption function will propagate indefinitely in an infinite horizon model if there is a borrowing constraint, eventually resulting in an overflow error. If there is no artificial borrowing constraint, then the number of gridpoints used to represent the consumption function is always exactly two.

The last two parameters in the table specify the “nature of time” for this type: the number of (non-terminal) periods in this type’s “cycle”, and the number of times that the “cycle” occurs. Every subclass of AgentType uses these two code parameters to define the nature of time. Here, T_cycle has the value \(1\), indicating that there is exactly one period in the cycle, while cycles is \(0\), indicating that the cycle is repeated in infinite number of times– it is an infinite horizon model, with the same “kind” of period repeated over and over.

In contrast, we could instead specify a life-cycle model by setting T_cycle to \(1\), and specifying age-varying sequences of income growth and survival probability. In all cases, the number of elements in each time-varying parameter should exactly equal T_cycle.

The parameter AgentCount specifies how many consumers there are of this type– how many individuals have these exact parameter values and are ex ante homogeneous. This information is not relevant for solving the model, but is needed in order to simulate a population of agents, introducing ex post heterogeneity through idiosyncratic shocks. Of course, simulating a perfect foresight model is quite boring, as there are no idiosyncratic shocks other than death!

The cell below defines a dictionary that can be passed to the constructor method for PerfForesightConsumerType, with the values from the table here.

PerfForesightDict = {
    # Parameters actually used in the solution method
    "CRRA": 2.0,  # Coefficient of relative risk aversion
    "Rfree": 1.03,  # Interest factor on assets
    "DiscFac": 0.96,  # Default intertemporal discount factor
    "LivPrb": [0.98],  # Survival probability
    "PermGroFac": [1.01],  # Permanent income growth factor
    "BoroCnstArt": None,  # Artificial borrowing constraint
    "aXtraCount": 200,  # Maximum number of gridpoints in consumption function
    # Parameters that characterize the nature of time
    "T_cycle": 1,  # Number of periods in the cycle for this agent type
    "cycles": 0,  # Number of times the cycle occurs (0 --> infinitely repeated)

Solving and examining the solution of the perfect foresight model#

With the dictionary we have just defined, we can create an instance of PerfForesightConsumerType by passing the dictionary to the class (as if the class were a function). This instance can then be solved by invoking its solve method.

PFexample = PerfForesightConsumerType(**PerfForesightDict)
PFexample.cycles = 0

The solve method fills in the instance’s attribute solution as a time-varying list of solutions to each period of the consumer’s problem. In this case, solution will be a list with exactly one instance of the class ConsumerSolution, representing the solution to the infinite horizon model we specified.

[<HARK.ConsumptionSaving.ConsIndShockModel.ConsumerSolution object at 0x000002974189DF10>]

Each element of solution has a few attributes. To see all of them, we can use the vars built in function:

the consumption functions reside in the attribute cFunc of each element of ConsumerType.solution. This method creates a (time varying) attribute cFunc that contains a list of consumption functions.

{'cFunc': <HARK.interpolation.LinearInterp object at 0x00000297418A3F40>, 'vFunc': <HARK.interpolation.ValueFuncCRRA object at 0x00000297418A1F10>, 'vPfunc': <HARK.interpolation.MargValueFuncCRRA object at 0x00000297418A2F40>, 'vPPfunc': <HARK.utilities.NullFunc object at 0x0000029741892FA0>, 'mNrmMin': -50.49994992551661, 'hNrm': 50.49994992551661, 'MPCmin': 0.04428139169919579, 'MPCmax': 0.04428139169919579}

The two most important attributes of a single period solution of this model are the (normalized) consumption function cFunc and the (normalized) value function vFunc. Let’s plot those functions near the lower bound of the permissible state space (the attribute mNrmMin tells us the lower bound of \(m_t\) where the consumption function is defined).

print("Linear perfect foresight consumption function:")
mMin = PFexample.solution[0].mNrmMin
plot_funcs(PFexample.solution[0].cFunc, mMin, mMin + 10.0)
Linear perfect foresight consumption function:
print("Perfect foresight value function:")
plot_funcs(PFexample.solution[0].vFunc, mMin + 0.1, mMin + 10.1)
Perfect foresight value function:

An element of solution also includes the (normalized) marginal value function vPfunc, and the lower and upper bounds of the marginal propensity to consume (MPC) MPCmin and MPCmax. Note that with a linear consumption function, the MPC is constant, so its lower and upper bound are identical.

Liquidity constrained perfect foresight example#

Without an artificial borrowing constraint, a perfect foresight consumer is free to borrow against the PDV of his entire future stream of labor income – his “human wealth” hNrm – and he will consume a constant proportion of his total wealth (market resources plus human wealth). If we introduce an artificial borrowing constraint, both of these features vanish. In the cell below, we define a parameter dictionary that prevents the consumer from borrowing at all, create and solve a new instance of PerfForesightConsumerType with it, and then plot its consumption function.

LiqConstrDict = copy(PerfForesightDict)
LiqConstrDict["BoroCnstArt"] = 0.0  # Set the artificial borrowing constraint to zero

LiqConstrExample = PerfForesightConsumerType(**LiqConstrDict)
LiqConstrExample.cycles = 0  # Make this type be infinite horizon

print("Liquidity constrained perfect foresight consumption function:")
plot_funcs(LiqConstrExample.solution[0].cFunc, 0.0, 10.0)
Liquidity constrained perfect foresight consumption function:

At this time, the value function for a perfect foresight consumer with an artificial borrowing constraint is not computed nor included as part of its solution.

Simulating the perfect foresight consumer model#

Suppose we wanted to simulate many consumers who share the parameter values that we passed to PerfForesightConsumerType – an ex ante homogeneous type of consumers. To do this, our instance would have to know how many agents there are of this type, as well as their initial levels of assets \(a_t\) and permanent income \(P_t\).

Setting simulation parameters#

Let’s fill in this information by passing another dictionary to PFexample with simulation parameters. The table below lists the parameters that an instance of PerfForesightConsumerType needs in order to successfully simulate its model using the simulate method.



Example value

Number of consumers of this type



Number of periods to simulate



Mean of initial log (normalized) assets



Stdev of initial log (normalized) assets



Mean of initial log permanent income



Stdev of initial log permanent income



Aggregrate productivity growth factor



Age after which consumers are automatically killed



We have specified the model so that initial assets and permanent income are both distributed lognormally, with mean and standard deviation of the underlying normal distributions provided by the user.

The parameter PermGroFacAgg exists for compatibility with more advanced models that employ aggregate productivity shocks; it can simply be set to 1.

In infinite horizon models, it might be useful to prevent agents from living extraordinarily long lives through a fortuitous sequence of mortality shocks. We have thus provided the option of setting T_age to specify the maximum number of periods that a consumer can live before they are automatically killed (and replaced with a new consumer with initial state drawn from the specified distributions). This can be turned off by setting it to None.

The cell below puts these parameters into a dictionary, then gives them to PFexample. Note that all of these parameters could have been passed as part of the original dictionary; we omitted them above for simplicity.

SimulationParams = {
    "AgentCount": 10000,  # Number of agents of this type
    "T_sim": 120,  # Number of periods to simulate
    "aNrmInitMean": -6.0,  # Mean of log initial assets
    "aNrmInitStd": 1.0,  # Standard deviation of log initial assets
    "pLvlInitMean": 0.0,  # Mean of log initial permanent income
    "pLvlInitStd": 0.0,  # Standard deviation of log initial permanent income
    "PermGroFacAgg": 1.0,  # Aggregate permanent income growth factor
    "T_age": None,  # Age after which simulated agents are automatically killed


To generate simulated data, we need to specify which variables we want to track the “history” of for this instance. To do so, we set the track_vars attribute of our PerfForesightConsumerType instance to be a list of strings with the simulation variables we want to track.

In this model, valid arguments to track_vars include mNrm, cNrm, aNrm, and pLvl. Because this model has no idiosyncratic shocks, our simulated data will be quite boring.

Generating simulated data#

Before simulating, the initialize_sim method must be invoked. This resets our instance back to its initial state, drawing a set of initial aNrm and pLvl values from the specified distributions and storing them in the attributes aNrmNow_init and pLvlNow_init. It also resets this instance’s internal random number generator, so that the same initial states will be set every time initialize_sim is called. In models with non-trivial shocks, this also ensures that the same sequence of shocks will be generated on every simulation run.

Finally, the simulate method can be called.

PFexample.track_vars = ["mNrm"]
{'mNrm': array([[  1.00095989,   1.00282396,   1.00111932, ...,   1.00185921,
           1.00044451,   1.00448108],
        [ -0.30491013,  -0.30309332,  -0.30475474, ...,  -0.30403362,
          -0.30541244,  -0.30147822],
        [ -1.57766816,  -1.57589742,  -1.57751671, ...,  -1.57681387,
          -1.57815773,  -1.57432327],
        [-21.97118426,  -9.62717581, -36.59971086, ...,   1.00376132,
          -2.81712554, -46.01045686],
        [-22.69456637, -10.66355594,   1.00072865, ...,  -0.30217974,
          -4.02618315, -46.12429267],
        [-23.39960622, -11.67365734,  -0.30513551, ...,  -1.575007  ,
          -5.20458357, -46.23524203]])}

Each simulation variable X named in track_vars will have the history of that variable for each agent stored in the attribute X_hist as an array of shape (T_sim, AgentCount). To see that the simulation worked as intended, we can plot the mean of \(m_t\) in each simulated period:

plt.plot(np.mean(PFexample.history["mNrm"], axis=1))
plt.ylabel("Mean normalized market resources")

A perfect foresight consumer can borrow against the PDV of his future income – his human wealth – and thus as time goes on, our simulated agents approach the (very negative) steady state level of \(m_t\) while being steadily replaced with consumers with roughly \(m_t=1\).

The slight wiggles in the plotted curve are due to consumers randomly dying and being replaced; their replacement will have an initial state drawn from the distributions specified by the user. To see the current distribution of ages, we can look at the attribute t_age.

N = PFexample.AgentCount
F = np.linspace(0.0, 1.0, N)
plt.plot(np.sort(PFexample.t_age), F)
plt.xlabel("Current age of consumers")
plt.ylabel("Cumulative distribution")

The distribution is (discretely) exponential, with a point mass at 120 with consumers who have survived since the beginning of the simulation.

One might wonder why HARK requires users to call initialize_sim before calling simulate: Why doesn’t simulate just call initialize_sim as its first step? We have broken up these two steps so that users can simulate some number of periods, change something in the environment, and then resume the simulation.

When called with no argument, simulate will simulate the model for T_sim periods. The user can optionally pass an integer specifying the number of periods to simulate (which should not exceed T_sim).

In the cell below, we simulate our perfect foresight consumers for 80 periods, then seize a bunch of their assets (dragging their wealth even more negative), then simulate for the remaining 40 periods.

The state_prev attribute of an AgentType stores the values of the model’s state variables in the previous period of the simulation.

] += -5.0  # Adjust all simulated consumers' assets downward by 5

plt.plot(np.mean(PFexample.history["mNrm"], axis=1))
plt.ylabel("Mean normalized market resources")