A Python solver for DSGE models with occasionally binding constraints, using the Parameterised Expectations Algorithm on Tasmanian sparse grids. Solves a calibrated DMP search model in under 60 seconds on a laptop, with Euler equation errors below 0.01%.
import numpy as np
from sparsepea.models import rbc_jit
from sparsepea.tools import tools
model = rbc_jit()
solver = tools(model=model, depth=8)
grid, grid_points = solver.make_states_grid()
e_init = np.ones((grid_points.shape[0], 1)) * 0.5
e_solution, policy, multiplier, status = solver.compute_solution(e_init)
print(status) # "Converged after 142 iterations"
solver.plot_policy_3d(policy)Two problems make DSGE models hard to solve numerically:
-
The curse of dimensionality. Tensor-product grids grow exponentially in the number of state variables. A 2D problem with 100 points per dimension needs 10,000 grid points; a 3D problem needs 1,000,000. Sparse grids (Smolyak, 1963) reduce this to polynomial growth — roughly 100 points in 2D and 500 in 3D — by keeping only the grid points that matter most for interpolation accuracy.
-
Occasionally binding constraints. Perturbation methods linearise around a steady state, which fails at the kink where a constraint starts binding. The PEA is a global projection method that checks complementarity conditions pointwise, so it handles the binding and non-binding regions simultaneously without smoothing or penalty functions.
SparsePEA combines both, using Tasmanian for sparse grid construction and quadrature, and Numba @jitclass for JIT-compiled inner loops.
SparsePEA depends on the Tasmanian sparse grid library from Oak Ridge National Laboratory:
# macOS
brew install tasmanian
# Linux / conda
conda install -c conda-forge tasmanianThen install the package:
git clone https://github.com/Simon-Mishricky/sparsepea.git
cd sparsepea
pip install -e .rbc_jit — Real Business Cycle with irreversible investment. A social planner maximises lifetime CRRA utility subject to a Cobb–Douglas production function and the constraint that gross investment cannot be negative. TFP follows a log-AR(1). The irreversibility constraint is enforced via KKT complementarity conditions. Parameters: capital share α, depreciation δ, risk aversion η, TFP persistence ρ.
dmp_jit — Diamond–Mortensen–Pissarides search and matching with the Hagedorn–Manovskii (2008) calibration. Firms post vacancies, workers search, and a CES matching function governs meetings. Wages are Nash-bargained. The flow value of unemployment is set close to productivity, generating the large labour-market fluctuations that standard calibrations miss (the Shimer puzzle). The zero-vacancy constraint binds in bad states and is enforced via KKT conditions. Parameters: bargaining power η, separation rate s, matching elasticity ι, vacancy cost κ.
The PEA parameterises the conditional expectation in the Euler equation:
and iterates on a fixed-point mapping: given a guess
Sparse grids enter in two places: the state-space grid on which
The solver iterates until the sup-norm distance between successive
SparsePEA reports solution quality via Euler equation residuals — the percentage error in the Euler equation at a fine regular grid over the state space. If the solution were exact, the residual would be zero everywhere. Residuals below
solver.plot_euler_residuals_3d(e_init) # 3D surface of residuals
solver.plot_euler_residuals_distribution() # histogram| Notebook | Description |
|---|---|
tutorial.ipynb |
Solves both models from scratch with full mathematical exposition |
petrosky_nadeau_zhang_replication.ipynb |
Replicates Panel D of Table 1 from Petrosky-Nadeau & Zhang (2017, QE) |
| Model | Grid depth | Solve time | Median Euler error |
|---|---|---|---|
| RBC | 8 | 10–30 s | < 0.01% |
| DMP | 8 | 30–60 s | < 0.01% |
Benchmarked on an Apple M-series laptop.
sparsepea/
├── sparsepea/
│ ├── __init__.py # Public API
│ ├── models.py # JIT-compiled model specifications
│ └── tools.py # Solver, interpolation, diagnostics
├── tests/
│ └── test_models.py
├── images/ # Plots used in this README
├── tutorial.ipynb # Package walkthrough
├── petrosky_nadeau_zhang_replication.ipynb
├── setup.py
├── requirements.txt
└── LICENSE # MIT
Write a @jitclass that implements four methods:
| Method | Purpose |
|---|---|
x_axis_grid(e, grid) |
Map expectations → next-period endogenous state, policy, and KKT multiplier |
rhs_euler(grid, x_p, z_p, psi_p, e, w) |
Evaluate the RHS of the Euler equation via quadrature |
c_implied(e_fine, mu_fine, grid) |
Implied policy for Euler residual diagnostics |
ar1_conditional_density(y, z) |
Transition density for the exogenous shock process |
The solver handles grid construction, interpolation, iteration, and plotting. See models.py for working examples.
- Judd, K., Maliar, L. and Maliar, S. (2011). Numerically stable and accurate stochastic simulation approaches for solving dynamic economic models. Quantitative Economics, 2(2), 173–210.
- Maliar, L. and Maliar, S. (2015). Merging simulation and projection approaches to solve high-dimensional problems with an application to a new Keynesian model. Quantitative Economics, 6(1), 1–47.
- Petrosky-Nadeau, N. and Zhang, L. (2017). Solving the Diamond–Mortensen–Pissarides model accurately. Quantitative Economics, 8(2), 611–650.
- Hagedorn, M. and Manovskii, I. (2008). The cyclical behavior of equilibrium unemployment and vacancies revisited. American Economic Review, 98(4), 1692–1706.
@software{mishricky2025sparsepea,
author = {Mishricky, Simon},
title = {{SparsePEA}: Sparse Grid Parameterised Expectations Algorithm},
year = {2025},
url = {https://github.com/Simon-Mishricky/sparsepea}
}MIT — see LICENSE for details.