🇬🇧 English · 🇪🇸 Español
Higher-Order Perturbation, Constraints & Nonlinear DSGE Methods
Puremacro 2.8.0 introduces Tier 2: Higher Order & Constraints, expanding the DSGE engine beyond first- and second-order local approximations into global non-linear transitions, state pruning, occasionally binding inequality constraints, sequential Monte Carlo sampling, and Ramsey optimal commitment policy.
Every algorithm in this release is implemented in 100% pure Python under the strict Pyodide four-package contract (numpy, scipy, pandas, matplotlib), requiring zero C-extensions, zero external parser generators, and zero heavy symbolic algebra dependencies (no SymPy, no JAX).
1. Overview & Architecture
Linearized DSGE models (e.g. solved via Klein QZ decomposition) provide first-order approximations around a deterministic steady state. While computationally fast, linear solutions: 1. Disregard precautionary behavior and stochastic risk premia (\(g_{\sigma\sigma} = 0\)). 2. Suffer from certainty equivalence: macroeconomic volatility does not shift policy decision rules or endogenous steady states. 3. Cannot represent asymmetric macroeconomic bounds such as the Zero Lower Bound (ZLB) on nominal interest rates or collateral borrowing constraints. 4. Fail across permanent regime shifts where the economy transitions between fundamentally distinct initial and terminal steady states.
Tier 2 provides a unified suite of methods to overcome these limitations:
| Module | Core Capability | Key Method / Algorithm | Benchmark Reference |
|---|---|---|---|
puremacro.dsge.pruning |
Order-3 Perturbation & Ergodic Pruning | 3-fold Schur Sylvester solver & 3-state expansion | Andreasen, Fernández-Villaverde & Rubio-Ramírez (2018) |
puremacro.dsge.perfect_foresight |
Deterministic Transitions & MCP | Semismooth Newton with Fischer-Burmeister complementarity | Boucekkine (1995); Juillard (1996) |
puremacro.dsge.occbin |
Multi-Constraint Piecewise Linear | Dual-regime relaxation & Piecewise Kalman Filter | Guerrieri & Iacoviello (2015); Giovannini et al. (2021) |
puremacro.dsge.smc |
Sequential Monte Carlo & Particle Filtering | Adaptive tempering SMC & Nonlinear bootstrap particle filter | Herbst & Schorfheide (2014, 2015) |
puremacro.dsge.ramsey |
Ramsey Optimal Policy (linear-quadratic) & BGP | LQ commitment from linearized constraints and the objective Hessian, Klein QZ; symbolic FOCs for display | Dennis (2007); Clarida, Galí & Gertler (1999) |
2. 3rd-Order Perturbation & Andreasen Pruning
2.1 Theoretical Foundations & The Curse of Dimensionality
A dynamic stochastic general equilibrium model is characterized by the system of expectational equations:
where \(y_t\) is an \(n_y\)-dimensional vector of endogenous variables and \(u_t \sim \mathcal{N}(0, \Sigma)\) is an \(n_u\)-dimensional vector of exogenous innovations scaled by perturbation parameter \(\sigma\).
Partitioning \(y_t\) into predetermined states \(x_t\) and non-predetermined controls \(w_t\), the exact policy function takes the form:
Expanding \(g\) up to third order via Taylor perturbation yields:
Evaluating the dynamic third tensor derivative:
over \(z = (y_{t+1}, y_t, y_{t-1}, u_t)\) requires evaluating \(O((3n_y + n_u)^3)\) elements. Puremacro's symbolic differentiation engine (puremacro.dsge._symbolic) avoids dense cubic tensor allocation by:
- Enforcing exact 6-fold permutation symmetry (\(j \le k \le l\)).
- Common Subexpression Elimination (CSE) across partial derivatives.
- Direct vectorization into compiled Python callables.
2.2 Generalized Schur Sylvester Solver for 3rd-Order Tensors
The third-order state curvature tensor \(g_{xxx}\) satisfies the generalized matrix Sylvester equation:
where \(\hat{A} = A_0 + A_+ g_x P_s\) and \(h_x = P_s g_x\) is the stable state transition companion matrix.
Assembling the dense Kronecker matrix \((h_x \otimes h_x \otimes h_x)\) of dimension \(n_x^3 \times n_x^3\) would require hundreds of gigabytes of RAM for standard medium-scale DSGE models (such as Smets-Wouters 2007 with \(n_x = 14\), where \(14^3 = 2,744\), creating an \(11 \times 10^7\) element system).
Puremacro implements the decoupled 3-Fold Generalized Schur Sylvester Solver (puremacro.dsge._sylvester):
1. Computes the complex Schur decomposition \(h_x = U_h T_h U_h^H\) with upper-triangular factor \(T_h\).
2. Transforms \(K_{xxx}\) into Schur coordinates: \(\tilde{K} = K_{xxx} (U_h \otimes U_h \otimes U_h)\).
3. Solves vector-by-vector via triangular back-substitution with LU caching:
- Rotates the solution back: \(g_{xxx} = \tilde{g}_{xxx} (U_h^H \otimes U_h^H \otimes U_h^H)\).
On the benchmark Smets-Wouters (2007) model, this solver executes in 0.034 seconds with < 1 MB memory overhead, completely bypassing dense tensor assembly.
2.3 The Andreasen et al. (2018) Pruning Algorithm
Unpruned polynomial approximations of order \(k \ge 2\) generate spurious explosive paths in simulations: large shocks propel states into nonlinear domains where quadratic and cubic terms overpower stable linear feedback roots.
Following Andreasen, Fernández-Villaverde, and Rubio-Ramírez (2018, Review of Economic Studies), puremacro decomposes the state deviation \(\hat{x}_t = x_t - x_{ss}\) into first-, second-, and third-order components:
whose recursive state space evolves without higher-order feedback loops:
Control variables \(y_t\) are evaluated analogously:
where \(y_t^{(2)}\) and \(y_t^{(3)}\) are the right-hand sides of the \(x_t^{(2)}\) and \(x_t^{(3)}\) recursions with every \(h\) replaced by the matching \(g\). Both depend on the lagged state components and the current shock.
Two properties follow:
- Stability: the pruned system is stable whenever the first-order transition \(h_x\) is, so simulations do not explode.
- Exact moments: the augmented state \(z_t = [x_t^{(1)};\, x_t^{(2)};\, x_t^{(1)} \otimes x_t^{(1)};\, x_t^{(3)};\, x_t^{(1)} \otimes x_t^{(2)};\, x_t^{(1)} \otimes x_t^{(1)} \otimes x_t^{(1)}]\) follows a linear law of motion driven by innovations that are uncorrelated over time. theoretical_moments() therefore returns the exact mean, covariance and autocorrelations of every variable under Gaussian shocks. On the five models in tests/fixtures/dynare_live, its mean and covariance equal Dynare 8's "theoretical moments based on pruned state space" to machine precision.
The autocorrelations differ from Dynare's. Dynare's order-3 recursion drops the correlation between the innovation \(x_{t-1}^{(1)} \otimes u_t \otimes u_t\) and past shocks. Long simulations, Dynare's own included, agree with puremacro. For y in correlated_cubic.mod the lag-1 autocorrelation is 0.475, against 0.330 in Dynare's table. The augmented state has \(3n + 2n^2 + n^3\) entries for \(n\) predetermined states. The default max_state_dim=3000 admits up to 13 states, and stoch_simul(order=3) skips the table with a warning above that.
- Shape moments: skewness and kurtosis have no closed form here. theoretical_moments() reports them as NaN. ergodic_moments() keeps the exact mean and variance and estimates both from a pruned simulation (periods=100_000 by default). Its Monte Carlo error is large for very persistent variables.
- Second order: PrunedDSGESolution.theoretical_moments(pruning=True) (and stoch_simul(order=2, pruning=True)) gives the exact moments of the pruned second-order solution, which reproduce Dynare 8's stoch_simul(order=2, pruning) to 7e-14, autocorrelations included. The default follows Dynare's stoch_simul(order=2) without pruning: the risk-corrected mean with first-order second moments.
2.4 Ergodic Mean, Risky Steady State and the Risk Term
A higher-order solution gives two different answers to "where does the economy sit under risk?", and they need not have the same sign. Both solution classes (PrunedDSGESolution at order 2, Order3PrunedSolution at order 3) return them as deviations from the deterministic steady state, in the units of the model's variables.
- Ergodic mean,
sol.ergodic_mean(): the unconditional mean of the pruned process, i.e. the long-run average of a simulation.sol.stochastic_steady_state()is an alias kept for backward compatibility; despite its name it returns this mean. With \(\Omega = h_x \Omega h_x' + h_u \sigma^2 \Sigma_u h_u'\) the first-order state covariance, and \(\mathbb{E}[x_{t-1} \otimes u_t] = 0\) in Dynare's timing,
With Gaussian shocks every third-order forcing term has mean zero, so this is also the exact order-3 mean. It equals the Mean column of theoretical_moments() and Dynare 8's oo_.mean with pruning.
- Risky steady state,
sol.risky_steady_state(): the point where the economy settles when agents expect shocks but none is realized (Coeurdacier, Rey and Winant 2011). In the pruned system the first-order state stays at zero without shocks, so
the limit of sol.simulate(shocks=np.zeros(...), burn=0). risky_steady_state(pruned=False) solves instead for the fixed point of the unpruned approximated policy by Newton's method; the two differ by terms of order \(\sigma^4\).
Only \(g_{\sigma\sigma}\) (Dynare's ghs2) moves the risky steady state. It is the precautionary effect of anticipated risk. The other two terms of the mean are the curvature of the policy times the realized dispersion of states and shocks, a Jensen effect that is there even when \(g_{\sigma\sigma} = 0\). sol.risk_decomposition() returns the three terms (risk, state_curvature, shock_curvature) and their sum (ergodic_mean).
In the RBC model of tests/fixtures/dynare_live/rbc.mod (in levels, shock s.d. 0.01), the terms for capital are, in percent of its deterministic steady state:
| Term | Capital \(k\) (% of \(k_{ss}\)) |
|---|---|
| Risk term \(\tfrac{1}{2} h_{\sigma\sigma}\) (= risky steady state) | −0.0045 |
| State curvature \(\tfrac{1}{2} h_{xx} \operatorname{vec}(\Omega)\) | +0.0705 |
| Shock curvature \(\tfrac{1}{2} h_{uu} \operatorname{vec}(\Sigma_u)\) | +0.0125 |
| Ergodic mean | +0.0785 |
Dynare's own ghs2 for \(k\) in this model is negative (−9.76e-5 in tests/fixtures/dynare_live/rbc_order3.npz), so anticipated risk lowers capital. The positive mean comes from curvature and should not be read as precautionary capital. Two closed forms make the distinction exact, and tests/test_fix_pruning_risky_steady_state.py checks both:
- A claim to \(x^2\). With \(x_t = \rho x_{t-1} + e_t\) and \(\operatorname{Var}(e) = s^2\), the price \(y_t = \beta \mathbb{E}_t y_{t+1} + x_t^2\) is exactly \(y = A x^2 + B\) with \(A = 1/(1 - \beta\rho^2)\) and \(B = \beta A s^2 / (1 - \beta)\). The ergodic mean is \(A \operatorname{Var}(x) + B\) and the risky steady state is \(B\).
- Brock and Mirman (1972), in levels. The policy \(k_t = \alpha\beta e^{a_t} k_{t-1}^\alpha\) does not depend on risk, so \(g_{\sigma\sigma} = 0\) and the risky steady state is the deterministic one. Still, \(\mathbb{E}[k] - k_{ss} = k_{ss} \operatorname{Var}(\log k)/2 > 0\) at second order.
from pathlib import Path
from puremacro.dsge import load_mod
sol3 = load_mod(Path("tests/fixtures/dynare_live/rbc.mod").read_text(), order=3)
dec = sol3.risk_decomposition() # deviations from the deterministic steady state
print((100 * dec.div(sol3.steady_state[dec.index], axis=0)).loc[["k", "c", "y"]].round(4))
print(sol3.risky_steady_state()["states"]["k"]) # -0.00129: below k_ss
print(sol3.ergodic_mean()["states"]["k"]) # +0.02224: above k_ss
2.5 Generalized Impulse Response Functions (GIRF) & API
Because higher-order models violate linear superposition, impulse responses depend on the initial state \(x_0\) and the shock sign/magnitude. Puremacro evaluates Generalized Impulse Responses (GIRF) (Koop, Pesaran & Potter 1996):
import numpy as np
from puremacro.dsge import load_mod
# Load nonlinear DSGE model
mod_text = """
var c k y a;
varexo e_a;
parameters beta alpha delta rho sigma_a;
beta = 0.99; alpha = 0.36; delta = 0.025; rho = 0.95; sigma_a = 0.01;
model;
1/c = beta * (1/c(+1)) * (alpha * exp(a(+1)) * k^(alpha - 1) + 1 - delta);
c + k = exp(a) * k(-1)^alpha + (1 - delta) * k(-1);
y = exp(a) * k(-1)^alpha;
a = rho * a(-1) + e_a;
end;
initval;
k = 38.0; c = 2.7; y = 3.6; a = 0;
end;
shocks;
var e_a; stderr 0.01;
end;
"""
model = load_mod(mod_text)
# Solve order-3 perturbation with Andreasen pruning
sol3 = model.solve(order=3, pruning=True)
# 1. Exact moments of the pruned state space (mean, std. dev., autocorrelations)
moments = sol3.theoretical_moments()
print(moments.moments[["Mean", "Std.Dev."]].round(4))
print("Risk adjustment of mean capital:", moments.moments.loc["k", "Mean"] - sol3.steady_state["k"])
# 2. Skewness and kurtosis have no closed form: estimated from a long pruned simulation
shape = sol3.ergodic_moments(periods=50_000, seed=0)
print(shape[["Skewness", "Kurtosis"]].round(3))
# 3. Generalized Impulse Response Functions (GIRF)
girf = sol3.girf(shock="e_a", size=2.0, horizon=40)
girf.plot(title="Order-3 GIRF to a 2-std Dev Productivity Innovation")
# 4. Pruned stochastic simulation: one finite sample of deviations from the steady state
sim = sol3.simulate(periods=20_000, seed=42)
print("Simulated s.d. of output:", sim["y"].std(), "exact:", moments.moments.loc["y", "Std.Dev."])
3. Deterministic Transitions & Mixed Complementarity Problems (MCP)
3.1 Extended Path & Terminal Steady State Shifts
Policy analysis frequently investigates permanent structural shifts, such as: - A permanent rise in the inflation target \(\pi^*\). - Permanent carbon taxation reforms. - Permanent productivity level shifts.
Under permanent shocks, the terminal steady state \(y_{end}\) differs from the initial steady state \(y_{init}\). Puremacro parses histval; ... end; and endval; ... end; blocks to solve exact non-linear transitions over horizon \(T\):
with boundary conditions \(y_0 = y_{init}\) and \(y_{T+1} = y_{end}\).
3.2 Anticipated and Surprise Exogenous Paths
Puremacro differentiates between:
1. Anticipated deterministic shocks (varexo_det): Exogenous variables announced at \(t=0\) whose future path is known with certainty.
2. Surprise shocks (simulate_surprise_shocks): Unanticipated innovations arriving sequentially at date \(t\), triggering rolling replanning across the remaining horizon.
3.3 Semismooth Newton MCP with Fischer-Burmeister Complementarity
Macroeconomic regimes frequently involve inequality constraints, such as the Zero Lower Bound (ZLB) on nominal interest rates:
Expressed as a Mixed Complementarity Problem (MCP):
To avoid combinatorial regime branching, puremacro implements a Semismooth Newton solver using the Fischer-Burmeister complementarity function:
Let \(\mathbf{F}(\mathbf{Y}) = \mathbf{0}\) denote the stacked equations over horizon \(t=1,\dots,T\). For each bounded variable at period \(t\), the equality condition is replaced by \(\Phi(y_{i,t} - \underline{y}_i, \lambda_{i,t}) = 0\).
The generalized Jacobian \(\partial_B \Phi\) is semismooth:
Puremacro assembles the stacked block-tridiagonal Jacobian directly in sparse format, solving the entire non-linear trajectory in \(O(T)\) operations with superlinear local convergence.
from puremacro.dsge import load_mod, solve_perfect_foresight
MOD_ZLB = """
var y pi i;
varexo eps_r;
parameters beta sigma phi_pi phi_y;
beta = 0.99; sigma = 1.0; phi_pi = 1.5; phi_y = 0.5;
model;
y = y(+1) - (1/sigma) * (i - pi(+1));
pi = beta * pi(+1) + 0.1 * y;
i = max(0, phi_pi * pi + phi_y * y + eps_r);
end;
initval; y = 0; pi = 0; i = 0.02; end;
"""
model = load_mod(MOD_ZLB)
# Solve with Semismooth Newton MCP enforcing the Zero Lower Bound
res_mcp = solve_perfect_foresight(
model,
periods=100,
shocks={"eps_r": [-0.05, -0.03, -0.01]},
mcp=True,
mcp_bounds={"i": (0.0, np.inf)},
)
print(f"Converged in {res_mcp.iterations} iterations.")
print("Binding ZLB periods:", res_mcp.binding_periods["i"])
res_mcp.plot(title="NK Deflation Experiment with Active ZLB")
4. Multi-Constraint OccBin & Piecewise Kalman Filter
4.1 Multi-Constraint Piecewise Linear Architecture
OccBin (Guerrieri & Iacoviello 2015) approximates models with occasionally binding constraints by defining piecewise linear regimes: - Reference unconstrained regime: \(A_0 y_t + A_+ \mathbb{E}_t y_{t+1} + A_- y_{t-1} + C_0 = 0\) - Constrained regimes (\(k=1,\dots,K\)): \(A_0^{(k)} y_t + A_+^{(k)} \mathbb{E}_t y_{t+1} + A_-^{(k)} y_{t-1} + C_0^{(k)} = 0\)
Puremacro 2.8.0 extends OccBin to \(K \ge 2\) simultaneous constraints, supporting up to \(2^K\) discrete regimes (such as joint monetary ZLB and collateral borrowing limits).
The algorithm: 1. Initializes a regime sequence guess \((r_1, \dots, r_H)\) across horizon \(H\). 2. Performs backward time-varying Riccati recursions:
- Simulates forward state trajectories and updates shadow multiplier values.
- Checks convergence of the binding sequence.
The solve_multiconstraint_occbin API
The multi-constraint solver predates 3.4.0 — it shipped in 2.8.0 —
and 3.4.0 extends and hardens it rather than introducing it: the regime
splice now carries every row in which a constrained model differs from
the reference, non-convergence is reported instead of implied, and the
pairing rules below are enforced with ValueError.
solve_multiconstraint_occbin(
m_unconstrained: LinearModel,
m_constrained_dict: Mapping[str, LinearModel] | Sequence[LinearModel],
shock_seq: np.ndarray, # (n_shocks,) or (horizon, n_shocks)
constraints: Mapping[str, OccBinConstraint]
| Sequence[OccBinConstraint]
| None = None,
horizon: int = 40,
max_iter: int = 50,
) -> OccBinResult
One model per constraint, paired one-to-one. m_constrained_dict
holds the model in which that constraint, and only that one, binds;
constraints must be a mapping with exactly the model keys, a sequence
in the model order, or a single constraint when there is a single model.
Mismatched keys, a wrong-length sequence, or one constraint offered for
several models each raise ValueError — the solver never guesses a
pairing. Passing constraints=None deduces each constraint from its
model (the pegged variable, \(-c/a\) as the threshold, the operator read
off the key or the sign of the threshold).
The regime sequence is a bitmask. result.regimes[t] is an integer
whose bit \(k\) is set when constraint \(k\) — the \(k\)-th entry of
m_constrained_dict — binds in period \(t\). With two constraints:
0 neither, 1 the first only, 2 the second only, 3 both. That is
why regime counts are written as mask tests rather than equality tests:
import numpy as np
from puremacro.dsge import build_dynare, OccBinConstraint, solve_multiconstraint_occbin
params = {
"beta": 0.99, "sigma": 1.0, "kappa": 0.15, "phi_pi": 1.5, "phi_y": 0.25,
"rho_r": 0.6, "rho_b": 0.5, "rho_g": 0.7, "gamma_y": 0.2, "chi": 0.1,
"r_ss": 0.015, "b_bar": 0.02,
}
variables = ["y", "pi", "r", "b", "g"]
shocks = ["eps_g", "eps_r", "eps_b"]
def ref_eqs(lead, curr, lag, s, p): # reference: no constraint binds
return [
curr.y - lead.y + (curr.r - lead.pi) / p.sigma - curr.g + p.chi * curr.b,
curr.pi - p.beta * lead.pi - p.kappa * curr.y,
curr.r - (p.rho_r * lag.r + (1.0 - p.rho_r)
* (p.phi_pi * curr.pi + p.phi_y * curr.y) + s.eps_r),
curr.b - (p.rho_b * lag.b + p.gamma_y * curr.y + s.eps_b),
curr.g - p.rho_g * lag.g - s.eps_g,
]
def zlb_eqs(lead, curr, lag, s, p): # only the ZLB binds
eqs = ref_eqs(lead, curr, lag, s, p)
eqs[2] = curr.r - (-p.r_ss)
return eqs
def borr_eqs(lead, curr, lag, s, p): # only the borrowing cap binds
eqs = ref_eqs(lead, curr, lag, s, p)
eqs[3] = curr.b - p.b_bar
return eqs
ss = {v: 0.0 for v in variables}
kw = dict(variables=variables, shocks=shocks, params=params, steady_state=ss)
m_ref = build_dynare(ref_eqs, **kw)
m_zlb = build_dynare(zlb_eqs, check_steady_state=False, strict=False, **kw)
m_borr = build_dynare(borr_eqs, check_steady_state=False, strict=False, **kw)
c_zlb = OccBinConstraint(variable="r", threshold=-params["r_ss"], operator="<")
c_borr = OccBinConstraint(variable="b", threshold=params["b_bar"], operator=">")
shock_seq = np.zeros((40, 3))
shock_seq[0, 0] = -0.06 # eps_g: demand contraction
shock_seq[0, 2] = 0.05 # eps_b: credit surge
res = solve_multiconstraint_occbin(
m_unconstrained=m_ref,
m_constrained_dict={"zlb": m_zlb, "borrowing": m_borr},
shock_seq=shock_seq,
constraints={"zlb": c_zlb, "borrowing": c_borr},
horizon=40,
)
regimes = np.asarray(res.regimes)
print(res.converged, res.iterations) # True 3
print(regimes[:12]) # [3 1 1 0 0 0 0 0 0 0 0 0]
print(np.sum((regimes & 1) == 1)) # 3 — ZLB binds, alone or jointly
print(np.sum(regimes == 3)) # 1 — BOTH bind
print([c for c in res.shadow_path.columns if c.endswith("_shadow")])
# # ['r_shadow', 'b_shadow']
result.constraints maps each model key to the constraint actually
used, and result.shadow_path adds one <variable>_shadow column per
constraint (r_shadow, b_shadow above), which is what the
complementary-slackness check reads.
Non-convergence is now reported, not implied. result.converged is
True only when the regime iteration reached a fixed point, the returned
path respects every constraint where that is meaningful for its style,
and every constraint is slack in the final period so the terminal
condition is verified. Otherwise the function returns converged=False
and emits a single UserWarning naming every reason — max_iter
exhausted, a regime cycle damping could not break, a violated bound, a
constraint still binding at \(T\). The path that comes back is then the one
solved under result.regimes: a diagnostic, not a solution.
Other ValueError conditions: a non-positive horizon or max_iter, no
constrained model at all, a constraint naming an unknown variable or
relax_variable, a constrained model identical to the reference, two
constrained models rewriting the same equation row, and an alternative
regime that pegs a variable in a row that does not determine it in the
reference model without a relax_variable (the relax test would be
vacuous).
4.2 The Piecewise Kalman Filter (Giovannini et al. 2021)
Estimating models with occasionally binding constraints historically required particle filtering or nonlinear likelihood inversions.
The Piecewise Kalman Filter (PKF) of Giovannini, Pfeiffer, and Ratto (2021, Journal of Economic Dynamics and Control) solves this problem by embedding the piecewise linear state space directly within forward filtering: 1. Forecast Step: Given filtered state \(\hat{x}_{t-1|t-1}\) and covariance \(P_{t-1|t-1}\), solve OccBin backward recursions under zero expected future shocks to obtain time-varying transition matrices \(T_t(r)\) and constants \(c_t(r)\). 2. Measurement Update: Compute forecast errors \(v_t = y_t - Z \hat{x}_{t|t-1} - d_t\) and update state and covariance estimates via the standard Kalman gain \(K_t\). 3. Likelihood Accumulation: Evaluate the log-likelihood Gaussian density piece-by-piece:
4.3 Bayesian Estimation with PKF
from puremacro.dsge import load_mod, OccBinConstraint
# Define unconstrained and constrained models
m_ref = load_mod("model_unconstrained.mod")
m_zlb = load_mod("model_zlb.mod")
# Define constraint condition: policy rate hits 0 when notional rate <= 0
constraint = OccBinConstraint(
name="zlb",
condition="i_notional <= 0",
constrained_model=m_zlb,
)
# Estimate parameters using the Piecewise Kalman Filter
res_est = m_ref.estimate(
data=data_df,
method="piecewise_kalman",
occbin_regimes=[constraint],
n_draws=2000,
burn_in=500,
)
print(res_est.summary())
5. Sequential Monte Carlo (SMC) & Particle Filtering
5.1 The Herbst & Schorfheide (2014, 2015) SMC Algorithm
Standard Random Walk Metropolis-Hastings (RWMH) algorithms struggle on complex macroeconomic likelihood surfaces characterized by multiple local modes, ridge collinearities, and flat parameter directions.
Puremacro implements the Sequential Monte Carlo (SMC) algorithm of Herbst & Schorfheide (2014, Journal of Applied Econometrics; 2015, Princeton University Press). SMC bridges the prior \(\pi(\theta)\) to the posterior \(p(\theta|Y)\) through a sequence of \(N\) intermediate tempered distributions:
Key features: 1. Adaptive Tempering: Instead of a fixed grid, puremacro dynamically chooses \(\phi_{n+1}\) by solving:
using a 1D bisection solver, ensuring optimal particle diversity across stages. 2. Systematic Resampling: Particles with low weights are resampled when effective sample size falls below the threshold \(ESS < 0.5 N_{part}\). 3. Particle Mutation: Resampled particles mutate through multiple steps of Metropolis-Hastings with empirical covariance matrix \(\Sigma_n = \text{Cov}_\pi(\theta)\) and adaptive scaling factor \(c_n\).
5.2 Exact Marginal Data Density (MDD)
A decisive advantage of SMC over MCMC is the exact computation of the Marginal Data Density (MDD) \(\ln p(Y)\) as a cumulative product of stage normalizing constants:
where \(w_n^{(m)} = [p(Y \mid \theta_{n-1}^{(m)})]^{\phi_n - \phi_{n-1}}\) are the incremental importance weights. Puremacro also reports the asymptotic numerical standard error \(\text{SE}(\ln \hat{p}(Y))\).
5.3 Nonlinear Bootstrap Particle Filter
For nonlinear DSGE models (orders 2 and 3) or non-Gaussian measurement densities, puremacro provides a native pure-NumPy Bootstrap Particle Filter: - Propagates particles through the pruned nonlinear state equations. - Reweights particles against empirical observation densities. - Evaluates the exact nonlinear log-likelihood with systematic resampling.
from puremacro.dsge import load_mod
from puremacro.dsge.smc import SMCSampler
model = load_mod("smets_wouters_07.mod")
# Configure SMC Sampler
smc = SMCSampler(
model,
data=macro_data,
varobs=["dy", "dc", "dinve", "dw", "pinf", "robs", "labobs"],
n_particles=2000,
n_stages=50,
adaptive_tempering=True,
target_ess=0.5,
seed=123,
)
# Sample posterior
res_smc = smc.sample()
print(f"Log Marginal Data Density (MDD): {res_smc.mdd:.3f} +/- {res_smc.mdd_se:.3f}")
print(res_smc.posterior_summary)
# Visual stage diagnostics
res_smc.plot_stages()
res_smc.plot_posterior()
6. Nonlinear Ramsey Optimal Policy & BGP Detrending
6.1 Automated Ramsey Optimal Policy
Under commitment, a benevolent policymaker maximizes intertemporal household welfare subject to the decentralized structural equilibrium conditions of the economy:
Puremacro's ramsey_model (puremacro.dsge.ramsey) sets this problem up from an objective string and solves its linear-quadratic approximation:
1. Forms the planner's Lagrangian:
- Derives the First-Order Conditions (FOCs) symbolically over the expression DAG, with respect to every endogenous variable \(y_{i,t}\) and Lagrange multiplier \(\lambda_{j,t}\). They are for display only:
focsholds them as readable equations in Dynare syntax andfoc_nodesas expression trees.
- Assembles the system it actually solves numerically, a linear-quadratic one: the constraints linearized at the steady state (the model's linear matrices, or the
.modequations differentiated at the steady state) and the Hessian of the objective at the steady state, with the steady-state multipliers set to zero. The state vector is augmented with the multipliers \(\lambda_t\). - Solves the saddle-path equilibrium with Klein QZ. The law of motion is the same for the Ramsey plan chosen at \(t_0\) and for the timeless-perspective rule (Woodford 2003); impulse responses start from the steady state with \(\lambda_{-1} = 0\), where the two coincide (see DSGE Frontier, §1.3).
The solution is exact when the objective is a quadratic loss centred on the steady state, as in the example below. For a nonlinear welfare objective whose steady-state multipliers are not zero, it is only a linear-quadratic approximation, without the second-order terms of a full Ramsey linearization. On a linear model, ramsey_model shares the matrices and the QZ solver with lq_commitment.
The model below has two equations for three variables: the interest rate i is the instrument and has no rule. load_mod needs as many equations as variables, so the .mod text goes to ramsey_model directly, with the instrument named.
from puremacro.dsge import ramsey_model
# Canonical 3-equation New Keynesian model without a policy rule: the planner's
# constraints are the IS curve and the Phillips curve, and i is the instrument.
NK_MOD = """
var x pi i;
varexo eps_u;
parameters beta sigma kappa;
beta = 0.99; sigma = 1.0; kappa = 0.15;
model;
x = x(+1) - (1/sigma) * (i - pi(+1));
pi = beta * pi(+1) + kappa * x + eps_u;
end;
"""
# Optimal commitment policy minimizing the quadratic loss L = pi^2 + 0.1 * x^2
ramsey_res = ramsey_model(
NK_MOD,
objective="pi^2 + 0.1 * x^2",
planner_discount=0.99,
instruments=["i"],
)
print("Ramsey FOCs (for display):")
for eq in ramsey_res.focs:
print(" ", eq)
# Responses to a cost-push shock satisfy the commitment target criterion
# pi_t = -(0.1 / kappa) * (x_t - x_{t-1}): pi_0 = 0.6274, x_0 = -0.9411
print(ramsey_res.irf("eps_u", horizon=4).round(4))
# Impulse responses of the endogenous variables and the multipliers
fig = ramsey_res.plot(shock="eps_u", horizon=20)
6.2 Balanced Growth Path (BGP) Detrending
Empirical macro models often feature non-stationary deterministic trends (e.g. labor-augmenting technological growth \(A_t = \gamma^t A_0\) or investment-specific technical progress).
Puremacro provides automated Balanced Growth Path (BGP) stationarization:
- Declare trend variables: trend_var A; or log_trend_var a;
- Assign deflators to trending variables: var(deflator=A) Y C I;
- Puremacro automatically generates the stationary transformed system \(y_t^* = Y_t / A_t\) and derives the steady state along the balanced growth path.
7. Summary Comparison Matrix
| Capability | Module | Input Class | Output Class | Key Analytical Method |
|---|---|---|---|---|
| Order-3 Perturbation | puremacro.dsge.pruning |
LinearModel / .mod |
Order3PrunedSolution |
3-Fold Schur Sylvester & Andreasen Pruning |
| Deterministic Transitions | puremacro.dsge.perfect_foresight |
LinearModel / .mod |
PerfectForesightResult |
Newton-Raphson sparse block-tridiagonal solve |
| Complementarity MCP | puremacro.dsge.perfect_foresight |
LinearModel / .mod |
MCPResult |
Semismooth Newton & Fischer-Burmeister |
| Multi-Constraint OccBin | puremacro.dsge.occbin |
Dict of LinearModel |
OccBinResult |
Guerrieri-Iacoviello Riccati iterations |
| Piecewise Kalman Filter | puremacro.dsge.estimate |
LinearModel + Dict |
PiecewiseKalmanResult |
Giovannini-Pfeiffer-Ratto likelihood |
| Sequential Monte Carlo | puremacro.dsge.smc |
LinearModel + Data |
SMCResult |
Herbst-Schorfheide adaptive tempering |
| Particle Filter | puremacro.dsge.smc |
Order3PrunedSolution |
tuple[float, np.ndarray] |
Bootstrap sequential importance resampling |
| Ramsey Optimal Policy | puremacro.dsge.ramsey |
LinearModel / .mod + Obj |
RamseyResult |
LQ commitment (linearized constraints + objective Hessian) + Klein QZ; symbolic FOCs for display |
| BGP Detrending | puremacro.dsge._parser |
.mod with deflator |
ParsedModelDAG |
Automated algebraic stationarization |
8. References
- Andreasen, M. M., Fernández-Villaverde, J., & Rubio-Ramírez, J. F. (2018). The Pruned State-Space System for Non-Linear DSGE Models: Theory and Empirical Applications. Review of Economic Studies, 85(1), 1–49.
- Auclert, A., Bardóczy, B., Rognlie, M., & Straub, L. (2021). Using the Sequence-Space Jacobian to Solve and Estimate Heterogeneous-Agent Models. Econometrica, 89(6), 2787–2815.
- Boucekkine, R. (1995). An Alternative Methodology for Solving Nonlinear Forward-Looking Models. Journal of Economic Dynamics and Control, 19(4), 711–734.
- Brock, W. A., & Mirman, L. J. (1972). Optimal Economic Growth and Uncertainty: The Discounted Case. Journal of Economic Theory, 4(3), 479–513.
- Clarida, R., Galí, J., & Gertler, M. (1999). The Science of Monetary Policy: A New Keynesian Perspective. Journal of Economic Literature, 37(4), 1661–1707.
- Coeurdacier, N., Rey, H., & Winant, P. (2011). The Risky Steady State. American Economic Review, 101(3), 398–401.
- Dennis, R. (2007). Optimal Policy in Rational Expectations Models: New and Alternative Solutions. Journal of Economic Dynamics and Control, 31(12), 3959–3983.
- Giovannini, M., Pfeiffer, P., & Ratto, M. (2021). The Piecewise Kalman Filter for Occasionally Binding Constraints. Journal of Economic Dynamics and Control, 128, 104128.
- Guerrieri, L., & Iacoviello, M. (2015). OccBin: A Toolkit for Solving Dynamic Models with Occasionally Binding Constraints Easily. Journal of Monetary Economics, 75, 38–54.
- Herbst, E., & Schorfheide, F. (2014). Sequential Monte Carlo Sampling for DSGE Models. Journal of Applied Econometrics, 29(7), 1073–1098.
- Herbst, E., & Schorfheide, F. (2015). Bayesian Estimation of DSGE Models. Princeton University Press.
- Juillard, M. (1996). Dynare: A Program for the Resolution and Simulation of Dynamic Models with Forward-Looking Variables Through the Use of a Relaxation Algorithm. CEPREMAP Working Paper 9602.
- Klein, P. (2000). Using the Generalized Schur Form to Solve a System of Linear Expectational Difference Equations. Journal of Economic Dynamics and Control, 24(10), 1405–1423.
- Woodford, M. (2003). Interest and Prices: Foundations of a Theory of Monetary Policy. Princeton University Press.