Skip to content

🇬🇧 English · 🇪🇸 Español

Editable HANK aggregate equations

The .mod HANK bridge keeps its existing built-in New Keynesian closure by default. Set closure="model" to use the parsed aggregate equations instead. This opt-in mode solves homogeneous affine equations in level deviations, together with explicitly mapped household responses. model(linear) makes that intent clear; the parser also recognizes affine model blocks.

import numpy as np
from puremacro.dsge.hank import solve_hank_bridge

source = """
var Y C r pi i;
varexo monetary government;
parameters beta gamma r_ss phi_pi kappa;
beta=.985; gamma=1; r_ss=.01; phi_pi=1.5; kappa=.1;
hetagent_block; model=one_asset_hank; n_a=30; a_max=30; end;
model(linear);
Y=C+government;
pi=beta*pi(+1)+kappa*Y;
i=.3*i(-1)+phi_pi*pi+monetary;
r=i-pi(+1);
end;
"""
T = 12
result = solve_hank_bridge(
    source, horizon=T, closure="model",
    household_inputs={"r": "r", "Y": "Y"},
    household_outputs={"C": "C"},
    shock_paths={"monetary": -.002*.6**np.arange(T),
                 "government": .001*.4**np.arange(T)},
)
assert result.converged
assert result.diagnostics["aggregate_residual_max_abs"] < 1e-9
print(result.transition_paths.head())

Each mapping goes from a household name to a declared aggregate name. Inputs may map to endogenous variables or exogenous shocks. Outputs must map to distinct endogenous variables. There must be exactly one equation per endogenous variable after adding one household-response equation per mapped output. Unsupported names, duplicate targets, nonhomogeneous constants and nonlinear expressions fail explicitly. Unresolved variables are not filled with zero. For example, write i=phi_pi*pi+monetary for deviations, not i=r_ss+phi_pi*pi+monetary.

shock_paths contains finite arrays of exactly horizon observations; omitted declared shocks are zero. Without it, the selected declared shock receives magnitude*rho**arange(horizon). In this mode the shock name determines which equation is affected. All endogenous and exogenous deviations before date 0 and after date T-1 are zero. This is a finite-horizon boundary condition; extend the horizon and compare paths when terminal effects matter.

The solver checks full matrix rank and independently evaluates the original aggregate equations and household identities after solving. Diagnostics report rank, dimension, condition number and both maximum residuals. Full rank means finite-horizon solvability, not Blanchard–Kahn determinacy, uniqueness of an infinite-horizon equilibrium, or household grid accuracy. closure_tol controls the absolute residual acceptance threshold (default 1e-9). No pseudoinverse or regularization is silently substituted. This implementation assembles a dense matrix of size (variables*horizon)**2; start with moderate horizons.

Household units and timing

All inputs and outputs use level deviations, including rates expressed in the same period units as their calibration. They are not percentages or log deviations. Mappings rename coordinates without scaling them.

Household Available inputs Available outputs Convention
Built-in one asset r, Y C r is the ex-ante rate realized next period; Y changes gross wages by w_ss/C_ss, retaining the household's balanced-budget interest tax.
Built-in two assets r_b, r_a, Y C, D, A, B, CHI, z Ex-ante rates; nonfinancial income is closed using output less returns on inherited assets. A and B are saving choices.
Declarative dp.Model or DPSolution Declared supported household inputs Declared aggregate expressions and supported aliases Raw household price timing and units; no additional fiscal equation or wage scaling.

For a declarative household, A="a" means assets carried into the date, while S="a(+1)" means saving chosen that date. Those meanings survive mapping to aggregate variables. Specify wage, fiscal, asset-market and price equations explicitly in the aggregate block. Supplying an already solved DPSolution also fixes which household calibration generates the Jacobians.

The built-in defaults remain useful for comparison. Options that only make sense with editable equations are rejected under closure="builtin".

Nonlinear household equilibrium

With the same mappings, nonlinear=True replaces the linear household identities with exact backward-policy and forward-distribution transitions. The aggregate equations still must be affine and homogeneous in deviations. This supports built-in and declarative one- and two-asset households; it is not an arbitrary nonlinear aggregate equation language.

nonlinear = solve_hank_bridge(
    source, horizon=T, closure="model", nonlinear=True,
    household_inputs={"r": "r", "Y": "Y"},
    household_outputs={"C": "C"},
    shock_paths={"monetary": -.002*.6**np.arange(T)},
    closure_tol=1e-9, closure_max_iter=100,
)
assert nonlinear.converged
assert nonlinear.diagnostics["household_budget_max_abs"] < 1e-8
assert nonlinear.diagnostics["distribution_mass_max_abs"] < 1e-10

The linear solution initializes damped Broyden iterations. An infeasible initial guess is retried at zero endogenous deviations; trial paths must reduce the residual norm and preserve feasible household budgets and distribution mass. Consumption-floor paths are rejected. Initial distributions and terminal household continuation values are their calibrated steady-state values; the ex-ante return convention includes the return realized at the terminal date. Declarative household inputs retain their original realized-date convention.

Diagnostics report iteration count, residual history, final budget and mass errors, and termination reason. Failed iterations warn and return the last path with converged=False; the result is never replaced by a linear answer. An exogenous input that makes even the initial household path infeasible raises ValueError. Successful numerical convergence does not certify equilibrium uniqueness, grid convergence, or an independent Euler audit. The private context retains final aggregate household responses without triggering another solve.

Two-asset household panels

A solved two-asset EGM household supports the regular panel interface:

# sol is a two-asset DPSolution, with declared assets a and b.
panel = sol.simulate(
    n_agents=1000, periods=40, seed=2026, burn_in=10,
    record=["a", "b", "a(+1)", "b(+1)", "c"],
)
microdata = panel.to_microframe(t=20)

Both asset choices are interpolated bilinearly and remain continuous. The recorded consumption and adjustment costs follow the model's budget at those actual choices, rather than a separately interpolated consumption table. Productivity follows the solved Markov chain. The same seed reproduces the panel exactly, including burn-in; reversed asset declaration order is handled explicitly. Fixed initial values may be off-grid, and out-of-grid values are clipped with a warning.

Stationary initialization samples the solved joint grid distribution. Later continuous policies need not preserve the discretized lottery distribution exactly: compare simulated moments with sol.mean(...) and refine grids when that approximation matters. These are stationary-policy panels, not panels along a general-equilibrium transition.