Getting Started#
Key concepts#
Dynare lag notation#
Equations are written using the v(name, lag) helper, which creates a time-indexed SymPy symbol. The lag argument follows Dynare’s convention:
Expression |
Meaning |
|---|---|
|
\(k_{t-1}\) — lagged value (one period ago) |
|
\(k_t\) — current-period value |
|
\(c_{t+1}\) — lead value (one period ahead) |
For example, the standard capital accumulation equation \(k_t = k_{t-1}^\alpha - c_t\) is written:
eq_kacc = v("k", 0) - v("k", -1)**ALPHA + v("c", 0)
Note that k appears at lag -1 — this is Dynare’s convention for a stock variable that accumulates from last period.
initial_state semantics#
initial_state is always the pre-period-0 value of the stock variable(s) — i.e., \(k_{-1}\) in Dynare notation. The period-0 values of all variables, including jump variables such as consumption, are determined simultaneously by the model equations.
Do not confuse initial_state with \(k_0\) (the period-0 value of capital, which is endogenous). If initial_state is omitted it defaults to ss[stock_var_indices], meaning the economy starts at the initial steady state.
Stock variable inference#
A variable is classified as a stock (predetermined) variable if it appears at any negative lag in the model equations. A variable that only appears at lag 0 or positive lags is a jump variable — it is free to respond at \(t=0\) and is not pinned by initial_state.
stock_var_indices is inferred automatically from the lead-lag incidence table computed during process_model. You can always pass it explicitly to override the inference:
sol = solve_perfect_foresight(..., stock_var_indices=[1]) # force k (index 1) as stock
BVP (augmented-path) formulation#
The solver always uses the augmented-path BVP formulation. It builds a T+2-row path:
Row 0 (
initval): pre-period-0 boundary — stock variables frominitial_state, all others fromss_initialRows 1 … T: the
Tfree periods (the unknowns being solved)Row T+1 (
endval): terminal steady statess(or a user-suppliedendval)
Residuals are evaluated at \(t = 0, \ldots, T-1\) using the full augmented path, so all \(T \times n\) unknowns are determined simultaneously. This correctly handles jump variables: pinning X[0] directly would over-constrain them and produce a structurally singular Jacobian.
Minimal RBC example#
Here is a complete two-variable RBC model — Euler equation and capital accumulation — solved with a 10% capital shock.
import numpy as np
from pyperfectforesight import v, process_model, solve_perfect_foresight
# Parameters baked in numerically
ALPHA = 0.36
BETA = 0.99
# Dynare-style equations:
# Euler: 1/c_t = beta * alpha * k_t^(alpha-1) / c_{t+1}
# Capital: k_t = k_{t-1}^alpha - c_t
#
# k appears at lag -1 in the accumulation equation (Dynare convention).
eq_euler = v("c", 0)**(-1) - BETA * ALPHA * v("k", 0)**(ALPHA-1) * v("c", 1)**(-1)
eq_kacc = v("k", 0) - v("k", -1)**ALPHA + v("c", 0)
vars_dyn = ["c", "k"]
model_funcs = process_model([eq_euler, eq_kacc], vars_dyn)
# Steady state
K_SS = (ALPHA * BETA) ** (1 / (1 - ALPHA))
C_SS = K_SS**ALPHA - K_SS
ss = np.array([C_SS, K_SS])
# Transition path: k_{-1} starts 10% above steady state
T = 100
# initial_state = k_{-1} (pre-period-0 capital, Dynare convention)
k_neg1 = np.array([K_SS * 1.1])
sol = solve_perfect_foresight(
T, {}, ss, model_funcs, vars_dyn,
initial_state=k_neg1,
stock_var_indices=[1], # index of k in vars_dyn
)
print(f"Converged: {sol.success}")
# Unpack solution
X = sol.x.reshape(T, -1) # shape (T, 2): columns are [c, k]
c_path = X[:, 0]
k_path = X[:, 1]
RBC model with exogenous TFP shock#
When the model has exogenous variables, pass vars_exo to process_model and supply a T × n_exo array as exog_path:
import sympy as sp
import numpy as np
from pyperfectforesight import v, process_model, solve_perfect_foresight
ALPHA, BETA = 0.36, 0.99
# TFP shock z enters the production function
eq_euler = v("c", 0)**(-1) - BETA * ALPHA * v("k", 0)**(ALPHA-1) * v("c", 1)**(-1)
eq_kacc = v("k", 0) - sp.exp(v("z", 0)) * v("k", -1)**ALPHA + v("c", 0)
vars_dyn = ["c", "k"]
model_funcs = process_model([eq_euler, eq_kacc], vars_dyn, vars_exo=["z"])
K_SS = (ALPHA * BETA) ** (1 / (1 - ALPHA))
C_SS = K_SS**ALPHA - K_SS
ss = np.array([C_SS, K_SS])
T = 100
# AR(1) TFP shock: 1% on impact, rho=0.9 decay
rho = 0.9
exog = np.zeros((T, 1))
exog[0, 0] = 0.01
for t in range(1, T):
exog[t, 0] = rho * exog[t-1, 0]
k_neg1 = np.array([K_SS]) # k_{-1} at steady state
sol = solve_perfect_foresight(
T, {}, ss, model_funcs, vars_dyn,
initial_state=k_neg1,
stock_var_indices=[1],
exog_path=exog,
)
print(f"Converged: {sol.success}")