Deterministic & Perfect Foresight Solvers¶
When analyzing non-linear transition dynamics, anticipated policy reforms, or deterministic shocks over a finite time horizon, first-order perturbation is insufficient. Dyno implements a high-performance stacked-time Newton-Raphson solver with exact automatic differentiation and sparse block-tridiagonal matrix factorization.
Overview¶
The deterministic_solve method solves the non-linear perfect foresight system:
from dyno import DynoModel
from dyno.solver import deterministic_solve
model = DynoModel("examples/ramst.dyno")
trajectory = deterministic_solve(model, T=50)
Parameters:
model: ADynoModelinstance containing equations, calibration, and boundary values.x0(optional): Initial guess matrix for the solution path across all periods. Defaults to steady state.T(optional): Time horizon (number of periods). Inferred from model context if omitted.verbose(bool): Print iteration progress and residual norms.
Returns:
- A
pandas.DataFramewhere rows correspond to time periods \(t = 0, 1, \dots, T\) and columns represent model variables.
Mathematical Formulation¶
Variables and Stacking¶
Let:
- \(p\) be the total number of variables (endogenous + exogenous).
- \(q\) be the number of dynamic equations (\(q \le p\)).
- \(T\) be the simulation horizon.
- \(v_t \in \mathbb{R}^p\) be the vector of all variables at time \(t\).
- \(V = (v_0, v_1, \ldots, v_T) \in \mathbb{R}^{(T+1) \times p}\) be the stacked trajectory matrix.
The model equations are expressed in zero-form:
Boundary Conditions¶
To close the \((T+1) \times p\) dimensional system, three sets of boundary conditions are applied:
-
Initial Condition (\(t = 0\)): State variables at date 0 are pinned to predetermined historical or policy values: $\(v_0 = \bar{v}_0\)$
-
Exogenous Path Pinning (\(t = 0, \ldots, T\)): The \(p - q\) exogenous variables (e.g., tax rates, technology paths, government spending) are pinned to their deterministic profiles: $\(v_t^{\text{exo}} = \bar{v}_t^{\text{exo}}\)$
-
Terminal Condition (\(t = T\)): At the horizon \(T\), forward-looking variables are assumed to have converged to a stationary steady state. Dyno collapses forward indices: $\(f(v_T, v_T, v_T) = 0\)$
The Complete Stacked System¶
The full non-linear system \(F(V) = 0\) is defined as:
Sparse Block-Tridiagonal Jacobian Structure¶
Because equations at time \(t\) depend only on \(v_{t-1}\), \(v_t\), and \(v_{t+1}\), the global Jacobian \(J = \frac{\partial F}{\partial V}\) exhibits a sparse block-tridiagonal structure:
where:
- \(D_t^{(-1)} = \frac{\partial F_t}{\partial v_{t-1}}\) (derivatives with respect to lags)
- \(D_t^{(0)} = \frac{\partial F_t}{\partial v_t}\) (derivatives with respect to current variables)
- \(D_t^{(1)} = \frac{\partial F_t}{\partial v_{t+1}}\) (derivatives with respect to leads)
Computational Efficiency¶
- Automatic Differentiation: Dyno uses a forward-mode automatic differentiation engine (
DNumber) to evaluate exact machine-precision Jacobians without symbolic explosion. - Sparse CSR Format: Storing \(J\) in SciPy's Compressed Sparse Row (
csr_matrix) format reduces memory footprint from \(\mathcal{O}(T^2 p^2)\) to \(\mathcal{O}(T p^2)\). - Sparse Direct Factorization: Linear Newton steps \(J \Delta V = -F(V)\) are solved in \(\mathcal{O}(T)\) operations using sparse LU factorization (
scipy.sparse.linalg.spsolve).
Solution Procedure¶
graph TD
A[Initial Guess V_0 from Steady State] --> B[Evaluate Residuals F V_k and Jacobian J V_k]
B --> C{Check Convergence ||F|| < tol?}
C -->|Yes| D[Return Solution Trajectory DataFrame]
C -->|No| E[Solve Sparse Linear System J ΔV = -F]
E --> F[Update Trajectory: V_k+1 = V_k + ΔV]
F --> B
Practical Example: A Permanent Shock¶
Consider a deterministic Ramsey model with an anticipated technology shock (examples/ramst.dyno):
@name: Ramsey_Deterministic
alph <- 0.5
gam <- 0.5
delt <- 0.02
bet <- 0.05
aa <- 0.5
T <- 50
# Dynamic Equations
0 = c[t] + k[t] - aa*x[t]*k[t-1]^alph - (1-delt)*k[t-1]
0 = c[t]^(-gam) - (1+bet)^(-1)*(aa*alph*x[t+1]*k[t]^(alph-1) + 1 - delt)*c[t+1]^(-gam)
# Steady state
x[~] <- 1
k[~] <- ((delt+bet)/(1.0*aa*alph))^(1/(alph-1))
c[~] <- aa*k[~]^alph-delt*k[~]
# Anticipated technology shock in period 1
x[1] <- 1.2
Simulate and inspect the transition trajectory:
from dyno import DynoModel
model = DynoModel("examples/ramst.dyno")
# Deterministic models are simulated directly via model.simulate()
trajectory = model.simulate(T=100)
print(trajectory[["k", "c"]].head(10))
# Plot transition
trajectory[["k", "c"]].plot(title="Deterministic Transition Path")
Deterministic Models and model.solve()
Because deterministic models under perfect foresight do not have an invariant state-space decision rule, model.solve() is reserved for stochastic models. Use model.simulate() to compute the stacked-time transition trajectory.